【アルゴリズム解説】「アルゴリズム探求」簡単解説‼【①ソート】

こんにちはヤク学長です。
データサイエンティスト兼ファーマシストで、アルゴリズムやBI開発を行っています。

本記事の目的は、「アルゴリズムの基本を知る」ことを目的としています。

【本記事のもくじ】

下記の方法で、簡単に概要を抑えることができます。

  • 1.アルゴリズムとは
  • 2.ソート
  • 3.バイナリサーチ

それでは、上から順番に見ていきます。
なお、本上記の方法を順番に抑えれば成果が出ます。

記事の内容は「転載 & 引用OK」問題ありません。

1.アルゴリズムとは

アルゴリズムとは

アルゴリズムとは、計算機科学や数学において、特定の問題を解決するために設計された手順や規則のことを指します。アルゴリズムは、入力を取り、それを処理して出力を生成する計算手順の一連のステップから構成されます。

アルゴリズムは、コンピュータプログラムやソフトウェアアプリケーションの中核となるものであり、例えば、ソートや検索、データ圧縮、暗号化、グラフ理論、機械学習、人工知能などの分野で広く使用されています。

アルゴリズムは、特定の問題を解決するために設計されるため、最適なアルゴリズムは問題によって異なります。アルゴリズムの性能は、入力サイズに対してどの程度効率的に動作するかによって測定され、一般的に、時間計算量や空間計算量などの指標が使用されます。

実際の現場ではどう使われる?

実際の現場では、アルゴリズムは多くの場面で活用されています。以下にいくつかの例を挙げます。

  • ソフトウェア開発:プログラムの中核となるアルゴリズムを設計し、実装することによって、ソフトウェアの機能やパフォーマンスを向上させることができます。
  • データサイエンス:機械学習やデータマイニングにおいて、アルゴリズムはデータの分析や予測に不可欠な役割を果たしています。
  • ネットワーク設計:ルーティングや通信プロトコルなど、ネットワークの設計や最適化にアルゴリズムが活用されています。
  • 組合せ最適化:組合せ最適化は、複数の要素から最適な組み合わせを見つけ出すことを目的とした分野であり、多くの場面でアルゴリズムが活用されています。
  • 量子コンピュータ:量子アルゴリズムは、量子コンピュータに特化したアルゴリズムであり、高速に素因数分解や最適化問題を解決することができます。

アルゴリズムは、様々な分野で広く活用されており、技術の進歩によってますます重要性が高まっています。

データ構造とは

データ構造とは、コンピュータ上でデータを効率的に管理するための方法論のことを指します。データ構造は、データの格納、処理、検索、取り出しなどを効率的に行うことができるように設計されています。

データ構造は、基本的には、データを表現するための方法と、そのデータを操作するための方法を提供することによって機能します。データ構造は、以下のような機能を提供します。

  • 格納:データを効率的に格納する方法を提供します。例えば、配列やリストなどのデータ構造を使用することによって、複数のデータを格納することができます。
  • 検索:データを効率的に検索する方法を提供します。例えば、ハッシュテーブルや木構造などのデータ構造を使用することによって、データを高速に検索することができます。
  • ソート:データを効率的にソートする方法を提供します。例えば、クイックソートやマージソートなどのアルゴリズムを使用することによって、データを高速にソートすることができます。

データ構造は、アルゴリズムの中核となる要素であり、コンピュータ科学や情報工学の分野で広く使用されています。データ構造は、問題に応じて適切に選択することが重要であり、効率的なプログラムの開発に不可欠な要素の一つです。

Big O Notation

Big O Notation(ビッグ・オー記法)は、アルゴリズムの実行時間や空間利用量を表すために使用される記法です。アルゴリズムの性能を評価するために使用されます。

Big O Notationは、アルゴリズムの実行時間または空間利用量を、問題の入力サイズに対する上限として表します。例えば、n個のデータを処理するアルゴリズムの実行時間がO(n)である場合、nが大きくなるにつれて、アルゴリズムの実行時間はnに比例して増加することを示しています。また、O(1)であれば、入力サイズに関係なく一定の実行時間を持つことを示しています。

