七大經典排序

七大經典排序

插入排序

插入排序開始先把第一個數字作爲一個有序子數組,然後從第二個數字開始。

既然前面是一個有序的數組,那麼當前這個數組只要逐個跟前面的有序子數組比較就行。

如果當前數字比前面的數字小,就把前面的數字往後挪一個位置。

直到當前的數字比比較的數字( 下標爲:j )大爲止,把當前數字填入到 j + 1 的位置。

即可表示當前數字插入成功。

// 插入排序
//
// 時間複雜度
// a. 最好:O(N)    已經有序的情況
// b. 平均:O(N^2)  
// c. 最壞:O(N^2)  已經有序是逆序
//
// 空間複雜度
// O(1)
//
// 穩定性:穩定
void InsertSort(std::vector<int>& arr)
{
    // 外循環實現減治過程
    // 一次取一個數,插入到前面的有序區間裏
    for (size_t i = 1; i < arr.size(); i++)
    {
        int key = arr[i];
        // 內部的循環實現的是插入的過程
        // j 從 [i - 1, 0]
        // 如果 arr[j] > key, 往後搬
        // 如果 arr[j] == key, 跳出循環(保證了穩定性)
        // 如果 arr[j] < key, 跳出循環
        int j;
        for (j = i - 1; j >= 0 && arr[j] > key; j--)
        {
            arr[j + 1] = arr[j];
        }

        // j + 1 就是要插入的位置
        arr[j + 1] = key;
    }
}

希爾排序

希爾排序是在插入排序的基礎上做了一個優化。

希爾排序是把一個數組先分成若干組,先對這幾組的數據,每組之間進行插入排序。

然後,縮小間隔(gap),也就是說,減少組的數量,再次進行插入排序。

這樣做的目的就是爲了在最後一次,只有一組的情況下,儘可能的讓這個數組裏的數字已經有序。

gap 的變化是 :

先讓 gap == arr.size()

然後 gap = gap / 3 + 1

再進行排序

直到 gap == 1 的情況排完,說明,這個數組已經完成了排序。(gap == 1, 說明它把整個數組分成了一個組,進行插入排序,排完一定有序),就可以跳出了。

// 希爾排序
// 希爾排序是插入排序的優化版本
// 在插入排序之前,儘可能的讓數據有序
// 分組插排
// 通過維護一個 gap 來實現分組
// gap 初始化 爲 gap = arr.size() / 3 + 1
// 後面 gap = gap / 3 + 1
//
// 時間複雜度
// a. 最好: O(N)
// b. 平均: O(N^1.2) ~ O(N^1.3)
// c. 最壞: O(N^2) 這裏雖然跟插入排序的最壞時間複雜度一樣
// 可是它把遇到最壞情況的概率變小了
//
// 空間複雜度
// O(1)
//
// 穩定性:不穩定。
// 一旦分組,很難保證相同的數在一個組裏

// 可以設置 gap 間隔的插入排序
void InsertSortWithGap(std::vector<int>& arr, int gap)
{
    for (size_t i = gap; i < arr.size(); i++)
    {
        int key = arr[i];
        int j;
        for (j = i - gap; j >= 0 && arr[j] > key; j -= gap)
        {
            arr[j + 1] = arr[j];
        }

        // j + 1 就是要插入的位置
        arr[j + gap] = key;
    }
}

// 希爾排序
void ShellSort(std::vector<int>& arr)
{
    int gap = arr.size();
    while (true)
    {
        gap = gap / 3 + 1;

        InsertSortWithGap(arr, gap);

        // 如果 gap == 1 ,說明上次排序已經排完了整個數組
        if (gap == 1)
            break;
    }
}

堆排序

堆排序比較好理解。

如果排升序,首先建一個大堆。

然後每次把對頂元素(數組裏最大的數字)和最後一個元素交換。這個時候向下調整的時候,size - 1 。也就是說,把最後一個元素進行向下調整,忽略已經在數組最後的堆頂元素。

以此類推,直到 size = 1 已經把所有數字排完。(就是依次把當前 size 個數組元素裏最大的數字放到最後)

