C++ 數據結構(查找與排序)

冒泡排序 Bubble Sort


簡明解釋

通過依次比較、交換相鄰的元素大小(按照由小到大的順序,如果符合這個順序就不用交換)。1 次這樣的循環可以得到一個最大值,n - 1 次這樣的循環可以排序完畢

屬性

  • 穩定
  • 時間複雜度 O(n²)
  • 交換 O(n²)
  • 對即將排序完成的數組進行排序 O(n)(但是這種情況下不如插入排序塊,請繼續看下文)

核心概念

  • 利用交換,將最大的數冒泡到最後

基本實現

首先定義一個數組,並求出數組長度,將這兩個以參數形式傳入函數實現

int arr[]     = {1, 5, 4, 2, 7, 8, 9, 3, 6};
size_t length = end(arr) - begin(arr);
cout << "冒泡排序:" << endl;

BubbleSort(arr, length);
for (auto iter : arr) {
    cout << iter << " ";
}

實現:

void BubbleSort(int *p, size_t length) {
    for (auto i = 0; i < length; ++i) {
        for (auto j = 0; j < length - i - 1; ++j) {
            swap(p[j], p[j + 1]);
        }
    }
}

加入我有一個需求,以一個函數指針爲參數,用以實現升序或降序的需求呢

BubbleSort(arr, length, [](int a, int b) { return b - a; });
void BubbleSort(int *p, size_t length, function<int(int lhs, int rhs)> compareFunc) {
    for (auto i = 0; i < length; ++i) {
        for (auto j = 0; j < length - i - 1; ++j) {
            if (compareFunc(p[j], p[j + 1]) > 0) {
                swap(p[j], p[j + 1]);
            }
        }
    }
}

調試輸出:

 

選擇排序 Selection Sort


簡明解釋

每一次內循環遍歷尋找最小的數,記錄下 minIndex,並在這次內循環結束後交換 minIndex 和 i 的位置

重複這樣的循環 n - 1 次即得到結果。

屬性

  • 不穩定
  • Θ(n²) 無論什麼輸入,均爲 Θ(n²)
  • Θ(n) 交換注意,這裏只有 n 次的交換,選擇排序的唯一優點*

核心概念

  • “可預測”的時間複雜度,什麼進來都是 O(n²),但不穩定,唯一的優點是減少了 swap 次數

基本實現

void SelectionSort(int *p, size_t length) {
    for (auto i = 0; i < length - 1; ++i) {
        int min = i;
        for (auto j = i + 1; j < length; ++j) {
            if (p[min] > p[j]) {
                min = j;
            }
        }
        if (min != i) {
            swap(p[i], p[min]);
        }
    }
}

 

插入排序 Insertion Sort


默認 a[0] 爲已排序數組中的元素arr[1] 開始逐漸往已排序數組中插入元素從後往前一個個比較,如果待插入元素小於已排序元素,則已排序元素往後移動一位,直到待插入元素找到合適的位置並插入已排序數組。

經過 n - 1 次這樣的循環插入後排序完畢。

屬性

  • 穩定
  • 適合場景:對快要排序完成的數組時間複雜度爲 O(n)
  • 非常低的開銷
  • 時間複雜度 O(n²)

由於它的優點(自適應,低開銷,穩定,幾乎排序時的O(n)時間),插入排序通常用作遞歸基本情況(當問題規模較小時)針對較高開銷分而治之排序算法, 如希爾排序快速排序

核心概念

  • 高性能(特別是接近排序完畢時的數組),低開銷,且穩定
  • 利用二分查找來優化

基本實現

void InsertSort(int *p, int *begin, int *end) {
    for (auto iter = begin + 1; iter != end; ++iter) {
        const int tmp = *iter;
        auto index    = iter - begin;
        auto j        = index;
        while (j > 0 && tmp > p[j - 1]) {
            p[j] = p[j - 1];
            --j;
        }
        if (j != index) {
            p[j] = tmp;
        }
    }
}

代碼可能比for i循環類型的難理解,原理一樣,也可以利用其它語言的語法糖。

 

希爾排序 Shell Sort


簡明解釋

希爾排序是插入排序的改進版,它克服了插入排序只能移動一個相鄰位置的缺陷(希爾排序可以一次移動 gap 個距離),利用了插入排序在排序幾乎已經排序好的數組的非常快的優點

使用可以動態定義的 gap 來漸進式排序,先排序距離較遠的元素,再逐漸遞進,而實際上排序中元素最終位置距離初始位置遠的概率是很大的,所以希爾排序大大提升了性能(尤其是 reverse 的時候非常快,想象一下這時候冒泡排序和插入排序的速度)。

而且希爾排序不僅效率較高(比冒泡和插入高),它的代碼相對要簡短,低開銷(繼承插入排序的優點),追求這些特點(效率要求過得去就好,代碼簡短,開銷低,且數據量較小)的時候希爾排序是好的 O(n·log(n)) 算法的替代品

總而言之:希爾排序的性能優化來自增量隊列的輸入gap 的設定

屬性

  • 不穩定
  • 在快要排序完成的數組有 O(n·log(n)) 的時間複雜度(並且它對於反轉數組的速度非常快)
  • O(n^3/2) time as shown 