以下に、ビッグ・オー記法の表記方法の例を示します。

  • O(1):アルゴリズムの実行時間が一定で、問題の入力サイズに依存しない場合に使用されます。
  • O(log n):アルゴリズムの実行時間が、入力サイズの対数関数に比例する場合に使用されます。例えば、バイナリサーチアルゴリズムはO(log n)で実行されます。
  • O(n):アルゴリズムの実行時間が、入力サイズに比例する場合に使用されます。例えば、線形探索アルゴリズムはO(n)で実行されます。
  • O(n log n):アルゴリズムの実行時間が、入力サイズとその対数関数の積に比例する場合に使用されます。例えば、マージソートアルゴリズムはO(n log n)で実行されます。
  • O(n^2):アルゴリズムの実行時間が、入力サイズの二乗に比例する場合に使用されます。例えば、バブルソートアルゴリズムはO(n^2)で実行されます。

ビッグ・オー記法は、アルゴリズムの実行時間や空間利用量を評価するために非常に重要なツールであり、アルゴリズムの選択や設計に役立ちます。しかし、ビッグ・オー記法は、アルゴリズムの全体的なパフォーマンスを評価するための指標であり、実際の実行時間や空間利用量とは異なる場合があります。

安定ソート:stable sort と unstable sort

安定ソート(stable sort)とは、ソートアルゴリズムにおいて、同一の値を持つ複数の要素の間の相対的な順序が、ソート前とソート後で変わらないことを保証するソートアルゴリズムのことを指します。逆に、安定ソートでないソートアルゴリズムを不安定ソート(unstable sort)と呼びます。

たとえば、以下のようなデータがあったとします。

[5a, 2, 4, 5b, 1]

ここで、5aと5bは同じ値を表す要素であり、相対的な順序を保つ必要があります。安定ソートを行った場合は、ソート後も5aと5bの間の相対的な順序が変わりません。

一方、不安定ソートを行った場合は、ソートの途中で5aと5bの間の相対的な順序が変わることがあります。たとえば、クイックソートは不安定ソートの一例です。以下の例を見てみましょう。

[5a, 2, 4, 5b, 1]

このデータをクイックソートでソートする場合、最初にpivotとして選ばれた値が5aである場合、以下のようにソートされます。

[2, 4, 1, 5a, 5b]

このソート結果では、5aと5bの間の相対的な順序が変わっています。同様に、5bをpivotとして選んだ場合にも5aと5bの間の相対的な順序が変わることがあります。

安定ソートの例としては、マージソート、バブルソート、挿入ソートなどがあります。不安定ソートの例としては、クイックソート、ヒープソート、選択ソートなどがあります。安定ソートの利点は、同一の値を持つ要素の順序が維持されるため、より精度の高いソート結果を得ることができることです。

2.ソート

重要なものは題目を赤文字で記載していますので、時間がない方はサクッとご覧ください。

bogoソート

bogoソートは、ランダムな順列から始めて、正しい順序になるまで要素をランダムに入れ替えながらソートを行うアルゴリズムです。ランダムに入れ替える操作を繰り返すため、実行時間が非常に長くなることがあります。最悪の場合、実行時間は無限にかかる可能性もあるため、実用的なソートアルゴリズムとは言えません。

Pythonでのbogoソートの実装例を示します。

import random

def is_sorted(lst):
"""
リストがソートされているかどうかを判定する関数
"""
for i in range(len(lst) - 1):
if lst[i] > lst[i + 1]:
return False
return True

def bogo_sort(lst):
"""
bogoソートを実行する関数
"""
while not is_sorted(lst):
random.shuffle(lst)
return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = bogo_sort(lst)
print(sorted_lst)

この実装では、ランダムな順列を作成し、is_sorted関数でソートされているかどうかを判定して、ソートされていなければランダムに入れ替えを行います。is_sorted関数では、リストの隣り合う要素を比較し、昇順になっているかどうかを判定しています。bogo_sort関数は、ソートされたリストを返します。

