一點就懂的經典十大排序算法

1、概述

十種常見排序算法可以分爲兩大類

  • 比較類排序:通過比較來決定元素間的相對次序,由於其時間複雜度不能突破O(nlogn),因此也稱爲非線性時間比較類排序。
  • 非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基於比較排序的時間下界,以線性時間運行,因此也稱爲線性時間非比較類排序。
    如圖所示
    在這裏插入圖片描述
    一些概念小結
  • 穩定性判斷:假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序算法是穩定的;否則稱爲不穩定的。
  • 時間複雜度:對排序數據的總的操作次數。反映當n變化時,操作次數呈現什麼規律。使用大O估計法
  • 空間複雜度:是指算法在計算機內執行時所需存儲空間的度量,它也是數據規模n的函數。使用大O估計法

關於穩定性:需要注意的是,排序算法是否爲穩定的是由具體算法決定的,不穩定的算法在某種條件下可以變爲穩定的算法,而穩定的算法在某種條件下也可以變爲不穩定的算法。

2、十大算法性能比較

在這裏插入圖片描述

3、排序算法精講

3.1 超級經典的排序——冒泡排序和它的優化

1)原理
n個元素的序列,從第一個數開始,每次比較相鄰的兩個數,如果符合a[j]<a[j+1],就交換,直到把當前的最大數“冒泡”到最上面,我們完成一輪的冒泡趟數;每一輪冒泡趟數,找到最大的那個數放到數組最後面,然後接着對剩下的n-1個數進行下一輪的冒泡。這樣總共要冒泡 n-1趟
2)代碼實現(java)

    public static void bubbleSort(int[]array,boolean exchange) {//重載,exchange標誌表示爲升序排序還是降序排序
        for (int i =1; i < array.length; i++)//控制次數,第幾趟排序,只需要n-1趟
        {
            for (int j = 0; j < array.length - i; j++)//i表示進行的趟數,每一趟能排好一個數,只需要排剩下的array.length-i個數
                if (exchange ? array[j] > array[j + 1] : array[j] < array[j + 1])//控制升序還是降序
                {
                    int temp = array[j];
        			array[j]=arrjay[j+1];
        			array[j+1]=temp;//交換兩個數 
                }             

        }
    }

3)優化
我們知道每一趟的進行,只是爲了把當前的最大的數冒泡上去,那如果當前趟數的最大數本身就是在數組的最右邊(即本身已經冒泡好了),那我們何必繼續該輪冒泡呢?基於這一點,我們可以推出:上一輪沒有發生交換,說明數據的順序已經排好,沒有必要繼續進行下去。

解決辦法:加一個標誌位,記錄上一次是否發生了交換,如果是,我們則進行下一輪,如果沒有,說明已經冒泡好了

代碼實現:

public static void bubbleSort(int[]array,boolean exchange) {//重載,exchange標誌表示爲升序排序還是降序排序
   boolean flag = true;
   for (int i =1; i < array.length&&flag; i++)//控制次數,第幾趟排序,只需要n-1趟,有交換時進行,只有flag=false就說明上一次一個元素都沒有進行交換
  {
   	flag = false;//假定未交換
  	for (int j = 0; j < array.length - i; j++)//i表示進行的趟數,每一趟能排好一個數,只需要排剩下的array.length-i個數
  		if (exchange ? array[j] > array[j + 1] : array[j] < array[j + 1])//控制升序還是降序
             {
                int temp = array[j];
        		array[j]=arrjay[j+1];
        		array[j+1]=temp;//交換兩個數 
        		flag = true;//記錄是否發生交換,
              }             

        }
    }

4)Demo演示
原算法:
在這裏插入圖片描述
優化以後:
在這裏插入圖片描述
5)評價
它的時間複雜度達到了 O(N2)。這是一個非常高的時間複雜度。冒泡排序早在 1956 年就有人開始研究,之後有很多人都嘗試過對冒泡排序進行改進,但結果卻令人失望。如 Knuth(Donald E. Knuth 中文名爲高德納,1974 年圖靈獎獲得者)所說:“冒泡排序除了它迷人的名字和導致了某些有趣的理論問題這一事實之外,似乎沒有什麼值得推薦的。” 假如我們的計算機每秒鐘可以運行 10 億次,那麼對 1 億個數進行排序,桶排序則只需要 0.1 秒,而冒泡排序則需要 1 千萬秒,達到 115 天之久!有沒有既不浪費空間又可以快一點的排序算法呢?那就是“快速排序”啦!

3.2 最常用的排序——快速排序(基準值分段,交換,分而治之,遞歸實現)

1)原理
設要排序的數組是A[0]……A[N-1],首先任意選取一個數據(通常選用數組的第一個數)作爲基準數據,然後將所有比它小的數都放到它左邊所有比它大的數都放到它右邊這個過程稱爲一趟快速排序。值得注意的是,然後我們就相當於將一個數組分成了兩截,這個時候我們可以繼續下去,對這兩個子數組繼續劃分,而實現這個,可以用遞歸。我們將大的原始數組分成小的兩段,再繼續分,1分2,2分4...,體現了分而治之遞的思想,而實現上我們利用了遞歸!

