排序法 java |
最好時間分析 算法 |
最差時間分析 windows |
平均時間複雜度 數組 |
穩定度 函數 |
空間複雜度 性能 |
冒泡排序 測試 |
O(n)(改進的冒泡排序) ui |
O(n2) spa |
O(n2) .net |
穩定 |
O(1) |
快速排序 |
O(n*log2n) |
O(n2) |
O(n*log2n) |
不穩定 |
O(log2n)~O(n) |
選擇排序 |
O(n2) |
O(n2) |
O(n2) |
不穩定 |
O(1) |
二叉樹排序 |
|
O(n2) |
O(n*log2n) |
|
O(n) |
插入排序 |
O(n) |
O(n2) |
O(n2) |
穩定 |
O(1) |
堆排序 |
O(n*log2n) |
O(n*log2n) |
O(n*log2n) |
不穩定 |
O(1) |
希爾排序 |
/ |
與增量序列的選取有關 |
與增量序列的選取有關 |
不穩定 |
O(1) |
歸併排序 |
O(n*log2n) |
O(n*log2n) |
O(n*log2n) |
穩定 | O(n) |
本文中的排序驗證OJ:http://pta.patest.cn/pta/test/18/exam/4/question/633
OJ測試點說明:
假定在待排序的記錄序列中,存在多個具備相同的關鍵字的記錄,若通過排序,這些記錄的相對次序保持不變,即在原序列中,ri=rj,且ri在rj以前,而在排序後的序列中,ri仍在rj以前,則稱這種排序算法是穩定的;不然稱爲不穩定的。
對於不穩定的排序算法,只要舉出一個實例,便可說明它的不穩定性;而對於穩定的排序算法,必須對算法進行分析從而獲得穩定的特性。須要注意的是,排序算法是否爲穩定的是由具體算法決定的,不穩定的算法在某種條件下能夠變爲穩定的算法,而穩定的算法在某種條件下也能夠變爲不穩定的算法。
穩定的算法在排序複雜數據時能保持一樣數據的本來順序不變,好比你已經有一組按年齡排好的學生信息,你想按照身高排序而且相同身高時,年齡是有序的,這時穩定的算法才能達到要求。
冒泡排序算法的運做以下:(從後往前)
代碼:
public void bubbleSort(int num[]) { for (int i = 0; i < num.length - 1; i++) { for (int j = 0; j < num.length - 1 - i; j++) { if (num[j] > num[j + 1]) { int temp = num[j]; num[j] = num[j + 1]; num[j + 1] = temp; } } } }性能分析:
須要n(n-1)/2次比較,複雜度爲O(n^2),最差最好都是O(n^2),空間複雜度爲O(1),可是若是排好序的很明顯一次都不用移動,應該是O(n)纔對。
加了一個標誌位來判斷是否有過交換。使最好的排序複雜度爲O(n)
public void bubbleSort(int num[]) { boolean swap = false; for (int i = 0; i < num.length; i++) { swap = false; for (int j = 0; j < num.length - 1 - i; j++) { if (num[j] > num[j + 1]) { int temp = num[j]; num[j] = num[j + 1]; num[j + 1] = temp; swap = true; } } if (!swap) { break; } } }
改進後的冒泡排序代碼在OJ上的運行狀況:
能夠發如今數據達到10^5時就超時了,固然在順序序列時依舊可以經過。
插入排序的基本思想是:
每步將一個待排序的紀錄,按其關鍵碼值的大小插入前面已經排序的文件中適當位置上,直到所有插入完爲止。
代碼:
public static void InsertSort(int num[], int N) { int temp = 0; int j = 0; for (int i = 1; i < N; i++) { temp = num[i]; for (j = i; j > 0 && temp < num[j - 1]; j--) { num[j] = num[j - 1]; } num[j] = temp; } }
插入排序在OJ上的運行狀況:
性能分析:
插入排序的最好狀況是,待排序列正好是正序的,此時每次只要在末尾插入數字便可,不須要進行移位判斷,此時時間複雜度爲O(n);最壞狀況是,待排序列是逆序的,此時每次都要移位全部的數字,騰出第一個位置再插入,此時時間複雜度爲O(n^2)。
咱們能夠發現,冒泡排序和插入排序交換的次數是相同的。這裏提出逆序對的概念,逆序對就是對於下標i<j,若是A[i]>A[j],則稱(i,j)是一對逆序對。每次交換就是消滅了一對逆序對,因此交換次數就是逆序對的個數。
因此插入排序的複雜度T(N,I)=O(N+I),I是逆序對的個數。因此若是I不多(基本有序),則插入排序簡單高效。
這裏提出個定理:任意N個不一樣元素組成的序列平均具備N(N-1)/4個逆序對。
定理:任何僅以交換相鄰兩元素來排序的算法,其平均複雜度爲Ω(n^2)(Ω指的是下界)。
這意味着:要提升算法效率,咱們必須每次消去不止一個逆序對,每次交換相隔較遠的的2個元素。
希爾排序的基本思想是:
希爾排序是把記錄按下標的必定增量分組,對每組使用直接插入排序算法排序;隨着增量逐漸減小,每組包含的關鍵詞愈來愈多,當增量(增量要互質)減至1時,整個文件恰被分紅一組,算法便終止。
低(好比2)間隔有序的序列,高間隔也有序(好比5)。
原始希爾排序:
public static void ShellSort(int num[], int N) { int temp = 0; int j = 0; for (int n = N / 2; n > 0; n = n / 2) { for (int i = n; i < N; i++) { temp = num[i]; for (j = i; j >= n && temp < num[j - n]; j = j - n) { num[j] = num[j - n]; } num[j] = temp; } } }
最壞狀況θ(n^2)(θ表示便是上界也是下界,代表增加速度是跟n^2同樣快的)
希爾排序在OJ上運行狀況:
能夠發現相比於插入排序來講,快了不少,速度比較穩定。
性能分析:
上述代碼最壞狀況的緣由是增量不互質(好比8,4,2,1)時,就如同插入排序。增量序列的選擇對希爾排序的時間複雜度影響很大。
已知的最好步長序列是由Sedgewick提出的(1, 5, 19, 41, 109,...),該序列的項來自
這兩個算式。
通常來講希爾排序的速度在插入排序和快速排序之間。
選擇排序(Selection sort)是一種簡單直觀的排序算法。它的工做原理是每一次從待排序的數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,直到所有待排序的數據元素排完。 選擇排序是不穩定的排序方法(好比序列[5, 5, 3]第一次就將第一個[5]與[3]交換,致使第一個5挪動到第二個5後面)。
public static void SelectionSort(int num[], int N) { int min = 0; for (int i = 0; i < N - 1; i++) { min = i; for (int j = i; j < N; j++) { if (num[j] < num[min]) { min = j; } } if (min != i) { int temp = num[min]; num[min] = num[i]; num[i] = temp; } } }
選擇排序在OJ上的結果:
性能分析:
不管最好最壞狀況,都須要n(n-1)/2次比較,最壞狀況須要每次交換,最好狀況不交換,因此不管最好最壞都是O(n^2)
發現選擇排序和冒泡排序其實差很少,每次都是最大(小)的數字交換到開頭,那二者有什麼區別呢?
1. 冒泡排序是穩定的,選擇排序不穩定。
2. 選擇排序每次只交換一組數據,冒泡排序可能交換屢次,可是二者比較次數是同樣的。
3. 冒泡最壞的狀況複雜度纔是O(n^2),最好是O(n), 選擇平均複雜度就是O(n^2) 可是冒泡的最壞狀況處理要比選擇慢。
選擇排序太慢了,時間複雜度達到O(n^2)。如何減小時間複雜度呢?因爲在選擇排序中找到最小元須要O(n)的時間複雜度,有沒有更快的方法呢?咱們想到了堆。堆排序實際上是對選擇排序的一種改進。
public static void HeapSort(int num[], int N) { BuildMinHeap(num);//O(n) int[] temp = new int[N]; for (int i = 0; i < N; i++) { temp[i] = DeleteMin(num);//O(logN) } for (int i = 0; i < N; i++) { num[i] = temp[i];//O(n) } }很直觀的想法,創建一個最小堆,而後不斷取出根節點,保存到臨時數組中,爲了與其餘算法保持一致,咱們還需把臨時數組賦值給本來的數組。時間複雜度爲O(NlogN),可是空間複雜度就很高了。其實複製temp數組給num數組又費時間又費空間,有沒有辦法把這一步簡化掉呢?
咱們想到一種方法:不創建最小堆,創建最大堆,建好堆之後,把根節點和最後一位進行交換(交換後最大的那個數放到了最後一位)。而後排除最後一位,繼續建最大堆,重複這個過程。
public static void HeapSort(int num[], int N) { BuildMaxHeap(num, N);//創建最大堆O(N) for (int i = N - 1; i > 0; i--) { int temp = num[0]; num[0] = num[i]; num[i] = temp; MaxHeapFixdown(num, i, 0);//這個就至關於只調整根元素,只須要一次logN } } private static void BuildMaxHeap(int num[], int N) { for (int i = (N % 2 == 0 ? (N - 1) / 2 : (N - 2) / 2); i >= 0; i--) { MaxHeapFixdown(num, N, i); } } private static void MaxHeapFixdown(int[] num, int N, int i) { int child = 0; int temp = num[i]; int j = 0; for (j = i; (j * 2 + 1) < N; j = child) { child = 2 * j + 1; if (2 * j + 2 < N && num[2 * j + 1] < num[2 * j + 2]) { child++; } if (temp > num[child]) { break; } else { num[j] = num[child]; } } num[j] = temp; }
堆排序在OJ上的結果:
因爲每次從新恢復堆的時間複雜度爲O(logN),共N - 1次從新恢復堆操做,再加上前面創建堆時間複雜度也爲O(N)。二次操做時間相加仍是O(N * logN)。故堆排序的時間複雜度爲O(N * logN)。
定理:堆排序處理N個不一樣元素的隨機排列的平均比較次數是:2NlogN-O(NloglogN)
雖然堆排序給出最佳平均時間複雜度,但實際效果不如Sedgewick增量序列的希爾排序。
思想很簡單,下面的圖就一目瞭然了,整體思想就是「分解」和「合併」
最樸素的遞歸的思想:把總體數組分紅兩組,而後每組再分紅兩組,直到兩個數組總體有序,再不斷合併起來。
private static void MergeSort(int[] num, int n) { int temp[] = new int[n]; MSort(num, temp, 0, n - 1); } public static void MSort(int num[], int temp[], int left, int right) { int center; if (left < right) { center = (left + right) / 2; MSort(num, temp, left, center); MSort(num, temp, center + 1, right); Merge(num, temp, left, center + 1, right); } } /** * * @param num * 待排數組 * @param temp * 臨時數組 * @param left * 第一個數組的第一個位置 * @param right * 第二個數組的第一個位置(兩個數組是緊挨着的) * @param end * 最後一個位置 */ private static void Merge(int[] num, int[] temp, int left, int right, int end) { int tmp = left;// 指向插入到temp數組的位置 int leftEnd = right - 1; int sum = end - left + 1; while (left <= leftEnd && right <= end) { if (num[left] < num[right]) { temp[tmp++] = num[left++]; } else { temp[tmp++] = num[right++]; } } while (left <= leftEnd) { temp[tmp++] = num[left++]; } while (right <= end) { temp[tmp++] = num[right++]; } for (int i = end; sum > 0; i--, sum--)//left已經改變,只能從後往前賦值 { num[i] = temp[i]; } }
歸併排序在OJ上的結果:
因爲分治的思想,而且一次merge的時間複雜度爲O(n),因此T(n)=T(n/2)+T(n/2)+O(n)=>T(n)=O(NlogN),歸併排序的平均時間複雜度,最差最好時間複雜度都是爲O(NlogN)
在這裏簡單證實一下:
T(n)=2T(n/2)+O(n)=>T(n)=2^k*(T(n/(2^k)))+k*O(n)
取2^k=n => k=logn => T(n)=n*T(1)+O(nlogn) => T(n)=O(nlogn)
在上述代碼中,臨時數組temp[]是在MergeSort中就聲明瞭,可是隻有在Merge時纔用到,在MSort時每次都被傳來傳去的,爲何不在Merge時聲明呢?
上圖給瞭解釋,若是在Merge中聲明,則要不斷的聲明而後釋放,若是一開始就聲明,Merge時用的都是一個數組。
上面給出了遞歸方式的歸併排序,咱們知道遞歸對棧的開銷很大,而且速度上也會慢,有沒有非遞歸的歸併排序呢?
固然上圖是非遞歸的思想。咱們能夠看出,和遞歸的分治思想相比,非遞歸的感受就是一種合併的過程,不斷合併,直到有序。
private static void MergeSort(int[] num, int n) { int temp[] = new int[n]; int length = 1; while (length < n) { MergePass(num, temp, n, length);//把num賦給了temp length = length * 2; MergePass(temp, num, n, length);//有可能這一步會多餘,可是確保了最後結果會在num中 length = length * 2; } } private static void MergePass(int[] num, int[] temp, int n, int length) { int i = 0; for (i = 0; i <= n - 2 * length; i = i + 2 * length) { Merge(num, temp, i, i + length, i + 2 * length - 1); } if (i + length < n)//剩餘兩個不等長的數組 { Merge(num, temp, i, i + length, n - 1); } else//就剩一個 { for (int j = i; j < n; j++) { temp[j] = num[j]; } } } /** * * @param num * 待排數組 * @param temp * 臨時數組 * @param left * 第一個數組的第一個位置 * @param right * 第二個數組的第一個位置(兩個數組是緊挨着的) * @param end * 最後一個位置 */ private static void Merge(int[] num, int[] temp, int left, int right, int end) { int tmp = left;// 指向插入到temp數組的位置 int leftEnd = right - 1; int sum = end - left + 1; while (left <= leftEnd && right <= end) { if (num[left] < num[right]) { temp[tmp++] = num[left++]; } else { temp[tmp++] = num[right++]; } } while (left <= leftEnd) { temp[tmp++] = num[left++]; } while (right <= end) { temp[tmp++] = num[right++]; } //最後再也不賦值給num }
與遞歸的相比彷佛沒什麼太大變化,時間複雜度都是O(NlogN),Merge函數有了一點區別,再也不最後再次賦給了num數組,爲何使最終結果在num數組中,MergeSort函數while循環中每次都執行兩次Merge,固然在最後時,最後一個Merge有多是多餘的操做,可是爲了確保結果在num中,也就不kao慮這些了。
在實際應用中,歸併排序常常被用於外排序(所有元素不能在內存中一次完成)
該方法的基本思想是:
1.先從數列中取出一個數做爲基準數(pivot)。
2.分區過程,將比這個數大的數全放到它的右邊,小於或等於它的數全放到它的左邊。
3.再對左右區間重複第二步,直到各區間只有一個數。
性能分析:
快排最好的狀況是,每次正好中分,複雜度爲O(nlogn)。最差狀況,複雜度爲O(n^2),退化成冒泡排序
快排中有些要注意的問題:
第一個問題:pivot的選擇
剛剛談到快排的最壞狀況。最快狀況和pivot的選擇有關,假設咱們選擇了第一個元素做爲pivot,當待排序列爲順序時,每次將除了pivot之外的其餘元素再次遞歸,
時間複雜度T(n)=T(n-1)+O(n)=>T(n)=O(n^2),退化成冒泡排序。
經典的取pivot的方法有如下幾種:
1. 取頭、中、尾的中位數
public static int mid3(int[] arr, int left, int right) { int mid = (left + right) / 2; int temp; if (arr[left] > arr[mid]) { temp = arr[left]; arr[left] = arr[mid]; arr[mid] = temp; } if (arr[left] > arr[right]) { temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } if (arr[mid] > arr[right]) { temp = arr[mid]; arr[mid] = arr[right]; arr[right] = temp; } temp = arr[mid]; arr[mid] = arr[right - 1]; arr[right - 1] = temp; //Sort(arr, left + 1, right - 2); return arr[right - 1]; }
將頭、中、尾按大小排好,把中當作pivot,移到right-1處,此時只需將left+1到right-2排序就能夠了,由於left確定比mid小,right確定比mid大。
2. 取頭
3. 取尾
4. 隨機函數(隨機函數的代價很大,不建議)
第二個問題:若是有元素等於pivot,是換仍是不換呢?
1. 若是換,那當遇到全是同樣的數,每一次都要交換,可是有一個好處是,pivot會移動到中間的地方,正好中分,符合快排最好的狀況,複雜度爲O(nlogn)
2. 若是不換,一樣是全是同樣的數,的確不用交換,i指針一直移到最後遇到j指針,pivot被移動到最後一位。分治時,右邊沒有元素,左邊n-1個元素,重複一下。符合快排最壞狀況,複雜度爲O(n^2)
綜上,仍是換吧。
private static void quickSort(int[] arr, int n) { Sort(arr, 0, n - 1); } public static void Sort(int[] arr, int left, int right) { if (left >= right) { return; } int pivot = mid3(arr, left, right); int i = left; int j = right - 1; for (;;) { while (i < right - 1 && arr[++i] < pivot) ; while (j > left + 1 && arr[--j] > pivot) ; if (i < j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } else { break; } } arr[right - 1] = arr[i]; arr[i] = pivot; Sort(arr, left, i - 1); Sort(arr, i + 1, right); } public static int mid3(int[] arr, int left, int right) { int mid = (left + right) / 2; int temp; if (arr[left] > arr[mid]) { temp = arr[left]; arr[left] = arr[mid]; arr[mid] = temp; } if (arr[left] > arr[right]) { temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } if (arr[mid] > arr[right]) { temp = arr[mid]; arr[mid] = arr[right]; arr[right] = temp; } temp = arr[mid]; arr[mid] = arr[right - 1]; arr[right - 1] = temp; // Sort(arr, left + 1, right - 2); return arr[right - 1]; }
這裏要注意的是,因爲我把pivot放到了right-1處,因此要i指針先動,若是放到left,就先動j指針。同理,在取a[0]爲pivot時,就先動j指針,取a[n-1]爲pivot時,就先動i指針。上面已經描述了,其實真正排序的是left+1到right-2的數據,代碼中i,j指針的初始值定義爲left,right-1是由於我後面的判斷是arr[++i],在此簡單說明一下。
看下在OJ上跑的結果:
第三個問題:小規模數據時,快排的速度。
因爲傳統快排是使用遞歸的,遞歸速度很慢,在小規模數據時(例如N<50),可能還不如插入排序快。
因此在小規模數據時,不要用傳統快排。(或者在寫快排時,設置一個Cutoff值,在規模小於Cutoff值時,調用插入排序,在規模大於Cutoff值時,再使用傳統快排)。
加上cutoff之後:
private static void quickSort(int[] arr, int n) { Sort(arr, 0, n - 1); } public static final int CUTOFF = 50; public static void Sort(int[] arr, int left, int right) { if (right - left >= CUTOFF) { if (left >= right) { return; } int pivot = mid3(arr, left, right); int i = left; int j = right - 1; for (;;) { while (i < right - 1 && arr[++i] < pivot) ; while (j > left + 1 && arr[--j] > pivot) ; if (i < j) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } else { break; } } arr[right - 1] = arr[i]; arr[i] = pivot; Sort(arr, left, i - 1); Sort(arr, i + 1, right); } else { InsertSort(arr, left, right); } } public static int mid3(int[] arr, int left, int right) { int mid = (left + right) / 2; int temp; if (arr[left] > arr[mid]) { temp = arr[left]; arr[left] = arr[mid]; arr[mid] = temp; } if (arr[left] > arr[right]) { temp = arr[left]; arr[left] = arr[right]; arr[right] = temp; } if (arr[mid] > arr[right]) { temp = arr[mid]; arr[mid] = arr[right]; arr[right] = temp; } temp = arr[mid]; arr[mid] = arr[right - 1]; arr[right - 1] = temp; return arr[right - 1]; } private static void InsertSort(int[] num, int start, int end) { int temp = 0; int j = 0; for (int i = start; i <= end; i++) { temp = num[i]; for (j = i; j > 0 && temp < num[j - 1]; j--) { num[j] = num[j - 1]; } num[j] = temp; } }
在OJ上的結果:
感受稍微快了點吧。
性能分析:
快速排序爲何快呢?由於每次將pivot交換後的位置都是pivot這個數的最終位置,他不像插入排序那樣,位置始終在變化。
快排最好的狀況是,每次正好中分,複雜度爲O(nlogn)。最差狀況是,若是pivot選的是第一個元素,那麼排好序的狀況耗時最多,複雜度爲O(n^2)
快排的空間複雜度呢?快速排序在對序列的操做過程當中只需花費常數級的空間。空間複雜度O(1)。 但須要注意遞歸棧上須要花費最少O(logn) 最多O(n)的空間。
1. http://blog.csdn.net/morewindows/article
2. http://www.cnblogs.com/luchen927/tag/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/
3. http://mooc.study.163.com/learn/ZJU-1000033001?tid=1000044001#/learn/content