bubbleソート

bubbleソートは、隣り合う2つの要素を比較し、必要に応じて交換していくことで、小さい値を前方に移動させることを繰り返してソートを行うアルゴリズムです。この操作を繰り返すことで、最大値が配列の最後に移動し、次のステップでは最後尾を除いた部分配列に同様の操作を繰り返すことでソートを完成させます。

以下は、Pythonでのbubbleソートの実装例です。

def bubble_sort(lst):
"""
bubbleソートを実行する関数
"""
n = len(lst)
for i in range(n - 1):
for j in range(n - i - 1):
if lst[j] > lst[j + 1]:
lst[j], lst[j + 1] = lst[j + 1], lst[j]
return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = bubble_sort(lst)
print(sorted_lst)

この実装では、2つのforループを用いて、リストの要素を順番に比較し、必要に応じて交換しています。外側のループは、配列の末尾から順に、すなわち最後には1つの要素が残るまで、処理を繰り返します。内側のループは、現在のステップで比較すべき要素を指定しています。比較する要素のペアを見て、必要に応じて交換していきます。

cocktailソート

cocktailソートは、bubbleソートの改良版で、配列を双方向にスキャンすることで交換回数を減らし、ソートの速度を向上させるアルゴリズムです。bubbleソートと同様に、隣り合う2つの要素を比較し、必要に応じて交換していくことでソートを行いますが、1度のループでは両端から交互に要素を比較していくことが特徴です。

以下は、Pythonでのcocktailソートの実装例です。

def cocktail_sort(lst):
"""
cocktailソートを実行する関数
"""
n = len(lst)
left, right = 0, n - 1
while left < right:
# 右に向かってスキャン
for i in range(left, right):
if lst[i] > lst[i + 1]:
lst[i], lst[i + 1] = lst[i + 1], lst[i]
right -= 1

# 左に向かってスキャン
for i in range(right, left, -1):
if lst[i - 1] > lst[i]:
lst[i], lst[i - 1] = lst[i - 1], lst[i]
left += 1
return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = cocktail_sort(lst)
print(sorted_lst)

この実装では、左右の端から順に要素を比較していき、必要に応じて交換していくことで、ソートを行います。外側のループは、配列の先頭と末尾が交差するまで処理を繰り返します。内側のループは、交互に左右のスキャンを行い、現在の位置と次の位置の要素を比較して必要に応じて交換していきます。このように、双方向にスキャンすることで、要素の比較回数を減らし、ソートの効率を向上させることができます。

comblソート

comblソートは、バブルソートの改良版で、隣り合う要素だけでなく、ある程度離れた要素を比較・交換することで、より効率的にソートを行うアルゴリズムです。bubbleソートと同様に、隣り合う2つの要素を比較し、必要に応じて交換していくことでソートを行いますが、1度のループでのステップ幅を徐々に減らしていくことが特徴です。

以下は、Pythonでのcomblソートの実装例です。

def comb_sort(lst):
"""
comblソートを実行する関数
"""
n = len(lst)
gap = n
shrink = 1.3
sorted_flag = False

while not sorted_flag:
gap = int(gap / shrink)
if gap <= 1:
gap = 1
sorted_flag = True

i = 0
while i + gap < n:
if lst[i] > lst[i + gap]:
lst[i], lst[i + gap] = lst[i + gap], lst[i]
sorted_flag = False
i += 1

return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = comb_sort(lst)
print(sorted_lst)

この実装では、まず初期のステップ幅を配列の長さとし、徐々に縮小していきます。ステップ幅は、縮小率を掛けたものを整数に変換することで求められます。ソート中に要素の交換が行われた場合には、配列がまだソートされていないことを示すフラグを立て、ループを続けます。要素の比較と交換は、現在の位置とステップ幅だけずれた位置にある要素を比較して行います。ステップ幅を徐々に縮小することで、離れた位置にある要素も比較するため、効率的にソートを行うことができます。

