七大經典排序
插入排序
插入排序開始先把第一個數字作爲一個有序子數組,然後從第二個數字開始。
既然前面是一個有序的數組,那麼當前這個數組只要逐個跟前面的有序子數組比較就行。
如果當前數字比前面的數字小,就把前面的數字往後挪一個位置。
直到當前的數字比比較的數字( 下標爲: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)
穩定性:穩定