關於不穩定:

我們知道, 單次直接插入排序是穩定的,它不會改變相同元素之間的相對順序,但在多次不同的插入排序過程中, 相同的元素可能在各自的插入排序中移動,可能導致相同元素相對順序發生變化。因此, 希爾排序並不穩定

關於 worse-case time 有一點複雜:

The worse-case time complexity of shell sort depends on the increment sequence. For the increments 1 4 13 40 121…, which is what is used here, the time complexity is O(n3/2). For other increments, time complexity is known to be O(n4/3) and even O(n·log2(n)).

核心概念

希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  1. 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到 O(n) 的效率
  2. 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位 ;

其中 gap(增量)的選擇是希爾排序的重要部分。只要最終 gap 爲 1 任何 gap 序列都可以工作。算法最開始以一定的 gap 進行排序。然後會繼續以一定 gap 進行排序,直到 gap = 1 時,算法變爲插入排序

Donald Shell 最初建議 gap 選擇爲 n / 2 並且對 gap 取半直到 gap 達到 1 。雖然這樣取可以比 O(n²) 類的算法(插入排序、冒泡排序)更好,但這樣仍然有減少平均時間和最差時間的餘地。

void ShellSort(int *arr, size_t len) {
    int gap = 1;
    while (gap < len) {
        gap = gap * 3 + 1;
    }
    while (gap > 0) {
        for (auto i = gap; i < len; ++i) {
            auto idx = i - gap;
            auto tmp = arr[i];
            while (idx >= 0 && tmp < arr[idx]) {
                arr[idx + gap] = arr[idx];
                idx -= gap;
            }
            arr[idx + gap] = tmp;
        }
        gap = (int)floor(gap / 3);
    }
}

快速排序 Quick Sort


簡明解釋

  1. 從數列中挑出一個元素,稱爲"基準"(pivot),
  2. 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(相同的數可以到任何一邊)。在這個分區結束之後,該基準就處於數列的中間位置。這個稱爲分區(partition)操作
  3. 遞歸地(recursively)把小於基準值元素的子數列和大於基準值元素的子數列排序。

屬性

  • 不穩定
  • O(n²) time, 但是通常都是 O(n·log(n)) time (或者更快)
  • O(log(n)) extra space

When implemented well, it can be about two or three times faster than its main competitors, merge sort and heap sort

核心概念

  • 使用了分而治之的思想

基本實現

    cout << "\n快速排序:" << endl;
    QuickSort(arr, 0, end(arr) - begin(arr) - 1);
    for (auto iter : arr) {
        cout << iter << " ";
    }
int Partition(int *p, int l, int r) {
    auto x = p[l];
    while (l < r) {
        while (l < r && p[r] >= x) // 從右向左找第一個小於x的數
            r--;
        if (l < r)
            p[l++] = p[r];
        while (l < r && p[l] < x) // 從左向右找第一個大於等於x的數
            l++;
        if (l < r)
            p[r--] = p[l];
    }
    p[l] = x;
    return l;
}

void QuickSort(int *p, int l, int r) {
    auto position = 0;
    if (l < r) {
        position = Partition(p, l, r); //返回劃分元素的最終位置
        QuickSort(p, l, position - 1); //劃分左邊遞歸
        QuickSort(p, position + 1, r); //劃分右邊遞歸
    }
}

二分查找 BinarySearch


這裏補充一下二分查找的算法的實現。

核心概念是:折半

//調用處
cout << "\n二分查找:" << endl;
auto index = BinarySearch(arr, length, 9);
cout << "\n二分查找結果:" << index << endl;
//定義

int BinarySearch(int p[] /*此處int *p 也可以*/, size_t len, int target) {
    int min = 0;
    int max = static_cast<int>(len - 1);
    while (min <= max) {
        const auto mid = (min + max) / 2;
        cout << "  find index: " << p[mid] << endl;
        if (target == p[mid]) {
            cout << "  target finded ! " << p[mid] << endl;
            return mid;
        } else if (target < p[mid]) {
            max = mid - 1;
        } else if (target > p[mid]) {
            min = mid + 1;
        }
    }
    cout << "  target not found" << endl;
    return 0;
}

總結


  • 數據幾乎快排序完成時?

插入排序不解釋

  • 數據量小,對效率要求不高,代碼簡單時?

性能大小:希爾排序 > 插入排序 > 冒泡排序 > 選擇排序

  • 數據量大,要求穩定的效率(不會像快速排序一樣有 O(n²) 的情況)(如數據庫中)?

堆排序

  • 數據量大,要求效率高,而且要穩定?

歸併排序

  • 數據量大,要求最好的平均效率?

性能大小:快速排序 > 堆排序 > 歸併排序

因爲雖然堆排序做到了 O(n·log(n),而快速排序的最差情況是 O(n²),但是快速排序的絕大部分時間的效率比 O(n·log(n) 還要快,所以快速排序真的無愧於它的名字。(十分快速)

  • 選擇排序絕對沒用嗎?

選擇排序只需要 O(n) 次交換,這一點它完爆冒泡排序。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章