selectionソート

selectionソートは、未整列の部分から最小値を選択して、それを整列済み部分の末尾に追加していくことで、ソートを行うアルゴリズムです。以下は、Pythonでのselectionソートの実装例です。

def selection_sort(lst):
"""
selectionソートを実行する関数
"""
n = len(lst)
for i in range(n):
min_idx = i
for j in range(i + 1, n):
if lst[j] < lst[min_idx]:
min_idx = j
lst[i], lst[min_idx] = lst[min_idx], lst[i]

return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = selection_sort(lst)
print(sorted_lst)

この実装では、未整列部分の先頭から最小値を選択するためのループを、配列の長さ分繰り返します。未整列部分の先頭要素を現在の最小値とし、その後ろの要素を順番に比較して、より小さい値があれば最小値を更新します。ループが終了すると、最小値を整列済み部分の末尾に追加し、次のループで未整列部分の先頭を1つ進めて続けます。このように、配列の長さ分のループを繰り返すことで、全体をソートすることができます。

Gnomeソート

Gnomeソートは、要素を1つずつ進めて整列させていくアルゴリズムで、一般的には単純挿入ソートに似た特徴を持ちます。以下は、PythonでのGnomeソートの実装例です。

def gnome_sort(lst):
"""
Gnomeソートを実行する関数
"""
n = len(lst)
i = 0
while i < n:
if i == 0 or lst[i] >= lst[i-1]:
i += 1
else:
lst[i], lst[i-1] = lst[i-1], lst[i]
i -= 1

return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = gnome_sort(lst)
print(sorted_lst)

この実装では、要素を先頭から順番に進めていき、整列されていない要素を前方に移動していくことで、ソートを行います。未整列部分の先頭を表すインデックスiを0で初期化し、配列を先頭から順番に見ていきます。未整列部分の先頭が配列の先頭または、前の要素が現在の要素より大きい場合は、未整列部分の先頭を1つ進めます。そうでない場合は、前の要素と現在の要素を交換し、未整列部分の先頭を1つ戻します。このようにして、整列されていない要素が先頭になるように進めていくことで、配列全体をソートすることができます。

insertionソート

Insertionソートは、整列された部分列を維持しながら、未整列の部分列の要素を正しい位置に挿入していくアルゴリズムです。以下は、PythonでのInsertionソートの実装例です。

def insertion_sort(lst):
"""
Insertionソートを実行する関数
"""
n = len(lst)
for i in range(1, n):
key = lst[i]
j = i - 1
while j >= 0 and lst[j] > key:
lst[j+1] = lst[j]
j -= 1
lst[j+1] = key

return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = insertion_sort(lst)
print(sorted_lst)

この実装では、要素を先頭から順番に進めていき、未整列部分の要素を整列された部分列の適切な位置に挿入していくことで、ソートを行います。未整列部分の先頭を表すインデックスiを1で初期化し、配列を先頭から順番に見ていきます。未整列部分の先頭の要素をkeyに保存し、その前の要素を表すインデックスjを初期値i-1で設定します。jが0以上で、jの要素がkeyより大きい場合は、jの要素を一つ後ろに移動します。これをjが0未満になるか、jの要素がkey以下になるまで繰り返し、keyを挿入するべき位置に挿入します。このようにして、整列された部分列を維持しながら、未整列の部分列を適切に挿入していくことで、配列全体をソートすることができます。

buketソート

Bucketソートは、数列をいくつかのバケット(桶)に分割し、各バケット内でソートを行い、バケットを結合することでソートを行うアルゴリズムです。以下は、PythonでのBucketソートの実装例です。

def bucket_sort(lst, bucket_size=5):
"""
Bucketソートを実行する関数
"""
if len(lst) == 0:
return lst

# 最小値と最大値を求める
min_value = min(lst)
max_value = max(lst)

# バケットの数を決定する
bucket_count = (max_value - min_value) // bucket_size + 1

# バケットを初期化する
buckets = [[] for _ in range(bucket_count)]

