2014-01-23 3 views
7

Burrows-Wheeler을 Python으로 변환하려고했습니다. (이것은 온라인 과정에서의 과제 중 하나이지만 도움을 요청할 수있는 자격을 갖추기를 희망합니다.)Python의 Burrows-Wheeler에서 성능 문제가 발생했습니다.

알고리즘은 다음과 같이 작동합니다. 특수 문자 (필자의 경우 $)로 끝나는 문자열을 가져 와서이 문자열에서 모든 순환 문자열을 만듭니다. 이러한 모든 문자열을 사전 순으로 정렬하십시오. 특수 문자는 항상 다른 문자보다 작습니다. 이후에 각 문자열의 마지막 요소를 가져옵니다. 내가 처리하려고 할 때

60 000 chars - 16 secs 
40 000 chars - 07 secs 
25 000 chars - 02 secs 

그러나 :

''.join([i[-1] for i in sorted([text[i:] + text[0:i] for i in xrange(len(text))])] 

(문제를 해결하기에 충분하다) 정확하고 합리적으로 빨리 합리적으로 큰 문자열입니다 :

나에게 oneliner했다 몇 백만 개의 문자가 포함 된 정말 거대한 문자열인데 실패했습니다 (처리하는 데 너무 많은 시간이 필요함).

문제는 메모리에 너무 많은 문자열을 저장하는 것으로 가정합니다.

이 문제를 극복 할 방법이 있습니까?

P. 이 문제가 숙제 문제처럼 보일 수도 있다는 것을 지적하고 싶습니다. 내 솔루션은 이미 그레이더를 통과했으며 더 빨리 진행할 방법을 찾고 있습니다. 또한 나는 다른 사람들을위한 재미를 망치고 있지 않다. 왜냐하면 그들이 해결책을 찾고 싶다면, 위키 기사는 나의 것과 비슷한 것을 가지고 있기 때문이다. 나는 비슷하게 들리 겠지만이 알고리즘으로 코딩 된 문자열을 해독하는 방법에 대해 열심히 질문한다. this questio n을 검사했다.

+0

한 가지 간단한 방법 문자열 및 오프셋 및 구현 비교 그것이 인 것처럼 소요 프록시 클래스를 작성하는 것입니다 회전 된 문자열. 하지만 작은 문자열에 대해서는 많은 인터프리터 오버 헤드를 도입 할 것입니다. – user2357112

+0

성능에 많은 관심이 있다면 원시 코드를 원시 컴퓨터 코드로 컴파일하는 언어를 사용하십시오. – xis

+0

@XiaogeSu 여기서 문제는 언어가 아니라 문제 해결을위한 접근 방법입니다. –

답변

10

긴 문자열로 모든 문자열 조각을 만드는 데 시간이 오래 걸립니다. 이상 O (N^2) (N 길이의 N 개의 문자열을 생성하므로 원본 데이터를 소스에서 가져와 메모리에 복사해야하므로 전체 성능이 저하되고 정렬이 적절하지 않습니다. 메모리 요구 사항은 말할 것도 없습니다! 실제로 만들지 않고 - 대신 실제로 문자열을 자르는

, 다음 생각은 결과 문자열 비교 얼마나의 순서로 순환 문자열을 만드는 데 사용하는 i 값을 주문하는 것입니다. 이것은 다소 까다로울 수밖에 없습니다. (은/제거 잘못 여기에 몇 가지 물건을 편집 한. @TimPeters '대답을 참조하십시오를)

내가 여기에 찍은 접근 방식은 표준 라이브러리 무시하는 것입니다 - 그것은 어렵게 (생각을를 불가능) 해당 문자열을 '요구시'와 비교하고 내 자신의 정렬을 수행합니다. 알고리즘의 자연 선택은 기수 정렬입니다. 왜냐하면 우리는 한 번에 한 문자 씩 문자열을 고려해야하기 때문입니다.

먼저 설정하십시오. 3.2 버전의 코드를 작성 중이므로 시즌을 맛보아야합니다. (특히, 3.3 이상에서는 yield from을 이용할 수있었습니다.'(우리를 물론

def radix_sort(values, key, step=0): 
    if len(values) < 2: 
     for value in values: 
      yield value 
     return 

    bins = {} 
    for value in values: 
     bins.setdefault(key(value, step), []).append(value) 

    for k in sorted(bins.keys()): 
     for r in radix_sort(bins[k], key, step + 1): 
      yield r 

, 우리는 범용을 할 필요가 없습니다

from random import choice 
from timeit import timeit 
from functools import partial 

내가 이런 범용 기수 정렬 기능을 썼다) 나는 다음과 같은 수입을 사용하고 있습니다 bins '는 하나의 문자로만 레이블 될 수 있으며 은 바이트)의 시퀀스에 알고리즘을 적용한다는 의미입니다.)),하지만 해를 끼치 지 않습니다. 재사용할만한 게 있겠지, 그렇지? 어쨌든 아이디어는 간단합니다. 기본 케이스를 처리 한 다음, 키 함수의 결과에 따라 각 요소를 "빈"에 놓은 다음 정렬 된 빈 순서로 빈에서 값을 가져 와서 재귀 적으로 정렬합니다 bin의 내용.

key(value, n)n의 "기수"를 value으로 제공해야합니다. 문자열을 직접 비교하는 간단한 경우에는 lambda v, n: return v[n]과 같이 간단 할 수 있습니다. 그러나 여기서 생각해 보면, 그 시점에있는 문자열의 데이터 (주기적으로 간주 됨)에 따라 인덱스를 문자열과 비교하는 것이 좋습니다. 그럼 키를 정의 할 수 있습니다 :

def bw_key(text, value, step): 
    return text[(value + step) % len(text)] 

지금 올바른 결과를 얻는 데 트릭은 우리가 개념적으로 우리가 실제로 생성되지 않은 문자열의 마지막 문자를 결합하고 기억하는 것입니다. 인덱스 n을 사용하여 만든 가상 문자열을 고려하면 마지막 문자는 n - 1입니다. 왜냐하면 우리가 랩 어라운드 방식을 사용하기 때문입니다. 잠시 생각하면 n == 0;을 사용할 때 여전히 작동합니다. [그러나 앞으로 감쌀 때 문자열 인덱스를 인바운드로 유지해야합니다. 따라서 키 함수의 모듈러스 연산을 유지해야합니다.

이 키는 text에 전달되어야하는 일반 키 기능입니다. 비교를 위해 value을 변환 할 때 참조 할 것입니다. 그게 functools.partial이 나온 곳입니다. lambda으로 주위를 어지럽 힐 수도 있지만, 이것은 틀림없이 깨끗합니다.

어쨌든, 지금 우리가 쉽게 실제 키를 사용하여 변환을 작성할 수 있습니다

def burroughs_wheeler_custom(text): 
    return ''.join(text[i - 1] for i in radix_sort(range(len(text)), partial(bw_key, text))) 
    # Notice I've dropped the square brackets; this means I'm passing a generator 
    # expression to `join` instead of a list comprehension. In general, this is 
    # a little slower, but uses less memory. And the underlying code uses lazy 
    # evaluation heavily, so :) 

니스와 꽤 있습니다. 어떻게되는지 보자, 그렇지? 우리는에 대해 비교하는 표준이 필요합니다

def burroughs_wheeler_standard(text): 
    return ''.join([i[-1] for i in sorted([text[i:] + text[:i] for i in range(len(text))])]) 

그리고 타이밍 루틴 :

def test(n): 
    data = ''.join(choice('abcdefghijklmnopqrstuvwxyz') for i in range(n)) + '$' 
    custom = partial(burroughs_wheeler_custom, data) 
    standard = partial(burroughs_wheeler_standard, data) 
    assert custom() == standard() 
    trials = 1000000 // n 
    custom_time = timeit(custom, number=trials) 
    standard_time = timeit(standard, number=trials) 
    print("custom: {} standard: {}".format(custom_time, standard_time)) 

공지 사항 내가했던 수학은 반비례의 길이와 관련, trials의 수를 결정하는 test 문자열. 이것은 합리적으로 좁은 범위에서 테스트에 사용 된 총 시간을 유지해야합니다 - 맞습니까? .,

>>> imp.reload(burroughs_wheeler) 
<module 'burroughs_wheeler' from 'burroughs_wheeler.py'> 
>>> burroughs_wheeler.test(100) 
custom: 4.7095093091438684 standard: 0.9819262643716229 
>>> burroughs_wheeler.test(1000) 
custom: 5.532266880287807 standard: 2.1733253807396977 
>>> burroughs_wheeler.test(10000) 
custom: 5.954826800612864 standard: 42.50686064849015 

워 :))

은의 그것 (* 드럼 롤의 *) 어떻게하는지 보자 (잘못된 물론, 이후 우리는 standard 알고리즘은 최소 O (N^2입니다 설립) 그것은 무서운 점프의 약간이다. 어쨌든, 당신이 볼 수 있듯이, 새로운 접근법은 짧은 문자열에 오버 헤드 톤을 추가하지만 문자열 정렬 대신 병목 현상이 될 실제 정렬을 가능하게합니다.:)

+0

이것은 내 질문에 대한 최고의 설명 중 하나입니다. 고맙습니다. –

+0

기수 정렬을 더 일반적으로 사용하려면, 길이가 다른 문자열을 처리하려는 경우 키 기능을 조금 더 정교하게해야합니다. 구체적으로,이 경우'step'이 더 짧은 문자열 중 하나의 길이를 초과 할 때 발생하는'IndexError'를 잡아서 다른 것보다 "작은 것"을 비교하는 어떤 종류의 센티널 값을 반환하고자 할 것입니다. 너무 나쁜'chr (-1)'은 작동하지 않습니다;) –

6

@ KarlKnechtel의 spot-on 응답에 비트를 추가하기 만하면됩니다.

먼저 순환 순열 추출 속도를 높이는 "표준 방법"은 두 개의 복사본을 붙여넣고 바로 색인을 생성하는 것입니다. 후 :

N = len(text) 
text2 = text * 2 

다음 인덱스 i에서 시작하는 순환 순열 단지 text2[i: i+N] 및 문자 j 그 순열의 단지 text2[i+j]이다. 두 개의 슬라이스 또는 계수 (%) 작업을 함께 붙여 넣을 필요가 없습니다. 그것은 (문자열의 길이에 비해) 몇 가지 서로 다른 문자로 문자열에 대한

  • 칼의 기수 정렬합니다 ;-) 펑키의

    1. :하지만

      둘째, 내장 sort()이 사용할 수 있습니다 거의 확실합니다. (이 파이썬 2 스틱 있지만)

    는 개념 증명으로, 여기 칼의 코드의 일부에 대한 드롭 인 교체의 :

    def burroughs_wheeler_custom(text): 
        N = len(text) 
        text2 = text * 2 
        class K: 
         def __init__(self, i): 
          self.i = i 
         def __lt__(a, b): 
          i, j = a.i, b.i 
          for k in xrange(N): # use `range()` in Python 3 
           if text2[i+k] < text2[j+k]: 
            return True 
           elif text2[i+k] > text2[j+k]: 
            return False 
          return False # they're equal 
    
        inorder = sorted(range(N), key=K) 
        return "".join(text2[i+N-1] for i in inorder) 
    

    주 그 내장 sort()의 구현 입력의 각 요소에 대해 키를 정확히 한 번 계산하고 을 수행하여 정렬 기간 동안 결과를 저장합니다. 이 경우, 결과는 단지 시작 인덱스를 기억하고, __lt__ 메소드는 한 번에 하나의 문자 쌍을 "미만!"까지 비교하는 게으른 작은 K 인스턴스입니다. 또는 "보다 큼!" 해결되었습니다.

  • +1

    오, 물론이지. Java로 컴파일하고 Comparator를 생성해야한다면 분명 할 것입니다.) –

    +0

    LOL! "Obvious"이 마음에 들지는 않지만 기수 정렬은 더 빠릅니다. –

    +0

    Timsort가 핵심 결과 인 BTW를 캐시합니다. :) –

    1

    이전 답변에 동의합니다. 파이썬에서 string/list slicing은 거대한 알고리즘 계산을 수행 할 때 병목 현상이됩니다. 아이디어는 은 슬라이스하지 않습니다입니다.

    [편집 : 슬라이싱은 없지만 목록 색인 생성. 목록 대신 array.array를 사용하면 실행 시간이 절반으로 줄어 듭니다. 배열 인덱싱은 간단합니다. 인덱싱 목록은 더 복잡한 프로세스입니다.]

    여기에는 문제에 대한 더 효과적인 해결 방법이 있습니다.

    아이디어에는 발전기가있어 슬라이서 (rslice)로 작동합니다. itertools.islice와 비슷한 개념이지만 끝에 도달하면 문자열의 시작 부분으로 이동합니다. 그리고 생성 할 때 지정한 시작 위치에 도달하기 전에 중지됩니다. 이 트릭을 사용하면 메모리에 하위 문자열을 복사하지 않으므로 결국에는 모든 곳에서 복사본을 만들지 않고 문자열 위에 포인터 만 이동할 수 있습니다.

    따라서 우리는 [슬라이스의 rslices, lastchar] 을 포함하는 목록을 만들고 rslice를 키로 사용하여 정렬합니다 (cf sort 함수에서 볼 수있는 것처럼).

    정렬 할 때 목록의 각 요소에 대해 두 번째 요소 (이전에 저장된 슬라이스의 마지막 요소) 만 수집하면됩니다.

    from itertools import izip 
    def cf(i1,i2): 
        for i,j in izip(i1[0](),i2[0]()): # We grab the the first element (is a lambda) and execute it to get the generator 
         if i<j: return -1 
         elif i>j: return 1 
        return 0 
    
    def rslice(cad,pos): # Slice that rotates through the string (it's a generator) 
        pini=pos 
        lc=len(cad) 
        while pos<lc: 
         yield cad[pos] 
         pos+=1 
        pos=0 
        while pos<pini-1: 
         yield cad[pos] 
         pos+=1 
    
    def lambdagen(start,cad): # Closure to hold a generator 
        return lambda: rslice(cad,start) 
    
    def bwt(txt): 
        lt=len(txt) 
        arry=list(txt)+[None] 
    
        l=[(lambdagen(0,arry),None)]+[(lambdagen(i,arry),arry[i-1]) for i in range(1,lt+1)] 
        # What we keep in the list is the generator for the rotating-slice, plus the 
        # last character of the slice, so we save the time of going through the whole 
        # string to get the last character 
    
        l.sort(cmp=cf) # We sort using our cf function 
        return [i[1] for i in l] 
    
    print bwt('Text I want to apply BTW to :D') 
    
    # ['D', 'o', 'y', 't', 'o', 'W', 't', 'I', ' ', ' ', ':', ' ', 'B', None, 'T', 'w', ' ', 
    # 'T', 'p', 'a', 't', 't', 'p', 'a', 'x', 'n', ' ', ' ', ' ', 'e', 'l'] 
    

    편집 : 사용 배열 (이 감소 실행 시간) :

    def bwt(txt): 
        lt=len(txt) 
        arry=array.array('h',[ord(i) for i in txt]) 
        arry.append(-1) 
    
        l=[(lambdagen(0,arry),None)]+[(lambdagen(i,arry),arry[i-1]) for i in range(1,lt+1)] 
    
        l.sort(cmp=cf) 
        return [i[1] for i in l] 
    
    관련 문제