十大排序演算法總結 內部排序
目錄
排序測試模板
基本上所有演算法都是根據這個模板測試的,只需修改createRandomIntArray的引數和
long begin = System.currentTimeMillis();
SelectSort.selectSort(ito);//執行程式
long end = System.currentTimeMillis();
這段中執行的方法即可
package algorithm.sort.selectSort; import algorithm.sort.selectSort.SelectSort; public class Main { public static void main(String[] args) { //int[][] testTable = {{1,2,3,0},{1,2,3,4},{1,2567678,3,4,6767,45,12,345,3435,34,66666}, //{1,1,1,3,3,4,3,2,4,2},{1,6,2,7,1}}; int[][] testTable={createRandomIntArray(-100,100,50), createRandomIntArray(-100,100,400),createRandomIntArray(-1000,1000,500)}; for (int[] ito : testTable) { test(ito); } } private static void test(int[] ito) { boolean rtn; for (int i = 0; i < ito.length; i++) { System.out.print(ito[i]+" "); } System.out.println(); System.out.println("length="+ito.length); //開始時列印陣列 long begin = System.currentTimeMillis(); SelectSort.selectSort(ito);//執行程式 long end = System.currentTimeMillis(); //System.out.println(ito + ": rtn=" +rtn); for (int i = 0; i < ito.length; i++) { System.out.print(ito[i]+" "); }//列印結果幾陣列 System.out.println(); System.out.println("耗時:" + (end - begin) + "ms"); System.out.println("-------------------"); System.out.println("-------------------"); } public static int[] createRandomIntArray(int min,int max,int length){ int[] result=new int[length]; for(int i=0;i<length;i++){ double rand=Math.random(); result[i]=(int)(min+(max-min)*rand); } return result; } }
一、氣泡排序
總體思想
其大體思想就是通過與相鄰元素的比較和交換來把大的數交換到最後面。這個過程類似於水泡向上升一樣,因此而得名
1 比較相鄰的元素。如果第一個比第二個大,就交換他們兩個。
2 對每一對相鄰元素作同樣的工作,從開始第一對到結尾的最後一對。這步做完後,最後的元素會是最大的數。
3 針對所有的元素重複以上的步驟,除了最後一個。
4 持續每次對越來越少的元素重複上面的步驟,直到沒有任何一對數字需要比較。
程式設計思想
從i=0到length-2進行迴圈a
在迴圈a內,從j=0到length-2-i進行迴圈b
在迴圈b內,如果j位比j+1位大,則二者互換,
迴圈b結束,迴圈b導致從0到length-2-i內最大的那個數在length-1-i位上
迴圈a結束。
每次最大的數在length-1-0(最後一位),length-1-1,...length-1-(length-2)=1位上
public class BubbleSort {
public static void bubbleSort(int[] nums){
int length=nums.length;
if(length==0){
return;
}
for(int i=0;i<length-1;i++){
for(int j=0;j<length-1-i;j++){
if(nums[j]>nums[j+1]){
swap(nums, j, j+1);
}
}
}
}
public static void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
複雜度及優缺點
泡排序總比較次數=(n-1)+(n-2)+(n-3)+...+3+2+1=n(n-1)/2
n(n-1)/2是O(n^2)階的級,所以
氣泡排序速度是階O(n^2)的演算法。
最差情況是O(n^2),最好情況,普通演算法是O(n^2),優化過是O(n)
空間是o(1)的演算法
速度很慢,因為還要不斷交換
氣泡排序就是把小的元素往前調或者把大的元素往後調。比較是相鄰的兩個元素比較,交換也發生在這兩個元素之間。所以,如果兩個元素相等,我想你是不會再無聊地把他們倆交換一下的;如果兩個相等的元素沒有相鄰,那麼即使通過前面的兩兩交換把兩個相鄰起來,這時候也不會交換,所以相同元素的前後順序並沒有改 變,所以氣泡排序是一種穩定排序演算法。
優化
設定一個變數,初始值為false,當發生一次交換就將變數設定為true,如果迴圈完後還是false,說明陣列已經有序,結束迴圈。
所以最優情況下速度為o(n)
如果原始有序,那麼一次掃描完一次交換也不會有,即變數=false,此時return,終止排序。此時時間複雜度是O(n).
note:原始資料只要有一對需要改順序,複雜度就又會變成O(n^2)的。
二、選擇排序
總體思想
將整個陣列遍歷一遍,將最小的數和首個元素互換
然後將第二個到最後的陣列遍歷,其中最小的和第二個互換
以此類推。
舉個例子,對5,3,8,6,4這個無序序列進行簡單選擇排序,首先要選擇5以外的最小數來和5交換,也就是選擇3和5交換,一次排序後就變成了3,5,8,6,4.對剩下的序列一次進行選擇和交換,最終就會得到一個有序序列。
程式設計思想
1 計算陣列長度length
2 從i=0到length-1迴圈
3 每個迴圈內,先設定min=num[i],minIndex=i,初始化
4 再從j=i到length-1迴圈
5 在這個迴圈內,找到最小的min和對應的minIndex
6 這個迴圈結束,將i與minIndex位的陣列元素顛倒,另第i位為從i到length-1的最小數
7 這個迴圈結束,排序成功
public class SelectSort {
public static void selectSort(int[] nums){
int length=nums.length;
if(length==0){
return;
}
int min=0;
int minIndex=0;
for(int i=0;i<length;i++){
min=nums[i];
minIndex=i;
for(int j=i+1;j<length;j++){
if(nums[j]<min){
min=nums[j];
minIndex=j;
}
}
if(minIndex!=i){
swap(nums,i,minIndex);
}
}
}
public static void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
複雜度及優缺點
選擇排序總比較次數=(n-1)+(n-2)+(n-3)+...+3+2+1=n(n-1)/2
n(n-1)/2是O(n^2)階的級,所以
選擇排序的速度是階O(n^2)的演算法。
空間是o(1)的演算法
適用於小列表排序。
其實選擇排序可以看成氣泡排序的優化,因為其目的相同,只是選擇排序只有在確定了最小數的前提下才進行交換,大大減少了交換的次數。
選擇排序是給每個位置選擇當前元素最小的,比如給第一個位置選擇最小的,在剩餘元素裡面給第二個元素選擇第二小的,依次類推,直到第n-1個元素,第n個 元素不用選擇了,因為只剩下它一個最大的元素了。那麼,在一趟選擇,如果當前元素比一個元素小,而該小的元素又出現在一個和當前元素相等的元素後面,那麼 交換後穩定性就被破壞了。比較拗口,舉個例子,序列5 8 5 2 9, 我們知道第一遍選擇第1個元素5會和2交換,那麼原序列中2個5的相對前後順序就被破壞了,所以選擇排序不是一個穩定的排序演算法。
三、插入排序
總體思想
1 將陣列劃分為已排序的和未排序的,已排序的在前,初始為第一個
2 每次迴圈,從未排序的取出第一個,按照大小,插入已排序的陣列的對應位置,比它大的依次向後移動一位
或者
1)將第一待排序序列第一個元素看做一個有序序列,把第二個元素到最後一個元素當成是未排序序列。
2)從頭到尾依次掃描未排序序列,將掃描到的每個元素插入有序序列的適當位置。(如果待插入的元素與有序序列中的某個元素相等,則將待插入元素插入到相等元素的後面。)
舉個栗子,對5,3,8,6,4這個無序序列進行簡單插入排序,首先假設第一個數的位置時正確的,想一下在拿到第一張牌的時候,沒必要整理。然後3要插到5前面,把5後移一位,變成3,5,8,6,4.想一下整理牌的時候應該也是這樣吧。然後8不用動,6插在8前面,8後移一位,4插在5前面,從5開始都向後移一位。注意在插入一個數的時候要保證這個數前面的數已經有序。
程式設計思想
1 迴圈a從i=1到length-1結束
2 在迴圈a內,now為num[i],hasINsert初始為false
3 在迴圈a內,開始從j=i到j>0的迴圈b
4 在b內,如果nums[j]>now,那麼nums[j]=nums[j-1],如果不是,那麼nums[j]=now,hasInsert=true
比如已排序的 0 2 3 現在now=1,i=3,j=3
3>1,所以 nums[3]=3(nums[2]),j=2
2>1,所以nums[2]=2,j=1
0<1,所以nums[1]=1,
5 迴圈b結束,如果hasInsert=false,說明b是通過i>0的條件退出,而不是break的,這種情況已排序的陣列都比now大
所以,nums[0]=now
6 迴圈a結束
package algorithm.sort.insertSort;
import java.util.Arrays;
/* 插入排序
1 將陣列劃分為已排序的和未排序的,已排序的在前,初始為第一個
2 每次迴圈,從未排序的取出第一個,按照大小,插入已排序的陣列的對應位置,比它大的依次向後移動一位
1 迴圈a從i=1到length-1結束
2 在迴圈a內,now為num[i],hasINsert初始為false
3 在迴圈a內,開始從j=i到j>0的迴圈b
4 在b內,如果nums[j]>now,那麼nums[j]=nums[j-1],如果不是,那麼nums[j]=now,hasInsert=true
比如已排序的 0 2 3 現在now=1,i=3,j=3
3>1,所以 nums[3]=3(nums[2]),j=2
2>1,所以nums[2]=2,j=1
0<1,所以nums[1]=1,
5 迴圈b結束,如果hasInsert=false,說明b是通過i>0的條件退出,而不是break的,這種情況已排序的陣列都比now大
所以,nums[0]=now
6 迴圈a結束
*/
public class InsertSort {
public static void insertSort(int[] nums){
int length=nums.length;
if(length<=1){
return;
}
for(int i=1;i<length;i++){
int now=nums[i];
boolean hasInsert=false;
for(int j=i;j>0;j--){
if(nums[j-1]>now){
nums[j]=nums[j-1];
}
else{
nums[j]=now;
hasInsert=true;
break;
}
}
if(!hasInsert){
nums[0]=now;
}
}
}
}
也可以
for (int i = 0; i < arr.length; i++) {//假設第一個元素放到了正確的位置上,這樣僅需遍歷1~n-1
int j=i;
int target=arr[i];
while(j>0&&target<arr[j-1]){
arr[j]=arr[j-1];
j--;
}
arr[j]=target;
}
複雜度及優缺點
簡單插入排序的時間複雜度也是O(n^2)。
最佳用例效率:O(n),當列表已經被排序時,產生最佳用例。
最差用例效率:O(n^2),當列表反向順序排列時,產生最差用例。
插入排序是在一個已經有序的小序列的基礎上,一次插入一個元素。當然,剛開始這個有序的小序列只有1個元素,就是第一個元素。比較是從有序序列的末尾開 始,也就是想要插入的元素和已經有序的最大者開始比起,如果比它大則直接插入在其後面,否則一直往前找直到找到它該插入的位置。如果碰見一個和插入元素相 等的,那麼插入元素把想插入的元素放在相等元素的後面。所以,相等元素的前後順序沒有改變,從原無序序列出去的順序就是排好序後的順序,所以插入排序是穩 定的。
四、希爾排序
總體思想
希爾排序,也稱遞減增量排序演算法,是插入排序的一種更高效的改進版本。但希爾排序是非穩定排序演算法。
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:
插入排序在對幾乎已經排好序的資料操作時, 效率高, 即可以達到線性的效率
但插入排序一般來說是低效的, 因為插入排序每次只能將資料移動一位
希爾排序的基本思想是:先將整個待排序的記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄“基本有序”時,再對全體記錄進行依次直接插入排序。
子序列的構成不是簡單的逐段分割,而是將某個相隔某個增量的記錄組成一個子序列。
先建立一個遞減間隔的陣列,比如陣列長度為100,遞減的陣列為50,20,10,5,2,1
然後對逐個的間隔50到1逐個進行排序
比如對於50,將第1和51個進行直接插入排序,2和52個等等
然後對於20,將1,21,41,61,81,進行 直接插入排序,2,22,42,62,82等等
陣列的最後一定是1,進行最後的插入排序
程式設計思想
insertSort(int[] nums,int interval)
確定一個數組,根據一個interval進行排序
首先對i=0到interval-1進行迴圈a
迴圈a內,i此時為排序的分陣列的第一個
如果i+interval>length-1,說明分陣列只有一個,不排序,break
然後對j=i+interval到length-1進行迴圈b,其中j每次加interval
迴圈b內,now=nums[j],k=j,
然後進行迴圈c,k從j到k>interval-1並且nums[k-interval]>nums[k]
迴圈c內,nums[k]=nums[k-interval],將比now大的數,逐個向後移動一位
結束迴圈c,nums[k]=now,成功插入新增的數
結束迴圈b
結束迴圈a
遞減陣列1
length/2 length/4 length/8 ...1
比如
length=400
200 100 50 25 12 6 3 1
遞減陣列2
max2^k -1 2^(k-1)-1 ... 1
length=500
255 127 63 31 15 7 3 1
遞減陣列3
length/2 length/2-interval*1 length/2-interval*2 ... 1
length=50
25 22 19 16 13 10 7 4 1
public class ShellSort {
public static void insertSort(int[] nums){
int length=nums.length;
if(length<=1){
return;
}
List<Integer> decrementArray=createDecrementArray1(length);
for(int i=0;i<decrementArray.size();i++){
/*System.out.println("decre="+decrementArray.get(i)+" ");*/
insertSort(nums,decrementArray.get(i));
/*for(int j=0;j<length;j++){
System.out.print(nums[j]+" ");
}
System.out.println();*/
}
System.out.println();
}
public static List<Integer> createDecrementArray1(int length){
List<Integer> list=new ArrayList<>();
Integer now=length/2;
while(now>1){
list.add(now);
now=now/2;
}
list.add(1);
return list;
}
public static List<Integer> createDecrementArray2(int length){
List<Integer> list=new ArrayList<>();
int max=(int)(Math.log10(length)/Math.log10(2));
for(int i=max;i>0;i--){
int now=(int)Math.pow(2, i)-1;
list.add(now);
}
return list;
}
public static List<Integer> createDecrementArray3(int length,int interval){
List<Integer> list=new ArrayList<>();
int now=length/2;
while(now>1){
list.add(now);
now=now-interval;
}
list.add(1);
return list;
}
public static void insertSort(int[] nums,int interval){
int length=nums.length;
for(int i=0;i<interval;i++){
//i為begin的地方
if(i+interval>length-1){
break;
}
for(int j=i+interval;j<length;j=j+interval){
int now=nums[j];
int k=j;
while(k>interval-1&&now<nums[k-interval]){
nums[k]=nums[k-interval];
k=k-interval;
}
nums[k]=now;
}
}
}
}
複雜度及優缺點
希爾排序的速度與它選擇的遞減陣列密切相關,速度快的陣列,最好是互質的
如果選擇不好,可能o(n^2)
希爾排序的分析是複雜的,時間複雜度是所取增量的函式,這涉及一些數學上的難題。但是在大量實驗的基礎上推出當n在某個範圍內時,時間複雜度可以達到O(n^1.3)。
好的話,如下
希爾排序是按照不同步長對元素進行插入排序,當剛開始元素很無序的時候,步長最大,所以插入排序的元素個數很少,速度很快;當元素基本有序了,步長很小, 插入排序對於有序的序列效率很高。所以,希爾排序的時間複雜度會比o(n^2)好一些。由於多次插入排序,我們知道一次插入排序是穩定的,不會改變相同元 素的相對順序,但在不同的插入排序過程中,相同的元素可能在各自的插入排序中移動,最後其穩定性就會被打亂,所以shell排序是不穩定的。
五、歸併排序
總體思想
多次將若干個已經排序好的有序表合併成一個有序表。直接將兩個表合併的歸併成為二路歸併。
其基本思想是,先遞迴劃分子問題,然後合併結果。把待排序列看成由兩個有序的子序列,然後合併兩個子序列,然後把子序列看成由兩個有序序列。。。。。倒著來看,其實就是先兩兩合併,然後四四合並。。。最終形成有序序列。空間複雜度為O(n),時間複雜度為O(nlogn)。
首先mergeSort(nums,0,length-1)
進行遞迴
首先mergeSort陣列begin與end的左側,讓左側有序
再mergerSort右側
左側和右側都有序後,對左側和右側進行merge,一起排序
由於此次排序左右都有序,建立一個臨時陣列,對左右從頭到尾進行掃描,誰小就進入陣列,一遍掃描即可
程式設計思想
mergeSort
1 如果end<=begin 說明此時排序的陣列僅有一個或沒有,不排序
2 mid(begin+end)/2
3 mergeSort(nums, begin, mid);
4 mergeSort(nums, mid+1, end);
5 merge(nums, begin,mid, end);
merge
1 如果mid>end 說明begin到mid中已經排序完,不用排序
2 如果mid=end,說明在merge的上一步mergeSort(nums, begin, mid);已經排序完mergeSort(nums, begin, end);
不用再排序
3 以end-begin+1為長度建立temp陣列,陣列index為k
4 begin到mid的index為i=begin mid+1到end的index為mid+1
5 以i<=mid&&j<=end 為迴圈條件進行迴圈
6 在迴圈中,nums[i],nums[j] 誰小,就i或j++,temp[k]=nums[i或j],然後k++
7 迴圈結束後將其中沒有塞滿的陣列加入temp
8 將temp的陣列逐個替換begin到end的nums陣列的東西
或者merge理解
1. 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
2. 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
3. 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
4. 重複步驟3直到某一指標達到序列尾
5. 將另一序列剩下的所有元素直接複製到合併序列尾
public class MergeSort {
public static void mergeSort(int[] nums,int begin,int end){
int length=nums.length;
if(length<=1){
return;
}
if(end<=begin){
return;
}
int mid=(begin+end)/2;
mergeSort(nums, begin, mid);
mergeSort(nums, mid+1, end);
merge(nums, begin,mid, end);
}
public static void merge(int[] nums,int begin,int mid,int end){
if(mid>=end){
return;
}
int length=end-begin+1;
int[] temp=new int[length];
int k=0;
int i=begin;
int j=mid+1;
while(i<=mid&&j<=end){
if(nums[i]<nums[j]){
temp[k]=nums[i];
i++;
}
else{
temp[k]=nums[j];
j++;
}
k++;
}
if(i>mid){
while(j<=end){
temp[k]=nums[j];
j++;
k++;
}
}
if(j>end){
while(i<=mid){
temp[k]=nums[i];
i++;
k++;
}
}
for(int m=0;m<length;m++){
nums[begin+m]=temp[m];
}
}
}
複雜度及優缺點
時間複雜度為O(nlogn)。
空間複雜度為O(n)。
速度較快,但是需要額外的空間
歸併排序是把序列遞迴地分成短序列,遞迴出口是短序列只有1個元素(認為直接有序)或者2個序列(1次比較和交換),然後把各個有序的段序列合併成一個有 序的長序列,不斷合併直到原序列全部排好序。可以發現,在1個或2個元素時,1個元素不會交換,2個元素如果大小相等也沒有人故意交換,這不會破壞穩定 性。那麼,在短的有序序列合併的過程中,穩定是否受到破壞?沒有,合併過程中我們可以保證如果兩個當前元素相等時,我們把處在前面的序列的元素儲存在結 果序列的前面,這樣就保證了穩定性。所以,歸併排序也是穩定的排序演算法。
六、快速排序
總體思想
快速排序使用分治法(Divide and conquer)策略來把一個序列(list)分為兩個子序列(sub-lists)。
1 從數列中挑出一個元素,稱為 “基準”(pivot),
2 重新排序數列,所有元素比基準值小的擺放在基準前面,所有元素比基準值大的擺在基準的後面(相同的數可以到任一邊)。在這個分割槽退出之後,該基準就處於數列的中間位置。這個稱為分割槽(partition)操作。
3 遞迴地(recursive)把小於基準值元素的子數列和大於基準值元素的子數列排序。
1、先從數列中取出一個數作為基準數
2、分割槽過程,將比這個數大的數全放到它的右邊,小於或等於它的數全放到它的左邊
3、再對左右區間重複第二步,直到各區間只有一個數
程式設計思想
快速排序程式設計的難點就是如何把陣列按照基準點排序,共有兩種方法,填坑法,交換法
填坑法
1.i =L; j = R; 將基準數挖出形成第一個坑a[i]。
2.j–由後向前找比它小的數,找到後挖出此數填前一個坑a[i]中。
3.i++由前向後找比它大的數,找到後也挖出此數填到前一個坑a[j]中。
4.再重複執行2,3二步,直到i==j,將基準數填入a[i]中。
public class QuickSort {
public static void quickSort(int[] nums,int begin,int last) {
int length=nums.length;
if(length==0||length==1){
return;
}
int base=nums[begin];
int i=begin;
int j=last;
int mid=0;
//i為第一個,j為最後一個,base為第一個數字,此時i處為base
while(true){
//j從後往前找到比base小的數字,與i作顛倒,顛倒後j處為base,i處為之前j處比base小的數
while(j!=i){
if(nums[j]<base ){
nums[i]=nums[j];
nums[j]=base;
break;
}
else{
j--;
}
}
//如果上一個j過程結束,如果i與j相同,中間為i,跳出大過程
if(i==j){
mid=i;
break;
}
//i從後往前找到比base大的數字,與j作顛倒,顛倒後i處為base,j處為之前i處比base大的數,跳出這個小迴圈
while(i!=j){
if(nums[i]>base){
nums[j]=nums[i];
nums[i]=base;
break;
}
else{
i++;
}
}
//如果上一個i過程結束,如果i與j相同,中間為i,跳出大過程
if(i==j){
mid=i;
break;
}
}
//System.out.println(Arrays.toString(nums));
//從此處開始mid處為base,比mid處大的,則比base大,反之亦然
//對兩次進行遞迴快速排序
if((mid-begin)>1){
quickSort(nums, begin, mid-1);
}
if((last-mid)>1){
quickSort(nums, mid+1, last);
}
return;
}
}
交換法
舉個栗子:對5,3,8,6,4這個無序序列進行快速排序,思路是右指標找比基準數小的,左指標找比基準數大的,交換之。
5,3,8,6,4 用5作為比較的基準,最終會把5小的移動到5的左邊,比5大的移動到5的右邊。
5,3,8,6,4 首先設定i,j兩個指標分別指向兩端,j指標先掃描(思考一下為什麼?)4比5小停止。然後i掃描,8比5大停止。交換i,j位置。
5,3,4,6,8 然後j指標再掃描,這時j掃描4時兩指標相遇。停止。然後交換4和基準數。
4,3,5,6,8 一次劃分後達到了左邊比5小,右邊比5大的目的。之後對左右子序列遞迴排序,最終得到有序序列。
上面留下來了一個問題為什麼一定要j指標先動呢?首先這也不是絕對的,這取決於基準數的位置,因為在最後兩個指標相遇的時候,要交換基準數到相遇的位置。一般選取第一個數作為基準數,那麼就是在左邊,所以最後相遇的數要和基準數交換,那麼相遇的數一定要比基準數小。所以j指標先移動才能先找到比基準數小的數。
/**
*@Description:<p>實現快速排序演算法</p>
*@author 王旭
*@time 2016-3-3 下午5:07:29
*/
public class QuickSort {
//一次劃分
public static int partition(int[] arr, int left, int right) {
int pivotKey = arr[left];
int pivotPointer = left;
while(left < right) {
while(left < right && arr[right] >= pivotKey)
right --;
while(left < right && arr[left] <= pivotKey)
left ++;
swap(arr, left, right); //把大的交換到右邊,把小的交換到左邊。
}
swap(arr, pivotPointer, left); //最後把pivot交換到中間
return left;
}
public static void quickSort(int[] arr, int left, int right) {
if(left >= right)
return ;
int pivotPos = partition(arr, left, right);
quickSort(arr, left, pivotPos-1);
quickSort(arr, pivotPos+1, right);
}
public static void sort(int[] arr) {
if(arr == null || arr.length == 0)
return ;
quickSort(arr, 0, arr.length-1);
}
public static void swap(int[] arr, int left, int right) {
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
}
}
複雜度及優缺點
快速排序是不穩定的,它的速度與它的基準點有關,基準點的好壞大大影響速度
在最差情況下,劃分由 n 個元素構成的陣列需要進行 n 次比較和 n 次移動。因此劃分所需時間為 O(n) 。最差情況下,每次主元會將陣列劃分為一個大的子陣列和一個空陣列。這個大的子陣列的規模是在上次劃分的子陣列的規模減 1 。該演算法需要 (n-1)+(n-2)+…+2+1= O(n^2) 時間。
在最佳情況下,每次主元將陣列劃分為規模大致相等的兩部分。設 T(n) 表示使用快速排序演算法對包含 n 個元素的陣列排序所需的時間,因此,和歸併排序的分析相似,快速排序的 T(n)= O(nlogn)。
空間複雜度
其實這個空間複雜度不太好計算,因為有的人使用的是非就地排序,那樣就不好計算了(因為有的人用到了輔助陣列,所以這就要計算到你的元素個數了);我就分析下就地快速排序的空間複雜度吧;
首先就地快速排序使用的空間是O(1)的,也就是個常數級;而真正消耗空間的就是遞迴呼叫了,因為每次遞迴就要保持一些資料;
最優的情況下空間複雜度為:O(logn) ;每一次都平分陣列的情況
最差的情況下空間複雜度為:O( n ) ;退化為氣泡排序的情況
快速排序有兩個方向,左邊的i下標一直往右走,當a[i] <= a[center_index],其中center_index是中樞元素的陣列下標,一般取為陣列第0個元素。而右邊的j下標一直往左走,當a[j] > a[center_index]。如果i和j都走不動了,i <= j, 交換a[i]和a[j],重複上面的過程,直到i>j。 交換a[j]和a[center_index],完成一趟快速排序。在中樞元素和a[j]交換的時候,很有可能把前面的元素的穩定性打亂,比如序列為 5 3 3 4 3 8 9 10 11, 現在中樞元素5和3(第5個元素,下標從1開始計)交換就會把元素3的穩定性打亂,所以快速排序是一個不穩定的排序演算法,不穩定發生在中樞元素和a[j] 交換的時刻。
七、堆排序
總體思想
利用堆這種資料結構所設計的一種排序演算法。堆積是一個近似完全二叉樹的結構,並同時滿足堆積的性質:即子結點的鍵值或索引總是小於(或者大於)它的父節點。
堆排序是藉助堆來實現的選擇排序,思想同簡單的選擇排序,以下以大頂堆為例。注意:如果想升序排序就使用大頂堆,反之使用小頂堆。原因是堆頂元素需要交換到序列尾部。
首先,實現堆排序需要解決兩個問題:
1. 如何由一個無序序列鍵成一個堆?
2. 如何在輸出堆頂元素之後,調整剩餘元素成為一個新的堆?
第一個問題,可以直接使用線性陣列來表示一個堆,由初始的無序序列建成一個堆就需要自底向上從第一個非葉元素開始挨個調整成一個堆。
第二個問題,怎麼調整成堆?首先是將堆頂元素和最後一個元素交換。然後比較當前堆頂元素的左右孩子節點,因為除了當前的堆頂元素,左右孩子堆均滿足條件,這時需要選擇當前堆頂元素與左右孩子節點的較大者(大頂堆)交換,直至葉子節點。我們稱這個自堆頂自葉子的調整成為篩選。
從一個無序序列建堆的過程就是一個反覆篩選的過程。若將此序列看成是一個完全二叉樹,則最後一個非終端節點是n/2取底個元素,由此篩選即可。舉個栗子:
49,38,65,97,76,13,27,49序列的堆排序建初始堆和調整的過程如下:
程式設計思想
1 陣列先初始化成最大堆
2 將最大的與最後的互換,然後最後的開始下沉,讓第一個為原來次大的
3 依次迴圈,最後最大,第一個最小
陣列第i位的兩個子節點
左2*(i+1)-1=2*i+1 右 2*i+2
i位的父節點
(i+1)/2-1=(i-1)/2 向下取整
建立初始最大堆
1 last為完全二叉樹(除了最底一層其他每層都是滿的1,2,4,8個。。。)倒數第二層的最後一個
2 從last到第一個逐個上浮,upjust
3 upadjust中如果它<左子樹,則二者互換,如果它<右子樹,則二者互換。
4 然後檢查它的左右子節點,是否大於左右子節點的左右子節點。如果小於,則upadjust它
下沉
1 將第一個與最後一個互換,然後length-1,相當於不去動最後一個
2 然後downAdjust(nums,0,length-i-1);
3 在downAdjust,比價i的左右子樹,與他的最大的並且比i大的互換,然後downAdjust(left/right)
public class HeapSort {
public static void heapSort(int[] nums){
int length=nums.length;
if(length<=1){
return;
}
int last=(int)Math.pow(2, (int)(Math.log10(length+1)/Math.log10(2)))-2;
for(int i=last;i>=0;i--){
upAdjust(nums,i,length);
}
for(int i=0;i<length;i++){
swap(nums, 0, length-i-1);
downAdjust(nums,0,length-i-1);
}
}
//調整i位和它的子節點的大小,使大的上浮
public static void upAdjust(int[] nums,int i,int length){
int left=2*i+1;
int right=2*i+2;
if(left<length&&nums[left]>nums[i]){
swap(nums, i, left);
}
if(right<length&&nums[right]>nums[i]){
swap(nums, i, right);
}
if(left<length){
upCheck(nums, left, length);
}
if(right<length){
upCheck(nums, right, length);
}
}
public static void upCheck(int[] nums,int i,int length){
int left=2*i+1;
int right=2*i+2;
if((left<length&&nums[left]>nums[i])||(right<length&&nums[right]>nums[i])){
upAdjust(nums, i, length);
}
}
public static void downAdjust(int[] nums,int i,int length){
int left=2*i+1;
int right=2*i+2;
if(left<length&&nums[left]>nums[i]&&(right>=length||(right<length&&nums[left]>=nums[right]))){
swap(nums, i, left);
downAdjust(nums, left, length);
}
if(right<length&&nums[right]>nums[i]&&nums[left]<nums[right]){
swap(nums, i, right);
downAdjust(nums, right, length);
}
}
public static void swap(int[] nums,int i,int j){
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
}
複雜度及優缺點
堆排序的平均時間複雜度為Ο(nlogn) 。
我們知道堆的結構是節點i的孩子為2*i和2*i+1節點,大頂堆要求父節點大於等於其2個子節點,小頂堆要求父節點小於等於其2個子節點。在一個長為n 的序列,堆排序的過程是從第n/2開始和其子節點共3個值選擇最大(大頂堆)或者最小(小頂堆),這3個元素之間的選擇當然不會破壞穩定性。但當為n /2-1, n/2-2, ...1這些個父節點選擇元素時,就會破壞穩定性。有可能第n/2個父節點交換把後面一個元素交換過去了,而第n/2-1個父節點把後面一個相同的元素沒 有交換,那麼這2個相同的元素之間的穩定性就被破壞了。所以,堆排序不是穩定的排序演算法。
八、計數排序
總體思想
基本思想如下
首先找到陣列中的最大值,然後新建一個數組,bucket 此陣列的長度是陣列最大值+1,其實新建的這個陣列中的下標值就是原陣列的資料值,這裡為什麼長度是陣列最大值加一呢
注意*:是因為比如陣列最大值是9,然後如果你設定bucket陣列的長度為9,那麼他的下標最大值就是8,那麼原陣列的9就沒有桶存了
好了繼續,找到最大值後,開始遍歷原陣列,把原陣列的資料加入bucket的下表中,bucket[i],每當有1個i bucket[i]的值就加一, 然後已經裝入桶後, 遍歷桶,如果bucket[j]位置-->0就說明此下標有資料,也就是說,此下標在原數組裡有這個值, 然後排序 就是從大到小了 arr[i++]=j;
然後將原陣列的按個=新的陣列的元素(有幾個的話,=多次,比如nums[2]=5,nums[3]=5)
程式設計思想
因為排序的陣列可能不是從0開始,所以從min開始,到max,建立max-min+1 的陣列
如果min=-1 max=3 size=5 now=1 index=2(now-min)
min=0 max=3 now=1 index=1
min=1 max=3 now=2 index=1(now-min)
可以得到數字的值now與新陣列的index的關係
index=now-min now=index+min
public class CountSort {
public static void countSort(int[] nums){
int length=nums.length;
if(length<=1){
return;
}
int max=nums[0];
int min=nums[0];
for(int i=0;i<length;i++){
int now=nums[i];
if(now>max){
max=now;
}
if(now<min){
min=now;
}
}
int size=max-min+1;
int[] map=new int[size];
for(int i=0;i<length;i++){
int now=nums[i];
map[now-min]++;
}
int index=0;
for(int i=0;i<length;i++){
while(map[index]==0){
index++;
}
map[index]--;
int now=index+min;
nums[i]=now;
}
}
}
複雜度及優缺點
計數排序是一種非常快捷的穩定性強的排序方法,時間複雜度O(n+k),其中n為要排序的數的個數,k為要排序的數的組max-min+1。
計數排序對一定量的整數排序時候的速度非常快,一般快於其他排序演算法。但計數排序侷限性比較大,只限於對整數進行排序。計數排序是消耗空間發雜度來獲取快捷的排序方法,其空間發展度為O(K)同理K為要排序的max-min+1。
九、桶排序
總體思想
簡單來說就是把陣列 arr 劃分為n個大小相同子區間(桶),每個子區間各自排序,最後合併。這樣說是不是和分治法有點像了 啊!因為分治法就是分解 —— 解決 ——合併這樣的套路。我認為這樣想沒毛病。可以理解為桶排序是一種特殊的分治法,特殊的地方主要體現在前兩部分(這樣說不知道對不對~)。
分組排序的演算法可以用快速排序,因為內部的元素比較隨機,速度快
假設有一組長度為N的待排關鍵字序列K[1….n]。首先將這個序列劃分成M個的子區間(桶) 。然後基於某種對映函式 ,將待排序列的關鍵字k對映到第i個桶中(即桶陣列B的下標 i) ,那麼該關鍵字k就作為B[i]中的元素(每個桶B[i]都是一組大小為N/M的序列)。接著對每個桶B[i]中的所有元素進行比較排序(可以使用快排)。然後依次列舉輸出B[0]….B[M]中的全部內容即是一個有序序列。
程式設計思想
1 桶的數目buckets為length的開方,保證有效率
2 每個數對應的桶的index=(now-min)/(max-min+1)*buckets
此時(min-min)/(max-min+1)*buckets=0
(max-min)/(max-min+1)*buckets=buckets-1
很合適,不然,用max-min作為除數,max時,為buckets,超出桶數
3 對每個桶做排序quickSortFirst(totalList.get(i));
這個函式對arraylist和陣列進行轉換,讓我原來的快速排序複用
4 將桶排過序的元素逐個取代nums的東西
public class BucketSort {
public static void bucketSort(int[] nums){
int length=nums.length;
if(length<=1){
return;
}
int max=nums[0];
int min=nums[0];
for(int i=0;i<length;i++){
int now=nums[i];
if(now>max){
max=now;
}
if(now<min){
min=now;
}
}
int buckets=(int)Math.sqrt(length);
List<ArrayList<Integer>> totalList=new ArrayList<>();
for(int i=0;i<buckets;i++){
totalList.add(new ArrayList<>());
}
for(int i=0;i<length;i++){
int now=nums[i];
int index=(now-min)/(max-min+1)*buckets;
totalList.get(index).add(now);
}
for(int i=0;i<buckets;i++){
quickSortFirst(totalList.get(i));
}
int index=0;
for(int i=0;i<buckets;i++){
List<Integer> list=totalList.get(i);
int size=list.size();
if(size==0){
continue;
}
for(int j=0;j<size;j++){
nums[index]=list.get(j);
index++;
}
}
}
public static void quickSortFirst(ArrayList<Integer> list){
int length=list.size();
if(length==0||length==1){
return;
}
int[] nums=new int[length];
for(int i=0;i<length;i++){
nums[i]=list.get(i);
}
quickSort(nums, 0, length-1);
for(int i=0;i<length;i++){
list.set(i, nums[i]);
}
}
public static void quickSort(int[] nums,int begin,int last) {
int length=nums.length;
if(length==0||length==1){
return;
}
int base=nums[begin];
int i=begin;
int j=last;
int mid=0;
//i為第一個,j為最後一個,base為第一個數字,此時i處為base
while(true){
//j從後往前找到比base小的數字,與i作顛倒,顛倒後j處為base,i處為之前j處比base小的數
while(j!=i){
if(nums[j]<base ){
nums[i]=nums[j];
nums[j]=base;
break;
}
else{
j--;
}
}
//如果上一個j過程結束,如果i與j相同,中間為i,跳出大過程
if(i==j){
mid=i;
break;
}
//i從後往前找到比base大的數字,與j作顛倒,顛倒後i處為base,j處為之前i處比base大的數,跳出這個小迴圈
while(i!=j){
if(nums[i]>base){
nums[j]=nums[i];
nums[i]=base;
break;
}
else{
i++;
}
}
//如果上一個i過程結束,如果i與j相同,中間為i,跳出大過程
if(i==j){
mid=i;
break;
}
}
//System.out.println(Arrays.toString(nums));
//從此處開始mid處為base,比mid處大的,則比base大,反之亦然
//對兩次進行遞迴快速排序
if((mid-begin)>1){
quickSort(nums, begin, mid-1);
}
if((last-mid)>1){
quickSort(nums, mid+1, last);
}
return;
}
}
複雜度及優缺點
桶排序是計數排序的改進,當桶每次只裝一個的時候,就是計數排序
桶排序利用函式的對映關係,減少了幾乎所有的比較工作。實際上,桶排序的f(k)值的計算,其作用就相當於快排中劃分,已經把大量資料分割成了基本有序的資料塊(桶)。然後只需要對桶中的少量資料做先進的比較排序即可。
對N個關鍵字進行桶排序的時間複雜度分為兩個部分:
(1) 迴圈計算每個關鍵字的桶對映函式,這個時間複雜度是O(N)。
(2) 利用先進的比較排序演算法對每個桶內的所有資料進行排序,其時間複雜度為 ∑ O(Ni*logNi) 。其中Ni 為第i個桶的資料量。
很顯然,第(2)部分是桶排序效能好壞的決定因素。儘量減少桶內資料的數量是提高效率的唯一辦法(因為基於比較排序的最好平均時間複雜度只能達到O(N*logN)了)。因此,我們需要儘量做到下面兩點:
(1) 對映函式f(k)能夠將N個數據平均的分配到M個桶中,這樣每個桶就有[N/M]個數據量。
(2) 儘量的增大桶的數量。極限情況下每個桶只能得到一個數據,這樣就完全避開了桶內資料的“比較”排序操作。 當然,做到這一點很不容易,資料量巨大的情況下,f(k)函式會使得桶集合的數量巨大,空間浪費嚴重。這就是一個時間代價和空間代價的權衡問題了。
對於N個待排資料,M個桶,平均每個桶[N/M]個數據的桶排序平均時間複雜度為:
O(N)+O(M*(N/M)*log(N/M))=O(N+N*(logN-logM))=O(N+N*logN-N*logM)
當N=M時,即極限情況下每個桶只有一個數據時。桶排序的最好效率能夠達到O(N)。
總結:桶排序的平均時間複雜度為線性的O(N+C),其中C=N*(logN-logM)。如果相對於同樣的N,桶數量M越大,其效率越高,最好的時間複雜度達到O(N)。當然桶排序的空間複雜度為O(N+M),如果輸入資料非常龐大,而桶的數量也非常多,則空間代價無疑是昂貴的。此外,桶排序是穩定的。
十、基數排序
總體思想
基數排序又稱為“桶子法”,從低位開始將待排序的數按照這一位的值放到相應的編號為0~9的桶中。等到低位排完得到一個子序列,再將這個序列按照次低位的大小進入相應的桶中,一直排到最高位為止,陣列排序完成。
基數排序不同於其他的排序演算法,它不是基於比較的演算法。基數排序是一種藉助多關鍵字排序的思想對單邏輯關鍵字進行排序的方法。它是一種穩定的排序演算法。多關鍵字排序中有兩種方法:最高位優先法(MSD)和最低位優先法(LSD)。通常用於對數的排序選擇的是最低位優先法,即先對最次位關鍵字進行排序,再對高一位的關鍵字進行排序,以此類推。
演算法的思想:類似於桶式排序,我們需要給待排序記錄準備10個桶,為什麼是10個??因為一個數的任何一位上,其數字大小都位於0~9之間,因此採用10個桶,桶的編號分別為0,1,2,3,4...9,對應待排序記錄中每個數相應位的數值,基數排序也是因此而得名。我們先根據待排序記錄的每個數的個位來決定讓其加入哪個桶中。例如:待排序陣列為
278 109 63 930 589 184 505 269 8 83
求取每個數的個位數,依次為:8 9 3 0 9 4 5 9 8 3
依照其個位數決定將其加入哪個桶中
[0] | 930 | |||
[1] | ||||
[2] | ||||
[3] | 63 | 83 | ||
[4] | 184 | |||
[5] | 505 | |||
[6] | ||||
[7] | ||||
[8] | 278 | 8 | ||
[9] | 109 | 589 | 269 |
此步驟即為教材中所說的分配,接下來就是要進行收集,依照桶的編號,將含有資料的桶中的資料依次取出,形成的新的資料記錄為:
930 63 83 184 505 278 8 109 589 269
再對這個陣列按照十分位進行分配進桶,收集,最後再按照百位進行分配進桶,收集。就可得到最終的排序結果。
程式設計思想
1 先得到nums陣列的max,確定最大位數
2 radixSortDigit(nums,i) i從1到最大位數,從個位排序到最大位
3 radixSortDigit(int[] nums,int digit) 排第i位(從後往前數,個位為digit=1)
4 List<ArrayList<Integer>> buckets 中有10個arraylist,
5 對nums進行迴圈,對每個num求第digit位,加入對應的arraylist
6 將buckets中從0-9的arraylist逐個取出,將arraylsit中的東西逐個覆蓋nums
public class RadixSort {
public static void radixSort(int[] nums) {
int length=nums.length;
if(length==0||length==1){
return;
}
int max=nums[0];
for(int i=0;i<length;i++){
if(nums[i]>max){
max=nums[i];
}
}
int digits=((int)Math.log10(max))+1;
for(int i=1;i<=digits;i++){
radixSortDigit(nums,i);
}
}
public static void radixSortDigit(int[] nums,int digit){
List<ArrayList<Integer>> buckets=new ArrayList<>();
int length=nums.length;
for(int i=0;i<10;i++){
buckets.add(new ArrayList<Integer>());
}
for(int i=0;i<length;i++){
int now=nums[i];
int dig=getDigit(now, digit);
buckets.get(dig).add(now);
}
int index=0;
for(int i=0;i<10;i++){
List<Integer> list=buckets.get(i);
int size=list.size();
if(size==0){
continue;
}
for(int j=0;j<size;j++){
nums[index]=list.get(j);
index++;
}
}
}
public static int getDigit(int num,int digit){
if(num<Math.pow(10, digit-1)){
return 0;
}
//1301mod1000=301 301/100=3
int result=(int)num%((int)Math.pow(10, digit));
result=result/((int)Math.pow(10, digit-1));
return result;
}
}
複雜度及優缺點
基數排序是按照低位先排序,然後收集;再按照高位排序,然後再收集;依次類推,直到最高位。有時候有些屬性是有優先順序順序的,先按低優先順序排序,再按高優 先級排序,最後的次序就是高優先順序高的在前,高優先順序相同的低優先順序高的在前。基數排序基於分別排序,分別收集,所以其是穩定的排序演算法。
總體複雜度
排序方法 | 平均時間 | 最壞情況 | 最好情況 | 輔助空間 | 穩定性 |
氣泡排序 | O(n^2) | O(n^2) | 普通O(n^2)優化O(n) | O(1) | 是 |
選擇排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 否 |
插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 是 |
希爾排序 | 視遞減陣列 O(n^5/4) | O(n^3/2) | O(n) | O(1) | 否 |
歸併排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 是 |
快速排序 | O(nlogn) | O(n^2) | O(nlogn) | O(nlogn)-最差O(n^2) | 否 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 否 |
計數排序 | O(n+k) k為要排序的數的組max-min+1 | O(n+k) | O(n+k) | O(k) | 是 |
桶排序 | O(N+C),其中C=N*(logN-logM),M為桶的數量 | M=1,就是快速排序 | M=max-min+1,變成計數排序 |