一趟快速排序的算法是:
1)設置兩個變量i、j(i,j我們可以看成是兩個哨兵,i是從左到右放哨,j是從右到左放哨)排序開始的時候:i=0,j=N-1;
2)每次都是將該數組第一個元素作爲基準數據,賦值給base,即base=A[0];
3)從j(哨兵j先走)開始放哨,即由後開始向前搜索(j--),找到第一個小於base的值A[j]每找到一個小於base的值,我們就查看在i<j的條件下(哨兵i開始前進)是否存在比base值大的數A[i]

  • 存在就進行交換swap(A[i],A[j]),然後回跳到哨兵j繼續走
  • 不存在,哨兵i就會繼續放哨,直到i=j,完成一趟快速排序

最後我們把這個過程遞歸即可,需要注意的就是分而治之時的基準值都是每一子數組的第一個元素。(如圖,基準爲6,第一個元素)
在這裏插入圖片描述
在這裏插入圖片描述
。。。。。
2)代碼實現
這是我自己看一半的原理,然後自己寫的:

 public void quickSort(int[]array,int begin,int end){

        if(begin>=0&&end>=0&&begin<end){//遞歸結束條件
            int i=begin,j=end;
            //以第一個爲基準值
           int base = array[begin];
           while(i<j)//哨兵j先移動
            {
                if(array[j]<base)//哨兵j每找到一個小於基準值的
                    for(;i<j;i++){
                        if(array[i]>base){//哨兵i就找一次大於基準值的進行交換,交換一次就回去繼續下一輪交換
                            swap(array,i,j);
                            break;
                        }
                        //找不到比基準大的哨兵i繼續往右移動。
                    }
                if(i==j)//因爲現在i不清楚是從哪裏出來的我們需要判斷是break出來還是i==j
                    break;
                j--;//否則我們哨兵j繼續搜索

            }
            
           swap(array,begin,j);//交換基準值,實現小於基準值的都在左邊,大於基準值的都在右邊

            //遞歸
            quickSort(array,0,j-1);//左子數組
            quickSort(array,j+1,end);//右子數組

        }

    }

書本的標準寫法:

public static int[] qsort(int arr[],int start,int end) {        
    int pivot = arr[start];        
    int i = start;        
    int j = end;        
    while (i<j) {            
        while ((i<j)&&(arr[j]>pivot)) {                
            j--;            
        }            
        while ((i<j)&&(arr[i]<pivot)) {                
            i++;            
        }            
        if ((arr[i]==arr[j])&&(i<j)) {                
            i++;            
        } else {                
            int temp = arr[i];                
            arr[i] = arr[j];                
            arr[j] = temp;            
        }        
    }        
    if (i-1>start) arr=qsort(arr,start,i-1);        
    if (j+1<end) arr=qsort(arr,j+1,end);        
    return (arr);    
}    

3)Demo演示
我的:

 public static void main(String[]args){
        int[] array = new int[]{1,2,5,5,6,6,0,0,1,2,5,55,555,7777};
        for(int arr:array){
            System.out.print(arr+" ");
        }
        System.out.println();
        new QuickSort().quickSort(array);
        for(int arr:array){
            System.out.print(arr+" ");
        }

在這裏插入圖片描述
4)評價
從上面的結果就能知道快速排序是不穩定的,原因也很容易想,這裏就不贅述。快速排序的一次劃分算法從兩頭交替搜索,直到i和j重合,因此其時間複雜度是O(n);而整個快速排序算法的時間複雜度與劃分的趟數有關。
理想的情況是,每次劃分所選擇的中間數恰好將當前序列幾乎等分,經過log2n趟劃分,便可得到長度爲1的子表。這樣,整個算法的時間複雜度爲O(nlog2n)。
最壞的情況是,每次所選的中間數是當前序列中的最大或最小元素,這使得每次劃分所得的子表中一個爲空表,另一子表的長度爲原表的長度-1。這樣,長度爲n的數據表的快速排序需要經過n趟劃分,使得整個排序算法的時間複雜度爲O(n2)。
可以證明快速排序的平均時間複雜度也是O(nlog2n)。因此,該排序方法被認爲是目前最好的一種內部排序方法。
從空間性能上看,儘管快速排序只需要一個元素的輔助空間,但快速排序需要一個棧空間來實現遞歸。最好的情況下,即快速排序的每一趟排序都將元素序列均勻地分割成長度相近的兩個子表,所需棧的最大深度爲log2(n+1);但最壞的情況下,棧的最大深度爲n。這樣,快速排序的空間複雜度爲O(log2n))

3.3 最簡單直接的排序——直接選擇排序(挑最大/最小的那個)

1)原理

它的工作原理是:第一次從待排序的n個數據元素中選出最小(或最大)的一個元素,存放在序列的起始位置,然後再從剩餘的n-1個未排序元素中尋找到最小(大)元素,然後放到當前的序列的起始位置。以此類推,直到全部待排序的數據元素的個數爲零。趟數是n-1趟。是不是很簡單啊?我們每次都只用找最小的一個數,然後把它從左到右依次放就好了。
2)代碼實現

  public static void selectSort(int[]array){
        for(int i=0;i<array.length;i++){
            int min= i;//假定當前的序列第一個數就是最小的值
            for(int j=i+1;j<array.length;j++){
                if(array[j]<array[min])
                    min = j;//每次我們從剩下的數裏邊找最小值,記錄它的下標
            }
            //然後交換
            if(i!=min)//如果最小值不是它自己我們才進行交換了。
                swap(array,i,min);
        }

    }