# 各要素を適切なバケットに振り分ける
for value in lst:
index = (value - min_value) // bucket_size
buckets[index].append(value)

# 各バケットをソートする
for bucket in buckets:
bucket.sort()

# ソート済みの要素を結合する
sorted_lst = [value for bucket in buckets for value in bucket]

return sorted_lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = bucket_sort(lst)
print(sorted_lst)

この実装では、数列の最小値と最大値を求め、バケットの数を決定します。各要素を適切なバケットに振り分け、各バケットをソートします。最後に、バケットの中身を結合してソート済みのリストを返します。バケットの数やサイズによっては、バケットソートの実行時間が改善されることがあります。しかし、要素の分布が偏っていたり、バケットの数が多すぎる場合は、ソートの効率が悪くなる可能性があることに注意してください。また、要素の種類が多い場合には、バケットの数が膨大になり、メモリの使用量が大きくなるため、注意が必要です。

Shellソート

Shellソートは、挿入ソートを改良したアルゴリズムで、要素を一定の間隔でグループ分けし、各グループで挿入ソートを行い、グループの間隔を狭めていくことで、最終的に全体をソートします。以下は、PythonでのShellソートの実装例です。
def shell_sort(lst):
"""
Shellソートを実行する関数
"""
n = len(lst)
gap = n // 2 # 初期間隔を設定する

# ギャップが0になるまで繰り返す
while gap > 0:
# 各グループについて挿入ソートを実行する
for i in range(gap, n):
temp = lst[i]
j = i
while j >= gap and lst[j - gap] > temp:
lst[j] = lst[j - gap]
j -= gap
lst[j] = temp

gap //= 2 # ギャップを縮小する

return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = shell_sort(lst)
print(sorted_lst)

この実装では、初期間隔を設定し、各グループについて挿入ソートを実行します。ギャップを縮小しながら、グループの間隔を狭めていき、最終的に全体をソートします。グループの間隔の初期値や、狭め方によって、ソートの効率が変わることがあります。Shellソートは、平均的には高速なソートアルゴリズムであり、小規模なリストでも高速にソートできる点が特徴です。しかし、最悪実行時間がO(n^2)であるため、大規模なリストに対しては他のアルゴリズムがより適している場合があります。

Countソート

Countソートは、各要素の出現回数を数え、出現回数に応じて要素を並べ替えるアルゴリズムです。以下は、PythonでのCountソートの実装例です。

def count_sort(lst):
"""
Countソートを実行する関数
"""
# 最大値と最小値を求める
min_value = min(lst)
max_value = max(lst)

# 各要素の出現回数を数える
count = [0] * (max_value - min_value + 1)
for value in lst:
count[value - min_value] += 1

# 各要素の累積和を計算する
for i in range(1, len(count)):
count[i] += count[i - 1]

# ソート済みの要素を格納するリストを初期化する
sorted_lst = [0] * len(lst)

# 各要素をソート済みのリストに格納する
for value in lst:
index = count[value - min_value] - 1
sorted_lst[index] = value
count[value - min_value] -= 1

return sorted_lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = count_sort(lst)
print(sorted_lst)

この実装では、最大値と最小値を求め、各要素の出現回数を数えます。各要素の累積和を計算し、ソート済みの要素を格納するリストを初期化します。各要素をソート済みのリストに格納するときは、各要素の出現回数を使って、ソート済みリストのインデックスを求めます。これにより、出現回数が大きい要素が先頭に、出現回数が小さい要素が後ろに並ぶようにソートされます。Countソートは、各要素の値が整数である場合に高速に動作する点が特徴です。また、出現回数をカウントする配列のサイズが大きくなる場合があるため、注意が必要です。

Radixソート

Radixソートは、各要素の桁数を考慮しながら、桁ごとにソートを行うアルゴリズムです。以下は、PythonでのRadixソートの実装例です。

def radix_sort(lst):
"""
Radixソートを実行する関数
"""
# 最大値と最小値を求める
min_value = min(lst)
max_value = max(lst)

