一文搞定十大經典排序算法,附代碼詳解

排序算法日常使用的最基本的算法之一,大體可以分爲外部排序內部排序

  • 內部排序指的是待排序列完全存放在內存中所進行的排序過程,只適合較短的元素序列,不適合大數據排序。
  • 外部排序指的是大文件排序,待排序列存儲在外存儲器上,體積過大無法一次裝入內存,需要在內存和外存儲器之間進行多次數據交換。

一、內部排序的比較

排序算法 平均時間複雜度 最好情況 最壞情況 空間複雜度 穩定性
冒泡排序 O(n^2) O(n) O(n^2) O(1) 穩定
選擇排序 O(n^2) O(n^2) O(n^2) O(1) 不穩定
插入排序 O(n^2) O(n) O(n^2) O(1) 穩定
希爾排序 O(nlogn) O(n) O(n ^ s) 1<s<2 O(1) 不穩定
歸併排序 O(nlogn) O(nlogn) O(nlogn) O(n) 穩定
快速排序 O(nlogn) O(nlogn) O(n ^ 2) O(nlogn) 不穩定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不穩定
計數排序 O(n + k) O(n + k) O(n + k) O(k) 穩定
桶排序 O(n + k) O(n + k) O(n^2) O(n + k) 穩定
基數排序 O(n * k) O(n * k) O(n * k) O(n + k) 穩定

基本有序情況下,高效地算法是:直接插入排序,冒泡排序
無序情況下:高效的算法是:堆排序,快速排序,歸併排序。

關於時間複雜度:

  1. 平方階排序(On^2):插入、選擇和冒泡排序
  2. 線性對數階(O(nlog2n))排序:快速排序、堆排序、歸併排序
  3. O(n1+§)) 排序,§ 是介於 0 和 1 之間的常數。 希爾排序
  4. 線性階 (O(n)) 排序 基數排序,此外還有桶、箱排序。

關於穩定性:

  1. 穩定的排序算法:冒泡排序、插入排序、歸併排序和基數排序
  2. 不穩定的排序算法:選擇排序、快速排序、希爾排序、堆排序

1.1 冒泡排序

1.1.1 算法步驟:

依次比較相鄰的兩個數,把小數放在前面,大數放在後面。首先第一趟,比較第一個和第二個數,小數在前,大數在後,然後比較第二和第三兩個數,小數在前,大數在後,如此依次比較,直至到數組末尾第一趟結束。針對除了最後一個元素的所有元素重複以上步驟,直至沒有任何一對數字需要比較。
規律:N個數字排序,總共進行N-1趟排序,每趟將一個最大值移至末尾。

示例:以3,1,0,2,4,9,8,6,7,5爲例:

  • 第1趟:[1, 0, 2, 3, 4, 8, 6, 7, 5, 9]
  • 第2趟:[0, 1, 2, 3, 4, 6, 7, 5, 8, 9]
  • 第3趟:[0, 1, 2, 3, 4, 6, 5, 7, 8, 9]
  • 第4趟:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 第5趟:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

結果 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1.1.2 參考代碼:

/**
 * 冒泡排序
 * */
public void bubbleSort(int[] nums) {
    int n = nums.length;
    for (int i = 0;i < n-1; i ++) {
        boolean flag = true;
        for (int j = 0;j < n - i-1; j++) {
            if (nums[j] > nums[j + 1]) {
                int tmp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = tmp;
                flag = false;
            }
        }
        if (flag) break;
    }
}

1.2 選擇排序

1.2.1 算法步驟:

首先在待排序列中選擇最小(大)元素,存放到待排序列的起始(結束)位置。然後從剩餘待排序列中繼續選擇最小(大元素),存放到已排序列的末尾。重複以上步驟,直至整個序列排序完成。

