排序算法一表總覽
其他注意事項:
- 計數排序中,k kk是整數的範圍
- 穩定性是指,序列中相同的數是否有可能交換順序,例如序列中有兩個8,順序爲8 88和8 ′ 8^{'}8′,如果在排序完之後,順序有可能變爲8 ′ 8^{'}8′和8 88,那麼這種排序就是不穩定的排序算法;若不可能改變順序,則是穩定算法。
- 只有歸併排序可用於外部排序
- 在下面的代碼中,swap函數用於元素交換,實現爲:
private static void swap(int nums, int i, int j){
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
各排序算法說明及其實現
快速排序
-
圖示:
原理:選擇一個軸元素key,通過i j ijij指針定位元素,將待排序數組分爲左側小於等於key和右邊大於等於key兩組,遞歸操作。
時間複雜度:二分思想,故平均複雜度O ( n log n ) O(n \log n)O(nlogn);但如果每次二分選擇的軸都極不平均則更高,極端舉例:只把元素分爲1和n − 1 n-1n−1個,則出現需要劃分n次,而每次都要遍歷數組,因此最壞複雜度爲O ( n 2 ) O(n^2)O(n2)。例如完全正序,完全逆序都將出現最壞情況
空間複雜度:只有軸需要消耗空間,而二分法,每個部分都需要一個軸,因此空間爲 O ( log n ) O(\log n)O(logn)
穩定性:根據圖示可知,當i j ijij都定位到軸元素key時,將發生位置交換,因此不具有穩定性
參考代碼
public static void quickSort(int[] nums, int l, int r){
if(l >= r) return;
int i = l - 1, j = r + 1; //先定位到邊界兩側
int key = nums[l];
while(i < j){
while(nums[++i] < key); //先移動再與關鍵字判斷
while(nums[--j] > key); //先移動在與關鍵字判斷
if(i < j)
swap(nums, i, j); //交換兩側值
}
quickSort(nums, l, j);
quickSort(nums, j + 1, r);
}
堆排序
- 圖示:較爲複雜,暫無
- 原理:建立大頂堆,循環n nn次,每次:交換堆頂元素與堆尾元素使得堆中最大元素歸於排序的正確位置,之後通過向下調整堆來完成堆的重構。
- 時間複雜度:每次調整堆顯然需要經過樹高h = log n h = \log nh=logn次比較,總共有n nn個數,需要進行n nn輪次調整堆,因此複雜度爲O ( n log n ) O(n \log n)O(nlogn)。另外,通過向下調整建堆時間複雜度也是O ( n log n ) O(n \log n)O(nlogn),而若通過向上調整建堆時間複雜度可將降到O ( n ) O(n)O(n)
- 空間複雜度:消耗常數個空間用於存放比較時的臨時變量
- 穩定性:堆尾元素會被放到堆頂用於向下調整,而無論向下調整堆是怎樣的比較方案,都無法保證相同的元素不會被交換其原有位置。
- 參考代碼
public static void heapSort(int[] nums){
for (int i = (nums.length >>> 1) - 1; i >= 0; i--){ //建堆
siftDown(nums, i, nums.length);
}
for(int i = nums.length - 1; i > 0; i--){ //排序
swap(nums, 0, i);
siftDown(nums, 0, i);
}
}
private static void siftDown(int[] heap, int i, int len){
int curNum = heap[i];
int half = len >>> 1;
while (i < half){ //直到到沒有子結點
int lcIdx = (i << 1) + 1; //左子結點索引
int rcIdx = lcIdx + 1; //右子結點索引
int temp = heap[lcIdx]; //選取左子結點作爲臨時值
if(rcIdx < len && temp < heap[rcIdx]){
temp = heap[lcIdx = rcIdx];
}
if (curNum >= temp)
break;
heap[i] = temp;
i = lcIdx; //下一層檢測
}
heap[i] = curNum;
}
歸併排序
-
圖示:
原理:對於已排好序的兩個數組合並,只需要通過兩個數組各自的指針i j ijij進行遍歷,將比較結果放入新數組中,之後移動指針即可。歸併排序通過上述理論,首先將數組兩兩劃分,直到劃分到長爲1(有序),之後兩兩合併完成操作。
時間複雜度:典型分治,時間複雜度O ( n log n ) O(n \log n)O(nlogn)
空間複雜度:合併時開闢一個存放合併結果的輔助數組,數組長度只需要和原數組長度一致即可,因此爲O ( n ) O(n)O(n)
穩定性:合併時不會影響次序,具有穩定性
參考代碼
public void mergeSort(int nums, int l, int r){
if(l >= r) return;
int mid = (l + r) >> 1;
mergeSort(nums, l, mid);
mergeSort(nums. mid + 1, r);
int k = 0, i = l, j = mid + 1;
while (i <= mid && j <= r){
if (nums[i] <= nums[j])
temp[k++] = nums[i++];
else
temp[k++] = nums[j++];
}
//temp是外部開好的數組
while (i <= mid) temp[k++] = nums[i++];
while (j <= r) temp[k++] = nums[j++];
for (i = l, j = 0; i <= r; i++, j++)
nums[i] = temp[j];
}
選擇排序
-
圖示:
原理:一輪遍歷可以找到一個數組的最小值,選擇排序的方式就是每一輪都找到一個數組的最小值,是最容易理解的排序方法。
時間複雜度:需要n / 2 n/2n/2次遍歷,每次遍歷平均需要搜索n / 2 n/2n/2個元素,因此,時間複雜度O ( n ) O(n)O(n)
空間複雜度:需要常數輔助空間作爲比較元素的臨時遍歷
穩定性:每次選擇都是從前往後選,因此可以是穩定的
參考代碼
public static void selectSort(int nums){
for(int i = 0; i < nums.length; i++){
int idx = i;
for(int j = i; j < nums.length; j++){
if(nums[j] < nums[i])
idx = j;
}
swap(nums, i, idx);
}
}
插入排序
-
圖示:
原理:在排好順中的序列中,可以通過遍歷找到某個待插入正確位置使得新數組是有序的,因此,插入排序從前往後,依次將元素插入已排好序的數組中即可。(一個元素的數組可以看作有序數組)
時間複雜度:查找元素和插入平均都需要n / 2 n/2n/2次操作,因此,時間複雜度O ( n 2 ) O(n^2)O(n2);若數組已經升序,則只需要進行n − 1 n-1n−1次比較,因此最好時間複雜度爲O ( n ) O(n)O(n),二分查找優化可降到O ( log n ) O(\log n)O(logn)
空間複雜度:比較後插入時需要常數個輔助空間存放臨時變量
穩定性:可以是穩定的,設置好比較方法使得相同時插入在後面即可
參考代碼
public static void insertSort(int nums){
for(int i = 1; i < nums.length; i++){
int temp = nums[i];
for(int j = i; j > 0 && nums[j - 1] > temp; j--){
nums[j] = nums[j - 1];
}
nums[j] = temp;
}
}
- 二分查找插入排序(即查找插入位置時使用二分法優化)
public static void insertSort(int nums){
for(int i = 1; i < nums.length; i++){
int temp = nums[i];
int l = 0, r = i - 1;
while(l <= r){
int mid = (l + r)/2;
if(nums[mid] > temp) r = mid - 1;
else l = mid + 1;
}
for(int j = i; j > low; j--){
nums[j] = nums[j - 1];
}
nums[j] = temp;
}
}
希爾排序
-
圖示:
原理:插入排序的改進辦法,通過縮小增量的插入排序來降低時間複雜度,gap開始爲數組的一半,每次減半,每輪都完成一次跨越gap的插入排序。
時間複雜度:O ( n 1.3 ) O(n^{1.3})O(n1.3),較難證明,可以寫作O ( n log n ) O(n\log n)O(nlogn)
空間複雜度:同時間複雜度
穩定性:由於分組的存在,不穩定
參考代碼
public static void shellSort(int[] nums){
int gap = nums.length;
while(true){
gap /= 2;
for(int i = 0; i < gap; i++){
temp = nums[i];
for(int j = i; j > 0 && temp > nums[j]; j -= gap){
nums[j] = nums[j - gap];
}
nums[i] = temp;
}
if(gap == 1) break;
}
}
冒泡排序
-
圖示:
原理:”車輪式的比武,每次決出勝者將參與下一次比武,直到選出最強者;每次選出剩餘角色的最強者都需要進行一輪比武"
時間複雜度:由原理易知爲O ( n 2 ) O(n^2)O(n2)
空間複雜度:常數
穩定性:穩定
參考代碼
public static void bubbleSort(int nums){
for(int i = 1; i < nums.length; i++){
boolean changed = false;
for(int j = 0; j < nums.length - i;j++){
if(nums[j] > nums[j + 1]){
swap(nums, j, j + 1);
changed = true;
}
}
if(changed == false) break;
}
}
- 雙向冒泡(時間有所優化,又稱“雞尾酒排序”)
public static void bubbleSort(int nums){
int l = 0, r = nums.length - 1, shift = 1;
while(l < r){
boolean changed = false;
for(int i = l; i < r; i++){
if(nums[i] > nums[i + 1]){
swap(nums, i, i + 1);
shift = i;
}
}
r = shift;
for(int i = r - 1; i >= l; i--){
if(nums[i] > nums[i + 1]){
swap(nums, i, i + 1);
shift = i + 1;
}
}
l = shift;
}
}
計數排序
- 圖示:較爲簡單,故無圖示
- 原理:統計數字出現次數即可
- 時間複雜度:與整數範圍有關
- 空間複雜度:與整數範圍有關
- 穩定性:穩定
- 參考代碼
public static void countingSort(int nums){
int[] counter = new int[65535];
//此處的counter數組需要隨情況變化
for(int i = 0; i < nums.length; i++){
counter[nums[i]]++;
}
int idx = 0;
for(int i = 0; i < counter.length; i++){
while(counter[i] > 0)
nums[idx++] = counter[i];
}
}
- 侷限性:只能排序整數;整數範圍較大則開闢空間較多
排序算法的應用舉例
刷題需要掌握的幾種算法
- 快速排序與歸併排序的算法思想:分治。
分治就是把一個複雜的問題分成兩個或更多的相同或相似的子問題,直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。顯然到最後,快速排序的簡單拼接和歸併排序的有序數組合並都屬於這裏所說的簡單問題。 - 堆排序的核心優勢–求k個最值。
前面我們說到,通過向上調整建堆時間複雜度可降低到O ( n ) O(n)O(n),而每次向下調整堆的時間複雜度在O ( log n ) O(\log n)O(logn)。因此,求k個最值用堆排序,即向上調整建堆,取得堆頂最值,向下調整堆,繼續取堆頂最值,循環往復。如此一來,當k較小( < log n ) (<\log n)(<logn)時,求前k個最值時間複雜度即爲建堆複雜度O ( n ) O(n)O(n) - 已知元素範圍的最佳排序–計數排序
其實在很多時候,我們待排序的數的範圍已知並且最大最小值相差不大時,計數排序是最優秀的算法
leetcode排序算法題舉例
- leetcode[75] 顏色分類 【中等】:
給定一個包含紅色、白色和藍色,一共 n 個元素的數組,原地對它們進行排序,使得相同顏色的元素相鄰,並按照紅色、白色、藍色順序排列。
此題中,我們使用整數 0、 1 和 2 分別表示紅色、白色和藍色。 - 【思路】典型的counting sort應用,由於紅、白、藍分別是0,1,2,因此待排序數組是整數,而且範圍0-2很小,可以使用計數排序,而且原地排序符合要求。
class Solution {
public void sortColors(int[] nums) {
int[] counter = new int[3];
for(int i = 0; i < nums.length; i++){
counter[nums[i]]++;
}
int idx = 0;
for(int i = 0; i < 3; i++){
while(counter[i] > 0){
nums[idx++] = i;
counter[i]--;
}
}
}
}
- leetcode [1366] 通過投票對團隊排名【中等】
現在有一個特殊的排名系統,依據參賽團隊在投票人心中的次序進行排名,每個投票者都需要按從高到低的順序對參與排名的所有團隊進行排位。
排名規則如下:
參賽團隊的排名次序依照其所獲「排位第一」的票的多少決定。如果存在多個團隊並列的情況,將繼續考慮其「排位第二」的票的數量。以此類推,直到不再存在並列的情況。
如果在考慮完所有投票情況後仍然出現並列現象,則根據團隊字母的字母順序進行排名。
給你一個字符串數組 votes 代表全體投票者給出的排位情況,請你根據上述排名規則對所有參賽團隊進行排名。
請你返回能表示按排名系統 排序後 的所有團隊排名的字符串。 - 【題解】這裏提供一個計數排序方法,HashMap也可解。通過統計每一個團隊的投票信息進行排序,最後得到排序結果打印即可。這裏相當於使用一個大小26的數組統計字符個數,是一種常用方法。
class Solution {
public String rankTeams(String[] votes) {
int len = votes[0].length();
int[][] map = new int[26][len + 1]; // 多1用於存放團隊信息
for(int i = 0; i < 26; i++) map[i][len] = i;
for(int i = 0; i < votes.length; i++){ //投票統計
String s = votes[i];
for(int j = 0; j < len; j++){
map[s.charAt(j) - 'A'][j]++;
}
}
Arrays.sort(map, (a, b) ->{ //投票結果排序
for(int i = 0; i < len; i++){
if(a[i] < b[i]) return 1;
if(a[i] > b[i]) return -1;
}
return 0;
});
StringBuilder sb = new StringBuilder();
for(int i = 0; i < len; i++){ //獲取結果對應團隊
sb.append((char)('A' + map[i][len]));
}
return sb.toString();
}
}
- leetcode[973] 最接近原點的 K 個點(堆排序的典型應用)
- leetcode[1471] 數組中的 k 個最強值(堆排序的典型應用)
- leetcode[1481] Least Number of Unique Integers after K Removals(堆排序的典型應用)
- leetcode[179] 最大數(排序規則設計)
- leetcode [242] Valid Anagram(排序規則設計)
- leetcode [524] Longest Word in Dictionary through Deleting(排序規則設計)
- leetcode [976] Largest Perimeter Triangle(排序規則設計)
- leetcode [1451] Rearrange Words in a Sentence(排序規則設計)
- leetcode [853] Car Fleet
N 輛車沿着一條車道駛向位於 target 英里之外的共同目的地。
每輛車 i 以恆定的速度 speed[i] (英里/小時),從初始位置 position[i] (英里) 沿車道駛向目的地。
一輛車永遠不會超過前面的另一輛車,但它可以追上去,並與前車以相同的速度緊接着行駛。
此時,我們會忽略這兩輛車之間的距離,也就是說,它們被假定處於相同的位置。
車隊 是一些由行駛在相同位置、具有相同速度的車組成的非空集合。注意,一輛車也可以是一個車隊。
即便一輛車在目的地才趕上了一個車隊,它們仍然會被視作是同一個車隊。 - 【題解】此題也是排序規則設計,基於這樣一個事實:如果某輛車起點在前面,但(如果單獨行動)到達目的地的時間比起點在後面的車晚,則可以說明被超車,會被組成車隊;因此計算到達花費時間,排序,然後統計車隊數量即可
class Solution {
public int carFleet(int target, int[] position, int[] speed) {
int n = position.length, res = 0;
double[][] cars = new double[n][2];
//分別記錄開始位置和到達時間
for(int i = 0; i < n; i++){
cars[i][0] = position[i];
cars[i][1] = (double)(target - position[i])/speed[i];
}
//開始位置排序
Arrays.sort(cars, (a, b) -> Double.compare(a[0], b[0]));
double cur = 0;
for(int i = n - 1; i >= 0 ; i--){
//能否追上前車
if(cars[i][1] > cur){
cur = cars[i][1];
res++;
}
}
return res;
}
}
小結
- 基本有序時或逆序時快速排序慢;基本有序時插入、希爾、冒泡快;逆序時冒泡排序慢
- “分組錯亂”的方法都不穩定
- 平均時間O ( n log n ) O(n \log n)O(nlogn)方法優缺點要明確:快排速度最快,但可能出現較慢情況,不穩定;歸併穩定且可用於外部排序,但消耗空間大;堆排序適用於排前k個,不穩定
- 計數排序很好用,若是整數且範圍小,請用計數排序
- 求前k個用堆排序或選擇排序
- 快排核心在於劃分思想