// 堆排序--》升序建大堆
//
// 時間複雜度
// 最好 = 平均 = 最壞 = O(N * LogN)
// 向下調整時間複雜度是 O(LogN)
// 建堆的時間複雜度是 O(N * LogN)
// 排序的時間複雜是 O(O * LogN)
//
// 空間複雜度 O(1)
//
// 穩定性:不穩定
// 向下調整過程中,無法保證相等數的前後關係
//
// 向下調整
void AdJustDown(std::vector<int>& arr, int size, int root)
{
    // 如果此時 root 是葉子結點,調整結束退出
    if (root * 2 + 1 >= size)
        return;

    // 找到最大的孩子
    int left = (root * 2) + 1;
    int max = left;
    int right = (root * 2) + 2;
    if (right < size && arr[right] > arr[left])
    {
        max = right;
    }

    // 判斷當前 root 是否小於葉子結點
    if (arr[root] < arr[max])
    {
        std::swap(arr[root], arr[max]);
        AdJustDown(arr, size, max);
    }
    
    return;
}
// 建堆
void CreateHeap(std::vector<int>& arr)
{
    // 從第一個雙親結點開始向下調整
    // 逐漸向上走,直到根結點向下調整結束
    for (int i = (int)(arr.size() - 2) / 2; i >= 0; i--)
    {
        AdJustDown(arr, (int)arr.size(), i);
    }
}
// 堆排序
void HeapSort(std::vector<int>& arr)
{
    // 先建堆
    CreateHeap(arr);

    // 然後每次將堆頂元素和最後一個元素交換
    // size - 1
    // 向下調整
    for (int i = 0; i < (int)arr.size(); i++)
    {
        std::swap(arr[0], arr[arr.size() - i - 1]);

        AdJustDown(arr, arr.size() - i - 1, 0);

    }
}

選擇排序

這個排序也很簡單。

就是每次把當前數組裏最大的元素放到最後。然後進行減治運算就行。

(優化版本也就是每次選兩個,一個最大元素的下標,一個最小元素的下標。然後把最大的放到最後,最小的放到最前面,再進行減治)

// 選擇排序
//
// 每次遍歷無序區間,找到最大數的下標
// 1. 交換最大的數和無序區間的最後一個數
// 2. 區間 size - 1
// 3. 重新循環

// 時間複雜度
// 最好 = 最壞 = 平均 = O(N^2)
//
// 空間複雜度
// O(1)
//
// 穩定性:不穩定。 
// {9, 4, 3, 5a, 5b} 無法保證5a在5b前
void SelectSort(std::vector<int>& arr)
{
    for (int i = 0; i < (int)arr.size() - 1; i++)
    {
        int max = 0;
        for (int j = 0; j < (int)arr.size() - i; j++)
        {
            if (arr[j] > arr[max])
                max = j;
        }

        std::swap(arr[max], arr[arr.size() - 1 - i]);
    }
}

冒泡排序

這個跟選擇排序差不多,但是冒泡排序是每次通過交換的方式,把最大的放到最後面。

然後進行減治。

// 冒泡排序
//
// 時間複雜度
// 最好 :O(N)
// 最壞|平均:O(N^2) 逆序
//
// 空間複雜度
// O(1)
//
// 穩定性:穩定

void BubbleSort(std::vector<int>& arr)
{
    for (int i = 0; i < (int)arr.size(); i++)
    {
        int sorted = 0;
        for (int j = 0; j < (int)arr.size() - 1 - i; j++)
        {
            if (arr[j] > arr[j + 1])
            {
                std::swap(arr[j], arr[j + 1]);
                sorted = 1;
            }
        }

        if (sorted == 0)
            break;
    }
}

快速排序

快速排序是先確定一個基準值(一般取數組的最左邊的數或者最右邊的數)我這裏取數組最右邊的數,然後確定兩個下標left right。

如果基準值取最右邊的數,那麼先讓左側的下標從左往右找,找到第一個比基準值大的數字停下來,然後右側的下標開始從右往左遍歷數組,找到第一個比基準值小的值停下來。交換 left right 所對應的值。

直到兩個下標相遇,把相遇的數字和基準值交換就可以了。

然後以基準值爲中心,把數組分成兩個小數組,再次在小數組內進行剛纔的操作。

直到小數組的元素個數小於等於1,就可以停下來了。

三種分割方法

1. hoare 法

// hoare 版本
int Parition(std::vector<int>& arr, int left, int right)
{
    int div = right;

    while (left < right)
    {
        // 比較的時候要加 = 的情況
        // 比如說一個例子 1 1 1 1 1 
        // 如果不加等於 就成死循環了
        
        // 先從左邊找到一個比基準值大的數字
        while (left < right && arr[left] <= arr[div])
            left++;

        // 從右邊開始找一個比基準大小的數字
        while (left < right && arr[right] >= arr[div])
            right--;

        if (left < right)
            std::swap(arr[left], arr[right]);
    }

    std::swap(arr[left], arr[div]);

    return left;
}

