【データ構造とアルゴリズムC++実装】 3. ソートアルゴリズム

元のビデオはZuo ChengyunのBステーションの指導です



以下のすべての swap() 関数について、関数は次のように定義されます。

void swap(int& a, int& b)
{
    
    
	int t = a;
	a = b;
	b = t;
}
// 也可以用异或,但不能传入同一个变量,可以是不同变量相同值
void swap(int& a, int& b)
{
    
    
	a = a ^ b;
	b = a ^ b;
	a = a ^ b;
}

1 バブルソートO ( N 2 ) O(N^2)O ( N2 )(データの状態に関係なく)

最も基本的でシンプルな並べ替え、言うことなし
画像の説明を追加してください

#incldue <vector>
void bubbleSort(std::vector<int>& arr) 
{
    
    
    for (int i = 0; i < arr.size() - 1; i++) {
    
    
        for (int j = 0; j < arr.size() - 1 - i; j++) {
    
    
            if (arr[j] > arr[j+1]) {
    
            // 相邻元素两两对比
				swap(arr[j], arr[j+1]);
            }
        }
    }
}

2 選択ソートO ( N 2 ) O(N^2)O ( N2 )(データの状態に関係なく)

基本的な考え方は、毎回ソートする要素から最小 (または最大) の要素を選択し、それをソートされたシーケンスの最後に置くことです。選択ソートの主な手順は次のとおりです。

  • 1. ソートするシーケンスをトラバースし、現在位置を最小値の位置として設定します。
  • 2. 現在の位置から開始して、現在の要素と次の要素を順番に比較し、最小の要素を見つけて、その位置を記録します。
  • 3. 最小の要素を現在の位置の要素と交換します。
  • 4. シーケンス全体が完了するまで、ステップ 2 と 3 を繰り返します。
    画像の説明を追加してください
#include<vector>
void selectionSort(std::vector<int>& arr)
{
    
    
	int len = arr.size();
	if (arr.empty() || len < 2) return;

	for (int i = 0; i < len -1; i++){
    
     // len -1 位置不用再循环了,因为最后剩一个必然是最值
		int minIndex = i;		// 在i ~ len-1 上寻找最小值的下标
		for (int j = i; j < len - 1; j++){
    
    
			minIndex = arr[j] < arr[minIndex] ? j : minIndex;
		}
		swap(arr[minIndex], arr[i]);
	}
}

3 挿入ソートO ( N 2 ) O(N^2)O ( N2 )(データステータスとの関係)

この原理は、ポーカーの役を分類するプロセスに似ています。ソートされていない要素を、ソートされた部分の適切な位置に 1 つずつ挿入します。

論理的な順序は、最初に 0 ~ 0 を規則正しくし、次に 0 ~ 1 を規則的にし、0 ~ 2 ... 0 ~ n-1 を規則的にします。

アルゴリズムの流れは次のとおりです。

  • 1. 最初の要素から始めて、要素はソートされていると見なされます。
  • 2. 未ソート部分から最初の値を取得し、それをソート済み部分の適切な位置に挿入します。この位置の選択方法は、ソートされていない現在の値と左隣の値を比較し、小さい場合は位置を交換します境界に遭遇するか、左隣の値以上になるまで終了します。(内側のループ: 挿入する適切な位置を見つけます)
  • 3. すべての要素が並べ替えられたシーケンスに挿入されるまで、手順 2 を繰り返します。(外側のループ: ソートされていない各数値を走査します)

バブル ソートや選択ソートとは異なり、これは固定操作です (データのステータスは無関係です)。挿入ソートでは、順序が与えられていれば、外側のループは毎回完了するので、 O(N)になりますが、時間計算量は最悪の場合であるため、それでもO ( N 2 ) Oになります。 (N^2)O ( N2 )

画像の説明を追加してください

