7

その答えに基づいて、ここにマージソートに使用されるマージ関数の2つのバージョンがあります。2番目の方がはるかに速い理由を理解するのを手伝っていただけませんか。私はそれを50000のリストでテストしましたが、2番目のリストは8倍高速です(Gist)。

def merge1(left, right):
    i = j = inv = 0
    merged = []
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            merged.append(left[i])
            i += 1
        else:
            merged.append(right[j])
            j += 1
            inv += len(left[i:])

    merged += left[i:]
    merged += right[j:]
    return merged, inv

def merge2(array1, array2):
    inv = 0
    merged_array = []
    while array1 or array2:
        if not array1:
            merged_array.append(array2.pop())
        elif (not array2) or array1[-1] > array2[-1]:
            merged_array.append(array1.pop())
            inv += len(array2)
        else:
            merged_array.append(array2.pop())
    merged_array.reverse()
    return merged_array, inv

並べ替え関数は次のとおりです。

def _merge_sort(list, merge):
    len_list = len(list)
    if len_list < 2:
        return list, 0
    middle = len_list / 2
    left, left_inv   = _merge_sort(list[:middle], merge)
    right, right_inv = _merge_sort(list[middle:], merge)
    l, merge_inv = merge(left, right)
    inv = left_inv + right_inv + merge_inv
    return l, inv

import numpy.random as nprnd
test_list = nprnd.randint(1000, size=50000).tolist()

test_list_tmp = list(test_list) 
merge_sort(test_list_tmp, merge1)

test_list_tmp = list(test_list) 
merge_sort(test_list_tmp, merge2)
4

3 に答える 3

10

上記のkreativiteaと同様の答えですが、より多くの情報があります(私は思います!)

したがって、2つの50Kアレイをマージするために、実際のマージ関数をプロファイリングします。

マージ1

         311748 function calls in 15.363 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001   15.363   15.363 <string>:1(<module>)
        1   15.322   15.322   15.362   15.362 merge.py:3(merge1)
   221309    0.030    0.000    0.030    0.000 {len}
    90436    0.010    0.000    0.010    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

merge2

         250004 function calls in 0.104 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.001    0.001    0.104    0.104 <string>:1(<module>)
        1    0.074    0.074    0.103    0.103 merge.py:20(merge2)
    50000    0.005    0.000    0.005    0.000 {len}
   100000    0.010    0.000    0.010    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
   100000    0.014    0.000    0.014    0.000 {method 'pop' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'reverse' of 'list' objects}

したがって、merge1の場合lenは221309、90436appendであり、15.363秒かかります。したがって、merge2の場合、50000、100000、および100000lenであり、0.104秒かかります。appendpop

lenappend popはすべてO(1)(詳細はこちら)であるため、これらのプロファイルは実際に時間がかかっていることを示していません。それだけで、より高速になるはずですが、20%程度しかありません。

コードを読んだだけで、原因は実際にはかなり明白です。

最初の方法では、次の行があります。

inv += len(left[i:])

したがって、それが呼び出されるたびに、配列を再構築する必要があります。この行をコメントアウトする(または単にinv += 1何かに置き換える)と、他の方法よりも高速になります。これは、時間の増加に関与する単一の回線です。

これが原因であることに気付いたので、コードを改善することで問題を修正できます。スピードアップのためにこれに変更してください。これを行った後、それはより速くなりますmerge2

inv += len(left) - i

これに更新します:

def merge3(left, right):
    i = j = inv = 0
    merged = []
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            merged.append(left[i])
            i += 1
        else:
            merged.append(right[j])
            j += 1
            inv += len(left) - i

    merged += left[i:]
    merged += right[j:]
    return merged, inv
于 2012-12-03T17:56:45.830 に答える
3

優れたcProfileモジュールを使用して、このような問題を解決できます。

>>> import cProfile
>>> a = range(1,20000,2)
>>> b = range(0,20000,2)
>>> cProfile.run('merge1(a, b)')
         70002 function calls in 0.195 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.184    0.184    0.195    0.195 <pyshell#7>:1(merge1)
        1    0.000    0.000    0.195    0.195 <string>:1(<module>)
    50000    0.008    0.000    0.008    0.000 {len}
    19999    0.003    0.000    0.003    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


>>> cProfile.run('merge2(a, b)')
         50004 function calls in 0.026 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.016    0.016    0.026    0.026 <pyshell#12>:1(merge2)
        1    0.000    0.000    0.026    0.026 <string>:1(<module>)
    10000    0.002    0.000    0.002    0.000 {len}
    20000    0.003    0.000    0.003    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
    20000    0.005    0.000    0.005    0.000 {method 'pop' of 'list' objects}
        1    0.000    0.000    0.000    0.000 {method 'reverse' of 'list' objects}

情報を少し見てみると、コメント投稿者は正しいように見えます-len関数ではありません-それは文字列モジュールです。文字列モジュールは、次のように、物事の長さを比較するときに呼び出されます。

>>> cProfile.run('0 < len(c)')
         3 function calls in 0.000 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {len}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

リストをスライスするときにも呼び出されますが、これは非常に迅速な操作です。

>>> len(c)
20000000
>>> cProfile.run('c[3:2000000]')
         2 function calls in 0.011 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.011    0.011    0.011    0.011 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

TL; DR:文字列モジュールの何かが最初の関数で0.195秒、2番目の関数で0.026秒かかっています。:どうやら、inv += len(left[i:])この行の配列の再構築。

于 2012-12-03T17:27:28.923 に答える
0

推測しなければならないのは、おそらくリストから要素を削除するコストに関係していると思います。最後から削除する(ポップ)方が最初から削除するよりも高速です。2つ目は、リストの最後から要素を削除することを優先します。

パフォーマンスノートを参照してください:http: //effbot.org/zone/python-list.htm

「アイテムを削除するのに必要な時間は、同じ場所にアイテムを挿入するのに必要な時間とほぼ同じです。最後のアイテムの削除は速く、最初のアイテムの削除は遅いです。」

于 2012-12-03T17:55:40.240 に答える