# 各桁についてソートする
exp = 1
while (max_value - min_value) // exp > 0:
# カウント用の配列を初期化する
count = [0] * 10

# 各要素の出現回数を数える
for value in lst:
index = (value - min_value) // exp % 10
count[index] += 1

# 各要素の累積和を計算する
for i in range(1, 10):
count[i] += count[i - 1]

# ソート済みの要素を格納するリストを初期化する
sorted_lst = [0] * len(lst)

# 各要素をソート済みのリストに格納する
for value in reversed(lst):
index = (value - min_value) // exp % 10
sorted_lst[count[index] - 1] = value
count[index] -= 1

# ソート済みのリストを更新する
lst = sorted_lst

# 桁数を更新する
exp *= 10

return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = radix_sort(lst)
print(sorted_lst)

この実装では、最大値と最小値を求め、各桁についてソートを行います。カウント用の配列を初期化し、各要素の出現回数を数えます。各要素の累積和を計算し、ソート済みの要素を格納するリストを初期化します。各要素をソート済みのリストに格納するときは、各要素の桁数を使って、ソート済みリストのインデックスを求めます。これにより、各桁について小さい数から順にソートされます。Radixソートは、各要素の値が整数で、桁数が一定の場合に高速に動作する点が特徴です。また、桁数に応じてソートを行うため、他のアルゴリズムよりも安定した実行時間を持ちます。ただし、桁数が多い場合や、要素の種類が多い場合は、ソートの効率が悪くなる可能性があります。

Quickソート

Quickソートは、分割統治法を用いた高速なソートアルゴリズムで、以下の手順で実行されます。

  • ピボットを選択する。一般的に、リストの先頭や末尾、中央などを選びます。
  • ピボットより小さい要素と大きい要素に分割する。
  • 分割された2つの部分リストに対して、再帰的にQuickソートを実行する。

以下は、PythonでのQuickソートの実装例です。

def quick_sort(lst):
"""
Quickソートを実行する関数
"""
if len(lst) <= 1:
return lst

# ピボットを選択する
pivot = lst[0]

# ピボットより小さい要素と大きい要素に分割する
left_lst = []
right_lst = []
for value in lst[1:]:
if value < pivot:
left_lst.append(value)
else:
right_lst.append(value)

# 分割された部分リストに対して、再帰的にQuickソートを実行する
sorted_left_lst = quick_sort(left_lst)
sorted_right_lst = quick_sort(right_lst)

# ソート済みの部分リストを結合する
sorted_lst = sorted_left_lst + [pivot] + sorted_right_lst

return sorted_lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = quick_sort(lst)
print(sorted_lst)

この実装では、リストが1以下の場合はそのまま返します。リストの先頭をピボットとして選択し、ピボットより小さい要素と大きい要素に分割します。分割された部分リストに対して、再帰的にQuickソートを実行します。最後に、ソート済みの部分リストを結合して、全体をソートします。ピボットの選び方によって、ソートの効率が変わることがあります。また、最悪実行時間がO(n^2)であるため、要素の分布によっては他のアルゴリズムがより適している場合があります。しかし、一般的には高速なソートアルゴリズムであるとされています。

Mergeソート

Mergeソートは、分割統治法を用いた安定なソートアルゴリズムで、以下の手順で実行されます。

  • リストを2つの部分リストに分割する。
  • 各部分リストに対して、再帰的にMergeソートを実行する。
  • 分割された2つの部分リストをマージする。マージの際には、2つの部分リストの先頭から小さい値を選んで、新しいリストに追加していく。

以下は、PythonでのMergeソートの実装例です。

def merge_sort(lst):
"""
Mergeソートを実行する関数
"""
if len(lst) <= 1:
return lst

# リストを2つの部分リストに分割する
mid = len(lst) // 2
left_lst = lst[:mid]
right_lst = lst[mid:]

# 各部分リストに対して、再帰的にMergeソートを実行する
sorted_left_lst = merge_sort(left_lst)
sorted_right_lst = merge_sort(right_lst)