#include <vector>
void insertionSort(std::vector<int>& arr)
{
    
    
	int len = arr.size();
	if (arr.empty() || len < 2) return;
	// 0~0已经有序了
	for (int i = 1; i < len; i++){
    
     	// 0~i做到有序
		// 把当前值(j+1)对比左邻值(j),比他小则交换,不满足循环条件则说明找到合适位置了
		for (int j = i - 1; j >= 0 && arr[j] > arr[j + 1]; j--){
    
     
			swap(arr[j+1], arr[j]);
		}
	}
}

内側のループでは、現在の要素 arr[j + 1] とその左隣の要素 arr[j] を比較し、適切な挿入位置が見つかるまで、その要素の方が小さい場合は転置します。
ループの終了 (適切な位置の検索) の条件は、左の境界に到達するか、現在の値が左の境界の値より大きいです。

引き続き下を見てください。最初に二分探索と基本的な再帰をマスターすることをお勧めします

4 マージソートO ( N log N ) O(NlogN)O ( Nl o g N ) (データの状態は関係ありません)

その中心となるアイデアは、ソート対象のシーケンスを、各サブシーケンスの要素が 1 つだけになるまで小さなサブシーケンスに分割し、シーケンス全体が最終的にソートされるまでこれらのサブシーケンスをペアでマージすることです。

マージソートの一般的な手順は次のとおりです。

  • セグメンテーション: ソート対象のシーケンスを中間位置から 2 つのサブシーケンスに分割し、各サブシーケンスに要素が 1 つだけ残るまで各サブシーケンスを再帰的に分割し続けます。
  • Merge : 2 つのソートされたサブシーケンスを 1 つのソートされたシーケンスにマージします。2 つのサブシーケンスの最初の要素から比較を開始し、小さい要素を一時配列に配置し、一方のサブシーケンスのすべての要素が一時配列に配置されるまで、対応するサブシーケンスのインデックスを 1 ビット後方に移動します。次に、他のサブシーケンスの残りの要素を一時配列に直接入れます。
  • 繰り返しマージ: すべてのサブシーケンスが順序付けられたシーケンスにマージされるまで、マージ操作が繰り返されます。

データに関係なく、時間計算量は常にO(NlogN)です。以前のバブリング、選択、挿入ソートよりも高速ですが、空間の複雑さはO(N)です。

次の図は、このプロセスを非常に正確に示しています。画像の説明を追加してください

#include <vector>
// 第二阶段:merge阶段时,L~M 和 M~R 之间的数必然有序
void merge(std::vector<int>& arr, int L, int M, int R)
{
    
    
	std::vector help(R - L + 1);
	int i = 0;		// 临时数组的起始索引
	int p1 = L;		// 左半部分的起始索引
	int p2 = M + 1;	// 右半部分的起始索引

	//外排序,把两个子数组中的元素从小到大放到help数组
	while (p1 <= M && p2 <= R){
    
     
		help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
	}

 	// 如果左边部分还有剩的,全部依次加到help的末尾
	while (p1 <= M){
    
    
		help[i++] = arr[p1++];
	}
	// 同理如果右边还有剩的
	while (p2 <= R){
    
     
		help[i++] = arr[p2++];
	}

	// 结束,把临时数组的内容覆盖到原数组中
	for (i = 0; i < R - L + 1; i++){
    
    
		arr[L + i] = help[i]; // 注意arr从L位置开始写入的
	}
}

// 务必先看这个函数 这算是第一阶段拆分
void mergeSort(std::vector<int>& arr, int L, int R)
{
    
    
	if (L == R) return;	// 拆分完毕,不能再拆了
	
	int mid = L + ((R - L) >> 1);
	mergeSort(arr, L, mid);
	mergeSort(arr, mid+1, R);
	merge(arr, L, mid, R); // 执行到这一步的条件是 L == mid 且 mid+1 == R 即左右子树都已二分到只有一个元素
}

時間計算量 ( 2.1 マスター公式を参照)

  • T ( N ) = 2 ⋅ T ( N 2 ) + O ( N ) T(N) = 2・T(\frac{N}{2})+O(N)T ( N )=2 T (2N)+O ( N )
  • a = 2; b = 2; d = 1
  • logab = 1 = d log_ab=1=dログ_ _b=1=dであるため、時間計算量はO ( N ⋅ log N ) O(N · logN)O ( N ログN ) _ _