示例:以3,1,0,2,4,9,8,6,7,5爲例:

  • 第1趟:[0, 1, 3, 2, 4, 9, 8, 6, 7, 5]
  • 第2趟:[0, 1, 3, 2, 4, 9, 8, 6, 7, 5]
  • 第3趟:[0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • 第4趟:[0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • 第5趟:[0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • 第6趟:[0, 1, 2, 3, 4, 5, 8, 6, 7, 9]
  • 第7趟:[0, 1, 2, 3, 4, 5, 6, 8, 7, 9]
  • 第8趟:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 第9趟:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  • 第10趟:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

結果 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1.2.2 參考代碼:

/**
 * 簡單選擇排序
 * */
public void selectionSort(int[] nums) {
    for (int i = 0;i < nums.length; i++) {
        int small_index = i;
        for (int j = i + 1; j < nums.length; j++) {
            if (nums[j] < nums[small_index]) {
                small_index = j;
            }
        }
        if (i < small_index) {
            int tmp = nums[i];
            nums[i] = nums[small_index];
            nums[small_index] = tmp;
        }
        System.out.println("* 第" + (i+1) + "趟:" + Arrays.toString(nums));
    }
}

1.3 插入排序

1.3.1 算法步驟:

把初始n個待排序的元素,看成有序表+無序表。初始狀態,有序表只有待排序序列的第一個元素,無序表中有n-1個元素。每次排序,從無序表中取出第i個原則,插入都有序表中對應合適的位置。重複這個步驟,直至所有元素都插入有序表,代表整個序列排序完成。

示例:以3,1,0,2,4,9,8,6,7,5爲例:

  • 第1趟:[1, 3, 0, 2, 4, 9, 8, 6, 7, 5]
  • 第2趟:[0, 1, 3, 2, 4, 9, 8, 6, 7, 5]
  • 第3趟:[0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • 第4趟:[0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • 第5趟:[0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • 第6趟:[0, 1, 2, 3, 4, 8, 9, 6, 7, 5]
  • 第7趟:[0, 1, 2, 3, 4, 6, 8, 9, 7, 5]
  • 第8趟:[0, 1, 2, 3, 4, 6, 7, 8, 9, 5]
  • 第9趟:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

結果:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1.3.2 參考代碼:

/**
 * 直接插入排序
 * */
public void insertSort(int[] nums) {
    if(nums==null||nums.length==0)
        return;
    for (int i = 1;i < nums.length; i++) {
        int tmp = nums[i] , j = i;
        if (nums[j - 1] > tmp) {
            while (j >= 1 && nums[j - 1] > tmp) {
                nums[j] = nums[j - 1];
                j--;
            }
        }
        nums[j] = tmp;
        System.out.println("* 第" + (i) + "趟:" + Arrays.toString(nums));
    }
}

1.4 希爾排序

1.4.1 算法步驟:

首先選擇一個增量序列 t1 , t2, … , tk , 其中ti > tj , tk = 1; 然後按照增量分別將待排數組分成多個子序列,使每個子序列元素個數相對較少,然後對各個子序列分別進行直接插入排序,代整個排序序列基本有序後,對所有元組再進行一次直接插入排序。

示例:以3,1,0,2,4,9,8,6,7,5爲例:

  • 增量爲5的時:[3, 1, 0, 2, 4, 9, 8, 6, 7, 5]
  • 增量爲2的時:[0, 1, 3, 2, 4, 5, 7, 6, 8, 9]
  • 增量爲1的時:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1.4.2 參考代碼:

/**
 * 希爾排序
 * */
public static void hillSort(int[] nums){
    if(nums == null || nums.length <= 1){
        return;
    }
    int i,j;
    int increment;
    int temp;
    for(increment=nums.length/2;increment>0;increment/=2) {
        for(i=increment;i<nums.length;i++) {
            temp=nums[i];
            for(j=i-increment;j>=0;j-=increment) {
                if(temp<nums[j]) {
                    nums[j+increment]=nums[j];
                }else
                    break;
            }
            nums[j+increment]=temp;
        }
    }
}

1.5 歸併排序

1.5.1 算法步驟

歸併排序是利用歸併的思想實現的排序方法,該方法採用的經典的分治策略(把問題分解成若干個小問題然後遞歸求解)。首先我們需要申請空間,使其大小爲兩個已排序序列之和,該空間用來存放合併後的序列。然後我們設定兩個指針,最初位置分別是兩個已經排序序列的起始位置。比較兩個比較兩個指針所指向的元素,選擇相對小的元素放入到合併空間,並移動指針到下一位置;重複這個步驟 直到某一指針達到序列尾;然後將將另一序列剩下的所有元素直接複製到合併序列尾。

示例:以3,1,0,2,4,9,8,6,7,5爲例:

  • [3, 1, 0, 2, 4, 9, 8, 6, 7, 5]
  • [3, 1, 0, 2, 4, 9, 8, 6, 7, 5]
  • [3, 1, 0, 2, 4, 9, 8, 6, 7, 5]
  • [3, 1, 0, 2, 4, 9, 8, 6, 7, 5]
  • [0, 1, 3, 2, 4, 9, 8, 6, 7, 5]
  • [0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • [0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • [0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • [0, 1, 2, 3, 4, 6, 8, 9, 7, 5]

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1.5.2 參考代碼

/**
 * 歸併排序
 * */
public static void mergeSort(int []arr){
    int []temp = new int[arr.length];//在排序前,先建好一個長度等於原數組長度的臨時數組,避免遞歸中頻繁開闢空間
    sort(arr,0,arr.length-1,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[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;//因爲需要拷貝所以要把臨時數組的指針置0(即指向第一個元素)
    //將temp中的元素全部拷貝到原數組中
    while(left <= right){
        arr[left++] = temp[t++];
    }
}
private static void sort(int[] arr,int left,int right,int []temp){
    if(left<right){
        int mid = (left+right)/2;
        sort(arr,left,mid,temp);//左邊歸併排序,使得左子序列有序
        sort(arr,mid+1,right,temp);//右邊歸併排序,使得右子序列有序
        merge(arr,left,mid,right,temp);//將兩個有序子數組合並操作
    }
}

1.6 快速排序

1.6.1 算法步驟

每次挑選出一個元素爲基準,設兩個指針,一個left一個right,分別指向序列的頭和尾。用兩個指針掃描數組,把所有比基準大的,都擺放在基準的後面,所有比基準小的,都擺放在基準的後面。這一次掃描之後,基準就位於它的最終位置。然後分別在基準左側和基準右側再次執行這個操作。知道整個序列排列完成。

示例:以3,1,0,2,4,9,8,6,7,5爲例:

  • [3, 1, 0, 2, 4, 9, 8, 6, 7, 5]
  • [2, 1, 0, 3, 4, 9, 8, 6, 7, 5]
  • [0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • [0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • [0, 1, 2, 3, 4, 9, 8, 6, 7, 5]
  • [0, 1, 2, 3, 4, 5, 8, 6, 7, 9]
  • [0, 1, 2, 3, 4, 5, 8, 6, 7, 9]
  • [0, 1, 2, 3, 4, 5, 7, 6, 8, 9]

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1.6.2 參考代碼

/**
 * 快速排序
 * */
private void quickSort(int nums[], int left , int right) {
    if (left < right) {
        int i = left , j = right,temp = nums[left];
        System.out.println(" * "+Arrays.toString(nums));
        while (i < j) {
            while (nums[j] > temp && i < j) j--;
            if (i < j){
                nums[i] = nums[j]; i++;
            }
            while (nums[i] < temp && i < j) i++;
            if (i < j) {
                nums[j] = nums[i]; j--;
            }
        }
        nums[i] = temp;
        quickSort(nums , left , i -1);
        quickSort(nums , i+1 , right);
    }
}

1.7 堆排序(重要!!!)

1.7.1 算法步驟

首先,將待排序列構造成一個大頂堆。我們用數組+限定規則構造大頂堆,下標爲i的數組元素是下標爲2*i+12*i+2元素的父節點。且i的數組元素的父節點下標爲(i-1)/2

此時堆頂元素就是序列的最大值。然後我們把最後一個位置的數和堆頂位置的數做交換,把最大值放到數組最後的位置。此時讓堆大小減1,最大值就被固定在了末尾。
把除最後一個元素的序列重新調整成大頂堆,然後依次執行上面的步驟,輸出倒數第二大的數。依次類推,把所有元素都輸出之後,整個序列就變成了有序序列。

示例:以3,1,0,2,4,9,8,6,7,5爲例:

  • [2, 7, 8, 6, 5, 0, 4, 1, 3, 9]
  • [3, 7, 4, 6, 5, 0, 2, 1, 8, 9]
  • [1, 6, 4, 3, 5, 0, 2, 7, 8, 9]
  • [2, 5, 4, 3, 1, 0, 6, 7, 8, 9]
  • [0, 3, 4, 2, 1, 5, 6, 7, 8, 9]
  • [1, 3, 0, 2, 4, 5, 6, 7, 8, 9]
  • [1, 2, 0, 3, 4, 5, 6, 7, 8, 9]
  • [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
  • [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

1.7.2 參考代碼

/**
 * 堆排序
 * */
public void heapSort(int [] arr) {
    if(arr==null||arr.length<2) {
        return;
    }
    for(int i=0;i<arr.length;i++) {
        heapInsert(arr,i);//依次將數組中i位置上的數加進來,讓它0~i之間的數形成大頂堆
    }
    int heapSize=arr.length;//堆大小heapSize一開始等於數組的全部
    swap(arr,0,--heapSize);//最後一個位置上的數與第一個位置上的數(堆頂元素)交換,堆大小減1,即最後一個位置上的數不動了
    while(heapSize>0) {
        heapify(arr,0,heapSize);//從0位置開始,將當前形成的堆繼續調整爲一個大頂堆
        swap(arr,0,--heapSize);//最後一個位置上的數與第一個位置上的數(堆頂元素)交換,堆大小減1,即最後一個位置上的數不動了
    }
}

//建立大頂堆的過程
public void heapInsert(int[] arr,int index) {
    while(arr[index] > arr[(index-1)/2]) {//當前index位置上的數若比其父結點上的數大,則交換他倆的位置
        swap(arr,index,(index-1)/2);
        index=(index-1)/2;//然後index來到了它的父節點位置,繼續上面的while
    }
}

//若有一個節點的值變小了,則需要往下沉(與其左右孩子中較大的數進行交換位置的)的操作
public void heapify(int[] arr,int index,int heapSize) {
    int left=index*2+1;//左孩子下標
    while(left<heapSize) {//未越界,即該節點並非葉子節點,存在左孩子
        //該節點有右孩子,讓largest作爲左右孩子較大值的下標
        int largest=(left+1 < heapSize) && arr[left+1] > arr[left] ? left+1 : left;
        //讓largest成爲該節點與該節點左右孩子中較大值的下標
        largest=arr[largest] > arr[index] ? largest : index;
        if(largest==index) {
            break;
        }
        swap(arr,largest,index);//largest!=index
        index=largest;//該節點下標變成了較大孩子的下標
        left=index*2+1;//繼續往下走,重複上面的while
    }
}

public void swap(int[] arr,int i,int j) {
    int temp=arr[i];
    arr[i]=arr[j];
    arr[j]=temp;
}


代碼已上傳至本人Github倉庫 , 歡迎大家star,follow

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