經典的算法問題,也是面試過程當中常常被問到的問題。排序算法簡單分類以下:html
這些排序算法的時間複雜度等參數以下:git
其中,n表明數據規模,k表明桶的個數,In-place表明不須要額外空間,Out-place表明須要額外的空間。面試
最簡單易懂的排序方法。每次比較兩個元素,若是順序錯誤,則交換之。重複地訪問整個序列,直到沒有元素須要交換。算法
最佳狀況:\(T(n) = O(n)\),最差狀況:\(T(n) = O(n^2)\),平均狀況:\(T(n) = O(n^2)\)。shell
void bubbleSort(vector<int> &nums) { int n = nums.size(); for(int i = 0; i < n; i++) { for(int j = 0; j < n-1-i; j++) { if(nums[j] > nums[j+1]) { // 元素交換 nums[j] = nums[j]^nums[j+1]; nums[j+1] = nums[j]^nums[j+1]; nums[j] = nums[j]^nums[j+1]; } } } }
最穩定的排序方法之一,不管什麼狀況時間複雜度都是 \(O(n^2)\),不須要額外空間。簡單直觀,每次找到未排序中的最小(最大)元素,放至相應位置,執行(n-1)次排序完成。api
最佳狀況:\(T(n) = O(n^2)\),最差狀況:\(T(n) = O(n^2)\),平均狀況:\(T(n) = O(n^2)\)。數組
void selectSort(vector<int> &nums) { int n = nums.size(), minIndex; for(int i = 0; i < n-1; i++) { minIndex = i; for(int j = i+1; j < n; j++) { if(nums[j] < nums[minIndex])//尋找最小元素 minIndex = j; } if(i == minIndex) continue;//相同位置元素不可異或交換 // 元素交換 nums[i] = nums[i]^nums[minIndex]; nums[minIndex] = nums[i]^nums[minIndex]; nums[i] = nums[i]^nums[minIndex]; } }
一樣是一種簡單易懂的排序算法,不須要額外空間。經過構建有序序列,對於未排序元素,在已排序序列中從後向前掃描,找到相應位置並插入。插入排序在實現上,一般採用in-place排序(即只需用到O(1)的額外空間的排序),於是在從後向前掃描過程當中,須要反覆把已排序元素逐步向後挪位,爲最新元素提供插入空間。數據結構
通常來講,插入排序都採用in-place在數組上實現。具體步驟以下:ide
最佳狀況:\(T(n) = O(n)\),最壞狀況:\(T(n) = O(n^2)\),平均狀況:\(T(n) = O(n^2)\)。函數
void insertSort(vector<int> &nums) { int n = nums.size(), prev, num; for(int i = 0; i < n; i++) { prev = i-1;//有序序列尾部 num = nums[i];//當前元素 while(prev>=0 && nums[prev]>num) { nums[prev+1] = nums[prev];//後移 prev--; } nums[prev+1] = num; } }
第一個突破O(n^2)的排序算法,是簡單插入排序的改進版。它與插入排序的不一樣之處在於,它會優先比較距離較遠的元素。希爾排序又叫縮小增量排序。
希爾排序的核心在於間隔序列的設定。既能夠提早設定好間隔序列,也能夠動態的定義間隔序列。動態定義間隔序列的算法是《算法(第4版》的合著者Robert Sedgewick提出的。
上面的算法描述可能不是很好懂,舉個例子說明一下。對與{5, 2, 4, 1, 5, 9, 7, 8, 9, 0}序列,第一趟排序,增量t1=4(自定義),序列分爲{5, 5, 9},{2,9,0},{4,7},{1,8},分別對其進行插入排序,序列變爲{5,0,4,1, 5,2,7,8, 9,9};第二趟排序,增量t2=2,序列分爲{5,4,5,7,9},{0,1,2,8,9},對其進行插入排序,序列變爲{4,0, 5,1, 5,2, 7,8, 9,9};第三趟排序,增量t3=1,序列爲{4,0,5,1,5,2,7,8,9,9},對其進行插入排序,變爲{0,1,2,4,5,5,7,8,9,9}。
希爾排序的時間複雜度和其增量序列有關係,這涉及到數學上還沒有解決的難題;不過在某些序列中複雜度能夠視爲O(n^1.3);希爾排序時間複雜度的下界是n*log^2 n。
void shellSort(vector<int> &nums) { int n = nums.size(); int gap, i, j; for(gap = n/2; gap > 0; gap /= 2) { //插入排序簡潔寫法 for(i = gap; i < n; i++) { int num = nums[i]; for(j = i-gap; j>=0 && nums[j]>num; j-=gap) nums[j+gap] = nums[j]; nums[j+gap] = num; } } }
和選擇排序同樣,歸併排序的性能不受輸入數據的影響,但表現比選擇排序好的多,時間複雜度始終都是O(n log n)。代價是須要額外的內存空間。
歸併排序是創建在歸併操做上的一種有效的排序算法。該算法是採用分治法(Divide and Conquer)的一個很是典型的應用。歸併排序是一種穩定的排序方法。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲2-路歸併。
歸併排序有很多的應用,好比求解逆序對問題,只須要在歸併排序的過程當中添加一行代碼就能夠。
合併的過程須要額外的空間,利用一個新數組,比較兩個子序列,不斷將較小元素加入新數組,最後再將新數組更新至原序列。
最佳狀況:\(T(n) = O(nlogn)\),最差狀況:\(T(n) = O(nlogn)\),平均狀況:\(T(n) = O(nlogn)\)。
空間複雜度爲\(O(n)\)。
void Merge(vector<int> &nums, int first, int med, int last) { int i = first, j = med+1; vector<int> temp(nums.size());//額外空間 int cur=0;//當前位置 while(i<=med && j<=last) { if(nums[i] <= nums[j]) temp[cur++] = nums[i++]; else temp[cur++] = nums[j++]; } while(i <= med) temp[cur++] = nums[i++]; while(j <= last) temp[cur++] = nums[j++]; for(int m = 0; m < cur; m++)//更新數組 nums[first++] = temp[m]; } void mergeSort(vector<int> &nums, int first, int last) { if(first < last) { int med = first+(last-first)/2; mergeSort(nums, first, med); mergeSort(nums, med+1, last); Merge(nums, first, med, last); } }
基本思想:選定一個排序基準進行一趟排序,將全部元素分爲兩部分(大於基準和小於基準),分別對兩部分在此進行快速排序。
快速排序能夠用於求解第K大問題,由於每一次排序以後,能夠固定一個元素。
快速排序使用分治法來把一個序列分爲兩個子序列。具體算法描述以下:
最佳狀況:\(T(n) = O(nlogn)\),最差狀況:\(T(n) = O(n2)\),平均狀況:\(T(n) = O(nlogn)\)。
void quickSort(vector<int> &nums, int left, int right) { if(left<right) { int l=left, r=right; int pivot = nums[left];//判斷標準值 while(l<r) { while(l<r && nums[r]>=nums[l])//必定記住要加等於號,在下面加也行 r--; swap(nums[l], nums[r]); while(l<r && nums[l]<nums[r])//在這裏加等於號也行,但必須有一個加 l++; swap(nums[l], nums[r]); } nums[l]=pivot; quickSort(nums, l+1, right); quickSort(nums, left, l-1); } }
堆排序(Heapsort)是指利用堆這種數據結構所設計的一種排序算法。堆積是一個近似徹底二叉樹的結構,並同時知足堆積的性質:即子結點的鍵值或索引老是小於(或者大於)它的父節點。
可能看起來看起來有點複雜,在本文的參考連接中有動圖解釋,可能好容易理解一些。
最佳狀況:\(T(n) = O(nlogn)\),最差狀況:\(T(n) = O(nlogn)\),平均狀況:\(T(n) = O(nlogn)\)。
int len; void heapify(vector<int> &nums, int i) { int left = 2*i+1; int right = 2*i+2; int largest = i; if(left<len && nums[left] > nums[largest]) largest = left; if(right<len && nums[right] > nums[largest]) largest = right; if(largest != i) { swap(nums[i], nums[largest]); heapify(nums, largest); } } void buildMaxHeap(vector<int> &nums) { len = nums.size(); for(int i = len/2; i>=0; i--) heapify(nums, i); } void heapSort(vector<int> &nums) { buildMaxHeap(nums); for(int i = nums.size()-1; i>0; i--) { swap(nums[0], nums[i]); len--; heapify(nums, 0); } }
計數排序的核心在於將輸入的數據值轉化爲鍵存儲在額外開闢的數組空間中。 做爲一種線性時間複雜度的排序,計數排序要求輸入的數據必須是有肯定範圍的整數。
計數排序(Counting sort)是一種穩定的排序算法。計數排序使用一個額外的數組C,其中第i個元素C[i]是待排序數組A中值等於i的元素的個數。而後根據數組C來將A中的元素排到正確的位置。它只能對整數進行排序。這種作法其實就是map的基本用法。
計數排序限制性太大,要求必須是肯定範圍的整數。實際作題中根本用不到,不過在某些特殊場景中可能能夠用上。
當輸入的元素是n 個0到k之間的整數時,它的運行時間是 \(O(n + k)\)。計數排序不是比較排序,排序的速度快於任何比較排序算法。因爲用來計數的數組C的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1),這使得計數排序對於數據範圍很大的數組,須要大量時間和內存。
最佳狀況:\(T(n) = O(n+k)\),最差狀況:\(T(n) = O(n+k)\),平均狀況:\(T(n) = O(n+k)\)。
void countingSort(vector<int> &nums, int maxValue) { int bucket[maxValue+1] = {0}; int n = nums.size(); int sorted = 0; for(int i = 0; i < nums.size(); i++) { bucket[nums[i]]++; } for(int i = 0; i < maxValue+1; i++) { while(bucket[i] > 0) { nums[sorted++] = i; bucket[i]--; } } }
桶排序是計數排序的升級版。它利用了函數的映射關係,高效與否的關鍵就在於這個映射函數的肯定(代碼中經過設定每一個桶的容量間接設定此映射關係)。
桶排序 (Bucket sort)的工做的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每一個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排序。
桶排序最好狀況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,由於其它部分的時間複雜度都爲O(n)。很顯然,桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
最佳狀況:T(n) = O(n+k),最差狀況:T(n) = O(n+k),平均狀況:T(n) = O(n2)。
void bucketSort(vector<int> &nums, int bucketSize) { int n = nums.size(); if(n == 0) return; int minValue = nums[0], maxValue = nums[0]; for(int i = 1; i < n; i++) { if(nums[i] > maxValue) maxValue = nums[i]; if(nums[i] < minValue) minValue = nums[i]; } if(bucketSize < 5) bucketSize = 5;//默認每一個桶的容量爲5 int bucketNum = (maxValue-minValue)/bucketSize + 1;//桶的數量 vector< vector<int> > buckets(bucketNum); for(int i = 0; i < nums.size(); i++) buckets[(nums[i]-minValue)/bucketSize].push_back(nums[i]); int sorted = 0; for(int i = 0; i < buckets.size(); i++) { insertSort(buckets[i]);//插入排序 for(int j = 0; j < buckets[i].size(); j++) nums[sorted++] = buckets[i][j]; } }
基數排序也是非比較的排序算法,對每一位進行排序,從最低位開始排序,複雜度爲O(kn),n爲數組長度,k爲數組中的數的最大的位數;
基數排序是按照低位先排序,而後收集;再按照高位排序,而後再收集;依次類推,直到最高位。有時候有些屬性是有優先級順序的,先按低優先級排序,再按高優先級排序。最後的次序就是高優先級高的在前,高優先級相同的低優先級高的在前。基數排序基於分別排序,分別收集,因此是穩定的。
最佳狀況:\(T(n) = O(n * k)\),最差狀況:\(T(n) = O(n * k)\),平均狀況:\(T(n) = O(n * k)\)。
基數排序有兩種方法:MSD,從高位開始進行排序;LSD,從低位開始進行排序。
//LSD void redixSort(vector<int> &nums, int maxDigit) { int mod = 10; int dev = 1; vector< vector<int> > buckets(10); for(int i = 0; i < maxDigit; i++, dev*=10, mod*=10) { for(int j = 0; j < nums.size(); j++) { int bid = nums[j] % mod / dev;//取出對應數位做爲桶編號 buckets[bid].push_back(nums[j]); } int sorted = 0; for(int i = 0; i < buckets.size(); i++) { for(int j = 0; j < buckets[i].size(); j++) nums[sorted++] = buckets[i][j]; buckets[i].clear(); } } }
冒泡排序是基礎,每輪遍歷將「最大元素」移至正確位置(「最右邊」),不穩定的O(n^2);
選擇排序要了解,選擇排序每輪遍歷將「最小(大)元素」移至正確位置(「最左(右)邊」),穩定的O(n^2);
插入排序最簡單,適合數據量較小的排序,依然是O(n^2);
希爾排序是插入排序升級版,很差用,爲O(nlog^2n);
歸併排序和快速排序要熟記原理並會寫代碼。時間複雜度都是O(nlogn),前者不穩定,後者穩定。最經常使用的排序方法。
堆排序代碼複雜,不太好理解,也很差用,爲O(nlogn)。
計數排序、桶排序、基數排序都不是比較排序,能夠歸爲一類,對數據有特殊的要求。其中計數排序是基礎,相似創建map對應;桶排序將數據的大小分到不一樣的桶中,桶內再微小排序;基數排序則是屢次的桶排序,每次桶排序根據對應數位將數據分到不一樣桶中。
本文版權歸做者AlvinZH和博客園全部,歡迎轉載和商用,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接,不然保留追究法律責任的權利.