7中排序演算法學習總結(圖解+程式程式碼)
我們通常所說的排序演算法往往指的是內部排序演算法,即資料記錄在記憶體中進行排序。
排序演算法大體可分為兩種:
一種是比較排序,時間複雜度O(nlogn) ~ O(n^2),主要有:氣泡排序,選擇排序,插入排序,歸併排序,堆排序,快速排序等。
另一種是非比較排序,時間複雜度可以達到O(n),主要有:計數排序,基數排序,桶排序等。
常用排序演算法的時間複雜度:
這裡介紹一下穩定性的概念。如果原序列中有A1 = A2,排序前A1在A2的前面,排序後A1仍然在A2的前面,則說明這種排序演算法是穩定的。否則不穩定。
並不是說氣泡排序法就一直是穩定的,如果程式程式碼中有a[i]>=a[i+1]
現再開始通過程式程式碼和圖解的方式介紹各種常用的排序演算法
一、氣泡排序法
演算法思想:
氣泡排序是一種簡單的排序演算法。它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯-誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。這個演算法的名字由來是因為越小的元素會經由交換慢慢“浮”到數列的頂端。
步驟:
- 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
- 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。在這一點,最後的元素應該會是最大的數。
- 針對所有的元素重複以上的步驟,除了最後一個。
- 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
![](http://my.csdn.net/uploads/201207/20/1342782078_9990.jpg)
氣泡排序動態效果:
演算法實現:
/*氣泡排序法*/
// 分類 -------------- 內部比較排序
// 資料結構 ---------- 陣列
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- 如果序列在一開始已經大部分排序過的話,會接近O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
#include <stdio.h>
void Bubble_sort(int *a,int n)
{
int i,j;
for(i = 0;i<n;i++)
{
for(j = 0;j < n-i-1;j++)
{
if(a[j] > a[j+1])
{
int temp = a[j];
a[j] = a[j+1];
a[j+1] = temp;
}
}
}
}
int main(int argc ,char *argv[])
{
int i;
int array[10] = {13,58,61,43,97,6,1,84,66,31};
printf("排序後的陣列為:");
Bubble_sort(array,10);//呼叫氣泡排序函式,傳入陣列名和陣列長度
for(i = 0;i<sizeof(array)/sizeof(int);i++)//這裡可使用sizeof來求出陣列長度
printf("%d ",array[i]);
return 0;
}
- 二、簡單選擇排序——選擇排序法
- 演算法思想:
-
在要排序的一組數中,選出最小(或者最大)的一個數與第1個位置的數交換;然後在剩下的數當中再找最小(或者最大)的與第2個位置的數交換,依次類推,直到第n-1個元素(倒數第二個數)和第n個元素(最後一個數)比較為止。
第一趟,從n 個記錄中找出關鍵碼最小的記錄與第一個記錄交換;
第二趟,從第二個記錄開始的n-1 個記錄中再選出關鍵碼最小的記錄與第二個記錄交換;
以此類推.....
第i 趟,則從第i 個記錄開始的n-i+1 個記錄中選出關鍵碼最小的記錄與第i 個記錄交換,
直到整個序列按關鍵碼有序。
演算法流程圖:![](http://my.csdn.net/uploads/201207/18/1342586432_7130.jpg)
選擇排序動態效果:
演算法實現:
/*選擇排序法*/
// 分類 -------------- 內部比較排序
// 資料結構 ---------- 陣列
// 最差時間複雜度 ---- O(n^2)
// 最優時間複雜度 ---- O(n^2)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
#include <stdio.h>
void Selection_sort(int *a,int n)
{
int i,j,k;
for(i = 0 ; i < n-1 ; i++)
{
k = i;
for(j = i+1 ; j < n ; j++)
{
if(a[k] > a[j])
k = j; //找出最小值的陣列下標
}
if(k != i) //如果最小值的下標和要交換的位置下標不相等,就交換位置
{
int temp = a[k];
a[k] = a[i];
a[i] = temp;
}
}
}
int main(int argc ,char *argv[])
{
int i;
int array[10] = {8, 5, 2, 6, 9, 3, 1, 4, 0, 7};
printf("排序後的陣列為:");
Selection_sort(array,10);//呼叫氣泡排序函式,傳入陣列名和陣列長度
for(i = 0;i<sizeof(array)/sizeof(int);i++)//這裡可使用sizeof來求出陣列長度
printf("%d ",array[i]);
return 0;
}
演算法實現過程動態圖示:
三、直接插入排序
演算法思想:
插入排序是一種簡單直觀的排序演算法。它的工作原理非常類似於我們抓撲克牌
對於未排序資料(右手抓到的牌),在已排序序列(左手已經排好序的手牌)中從後向前掃描,找到相應位置並插入。
插入排序在實現上,通常採用in-place排序(即只需用到O(1)的額外空間的排序),因而在從後向前掃描過程中,需要反覆把已排序元素逐步向後挪位,為最新元素提供插入空間。
步驟:- 從第一個元素開始,該元素可以認為已經被排序
- 取出下一個元素,在已經排序的元素序列中從後向前掃描
- 如果該元素(已排序)大於新元素,將該元素移到下一位置
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
- 將新元素插入到該位置後
- 重複步驟2~5
插入排序動態效果:
演算法實現:
/*簡單插入排序*/
// 分類 ------------- 內部比較排序
// 資料結構 ---------- 陣列
// 最差時間複雜度 ---- 最壞情況為輸入序列是降序排列的,此時時間複雜度O(n^2)
// 最優時間複雜度 ---- 最好情況為輸入序列是升序排列的,此時時間複雜度O(n)
// 平均時間複雜度 ---- O(n^2)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 穩定
#include <stdio.h>
void Insertion_sort(int *a, int n)
{
int i,j,get;
for (i = 1; i < n; i++) // 類似抓撲克牌排序
{
get = a[i]; // 右手抓到一張撲克牌
j = i - 1; // 拿在左手上的牌總是排序好的
while (j >= 0 && a[j] > get) // 將抓到的牌與手牌從右向左進行比較
{
a[j + 1] = a[j]; // 如果該手牌比抓到的牌大,就將其右移
j--;
}
a[j + 1] = get;// 直到該手牌比抓到的牌小(或二者相等),將抓到的牌插入到該手牌右邊(相等元素的相對次序未變,所以插入排序是穩定的)
}
}
int main(int argc,char *argv[])
{
int i;
int array[8] = {6, 5, 3, 1, 8, 7, 2, 4};
Insertion_sort(array,8);
printf("插入排序後的陣列:\n");
for(i = 0;i<sizeof(array)/sizeof(int);i++)
{
printf("%d ",array[i]);
}
return 0;
}
演算法實現過程動態圖示:
四、插入排序——希爾排序
演算法思想:
希爾排序,也稱遞減增量排序演算法,是插入排序的一種高速而穩定的改進版本。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
1、插入排序在對幾乎已經排好序的資料操作時, 效率高, 即可以達到線性排序的效率
2、但插入排序一般來說是低效的, 因為插入排序每次只能將資料移動一位>
先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。
步驟:
- 選擇一個增量序列t1,t2,…,tk,其中ti>tj,tk=1;
- 按增量序列個數k,對序列進行k 趟排序;
- 每趟排序,根據對應的增量ti,將待排序列分割成若干長度為m 的子序列,分別對各子表進行直接插入排序。僅增量因子為1 時,整個序列作為一個表來處理,表長度即為整個序列的長度
![](http://my.csdn.net/uploads/201207/18/1342577299_5077.jpg)
希爾排序動態效果:
![視覺直觀感受7種常用排序演算法](http://jbcdn2.b0.upaiyun.com/2012/01/Visual-and-intuitive-feel-of-7-common-sorting-algorithms6.gif)
演算法實現:
/*希爾排序法*/
// 分類 -------------- 內部比較排序
// 資料結構 ---------- 陣列
// 最差時間複雜度 ---- 根據步長序列的不同而不同。已知最好的為O(n(logn)^2)
// 最優時間複雜度 ---- O(n)
// 平均時間複雜度 ---- 根據步長序列的不同而不同。
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
#include <stdio.h>
void Shell_sort(int *a,int n)
{
int i,j,h = 0,get;
while (h <= n) // 生成初始增量
{
h = 3*h + 1;
}
while (h >= 1)
{
for (i = h; i < n; i++)
{
j = i - h;
get = a[i];
while ((j >= 0) && (a[j] > get))
{
a[j + h] = a[j];
j = j - h;
}
a[j + h] = get;
}
h = (h - 1) / 3; // 遞減增量
}
}
int main(int argc,char *argv[])
{
int i;
int array[10] = {13,58,61,43,97,6,1,84,66,31};
Shell_sort(array,10);
printf("希爾排序後的陣列為:\n");
for(i = 0;i<sizeof(array)/sizeof(int);i++)
printf("%d ",array[i]);
return 0;
}
希爾排序是不穩定的排序演算法,雖然一次插入排序是穩定的,不會改變相同元素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂。
比如序列:{ 3, 5, 10, 8, 7, 2, 8, 1, 20, 6 },h=2時分成兩個子序列 { 3, 10, 7, 8, 20 } 和 { 5, 8, 2, 1, 6 } ,未排序之前第二個子序列中的8在前面,現在對兩個子序列進行插入排序,得到 { 3, 7, 8, 10, 20 } 和 { 1, 2, 5, 6, 8 } ,即 { 3, 1, 7, 2, 8, 5, 10, 6, 20, 8 } ,兩個8的相對次序發生了改變。
五、選擇排序——堆排序 演算法思想: 堆排序是指利用堆這種資料結構所設計的一種排序演算法。堆是一個近似完全二叉樹的結構(通常堆是通過一維陣列來實現的),並同時滿足堆的性質:即子結點的鍵值總是小於(或者大於)它的父節點。堆的定義如下:具有n個元素的序列(k1,k2,...,kn),當且僅當滿足
![](http://my.csdn.net/uploads/201207/18/1342589718_3742.jpg)
時稱之為堆。由堆的定義可以看出,堆頂元素(即第一個元素)必為最小項(小頂堆)。
若以一維陣列儲存一個堆,則堆對應一棵完全二叉樹,且所有非葉結點的值均不大於(或不小於)其子女的值,根結點(堆頂元素)的值是最小(或最大)的。如:
(a)大頂堆序列:(96, 83,27,38,11,09)
(b) 小頂堆序列:(12,36,24,85,47,30,53,91)
初始時把要排序的n個數的序列看作是一棵順序儲存的二叉樹(一維陣列儲存二叉樹),調整它們的儲存序,使之成為一個堆,將堆頂元素輸出,得到n 個元素中最小(或最大)的元素,這時堆的根節點的數最小(或者最大)。然後對前面(n-1)個元素重新調整使之成為堆,輸出堆頂元素,得到n 個元素中次小(或次大)的元素。依此類推,直到只有兩個節點的堆,並對它們作交換,最後得到有n個節點的有序序列。稱這個過程為堆排序。
因此,實現堆排序需解決兩個問題:
1. 如何將n 個待排序的數建成堆;
2. 輸出堆頂元素後,怎樣調整剩餘n-1 個元素,使其成為一個新堆。
首先討論第二個問題:輸出堆頂元素後,對剩餘n-1元素重新建成堆的調整過程。
調整小頂堆的方法:
1)設有m 個元素的堆,輸出堆頂元素後,剩下m-1 個元素。將堆底元素送入堆頂((最後一個元素與堆頂進行交換),堆被破壞,其原因僅是根結點不滿足堆的性質。
2)將根結點與左、右子樹中較小元素的進行交換。
3)若與左子樹交換:如果左子樹堆被破壞,即左子樹的根結點不滿足堆的性質,則重複方法 (2).
4)若與右子樹交換,如果右子樹堆被破壞,即右子樹的根結點不滿足堆的性質。則重複方法 (2).
5)繼續對不滿足堆性質的子樹進行上述交換操作,直到葉子結點,堆被建成。
稱這個自根結點到葉子結點的調整過程為篩選。如圖:
再討論對n 個元素初始建堆的過程。
建堆方法:對初始序列建堆的過程,就是一個反覆進行篩選的過程。
1)n 個結點的完全二叉樹,則最後一個結點是第n/2個結點的子樹。
2)篩選從第n/2個結點為根的子樹開始,該子樹成為堆。
3)之後向前依次對各結點為根的子樹進行篩選,使之成為堆,直到根結點。
如圖建堆初始過程:無序序列:(49,38,65,97,76,13,27,49)
步驟:
- 建立一個堆
- 把堆頂元素(最大值)和堆尾元素互換
- 把堆的尺寸縮小1,並呼叫heapify(A, 0)從新的堆頂元素開始進行堆調整
- 重複步驟2,直到堆的尺寸為1
![](http://images2015.cnblogs.com/blog/739525/201603/739525-20160328213839160-2037856208.gif)
演算法實現:
/*堆排序演算法*/
// 分類 -------------- 內部比較排序
// 資料結構 ---------- 陣列
// 最差時間複雜度 ---- O(nlogn)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(1)
// 穩定性 ------------ 不穩定
#include <stdio.h>
int heapsize; // 堆大小
void exchange(int A[], int i, int j) // 交換A[i]和A[j]
{
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
void heapify(int A[], int i) // 堆調整函式(這裡使用的是最大堆)
{
int leftchild = 2 * i + 1; // 左孩子索引
int rightchild = 2 * i + 2; // 右孩子索引
int largest; // 選出當前結點與左右孩子之中的最大值
if (leftchild < heapsize && A[leftchild] > A[i])
largest = leftchild;
else
largest = i;
if (rightchild < heapsize && A[rightchild] > A[largest])
largest = rightchild;
if (largest != i)
{
exchange(A, i, largest); // 把當前結點和它的最大(直接)子節點進行交換
heapify(A, largest); // 遞迴呼叫,繼續從當前結點向下進行堆調整
}
}
void buildheap(int A[], int n) // 建堆函式
{
int i;
heapsize = n;
for ( i = heapsize / 2 - 1; i >= 0; i--) // 對每一個非葉結點
heapify(A, i); // 不斷的堆調整
}
void heapsort(int A[], int n)
{
int i;
buildheap(A, n);
for (i = n - 1; i >= 1; i--)
{
exchange(A, 0, i); // 將堆頂元素(當前最大值)與堆的最後一個元素互換(該操作很有可能把後面元素的穩定性打亂,所以堆排序是不穩定的排序演算法)
heapsize--; // 從堆中去掉最後一個元素
heapify(A, 0); // 從新的堆頂元素開始進行堆調整
}
}
int main()
{
int i;
int A[] = { 5, 2, 9, 4, 7, 6, 1, 3, 8 };// 從小到大堆排序
int n = sizeof(A) / sizeof(int);
heapsort(A, n);
printf("堆排序結果:");
for (i = 0; i < n; i++)
{
printf("%d ", A[i]);
}
printf("\n");
return 0;
}
堆排序是不穩定的排序演算法,不穩定發生在堆頂元素與A[i]交換的時刻。
比如序列:{ 9, 5, 7, 5 },堆頂元素是9,堆排序下一步將9和第二個5進行交換,得到序列 { 5, 5, 7, 9 },再進行堆調整得到{ 7,5, 5, 9 },重複之前的操作最後得到{ 5, 5, 7, 9 }從而改變了兩個5的相對次序。
六、歸併排序
演算法思想:
歸併排序是建立在歸併操作上的一種有效的排序演算法,效率為O(nlogn),1945年由馮·諾伊曼首次提出。
歸併排序的實現分為遞迴實現與非遞迴(迭代)實現。遞迴實現的歸併排序是演算法設計中分治策略的典型應用,我們將一個大問題分割成小問題分別解決,然後用所有小問題的答案來解決整個大問題。非遞迴(迭代)實現的歸併排序首先進行是兩兩歸併,然後四四歸併,然後是八八歸併,一直下去直到歸併了整個陣列。
歸併(Merge)排序法是將兩個(或兩個以上)有序表合併成一個新的有序表,即把待排序序列分為若干個子序列,每個子序列是有序的。然後再把有序子序列合併為整體有序序列。
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
- 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
- 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
- 重複步驟3直到某一指標到達序列尾
- 將另一序列剩下的所有元素直接複製到合併序列尾
歸併排序動態效果:
演算法實現:
/*歸併排序演算法*/
// 分類 -------------- 內部比較排序
// 資料結構 ---------- 陣列
// 最差時間複雜度 ---- O(nlogn)
// 最優時間複雜度 ---- O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(n)
// 穩定性 ------------ 穩定
#include <stdio.h>
#include <limits.h> // 包含極限值的標頭檔案,這裡用到了無窮大INT_MAX
int L[10]; // 兩個子陣列定義成全域性變數(輔助儲存空間,大小正比於元素的個數)
int R[10];
void merge(int A[], int left, int middle, int right)// 合併兩個已排好序的陣列A[left...middle]和A[middle+1...right]
{
int i,j,k;
int n1 = middle - left + 1; // 兩個陣列的大小
int n2 = right - middle;
for (i = 0; i < n1; i++) // 把兩部分分別拷貝到兩個陣列中
L[i] = A[left + i];
for (j = 0; j < n2; j++)
R[j] = A[middle + j + 1];
L[n1] = INT_MAX; // 使用無窮大作為哨兵值放在子陣列的末尾
R[n2] = INT_MAX; // 這樣可以免去檢查某個子陣列是否已讀完的步驟
i = 0;
j = 0;
for (k = left; k <= right; k++) // 依次比較兩個子陣列中的值,每次取出更小的那一個放入原陣列
{
if (L[i] <= R[j])
{
A[k] = L[i];
i++;
}
else
{
A[k] = R[j];
j++;
}
}
}
void mergesort_recursion(int A[], int left, int right) // 遞迴實現的歸併排序(自頂向下)
{
int middle = (left + right) / 2;
if (left < right) // 當待排序的序列長度為1時(left == right),遞迴“開始回升”
{
mergesort_recursion(A, left, middle);
mergesort_recursion(A, middle + 1, right);
merge(A, left, middle, right);
}
}
void mergesort_iteration(int A[], int left, int right) // 非遞迴(迭代)實現的歸併排序(自底向上)
{
int size;
int low, middle, high; // 子陣列索引,前一個為A[low...middle],後一個子陣列為A[middle+1...high]
for (size = 1; size <= right - left; size *= 2) // 子陣列的大小初始為1,每輪翻倍
{
low = left;
while (low + size - 1 <= right - 1 )// 後一個子陣列存在(需要歸併)
{
middle = low + size - 1;
high = middle + size;
if(high > right)// 後一個子陣列大小不足size
high = right;
merge(A, low, middle, high); //合併
low = high + 1; //前一個子陣列索引向後移動
}
}
}
int main()
{
int i;
int A1[] = { 6, 5, 3, 1, 8, 7, 2, 4 }; // 從小到大歸併排序
int A2[] = { 6, 5, 3, 1, 8, 7, 2, 4 };
int n1 = sizeof(A1) / sizeof(int);
int n2 = sizeof(A2) / sizeof(int);
mergesort_recursion(A1, 0, n1 - 1); // 遞迴實現
mergesort_iteration(A2, 0, n2 - 1); // 非遞迴實現
printf("遞迴實現的歸併排序結果:");
for (i = 0; i < n1; i++)
{
printf("%d ",A1[i]);
}
printf("\n");
printf("非遞迴實現的歸併排序結果:");
for (i = 0; i < n2; i++)
{
printf("%d ", A2[i]);
}
printf("\n");
return 0;
}
上述程式碼對序列{ 6, 5, 3, 1, 8, 7, 2, 4 }進行歸併排序的例項如下
七、快速排序
演算法思想:
快速排序是由東尼·霍爾所發展的一種排序演算法。在平均狀況下,排序n個元素要O(nlogn)次比較。在最壞狀況下則需要O(n^2)次比較,但這種狀況並不常見。事實上,快速排序通常明顯比其他O(nlogn)演算法更快,因為它的內部迴圈可以在大部分的架構上很有效率地被實現出來。
快速排序使用分治策略(Divide and Conquer)來把一個序列分為兩個子序列。
步驟:
- 從序列中挑出一個元素,作為"基準"(pivot).
- 把所有比基準值小的元素放在基準前面,所有比基準值大的元素放在基準的後面(相同的數可以到任一邊),這個稱為分割槽(partition)操作。
- 對每個分割槽遞迴地進行步驟1~3,遞迴的結束條件是序列的大小是0或1,這時整體已經被排好序了。
快速排序法動態效果:
演算法實現:
/*快速排序法*/
// 分類 ------------ 內部比較排序
// 資料結構 --------- 陣列
// 最差時間複雜度 ---- 每次選取的基準都是最大的元素(或者每次都是最小),導致每次只劃分出了一個子序列,需要進行n-1次劃分才能結束遞迴,時間複雜度為O(n^2)
// 最優時間複雜度 ---- 每次選取的基準都能使劃分均勻,只需要logn次劃分就能結束遞迴,時間複雜度為O(nlogn)
// 平均時間複雜度 ---- O(nlogn)
// 所需輔助空間 ------ O(logn)~O(n),主要是遞迴造成的棧空間的使用(用來儲存left和right等區域性變數),取決於遞迴樹的深度
// 一般為O(logn),最差為O(n)(基本有序的情況)
// 穩定性 ---------- 不穩定
#include <stdio.h>
int quicksort(int v[], int left, int right) //傳入陣列的最低和最高的下標
{
if(left < right)
{
int key = v[left]; //將陣列最左邊的值作為基準值用以比較
int low = left;
int high = right;
while(low < high) //遍歷陣列
{
while(low < high && v[high] >= key) //將大於等於基準值的值放在陣列右邊
{
high--; //最高位陣列下標自減
}
v[low] = v[high]; //如果高位下標的值小於基準值就放到陣列左邊
while(low < high && v[low] < key) //將小於基準值的值放在陣列左邊
{
low++; //最低位陣列下標自增
}
v[high] = v[low]; //如果低位下標的值大於基準值就放到陣列右邊
}
v[low] = key; //最後將基準值放入陣列
quicksort(v,left,low-1); //遍歷呼叫排序函式
quicksort(v,low+1,right);
}
}
int main(int argc, char *argv[])
{
int array[10] = {8,1,4,2,10,3,5,9,7,6};
int i;
printf("快速排序後的陣列為:");
quicksort(array,0,9);
for( i = 0; i < 10; i++ )
printf("%d ",array[i]);
printf("\n");
return 0;
}
快速排序是不穩定的排序演算法,不穩定發生在基準元素與A[tail+1]交換的時刻。
比如序列:{ 1, 3, 4, 2, 8, 9, 8, 7, 5 },基準元素是5,一次劃分操作後5要和第一個8進行交換,從而改變了兩個元素8的相對次序。
總結
各種排序的穩定性,時間複雜度和空間複雜度總結:
我們比較時間複雜度函式的情況:
時間複雜度函式O(n)的增長情況
所以對n較大的排序記錄。一般的選擇都是時間複雜度為O(nlog2n)的排序方法。
時間複雜度來說:
(1)平方階(O(n2))排序
各類簡單排序:直接插入、直接選擇和氣泡排序; (2)線性對數階(O(nlog2n))排序
快速排序、堆排序和歸併排序; (3)O(n1+§))排序,§是介於0和1之間的常數。
希爾排序(4)線性階(O(n))排序
基數排序,此外還有桶、箱排序。
說明:
當原表有序或基本有序時,直接插入排序和氣泡排序將大大減少比較次數和移動記錄的次數,時間複雜度可降至O(n);
而快速排序則相反,當原表基本有序時,將蛻化為氣泡排序,時間複雜度提高為O(n2);
原表是否有序,對簡單選擇排序、堆排序、歸併排序和基數排序的時間複雜度影響不大。
穩定性:
排序演算法的穩定性:若待排序的序列中,存在多個具有相同關鍵字的記錄,經過排序,
這些記錄的相對次序保持不變,則稱該演算法是穩定的;若經排序後,記錄的相對 次序發生了改變,則稱該演算法是不穩定的。
穩定性的好處:排序演算法如果是穩定的,那麼從一個鍵上排序,然後再從另一個鍵上排序,第一個鍵排序的結果可以為第二個鍵排序所用。基數排序就是這樣,先按低位排序,逐次按高位排序,低位相同的元素其順序再高位也相同時是不會改變的。另外,如果排序演算法穩定,可以避免多餘的比較;
穩定的排序演算法:氣泡排序、插入排序、歸併排序和基數排序
不是穩定的排序演算法:選擇排序、快速排序、希爾排序、堆排序
選擇排序演算法準則:
每種排序演算法都各有優缺點。因此,在實用時需根據不同情況適當選用,甚至可以將多種方法結合起來使用。
選擇排序演算法的依據
影響排序的因素有很多,平均時間複雜度低的演算法並不一定就是最優的。相反,有時平均時間複雜度高的演算法可能更適合某些特殊情況。同時,選擇演算法時還得考慮它的可讀性,以利於軟體的維護。一般而言,需要考慮的因素有以下四點:
1.待排序的記錄數目n的大小;
2.記錄本身資料量的大小,也就是記錄中除關鍵字外的其他資訊量的大小;
3.關鍵字的結構及其分佈情況;
4.對排序穩定性的要求。
設待排序元素的個數為n.
1)當n較大,則應採用時間複雜度為O(nlog2n)的排序方法:快速排序、堆排序或歸併排序序。
快速排序:是目前基於比較的內部排序中被認為是最好的方法,當待排序的關鍵字是隨機分佈時,快速排序的平均時間最短;
堆排序 : 如果記憶體空間允許且要求穩定性的,
歸併排序:它有一定數量的資料移動,所以我們可能過與插入排序組合,先獲得一定長度的序列,然後再合併,在效率上將有所提高。
2) 當n較大,記憶體空間允許,且要求穩定性 =》歸併排序
3)當n較小,可採用直接插入或直接選擇排序。
直接插入排序:當元素分佈有序,直接插入排序將大大減少比較次數和移動記錄的次數。
直接選擇排序 :元素分佈有序,如果不要求穩定性,選擇直接選擇排序
5)一般不使用或不直接使用傳統的氣泡排序。
6)基數排序它是一種穩定的排序演算法,但有一定的侷限性:
1、關鍵字可分解。
2、記錄的關鍵字位數較少,如果密集更好
3、如果是數字時,最好是無符號的,否則將增加相應的映射覆雜度,可先將其正負分開排序。