選擇最小元素,與第一個元素交換位置;剩下的元素中選擇最小元素,與當前剩餘元素的最前邊的元素交換位置。java
選擇排序的比較次數與序列的初始排序無關,比較次數都是N(N-1)/2。算法
移動次數最多隻有n-1次。shell
所以,時間複雜度爲O(N^2),不管輸入是否有序都是如此,輸入的順序只決定了交換的次數,可是比較的次數不變。數組
選擇排序是不穩定的,好比5 6 5 3的狀況。緩存
public class SelectionSort {
public void selectionSort(int[] nums){
if(nums==null)
return;
for(int i=0;i<nums.length;i++) {
int index = i;
for (int j = i; j < nums.length; j++) {
if (nums[j] < nums[index]) {
index = j;
}
}
swap(nums, i, index);
}
}
}
複製代碼
從左到右不斷交換相鄰逆序的元素,這樣一趟下來把最大的元素放到了最右側。不斷重複這個過程,知道一次循環中沒有發生交換,說明已經有序,退出。bash
元素兩兩交換時,相同元素先後順序沒有改變,所以具備穩定性。函數
public class BubbleSort {
public void bubbleSort(int[] nums){
for(int i=nums.length-1;i>0;i--){
boolean sorted=false;
for(int j=0;j<i;j++){
if(nums[j]>nums[j+1]){
Sort.swap(nums,j,j+1);
sorted=true;
}
}
if(!sorted)
break;
}
}
複製代碼
每次將當前元素插入到左側已經排好序的數組中,使得插入以後左側數組依然有序。ui
由於插入排序每次只能交換相鄰元素,令逆序數量減小1,所以交換次數等於逆序數量。spa
所以,插入排序的複雜度取決於數組的初始順序。操作系統
插入排序具備穩定性
public class InsertionSort {
public void insertionSort(int[] nums){
for(int i=1;i<nums.length;i++){
for(int j=i;j>0;j--){
if(nums[j]<nums[j-1])
swap(nums,j,j-1);
else
break;//已經放到正確位置上了
}
}
}
}
複製代碼
對於大規模的數組,插入排序很慢,由於它只能交換相鄰的元素,每次只能將逆序數量減小1。
希爾排序爲了解決插入排序的侷限性,經過交換不相鄰的元素,每次將逆序數量減小大於1。希爾排序使用插入排序對間隔爲 H 的序列進行排序,不斷減小 H 直到 H=1 ,最終使得整個數組是有序的。
希爾排序的時間複雜度難以肯定,而且 H 的選擇也會改變其時間複雜度。
希爾排序的時間複雜度是低於 O(N^2) 的,高級排序算法只比希爾排序快兩倍左右。
希爾排序不具有穩定性。
public class ShellSort {
public void shellSort(int[] nums){
int N=nums.length;
int h=1;
while(h<N/3){
h=3*h+1;
}
while(h>=1){
for(int i=h;i<N;i++){
for(int j=i;j>0;j--){
if(nums[j]<nums[j-1]){
swap(nums,j,j-1);
}else{
break;//已經放到正確位置上了
}
}
}
}
}
}
複製代碼
將數組分爲兩部分,分別進行排序,而後進行歸併。
public void merge(int[] nums, int left, int mid, int right) {
int p1 = left, p2 = mid + 1;
int[] tmp = new int[right-left+1];
int cur=0;
//兩個指針分別指向左右兩個子數組,選擇更小者放入輔助數組
while(p1<=mid&&p2<=right){
if(nums[p1]<nums[p2]){
tmp[cur++]=nums[p1++];
}else{
tmp[cur++]=nums[p2++];
}
}
//將還有剩餘的數組放入到輔助數組
while(p1<=mid){
tmp[cur++]=nums[p1++];
}
while(p2<=right){
tmp[cur++]=nums[p2++];
}
//拷貝
for(int i=0;i<tmp.length;i++){
nums[left+i]=tmp[i];
}
}
複製代碼
經過遞歸調用,自頂向下將一個大數組分紅兩個小數組進行求解。
public void up2DownMergeSort(int[] nums, int left, int right) {
if(left==right)
return;
int mid=left+(right-left)/2;
mergeSort(nums,left,mid);
mergeSort(nums,mid+1,right);
merge(nums,left,mid,right);
}
複製代碼
public void down2UpMergeSort(int[] nums) {
int N = nums.length;
for (int sz = 1; sz < N; sz += sz) {
for (int lo = 0; lo < N - sz; lo += sz + sz) {
merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
}
複製代碼
把一個規模爲N的問題分解成兩個規模分別爲 N/2 的子問題,合併的時間複雜度爲 O(N)。T(N)=2T(N/2)+O(N)。
獲得其時間複雜度爲 O(NlogN),而且在最壞、最好和平均狀況下時間複雜度相同。
歸併排序須要 O(N) 的空間複雜度。
歸併排序具備穩定性。
快速排序經過一個切分元素 pivot 將數組分爲兩個子數組,左子數組小於等於切分元素,右子數組大於等於切分元素,將子數組分別進行排序,最終整個排序。
取 a[l] 做爲切分元素,而後從數組的左端向右掃描直到找到第一個大於等於它的元素,再從數組的右端向左掃描找到第一個小於它的元素,交換這兩個元素。不斷進行這個過程,就能夠保證左指針 i 的左側元素都不大於切分元素,右指針 j 的右側元素都不小於切分元素。當兩個指針相遇時,將切分元素 a[l] 和 a[j] 交換位置。
private int partition(int[] nums, int left, int right) {
int p1=left,p2=right;
int pivot=nums[left];
while(p1<p2){
while(nums[p1++]<pivot&&p1<=right);
while(nums[p2--]>pivot&&p2>=left);
swap(nums,p1,p2);
}
swap(nums,left,p2);
return p2;
}
複製代碼
public void sort(T[] nums, int l, int h) {
if (h <= l)
return;
int j = partition(nums, l, h);
sort(nums, l, j - 1);
sort(nums, j + 1, h);
}
複製代碼
最好的狀況下,每次都正好將數組對半分,遞歸調用次數最少,複雜度爲 O(NlogN)。
最壞狀況下,是有序數組,每次只切分了一個元素,時間複雜度爲 O(N^2)。爲了防止這種狀況,在進行快速排序時須要先隨機打亂數組。
不具備穩定性。
對於有大量重複元素的數組,將數組分爲小於、等於、大於三部分,對於有大量重複元素的隨機數組能夠在線性時間內完成排序。
public void threeWayQuickSort(int[] nums,int left,int right){
if(right<=left)
return;
int lt=left,cur=left+1,gt=right;
int pivot=nums[left];
while(cur<=gt){
if(nums[cur]<pivot){
swap(nums,lt++,cur++);
}else if(nums[cur]>pivot){
swap(nums,cur,gt--);
}else{
cur++;
}
}
threeWayQuickSort(nums,left,lt-1);
threeWayQuickSort(nums,gt+1,right);
}
複製代碼
利用 partition() 能夠在線性時間複雜度找到數組的第 K 個元素。
假設每次能將數組二分,那麼比較的總次數爲 (N+N/2+N/4+..),直到找到第 k 個元素,這個和顯然小於 2N。
public int select(int[] nums, int k) {
int l = 0, h = nums.length - 1;
while (h > l) {
int j = partition(nums, l, h);
if (j == k) {
return nums[k];
} else if (j > k) {
h = j - 1;
} else {
l = j + 1;
}
}
return nums[k];
}
複製代碼
堆能夠用數組來表示,這是由於堆是徹底二叉樹,而徹底二叉樹很容易就存儲在數組中。位置 k 的節點的父節點位置爲 k/2,而它的兩個子節點的位置分別爲 2k 和 2k+1。在這裏,從下標爲1的索引開始 的位置,是爲了更清晰地描述節點的位置關係。
當一個節點比父節點大,不斷交換這兩個節點,直到將節點放到位置上,這種操做稱爲上浮。
private void shiftUp(int k) {
while (k > 1 && heap[k / 2] < heap[k]) {
swap(k / 2, k);
k = k / 2;
}
}
複製代碼
當一個節點比子節點小,不斷向下進行比較和交換,當一個基點有兩個子節點,與最大節點進行交換。這種操做稱爲下沉。
private void shiftDown(int k){
while(2*k<=size){
int j=2*k;
if(j<size&&heap[j]<heap[j+1])
j++;
if(heap[k]<heap[j])
break;
swap(k,j);
k=j;
}
}
複製代碼
把最大元素和當前堆中數組的最後一個元素交換位置,而且不刪除它,那麼就能夠獲得一個從尾到頭的遞減序列。
構建堆 創建堆最直接的方法是從左到右遍歷數組進行上浮操做。一個更高效的方法是從右到左進行下沉操做。葉子節點不須要進行下沉操做,能夠忽略,所以只須要遍歷一半的元素便可。
交換堆頂和最壞一個元素,進行下沉操做,維持堆的性質。
public class HeapSort {
public void sort(int[] nums){
int N=nums.length-1;
for(int k=N/2;k>=1;k--){
shiftDown(nums,k,N);
}
while(N>1){
swap(nums,1,N--);
shiftDown(nums,1,N);
}
System.out.println(Arrays.toString(nums));
}
private void shiftDown(int[] heap,int k,int N){
while(2*k<=N){
int j=2*k;
if(j<N&&heap[j]<heap[j+1])
j++;
if(heap[k]>=heap[j])
break;
swap(heap,k,j);
k=j;
}
}
private void swap(int[] nums,int i,int j){
int t=nums[i];
nums[i]=nums[j];
nums[j]=t;
}
}
複製代碼
創建堆的時間複雜度是O(N)。
一個堆的高度爲 logN, 所以在堆中插入元素和刪除最大元素的複雜度都是 logN。
在堆排序中,對N個節點進行下沉操做,複雜度爲 O(NlogN)。
現代操做系統不多使用堆排序,由於它沒法利用局部性原理進行緩存,也就是數組元素不多和相鄰的元素進行比較和交換。
排序算法 | 最好時間複雜度 | 平均時間複雜度 | 最壞時間複雜度 | 空間複雜度 | 穩定性 | 適用場景 |
---|---|---|---|---|---|---|
冒泡排序 | O(N) | O(N^2) | O(N^2) | O(1) | 穩定 | |
選擇排序 | O(N) | O(N^2) | O(N^2) | O(1) | 不穩定 | 運行時間和輸入無關,數據移動次數最少,數據量較小的時候適用。 |
插入排序 | O(N) | O(N^2) | O(N^2) | O(1) | 穩定 | 數據量小、大部分已經被排序 |
希爾排序 | O(N) | O(N^1.3) | O(N^2) | O(1) | 不穩定 | |
快速排序 | O(NlogN) | O(NlogN) | O(N^2) | O(logN)-O(N) | 不穩定 | 最快的通用排序算法,大多數狀況下的最佳選擇 |
歸併排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(N) | 穩定 | 須要穩定性,空間不是很重要 |
堆排序 | O(NlogN) | O(NlogN) | O(NlogN) | O(1) | O(1) | 不穩定 |
以前介紹的算法都是基於比較的排序算法,下邊介紹兩種不是基於比較的算法。
已知數據範圍 x1 到 x2, 對範圍中的元素進行排序。能夠使用一個長度爲 x2-x1+1 的數組,存儲每一個數字對應的出現的次數。最終獲得排序後的結果。
桶排序假設待排序的一組數均勻獨立的分佈在一個範圍中,並將這一範圍劃分紅幾個桶。而後基於某種映射函數,將待排序的關鍵字 k 映射到第 i 個桶中。接着將各個桶中的數據有序的合併起來,對每一個桶中的元素能夠進行排序,而後輸出獲得一個有序序列。