3)Demo演示

public static void main(String[]args){
        int[]array = {45,12,65,89,66,99,32,564,78};
        for(int arr:array){
            System.out.print(arr+" ");
        }
        System.out.println();
        SelectSort.selectSort(array);
        for(int arr:array){
            System.out.print(arr+" ");
        }
    }

運行結果:
在這裏插入圖片描述

4)評價

首先我們肯定能夠知道選擇排序是不穩定的排序方法。它的比較次數與數據初始排序狀態無關,第i趟比較的次數是 n-i移動的次數與初始的排列有關,如果本來就排序好了,移動0次,反之最大移動了3*(n-1)次(我們交換一次要三步,總次數n-1)。,總的比較次數N=(n-1)+(n-2)+...+1=n*(n-1)/2。
時間複雜度O(n^2),空間複雜度明顯常數階O(1)交換次數比冒泡排序少多了,由於交換所需CPU時間比比較所需的CPU時間多,n值較小時,選擇排序比冒泡排序快。但是因爲每次都要選擇最大的值交換,查找最大值浪費了好多時間。

3.4 看起來好煩的排序——堆排序(間接選擇排序、完全二叉樹)

1)原理
很口語的說法:你有一個神器,每次都能一下子找到當前數組序列裏面的最小值所在(或者最大值所在,只能其一)那這個時候我們進行直接選擇排序的時候我們就能節省了找最值的時間了。直接交換到當前序列第一個元素就行了。然後,在剩下的數組序列裏邊我們再用神器找到最小值所在,我們就交換,重複,直到排完數組的數。
那麼這個神器是什麼呢?那就是——,用數組實現的一個有特別屬性完全二叉樹戳我一下詳細瞭解堆的結構
堆分爲兩種

  • 最大堆,雙親結點的值比每一個孩子結點的值都要根結點值最大
  • 最小堆,雙親結點的值比每一個孩子結點的值都要根結點值最小

注意:堆的根結點中存放的是最大或者最小元素,但是其他結點的排序順序是未知的。例如,在一個最大堆中,最大的那一個元素總是位於根結點 的位置,但是最小的元素則未必是最後一個元素。唯一能夠保證的是最小的元素是一個葉結點,但是不確定是哪一個。

數組實現的堆(完全二叉樹)圖示:
在這裏插入圖片描述
可以和層序遍歷聯想一下就能清楚裏面的index的規律。

基於上面的一些介紹,我們可以知道算法的步驟應該是:

  • 1)把輸入的數組建立堆的結構,可以神器般速度獲取(根結點)最大值(最小值)的所在
  • 2)使用直接選擇排序將當前的堆獲取的最大值(最小值)與當前序列的末尾元素進行交換
  • 3)此時我們破壞了堆原有的屬性,我們要把剩下的數組序列進行重新建立堆
  • 4)回彈第二步,直到所有的數都排好序

所以難點就在於怎麼建立堆,重新建立堆!

2)代碼實現

    public static void heapSort(int[]array){
        //1.對傳入的數組進行建立堆,這裏默認建立最小堆
        heapSort(array,true);
    }
    public static void heapSort(int[]array,boolean minheap){
        for(int i=array.length/2-1;i>=0;i--){
            //創建最小最大堆,默認最小堆,即minheap=true
            sift(array,i,array.length-1,minheap);

        }
        //排序,使用直接選擇排序
        for(int j=array.length-1;j>0;j--){
            //現在的數組第一個就是根結點,最小值所在,進行交換,把它放到最右邊
            swap(array,0,j);
            //重新建立堆
            sift(array,0,j-1,minheap);//將剩下的j-1個數,把它們調整爲堆, 實質上是自上而下,自左向右進行調整的
           
    }
    //建立堆的方法

    /**
     * 私有方法,只允許被堆排序調用
     * @param array 要排序數組
     * @param parent 雙親結點
     * @param end 數組長度
     * @param minheap 是否建立最小堆
     */
    private  static void sift(int[]array,int parent,int end,boolean minheap){
        int child = 2*parent+1;//利用公式,創建child是parent的左孩子,+1則是右孩子
        int value = array[parent];//獲取當前雙親結點值
        for(;child<end;child=child*2+1){//遍歷

            //注意這裏的child必須小於end,防止越界,建立最小堆,右孩子如果比左孩子小,我們就將現在的孩子換到右孩子
            //因爲現在如果右孩子大於雙親,自然左孩子也大於雙親
            if(child<end&&(minheap?array[child]>array[child+1]:array[child]<array[child+1]))//比較左孩子與右孩子的大小
                child++;//右孩子如果比左孩子大,我們就將現在的孩子換到右孩子

            //判斷是否符合最小堆的特性, 如果右孩子大於雙親,自然左孩子也大於雙親,符合
            if(minheap?value>array[child]:value<array[child]){
              
                swap(array,parent,child);////右孩子沒有大於雙親,我們將其交換
                parent = child;//然後我們更新雙親結點和孩子結點
            }
            //如果不是,說明已經符合我們的要求了。
            else
                break;

        }
    }

3)Demo
在這裏插入圖片描述
4)評價
堆排序是利用堆這種數據結構而設計的一種排序算法,堆排序是一種選擇排序,整體主要由構建初始堆+交換堆頂元素和數組末尾元素並重建堆兩部分組成。其中構建初始堆經推導複雜度爲O(n),在交換並重建堆的過程中,需交換n-1次,而重建堆的過程中,根據完全二叉樹的性質,[log2(n-1),log2(n-2)…1]逐步遞減,近似爲nlogn。所以堆排序時間複雜度一般認爲就是O(nlogn)級。

