常見排序演算法總結(基於C++實現)
1.插入排序
1.1 直接插入
基本思想:
將待排序表看作左右兩部分,其中左邊為有序區,右邊為無序區,整個排序過程就是將右邊無序區中的元素逐個插入到左邊的有序區中,以構成新的有序區。
template<typename T>
void InsertSort(T A[], int n) {
for (int i = 1; i < n; i++)
{
T temp = A[i];
int j = i - 1;
while (A[j] > temp && j >= 0)
{
A[j + 1 ] = A[j];
j--;
}
A[j + 1] = temp;
}
}
1.2 希爾排序
基本思想:
將待排序列劃分為若干組,在每組內進行直接插入排序,以使整個序列基本有序,然後再對整個序列進行直接插入排序。
分組方法:
對給定的一個步長d(d>0),將下標相差為d的倍數的元素分在一組。d的取值依次為: d1=n/2, d2=d1/2, ……,dk=1
template<typename T>
void ShellSort(T A[], int n) {
int d = n / 2; //步長初始為n/2
while (d > 0) {
for (int i = d; i < n; i++) {
T x = A[i];
int j = i - d;
while (j >= 0 && x < A[j]) {
A[j + d] = A[j];
j = j - d;
}
A[j + d] = x;
}
d = d / 2; //步長變為原來的1/2
}
}
1.3 小結
排序方法 | 時間複雜度 | 空間複雜度(輔助空間) | 穩定性 |
---|---|---|---|
直接插入 | 平均情況 O(n^2) 最壞情況 O(n^2) 最好情況 O(n) | O(1) | 穩定 |
希爾排序 | 平均情況 O(n*Log2(n)) 最壞情況 O(n^2) 最好情況 O(n) | O(1) | 不穩定 |
2.選擇排序
2.1 氣泡排序
基本思想:
從一端開始,逐個比較相鄰的兩個元素,發現倒序即交換。這裡按從後往前(從下往上)逐個比較相鄰元素。
template<typename T>
void BubbleSort(T* elements, int n)
{
bool exchange = true;
for (int i=0; i<n && exchange; i++)
{
exchange = false;
for (int j=n-1; j>i; j--)
{
if (elements[j] < elements[j-1])
{
T temp = elements[j];
elements[j] = elements[j - 1];
elements[j - 1] = temp;
exchange = true;
}
}
}
}
2.2 快速排序
基本思想:
將資料劃分為兩部分,左邊的所有元素都小於右邊的所有元素;然後,對左右兩邊進行快速排序。
劃分方法:
選定一個參考點(中間元素),所有元素與之相比較,小的放左邊,大的放右邊。
template<typename T>
void partition(T a[], int low, int high, int &curPoint)
{
T mid = a[low]; //選定陣列的首元素作為中間元素
int i = low, j = high; //i是陣列的頭,j是陣列的尾
while (i != j)
{
//把右邊的比中間元素小的元素放到左邊
while (i < j && a[j] > mid) { j--; }
a[i] = a[j]; //if (i < j) { a[i] = a[j]; i++; }
//把左邊的比中間元素大的元素放到右邊
while (i < j && a[i] < mid) { i++; }
a[j] = a[i]; //if (i < j) { a[j] = a[i]; j--; }
}
//把中間元素放到中間
a[i] = mid;
//返回中間元素的位置
curPoint = i;
}
//對陣列下標為low~high的子表進行快速排序
template<typename T>
void QuickSort(T a[], int low, int high)
{
int i;
if (low < high)
{
partition(a, low, high, i);
QuickSort(a, low, i - 1);
QuickSort(a, i + 1, high);
}
}
一個更簡潔的實現如下:
template<typename T>
void QuickSort(int b, int e, T a[])
{
int i = b, j = e; //i是陣列的頭,j是陣列的尾
T x = a[(b + e) / 2]; //選取陣列的中間元素作為劃分的基準
while (i<j)
{
while (a[i]<x) i++;
while (a[j]>x) j--;
if (i <= j)
std::swap(a[i++], a[j--]);
}
if (i<e)
QuickSort(i, e, a);
if (j>b)
QuickSort(b, j, a);
}
2.3 小結
排序方法 | 時間複雜度 | 空間複雜度(輔助空間) | 穩定性 |
---|---|---|---|
氣泡排序 | 平均情況 O(n^2) 最壞情況 O(n^2) 最好情況 O(n) | O(1) | 穩定 |
快速排序 | 平均情況 O(nLog2(n)) 最壞情況 O(n^2) 最好情況 O(nLog2(n)) | O(1) | 不穩定 |
3.選擇排序
基本思想:
在每一趟排序中,從待排序子表中選出關鍵字最小(大)的元素放在其最終位置上。
3.1 直接選擇排序
基本思想:
在待排序子表中找出最大(小)元素,並將該元素放在子表的最前(後)面。
template<typename T>
void SelectSort(T A[], int n)
{
for (int i = 0; i < n - 1; i++)
{
T min = i;
//找到未排序子表中最小數的位置
for (int j = i + 1; j < n; j++)
if (A[j] < A[min])
min = j;
//將當前的最小數放到其位置上
if (min != i)
{
T temp = A[min];
A[min] = A[i];
A[i] = temp;
}
}
}
3.2 樹形選擇排序
基本思想:
1.樹形選擇排序(Tree Selection Sort),是一種按照錦標賽的思想進行選擇排序的方法。
2.首先對n個記錄進行兩兩比較,然後優勝者之間再進行兩兩比較,如此重複,直至選出最小關鍵字的記錄為止。這個過程可以用一棵有n個葉子結點的完全二叉樹表示。根節點中的關鍵字即為葉子結點中的最小關鍵字。
3.在輸出最小關鍵字之後,欲選出次小關鍵字,僅需將葉子結點中的最小關鍵字改為“最大值”,如∞,然後從該葉子結點開始,和其兄弟的關鍵字進行比較,修改從葉子結點到根的路徑上各結點的關鍵字,最後根結點的關鍵字即為次小關鍵字。
template<typename T>
void TreeSelectionSort(T data[], int n)
{
T MinValue = -100000000; //用該數表示負無窮
int baseSize; //剛好能儲存n個數的最小的2的冪,等於滿二叉樹最下一層的葉子樹
int treeSize; //整個二叉樹的節點數
int i;
T max;
int maxIndex;
baseSize = 1;
while (baseSize < n)
{
baseSize *= 2;
}
//用最下一層的葉子樹計算總節點數
treeSize = baseSize * 2 - 1;
//建立陣列存放二叉樹,資料從下標1開始存放
T* tree = new T[treeSize + 1]();
//將資料放入最下一層中
for (i = 0; i < n; i++)
{
tree[treeSize - i] = data[i];
}
//將二叉樹的其他節點初始化為負無窮,如果每輪是求一個最小值則應初始化為正無窮
for (; i < baseSize; i++)
{
tree[treeSize - i] = MinValue;
}
// 構造一棵樹,根節點是最大值
for (i = treeSize; i > 1; i -= 2)
{
tree[i / 2] = (tree[i] > tree[i - 1] ? tree[i] : tree[i - 1]);
}
n -= 1; //未排序的數量-1
while (n >= 0)
{
max = tree[1]; //取出最大值
data[n--] = max; //將當前找到的最大值放到陣列的最後面
//在二叉樹的最下面一層找到當前最大值的位置
maxIndex = treeSize;
while (tree[maxIndex] != max)
{
maxIndex--;
}
tree[maxIndex] = MinValue; //將樹中底層的現最大值替換為負無窮
//從被替換的位置向上計算,修改從葉子結點到根的路徑上各結點的值
while (maxIndex > 1)
{
if (maxIndex % 2 == 0)
{
tree[maxIndex / 2] = (tree[maxIndex] > tree[maxIndex + 1] ? tree[maxIndex] : tree[maxIndex + 1]);
}
else
{
tree[maxIndex / 2] = (tree[maxIndex] > tree[maxIndex - 1] ? tree[maxIndex] : tree[maxIndex - 1]);
}
maxIndex /= 2;
}
}
delete[] tree;
}
3.3 堆排序
實現堆排序還需要解決兩個問題:
(1)如何由一個無序序列建成一個堆?
(2)如何在輸出堆頂元素之後,調整剩餘元素成為一個新的堆?
問題(2)的解決方法是:
在輸出堆頂元素之後,以堆中最後一個元素替代之,此時根結點的左、右子樹均為堆,則僅需自上至下進行調整即可。
我們稱自堆頂至葉子的調整過程為“篩選”。
問題(1)的解決方法是:
從一個無序序列建堆的過程就是一個反覆“篩選”的過程。若將此序列看成是一個完全二叉樹,則最後一個非終端結點是第⌞n/2⌟個元素,由此“篩選”只需從第⌞n/2⌟個元素開始。
一個基於大根堆的升序排序演算法的簡單實現如下:
//將子序列(下標範圍:k~size)調整為大頂堆
template<typename T>
void Sift(T A[], int k, int size)
{
T x = A[k]; //k是待調整的子樹的根
int i = k //i指示空位
int j = 2*i; //j是i的左孩子
bool fininshed = false;
while (j <= size && !fininshed)
{
//讓j指向i的左右孩子中的較大者
if (j < size && A[j] < A[j + 1]) j = j + 1;
//如果根最大則直接結束
if (x >= A[j]) fininshed = true;
//否則,孩子節點比根節點大
else
{
A[i] = A[j]; //大的孩子節點上移
//繼續往下篩選
i = j;
j = 2 * i;
}
}
A[i] = x;
}
//通過大頂堆對陣列A進行升序排序,資料存放在下標1~n;
//實際上陣列尺寸為n+1,這樣做是為了便於二叉樹的處理,使得根節點的下標為1
template<typename T>
void HeapSort(T A[], int n)
{
//構建初始堆
for (int i = n / 2; i > 0; i--)
{
sift(A, i, n);
}
for (int i = n; i > 1; i--)
{
T x = A[1]; //輸出根
A[1] = A[i]; //交換當前最後一個元素和根的值
A[i] = x;
sift(A, 1, i - 1); //調整子序列為堆
}
}
使用函式比較大小可以方便地切換大/小頂堆,程式碼如下:
template<typename T>
bool fMax(const T& a, const T& b)
{
return a >= b;
}
template<typename T>
bool fMin(const T& a, const T& b)
{
return a <= b;
}
//將子序列(下標範圍:k~size)調整為(大/小)頂堆
template<typename T>
void Sift(T A[], int k, int size, bool (*compare)(const T&, const T&))
{
T x = A[k]; //k是待調整的子樹的根
int i = k; //i指示空位
int j = 2*i; //j是i的左孩子
bool fininshed = false;
while (j <= size && !fininshed)
{
//讓j指向i的左右孩子中的較(大/小)者
if (j < size && !compare(A[j], A[j + 1])) j = j + 1;
//如果根最(大/小)則直接結束
if (compare(x, A[j])) fininshed = true;
//否則,孩子節點比根節點(大/小)
else
{
A[i] = A[j]; //(大/小)的孩子節點上移
//繼續往下篩選
i = j;
j = 2 * i;
}
}
A[i] = x;
}
//通過(大/小)頂堆對陣列A進行(升/降)序排序,資料存放在下標1~n;實際上陣列尺寸為n+1,這樣做是為了便於二叉樹的處理,使得根節點的下標為1
template<typename T>
void HeapSort(T A[], int n, bool(*compare)(const T&, const T&))
{
//構建初始堆
for (int i = n / 2; i > 0; i--)
{
Sift(A, i, n, compare);
}
for (int i = n; i > 1; i--)
{
T x = A[1]; //輸出根
A[1] = A[i]; //交換當前最後一個元素和根的值
A[i] = x;
Sift(A, 1, i - 1, compare); //調整子序列為堆
}
}
int main()
{
int A[16] = { -1, 5,3,8,10,2,4,1,9,7,6,15,13,11,14,12};
HeapSort(A, sizeof(A) / sizeof(int) - 1, fMax); //使用大頂堆進行升序排序
HeapSort(A, sizeof(A) / sizeof(int) - 1, fMin);//使用小頂堆進行降序排序
return 0;
}
3.4 小結
排序方法 | 時間複雜度 | 空間複雜度(輔助空間) | 穩定性 |
---|---|---|---|
直接選擇排序 | 平均情況 O(n^2) 最壞情況 O(n^2) 最好情況 O(n^2) | O(1) | 不穩定 |
樹形選擇排序 | 平均情況 O(nLog2(n)) 最壞情況 O(nLog2(n)) 最好情況 O(nLog2(n)) | O(n-1) | 不穩定 |
堆排序 | 平均情況 O(nLog2(n)) 最壞情況 O(nLog2(n)) 最好情況 O(nLog2(n)) | O(1) | 不穩定 |
4. 歸併排序
基本思想:
是指將兩個或兩個以上的有序表合併成一個新的有序表。
利用歸併的思想進行排序:
首先將整個表看成是n個有序子表,每個子表的長度為1;
然後兩兩歸併,得到n/2個長度為2的有序子表;
然後再兩兩歸併,直至得到一個長度為n的有序表為止。
//合併陣列A中以a和b開始的長度為step的兩個子序列,n為整個陣列的長度
template<typename T>
void Merge(T A[], int start, int mid, int step, int n)
{
int rightLen = 0;
if (mid + step > n)
{
rightLen = n - mid;
}
else
{
rightLen = step;
}
//申請空間存放臨時結果
T *tempArray = new T[step + rightLen]();
int i = 0, j = 0, k = 0;
while (i < step && j < rightLen)
{
if (A[start + i] < A[mid + j])
{
tempArray[k++] = A[start + i];
i++;
}
else
{
tempArray[k++] = A[mid + j];
j++;
}
}
//如果左邊沒有歸併完,那麼直接將剩餘的元素複製到tempArray的後面
if (j == rightLen)
{
memcpy(tempArray + i + j, A + start + i, (step - i) * sizeof(T));
}
//如果右邊沒有歸併完,那麼直接將剩餘的元素複製到tempArray的後面
else if (i == step)
{
memcpy(tempArray + i + j, A + mid + j, (rightLen - j) * sizeof(T));
}
//將臨時陣列中排好序的內容複製回原陣列
memcpy(A + start, tempArray, (step + rightLen) * sizeof(T));
delete[] tempArray;
}
template<typename T>
void MergeSort(T A[], int n)
{
for (int step = 1; step < n; step *= 2)
{
for (int i = 0; i < n - step; i += 2*step)
{
Merge(A, i, i+step, step, n);
}
}
}
小結:
排序方法 | 時間複雜度 | 空間複雜度(輔助空間) | 穩定性 |
---|---|---|---|
歸併排序 | 平均情況 O(nLog2(n)) 最壞情況 O(nLog2(n)) 最好情況 O(nLog2(n)) | O(n) | 穩定 |