# 分割された2つの部分リストをマージする
sorted_lst = []
i = j = 0
while i < len(sorted_left_lst) and j < len(sorted_right_lst):
if sorted_left_lst[i] < sorted_right_lst[j]:
sorted_lst.append(sorted_left_lst[i])
i += 1
else:
sorted_lst.append(sorted_right_lst[j])
j += 1

# 余った要素を追加する
sorted_lst.extend(sorted_left_lst[i:])
sorted_lst.extend(sorted_right_lst[j:])

return sorted_lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = merge_sort(lst)
print(sorted_lst)

この実装では、リストが1以下の場合はそのまま返します。リストを2つの部分リストに分割し、各部分リストに対して再帰的にMergeソートを実行します。最後に、分割された2つの部分リストをマージして、全体をソートします。マージの際には、2つの部分リストの先頭から小さい値を選んで、新しいリストに追加していきます。Mergeソートは、安定なソートアルゴリズムであるため、要素の順序が保たれるという点が特徴です。また、再帰的な処理を行うため、スタック領域を多く必要とすることがあります。しかし、一般的には高速なソートアルゴリズムであるとされています。

Heapソート

Heapソートは、完全二分木を用いたソートアルゴリズムで、以下の手順で実行されます。

  • リストをヒープに変換する。ここでは、最大ヒープを用いるものとする。
  • ヒープから要素を取り出して、ソート済みのリストに追加していく。取り出した要素は、ヒープの最後尾の要素と交換する。
  • ヒープを再構築し、次の要素を取り出す。

以下は、PythonでのHeapソートの実装例です。

def heap_sort(lst):
"""
Heapソートを実行する関数
"""
# リストをヒープに変換する
def heapify(lst, index, heap_size):
largest = index
left = 2 * index + 1
right = 2 * index + 2

if left < heap_size and lst[left] > lst[largest]:
largest = left

if right < heap_size and lst[right] > lst[largest]:
largest = right

if largest != index:
lst[index], lst[largest] = lst[largest], lst[index]
heapify(lst, largest, heap_size)