5)應用
不僅用於排序算法,還可以應用於頻繁選擇極值的問題,如優先隊列,、Huffman、Prim、Kruskal、Dijkstra、Floyd等算法。

3.5 分治法的典型應用——歸併排序(分治遞歸)

1)原理
非常棒的歸併排序講解,圖解

歸併排序(英語:Merge sort,或mergesort),是創建在歸併操作上的一種有效的排序算法,效率爲 O(nlog n)}(大O符號)。1945年由約翰·馮·諾伊曼首次提出。該算法是採用分治法(Divide and Conquer)的一個非常典型的應用,且各層分治遞歸可以同時進行。
原理描述:遞歸回彈的過程真爽!

圖示
在這裏插入圖片描述

我們遞歸序列進行對半分(奇數則相差1),繼續遞歸進行對半分,直到無法再分
:然後遞歸回彈!開始對這些子序列進行排序及合併操作,兩個小小序列合併成小序列,兩個小序列合併成大序列,然後就排序好了。

作爲一種典型的分而治之思想的算法應用,歸併排序的實現分爲兩種方法:

  • 自上而下的遞歸;
  • 自下而上的迭代;

2)代碼實現
個人比較喜歡遞歸實現:

   public static void mergeSort(int []arr){
        int []temp = new int[arr.length];//在排序前,先建好一個長度等於原數組長度的臨時數組,避免遞歸中頻繁開闢空間
        mergeSort(arr,0,arr.length-1,temp);
    }

    /**
     *
     * @param arr 傳入數組
     * @param left 當前子數組的起始下標
     * @param right 當前子數組的結束下標
     * @param temp 拷貝暫存數組
     */
    private static void mergeSort(int[] arr,int left,int right,int []temp){
        if(left<right){//這裏是遞歸結束的條件,我們是對半分,那當left==right的時候肯定大家都是隻有一個元素了。
            int mid = (left+right)/2;//對半分,比如總長度是10,left=0,right=9,mid=4確實是中間分了,0~4,5~9
            //當長度9,left=0,right=8,mid=4,0~4,5~8
            mergeSort(arr,left,mid,temp);//左邊歸併排序,使得左子序列有序
            mergeSort(arr,mid+1,right,temp);//右邊歸併排序,使得右子序列有序
            merge(arr,left,mid,right,temp);//將兩個有序子數組合並操作
        }
    }
    private static void merge(int[] arr,int left,int mid,int right,int[] temp){
        int i = left;//左序列起始下標
        int j = mid+1;//右序列起始下標
        int t = 0;//臨時數組指針
        while (i<=mid && j<=right){
            if(arr[i]<=arr[j]){//比較兩個序列第一個元素誰小,誰小先拷貝誰到temp,然後對應子序列下標加1
                temp[t++] = arr[i++];
            }else {
                temp[t++] = arr[j++];
            }
        }
        while(i<=mid){//將左邊剩餘元素填充進temp中——左序列有一些數總是比右邊的大的數
            temp[t++] = arr[i++];
        }
        while(j<=right){//將右序列剩餘元素填充進temp中——右序列有一些數總是比左邊的大的數
            temp[t++] = arr[j++];
        }
        t = 0;
        //將temp中的元素全部拷貝到原數組中
        while(left <= right){//注意這裏的條件是left——right
            arr[left++] = temp[t++];
        }
    }

當然也得學會迭代法實現:

public static void mergeSort(int[] arr) {
	int[] orderedArr = new int[arr.length];//建立一個數組用於臨時拷貝
	for (int i = 2; i < arr.length * 2; i *= 2) {//逆向看,一個數組被拆分到最後有幾塊,對應子序列起始下標末尾下標的規律是什麼
		for (int j = 0; j < (arr.length + i - 1) / i; j++) {
			int left = i * j;//規律所在
			int mid = left + i / 2 >= arr.length ? (arr.length - 1) : (left + i / 2);
			int right = i * (j + 1) - 1 >= arr.length ? (arr.length - 1) : (i * (j + 1) - 1);
			int start = left, l = left, m = mid;
			while (l < mid && m <= right) {
				if (arr[l] < arr[m]) {
					orderedArr[start++] = arr[l++];
				} else {
					orderedArr[start++] = arr[m++];
				}
			}
			while (l < mid)
				orderedArr[start++] = arr[l++];
			while (m <= right)
				orderedArr[start++] = arr[m++];
			System.arraycopy(orderedArr, left, arr, left, right - left + 1);//拷貝
		}
	}
}

3)Demo
在這裏插入圖片描述
4)評價

歸併排序嚴格遵循從左到右或從右到左的順序合併子數據序列, 它不會改變相同數據之間的相對順序, 因此歸併排序是一種穩定的排序算法.分階段可以理解爲就是遞歸拆分子序列的過程,遞歸深度爲log2n,比較操作的次數介於 (nlog n)/2nlog n-n+1。 賦值操作的次數是2nlog n。歸併算法的空間複雜度爲:O(n)

