再帰と分割統治法を用いたソートと選択アルゴリズムの実験

実験内容

1. ソートアルゴリズム

マージソート、クイックソート、ヒープソートを実装します。入力サイズ N は 8, 16, 32, 64, 128, 256, 512, … とし、1-1000 のランダムな整数を生成して入力データとします。実験結果を記録し、実行時間と入力サイズの関係をグラフにプロットします。各アルゴリズムの時間計算量と空間計算量を説明し、グラフを基に3つのソートアルゴリズムの優劣を比較します。

2. データ検索

ランダムな整数の配列を生成します。配列サイズ N は 8, 16, 32, 64, 128, 256, 512, … とし、配列内のデータは 1-1000 のランダムな整数です。少なくとも2つのアルゴリズムを用いて配列の第 i 小要素を求めます。i はユーザーが入力します。グラフを作成し、横軸をデータサイズ、縦軸を時間とします。設計したアルゴリズムの時間計算量と実際の実行状況を分析します。設計した2つのアルゴリズムのうち、少なくとも一つは平均時間計算量が O(nlogn) 未満である必要があります。

ソートアルゴリズムの実装

以下に、3つのソートアルゴリズムの C++ 実装を示します。

マージソート

#include <iostream>
#include <vector>
#include <ctime>
#include <algorithm>

using namespace std;

void merge(vector<int>& data, vector<int>& buffer, int left, int mid, int right) {
    int i = left, j = mid + 1, k = left;
    // 2つの部分配列をマージ
    while (i <= mid && j <= right) {
        if (data[i] <= data[j]) {
            buffer[k++] = data[i++];
        } else {
            buffer[k++] = data[j++];
        }
    }
    // 残りの要素をコピー
    while (i <= mid) {
        buffer[k++] = data[i++];
    }
    while (j <= right) {
        buffer[k++] = data[j++];
    }
    // 元の配列に戻す
    for (int idx = left; idx <= right; ++idx) {
        data[idx] = buffer[idx];
    }
}

void mergeSort(vector<int>& data, vector<int>& buffer, int left, int right) {
    if (left >= right) return;
    int mid = left + (right - left) / 2;
    mergeSort(data, buffer, left, mid);
    mergeSort(data, buffer, mid + 1, right);
    merge(data, buffer, left, mid, right);
}

void performMergeSort(vector<int>& data) {
    vector<int> buffer(data.size());
    mergeSort(data, buffer, 0, data.size() - 1);
}

クイックソート

int partition(vector<int>& data, int left, int right) {
    int pivot = data[right];
    int i = left - 1;
    for (int j = left; j < right; ++j) {
        if (data[j] <= pivot) {
            ++i;
            swap(data[i], data[j]);
        }
    }
    swap(data[i + 1], data[right]);
    return i + 1;
}

void quickSort(vector<int>& data, int left, int right) {
    if (left >= right) return;
    int pivotIndex = partition(data, left, right);
    quickSort(data, left, pivotIndex - 1);
    quickSort(data, pivotIndex + 1, right);
}

ヒープソート

void heapify(vector<int>& data, int n, int i) {
    int largest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;

    if (left < n && data[left] > data[largest]) {
        largest = left;
    }
    if (right < n && data[right] > data[largest]) {
        largest = right;
    }
    if (largest != i) {
        swap(data[i], data[largest]);
        heapify(data, n, largest);
    }
}

void heapSort(vector<int>& data) {
    int n = data.size();
    // ヒープを構築
    for (int i = n / 2 - 1; i >= 0; --i) {
        heapify(data, n, i);
    }
    // ヒープから要素を1つずつ取り出す
    for (int i = n - 1; i > 0; --i) {
        swap(data[0], data[i]);
        heapify(data, i, 0);
    }
}

データ検索アルゴリズムの実装

以下に、2つのデータ検索アルゴリズムの C++ 実装を示します。

ソート後に検索する方法

この方法は、まずクイックソートで配列をソートし、その後、インデックスを用いて目的の要素を直接取得します。全体の時間計算量は O(nlogn) となります。

直接検索する方法(Quick Select)

この方法は、分割統治法を用いて配列を分割します。ピボットを基準に、左部分 `A[p, q-1]` の要素はすべて `q` 未満、右部分 `A[q+1, r]` の要素はすべて `q` より大きくなります。これにより、目的の位置 `i` がどちらの部分にあるかを判断し、その部分に対して再帰的に検索を続けます。平均的な時間計算量は O(n) となります。

int partitionForSelect(vector<int>& data, int left, int right) {
    int pivot = data[right];
    int i = left;
    for (int j = left; j < right; ++j) {
        if (data[j] <= pivot) {
            swap(data[i], data[j]);
            ++i;
        }
    }
    swap(data[i], data[right]);
    return i;
}

int quickSelect(vector<int>& data, int left, int right, int k) {
    if (left == right) {
        return data[left];
    }
    int pivotIndex = partitionForSelect(data, left, right);
    int rank = pivotIndex - left + 1;
    if (k == rank) {
        return data[pivotIndex];
    } else if (k < rank) {
        return quickSelect(data, left, pivotIndex - 1, k);
    } else {
        return quickSelect(data, pivotIndex + 1, right, k - rank);
    }
}

タグ: C++ マージソート クイックソート ヒープソート 分割統治法

7月22日 19:39 投稿