n = len(lst)
for i in range(n // 2 - 1, -1, -1):
heapify(lst, i, n)

# ヒープから要素を取り出して、ソート済みのリストに追加する
for i in range(n - 1, 0, -1):
lst[0], lst[i] = lst[i], lst[0]
heapify(lst, 0, i)

return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = heap_sort(lst)
print(sorted_lst)

この実装では、リストを最大ヒープに変換し、ヒープから要素を取り出してソート済みのリストに追加していきます。取り出した要素は、ヒープの最後尾の要素と交換しておくことで、ヒープのサイズを小さくしていきます。ヒープを再構築する際には、最大ヒープを維持するように要素を並び替えます。Heapソートは、比較的簡単な実装でありながら、一般的には高速なソートアルゴリズムであるとされています。しかし、データの状態によっては、他のアルゴリズムよりも効率が悪い場合があることに注意が必要です。

Timソート

Timソートは、MergeソートとInsertionソートを組み合わせたソートアルゴリズムで、以下の手順で実行されます。

  • リストを小さな塊に分割する。ここでは、サイズが32以下の塊に分割する。
  • 各塊に対してInsertionソートを実行する。
  • ソートされた塊をマージして、より大きな塊にする。塊のサイズを2倍にしていく。
  • 最後に、全体をMergeソートでソートする。

以下は、PythonでのTimソートの実装例です。

def tim_sort(lst):
"""
Timソートを実行する関数
"""
def insertion_sort(lst, left, right):
for i in range(left + 1, right + 1):
value = lst[i]
j = i - 1
while j >= left and lst[j] > value:
lst[j + 1] = lst[j]
j -= 1
lst[j + 1] = value

def merge(lst, left, mid, right):
lst1 = lst[left:mid + 1]
lst2 = lst[mid + 1:right + 1]
i = j = 0
k = left
while i < len(lst1) and j < len(lst2):
if lst1[i] < lst2[j]:
lst[k] = lst1[i]
i += 1
else:
lst[k] = lst2[j]
j += 1
k += 1

while i < len(lst1):
lst[k] = lst1[i]
i += 1
k += 1

while j < len(lst2):
lst[k] = lst2[j]
j += 1
k += 1

n = len(lst)
min_run = 32
for i in range(0, n, min_run):
insertion_sort(lst, i, min(i + min_run - 1, n - 1))

size = min_run
while size < n:
for left in range(0, n, 2 * size):
mid = left + size - 1
right = min(left + 2 * size - 1, n - 1)
merge(lst, left, mid, right)
size *= 2

return lst

# 実行例
lst = [5, 2, 4, 6, 1, 3]
sorted_lst = tim_sort(lst)
print(sorted_lst)

この実装では、リストを小さな塊に分割し、各塊に対してInsertionソートを実行します。次に、ソートされた塊をマージして、より大きな塊にしていきます。塊のサイズを2倍にしていくことで、最終的には全体をMergeソートでソートします。Timソートは、平均実行時間がO(n log n)であり、安定なソートアルゴリズムであるため、一般的には安定なソートアルゴリズムは、ソート前後で同じ値を持つ要素の順序が保たれるため、多くの場面で重要な役割を果たします。

例えば、従業員の情報を名前でソートする際に、同じ名前を持つ従業員が複数いる場合、ソート後も元の順序が保たれている必要があります。

また、一般的に採用される理由としては、安定なソートアルゴリズムは、データ構造を変更する必要がないため、オブジェクトのコピーが必要ない場合や、データ構造が大きくない場合に、効率的であることが挙げられます。また、安定なソートアルゴリズムは、同じデータに対して何度もソートする必要がある場合に、都度結果が同じになるため、信頼性が高いという利点もあります。

したがって、多くの場合、安定なソートアルゴリズムが好まれる傾向があります。しかし、安定なソートアルゴリズムが必ずしも最適な場合でもないことに注意が必要です。例えば、処理するデータが非常に大きく、安定なソートアルゴリズムでは処理時間が長くなる場合には、安定性を犠牲にしてでも処理速度を優先することがあります。

3.バイナリサーチ

バイナリサーチは、ソート済みのリストに対して、目的の要素を高速に検索するアルゴリズムです。以下は、Pythonでのバイナリサーチの実装例です。

def binary_search(lst, target):
"""
バイナリサーチを実行する関数
"""
left, right = 0, len(lst) - 1
while left <= right:
mid = (left + right) // 2
if lst[mid] == target:
return mid
elif lst[mid] < target:
left = mid + 1
else:
right = mid - 1

return -1

# 実行例
lst = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
target = 7
index = binary_search(lst, target)
if index == -1:
print("Not found")
else:
print(f"Found at index {index}")

この実装では、リストを対象とするバイナリサーチを行っています。リストの先頭と末尾を指すleftとrightを初期化し、リストの中央のインデックスを指すmidを計算します。midが目的の要素であれば、そのインデックスを返します。midが目的の要素よりも小さい場合は、リストの左半分に目的の要素は存在しないため、leftをmid + 1に更新します。midが目的の要素よりも大きい場合は、リストの右半分に目的の要素は存在しないため、rightをmid – 1に更新します。これを、leftがrightよりも大きくなるまで繰り返します。最終的に、目的の要素が見つかれば、そのインデックスを返します。目的の要素が見つからなければ、-1を返します。

バイナリサーチは、要素数が多いリストでも高速に目的の要素を検索できるため、一般的には効率的なアルゴリズムとされています。ただし、リストがソートされていない場合には、正しい検索結果を得られない可能性があるため、事前にソートが必要です。


というわけで、今回は以上です。大変大変お疲れ様でした。
引き続きで、徐々に発信していきます。

コメントや感想を受け付けています。ちょっとした感想でもいいので嬉しいです。

それでは、以上です。

最新情報をチェックしよう!