數據結構基礎(五)排序

簡單選擇排序

對于長度為n的數組a

  1. 找出后n個數(下標0~n-1)中最小的數,與a[0]交換
  2. 找出后n-1個數(下標1~n-1)中最小的數,與a[1]交換
  3. 找出后n-2個數(下標2~n-1)中最小的數,與a[2]交換
  4. 依次類推



    即每次找到剩余數組中最小的元素放在前面,代碼如下:

    /**
     * 簡單選擇排序,O(n^2)
     * @param a
     */
    public static void selectSort(int[] a) {    
        for (int i = 0; i < a.length-1; i++) {
            int k=i; //剩余數組中最小元素下標
            for (int j = i+1; j < a.length; j++) {
                if(a[k]>a[j])
                    k=j;
            }
            //交換a[k]與a[i]
            swap(a, k, i);
        }
    }

冒泡排序

對于長度為n的數組a

  1. 對前n個數進行冒泡式交換(從0到n-1,如果相鄰的兩個數,a[i]>a[i+1]則交換),最后使a[n-1]為前n個數中最大數
  2. 對前n-1個數進行冒泡式交換,最后使a[n-2]為前n-1個數中最大數
  3. 對前n-2個數進行冒泡式交換,最后使a[n-3]為前n-2個數中最大數
  4. 依次類推


即每次找出最大的數放在后面,與選擇排序相反。代碼如下:

    /**
     * 冒泡排序,O(n^2)
     * @param a
     */
    public static void bubbleSort(int[] a) {
        for (int i = 0; i < a.length; i++) {
              //一次排序后使尾部的數為最大的
            for (int j = 0; j < a.length-i-1; j++) {
                if(a[j]>a[j+1]){
                    swap(a, j, j+1);
                }
            }
        }
    }

插入排序

對于無序序列,假設前i個數為有序的,將第i+1個數插入前i個數中,使其成為i+1個有序序列。
如圖,對于長度為n的數組a

  1. 前1個數為有序序列,將a[1]插入其中,形成2個數的有序序列
  2. 前2個數為有序序列,將a[2]插入其中,形成3個數的有序序列
  3. 依次類推,最終前n個數都變成有序序列,排序成功


代碼如下:

    /**
     * 插入排序,O(n^2)
     * @param a
     */
    public static void insertSort(int[] a) {
        int temp,j;
        //將a[i]插入前面的有序序列中
        for (int i = 1; i < a.length; i++) {
            temp=a[i];
            //從有序序列尾部遞減,如果比a[i]大則向后放置
            for (j=i-1; j >=0&&a[j]>temp; j--) {
                a[j+1]=a[j];
            }
            //將a[i]插入有序序列
            a[j+1]=temp;
        }
    }

快速排序

稍微復雜點的算法,運用了“分治”的思想
對于無序序列,隨便找出其中一個數作為“基準數”,然后將序列中小于它的數放在其左邊,大于它的數放在它右邊。再對左右區間重復此過程,直到區間只有一個數,排序成功。具體過程如下圖:


過程很好理解,但難點在于如何實現這個左右區間呢?
我們設置兩個變量i和j,分別指向序列的頭和尾。讓i遞增,j遞減。每次找到比基準數大的a[i]和比基準數小的a[j]后便進行交換。一直到i=j,再最后一次交換基準數與a[j]。此時便實現了一個基準數左邊比它小,右邊比它大的序列。
需要注意的是,每次必須是j先遞減。原因是這樣最后一次交換時,保證基準數交換的是比它小的a[j]。
沒有畫圖來講述整個過程,還是不懂可以看:坐在馬桶上看算法:快速排序

下面是代碼:

    /**
     * 快速排序,O(nlogn)
     * @param a
     * @param left 
     * @param right
     */
    public static void quickSort(int[] a, int left, int right) {
        if (left > right)
            return;

        int middle = a[left]; // 基準數
        int i = left;
        int j = right;
        while (i != j) {
            // 從最右遞減找出小于基準數的數
            while (a[j] >= middle && i < j) {
                j--;
            }
            // 從最左遞增找出大于基準數的數
            while (a[i] <= middle && i < j) {
                i++;
            }
            // 交換
            swap(a, i, j);
            
        }
        // 將基準數和最后小于它的數進行交換
        a[left] = a[i];
        a[i] = middle;
        // 以基準數劃分左右區間,再對左右區間進行快排
        quickSort2(a, left, i - 1);
        quickSort2(a, i + 1, right);
    }

由于a[left]我們已經用middle保存了,所以也可以利用a[left]來作為中間變量交換a[i]和a[j],優化后的代碼如下:

    /**
     * 快速排序,O(nlogn)
     * @param a
     * @param left
     * @param right
     */
    public static void quickSort(int[] a,int left,int right) {
        
        if(left>right)
            return ;
        
        int middle=a[left];  //基準數
        int i=left;
        int j=right;
        while (i != j) {
            //從最右遞減找出小于基準數的數
            while (a[j] >= middle && i < j) {
                j--;
            }
            a[i]=a[j];
            //從最左遞增找出大于基準數的數
            while (a[i] <= middle && i < j) {
                i++;
            }
            a[j]=a[i];
        }
        //將基準數和最后小于它的數進行交換
        a[i]=middle;
        //以基準數劃分左右區間,再對左右區間進行快排
        quickSort(a, left, i-1);
        quickSort(a, i+1, right);
    }   