空間の複雑さ: O ( N ) O(N)()マージによってスペースが開かれるたびに、サイズは N になるためです。

4.1 マージソートの拡張(配列の小さい和を求める)

トピック: 配列では、現在の数値より小さい各数値の左側にある数値の合計を配列の小さい合計と呼びます。配列の最小合計を求めます。

例: [1,3,4,2,5]
1 の左側の数字は 1 より小さい、なし、
3 の左側の数字は 3 より小さい、1、4
の左側の数字は小さい4, 1, 3 より小さい;
2 の左側の数字は 2 より小さい 数字 1;
5 の左側の数字は 5, 1, 3, 4, 2 より小さい;
したがって、小さい和は 1+1 です+3+1+1+3+4+2=16
時間計算量O(NlogN)、空間計算量O(N)が必要

各位置の要素が左側の要素をすべて巡って、それより小さい数を見つけて合計すると、結果は簡単に得られ、時間計算量は O ( N 2 ) O(N^2) になりますO ( N2 )

考え方を変えます。左側にある現在位置の数値より小さい数値がいくつあるかを見つけて合計するのではなく、現在位置の数値より大きい数値がいくつあるかを数え、現在の数値の何倍になるべきかを計算します。蓄積されており、この処理はマージソートの外側のソートで行うことができます

ビデオの詳細な説明と正確な空挺、テキストを読むよりはるかに優れています

#include <iostream>
#include <vector>

long long mergeAndCount(std::vector<int>& arr, int left, int mid, int right) 
{
    
    
    std::vector<int> temp(right - left + 1); // 临时数组用于存储合并后的结果
    int i = 0; // 临时数组的起始索引
    int p1 = left; // 左半部分的起始索引
    int p2 = mid + 1; // 右半部分的起始索引
    long long count = 0; // 记录小和的累加和

    while (p1 <= mid && p2 <= right) {
    
    
        if (arr[p1] <= arr[p2]) {
    
    
            // 重点:如果arr[p1]比arr[p2]小,则arr[p1]比p2后面所有数都小!
            count += arr[p1] * (right - p2 + 1);
            temp[i++] = arr[p1++];
        } else {
    
    
            temp[i++] = arr[p2++];
        }
    }

    while (p1 <= mid) temp[i++] = arr[p1++];
    while (p2 <= right) temp[i++] = arr[p2++];

    // 将临时数组的结果复制回原数组
    for (i = 0; i < R - L + 1; i++) {
    
    
        arr[left + i] = temp[i];
    }

    return count;
}

long long mergeSortAndCount(std::vector<int>& arr, int left, int right) 
{
    
    
    if (left >= right) {
    
    
        return 0; // 单个元素无小和
    }

    int mid = left + (right - left) / 2;
    long long leftCount = mergeSortAndCount(arr, left, mid); // 左半部分的小和
    long long rightCount = mergeSortAndCount(arr, mid + 1, right); // 右半部分的小和
    long long mergeCount = mergeAndCount(arr, left, mid, right); // 合并过程中的小和

    return leftCount + rightCount + mergeCount; // 总的小和
}

long long calculateSmallSum(std::vector<int>& arr) 
{
    
    
    if (arr.empty()) return 0; // 空数组无小和

    int left = 0;
    int right = arr.size() - 1;
    return mergeSortAndCount(arr, left, right);
}

int main() 
{
    
    
    std::vector<int> arr = {
    
    3, 1, 4, 2, 5};
    long long smallSum = calculateSmallSum(arr);
    std::cout << "Small Sum: " << smallSum << std::endl;
	
	std::cin.get();
    return 0;
}

5 クイックソート

クイックソート 3.0 (1.0 および 2.0 については別の場所を参照)

おすすめ

転載: blog.csdn.net/Motarookie/article/details/131383596