前言
排序算法在計算機科學入門課程中很普遍,在學習排序算法的時候,涉及到大量的各種核心算法概念,例如大O表示法,分治法,堆和二叉樹之類的數據結構,隨機算法,最佳、最差和平均情況分析,時空權衡以及上限和下限,本文就介紹了十二種排序算法供大家學習。
簡介
排序算法是用來根據元素對應的比較運算符重新排列給定的數組的算法,輸出的數組是一個根據比較符從小到大或者從大到小依次排列的數組。比較運算符是用於確定相應數據結構中元素的新順序,比如在整數數組里面,對應的比較符號就是大於或者小於號,用戶也可以自己定義對應的比較運算符。
比如如果輸入是[4,2,3,1]
,按照從小到大輸出,結果應該是[1,2,3,4]
特性
穩定性
如果在數組中有兩個元素是相等的,在經過某個排序算法之后,原來在前面的的那個元素仍然在另一個元素的前面,那么我們就說這個排序算法是穩定的。
如果在排序之后,原來的兩個相等元素中在前面的一個元素被移到了后面,那么這個算法就是不穩定的。
比如排序之前數組為[3(a),2,3(b)]
(其中a
和b
分別代表兩個不同的3
),經過某個排序算法之后是[2,3(a),3(b)]
,那么這個算法就是穩定的;如果變成了[2,3(b),3(a)]
,那么這個算法是不穩定的。
再比如在按照身高排隊去食堂打飯的過程中,小明和小剛的身高都是170,原來小明在小剛前面,但是經過排序之后小明發現小剛到了他前面了,這樣小明肯定對這個不穩定的排序有意見。
時間復雜度
時間復雜度反映了算法的排序效率,通常用大O表示法來表示,通常暗示這個算法需要的最多操作次數的量級,比如\(O(n)\)表示最多需要進行\(n\)量級操作。
空間復雜度
空間復雜度反映了算法需要消耗的空間,比如\(O(1)\)表示只需要常數量級的空間,不會隨着數組大小的變化而變化。
如果一個排序算法不需要額外的存儲空間,可以直接在原來的數組完成排序操作,這個算法可以被稱之為原地算法,空間復雜度是\(O(1)\)
比較排序、非比較排序
如果一個算法需要在排序的過程中使用比較操作來判斷兩個元素的大小關系,那么這個排序算法就是比較排序,大部分排序算法都是比較排序,比如冒泡排序、插入排序、堆排序等等,這種排序算法的平均時間復雜度最快也只能是\(O(nlogn)\)。
非比較排序比較典型的有計數排序、桶排序和基數排序,這類排序能夠脫離比較排序時間復雜度的束縛,達到\(O(n)\)級別的效率。
算法
首先定義基本的交換數組元素的基本方法,節省后面的代碼量。
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
冒泡排序
冒泡排序是從左到右依次比較相鄰的兩個元素,如果前一個元素比較大,就把前一個元素和后一個交換位置,遍歷數組之后保證最后一個元素相對於前面的永遠是最大的。然后讓最后一個保持不變,重新遍歷前n-1
個元素,保證第n-1
個元素在前n-1
個元素里面是最大的。依此規律直到第2
個元素是前2
個元素里面最大的,排序就結束了。
因為這個排序的過程很像冒泡泡,找到最大的元素不停的移動到最后端,所以這個排序算法就叫冒泡排序。
圖片來自這里
用Java代碼實現
private void bubbleSort(int[] nums) {
for (int i = nums.length - 1; i >= 1; i--) { // 冒泡得到n-1個最大值
for (int j = 1; j <= i; j++) {
if (nums[j-1]>nums[j])
swap(nums, j, j-1); // 交換得到較大值
}
}
}
冒泡排序的最大特點就是代碼簡單,短短的五行代碼就能完成整個排序的操作。
時間復雜度比較穩定不管怎樣都需要\(O(n^2)\)次比較,所以是\(O(n^2)\)的時間復雜度。
空間復雜度是\(O(1)\),所有操作在原來的數組完成就可以了,不需要額外的空間。
算法是穩定的,在冒泡的過程中如果兩個元素相等,那么他們的位置是不會交換的。
選擇排序
選擇排序的思路比較簡單,先找到前n
個元素中最大的值,然后和最后一個元素交換,這樣保證最后一個元素一定是最大的,然后找到前n-1
個元素中的最大值,和第n-1
個元素進行交換,然后找到前n-2
個元素中最大值,和第n-2
個元素交換,依次類推到第2個元素,這樣就得到了最后的排序數組。
其實整個過程和冒泡排序差不多,都是要找到最大的元素放到最后,不同點是冒泡排序是不停的交換元素,而選擇排序只需要在每一輪交換一次。
原圖來自這里
代碼實現:
private void selectionSort(int[] nums) {
for (int i = nums.length - 1; i > 0; i--) {
int maxIndex = 0; // 最大元素的位置
for (int j = 0; j <= i; j++) {
if (nums[maxIndex]<nums[j]) {
maxIndex = j;
}
}
swap(nums, maxIndex, i); // 把這個最大的元素移到最后
}
}
時間復雜度和冒泡排序一樣比較穩定,都需要\(O(n^2)\)次比較,所以時間復雜度是\(O(n^2)\)
空間復雜度是\(O(1)\),不需要額外空間,是原地算法。
選擇排序最簡單的版本是不穩定的,比如數組[1,3,2,2]
,表示為[1,3,2(a),2(b)]
,在經過一輪遍歷之后變成了[1,2(b),2(a),3]
,兩個2
之間的順序因為第一個2
和3
的調換而顛倒了,所以不是穩定排序。
不過可以改進一下選擇排序變成穩定的。原來不穩定是因為交換位置導致的,現在如果改成插入操作(不是使用數組而是鏈表,把最大的元素插入到最后)的話,就能變成穩定排序。比如[1,3,2(a),2(b)]
,在第一輪中變成了[1,2(a),2(b),3]
,這樣就能夠保持相對位置,變成穩定排序。
插入排序
插入排序的核心思想是遍歷整個數組,保持當前元素左側始終是排序后的數組,然后將當前元素插入到前面排序完成的數組的對應的位置,使其保持排序狀態。有點動態規划的感覺,類似於先把前i-1
個元素排序完成,再插入第i
個元素,構成i
個元素的有序數組。
圖片來自這里
簡單代碼實現:
private void insertionSort(int[] nums) {
for (int i = 1; i < nums.length; i++) { // 從第二個元素開始遍歷
int j = i;
while (j>0&&nums[j]<nums[j-1]) { // 將當前元素移動到合適的位置
swap(nums, j, j-1);
j--;
}
}
}
時間復雜度上,插入排序在最好的情況,也就是數組已經排好序的時候,復雜度是\(O(n)\),在其他情況下都是\(O(n^2)\)。
空間復雜度是\(O(1)\),不需要額外的空間,是原地算法。
插入排序是穩定排序,每次交換都是相鄰元素的交換,不會有選擇排序的那種跳躍式交換元素。
希爾排序
希爾排序可以看作是一個冒泡排序或者插入排序的變形。希爾排序在每次的排序的時候都把數組拆分成若干個序列,一個序列的相鄰的元素索引相隔的固定的距離gap
,每一輪對這些序列進行冒泡或者插入排序,然后再縮小gap
得到新的序列一一排序,直到gap
為1
比如對於數組[5,2,4,3,1,2]
,第一輪gap=3
拆分成[5,3]
、[2,1]
和[4,2]
三個數組進行插入排序得到[3,1,2,5,2,4]
;第二輪gap=2
,拆分成[3,2,2]
和[1,5,4]
進行插入排序得到[2,1,2,4,3,5]
;最后gap=1
,全局插入排序得到[1,2,2,3,4,5]
圖片來自這里
簡單代碼實現:
private void shellSor(int[] nums) {
int gap = nums.length >> 1;
while (gap > 0) {
for (int i = 0; i < gap; i++) { // 對每個子序列進行排序
for (int j = i+gap; j < nums.length; j+=gap) { // 插入排序的部分
int temp = j;
while (temp > i && nums[temp] < nums[temp-gap]) {
swap(nums, temp, temp-gap);
temp -= gap;
}
}
}
gap >>= 1;
}
}
Donald Shell於1959年發布了這種排序算法,運行時間在很大程度上取決於它使用的間隔,在實際使用中,其時間復雜度仍然是一個懸而未決的問題,基本在\(O(n^2)\)和\(O(n^{4/3})\)之間。
空間復雜度是\(O(1)\),是原地算法。
這個算法是不穩定的,里面有很多不相鄰元素的交換操作。
歸並排序
歸並排序是典型的使用分治思想(divide-and-conquer)解決問題的案例。在排序的過程中,把原來的數組變成左右兩個數組,然后分別進行排序,當左右的子數組排序完畢之后,再合並這兩個子數組形成一個新的排序數組。整個過程遞歸進行,當只剩下一個元素或者沒有元素的時候就直接返回。
圖片來自這里
代碼如下:
private void mergeSort(int[] nums, int left, int right) { // 需要左右邊界確定排序范圍
if (left >= right) return;
int mid = (left+right) / 2;
mergeSort(nums, left, mid); // 先對左右子數組進行排序
mergeSort(nums, mid+1, right);
int[] temp = new int[right-left+1]; // 臨時數組存放合並結果
int i=left,j=mid+1;
int cur = 0;
while (i<=mid&&j<=right) { // 開始合並數組
if (nums[i]<=nums[j]) temp[cur] = nums[i++];
else temp[cur] = nums[j++];
cur++;
}
while (i<=mid) temp[cur++] = nums[i++];
while (j<=right) temp[cur++] = nums[j++];
for (int k = 0; k < temp.length; k++) { // 合並數組完成,拷貝到原來的數組中
nums[left+k] = temp[k];
}
}
時間復雜度上歸並排序能夠穩定在\(O(nlogn)\)的水平,在每一級的合並排序數組過程中總的操作次數是\(n\),總的層級數是\(logn\),相乘得到最后的結果就是\(O(nlogn)\)
空間復雜度是\(O(n)\),因為在合並的過程中需要使用臨時數組來存放臨時排序結果。
歸並排序是穩定排序,保證原來相同的元素能夠保持相對的位置。
快速排序
快速排序(有時稱為分區交換排序)是一種高效的排序算法。由英國計算機科學家Tony Hoare於1959年開發並於1961年發表,它在現在仍然是一種常用的排序算法。如果實現方法恰當,它可以比主要競爭對手(歸並排序和堆排序)快兩到三倍。
其核心的思路是取第一個元素(或者最后一個元素)作為分界點,把整個數組分成左右兩側,左邊的元素小於或者等於分界點元素,而右邊的元素大於分界點元素,然后把分界點移到中間位置,對左右子數組分別進行遞歸,最后就能得到一個排序完成的數組。當子數組只有一個或者沒有元素的時候就結束這個遞歸過程。
其中最重要的是將整個數組根據分界點元素划分成左右兩側的邏輯,目前有兩種算法,圖片展示的是第一種。
圖片來自這里
第一種實現,也是圖片中的排序邏輯的實現:
private void quickSort(int[] nums, int left, int right) {
if (left >= right) return;
int lo = left+1; // 小於分界點元素的最右側的指針
int hi = right; // 大於分界點元素的最左側的指針
while (lo<=hi) {
if (nums[lo]>nums[left]) { // 交換元素確保左側指針指向元素小於分界點元素
swap(nums, lo, hi);
hi--;
} else {
lo++;
}
}
lo--; // 回到小於分界點元素數組的最右側
swap(nums, left, lo); // 將分界點元素移到左側數組最右側
quickSort2(nums, left, lo-1);
quickSort2(nums, lo+1, right);
}
第二種,不用hi
來標記大於分界點元素的最右側,而是只用一個lo
來標記最左側。在遍歷整個數組的過程中,如果發現了一個小於等於分界點元素的元素,就和lo+1
位置的元素交換,然后lo
自增,這樣可以保證lo
的左側一定都是小於等於分界點元素的,遍歷到最后lo
的位置就是新的分界點位置,和最開始的分界點元素位置互換。
private void quickSort(int[] nums, int left, int right) {
if (left>=right) return;
int cur = left + 1; // 從左側第二個元素開始
int lo = left; // 分界點為第一個元素
while (cur <= right) {
if (nums[cur] <= nums[left]) { // 交換位置保證lo的左側都是小於num[left]
swap(nums, lo+1, cur);
lo ++;
}
cur++;
}
swap(nums, left, lo); // 把分界點元素移動到新的分界位置
quickSort(nums, left, lo-1);
quickSort(nums, lo+1, right);
}
時間復雜度在最佳情況是\(O(nlogn)\),但是如果分界點元素選擇不當可能會惡化到\(O(n^2)\),但是這種情況比較少見(比如數組完全逆序),如果隨機選擇分界點的話,時間復雜度能夠穩定在\(O(nlogn)\)。另外如果元素中相同元素數量比較多的話,也會降低排序性能。
空間復雜度在\(O(logn)\)水平,屬於堆棧調用,在最壞的情況下空間復雜度還是\(O(n)\),平均情況下復雜度是\(O(logn)\)
快速排序是不穩定的,因為包含跳躍式交換元素位置。
堆排序
堆排序是一個效率要高得多的選擇排序,首先把整個數組變成一個最大堆,然后每次從堆頂取出最大的元素,這樣依次取出的最大元素就形成了一個排序的數組。堆排序的核心分成兩個部分,第一個是新建一個堆,第二個是彈出堆頂元素后重建堆。
新建堆不需要額外的空間,而是使用原來的數組,一個數組在另一個維度上可以當作一個完全二叉樹(除了最后一層之外其他的每一層都被完全填充,並且所有的節點都向左對齊),對於下標為i
的元素,他的子節點是2*i+1
和2*i+2
(前提是沒有超出邊界)。在新建堆的時候從左向右開始遍歷,當遍歷到一個元素的時候,重新排列從這個元素節點到根節點的所有元素,保證滿足最大堆的要求(父節點比子節點要大)。遍歷完整個數組的時候,這個最大堆就完成了。
在彈出根節點之后(把根節點的元素和樹的最底層最右側的元素互換),堆被破壞,需要重建。從根節點開始和兩個子節點比較,如果父節點比最大的子節點小,那么就互換父節點和最大的子節點,然后把互換后在子節點位置的父節點當作新的父節點,和它的子節點比較,如此往復直到最后一層,這樣最大堆就重建完畢了。
圖片來自這里
簡單java代碼:
private void heapSort(int[] nums) {
heapify(nums); // 新建一個最大堆
for (int i = nums.length - 1; i >= 1; i--) {
swap(nums, 0, i); // 彈出最大堆的堆頂放在最后
rebuildHeap(nums, 0,i-1); // 重建最大堆
}
}
private void heapify(int[] nums) {
for (int i = 1; i < nums.length; i++) {
int par = (i-1)>>1; // 找到父節點
int child = i; // 定義子節點
while (child>0&&nums[par]<nums[child]) { // 從子節點到根節點構建最大堆
swap(nums, par, child);
child = par;
par = (par-1) >> 1;
}
}
}
private void rebuildHeap(int[] nums, int par, int last) {
int left = 2*par+1; // 左子節點
int right = 2*par+2; // 右子節點
int maxIndex = left;
if (right<=last && nums[right]>nums[left]) { // 找到最大子節點
maxIndex = right;
}
if (left<=last && nums[par] < nums[maxIndex]) {// 和最大子節點比較
swap(nums, par, maxIndex); // 互換到最大子節點
rebuildHeap(nums, maxIndex, last); // 重建最大子節點代表的子樹
}
}
時間復雜度穩定在\(O(nlogn)\),因為在構建堆的時候時間遍歷數組對於每個元素需要進行\(O(logn)\)次比較,時間復雜度是\(O(nlogn)\)。在彈出每個元素重建堆需要\(O(logn)\)的復雜度,時間復雜度也是\(O(nlogn)\),所以整體的時間復雜度是\(O(nlogn)\)
空間復雜度是\(O(1)\),在原數組進行所有操作就可以了。
堆排序是不穩定,堆得構建和重建的過程都會打亂元素的相對位置。
堆排序的代碼量相對於其他的排序算法來說是比較多的,理解上也比較難,涉及到最大堆和二叉樹等相關概念。雖然在實際使用中相對於快速排序不是那么好用,但是最壞情況下的\(O(nlogn)\)的時間復雜度也是優於快排的。空間使用是恆定的,是優於歸並排序。
二叉搜索樹排序
二叉樹搜索排序用數組內的所有元素構建一個搜索二叉樹,然后用中序遍歷重新將所有的元素填充回原來的數組中。因為搜索二叉樹不能用數組來表示,所以必須使用額外的數據結構來構建二叉樹。
圖片來自這里
簡單代碼如下:
private int[] bstSort(int[] nums) {
TreeNode root = new TreeNode(nums[0]); // 構建根節點
for (int i = 1; i < nums.length; i++) { // 將所有的元素插入到二叉搜索樹中
buildTree(root, nums[i]);
}
inorderTraversal(root, nums, new int[1]);// 中序遍歷獲取二叉樹中的所有節點
return nums;
}
private void inorderTraversal(TreeNode node, int[] nums, int[] pos) {
if (node == null) return;
inorderTraversal(node.left, nums, pos);
nums[pos[0]++] = node.val;
inorderTraversal(node.right, nums, pos);
}
private void buildTree(TreeNode node, int num) {
if (node == null) return;
if (num >= node.val) { // 插入到右子樹中
if (node.right == null) {
node.right = new TreeNode(num);
} else {
buildTree(node.right, num);
}
} else { // 插入到左子樹中
if (node.left == null) {
node.left = new TreeNode(num);
} else {
buildTree(node.left, num);
}
}
}
static class TreeNode { // 樹節點的數據結構
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
時間復雜度上面根據原數組變化比較大,最差情況是整個數組是已經排好序的,這樣二叉樹會變成一個鏈表結構,時間復雜度退化到了\(O(n^2)\),但是最優和平均情況下時間復雜度在\(O(nlogn)\)水平。
空間復雜度是\(O(n)\),因為要構建一個包含n
個元素的二叉搜索樹。
這個算法是穩定,在構建二叉樹的過程中能夠保證元素順序的一致性。
計數排序
計數排序是一個最基本的非比較排序,能夠將時間復雜度提高到\(O(n)\)的水平,但是使用上比較有局限性,通常只能應用在鍵的變化范圍比較小的情況下,如果鍵的變化范圍特別大,建議使用基數排序。
計數排序的過程是創建一個長度為數組中最小和最大元素之差的數組,分別對應數組中的每個元素,然后用這個新的數組來統計每個元素出現的頻率,然后遍歷新的數組,根據每個元素出現的頻率把元素放回到老的數組中,得到已經排好序的數組。
圖片來自這里
簡單代碼實現:
private void countSort(int[] nums) {
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int num : nums) { // 找到最大最小值
min = Math.min(min, num);
max = Math.max(max, num);
}
int[] count = new int[max-min+1]; // 建立新數組
for (int num : nums) { // 統計每個元素出現頻率
count[num-min]++;
}
int cur = 0;
for (int i = 0; i < count.length; i++) { // 根據出現頻率把計數數組中的元素放回到舊數組中
while (count[i]>0) {
nums[cur++] = i+min;
count[i]--;
}
}
}
計數排序能夠將時間復雜度降低到\(O(n+r)\)(r為數組元素變化范圍),不過這是對於數組元素的變化范圍不是特別大。隨着范圍的變大,計數排序的性能就會逐漸降低。
空間復雜度為\(O(n+r)\),隨着數組元素變化范圍的增大,空間復雜度也會變大。
計數排序是穩定的,原來排在前面的相同在計數的時候,仍然是排在每個計數位置的前面,在最后復原的時候也是從每個計數位的前面開始復原,所以最后相對位置還是相同的。
桶排序
桶排序是將所有的元素分布到一系列的區間(也可以稱之為桶)里面,然后對每個桶里面的所有元素分別進行排序的算法。
首先新建一個桶的數組,每個桶的規則需要提前制定好,比如元素在09為一個桶、1019為一個桶。然后遍歷整個待排序的數組,把元素分配到對應的桶里面。接下來單獨對每個桶里面的元素進行排序,排序算法可以選擇比較排序或者非比較排序,得到排序后的數組。最后把所有的桶內的元素還原到原數組里面得到最后的排序數組。
圖片來自這里
private void bucketSort(int[] nums) {
int INTERVAL = 100; // 定義桶的大小
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (int num : nums) { // 找到數組元素的范圍
min = Math.min(min, num);
max = Math.max(max, num);
}
int count = (max - min + 1); // 計算出桶的數量
int bucketSize = (count % INTERVAL == 0) ?( count / INTERVAL) : (count / INTERVAL+1);
List<Integer>[] buckets = new List[bucketSize];
for (int num : nums) { // 把所有元素放入對應的桶里面
int quotient = (num-min) / INTERVAL;
if (buckets[quotient] == null) buckets[quotient] = new ArrayList<>();
buckets[quotient].add(num);
}
int cur = 0;
for (List<Integer> bucket : buckets) {
if (bucket != null) {
bucket.sort(null); // 對每個桶進行排序
for (Integer integer : bucket) { // 還原桶里面的元素到原數組
nums[cur++] = integer;
}
}
}
}
時間復雜度上桶排序和計數排序一樣,是\(O(n+r)\)的水平,但是隨着數據元素范圍的增大,時間消耗也在增大。
空間復雜度也是\(O(n+r)\),需要額外的空間來保存所有的桶和桶里面的元素。
桶排序是穩定的(前提是桶內排序的邏輯是穩定的),和計數排序的邏輯類似,遍歷過程插入桶的過程中沒有改變相同元素的相對位置,排序也沒有改變,最后的還原也沒有改變。
基數排序
基數排序和桶排序有點相似,基數排序中需要把元素送入對應的桶中,不過規則是根據所有數字的某一位上面的數字來分類。
假設當前數組的所有元素都是正數,桶的數量就固定在了10個,然后計算出最大元素的位數。首先根據每個元素的最低位進行分組,比如1
就放入1
這個桶,13
就放入3
這個桶,111
也放入1
這個桶,然后把所有的數字根據桶的順序取出來,依次還原到原數組里面。在第二輪從第二位開始分組,比如1
(看作01
)放入0
這個桶,13
放入1
這個桶,111
也放入1
這個桶,再把所有的元素從桶里面依次取出放入原數組。經過最大元素位數次的這樣的操作之后,還原得到的數組就是一個已經排好序的數組。
圖片來自這里
考慮到數組里面還有負數的情況,可以把桶的大小擴大到19個,分別代表對應位在-9~9之間的數字,代碼如下:
private void radixSort(int[] nums) {
int max = -1;
int min = 1;
for (int num : nums) { // 計算最大最小值
max = Math.max(max, num);
min = Math.min(min, num);
}
max = Math.max(max, -min); // 求得絕對值最大的值
int digits = 0;
while (max > 0) { // 計算絕對值最大的值的位數
max /= 10;
digits++;
}
List<Integer>[] buckets = new List[19]; // 建一個包含所有位數的數組
for (int i = 0; i < buckets.length; i++) {
buckets[i] = new ArrayList<>();
}
int pos;
int cur;
for (int i = 0, mod = 1; i < digits; i++, mod*=10) { // 對十進制每一位進行基數排序
for (int num : nums) { // 掃描數組將值放入對應的桶
pos = (num / mod) % 10;
buckets[pos+9].add(num);
}
cur = 0;
for (List<Integer> bucket : buckets) { // 將桶內元素放回到數組里面
if (bucket!=null) {
for (Integer integer : bucket) {
nums[cur++] = integer;
}
bucket.clear(); // 將桶清空
}
}
}
}
時間復雜度基本在\(O(n·\frac{k}{d})\)水平,其中\(k\)為key的總數量,\(d\)為絕對值最大的數字的十進制位數。
空間復雜度是\(O(n+2^d)\)。
基數排序是一個穩定排序算法,在排序添加元素的過程中沒有改變相同元素的相互位置。
TimSort
Timsort是由Tim Peters在2002年實現的,自Python 2.3以來,它一直是Python的標准排序算法。Java在JDK中使用Timsort對非基本類型進行排序。Android平台和GNU Octave還將其用作默認排序算法。
Timsort是一種穩定的混合排序算法,同時應用了二分插入排序和歸並排序的思想,在時間上擊敗了其他所有排序算法。它在最壞情況下的時間復雜度為\(O(nlogn)\)優於快速排序;最佳情況的時間復雜度為\(O(n)\),優於歸並排序和堆排序。
由於使用了歸並排序,使用額外的空間保存數據,TimSort空間復雜度是\(O(n)\)
由於篇幅原因,TimSort的具體實現過程暫且就不講了,感興趣的同學可以看我的另外一篇博客——世界上最快的排序算法——Timsort
總結
排序算法 | 最好情況 | 平均情況 | 最差情況 | 空間復雜度 | 穩定性 |
---|---|---|---|---|---|
冒泡排序 | \(n^2\) | \(n^2\) | \(n^2\) | \(1\) | ✓ |
選擇排序 | \(n^2\) | \(n^2\) | \(n^2\) | \(1\) | |
插入排序 | \(n\) | \(n^2\) | \(n^2\) | \(1\) | ✓ |
希爾排序 | \(nlogn\) | \(n^{4/3}\) | \(n^{4/3}\) | \(1\) | |
二叉樹排序 | \(nlogn\) | \(nlogn\) | \(n^2\) | \(n\) | ✓ |
歸並排序 | \(nlogn\) | \(nlogn\) | \(nlogn\) | \(n\) | ✓ |
快速排序 | \(nlogn\) | \(nlogn\) | \(n^2\) | \(logn\) | |
堆排序 | \(nlogn\) | \(nlogn\) | \(nlogn\) | \(1\) | |
計數排序 | - | \(n+r\) | \(n+r\) | \(n+r\) | ✓ |
桶排序 | - | \(n+r\) | \(n+r\) | \(n+r\) | ✓ |
基數排序 | - | \(\frac{nk}{d}\) | \(\frac{nk}{d}\) | \(n+2^d\) | ✓ |
TimSort | \(n\) | \(nlogn\) | \(nlogn\) | \(n\) | ✓ |
備注:\(r\)為排序數字的范圍,\(d\)是數字總位數,\(k\)是數字總個數
上面的表格總結了講到的排序算法的時間和空間復雜度以及穩定性等,在實際應用中會有各種排序算法變形的問題,都可以通過優化排序算法來達到優化算法的目的。
如果對時間復雜度要求比較高並且鍵的分布范圍比較廣,可以使用歸並排序、快速排序和堆排序。
如果不能使用額外的空間,那么快速排序和堆排序都是不錯的選擇。
如果規定了排序的鍵的范圍,可以優先考慮使用桶排序。
如果不想寫太多的代碼同時時間復雜度沒有太高的要求,可以考慮冒泡排序、選擇排序和插入排序。
如果排序的過程中沒有復雜的額外操作,直接使用編程語言內置的排序算法就行了。
參考
超詳細十大經典排序算法總結(java代碼)
十大經典排序算法
十大經典排序算法(動圖演示)
Sorting algorithm
Timsort
Data Structure - Sorting Techniques
This is the fastest sorting algorithm ever
TimSort
Timsort: The Fastest sorting algorithm for real-world problems
更多內容請看我的個人博客