原地排序:就是特指空間複雜度是 O (1) 的排序算法。如下三個都是原地排序。java
穩定性:若是待排序的序列中存在值相等的元素,通過排序以後,相等元素之間原有的前後順序不變。算法
// 冒泡排序,a 表示數組,n 表示數組大小 public void bubbleSort(int[] a, int n) { if (n <= 1) return; for (int i = 0; i < n; ++i) { // 提早退出冒泡循環的標誌位 boolean flag = false; for (int j = 0; j < n - i - 1; ++j) { if (a[j] > a[j+1]) { // 交換 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; // 表示有數據交換 } } if (!flag) break; // 沒有數據交換,提早退出 } }
冒泡特點:編程
分析排序複雜度的兩個指標:
有序度:是數組中具備有序關係的元素對的個數。數組
有序元素對:a[i] <= a[j], 若是 i < j。
對於一個倒序排列的數組,好比 6,5,4,3,2,1,有序度是 0;對於一個徹底有序的數組,好比 1,2,3,4,5,6,有序度就是 n*(n-1)/2,也就是 15。咱們把這種徹底有序的數組的有序度叫做滿有序度。函數
逆序度:與有序度相反,逆序度 = 滿有序度 - 有序度。優化
將數組中的數據分爲兩個區間,已排序區間和未排序區間。
須要將數據a插入到已排序區間時,須要拿a與已排序區間的元素比較找到合適的插入位置。code
// 插入排序,a 表示數組,n 表示數組大小 public void insertionSort(int[] a, int n) { if (n <= 1) return; for (int i = 1; i < n; ++i) { int value = a[i]; int j = i - 1; // 查找插入的位置 for (; j >= 0; --j) { if (a[j] > value) { a[j+1] = a[j]; // 數據移動 } else { break; } } a[j+1] = value; // 插入數據 } }
插排特色:排序
和插排有點相似,也區分已排序區間和未排序區間。選擇排序每次會從未排序區間中找到最小的元素,將其放到已排序區間的末尾。
遞歸
選擇排序特色:內存
從代碼實現上來看,冒泡排序的數據交換要比插入排序的數據移動要複雜,冒泡排序須要 3 個賦值操做,而插入排序只須要 1 個。
冒泡排序中數據的交換操做: if (a[j] > a[j+1]) { // 交換 int tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = true; } 插入排序中數據的移動操做: if (a[j] > value) { a[j+1] = a[j]; // 數據移動 } else { break; }
接下來是兩種時間複雜度O (nlogn)的排序算法:歸併排序和快速排序。這兩種適合大規模數據排序,均用到了分治思想。
分治是一種解決問題的處理思想,遞歸是一種編程技巧,這二者並不衝突。
遞推公式: merge_sort(p…r) = merge(merge_sort(p…q), merge_sort(q+1…r)) 終止條件: p >= r 不用再繼續分解 (其實是分解成爲單個元素而後有序合併)
僞代碼:
// 歸併排序算法, A 是數組,n 表示數組大小 merge_sort(A, n) { merge_sort_c(A, 0, n-1) } // 遞歸調用函數 merge_sort_c(A, p, r) { // 遞歸終止條件 if p >= r then return // 取 p 到 r 之間的中間位置 q q = (p+r) / 2 // 分治遞歸 merge_sort_c(A, p, q) merge_sort_c(A, q+1, r) // 將 A[p...q] 和 A[q+1...r] 合併爲 A[p...r] merge(A[p...r], A[p...q], A[q+1...r]) }
其中merge()函數的僞代碼:
merge(A[p...r], A[p...q], A[q+1...r]) { var i := p,j := q+1,k := 0 // 初始化變量 i, j, k var tmp := new array[0...r-p] // 申請一個大小跟 A[p...r] 同樣的臨時數組 while i<=q AND j<=r do { if A[i] <= A[j] { tmp[k++] = A[i++] // i++ 等於 i:=i+1 } else { tmp[k++] = A[j++] } } // 判斷哪一個子數組中有剩餘的數據 var start := i,end := q if j<=r then start := j, end:=r // 將剩餘的數據拷貝到臨時數組 tmp while start <= end do { tmp[k++] = A[start++] } // 將 tmp 中的數組拷貝回 A[p...r] for i:=0 to r-p do { A[p+i] = tmp[i] } }
若是咱們定義求解問題 a 的時間是 T (a),求解問題 b、c 的時間分別是 T (b) 和 T ( c),那咱們就可獲得遞推關係式:
T(a) = T(b) + T(c) + K
K是講子問題b、c的結果合併成爲問題a的結果所消耗的時間。
歸併排序特色:
分區:
原地分區函數僞代碼(空間複雜度O(1)):
partition(A, p, r) { pivot := A[r] i := p for j := p to r-1 do { if A[j] < pivot { swap A[i] with A[j] i := i+1 } } swap A[i] with A[r] return i
快排特色:
快排與歸併的區別:
歸併處理過程是由下到上,先子問題,再合併。
快排處理過程是由上到下,先分區,再子問題。
O(n)時間複雜度內求無序數組中的第K大元素
借鑑快排的分治和分區思想。遞歸分區,而後比較pivot的下標p+1與K。
接下來三種時間複雜度爲O(n)的排序算法:桶排序、計數排序、基數排序。由於複雜度是線性的,因此叫作線性排序。主要緣由是:不涉及元素間的比較操做。
對排序數據要求比較高,好比給100萬用戶基於年齡排序。
核心思想:將數據分到幾個有序的桶裏,每一個桶裏的數據再單獨進行排序。桶內排完序以後,再把每一個桶裏的數據按照順序依次取出,可得有序序列。
數據有n個,均勻劃分到m個桶內。每一個桶k=n/m個元素,桶內快排O(k*logk)。桶排序的時間複雜度爲
O (n*log (n/m))。當桶的個數m接近數據個數n時,log(n/m)就是一個很是小的常量,桶排序的時間複雜度接近O(n)。空間複雜度爲O(1)。
桶排序比較適合用在外部排序中:數據比較大,沒法所有加載到內存中。
一個理解:計數排序能夠做爲桶排序的一種特殊狀況
舉例說明:8個考生,成績在0到5分之間,放在數組A[8]中,分別是:2,5,3,0,2,3,0,3。
考生的成繢從0到5分,咱們使用大小爲6的數組C[6]表示桶,其中下標對應分數。不過, C[6]內存儲的並非考生,而是對應的考生個數。像我剛剛舉的那個例子,咱們只須要遍歷一遍考生分數,就能夠獲得C[6]的值。
從圖中能夠看出,分數爲3的考生有3個,小於3分的考生有4個,因此,成績爲3分的考生在排序以後的有序數組R[8]中,會保存下標4,5,6的位置。
思路以下:對C[6]數組順序求和,C[k]存儲小於等於分數k的考生個數。
咱們從後到前依次掃描數組A。好比,當掃描到3時,咱們能夠從數組C中取出下標爲3的值7,也就是說,到目前爲止,包括本身在內,分數小於等於3的考生有7個,也就是說3是數組R中的第7個元素(也就是數組R中下標爲6的位置)。當3放入到數組R中後,小於等於3的元素就只剩下了6個 了,因此相應的C[3]要減1,變成6。
以此類推,當咱們掃描到第2個分數爲3的考生的時候,就會把它放入數組R中的第6個元素的位置(也就是下標爲5的位置)。當咱們掃描完整個數組A後,數組R內的數據就是按照分數從小到大有序排列的了。
// 計數排序,a 是數組,n 是數組大小。假設數組中存儲的都是非負整數。 public void countingSort(int[] a, int n) { if (n <= 1) return; // 查找數組中數據的範圍 int max = a[0]; for (int i = 1; i < n; ++i) { if (max < a[i]) { max = a[i]; } } int[] c = new int[max + 1]; // 申請一個計數數組 c,下標大小 [0,max] for (int i = 0; i <= max; ++i) { c[i] = 0; } // 計算每一個元素的個數,放入 c 中 for (int i = 0; i < n; ++i) { c[a[i]]++; } // 依次累加 for (int i = 1; i <= max; ++i) { c[i] = c[i-1] + c[i]; } // 臨時數組 r,存儲排序以後的結果 int[] r = new int[n]; // 計算排序的關鍵步驟,有點難理解 for (int i = n - 1; i >= 0; --i) { int index = c[a[i]]-1; r[index] = a[i]; c[a[i]]--; } // 將結果拷貝給 a 數組 for (int i = 0; i < n; ++i) { a[i] = r[i]; } }
計數排序特色:
十萬個手機號排序,桶排序和計數排序不適用。
一個思路:假設比較a,b兩個手機號碼的大小,若是前面幾位a比b大,則後面幾位不用看了。
先排最後一位,而後倒數第二位排序。這樣11次排序以後,手機號碼有序。
字符串舉例:
這是穩定排序算法思路。這樣,排序的數據有k位,就須要k次桶排序或者計數排序,總的時間複雜度是O(k*n)。
因此基數排序的時間複雜度近似於O(n)
實際上有些時候數據不是等長的,好比單詞排序,有長有短。能夠把全部單詞補齊到相同長度,位數不夠的在後面補0,根據ASCII值,全部字母大於「0」,因此不會影響大小順序。
總結:
基數排序對要排序的數據是有要求的,須要能夠分割出獨立的"位"來比較,並且位之間有遞進的關係,若是a數據的高位比b數據大,那剰下的低位就不用比較了。除此以外,每一位的數據範圍不能太大,要能夠用線性排序算法來排序(基數排序須要藉助桶排序或者計數排序來完成每一位的排序工做),不然,基數排序的時間複雜度就沒法作到〇(n) 了。
解答開篇:如何給100萬用戶基於年齡排序。使用桶排序。
常見幾種排序算法的比較:
小規模數據能夠選擇時間複雜度爲O(exp(n))的算法,若是對大規模數據排序,時間複雜度O(nlogn)更高效。因此,爲了兼顧更多狀況,通常都會選擇O(nlogn)排序算法實現排序。
快速排序優化方式:
由於有係數和常數因素,小規模數據的排序,O(exp(n))的排序算法並不必定比O(nlogn)排序算法執行的時間長。因此會選擇簡單、不須要遞歸的插入排序算法。