2. 挖坑法

// 挖坑法
int ParitionDiggintPit(std::vector<int>& arr, int left, int right)
{
    // 把基準值獲取到,然後這個位置成爲了一個坑
    // 下次找到滿足條件的數,就把那個數填進去
    // 那麼被填進去的數原本的位置就成了新的坑
    int base_value = arr[right];
    int pit = right;
    int div = right;

    while (left < right)
    {
        while (left < right && arr[left] <= arr[div])
        {
            left++;
        }
        // 找到一個比基準值大的值,將這個值填入坑中
        arr[pit] = arr[left];
        // 更新坑的位置
        pit = left;

        while (left < right && arr[right] >= arr[div])
        {
            right--;
        }
        // 找到了一個比基準值小的值,將這個值填入坑中
        arr[pit] = arr[right];
        // 更新坑的位置
        pit = right;
    }

    arr[pit] = base_value;

    return pit;
}

3. 拉窗簾法

// 拉窗簾法
int ParitionSlideWindow(std::vector<int>& arr, int left, int right)
{
    int div = right;
    int d = 0;
    int c = 0;

    while (c < div)
    {
        // 始終讓滑動窗口內部的值大於基準值
        if (arr[c] >= arr[div])
        {
            c++;
        }
        else
        {
            if (d < c)
                std::swap(arr[d], arr[c]);
            
            d++;
            c++;
        }
    }

    // 走到這表示 d 之前的值都比基準值小
    // 把基準值和 arr[d] 交換即可
    std::swap(arr[d], arr[div]);

    return d;
}

整體代碼:

// 快速排序
// [left, right]
// 1. 在要排序的區間內選擇一個基準值
//  具體方法:
//  1)選擇區間最右邊這個數 arr[right]
//
// 2. 遍歷整個區間,做一些數據交換,達到效果:
// 比基準值小的數,放到基準值左側
// 比基準值大的數,放到基準值右側
//
// 3. 分治算法:把一個問題變成兩個同樣的小問題
//
// 4. 用遞歸或非遞歸
// 終止條件:
//  1)分出來的小區間沒有數了:分出的區間 size == 0
//  2)分出來的小區間已經有序了:區間的 size == 1

// 時間複雜度
// 最好|平均:O(N * LogN)
// 遍歷一遍數組:O(N)
// 高度: Log N
//
// 最差:O(N^2) 
// 如果已經有序,就是一個單支樹。
//
// 空間複雜度
// O(LogN) ~ O(N)  
// 遞歸調用的深度(二叉樹的高度)
//
// 穩定性:不穩定

// Parition 三種方式
// hoare 版本
int Parition(std::vector<int>& arr, int left, int right)
{
    int div = right;

    while (left < right)
    {
        // 比較的時候要加 = 的情況
        // 比如說一個例子 1 1 1 1 1 
        // 如果不加等於 就成死循環了
        
        // 先從左邊找到一個比基準值大的數字
        while (left < right && arr[left] <= arr[div])
            left++;

        // 從右邊開始找一個比基準大小的數字
        while (left < right && arr[right] >= arr[div])
            right--;

        if (left < right)
            std::swap(arr[left], arr[right]);
    }

    std::swap(arr[left], arr[div]);

    return left;
}

// 挖坑法
int ParitionDiggintPit(std::vector<int>& arr, int left, int right)
{
    int base_value = arr[right];
    int pit = right;
    int div = right;

    while (left < right)
    {
        while (left < right && arr[left] <= arr[div])
        {
            left++;
        }
        // 找到一個比基準值大的值,將這個值填入坑中
        arr[pit] = arr[left];
        // 更新坑的位置
        pit = left;

        while (left < right && arr[right] >= arr[div])
        {
            right--;
        }
        // 找到了一個比基準值小的值,將這個值填入坑中
        arr[pit] = arr[right];
        // 更新坑的位置
        pit = right;
    }

    arr[pit] = base_value;

    return pit;
}