堆排序

堆其實就是一個完全二叉樹,分為最大堆(父結點>=子結點)、最小堆(父結點<=子結點)。一般用數組來存儲,i結點的父結點下標就為(i – 1) / 2。它的左右子結點下標分別為2i + 1和2i + 2。

堆的兩個操作

要明白堆排序,就要先明白堆的添加和刪除原理。


  • 在堆數組尾部添加元素時,需要不停將該元素與其父結點進行對比交換,類似于元素在“上升”。也就是使新添加的元素插入一個有序的序列中,形成一個新的有序堆序列
  • 堆刪除元素時,總是先刪除根結點,然后將最后一個元素移到根結點,與子結點對比交換,類似于元素在“下沉”。最終形成新的有序堆序列。

好的,明白堆的添加刪除后,我們就可以解決以下問題:

  1. 現在給了一個無序數組,如何將其變成堆數組?
    我們可以對數組反向迭代,從末尾開始每個元素進行“下沉處理”,這樣便保證迭代完成后的數組是一個最小堆
  2. 如何利用堆進行排序
    由于最小堆的根結點永遠是所有元素中最小的。我們可以進行刪除操作。依次取出頂點的結點,這些取出的頂點最終就形成了一個遞增序列。

堆排序代碼

注意:下面代碼為了方便,每次取出堆的頂點放入數組末尾,最終形成了一個遞減序列。也可以將取出的元素新放入另外一個數組,形成遞增序列。

public class Heap {
    private int[] a; //堆數組
    private int n; //結點個數

    /**
     * 構造一個堆化數組
     * @param a 數組
     * @param n 數組中元素個數
     */
    public Heap(int[] a,int n) {
        this.a = a;
        this.n=n;
        for (int i = (n-2)/2; i >=0; i--) {
            minHeapDown(i,n);
        }
    }
    
    /**
     * 最小堆,對index下標結點進行“上浮”處理
     * @param index
     */
    public void minHeapFixUp(int index) {
        int temp=a[index];
        //index的父結點
        int fIndex=(index-1)/2;
        //未到頂點時繼續
        while(fIndex>=0&&index>0){
            //如果該結點大于父結點,直接退出循環
            if(a[fIndex]<temp)
                break;
            //如果該結點小于父節點,則交換結點
            a[index]=a[fIndex];
            index=fIndex;
            fIndex=(index-1)/2;
        }
        a[index]=temp;
    }
    
    /**
     * 最小堆,對index下標結點進行“下沉”處理
     * @param index
     * @param n   堆中元素個數
     */
    public void minHeapDown(int index,int n) {
        int temp=a[index];
        int sIndex=2*index+2; //子結點下標為2*index+1、2*index+2
        while(sIndex<=n-1){
            //找出左右子結點中最小的一個
            sIndex=(a[sIndex]>a[sIndex-1])?sIndex-1:sIndex;
            //如果子結點比該結點大,退出循環
            if(a[sIndex]>temp)
                break;
            //如果子節點比該結點小,進行交換
            a[index]=a[sIndex];
            index=sIndex;
            sIndex=2*index+1;
        }
        a[index]=temp;
    }
    
    /**
     * 新添加一個結點在堆的下標index
     * @param index
     * @param content
     */
    public void add(int index,int content) {
        a[index]=content;
        minHeapFixUp(index);
    }
    
    /**
     * 堆的刪除操作,即刪除頭結點
     */
    public void remove() {
        Sort.swap(a, 0, n-1);
        minHeapDown(0,n);
    }
    
    /**
     * 堆排序,,O(nlogn)
     */
    public void minHeapSort(){
        for (int i = n-1; i >=1; i--) {
            Sort.swap(a, 0, i);
            minHeapDown(0,i);
        }
    }
    
    public static void main(String[] args) {
        int[] a={8,9,6,4,7,5,3,4,1};
        Heap heap=new Heap(a, a.length);
        heap.minHeapSort();
        System.out.println(Arrays.toString(a));
    }
}/**output:
[9, 8, 7, 6, 5, 4, 4, 3, 1]
*/

幾種排序算法時間復雜度的比較


排序算法嚴格來說沒有最快,各有各的適用環境。
如果非要找出一個最快,筆者通過上網查資料,發現大多都推薦的是桶排序。

參考
排序(本文圖片出自此網站)
白話經典算法系列之七 堆與堆排序

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 概述 排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    蟻前閱讀 5,220評論 0 52
  • 概述:排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部...
    每天刷兩次牙閱讀 3,743評論 0 15
  • 1.插入排序—直接插入排序(Straight Insertion Sort) 基本思想: 將一個記錄插入到已排序好...
    依依玖玥閱讀 1,282評論 0 2
  • 第一章 緒論 什么是數據結構? 數據結構的定義:數據結構是相互之間存在一種或多種特定關系的數據元素的集合。 第二章...
    SeanCheney閱讀 5,821評論 0 19
  • 概述排序有內部排序和外部排序,內部排序是數據記錄在內存中進行排序,而外部排序是因排序的數據很大,一次不能容納全部的...
    Luc_閱讀 2,299評論 0 35