前提・実現したいこと
Pythonの勉強のため再帰関数を使用したクイックソートを行いたいです。
実行環境は下記の通りです。
OS : Windows10(64bit), Ubuntu系のディストリビューション
Python : 3.10.0, 3.7.3
教えていただきたいこと
プログラムの停止する原因と対処法
エラーメッセージが出力されない原因
この問題が分かる方がいらっしゃれば、ご教示をお願いいたします。
発生している問題・エラーメッセージ
Pythonを用いてクイックソートのプログラムを作成し、ソートする要素の数を多くしたところ、エラーメッセージを表示しないでプログラムが終了しました。
エラーメッセージはありません
該当のソースコード
Python
1import random 2import time 3import sys 4import threading 5 6#再起する回数の上限を設定 7sys.setrecursionlimit(2100000000) 8#スタックの容量を設定 9threading.stack_size(255*1024*1024) 10 11#並び替えをするリスト内の要素数を設定するための変数 12N = 0 13#生成する乱数の大きさの範囲を設定するための変数 14random_range = 9999999 15 16#乱数リストを生成する関数 17def make_ramdom_list(): 18 global random_list 19 random_list = [] 20 for i in range(N): 21 random_list = random_list + [random.randrange(random_range)] 22 23#クイックソートのメインプログラム 24def quick_sort(sort_ramdom_list, first, last): 25 if first < last: 26 pivot = sort_ramdom_list[int((first + last) / 2)] 27 i = first 28 j = last 29 30 while sort_ramdom_list[i] < pivot: 31 i += 1 32 while sort_ramdom_list[j] > pivot: 33 j -= 1 34 if i <= j: 35 swap_number = sort_ramdom_list[j] 36 sort_ramdom_list[j] = sort_ramdom_list[i] 37 sort_ramdom_list[i] = swap_number 38 i += 1 39 j -= 1 40 41 quick_sort(sort_ramdom_list, first, j) 42 quick_sort(sort_ramdom_list, i, last) 43 44#ソートする要素の数を設定 45N = 200000 46print("N:" + str(N)) 47make_ramdom_list() 48start = time.time() 49quick_sort(random_list, 0, N - 1) 50#ソートされたリストを表示する(要素が多いと表示する量が多くなりますのでコメントアウトします。) 51#print(random_list) 52print("time:" + str(time.time() - start))
試したこと
再帰する回数の上限を設定
スタックの容量を設定
補足情報(FW/ツールのバージョンなど)
当方の実行環境ではリストの要素の数が数万程度であれば実行可能なのですが、20万程度になるとエラーメッセージが無くプログラムが停止します。
プログラム実行時に使用するメモリーの量を確認したところ、最大で13メガバイト程度でした。
同様の結果を出力するマージソートのプログラムを実行したところ問題なく実行されました。
参考にしたサイトでは再帰関数がタイムアウトする可能があるとの記述があったのですが、技術不足の為に理解することができませんでした。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/04 01:36
2021/11/04 02:00
2021/11/04 02:20
回答7件
0
一般論で言いますと、再起関数というのはスタックを消費するためにあまり深くは実行できません。
なので、対処法、というなら、再帰を使わない処理に書き変える、ということになろうかと思います
投稿2021/11/04 02:24
総合スコア88042
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/04 02:56
2021/11/04 02:59
2021/11/04 03:26
0
プログラムの停止する原因と対処法
エラーメッセージが出力されない原因
こちらの Windows 10 環境では quick_sort() の呼び出し深さが 2672 ぐらいで終わります。
pivot 値より小さい値と大きい値に振り分けが終わらないうちに再帰呼び出しになるため深く呼び出す状況となり、たぶんスタックオーバーフローになっていると思います。
ご提示の quick_sort ベースで quick sort にすると次のようになると思います。
Python
1def quick_sort(sort_ramdom_list, first, last): 2 if first < last: 3 pivot = sort_ramdom_list[(first + last) // 2] 4 i = first 5 j = last 6 while True: 7 while sort_ramdom_list[i] < pivot: 8 i += 1 9 while sort_ramdom_list[j] > pivot: 10 j -= 1 11 if i >= j: 12 break 13 sort_ramdom_list[i], sort_ramdom_list[j] = sort_ramdom_list[j], sort_ramdom_list[i] 14 i += 1 15 j -= 1 16 17 quick_sort(sort_ramdom_list, first, j) 18 quick_sort(sort_ramdom_list, j + 1, last)
できれば計算量が O(n^2) になることがないよう
Python
1def median(a, b, c): 2 ret = a 3 if a >= b >= c or a <= b <= c: ret = b 4 if b >= c >= a or b <= c <= a: ret = c 5 return ret 6 7def quick_sort(sort_ramdom_list, first, last): 8 ... 9 #pivot = sort_ramdom_list[int((first + last) / 2)] 10 val = sort_ramdom_list[(first + last) // 2] 11 pivot = median(sort_ramdom_list[first], val, sort_ramdom_list[last])
としておきたいですね。
そして list の生成は高速化しましょう。
Python
1#def make_ramdom_list(): 2# global random_list 3# random_list = [] 4# for i in range(N): 5# random_list = random_list + [random.randrange(random_range)] 6def make_ramdom_list(start, end, num): 7 return [random.randrange(start, end) for _ in range(num)] 8 9#make_ramdom_list() 10random_list = make_ramdom_list(0, random_range, N)
投稿2021/11/05 11:28
総合スコア1156
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/06 00:51
0
ベストアンサー
make_random_list を高速化し、quick_sort を正しく実装してみました。
python
1import random 2import time 3 4N = 200000 5random_range = 9999999 6 7def make_ramdom_list(): 8 global random_list 9 random_list = [0] * N 10 for i in range(N): 11 random_list[i] = random.randrange(random_range) 12 13def quick_sort(a, first, last): 14 if first < last: 15 pivot = a[last] 16 i = first 17 for j in range(first, last): 18 if a[j] <= pivot: 19 a[i], a[j] = a[j], a[i] 20 i += 1 21 a[i], a[last] = a[last], a[i] 22 quick_sort(a, first, i-1) 23 quick_sort(a, i+1, last) 24 25print("N:" + str(N)) 26make_ramdom_list() 27print(random_list[:8]) 28start = time.time() 29quick_sort(random_list, 0, N - 1) 30print("time:" + str(time.time() - start)) 31print(random_list[:8])
ソート前後のデータを先頭 8個だけ表示しています。
投稿2021/11/05 06:18
編集2021/11/05 08:03総合スコア8224
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/06 00:10
0
問題
- スタックオーバーフローで落ちている(推測)
- クイックソートではない
- スタックの設定が出来ていない
1. スタックオーバーフローで落ちている(推測)
何も表示されないのはおそらくWindowsだからだと思いますが、スタックオーバーフローで落ちています。
Windowsでコマンドプロンプトから実行している場合、実行終了時に
bat
1echo %ERRORLEVEL%
とすれば0以外のプロセス終了コードを確認することが出来ます。-1073741571(C00000FD)であればスタックオーバーフローです。
Unix系のOSであればセグメンテーション違反で検出され、大抵のシェルでは処理後にエラー表示があると思います。
2. クイックソートではない
既にご指摘があるとおり、クイックソートではありません。本来のクイックソートはリスト中の全値を走査し、pivot以下のグループ(A)とpivotより大きいグループ(B)に分け、(A)→pivot→(B)となる順に並べた後、(A)内と(B)内をそれぞれ同様に処理していく方法です。
現状のコードはpivotはあるものの1回しかスワップをせずに重複する範囲をさらに再帰呼び出ししているため、バブルソートより効率の悪いソートになっています。
またスワップ1回につき2度再帰呼び出しをするコードなのでスタックを大量に消費しています。ただのクイックソートであればここまで極端にスタックを消費することはありません。ただし正しいクイックソートでも、並び順によっては再帰呼び出しのバブルソートと同じくらいスタックを消費するので、入力が限定されないケースを再帰呼び出しでは実装しません。
まずクイックソートを学びなおしてください。
3. スタックの設定が出来ていない
python
1import threading 2threading.stack_size(255*1024*1024)
では自分で起動するスレッドのスタック設定は出来ても、メインスレッドの設定は出来ません。
メインスレッドの設定をしたければ以下のようにしてください。
python
1from resource import * 2setrlimit(RLIMIT_STACK, (255 * 1024 * 1024, -1))
回答
プログラムの停止する原因と対処法
原因
スタックの無駄遣いによるスタックオーバーフロー(推測)
対処法
まずは正しくクイックソートを実装してください。
余力があれば再帰呼び出しをやめてください。
エラーメッセージが出力されない原因
スタックオーバーフローだからです(推測)。再帰呼び出しの上限を自分で書き換えて、足りないスタックで動かしたためpython実行環境が落ちています。
投稿2021/11/05 04:07
退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/05 15:43
0
手元の環境で、ある操作を行ってから問題のスクリプトを実行してみました。
bash
1$ lsb_release -ir 2Distributor ID: Ubuntu 3Release: 21.04 4 5# デフォルトのスタックサイズは 8 Mib 6$ ulimit -a | grep stack 7stack size (kbytes, -s) 8192 8 9# スタックサイズを 512 KiB に変更 10$ ulimit -s 512 11 12# 実行 13$ python quick.py 14N:200000 15Segmentation fault (core dumped) # 短時間で発生 16 17# gdb による解析 18$ gdb /usr/bin/python core 19 : 20 21Core was generated by `python quick.py'. 22Program terminated with signal SIGSEGV, Segmentation fault. 23#0 0x0000000000515e42 in _PyEval_EvalFrameDefault ()
スクリプト内ではスタックサイズを 255 MiB に設定していますが、少なくとも Linux OS ではシステム側の設定(最近のデフォルトサイズは 8 MiB
)が優先されます。
python
1#スタックの容量を設定 2threading.stack_size(255*1024*1024)
プログラムの停止する原因と対処法
Linux OS と同様に Windows OS でもスタックサイズの上限が設定されているのではないかと思います。その値を変更することができるのかどうかは私には分かりませんけれども。
また、末尾再帰に関しては Tail Call Optimization Decorator がありますが、古い話なので効果の程は不明です。
エラーメッセージが出力されない原因
分かりません。
投稿2021/11/04 18:37
編集2021/11/04 18:40総合スコア20655
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/05 01:19
0
コメントをしたので、回答もしておきます。
アルゴリズムがクイックソートでない
ftlobw さんの指摘の通り、この処理はクイックソートにはなっていません。
- リストの真ん中をピボットにする。
- 右(小さい)から順に見ていって、最初のピボットより小さい値が出てくるところをiとする。
- 左(大きい)から順に見ていって、最初のピボットより大きい値が出てくるところをiとする。
- iとjの場所にある値を入れ替える。
- 小さい方からjまでで同じ処理をする
- 大きい方からiもでで同じ処理をする。
となっています。これはクイックソートのアルゴリズムではありません。
クイックソートは
- リストの真ん中をピボットにする。
- ピボットより左にある、ピボットより大きい値を全て右に移す。
- ピボットより右にある、ピボットより小さい値を全て左に移す。
- ピボットより左をソートしたもの + ピボットの値 + ピボットより右をソートしたもの が結果
ざっとという処理になるはずです。
再帰処理で多数のデータをソートするのは現実的ではありません。
クイックソートは再帰で表現するとすっきり書けますので、再帰処理の理解にはよい例だと思いますが、そのロジックで多数のデータをソートするのは現実的ではありません。
大きなデータをクイックソートでソートしたいのであれば、再帰を使わない憑肩で実装すべきです。
投稿2021/11/04 07:17
総合スコア13790
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/04 08:16
2021/11/04 09:07
2021/11/04 23:51
0
質問者さんのコメントを受けての追記
(コメントに書いてもらったプログラムはインデントが崩れていて読めないので、すみませんが試していません)
試しにWikipediaの記事にあったクイックソートを実行した場合の途中経過は以下の通りです。
(pivotの選択は、質問者さんの実装と同じにしてあります)
0~9の範囲で pivot: 3
を選択しているところまでは一緒ですが、
その後のpartition処理でpivot未満の数字が一気に左に寄っているのが分かると思います。
text
1before: [1, 7, 5, 4, 3, 8, 9, 2, 6, 0] 2first: 0, last: 9, pivot: 3 3after: [1, 0, 2, 3, 4, 8, 9, 5, 6, 7] 4<以下省略>
何をもってクイックソートと言うのかは、コンピュータサイエンスを専攻したわけではないので自信をもって回答はできないのですが、
Wikipediaの記事に書いてある「ピボットの選択:適当な値(ピボット(英語版)という)を境界値として選択する」を基準にすれば質問者さんの元々の実装は、
再帰を使っているか? → Yes
クイックソートか? → No
だと思います。
python
1from typing import Any, Callable, MutableSequence 2 3# 分割関数 4# 配列の指定範囲をピボットに従って分割する 5# 6# @param seq - 分割する配列 7# @param keyFn - 配列要素のキー値を計算する関数 8# @param first - 分割範囲の最初のインデックス 9# @param last - 分割範囲の最後のインデックス 10# @returns 分割点のインデックス 11def partition(seq: MutableSequence[Any], keyFn: Callable[[Any], Any], first: int, last: int): 12 pivot = seq[int((first + last) / 2)] 13 print(f'first: {first}, last: {last}, pivot: {pivot}') 14 while True: 15 while keyFn(seq[first]) < pivot: 16 first += 1 17 while pivot < keyFn(seq[last]): 18 last -= 1 19 if first >= last: 20 return last + 1 21 seq[first], seq[last] = seq[last], seq[first] 22 first += 1 23 last -= 1 24 25# クイックソート 26# 27# @param seq - ソートする配列 28# @param keyFn - 配列要素のキー値を計算する関数 29def quicksort(seq: MutableSequence[Any], keyFn: Callable[[Any], Any]): 30 def quicksortImpl(seq: MutableSequence, keyFn: Callable[[Any], int], first: int, last: int): 31 global cnt 32 while first < last: 33 cnt += 1 34 print('before:', seq) 35 p = partition(seq, keyFn, first, last) 36 print('after:', seq, end='\n\n') 37 if (p - first) < (last - p): 38 quicksortImpl(seq, keyFn, first, p - 1) 39 first = p 40 else: 41 quicksortImpl(seq, keyFn, p, last) 42 last = p - 1 43 quicksortImpl(seq, keyFn, 0, len(seq) - 1) 44 45 46cnt = 0 47lst = [1, 7, 5, 4, 3, 8, 9, 2, 6, 0] * 1 48quicksort(lst, lambda x: x) 49 50print('=' * 30) 51print(cnt, len(lst)) 52print(lst)
以下は以前の回答です。
pivotを決めた後、quick_sort()を再帰呼び出しするまでの処理が怪しいです。
試しにソートするデータのサイズを変更してquick_sort()の呼び出し回数を確認したところ、
サイズ50で2000回、サイズ100で7.5万回、150で231万回 でした。
このペースだと再帰呼び出しの回数やスタック容量を多少増やしても、
データサイズが大きくなるとスグに破綻すると思います。
添付プログラムのように、最初に [1, 7, 5, 4, 3, 8, 9, 2, 6, 0]
というデータを与えて
quick_sort()内でリストの並べ替え状況を確認するコードを埋め込んでみると以下のような結果になります。
これは、ピボットを境界値としてリストをピボット未満(左)とそれ以外(右)に値を分割していくクイックソートではなくて、値を1つづつ入れ替えていくような挙動になっているのではないかと思います。
Wikipediaの記事などを見て、実装を見直してみてはどうでしょうか。
text
1before: [1, 7, 5, 4, 3, 8, 9, 2, 6, 0] 2first: 0, last: 9, pivot: 3 3after: [1, 0, 5, 4, 3, 8, 9, 2, 6, 7] 4 5before: [1, 0, 5, 4, 3, 8, 9, 2, 6, 7] 6first: 0, last: 8, pivot: 3 7after: [1, 0, 2, 4, 3, 8, 9, 5, 6, 7] 8 9before: [1, 0, 2, 4, 3, 8, 9, 5, 6, 7] 10first: 0, last: 6, pivot: 4 11after: [1, 0, 2, 3, 4, 8, 9, 5, 6, 7] 12 13before: [1, 0, 2, 3, 4, 8, 9, 5, 6, 7] 14first: 0, last: 3, pivot: 0 15after: [0, 1, 2, 3, 4, 8, 9, 5, 6, 7] 16 17<以下省略>
python
1#クイックソートのメインプログラム 2def quick_sort(sort_ramdom_list, first, last): 3 global cnt 4 if first < last: 5 cnt += 1 6 print('before:', sort_ramdom_list) 7 8 pivot = sort_ramdom_list[int((first + last) / 2)] 9 i = first 10 j = last 11 12 while sort_ramdom_list[i] < pivot: 13 i += 1 14 while sort_ramdom_list[j] > pivot: 15 j -= 1 16 if i <= j: 17 swap_number = sort_ramdom_list[j] 18 sort_ramdom_list[j] = sort_ramdom_list[i] 19 sort_ramdom_list[i] = swap_number 20 i += 1 21 j -= 1 22 23 print(f'first: {first}, last: {last}, pivot: {pivot}') 24 print('after:', sort_ramdom_list, end='\n\n') 25 26 quick_sort(sort_ramdom_list, first, j) 27 quick_sort(sort_ramdom_list, i, last) 28 29cnt = 0 30lst = [1, 7, 5, 4, 3, 8, 9, 2, 6, 0] * 1 31quick_sort(lst, 0, len(lst) - 1) 32 33print('=' * 30) 34print(cnt, len(lst)) 35print(lst)
投稿2021/11/04 04:35
編集2021/11/04 08:38退会済みユーザー
総合スコア0
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/11/04 07:41
2021/11/05 00:04
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。