3.6 跟打牌一樣的排序——插入算法(每個數都找到它應該插入的位置)

1)原理
以第一個數爲起始子序列,從第二個數開始,將這個數與與子序列比較找到自己應該插入的位置,找到了,就插入進去,起始子序列變爲從第一個數到第二個數在這之前需要數組騰出空間給它插入)。完成一個數的插入,繼續下一個數。直到最後一個數插入完畢。

簡單得跟我們打牌整理牌一樣。

2)代碼實現

   //從右邊第二個數字開始,在與前面的子序列進行比較,找到屬於自己的位置進行插入
    public static void straghtInsertSort(int []array){
        //
        straghtInsertSort(array,true);//默認進行升序
    }
    public static void straghtInsertSort(int []array,boolean flag){
        if(flag){//選擇是升序還是降序排序
            for(int i=1;i<array.length;i++){//從第二個數開始
                int temp = array[i],j;
                for( j=i-1;j>=0&&temp<array[j];j--)//array[i]是要插入的數
                    //即從j到i-1這些數都需要往後面移動一位,空出的位置給array[i]插入,
                    array[j+1] = array[j];//每比較一次我們就知道這個數需要往後面諾出一個位置了。
                array[j+1] = temp ;//注意這裏要加1
                System.out.println("插入位置是"+i+",插入的數字是"+temp);
            }
        }

        else{//代碼基本一致
            for(int i=1;i<array.length;i++){//從第二個數開始,默認第一個數是最小的
                int temp = array[i],j;
                for( j=i-1;j>=0&&temp>array[j];j--)//array[i]是要插入的數
                    //即從j到i-1這些數都需要往後面移動一位,空出的位置給array[i]插入,
                    array[j+1] = array[j];//每比較一次我們就知道這個數需要往後面諾出一個位置了。
                array[j+1] = temp ;//注意這裏要加1
                System.out.println("插入位置是"+i+",插入的數字是"+temp);
            }
        }

    }

3)Demo
在這裏插入圖片描述
4)優化
我們在查找該數的插入位置是用順序查找,但是因爲與該數比較的序列它是有序的啊,那不就可以用二分查找的辦法了?這就是——二分法插入排序
代碼:

 public static void straghtInsertSort1(int []array){//優化
                for(int i=1;i<array.length;i++){//從第二個數開始,默認第一個數是最小的
                    int temp = array[i],start=0,end=i-1,mid=-1;
                    for( ;end>=start;)//array[i]是要插入的數
                    {
                        mid = start+(end-start)/2;
                        if(array[mid]>temp)
                            end=mid-1;
                        else //元素相同時,我們也插入後面,肯能破壞穩定性
                            start=mid+1;
                    }
                    //移動元素
                    for(int len=i-1;len>=start;len--){
                        array[len+1] = array[len];
                    }
                    array[start]=temp;
                    System.out.println("插入位置是"+start+",插入的數字是"+temp);


                }

            }

5)展示
原算法未優化時
在這裏插入圖片描述
使用二分法插入優化時:
在這裏插入圖片描述
。。數據比較少,沒體現出來。。。。
6)評價
跟打牌一樣的算法,算法是穩定的。時間複雜度平均時間O(n^2),空間複雜度O(1)

3.7 改進直接插入排序的排序——希爾排序(縮小增量排序,Δ牛筆)

1)原理
很簡單,一張圖就能看懂
在這裏插入圖片描述
希爾排序是基於插入排序的以下兩點性質而提出改進方法的:

  • 插入排序在對幾乎已經排好序的數據操作時,效率高,即可以達到線性排序的效率。
  • 但插入排序一般來說是低效的,因爲插入排序每次只能將數據移動一位。

該方法實質上是一種分組插入方法
比較相隔較遠距離(稱爲增量)的數,使得數移動時能跨過多個元素,則進行一次比較就可能消除多個元素交換。D.L.shell於1959年在以他名字命名的排序算法中實現了這一思想。算法先將要排序的一組數按某個增量d分成若干組,每組中記錄的下標相差d.對每組中全部元素進行排序,然後再用一個較小的增量對它進行,在每組中再進行排序。當增量減到1時,整個要排序的數被分成一組,排序完成。
一般的初次取序列的一半爲增量,以後每次減半,直到增量爲1。

2)代碼實現

  public static void shellSort(int[]array){
        for(int delta=array.length/2;delta>0;delta=delta/=2){//控制增量的變化

            for(int i=delta;i<array.length;i++){//遍歷每個組
                int temp = array[i],j;
                //組內排序
                for(j=i-delta;j>=0&&temp<=array[j];j-=delta){
                    array[j+delta]=array[j];
                }
                array[j+delta] = temp;
            }
        }
    }

3)Demo
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

???????????????????????????

