【資料結構】各類排序演算法及其優化總結
本文對各類排序演算法的實現、優化、複雜度、穩定性、適用場景作以全面總結,為了突出演算法的簡潔、易懂,去除了一些冗餘操作,預設為升序進行模擬。
一、插入排序
插入排序基本思想
:每一步將一個待排序的元素,按其排序碼的大小,插入到前面已經排好序的一組元素的合適位置上去,直到元素插完。
☞ 直接插入排序
基本思想
:當我們插入第i(i>=1)個元素時,前面的所有元素已經排好序,此時我們使用當前元素從後向前比較,直到找到一個小於array[i]的節點(若沒有,則插在陣列下標為0的地方),從下一個節點順序後移,將array[i]插入進來。
時間複雜度: O(n^2)
空間複雜度: O(1)
穩定性:穩定
適用場景
:1.資料量較小 2.基本接近有序
//直接插入排序
int InsertSort(int *num,int len)
{
assert(num);
int i = 0;
//第一個元素已經為有序序列,所以要進行len-1次排序
for (; i < len - 1; i++)
{
int end = i;
int tmp = num[i + 1];//儲存非有序區間第一個元素,否則在後邊的移動中會改變
//比較後移
while (end >= 0 && num[end] >tmp)
{
num[end+1] = num[end];
--end;
}
//插入到適當位置
num[end + 1] = tmp;
}
}
☞ 二分插入排序(優化)
基本思想
:在直接插入排序的基礎上進行優化,因為待插入元素之前的元素已經有序,我們沒必要每個都遍歷,藉助而二分法的思想可以有效降低查詢的時間。
void BinaryInsertSort(int *arr, int size)
{
if (arr == NULL || size <= 0)
return ;
int index = 0;
for (int idx = 1; idx < size; ++idx)
{
int tmp = arr[idx];//儲存待插入元素
int left = 0;
int right = idx - 1;
int mid = (left&right) + ((left^right) >> 1);
//二分法查詢插入位置
while (left <= right)
{
if (tmp < arr[mid])
{
right = mid - 1;
index = mid;//更新插入位置
}
else if (tmp >= arr[mid])
{
left = mid + 1;
index = mid + 1;//更新插入位置
}
mid = (left&right) + ((left^right) >> 1);//縮小空間
}
//後移元素
for (int j = idx; j > index; j--)
{
arr[j] = arr[j - 1];
}
//插入新元素
arr[index] = tmp;
}
}
☞ 希爾排序
基本思想
:希爾排序是插入排序的一個變種。不同之處在於我們按步長gap分組,對每組的記錄採用直接插入排序,隨著步長的逐漸減少,分組包含的記錄越來越多,直到gap=1時,構成了一個有序記錄。
時間複雜度: O(n^1.25) ~ 1.6*O(n^1.25)
空間複雜度: O(1)
穩定性:不穩定
適用場景
:資料量較大,有序
下圖中為了方便,對gap以gap/2來計算,但最優的演算法是gap/3+1。
void ShellSort(int* arr, int size)
{
if (arr == NULL || size <= 0)
return;
int gap = size;//gap為增量
while (gap > 1)
{
gap = gap / 3 + 1;//這樣給是最優的
for (int idx = gap; idx < size; ++idx)
{
int end = idx - gap;//分組後,當前元素的前一個元素
int key = arr[idx];//儲存當前元素
//按升序排序
while (end >= 0 && arr[end] > key)
{
arr[end + gap] = arr[end];
end -= gap;
}
arr[end + gap] = key;
}
}
}
二、選擇排序
基本思想
:每一趟(例如第i趟,i=0,1,…,n-2)在後面n-i個待排序的資料元素集合中選出關鍵碼最小的資料元素,作為有序元素序列的第i個元素,待到第n-2趟做完,待排序元素集合中只剩 下1個元素,排序結束。
時間複雜度: O(n^2)
空間複雜度: O(1)
穩定性:不穩定
適用場景
:資料量較小,交換次數比較少
☞ 選擇排序(單邊縮小空間)
基本思想
:我們定義一個maxIndex來標記區間內最大值的下標,第一次遍歷完陣列後,更新maxIndex的指向,與下標為end(最後一個元素)的元素交換,end–縮小空間,繼續遍歷更新maxIndex。
void SelectSort(int* arr, int size)
{
if (arr == NULL || size <= 0)
return;
for (int end = size - 1; end > 0; --end)
{
int maxIndex = end;//最大下標
for (int idx = 0; idx < end; ++idx)
{
//比最大的元素還大,更新最大下標
if (arr[idx] > arr[maxIndex])
maxIndex = idx;
}
//交換end與最大元素的值
std::swap(arr[maxIndex], arr[end]);
}
}
☞ 選擇排序(雙邊縮小空間1.0)
基本思想
:每一趟遍歷我們需要找出最大元素的下標與最小元素的下標,然後用處於最小下標的元素與begin(首元素下標)交換,用最大下標的元素與end(尾元素下標),最後begin++,end–同時縮短區間,直到begin與end相等。
注意
:如果begin與maxIndex相同,begin與minPos交換之後,maxPos也應該指向minPos。
void SelectSort(int* arr, int size)
{
if (arr == NULL || size <= 0)
return;
int begin = 0;
int end = size - 1;
while (begin < end)
{
int maxPos = begin;//最大元素下標
int minPos = begin;//最小元素下標
for (int idx = begin + 1; idx <= end; ++idx)
{
//比最大的元素還大,更新最大下標
if (arr[idx] > arr[maxPos])
maxPos = idx;
//比最小的元素還小,更新最大下標
if (arr[idx] < arr[minPos])
minPos = idx;
}
//交換begin與最小元素的值
std::swap(arr[begin], arr[minPos]);
//如果最大元素下標與begin相同,上面begin與minPos已經交換,因此maxPos也應該指向minPos
if (maxPos == begin)
maxPos = minPos;
//交換end與最大元素的值
std::swap(arr[end], arr[maxPos]);
//縮小區間
++begin;
--end;
}
}
☞ 選擇排序(雙邊縮小空間2.0)
基本思想
:1.0版本中,我們需要考慮最大下標與begin的重合問題,為了避免這樣的問題出現,我們將不再使用maxPos和minPos,而改用直接交換資料,然後縮小空間,直到左右空間重合。
void SelectSort(int* arr, int size)
{
if (arr == NULL || size <= 0)
return;
int left = 0;
int right = size - 1;
while (left < right)
{
for (int idx = left; idx <= right; ++idx)
{
//比left小的交換
if (arr[idx] < arr[left])
std::swap(arr[idx], arr[left]);
//比right大的交換
if (arr[idx] > arr[right])
std::swap(arr[idx], arr[right]);
}
//縮小左右區間
++left;
--right;
}
}
☞ 堆排序
基本思想
:堆排序本質上是一種樹形選擇排序。它也是對直接選擇排序的一種優化,堆結構在物理儲存上也是一種陣列,但是它在邏輯上是一棵完全二叉樹,在進行堆排序(升序)時,我們可以先建一個大堆,最大的元素在堆頂上,我們可以以O(1)的時間找到最大的元素,然後和最後一個元素交換。此時,這個堆的左右子樹仍然是一個堆,我們只要把[n-1]個數向下調整一次重新建個大堆即可,直到堆中剩下一個元素,既排序完成。
排升序–>建大堆 && 排降序–>建小堆
我們從最後一個非葉子結點建堆,步驟如下:
⑴ 將堆頂元素與當前最大堆的最後一個節點交換
⑵ 最大堆節點-1,即調整剩下的n-1個節點
⑶ 從堆頂繼續向下調整,試之滿足最大堆,迴圈⑴ ⑵ ,直至剩下一個節點。
時間複雜度: 0(NlogN)
穩定性:不穩定
適用場景
:topK問題等
void AdjustDown(int *arr, int root, int size)//建大堆
{
int parent = root;
int child = parent * 2 + 1;
while (child < size)
{
//保證child指向較大節點
if (child + 1 < size && arr[child + 1] > arr[child])
child += 1;
if (arr[child] > arr[parent])
{
std::swap(arr[child], arr[parent]);
parent = child;//下濾
child = parent * 2 + 1;
}
else
break;
}
}
//堆排序遞迴
void HeapSort(int *arr, int size)
{
assert(arr && size > 1);
//從最後一個非葉子節點建堆
for (int idx = (size - 2) / 2; idx >= 0; --idx)
{
AdjustDown(arr, idx, size);//下濾調整
}
int end = size - 1;
while (end > 0)
{
//堆頂與最後一個節點交換,升序
std::swap(arr[0], arr[end]);
AdjustDown(arr, 0, end);//下濾調整
--end;
}
}
四、交換排序
☞ 氣泡排序
基本思想
:一次確定一個最大值或者最小值,兩兩比較,將最大值或者最小交換到最右邊或者最左邊,N個元素需要N-1趟排序。
程式碼實現:
//氣泡排序
void BubbleSort(int* num, int len)
{
int flag = 0;
if (num == NULL || len <= 0)
return;
//確定迴圈躺數
for (int i = 0; i < len - 1; i++)
{
//確定比較次數
for (int j = 0; j < len - 1 - i; j++)
{
if (num[j]>num[j + 1])
{
Swap(&num[j], &num[j + 1]);
}
flag=1;
}
if(0 == flag)
break;
}
}
☞ 快速排序
基本思想
:在待排序序列中任意取一個元素作為基準元素,按照該基準元素將待排序序列分為兩個子序列,左邊子序列的值都小於基準值,右邊子序列的值都大於基準值。然後把左右子序列當做一個子問題,以同樣的方法處理左右子序列,直到所有的元素都排列在相對應的位置上為止。快排是一個遞迴問題,它是按照二叉遞迴樹的前序路線去劃分的。
關於快速排序,我詳細將快排的細節總結於我的另一篇部落格:快排總結
參考部落格
:快排總結
五、歸併排序
☞ 歸併排序
基本思想
:歸併排序是一個外排序,它可以對磁碟的檔案進行排序。它將待排序的元素序列分成兩個長度相等的子序列,對每一個子序列排序,然後在將他們合併為一個序列。合併兩個子序列的過程稱為二路歸併。歸併排序主要分為兩步分組和歸併。
void MergeSort(int* num, int len)
{
if (num == NULL || len <= 0)
return;
//開闢臨時空間,用來存放每次合併後的子序列
int* tmp = (int*)malloc(sizeof(int)*len);
_MergeSort(num, 0, len - 1, tmp);
//釋放空間
free(tmp);
tmp = NULL;
}
//歸併排序分開過程(遞迴樹按照前序路線展開)
void _MergeSort(int* num, int begin, int end,int* tmp)
{
assert(num&&tmp);
int mid = begin + (end - begin) / 2;
//只有一個元素,說明這個序列已經有序
if (begin == end)
return;
//子問題劃分左子序列
_MergeSort(num, begin, mid, tmp);
//子問題劃分右子序列
_MergeSort(num, mid + 1, end, tmp);
//合併兩個有序陣列
Merge(num, begin, mid, mid + 1, end, tmp);
}
//歸併排序合併過程
void Merge(int* num, int start1, int end1, int start2, int end2, int* tmp)
{
assert(num&&tmp);
int begin = start1;
int index = start1;//從start1的地方合併
//和兩條有序單鏈表的合併的過程類似
while ((start1 <= end1) && (start2 <= end2))
{
if (num[start1] < num[start2])
{
tmp[index++] = num[start1++];
}
else
{
tmp[index++] = num[start2++];
}
}
//把剩餘的合併到tmp上
while (start1 <= end1)
tmp[index++] = num[start1++];
while (start2 <= end2)
tmp[index++] = num[start2++];
//tmp是個臨時空間,最後到把合併的內容拷貝到num上
memcpy(num + begin, tmp + begin, sizeof(int)*(end2 - begin + 1));
}
六、計數排序
☞ 計數排序
計數排序
又稱為鴿巢原理,
它是對雜湊直接定址法的變形應用。計數排序是一種非比較的排序演算法,其核心在於將輸入的資料值轉化為鍵儲存在額外開闢的陣列空間中。這說明計數排序只適合用於比較資料較為集中的資料,如果資料太過分散是非常浪費空間的。
實現步驟
- 統計所有相同資料出現的次數
- 根據統計的次數將序列放回到原來的陣列返回
效率分析
計數排序是一個穩定
的排序演算法。當輸入的元素是 n 個 0到 k 之間的整數時,時間複雜度是O(n+k)。
空間複雜度也是(n+k),其排序速度快於任何比較排序演算法。當k不是很大並且序列比較集中時,計數排序是一個很有效的排序演算法
void CountSort(int* num,int len)
{
if(num==NULL||len<=0)
return;
int min=num[0],max=num[0];
int i=0;
for(;i<len;i++)
{
if(num[i]<min)
min=num[i];
if(num[i]>max)
max=num[i];
}
int range=max-min+1;
int* tmp=(int*)malloc(sizeof(int)*range);
if(tmp==NULL)
{
perror("use malloc");
exit(1);
}
memset(tmp,0,sizeof(int)*range);
for(i=0;i<len;i++)
{
tmp[num[i]-min]++;
}
int index=0;
for(i=0;i<range;i++)