2014-01-23 56 views
7

我試圖在python中實現Burrows-Wheeler變換。 (這是在線課程中的一項任務,但我希望我已經完成了一些工作,有資格尋求幫助)。蟒蛇中的Burrows-Wheeler中的性能問題

該算法的工作原理如下。以一個以特殊字符結尾的字符串(在我的例子中爲$)並從該字符串創建所有循環字符串。按字母順序對所有這些字符串進行排序,使特殊字符始終小於其他任何字符。在這之後得到每個字符串的最後一個元素。

這給了我一個oneliner:

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

哪個是正確的,相當快的相當大的字符串(這是足以解決問題):

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

但是,當我嘗試處理一個真正巨大的字符串,數百萬字符,我失敗了(處理需要太多時間)。

我假設問題是在內存中存儲太多字符串。

有什麼辦法可以解決這個問題嗎?

P.S.只是想指出,這也可能看起來像一個家庭作業問題,我的解決方案已經通過平地機,我只是想找一個方法來加快速度。另外,我不會爲其他人帶來樂趣,因爲如果他們想找到解決方案,wiki文章就有一個與我的相似的文章。我也檢查了this questio這聽起來相似,但回答了一個更難的問題,如何解碼用這種算法編碼的字符串。

+0

一個簡單的方法是寫一個代理類,需要一個字符串和偏移並實現比較,好像它是一個旋轉字符串。不過,你會爲小字符串引入大量的解釋器開銷。 – user2357112

+0

如果您真的關注性能,請使用將源代碼編譯爲本機機器碼的語言。 – xis

+0

@小哥蘇這裏的問題不在於語言,它是在解決問題的方法。 –

回答

10

用長字符串製作所有字符串片需要很長時間。它是至少 O(N^2)(因爲你創建了N個長度爲N的字符串,並且每一個都必須複製到內存中,從原始數據中獲取其源數據),這會破壞整體性能並使分類無關緊要。更不用說內存需求了!

,而不是實際切片串,在下以爲是爲了你用來創建循環串,在如何生成的字符串比較順序i值 - 沒有實際創建它。事實證明這有點棘手。 (刪除/編輯這裏的一些東西,這是錯誤的。請參閱@TimPeters的回答

我在這裏採取的方法是繞過標準庫 - 這使得很難(雖然不是不可能)比較這些字符串'按需' - 並做我自己的排序。這裏算法的自然選擇是基數排序,因爲我們無論如何都需要一次考慮一個字符串。

讓我們先設置。我正在編寫3.2版本的代碼,所以季節的味道。 (特別是在3.3以上,我們可以利用yield from。)我使用下面的進口:

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

我寫了一個通用的基數排序的功能是這樣的:

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 

當然,我們並不需要是通用(我們的「 bin'只能用單個字符來標記,並且大概是確實是的意思是將該算法應用於字節的序列;)),但它並沒有傷害。也可能有可重用的東西,對吧?無論如何,這個想法很簡單:我們處理一個基本情況,然後根據關鍵函數的結果將每個元素放入一個「bin」中,然後我們按照已排序的bin順序從bin中取出值,遞歸排序每個元素bin的內容。

接口要求key(value, n)給我們nvalue的「基數」。所以對於簡單的情況,比如直接比較字符串,這可能很簡單,如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字符串。這應該將測試所用的總時間保持在相當窄的範圍內 - 對嗎? ;)(錯了,當然,因爲我們建立的standard算法至少是O(N^2))

讓我們來看看它是如何做(*擊鼓聲*):

>>> 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 

哇。這有點可怕的跳躍。無論如何,正如你所看到的,新方法在短字符串上增加了大量的開銷,但是使實際的排序成爲瓶頸而不是字符串切分。:)

+0

這是我收到的關於我的問題的最好解釋之一。謝謝你 –

+0

請注意,如果你想更普遍地使用基數排序,如果你想要處理不同長度的字符串,關鍵函數必須更復雜一些。具體來說,在這種情況下,您希望捕捉當「step」超出其中一個較短字符串的長度時引發的'IndexError',並返回某種類型的比較「小於」其他任何內容的哨兵值。太糟糕了'chr(-1)'不起作用;) –

6

只需在@ KarlKnechtel的點對點響應中添加一點點即可。首先,加速循環置換抽取的「標準方法」就是將兩個拷貝粘貼在一起並直接編入索引。後:

N = len(text) 
text2 = text * 2 

然後從索引i循環置換隻是text2[i: i+N]和字符j在置換隻是text2[i+j]。無需粘貼兩個切片或模數(%)操作。

其次,內置sort()可用於這一點,雖然:

  1. 這是時髦;-)
  2. 對於幾個不同的字符的字符串(與字符串的長度),卡爾的基數排序將幾乎肯定會更快。

作爲證據的概念,這裏有一個下拉更換爲卡爾的代碼的一部分(雖然這堅持到Python 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()實施計算該鍵的輸入中的每個元素只有一次,並且確實在排序期間保存了這些結果。在這種情況下,結果只是記住起始索引的懶惰的小實例,並且其方法每次比較一個字符對直到「小於!」。或「大於!」已解決。

+1

哦,jeez,當然。這很明顯,如果我不得不在Java中做並創建一個比較器;) –

+0

LOL! 「顯而易見」並不是真的想到,但是你的基數排序更快:-) –

+0

感謝Timsort緩存關鍵結果的提示。 :) –

1

我同意以前的答案,python中的字符串/列表切片成爲執行巨大算法計算時的瓶頸。這個想法是沒有切片

[編輯:不是也切片,但列表索引。如果你使用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'] 

編輯:使用陣列(執行時間減少2):

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]