5)評價
希爾排序是非穩定排序算法。不需要大量的輔助空間,和歸併排序一樣容易實現。希爾排序是基於插入排序的一種算法, 在此算法基礎之上增加了一個新的特性,提高了效率。希爾排序的時間的時間複雜度爲O( n^1.5)),希爾排序時間複雜度的下界是n*log2n。希爾排序沒有快速排序算法快 O(n(logn)),因此中等大小規模表現良好,對規模非常大的數據排序不是最優選擇。但是比O( n^2)複雜度的算法快得多。並且希爾排序非常容易實現,算法代碼短而簡單。 此外,希爾算法在最壞的情況下和平均情況下執行效率相差不是很多,與此同時快速排序在最壞的情況下執行的效率會非常差。專家們提倡,幾乎任何排序工作在開始時都可以用希爾排序,若在實際使用中證明它不夠快,再改成快速排序這樣更高級的排序算法. 本質上講,希爾排序算法是直接插入排序算法的一種改進,減少了其複製的次數,速度要快很多。 Good?

3.8 傳說中時間複雜度爲O(n)的排序——計數排序(非比較穩定排序)

1)原理
計數排序(Counting sort)是一種穩定的線性時間排序算法。該算法於1954年由 Harold H. Seward 提出。計數排序使用一個額外的數組用來計數。

簡單巧妙的原理,假設有數組A[10]值爲:1~10個數,現在我們統計出現的每一個數的個數,如出現5一次我們記一次,假設出現了3次,我們就把3存進一個數組計數器相應的位置這個數組計數器相應的下標都加上或者減去一個定值(delta)能夠讓我們知道存進來的是數組A中的哪個數(看作映射),而這個計數器下標裏邊的數代表該數出現的次數

這樣我們就知道一個數出現的次數是多少了。我們利用了映射——找到最大最小值max,min,建立數組計數器count的長度max-min+1,那我們令delta爲0+min那count[0]存放的是(0+delta)的值出現的個數,即min的個數!

排序過程:就是把計數器裏的數一個一個地放回去。

所以就分兩步:

  • 計數
  • 放回排序

圖解
在這裏插入圖片描述

2)代碼實現

   public static void countSort(int[]array){
        //找到最大最小值,建立基於這個長度(max-min+1)的一個數組容器來計數,我們可以建立一個下標與存儲的數組的0值的映射
        int delta,min=array[0],max=array[0];
        //1.找最大最小值
        for(int i=1;i<array.length;i++){
            if(array[i]<min)
                min=array[i];//找最小值
            if(array[i]>max)
                max=array[i];//找最大值
            }
        //建立一個用於計數的數組,對應的下標映射實際數組的值
        delta = 0+min;//映射關係就是計數的數組下標與實際的值相差delta
        int []count_map = new int[max-min+1]; //count_map裏的值都初始化爲0;
        //開始計數
        for(int i=0;i<array.length;i++)
            //實現計數,array[i]-delta表示array[i]在count_map的數組下標映射,同一個下標則表示相同的數,該數大小爲i+delta,++則實現計數
            count_map[array[i]-delta]++;
        //計算完數組以後,我們開始依次放回,默認升序
        int i=0;//用來指示計數數組的下標是否需要移動,當對應的count_map[i]==0,i++
        for(int j=0;j<array.length;){//填充,j用來控制array下標
            if(count_map[i]>0){
               array[j] =i+delta;//放回一個數
               j++;
               count_map[i]--;//計數器減1;
           }
           else i++;//放完了,就進行放下一個不重複的數
       }
    }

3)Demo
還是上次選擇排序的那個數組,我這裏使用了降序:

 int []array1 = {12,121,5,45,4,4,74,978,979,7,4,6,64,6,4554,979,44,55,1,215,46,22,6464,7977,797,797,79,9,11};
        long  t1 = System.currentTimeMillis();
        for(int i=0;i<100;i++)

            countSort(array1);
        //countSort(array1,false);
        System.out.println((System.currentTimeMillis()-t1)+"ms");

在這裏插入圖片描述

4)評價
當輸入的元素是n 個0到k之間的整數時,它的運行時間是 O(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序算法。由於用來計數的數組的長度取決於待排序數組中數據的範圍(等於待排序數組的最大值與最小值的差加上1)這使得計數排序對於數據範圍很大的數組,需要大量時間和內存。值得一提它是穩定的排序。

最佳情況:T(n) = O(n+k) 最差情況:T(n) = O(n+k) 平均情況:T(n) = O(n+k)

3.9 快快快的桶裝數據排序——桶排序(利用了快排的特性和計數裏的類似映射)

1)原理

桶排序是計數排序的升級版。桶排序 (Bucket sort)的工作的原理:假設輸入數據服從均勻分佈,將數據分到有限數量的桶裏,每個桶再分別排序(有可能再使用別的排序算法或是以遞歸方式繼續使用桶排序進行排

讓我想一下怎麼用口水話說:

  • 1)我們收到一個數組,我們就以某種規則指定了桶的個數n每個飯桶有它自己的飯桶號碼,從0到n-1(嘻嘻)
  • 2) 現在我們開始將數組裏的數放進桶裏了,放進哪個編號的桶呢?規則就是找到最大值,最小值max,min,進而制定了每個飯桶它的“飯量範圍”,如圖,設置桶的數量爲5個空桶,找到最大值110,最小值7,每個桶的飯量範圍20.8=(110-7+1)/5 。對於0號桶,飯量[min,min+20.8X1)1號桶飯量就是[min+20.8X1,min+20.8X2)則4號飯桶的飯量就是[min+20.8X4,min+20.8X5),然後我們的飯桶就能愉快的“喫飯了”,把數放進自己對應的飯桶!
  • 在這裏插入圖片描述
  • 3)現在就是將每個飯桶內部的數進行排序,你可以使用遞歸桶排序,直到每個桶剩下一個數,也可以使用其他排序方法對桶內的數排序
  • 4)將內部排好序的桶按照飯量大小進行合併(也就是飯桶的桶號)