// 拉窗簾法
int ParitionSlideWindow(std::vector<int>& arr, int left, int right)
{
    int div = right;
    int d = 0;
    int c = 0;

    while (c < div)
    {
        // 始終讓滑動窗口內部的值大於基準值
        if (arr[c] >= arr[div])
        {
            c++;
        }
        else
        {
            if (d < c)
                std::swap(arr[d], arr[c]);
            
            d++;
            c++;
        }
    }

    // 走到這表示 d 之前的值都比基準值小
    // 把基準值和 arr[d] 交換即可
    std::swap(arr[d], arr[div]);

    return d;
}

void _QuickSort(std::vector<int>& arr, int left, int right)
{
    // 終止條件
    if (left >= right)
        return;

    int div; // 基準值取最右邊的值
    div = ParitionSlideWindow(arr, left, right); // 遍歷 arr[left, right],把小的放左,大的放右

    _QuickSort(arr, left, div - 1);     
    _QuickSort(arr, div + 1, right);
}

void QuickSort(std::vector<int>& arr)
{
    _QuickSort(arr, 0, (int)arr.size() - 1);
}

歸併排序

這個排序比較有意思。

現在說一個特殊的例子。如何給兩個有序的數組進行排序呢?就是先開闢一個空間足以容納兩個數組的數組,然後分別從兩個數組的開頭遍歷,誰小就往裏放就行了。

那麼歸併也是這意思。它是先把一個數組一直二分。直到分成最小數組(指的是數組裏只有一個或者兩個元素),然後兩個兩個的進行合併。

然後逐漸向上回溯,兩個兩個的數組越來越大的而已。最後進行的就是合併兩個有序數組。(這兩個數組各佔一半需要排序的數組)。

// 歸併排序
// 對一個大數組進行排序的問題
// 變成了對左右兩個小數組進行排序的問題o
//
// 時間複雜度
// 最好|平均|最壞:O(N * LogN)
//
// 空間複雜度
// O(N)
//
// 穩定性:穩定
//
// 優點:可以對硬盤上的數據進行排序

// 合併兩個有序數組
// arr[left, mid)
// arr[mid, right)
// 該操作的
// 時間複雜度 
// O(N)
//
// 空間複雜度
// O(N)
void Merge(std::vector<int>& arr, int left, int mid, int right, 
{

    int left_index = left;
    int right_index = mid;
    int index = 0;

    while (left_index < mid && right_index < right)
    {
        if (arr[left_index] <= arr[right_index]) // <= 之所以加 
        {
            tmp[index++] = arr[left_index++];
        }
        else
        {
            tmp[index++] = arr[right_index++];
        }
    }

    while (left_index < mid)
    {
        tmp[index++] = arr[left_index++];
    }

    while (right_index < right)
    {
        tmp[index++] = arr[right_index++];
    }
    
    for (int i = 0; i < right - left; i++)
    {
        arr[left + i] = tmp[i];
    }
}

void _MergeSort(std::vector<int>& arr, int left, int right, std:
{
    if (right == left + 1)// 區間內還剩一個數
    {
        return;
    }
    if (left >= right)// 區間內沒有數了
    {
        return;
    }

    int mid = left + (right - left) / 2;
    //  區間被分成左右兩個小區間
    //  [left, mid)
    //  [mid, right)
    //  先把左右兩個小區間進行排序,分治算法,遞歸解決
    
    _MergeSort(arr, 0, mid, tmp);
    _MergeSort(arr, mid, right, tmp);

    // 左右兩個小區間已經有序
    // 合併兩個有序數組
    Merge(arr, left, mid, right, tmp);
}

void MergeSort(std::vector<int>& arr)
{
    std::vector<int> tmp(arr.size());
    _MergeSort(arr, 0, arr.size(), tmp);
}

總結:

插入排序:

時間複雜度(平均):O(N^2)

空間複雜度:O(1)

穩定性:穩定

希爾排序:

時間複雜度:O(N^1.2) ~O(N^1.3)

空間複雜度:O(1)

穩定性:不穩定

選擇排序:

時間複雜度:O(N^2)

空間複雜度:O(1)

穩定性:不穩定

冒泡排序:

時間複雜度:O(N^2)

空間複雜度:O(1)

穩定性:穩定

堆排序:

時間複雜度:O(N*LogN)

空間複雜度:O(1)

穩定性:不穩定

快速排序:

時間複雜度:O(N*LogN)

空間複雜度:O(LogN) ~ O(N)

穩定性:不穩定

歸併排序:

時間複雜度:O(N*LogN)

空間複雜度:O(N)

穩定性:穩定

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