其實寫排序算法的博客已經(jīng)有很多了,其中不乏某些細心的博主去仔細講解各種排序的過程,甚至有使用gif圖來表現(xiàn)排序過程的博客,還有對已有排序算法進行改進的,我表示很佩服這些博主,謝謝你們。
這里附上一些我參考過的博客:
7種排序算法(系列博客) - 靜默虛空
常用排序算法總結(jié)(一) - SteveWang
[直觀學習排序算法] 視覺直觀感受若干常用排序算法 - todayx
白話經(jīng)典算法系列 - MoreWindows
常用排序算法穩(wěn)定性、時間復雜度分析 - jiuyueguang
八大排序算法
然后附上我重新寫的排序算法
這里的排序算法示例都用函數(shù)模板來寫
- 簡單排序算法:
- 選擇排序
- 冒泡排序
- 插入排序
- 復雜排序算法:
- 快速排序
- 歸并排序
- 堆排序
- shell排序
選擇排序
- 原理:遍歷元素集合,每次遍歷找到剩下的集合中最大\最小的元素放入已排序集合中,直到找完為止。
- 時間復雜度:O(n^2)
- 空間復雜度:O(1)
- 算法穩(wěn)定性:不穩(wěn)定排序。使用序列6 9 6 3 2來舉例,第一個6與3交換,導致第一個6排到了第二個6后面,所以選擇排序是不穩(wěn)定的排序算法。
- 算法示例:
template <class T>
void sort_array_select(T* dataArray, int dataSize)
{
//遍歷數(shù)據(jù)集合
for (int i = 0; i < dataSize; i++)
{
//記錄最小索引
int minIndex = i;
//遍歷剩余數(shù)據(jù)集合
for (int j = i; j < dataSize; j++)
{
//查找更小的值
if (dataArray[minIndex] > dataArray[j])
{
//保存更小值的索引
minIndex = j;
}
}
//判斷當前索引處是否是最小值
if (minIndex != i)
{
//將找到的最小值與當前索引處的值交換
T temp = dataArray[i];
dataArray[i] = dataArray[minIndex];
dataArray[minIndex] = temp;
}
}
}
冒泡排序
- 原理:遍歷元素集合,依次比較相鄰元素,將相鄰元素中較大\較小者移向一端,每次遍歷找到剩余數(shù)據(jù)集合中較大\較小者,直到全部排序完成。
-
時間復雜度:
- 最佳(已經(jīng)順序排好的集合):O(n)
- 最差(已經(jīng)逆序拍好的集合):O(n^2)
- 空間復雜度:O(1)
- 算法穩(wěn)定性:穩(wěn)定的排序。因為比較與交換均發(fā)生在相鄰的元素之間,對于兩個相等的元素不會進行交換,所以是穩(wěn)定的排序。
- 算法示例:
template <class T>
void sort_array_bubble(T* dataArray, int dataSize)
{
//遍歷集合
for (int i = 0; i < dataSize; i++)
{
//遍歷剩余元素集合
for (int j = 0; j < dataSize - i - 1; j++)
{
//比較相鄰元素大小
if(dataArray[j] > dataArray[j + 1])
{
//將較大元素后移
T temp = dataArray[j];
dataArray[j] = dataArray[j + 1];
dataArray[j + 1] = temp;
}
}
}
}
插入排序
- 原理:將數(shù)據(jù)集合中第一個數(shù)據(jù)視為已排序集合,依次獲取未排序集合中的元素,將獲取到的元素插入到已排序集合中的正確位置,直到全部排序完成。
-
時間復雜度:
- 最佳(已排序集合):O(n)
- 最差(逆序已排序集合):O(n^2)
- 空間復雜度:O(1)
- 算法穩(wěn)定性:穩(wěn)定的排序算法。因為比較的過程發(fā)生在相鄰元素之間,對于相等的元素,算法中不會改變他們的相對位置,所以是穩(wěn)定的排序算法。
- 算法示例:
template <class T>
void sort_array_insert(T* dataArray, int dataSize)
{
//遍歷數(shù)據(jù)集合(從1開始,0號元素已排序)
for (int i = 1; i < dataSize; i++)
{
//獲取未排序集合中第一個元素
T temp = dataArray[i];
int j = i;
//依次與已排序集合中元素比較,找到正確位置
while(j > 0 && temp < dataArray[j - 1])
{
dataArray[j] = dataArray[j - 1];
j--;
}
//取到的元素放入已排序列表中正確位置
dataArray[j] = temp;
}
}
快速排序
- 原理:應(yīng)用了分治的思想和以遞歸取代循環(huán)的思想。取一個元素作為flag,并將數(shù)據(jù)集合分為大于(等于)flag和小于(等于)flag兩個子集,然后對子集進行同樣的操作,直到子集元素個數(shù)為1或0,則所有元素完成排序。
-
時間復雜度:
- 最差(每次取到的flag都在邊界):O(n^2)
- 最佳(每次取到的flag都在中間):O(nlog2n)
- 空間復雜度:O(1)
- 算法穩(wěn)定性:不穩(wěn)定的排序。因為比較和替換不是發(fā)生在相鄰元素之間,而是從某個方向開始找到滿足條件的值,然后進行替換,這樣可能導致兩個相同元素的相對位置變化,所以是不穩(wěn)定的排序方式。
- 算法示例:
template <class T>
void sort_array_quick(T* dataArray, int left, int right)
{
//遞歸退出條件
if (left >= right)
{
return;
}
//取flag,并控制左右范圍
T flag = dataArray[left];
int sub_left = left;
int sub_right = right;
//根據(jù)flag來整理數(shù)據(jù)集合
while(sub_left < sub_right)
{
//在右側(cè)找小的值換到左側(cè)
//此時dataArray[sub_left]中的值是冗余的
while (sub_left < sub_right && dataArray[sub_right] >= flag)
{
sub_right--;
}
if (sub_left < sub_right)
{
dataArray[sub_left] = dataArray[sub_right];
}
//在左側(cè)找大的值換到右側(cè)
//此時dataArray[sub_right]中的值是冗余的
while (sub_left < sub_right && dataArray[sub_left] <= flag)
{
sub_left++;
}
if (sub_left < sub_right)
{
dataArray[sub_right] = dataArray[sub_left];
}
}
//上面的步驟進行完成后,dataArray[sub_left]中的值是冗余的,這里將flag放回
dataArray[sub_left] = flag;
//以flag為中心,左側(cè)的值小于等于flag,右側(cè)的值大于等于flag
//分別對左側(cè)的值的集合和右側(cè)的值的集合進行遞歸再次排序劃分
sort_array_quick(dataArray, left, sub_left - 1);
sort_array_quick(dataArray, sub_left + 1, right);
}
歸并排序
- 原理:應(yīng)用了分治的思想和以遞歸取代循環(huán)的思想。將待排序數(shù)據(jù)集合劃分為兩個子集,對子集分別進行排序,排序完成后將兩個有序子集中的元素。
- 時間復雜度:O(nlog2n)
- 空間復雜度:O(n)
- 算法穩(wěn)定性:穩(wěn)定的排序算法。在元素集合被拆分為n個子集合之后,合并集合時,是通過對已排序集合中值最相近的兩個元素進行比較并存儲的,所以不會造成值相同的元素相對位置變化。
- 算法示例:
//按順序合并集合
template <class T>
void array_merge(T* dataArray, int left, int mid, int right, T* sortedArray)
{
int i = left;
int j = mid + 1;
int count = 0;
//將dataArray中l(wèi)eft->mid和mid+1->right部分的元素按順序放入sortedArray中
while (i <= mid && j <= right)
{
if (dataArray[i] < dataArray[j])
{
sortedArray[count++] = dataArray[i++];
}
else
{
sortedArray[count++] = dataArray[j++];
}
}
//剩余元素直接放入sortedArray
while (i <= mid)
{
sortedArray[count++] = dataArray[i++];
}
while (j <= right)
{
sortedArray[count++] = dataArray[j++];
}
//排序好的元素放回dataArray
for (int i = 0; i < count; i++)
{
dataArray[left + i] = sortedArray[i];
}
}
//拆分集合
template <class T>
void sort_array_merge(T* dataArray, int left, int right, T* sortedArray)
{
//遞歸停止條件
if (left >= right)
{
return;
}
//集合分為兩個子集
int mid = (left + right) / 2;
//繼續(xù)拆分
sort_array_merge(dataArray, left, mid, sortedArray);
sort_array_merge(dataArray, mid + 1, right, sortedArray);
//按順序合并集合
array_merge(dataArray, left, mid, right, sortedArray);
}
堆排序
- 原理:應(yīng)用了二叉堆的特點,即父節(jié)點的值總是大于(小于)子節(jié)點的值。這樣每一次將待排序集合調(diào)整為堆時,便能得到待排序集合中的一個最值。堆排序分為兩步:第一步是建立堆,將無序的集合調(diào)整為滿足堆的條件的集合;第二步是依次取得最值,此時只破壞了堆頂,以堆頂為根進行一次調(diào)整,形成一個新的堆,然后循環(huán)第二步。
- 時間復雜度:O(nlog2n)
- 空間復雜度:O(1)
- 算法穩(wěn)定性:不穩(wěn)定的排序算法。因為比較與交換不是發(fā)生在相鄰元素之間,兩個相同的元素相鄰時會被分配到不同的子樹中,在調(diào)整子樹時可能導致值相同的元素的相對位置發(fā)生變化。
- 算法示例:
//調(diào)整為最大堆,保證父節(jié)點值大于子節(jié)點
template <class T>
void heap_update(T* dataArray, int rootIndex, int arraySize)
{
//遞歸終止條件,rootIndex處應(yīng)為非葉子節(jié)點
if (rootIndex >= arraySize / 2)
{
return;
}
//計算左右子節(jié)點的index
int left_child = rootIndex * 2 + 1;
int right_child = rootIndex * 2 + 2;
//查找父、左子、右子節(jié)點中最大值
int largest = rootIndex;
if (left_child < arraySize && dataArray[left_child] > dataArray[largest])
{
largest = left_child;
}
if (right_child < arraySize && dataArray[right_child] > dataArray[largest])
{
largest = right_child;
}
//將最大值替換到父節(jié)點位置
if (largest != rootIndex)
{
T temp = dataArray[rootIndex];
dataArray[rootIndex] = dataArray[largest];
dataArray[largest] = temp;
//largest所處位置元素相對其子節(jié)點來說,又是一個被破壞的堆頂,所以繼續(xù)調(diào)整
heap_update(dataArray, largest, arraySize);
}
//對左右子節(jié)點分別進行調(diào)整
//heap_update(dataArray, left_child, arraySize);
//heap_update(dataArray, right_child, arraySize);
}
//建立堆。即逆序?qū)λ蟹侨~子節(jié)點進行一次堆調(diào)整。
template <class T>
void heap_build(T* dataArray, int arraySize)
{
for (int i = arraySize / 2 - 1; i >= 0; i--)
{
heap_update(dataArray, i, arraySize);
}
}
//堆排序
template <class T>
void sort_array_heap(T* dataArray, int arraySize)
{
//建立堆
heap_build(dataArray, arraySize);
//循環(huán)獲得堆頂元素并調(diào)整堆
int count = arraySize;
while (count > 1)
{
//將堆頂元素與待排序數(shù)組末尾元素交換
T temp = dataArray[0];
dataArray[0] = dataArray[count - 1];
dataArray[count - 1] = temp;
//調(diào)整堆,只破壞了堆頂,這里以堆頂為root,對待排序的部分進行堆調(diào)整
count--;
heap_update(dataArray, 0, count);
}
}
shell排序
- 原理:對直接插入法排序的改良。因為直接插入法排序在元素基本有序的情況下效率最高,所以將待排序元素依次劃分為n組(n為size/2,size/4,... 首先保持元素數(shù)量最少,組內(nèi)排序完成后再重新劃分為元素更多的組,保持直接插入法的高效),然后對組內(nèi)進行直接插入法排序。
-
時間復雜度:
- 最差:O(n^2)
- 最佳(有序排列的集合):O(nlog2n)
- 空間復雜度:O(1)
- 算法示例:
template <class T>
void sort_array_shell(T* dataArray, int arraySize)
{
//使用step劃分組
for (int step = arraySize / 2; step > 0; step /= 2)
{
//逐個元素進行組內(nèi)插入排序
for (int i = step; i < arraySize; i++)
{
//組內(nèi)直接插入排序
T temp = dataArray[i];
int k = i - step;
//在組內(nèi)依次向前查找正確位置
while (k >= 0 && dataArray[k] > temp)
{
dataArray[k + step] = dataArray[k];
k -= step;
}
//元素插入到正確位置
dataArray[k + step] = temp;
}
}
}
上面所有的算法示例在排序一個int類型的數(shù)組時,是正常可用的。但是很多都有優(yōu)化的空間(比如看到一篇博客中對插入法排序?qū)懥硕喾N實現(xiàn)方法),而且使用臨時變量來交換兩個值的過程也值得思考。
總結(jié):以上排序算法只是提供一種思想,在我們面臨遍歷大量數(shù)據(jù)、從大量數(shù)據(jù)中查找某個值等問題的時候,其中的某些點是可以借鑒的。其中的分段、構(gòu)建二叉樹的思想是很值得學習的,以此告誡自己思維不要太刻板。