一個很棒的視頻講解——點我快速理解桶排序

2)代碼實現
我自己寫的使用二維數組實現的:(有點麻煩,因爲二維數組在插入數的時候,如果同時進行排序,要移動很多數)這裏設置桶數10。當然我們也可以使用函數映射生成桶的個數

 public static void bucketSort(int[] array)//data爲待排序數組
    {
        int n = array.length, max = array[0], min = array[0];
        int[][] bask = new int[10][n];//定義10個桶來放數據,桶的索引爲0到9,每個桶的數量最多存放n個數,
        //1、找最大值最小值制定桶的飯量
        for (int i = 1; i < array.length; i++) {
            if (min > array[i])
                min = array[i];
            if (max < array[i])
                max = array[i];
        }
        
        int delta = (max - min ) / 10+1;//注意這裏要加1,不然num會越界
       
        //定義一個數組來記錄對應的桶現在吃了多少個數
        int[] len_bask = new int[n];//默認值爲0
        

        //2、開始將數放進桶裏
        for (int i = 0; i < n; i++) {//遍歷原數組
            int num = (array[i] - min) / delta;//每一個數對應的桶號
            // 放進對應桶號的對應位置
            bask[num][len_bask[num]] = array[i];
            //桶內數的個數加1
            len_bask[num]++;
        }

        //3、各個桶分別排序,這裏使用計數排序
        for (int i = 0; i < 10; i++) {//遍歷桶,進行排序
            //因爲我們使用二維數組實現,當前每個桶存放的數值個數爲len_bask[i]
            if(len_bask[i]!=0)//對非空的桶排序
                countSort(bask[i], len_bask[i]);
        }
        //4、合併桶嘍
        int k=0;
        for(int i=0;i<10;i++){//遍歷桶,進行合併
            for(int j=0;j<len_bask[i];j++){
                array[k++]=bask[i][j];
            }
        }
    }

3)Demo
還是上次的計數排序的原數組
在這裏插入圖片描述
把打印信息去掉以後,只用了1ms,而計數排序(3.8的計數排序用了9ms
在這裏插入圖片描述
4)評價
桶排序最好情況下使用線性時間O(n),桶排序的時間複雜度,取決與對各個桶之間數據進行排序的時間複雜度,因爲其它部分的時間複雜度都爲O(n)。很顯然,桶劃分的越小,各個桶之間的數據越少,排序所用的時間也會越少。但相應的空間消耗就會增大。
平均時間複雜度爲線性的O(N+C),其中C=N*(logN-logM)。如果相對於同樣的N,桶數量M越大,其效率越高,最好的時間複雜度達到O(N)(當N=M時)即極限情況下每個桶只有一個數據時。桶排序的最好效率能夠達到O(N)。 當然桶排序的空間複雜度 爲O(N+M),如果輸入數據非常龐大,而桶的數量也非常多,則空間代價無疑是昂貴的。此外,桶排序是穩定的。
5)應用
1.海量數據

一年的全國高考考生人數爲500 萬,分數使用標準分,最低100 ,最高900 ,沒有小數,要求對這500 萬元素的數組進行排序。
分析:對500W數據排序,如果基於比較的先進排序,平均比較次數爲O(5000000log5000000)≈1.112億。但是我們發現,這些數據都有特殊的條件: 100=<score<=900。那麼我們就可以考慮桶排序這樣一個“投機取巧”的辦法、讓其在毫秒級別就完成500萬排序。
方法:創建801(900-100)個桶。將每個考生的分數丟進f(score)=score-100的桶中。這個過程從頭到尾遍歷一遍數據只需要500W次。然後根據桶號大小依次將桶中數值輸出,即可以得到一個有序的序列。而且可以很容易的得到100分有
**人,501分有***人。

2.大文件的數據排序

在一個文件中有10G個整數,亂序排列,要求找出中位數。內存限制爲2G。只寫出思路即可(內存限制爲2G意思是可以使用2G空間來運行程序,而不考慮本機上其他軟件內存佔用情況。) 關於中位數:數據排序後,位置在最中間的數值。即將數據分成兩部分,一部分大於該數值,一部分小於該數值。中位數的位置:當樣本數爲奇數時,中位數=(N+1)/2 ; 當樣本數爲偶數時,中位數爲N/2與1+N/2的均值(那麼10G個數的中位數,就第5G大的數與第5G+1大的數的均值了)。
分析:既然要找中位數,很簡單就是排序的想法。那麼基於字節的桶排序是一個可行的方法。
思想:將整型的每1byte作爲一個關鍵字,也就是說一個整形可以拆成4個keys,而且最高位的keys越大,整數越大。如果高位keys相同,則比較次高位的keys。整個比較過程類似於字符串的字典序。

參考資料:百度百科桶排序的應用

3.10 每次都看位數值的排序——基數排序(利用了桶,基數對應進制)

1)原理
以LSD實現爲例:(從最低位開始進桶的基數排序)
如下圖,最低位數相同的都進入相應下標的桶,如第一次,最低位是3的全進了3號桶最低位是9的全進了9號桶。因爲正整數的最低位只能是0~9,所以我們使用數組bask的長度爲10,相應的下標爲對應的位數的值,全部數組都進桶了完成一次對當前最低位的遍歷,然後將桶按照編號合併(跟桶排序的最後一步相同),我們開始進行下一個位的進桶,比如十位是1的全進1號桶,我們這個時候不去理個位了。重複直到遍歷完最大的數的最高位。最後得到的數組就排好序了。這就是10進制的LSD基數排序原理
在這裏插入圖片描述
實際例子:
假設原來有一串數值如下所示:
73, 22, 93, 43, 55, 14, 28, 65, 39, 81
1)首先根據個位數的數值,在走訪數值時將它們分配至編號0到9的桶子中:
0
1 81
2 22
3 73 93 43
4 14
5 55 65
6
7
8 28
9 39
2)接下來將這些桶子中的數值重新串接起來,成爲以下的數列:
81, 22, 73, 93, 43, 14, 55, 65, 28, 39
接着再進行一次分配,這次是根據十位數來分配
0
1 14
2 22 28
3 39
4 43
5 55
6 65
7 73
8 81
9 93
3)接下來將這些桶子中的數值重新串接起來,成爲以下的數列:
14, 22, 28, 39, 43, 55, 65, 73, 81, 93
這時候整個數列已經排序完畢;如果排序的對象有三位數以上,則持續進行以上的動作直至最高位數爲止。
LSD的基數排序適用於位數小的數列,如果位數多的話,使用MSD的效率會比較好。MSD的方式與LSD相反,是由高位數爲基底開始進行分配,但在分配之後並不馬上合併回一個數組中,而是在每個“桶子”中建立“子桶”,將每個桶子中的數值按照下一數位的值分配到“子桶”中。在進行完最低位數的分配後再合併回單一的數組中。

2)代碼實現
這裏演示全是正整數的排序:

    public static void radixSort(int[]array){
        int n=1;//表示從個位開始
        int max=array[0];//最大的數默認a【0】
        int [][]baske=new int[10][array.length];//因爲位數只能是0~9,所以我們建立了10個桶,每個桶最多容納array.length個數
        //我們又是用二維數組表示桶,則一樣需要一個數組來記錄該桶裏面存了多少個數
        int []len_bask = new int[array.length];
        for(int i=0;i<array.length;i++){
            len_bask[i]=0;//初始化爲0
            if(max<array[i])
                max = array[i];
        }
        int k=0;//用來控制array數組賦值下標
        //找到該數組裏最大是數有幾位
        while(max!=0){
            for (int value : array) {
                //取最低位的數值出來
                int lsd = (value / n) % 10;
                baske[lsd][len_bask[lsd]] = value;//位數相同的進同一個桶,
                len_bask[lsd]++; // 同時桶內數的個數加1
            }
            //現在將桶內的數取出來合併
            for(int i=0;i<10;i++){//外圍控制桶的下標
                if(len_bask[i]!=0)//桶不空
                {
                    for(int j=0;j<len_bask[i];j++){//控制桶內
                        array[k]=baske[i][j];
                        k++;
                }
                 len_bask[i]=0;//遍歷完該桶內的數,桶內的數就沒有了。
                }
            }
            //繼續進行下一個位
            k=0;
            n *=10;
            max/=10;//,同時我們讓最大值位數降1,假設剛纔是max=9,我們就只需要比較一個位數了。
        }
    }

3)Demo
還是上次的那個數組:
在這裏插入圖片描述
4)評價
標準的基數排序每次從小到大排序十進制下的一個位,並且只關心這一個位,也可以進行設計二進制的基數排序。
在這裏插入圖片描述
不知道細心的夥伴發現沒有,上面的算法我們只能進行正整數的排序。那假設有負整數和正整數呢?我們可以加桶的個數,比如0~9的桶裝正整數,10到19的桶裝負整數;那如果不是整數呢?感謝IEEE 754的標準,它制定的浮點標準還帶有一個特性:如果不考慮符號位(浮點數是按照類似於原碼使用一個bit表示符號位s,s = 1表示負數),那麼同符號浮點數的比較可以等價於無符號整數的比較,方法就是先做一次基數排序,然後再調整正負數部分順序即可。所以現在已經可以實現浮點數的基數排序了!!!點我查看基數排序之浮點數排序

最佳情況:T(n) = O(n * k) 最差情況:T(n) = O(n * k) 平均情況:T(n) = O(n * k)

基數排序有兩種方法:

  • MSD 從高位開始進行排序
  • LSD 從低位開始進行排序

5)基數排序 vs 計數排序 vs 桶排序

這三種排序算法都利用了桶的概念,但對桶的使用方法上有明顯差異:

  • 基數排序:根據鍵值的每位數字來分配桶(根據位數)
  • 計數排序:每個桶只存儲單一鍵值(下標映射實際的數)
  • 桶排序:每個桶存儲一定範圍的數值(根據一個函數映射得到一個桶容納數的範圍)

4、總結

借一張圖來總結吧:
在這裏插入圖片描述
以上是一邊學一邊總結的,如果有錯誤,請大佬們給小弟指出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章