一文搞定十大經(jīng)典排序算法(Java實(shí)現(xiàn))

本文總結(jié)十大經(jīng)典排序算法及變形淀弹,并提供Java實(shí)現(xiàn)。
參考文章:
十大經(jīng)典排序算法總結(jié)(Java語言實(shí)現(xiàn))
快速排序算法—左右指針法庆械,挖坑法薇溃,前后指針法,遞歸和非遞歸
快速排序及優(yōu)化(三路劃分等)

一缭乘、排序算法概述

1沐序、定義

將雜亂無章的數(shù)據(jù)元素,通過一定的方法按關(guān)鍵字順序排列的過程叫做排序堕绩。

2策幼、分類

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

  • 非線性時(shí)間比較類排序:通過比較來決定元素間的相對次序,由于其時(shí)間復(fù)雜度不能突破O(nlogn)奴紧,因此稱為非線性時(shí)間比較類排序垄惧。

  • 線性時(shí)間非比較類排序:不通過比較來決定元素間的相對次序,它可以突破基于比較排序的時(shí)間下界绰寞,以線性時(shí)間運(yùn)行到逊,因此稱為線性時(shí)間非比較類排序。

3滤钱、比較

4觉壶、相關(guān)概念

  • 穩(wěn)定:如果a原本在b前面且a=b,排序之后a仍然在b的前面件缸。
  • 不穩(wěn)定:如果a原本在b的前面且a=b铜靶,排序之后 a 可能會出現(xiàn)在 b 的后面。
  • 時(shí)間復(fù)雜度:對排序數(shù)據(jù)的總的操作次數(shù)他炊。反映當(dāng)n變化時(shí)争剿,操作次數(shù)呈現(xiàn)什么規(guī)律。
  • 空間復(fù)雜度:是指算法在計(jì)算機(jī)內(nèi)執(zhí)行時(shí)所需存儲空間的度量痊末,它也是數(shù)據(jù)規(guī)模n的函數(shù)蚕苇。
  • 內(nèi)部排序:所有排序操作都在內(nèi)存中完成。本文主要介紹的是內(nèi)部排序凿叠。
  • 外部排序:待排序記錄的數(shù)量很大涩笤,以致于內(nèi)存不能一次容納全部記錄嚼吞,所以在排序過程中需要對外存進(jìn)行訪問的排序過程。

二蹬碧、各算法原理及實(shí)現(xiàn)

下面我們來逐一分析十大經(jīng)典排序算法舱禽,主要圍繞下列問題展開:
1、算法的基本思想是什么恩沽?
2誊稚、算法的代碼實(shí)現(xiàn)?
3罗心、算法的時(shí)間復(fù)雜度是多少里伯?(平均、最好协屡、最壞)什么情況下最好?什么情況下最壞全谤?
4肤晓、算法的空間復(fù)雜度是多少?
5认然、算法的穩(wěn)定性如何补憾?

1、冒泡排序(Bubble Sort)

① 基本思想
冒泡排序是一種簡單的排序算法卷员。它重復(fù)地走訪過要排序的數(shù)列盈匾,一次比較兩個(gè)元素,如果它們的順序錯誤就把它們交換過來毕骡。走訪數(shù)列的工作是重復(fù)地進(jìn)行直到?jīng)]有再需要交換削饵,也就是說該數(shù)列已經(jīng)排序完成。這個(gè)算法的名字由來是因?yàn)槊刻吮容^將當(dāng)前數(shù)列未排序部分的最大的元素“沉”到數(shù)列末端未巫,而小的元素會經(jīng)由交換慢慢“浮”到數(shù)列的頂端窿撬。

② 算法描述
1)比較相鄰的元素。如果前一個(gè)比后一個(gè)大叙凡,就交換它們兩個(gè)劈伴;
2)對每一對相鄰元素作同樣的工作,從開始第一對到結(jié)尾的最后一對握爷,這樣在最后的元素應(yīng)該會是最大的數(shù)跛璧;
3)針對所有的元素重復(fù)以上的步驟,除了最后一個(gè)新啼;
4)重復(fù)步驟1~3追城,直到排序完成。為了優(yōu)化算法燥撞,可以設(shè)立一個(gè)布爾標(biāo)識漓柑,每趟排序開始前設(shè)為false,如果該趟排序發(fā)生了交換就置為true,如果一趟排序結(jié)束標(biāo)識仍為false表示該趟排序沒有發(fā)生交換辆布,即數(shù)組已經(jīng)有序瞬矩,可以提前結(jié)束排序。

③ 動圖演示

④ 代碼實(shí)現(xiàn)

 public static int[] bubbleSort(int[] array) {
      if (array.length == 0)
          return array;
      for (int i = 0; i < array.length; i++){  //外層循環(huán)一次為一趟排序
          /*設(shè)置標(biāo)識锋玲,判斷這趟排序是否發(fā)生了交換景用。
         如果未發(fā)生交換,則說明數(shù)組已經(jīng)有序惭蹂,不必再排序了*/
          boolean isSwap = false;
          for (int j = 0; j < array.length - 1 - i; j++) //內(nèi)層循環(huán)一次為一次相鄰比較
              if (array[j + 1] < array[j]) {
                  int temp = array[j + 1];
                  array[j + 1] = array[j];
                  array[j] = temp;
                  isSwap = true;
              }
          if(!isSwap)
              break;
      }
      return array;
  }

⑤ 時(shí)間復(fù)雜度
冒泡排序平均時(shí)間復(fù)雜度為O(n2)伞插,最好時(shí)間復(fù)雜度為O(n),最壞時(shí)間復(fù)雜度為O(n2)盾碗。
最好情況:如果待排序元素本來是正序的媚污,那么一趟冒泡排序就可以完成排序工作,比較和移動元素的次數(shù)分別是 (n - 1) 和 0廷雅,因此最好情況的時(shí)間復(fù)雜度為O(n)耗美。
最壞情況:如果待排序元素本來是逆序的,需要進(jìn)行 (n - 1) 趟排序航缀,所需比較和移動次數(shù)分別為 n * (n - 1) / 2和 3 * n * (n-1) / 2商架。因此最壞情況下的時(shí)間復(fù)雜度為O(n2)。

⑥ 空間復(fù)雜度
冒泡排序使用了常數(shù)空間芥玉,空間復(fù)雜度為O(1)

⑦ 穩(wěn)定性
當(dāng) array[j] == array[j+1] 的時(shí)候蛇摸,我們不交換 array[i] 和 array[j],所以冒泡排序是穩(wěn)定的灿巧。

⑧ 算法拓展
雞尾酒排序赶袄,又稱定向冒泡排序、攪拌排序等抠藕,是對冒泡排序的改進(jìn)弃鸦。在把最大的數(shù)往后面冒泡的同時(shí),把最小的數(shù)也往前面冒泡幢痘,同時(shí)收縮無序區(qū)的左右邊界唬格,有序區(qū)在序列左右逐漸累積。

動圖如下:



代碼如下:

public static void cocktailSort(int[] array) {
        int left = 0,right = array.length-1;
        while(left < right) {
            for(int i = left; i < right; i++) 
              if(array[i] > array[i+1]) 
                    swap(array,i,i + 1);
            right--;
            for(int i = right; i > left; i--) 
              if(array[i] < array[i-1]) 
                    swap(array,i,i-1);
            left++;
        }
}

雞尾酒排序是穩(wěn)定的颜说。它的平均時(shí)間復(fù)雜度為O(n2)购岗,最好情況是待排序列原先就是正序的,時(shí)間復(fù)雜度為O(n)门粪,最壞情況是待排序列原先是逆序的喊积,時(shí)間復(fù)雜度為O(n2)⌒瑁空間復(fù)雜度為O(1)乾吻。

2髓梅、簡單選擇排序(Selection Sort)

① 基本思想
簡單選擇排序(Selection-sort)是一種簡單直觀的排序算法。它的工作原理:首先在未排序序列中找到最幸锴(大)元素枯饿,存放到排序序列的起始位置,然后诡必,再從剩余未排序元素中繼續(xù)尋找最猩莘健(大)元素,然后放到已排序序列的末尾爸舒。以此類推蟋字,直到所有元素均排序完畢。

② 算法描述
n個(gè)記錄的簡單選擇排序可經(jīng)過(n-1)趟簡單選擇排序得到有序結(jié)果扭勉。具體算法描述如下:
1)初始狀態(tài):無序區(qū)為R[1..n]鹊奖,有序區(qū)為空;
2)第i趟排序(i=1,2,3…n-1)開始時(shí)涂炎,當(dāng)前有序區(qū)和無序區(qū)分別為R[1..i-1]和R[i..n]忠聚。該趟排序從當(dāng)前無序區(qū)中選出關(guān)鍵字最小的記錄 R[k],將它與無序區(qū)的第1個(gè)記錄R交換璧尸,使R[1..i]和R[i+1..n]分別變?yōu)橛涗泜€(gè)數(shù)增加1個(gè)的新有序區(qū)和記錄個(gè)數(shù)減少1個(gè)的新無序區(qū)咒林;
3)(n-1)趟結(jié)束熬拒,數(shù)組有序化了爷光。

③ 動圖演示

④ 代碼實(shí)現(xiàn)

public static int[] selectionSort(int[] array) {
        if (array.length == 0)
             return array;
        for (int i = 0; i < array.length; i++) {
            int minIndex = i;
            for (int j = i; j < array.length; j++) {
                if (array[j] < array[minIndex]) //找到最小的數(shù)
                    minIndex = j; //將最小數(shù)的索引保存
            }
            int temp = array[minIndex]; //將最小數(shù)和無序區(qū)的第一個(gè)數(shù)交換
            array[minIndex] = array[i];
            array[i] = temp;
        }
        return array;
    }

⑤ 時(shí)間復(fù)雜度
簡單選擇排序平均時(shí)間復(fù)雜度為O(n2),最好時(shí)間復(fù)雜度為O(n2)澎粟,最壞時(shí)間復(fù)雜度為O(n2)蛀序。
最好情況:如果待排序元素本來是正序的若未,則移動元素次數(shù)為 0蒋譬,但需要進(jìn)行 n * (n - 1) / 2 次比較。
最壞情況:如果待排序元素中第一個(gè)元素最大刻撒,其余元素從小到大排列啸盏,則仍然需要進(jìn)行 n * (n - 1) / 2 次比較重贺,且每趟排序都需要移動 3 次元素,即移動元素的次數(shù)為3 * (n - 1)次回懦。
需要注意的是气笙,簡單選擇排序過程中需要進(jìn)行的比較次數(shù)與初始狀態(tài)下待排序元素的排列情況無關(guān)。

⑥ 空間復(fù)雜度
簡單選擇排序使用了常數(shù)空間怯晕,空間復(fù)雜度為O(1)

⑦ 穩(wěn)定性
簡單選擇排序不穩(wěn)定潜圃,比如序列 2、4舟茶、2谭期、1堵第,我們知道第一趟排序第 1 個(gè)元素 2 會和 1 交換,那么原序列中 2 個(gè) 2 的相對前后順序就被破壞了隧出,所以簡單選擇排序不是一個(gè)穩(wěn)定的排序算法踏志。

3、直接插入排序(Insertion Sort)

① 基本思想
直接插入排序(Insertion-Sort)的算法描述是一種簡單直觀的排序算法鸳劳。它的工作原理是通過構(gòu)建有序序列狰贯,對于未排序數(shù)據(jù),在已排序序列中從后向前掃描赏廓,找到相應(yīng)位置并插入涵紊。

② 算法描述
一般來說,直接插入排序都采用in-place(原地算法)在數(shù)組上實(shí)現(xiàn)幔摸。具體算法描述如下:
1)從第一個(gè)元素開始摸柄,該元素可以認(rèn)為已經(jīng)被排序;
2)取出下一個(gè)元素既忆,在已經(jīng)排序的元素序列中從后向前掃描驱负;
3)如果該元素(已排序)大于新元素,將該元素移到下一位置患雇;
4)重復(fù)步驟3跃脊,直到找到已排序的元素小于或者等于新元素的位置;
5)將新元素插入到該位置后苛吱;
6)重復(fù)步驟2~5酪术。

③ 動圖演示

④ 代碼實(shí)現(xiàn)

public static int[] insertionSort(int[] array) {
        if (array.length == 0)
            return array;
        int current;
        for (int i = 1; i < array.length; i++) {
            current = array[i];
            int preIndex = i - 1;
            while (preIndex >= 0 && current < array[preIndex]) {
                array[preIndex + 1] = array[preIndex];
                preIndex--;
            }
            array[preIndex + 1] = current;
        }
        return array;
    }

⑤ 時(shí)間復(fù)雜度
直接插入排序平均時(shí)間復(fù)雜度為O(n2),最好時(shí)間復(fù)雜度為O(n)翠储,最壞時(shí)間復(fù)雜度為O(n2)绘雁。
最好情況:如果待排序元素本來是正序的,比較和移動元素的次數(shù)分別是 (n - 1) 和 0援所,因此最好情況的時(shí)間復(fù)雜度為O(n)庐舟。
最壞情況:如果待排序元素本來是逆序的,需要進(jìn)行 (n - 1) 趟排序住拭,所需比較和移動次數(shù)分別為 n * (n - 1) / 2和 n * (n - 1) / 2挪略。因此最壞情況下的時(shí)間復(fù)雜度為O(n2)。

⑥ 空間復(fù)雜度
直接插入排序使用了常數(shù)空間滔岳,空間復(fù)雜度為O(1)

⑦ 穩(wěn)定性
直接插入排序是穩(wěn)定的杠娱。

⑧ 算法拓展
在直接插入排序中,待插入的元素總是在有序區(qū)線性查找合適的插入位置澈蟆,沒有利用有序的優(yōu)勢墨辛,考慮使用二分查找搜索插入位置進(jìn)行優(yōu)化,即二分插入排序趴俘。

代碼實(shí)現(xiàn)如下:

 public static int[] BinaryInsertionSort(int[] array) {
        if (array.length == 0)
            return array;
        for(int i = 1;i < array.length;i++) {
            int left = 0;
            int right = i - 1;  // left 和 right 分別為有序區(qū)的左右邊界 
            int current = array[i];
            while (left <= right) {
           //搜索有序區(qū)中第一個(gè)大于 current 的位置睹簇,即為 current 要插入的位置
                int mid = left + ((right - left) >> 1);
                if(array[mid] > current){
                    right = mid - 1;
                }else{
                    left = mid + 1;
                }
            }
            for(int j = i - 1;j >= left;j--) {
                array[j + 1] = array[j];
            }
            array[left] = current; // left 為第一個(gè)大于 current 的位置奏赘,插入 current
        }
        return array;
 }

二分插入排序是穩(wěn)定的。它的平均時(shí)間復(fù)雜度是O(n2)太惠,最好時(shí)間復(fù)雜度為O(nlogn)磨淌,最壞時(shí)間復(fù)雜度為O(n2)。

4凿渊、希爾排序(Shell Sort)

① 基本思想
1959年Shell發(fā)明梁只,第一個(gè)突破O(n2)的排序算法,是直接插入排序的改進(jìn)版埃脏。它與直接插入排序的不同之處在于搪锣,它會優(yōu)先比較距離較遠(yuǎn)的元素。希爾排序又叫縮小增量排序彩掐。

② 算法描述
先將整個(gè)待排元素序列分割成 gap 個(gè)增量為 gap 的子序列(每個(gè)子序列由位置相差為 gap 的元素組成构舟,整個(gè)序列正好分割成 gap 個(gè)子序列,每個(gè)序列中有 n / gap 個(gè)元素)分別進(jìn)行直接插入排序堵幽,然后縮減增量為之前的一半再進(jìn)行排序狗超,待 gap == 1時(shí),希爾排序就變成了直接插入排序朴下。因?yàn)榇藭r(shí)序列已經(jīng)基本有序努咐,直接插入排序在元素基本有序的情況下(接近最好情況),效率是很高的殴胧。gap初始值一般取 len / 2渗稍。

③ 動圖演示

④ 代碼實(shí)現(xiàn)

 public static int[] ShellSort(int[] array) {
        int len = array.length;
        if(len == 0)
            return array;
        int current, gap = len / 2;
        while (gap > 0) {
            for (int i = gap; i < len; i++) {
                current = array[i];
                int preIndex = i - gap;
                while (preIndex >= 0 && array[preIndex] > current) {
                    array[preIndex + gap] = array[preIndex];
                    preIndex -= gap;
                }
                array[preIndex + gap] = current;
            }
            gap /= 2;
        }
        return array;
    }

⑤ 時(shí)間復(fù)雜度
希爾排序平均時(shí)間復(fù)雜度為O(nlogn),最好時(shí)間復(fù)雜度為O(nlog2n)溃肪,最壞時(shí)間復(fù)雜度為O(nlog2n)免胃。希爾排序的時(shí)間復(fù)雜度與增量序列的選取有關(guān)音五。

⑥ 空間復(fù)雜度
希爾排序使用了常數(shù)空間惫撰,空間復(fù)雜度為O(1)

⑦ 穩(wěn)定性
由于相同的元素可能在各自的序列中插入排序,最后其穩(wěn)定性就會被打亂躺涝,比如序列 2厨钻、4、1坚嗜、2夯膀,所以希爾排序是不穩(wěn)定的。

5苍蔬、歸并排序(Merge Sort)

① 基本思想
歸并排序是建立在歸并操作上的一種有效的排序算法诱建。該算法是采用分治法(Divide and Conquer)的一個(gè)非常典型的應(yīng)用。將已有序的子序列合并碟绑,得到完全有序的序列俺猿;即先使每個(gè)子序列有序茎匠,再使子序列段間有序。若將兩個(gè)有序表合并成一個(gè)有序表押袍,稱為2-路歸并诵冒。

② 算法描述
1)把長度為 n 的輸入序列分成兩個(gè)長度為 n / 2 的子序列;
2)對這兩個(gè)子序列分別采用歸并排序谊惭;
3)將兩個(gè)排序好的子序列合并成一個(gè)最終的排序序列汽馋。

③ 動圖演示

④ 代碼實(shí)現(xiàn)

    /**
     * 歸并排序
     *
     * @param array
     * @return
     */
    public static int[] MergeSort(int[] array) {
        if (array.length < 2) return array;
        int mid = array.length / 2;
        int[] left = Arrays.copyOfRange(array, 0, mid);
        int[] right = Arrays.copyOfRange(array, mid, array.length);
        return merge(MergeSort(left), MergeSort(right));
    }
    /**
     * 歸并排序——將兩段有序數(shù)組結(jié)合成一個(gè)有序數(shù)組
     *
     * @param left
     * @param right
     * @return
     */
    public static int[] merge(int[] left, int[] right) {
       int[] result = new int[left.length + right.length];
       int i = 0,j = 0,k = 0;
       while (i < left.length && j < right.length) {
            if (left[i] <= right[j]) {
                result[k++] = left[i++];
            } else {
                result[k++] = right[j++];
            }
        }
        while (i < left.length) {
            result[k++] = left[i++];
        }
        while (j < right.length) {
            result[k++] = right[j++];
        }
        return result;
    }

⑤ 時(shí)間復(fù)雜度
歸并排序平均時(shí)間復(fù)雜度為O(nlogn),最好時(shí)間復(fù)雜度為O(nlogn)圈盔,最壞時(shí)間復(fù)雜度為O(nlogn)豹芯。
歸并排序的形式就是一棵二叉樹,它需要遍歷的次數(shù)就是二叉樹的深度驱敲,而根據(jù)完全二叉樹的可以得出它在任何情況下時(shí)間復(fù)雜度均是O(nlogn)告组。

⑥ 空間復(fù)雜度
歸并排序空間復(fù)雜度為O(n)

⑦ 穩(wěn)定性
歸并排序是穩(wěn)定的。

⑧ 算法應(yīng)用
歸并排序可以用于求解逆序?qū)?shù)量問題癌佩,具體見:劍指offer - 數(shù)組中的逆序?qū)?/a>

解法如下:

import java.util.*;
public class Solution {
     private static final int MOD = 1000000007;
     private int cnt = 0;

    //遞歸調(diào)用
     private int[] MergeSort(int[] array) {
        if (array.length < 2) 
            return array;
        int mid = array.length / 2;
        int[] left = Arrays.copyOfRange(array, 0, mid);
        int[] right = Arrays.copyOfRange(array, mid, array.length);
        return merge(MergeSort(left), MergeSort(right));
    }
    /**
     * 將兩段有序數(shù)組結(jié)合成一個(gè)有序數(shù)組
     *
     * @param left
     * @param right
     * @return
     */
    private int[] merge(int[] left, int[] right) {
             int[] result = new int[left.length + right.length];
             int i = 0,j = 0,k = 0;
             while (i < left.length && j < right.length) {
                if (left[i] <= right[j]) {
                    result[k++] = left[i++];
                } else {
                    result[k++] = right[j++];
                     /*歸并同時(shí)統(tǒng)計(jì)逆序?qū)?shù)量木缝,因?yàn)闅w并的兩個(gè)子序列都已有序,故當(dāng)left[i] >  
                     right[j]围辙,有l(wèi)eft[i...left.length - 1]均大于right[j]*/
                    this.cnt = (this.cnt % MOD + (left.length - i) % MOD) % MOD;
                }
             }
             while (i < left.length) {
                result[k++] = left[i++];
             }
             while (j < right.length) {
                result[k++] = right[j++];
             }
             return result;
     }

     public int InversePairs(int [] array) {
        MergeSort(array);
        return cnt % MOD;
    }
}

6我碟、快速排序(Quick Sort)

① 基本思想
快速排序的基本思想:通過一趟排序?qū)⒋庞涗浄指舫瑟?dú)立的兩部分,其中一部分記錄的關(guān)鍵字均比另一部分的關(guān)鍵字小姚建,則可分別對這兩部分記錄繼續(xù)進(jìn)行排序矫俺,以達(dá)到整個(gè)序列有序。

② 算法描述
快速排序使用分治法來把一個(gè)數(shù)列分為兩個(gè)子數(shù)列掸冤。具體算法描述如下:
1)從數(shù)列中挑出一個(gè)元素厘托,稱為 “基準(zhǔn)”(pivot);
2)重新排序數(shù)列稿湿,所有比基準(zhǔn)值小的元素放在基準(zhǔn)前面铅匹,所有比基準(zhǔn)值大的元素放在基準(zhǔn)的后面(相同的數(shù)可以到任一邊),該基準(zhǔn)就處于數(shù)列的中間位置饺藤。這稱為分區(qū)(partition)操作包斑;
3)遞歸地(recursive)對小于基準(zhǔn)值元素的子數(shù)列和大于基準(zhǔn)值元素的子數(shù)列進(jìn)行快速排序。

③ 動圖演示

④ 代碼實(shí)現(xiàn)
快速排序最核心的步驟就是partition操作涕俗,即從待排序的數(shù)列中選出一個(gè)數(shù)作為基準(zhǔn)罗丰,將所有比基準(zhǔn)值小的元素放在基準(zhǔn)前面,所有比基準(zhǔn)值大的元素放在基準(zhǔn)的后面(相同的數(shù)可以到任一邊)再姑,該基準(zhǔn)就處于數(shù)列的中間位置萌抵。partition函數(shù)返回基準(zhǔn)的位置,然后就可以對基準(zhǔn)位置的左右子序列遞歸地進(jìn)行同樣的快排操作,從而使整個(gè)序列有序绍填。

下面我們來介紹partition操作的兩種實(shí)現(xiàn)方法:左右指針法挖坑法萎坷。

方法一:左右指針法
基本思路:
1.將數(shù)組的最后一個(gè)數(shù) right 作為基準(zhǔn)數(shù) key。
2.分區(qū)過程:從數(shù)組的首元素 begin 開始向后找比 key 大的數(shù)(begin 找大)沐兰;end 開始向前找比 key 小的數(shù)(end 找卸叩怠);找到后交換兩者(swap)住闯,直到 begin >= end 終止遍歷瓜浸。最后將 begin(此時(shí)begin == end)和最后一個(gè)數(shù)交換( 這個(gè)時(shí)候 end 不是最后一個(gè)位置),即 key 作為中間數(shù)(左區(qū)間都是比key小的數(shù)比原,右區(qū)間都是比key大的數(shù))
3.再對左右區(qū)間重復(fù)第二步插佛,直到各區(qū)間只有一個(gè)數(shù)。

/**
 * partition操作
 * @param array
 * @param left 數(shù)列左邊界
 * @param right 數(shù)列右邊界
 * @return
 */
public static int partition(int[] array,int left,int right) {
        int begin = left;
        int end = right;
        int key = right;

        while( begin < end ) {
            //begin找大
            while(begin < end && array[begin] <= array[key])
                begin++;
            //end找小
            while(begin < end && array[end] >= array[key])
                end--;
            swap(array,begin,end);
        }
        swap(array,begin,right);
        return begin;   //返回基準(zhǔn)位置
    }
 
/**
 * 交換數(shù)組內(nèi)兩個(gè)元素
 * @param array
 * @param i
 * @param j
 */
public static void swap(int[] array, int i, int j) {
    int temp = array[i];
    array[i] = array[j];
    array[j] = temp;
}

方法二:挖坑法
基本思路:
1.定義兩個(gè)指針 left 指向起始位置量窘,right 指向最后一個(gè)元素的位置雇寇,然后指定一個(gè)基準(zhǔn) key(right),作為坑蚌铜。
2.left 尋找比基準(zhǔn)(key)大的數(shù)字锨侯,找到后將 left 的數(shù)據(jù)賦給 right,left 成為一個(gè)坑冬殃,然后 right 尋找比基數(shù)(key)小的數(shù)字囚痴,找到將 right 的數(shù)據(jù)賦給 left,right 成為一個(gè)新坑审葬,循環(huán)這個(gè)過程深滚,直到 begin 指針與 end指針相遇,然后將 key 填入那個(gè)坑(最終:key的左邊都是比key小的數(shù)涣觉,key的右邊都是比key大的數(shù))痴荐,然后進(jìn)行遞歸操作。

/**
 * partition操作
 * @param array
 * @param left 數(shù)列左邊界
 * @param right 數(shù)列右邊界
 * @return
 */
public static int partition(int[] array,int left,int right) {
        int key = array[right];//初始坑
        while(left < right) {
            //left找大
            while(left < right && array[left] <= key )
                left++;
            array[right] = array[left];//賦值官册,然后left作為新坑
            //right找小
            while(left <right && array[right] >= key)
                right--;
            array[left] = array[right];//right作為新坑
        }
        array[left] = key;
       /*將key賦值給left和right的相遇點(diǎn)生兆,
        保持key的左邊都是比key小的數(shù),key的右邊都是比key大的數(shù)*/
        return left;//最終返回基準(zhǔn)
    }

實(shí)現(xiàn)了partition操作攀隔,我們就可以遞歸地進(jìn)行快速排序了

 /**
 * 快速排序方法
 * @param array
 * @param left 數(shù)列左邊界
 * @param right 數(shù)列右邊界
 * @return
 */
public static void Quicksort(int array[], int left, int right) {
        if(left < right){
            int pos = partition(array, left, right);
            Quicksort(array, left, pos - 1);
            Quicksort(array, pos + 1, right);
        }
    }

⑤ 代碼優(yōu)化
我們之前選擇基準(zhǔn)的策略都是固定基準(zhǔn)皂贩,即固定地選擇序列的右邊界值作為基準(zhǔn)栖榨,但如果在待排序列幾乎有序的情況下昆汹,選擇的固定基準(zhǔn)將是序列的最大(小)值婴栽,快排的性能不好(因?yàn)槊刻伺判蚝舐郑笥覂蓚€(gè)子序列規(guī)模相差懸殊,大的那部分最后時(shí)間復(fù)雜度很可能會達(dá)到O(n2))愚争。

下面提供幾種常用的快排優(yōu)化:
優(yōu)化一:隨機(jī)基準(zhǔn)
每次隨機(jī)選取基準(zhǔn)值映皆,而不是固定選取左或右邊界值挤聘。將隨機(jī)選取的基準(zhǔn)值和右邊界值進(jìn)行交換,然后就回到了之前的解法捅彻。
只需要在 partition 函數(shù)前增加如下操作即可:

int random = (int) (left + Math.random() * (right - left + 1));
//隨機(jī)選擇 left ~ right 之間的一個(gè)位置作為基準(zhǔn)
swap(array, random, right);
//把基準(zhǔn)值交換到右邊界

優(yōu)化二:三數(shù)取中法
基本思想:
取第一個(gè)數(shù)组去,最后一個(gè)數(shù),第(N/2)個(gè)數(shù)即中間數(shù)步淹,三個(gè)數(shù)中數(shù)值中間的那個(gè)數(shù)作為基準(zhǔn)值从隆。
舉個(gè)例子,對于int[] array = { 2缭裆,5键闺,4,9澈驼,3辛燥,6,8缝其,7挎塌,1,0}内边,2勃蜘、3、0分別是第一個(gè)數(shù)假残,第(N/2)個(gè)是數(shù)以及最后一個(gè)數(shù)缭贡,三個(gè)數(shù)中3最大,0最小辉懒,2在中間阳惹,所以取2為基準(zhǔn)值。
實(shí)現(xiàn)getMid函數(shù)即可:

/**
     * 三數(shù)取中眶俩,返回array[left]莹汤、array[mid]、array[right]三者的中間者下標(biāo)作為基準(zhǔn)
     * @param array
     * @param left
     * @param right
     * @return
     */
public static int getMid(int[] array,int left,int right) {
        int mid = left + ((right - left) >> 1);
        int a = array[left];
        int b = array[mid];
        int c = array[right];
        if ((b <= a && a <= c) || (c <= a && a <= b)) { //a為中間值
            return left;
        }
        if ((a <= b && b <= c) || (c <= b && b <= a)) { //b為中間值
            return mid;
        }
        if ((a <= c && c <= b) || (b <= c && c <= a)) { //c為中間值
            return right;
        }
        return left;
    }

優(yōu)化三:當(dāng)待排序序列的長度分割到一定大小后颠印,使用插入排序
在子序列比較小的時(shí)候纲岭,直接插入排序性能較好,因?yàn)閷τ谟行虻男蛄邢吆保迮趴梢赃_(dá)到O(n)的復(fù)雜度止潮,如果序列比較小,使用插排效率要比快排高钞楼。
實(shí)現(xiàn)方式也很簡單喇闸,快排是在子序列元素個(gè)數(shù)為 1 時(shí)才停止遞歸,我們可以設(shè)置一個(gè)閾值n,假設(shè)為5燃乍,則大于5個(gè)元素唆樊,子序列繼續(xù)遞歸,否則選用插排刻蟹。
此時(shí)QuickSort()函數(shù)如下:

public static void Quicksort(int array[], int left, int right) {
        if(right - left > 5){
            int pos = partition(array, left, right);
            Quicksort(array, left, pos - 1);
            Quicksort(array, pos + 1, right);
        }else{
            insertionSort(array);
        }
    }

這種優(yōu)化非常實(shí)用逗旁。
實(shí)測發(fā)現(xiàn)當(dāng)待排序列為 [100000,99999,99998,...,3,2,1] 時(shí),不加插入優(yōu)化的快排由于遞歸次數(shù)過多甚至拋出了 java.lang.StackOverflowError舆瘪!

而加入了插入優(yōu)化并選擇閾值為 12500 時(shí)痢艺,排序用時(shí)如下:


實(shí)驗(yàn)發(fā)現(xiàn)閾值的選擇也很關(guān)鍵,選擇閾值為 5 介陶,排序用時(shí)如下:


優(yōu)化四:三路劃分
如果待排序列中重復(fù)元素過多堤舒,也會大大影響排序的性能,這是因?yàn)榇罅肯嗤貐⑴c快排時(shí)哺呜,左右序列規(guī)模相差極大舌缤,快排將退化為冒泡排序,時(shí)間復(fù)雜度接近O(n2)某残。這時(shí)候国撵,如果采用三路劃分,則會很好的避免這個(gè)問題玻墅。
三路劃分的思想是利用 partition 函數(shù)將待排序列劃分為三部分:第一部分小于基準(zhǔn)v介牙,第二部分等于基準(zhǔn)v,第三部分大于基準(zhǔn)v澳厢。這樣在遞歸排序區(qū)間的時(shí)候环础,我們就不必再對第二部分元素均相等的區(qū)間進(jìn)行快排了,這在待排序列存在大量相同元素的情況下能大大提高快排效率剩拢。

來看下面的三路劃分示意圖:


說明:紅色部分為小于基準(zhǔn)v的序列线得,綠色部分為等于基準(zhǔn)v的序列,白色部分由于還未被 cur 指針遍歷到徐伐,屬于大小未知的部分贯钩,藍(lán)色部分為大于基準(zhǔn)v的序列。
left 指針為整個(gè)待排區(qū)間的左邊界办素,right 指針為整個(gè)待排區(qū)間的右邊界角雷。less 指針指向紅色部分的最后一個(gè)數(shù)(即小于v的最右位置),more 指針指向藍(lán)色部分的第一個(gè)數(shù)(即大于v的最左位置)性穿。cur 指針指向白色部分(未知部分)的第一個(gè)數(shù)勺三,即下一個(gè)要判斷大小的位置。

算法思路:
1)由于最初紅色和藍(lán)色區(qū)域沒有元素季二,初始化 less = left - 1檩咱,more = right + 1揭措,cur = left胯舷。整個(gè)區(qū)間為未知部分(白色)刻蚯。
2)如果當(dāng)前 array[cur] < v,則 swap(array,++less,cur++)桑嘶,即把紅色區(qū)域向右擴(kuò)大一格(less指針后移)炊汹,把 array[cur] 交換到該位置,cur 指針前移判斷下一個(gè)數(shù)逃顶。
3)如果當(dāng)前 array[cur] = v讨便,則不必交換,直接 cur++
4)如果當(dāng)前 array[cur] > v以政,則 swap(array,--more,cur)霸褒,即把藍(lán)色區(qū)域向左擴(kuò)大一格(more指針前移),把 array[cur] 交換到該位置盈蛮。特別注意废菱!此時(shí)cur指針不能前移,這是因?yàn)榻粨Q到cur位置的元素來自未知區(qū)域抖誉,還需要進(jìn)一步判斷array[cur]殊轴。

利用三路劃分,我們就可以遞歸地進(jìn)行三路快排了袒炉!并且可以愉快地避開所有重復(fù)元素區(qū)間旁理。

代碼實(shí)現(xiàn):

public static int[] partition(int[] array,int left,int right){
        int v = array[right]; //選擇右邊界為基準(zhǔn)
        int less = left - 1; // < v 部分的最后一個(gè)數(shù)
        int more = right + 1; // > v 部分的第一個(gè)數(shù)
        int cur = left;
        while(cur < more){
            if(array[cur] < v){
                swap(array,++less,cur++);
            }else if(array[cur] > v){
                swap(array,--more,cur);
            }else{
                cur++;
            }
        }
        return new int[]{less + 1,more - 1};  //返回的是 = v 區(qū)域的左右下標(biāo)
}

public static void Quicksort(int array[], int left, int right) {
        if (left < right) {
            int[] p = partition(array,left,right);
            Quicksort(array,left,p[0] - 1); //避開重復(fù)元素區(qū)間
            Quicksort(array,p[1] + 1,right);
        }
}

三路劃分可以解決經(jīng)典的荷蘭國旗問題,具體見 leetcode 75

解法如下:

class Solution {
    // 方法一:使用計(jì)數(shù)排序解決我磁,但需要兩趟掃描,不符合要求
    /*public void sortColors(int[] nums) {
        int[] count = new int[3];
        for(int i = 0; i < nums.length; i++)
            count[nums[i]]++;
        int k = 0;
        for(int i = 0; i < 3; i++){
            for(int j = 0; j < count[i]; j++){
                nums[k++] = i;
            }
        }
    }*/
    // 方法二:使用快速排序的三路劃分孽文,時(shí)間復(fù)雜度為O(n),空間復(fù)雜度為O(1)
    public void sortColors(int[] nums) {
        int len = nums.length;
        if(len == 0)
            return;
        int less = -1;
        int more = len;
        int cur = 0;
        while(cur < more){
            if(nums[cur] == 0){
                swap(nums,++less,cur++);
            }else if(nums[cur] == 2){
                swap(nums,--more,cur);
            }else{
                cur++;
            }
        }
    }
    
    public static void swap(int[] array,int i,int j){
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

}

⑥ 時(shí)間復(fù)雜度
快速排序平均時(shí)間復(fù)雜度為O(nlogn),最好時(shí)間復(fù)雜度為O(nlogn)夺艰,最壞時(shí)間復(fù)雜度為O(n2)叛溢。
最好情況:基準(zhǔn)選擇得當(dāng),partition函數(shù)每次恰好能均分序列劲适,其遞歸樹的深度就為logn楷掉,時(shí)間復(fù)雜度為O(nlogn)。
最壞情況:選擇了最大或者最小數(shù)字作為基準(zhǔn)霞势,每次劃分只能將序列分為一個(gè)元素與其他元素兩部分烹植,此時(shí)快速排序退化為冒泡排序,如果用樹畫出來愕贡,得到的將會是一棵單斜樹草雕,即所有的結(jié)點(diǎn)只有左(右)結(jié)點(diǎn)的樹,樹的深度為 n固以,時(shí)間復(fù)雜度為O(n2)墩虹。

⑦ 空間復(fù)雜度
快速排序的空間復(fù)雜度主要考慮遞歸時(shí)使用的椫鼋恚空間。
在最好情況下诫钓,即partition函數(shù)每次恰好能均分序列旬昭,空間復(fù)雜度為O(logn);在最壞情況下菌湃,即退化為冒泡排序问拘,空間復(fù)雜度為O(n)。平均空間復(fù)雜度為O(logn)惧所。

⑧ 穩(wěn)定性
快速排序是不穩(wěn)定的骤坐。

⑨ 算法拓展
快速選擇算法
快速選擇算法用于求解 Kth Element 問題(無序數(shù)組第K大元素),使用快速排序的 partition() 進(jìn)行實(shí)現(xiàn)下愈。
快速排序的 partition() 方法會返回一個(gè)整數(shù) j 使得 a[left..j-1] 小于等于 a[j]纽绍,且 a[j+1..right] 大于等于 a[j]。
此時(shí) a[j] 就是數(shù)組的第 j 小的元素势似,我們可以轉(zhuǎn)換一下題意拌夏,第 k 大的元素就是第 nums.size() - k 小的元素。
找到 Kth Element 之后叫编,再遍歷一次數(shù)組辖佣,所有大于等于 Kth Element 的元素都是 TopK Elements。
時(shí)間復(fù)雜度 O(N)搓逾,空間復(fù)雜度 O(1)卷谈。

還可以使用小根堆求解此問題,時(shí)間復(fù)雜度 O(NlogK)霞篡,空間復(fù)雜度 O(K)世蔗。具體見:leetcode 215

解法如下:

class Solution {
private:
    int partition(vector<int>& array,int left,int right) {
        int key = array[right]; //初始坑
        while(left < right) {
            //left找大
            while(left < right && array[left] <= key )
                left++;
            array[right] = array[left]; //賦值,然后left作為新坑
            //right找小
            while(left <right && array[right] >= key)
                right--;
            array[left] = array[right]; //right作為新坑
        }
        array[left] = key;
       /*將key賦值給left和right的相遇點(diǎn)朗兵,
        保持key的左邊都是比key小的數(shù)污淋,key的右邊都是比key大的數(shù)*/
        return left;//最終返回基準(zhǔn)
    }

public:
    
    /*方法1:堆。用于求解 TopK Elements 問題余掖,通過維護(hù)一個(gè)大小為 K 的小根堆寸爆,
    堆中的元素就是TopK Elements。堆頂元素就是 Kth Element盐欺。如果是第K小的元素
    就建立大根堆赁豆。時(shí)間復(fù)雜度 O(NlogK),空間復(fù)雜度 O(K)冗美。*/
   /* int findKthLargest(vector<int>& nums, int k) {
        int n = nums.size();
        priority_queue<int,vector<int>,greater<int>> q; 
        for(int i = 0;i < n;i++){
                q.push(nums[i]);
                if(q.size() > k) 
                    q.pop();
        }
        return q.top();
    }*/
    
    /*方法2:快速選擇魔种。用于求解 Kth Element 問題,使用快速排序的 partition() 進(jìn)行實(shí)現(xiàn)粉洼。
    快速排序的 partition() 方法會返回一個(gè)整數(shù) j 使得 a[left..j-1] 小于等于 a[j]节预,
    且 a[j+1..right] 大于等于 a[j]叶摄,此時(shí) a[j] 就是數(shù)組的第 j 小的元素,
    我們可以轉(zhuǎn)換一下題意安拟,第 k 大的元素就是第 nums.size() - k 小的元素蛤吓。
    找到 Kth Element 之后,再遍歷一次數(shù)組去扣,所有大于等于 Kth Element 的元素都是
    TopK Elements柱衔。時(shí)間復(fù)雜度 O(N)樊破,空間復(fù)雜度 O(1)*/
   int findKthLargest(vector<int>& nums, int k) {
    k = nums.size() - k;
    int left = 0, right = nums.size() - 1;
    while (left < right) {
        int j = partition(nums, left, right);
        if (j == k) { //選擇的基準(zhǔn)等于目標(biāo)愉棱,跳出循環(huán)
            break;
        } else if (j < k) { //選擇的基準(zhǔn)小于目標(biāo),在右側(cè)子序列中繼續(xù)選擇
            left = j + 1; 
        } else { //選擇的基準(zhǔn)大于目標(biāo)哲戚,在左側(cè)子序列中繼續(xù)選擇
            right = j - 1; 
        }
      }
        return nums[k];
    }
};

拓展:Arrays.sort() 和 Collections.sort() 原理奔滑,Collections.sort() 底層調(diào)用的是 Arrays.sort()。Arrays.sort() 原理見 剖析JDK8中Arrays.sort底層原理及其排序算法的選擇

7顺少、堆排序(Heap Sort)

① 基本思想
堆排序是一種樹形選擇排序方法朋其,它利用了這種數(shù)據(jù)結(jié)構(gòu)。在排序的過程中脆炎,將array[0梅猿,...,n-1]看成是一顆完全二叉樹的順序存儲結(jié)構(gòu)秒裕,利用完全二叉樹中雙親結(jié)點(diǎn)和孩子結(jié)點(diǎn)之間的關(guān)系袱蚓,在當(dāng)前無序區(qū)中選擇關(guān)鍵字最大(最小)的元素几蜻。

② 概念
堆:堆是一種完全二叉樹喇潘,且滿足所有父節(jié)點(diǎn)的值均大于等于(或小于等于)其子節(jié)點(diǎn)的值。
大根堆(最大堆):滿足所有父節(jié)點(diǎn)的值均大于等于其子節(jié)點(diǎn)的值的堆稱為大根堆梭稚,堆頂元素是堆中元素的最大值颖低。
小根堆(最小堆):滿足所有父節(jié)點(diǎn)的值均小于等于其子節(jié)點(diǎn)的值的堆稱為小根堆,堆頂元素是堆中元素的最小值弧烤。
堆的順序存儲結(jié)構(gòu):使用順序數(shù)據(jù)結(jié)構(gòu)(數(shù)組)存儲堆忱屑,表示方法為:
1.數(shù)組按層序遍歷的順序存放完全二叉樹的結(jié)點(diǎn),下標(biāo)為 0 處為堆頂暇昂,下標(biāo)為 len - 1 處為堆尾莺戒。
2.結(jié)點(diǎn) i 如果存在左孩子(下標(biāo)不超過 len - 1 就存在),左孩子的下標(biāo)為(2 * i + 1)话浇;如果存在右孩子脏毯,右孩子的下標(biāo)為(2 * i + 2)。結(jié)點(diǎn) i 的父結(jié)點(diǎn)下標(biāo)為 (i - 1) / 2 (下標(biāo)為 0 的結(jié)點(diǎn)除外幔崖,它沒有父結(jié)點(diǎn))食店。最后一個(gè)非葉子結(jié)點(diǎn)即為堆尾元素的父結(jié)點(diǎn)渣淤,下標(biāo)為 (len - 1 - 1) / 2 = (len - 2) / 2。

③ 算法描述
1)將初始待排序關(guān)鍵字序列(R1,R2….Rn)構(gòu)建成大頂堆吉嫩,此堆為初始的無序區(qū);
2)將堆頂元素R[1]與最后一個(gè)元素R[n]交換自娩,此時(shí)得到新的無序區(qū)(R1,R2,……Rn-1)和新的有序區(qū)(Rn),且滿足R[1,2…n-1]<=R[n];
3)由于交換后新的堆頂R[1]可能違反堆的性質(zhì)脐彩,因此需要對當(dāng)前無序區(qū)(R1,R2,……Rn-1)調(diào)整為新堆姊扔,然后再次將R[1]與無序區(qū)最后一個(gè)元素交換,得到新的無序區(qū)(R1,R2….Rn-2)和新的有序區(qū)(Rn-1,Rn)佛南。不斷重復(fù)此過程直到有序區(qū)的元素個(gè)數(shù)為(n-1)嵌言,則整個(gè)排序過程完成。

④ 動圖演示

⑤ 代碼實(shí)現(xiàn)

//聲明全局變量绵载,用于記錄數(shù)組array的長度蓬蝶;
static int len;
/**
 * 堆排序算法
 * @param array
 * @return
 */
public static int[] HeapSort(int[] array) {
        len = array.length;
        if (len == 0) return array;
        //1.構(gòu)建一個(gè)大根堆
        buildMaxHeap(array);
        //2.循環(huán)將堆頂(最大值)與堆尾交換尘分,刪除堆尾元素,然后重新調(diào)整大根堆
        while (len > 0) {
            swap(array, 0, len - 1);
            len--; //原先的堆尾進(jìn)入有序區(qū)丸氛,刪除堆尾元素
            adjustHeap(array, 0); //重新調(diào)整大根堆
        }
        return array;
 }

 /**
   * 自頂向下調(diào)整以 i 為根的堆為大根堆
   * @param array
   * @param i
   */
public static void adjustHeap(int[] array, int i) {
        int maxIndex = i;
        //如果有左子樹培愁,且左子樹大于父節(jié)點(diǎn),則將最大指針指向左子樹
        if (2 * i + 1 < len && array[2 * i + 1] > array[maxIndex])
            maxIndex = 2 * i + 1;
        //如果有右子樹缓窜,且右子樹大于父節(jié)點(diǎn)定续,則將最大指針指向右子樹
        if (2 * i + 2 < len && array[2 * i + 2] > array[maxIndex])
            maxIndex = 2 * i + 2;
        //如果父節(jié)點(diǎn)不是最大值,則將父節(jié)點(diǎn)與最大值交換禾锤,并且遞歸調(diào)整與父節(jié)點(diǎn)交換的位置私股。
        if (maxIndex != i) {
            swap(array, maxIndex, i);
            adjustHeap(array, maxIndex);
        }
 }

 /**
  * 自底向上構(gòu)建初始大根堆
  * @param array
  */
 public static void buildMaxHeap(int[] array) {
        //從最后一個(gè)非葉子節(jié)點(diǎn)開始自底向上構(gòu)造大根堆
        for (int i = (len - 2) / 2; i >= 0; i--) { 
            adjustHeap(array, i);
        }
 }

拓展:
1)插入元素:只需要把待插入的元素放置在堆尾,然后 len++ 把其納入堆恩掷,然后調(diào)用 adjustHeap 函數(shù)重新調(diào)整堆即可倡鲸。
2)刪除堆頂元素:只需要把堆頂元素交換到堆尾,然后 len-- 把其移出堆黄娘,然后調(diào)用 adjustHeap 函數(shù)重新調(diào)整堆即可峭状。

⑥ 時(shí)間復(fù)雜度
堆排序平均時(shí)間復(fù)雜度為O(nlogn)克滴,最好時(shí)間復(fù)雜度為O(nlogn),最壞時(shí)間復(fù)雜度為O(nlogn)优床。
堆排序的形式就是一棵二叉樹劝赔,它需要遍歷的次數(shù)就是二叉樹的深度,而根據(jù)完全二叉樹的可以得出它在任何情況下時(shí)間復(fù)雜度均是O(nlogn)胆敞。

⑦ 空間復(fù)雜度
堆排序使用了常數(shù)空間仍翰,空間復(fù)雜度為O(1)歉备。

⑧ 穩(wěn)定性
堆排序是不穩(wěn)定的。

8帽驯、計(jì)數(shù)排序(Counting Sort)

① 基本思想
計(jì)數(shù)排序不是基于比較的排序算法,其核心在于將輸入的數(shù)據(jù)值轉(zhuǎn)化為鍵存儲在額外開辟的數(shù)組空間中嫌术。 作為一種線性時(shí)間復(fù)雜度的排序度气,計(jì)數(shù)排序要求輸入的數(shù)據(jù)必須是有確定范圍的整數(shù)。

② 算法描述
1)找出待排序的數(shù)組中最大和最小的元素院领;
2)統(tǒng)計(jì)數(shù)組中每個(gè)值為 i 的元素出現(xiàn)的次數(shù)比然,存入數(shù)組C的第i項(xiàng)扒寄;
3)對所有的計(jì)數(shù)累加(從C中的第一個(gè)元素開始嵌戈,每一項(xiàng)和前一項(xiàng)相加);
4)反向填充目標(biāo)數(shù)組:將每個(gè)元素 i 放在新數(shù)組的第C(i)項(xiàng)于樟,每放一個(gè)元素就將C(i)減去1。

③ 動圖演示

④ 代碼實(shí)現(xiàn)

/**
     * 計(jì)數(shù)排序
     *
     * @param array
     * @return
     */
    public static int[] CountingSort(int[] array) {
        if (array.length == 0) return array;
        int bias, min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
        for (int i = 0; i < array.length; i++) {
            max = Math.max(max, array[i]);
            min = Math.min(min, array[i]);
        }
       //計(jì)算偏移量路捧,將 min ~ max 映射到 bucket 數(shù)組的 0 ~ (max - min) 位置上
        bias = -min; 
        int[] bucket = new int[max - min + 1];
        Arrays.fill(bucket, 0);
        for (int i = 0; i < array.length; i++) {
            bucket[array[i] + bias]++;
        }
        int index = 0, i = 0;
        while (index < array.length) {
            if (bucket[i] != 0) {
                array[index] = i - bias;
                bucket[i]--;
                index++;
            } else
                i++;
        }
        return array;
    }

⑤ 時(shí)間復(fù)雜度
計(jì)數(shù)排序平均時(shí)間復(fù)雜度為O(n + k),最好時(shí)間復(fù)雜度為O(n + k)章姓,最壞時(shí)間復(fù)雜度為O(n + k)。n 為遍歷一趟數(shù)組計(jì)數(shù)過程的復(fù)雜度系忙,k 為遍歷一趟桶取出元素過程的復(fù)雜度。

⑥ 空間復(fù)雜度
計(jì)數(shù)排序空間復(fù)雜度為O(k)见剩,k為桶數(shù)組的長度苍苞。

⑦ 穩(wěn)定性
計(jì)數(shù)排序是穩(wěn)定的骂际。

9、桶排序(Bucket Sort)

① 基本思想
桶排序與計(jì)數(shù)排序很相似凑耻,不過現(xiàn)在的桶不單計(jì)數(shù)香浩,是實(shí)實(shí)在在地放入元素餐弱。按照映射函數(shù)將數(shù)據(jù)分配到不同的桶里膏蚓,每個(gè)桶內(nèi)元素再分別排序(可能使用別的排序算法),最后拼接各個(gè)桶中排好序的數(shù)據(jù)。映射函數(shù)人為設(shè)計(jì)破停,但要保證桶 i 中的數(shù)均小于桶 j (i < j)中的數(shù)真慢,即必須桶間必須有序,桶內(nèi)可以無序朗鸠,可以考慮按照數(shù)的區(qū)間范圍劃分桶。下面代碼的桶映射函數(shù)為:(i - min) / arr.length忆家。

② 算法描述
1)設(shè)置一個(gè)定量的數(shù)組當(dāng)作空桶揭芍;
2)遍歷輸入數(shù)據(jù),并且把數(shù)據(jù)一個(gè)一個(gè)放到對應(yīng)的桶里去列另;
3)對每個(gè)不是空的桶的桶內(nèi)元素進(jìn)行排序(可以使用直接插入排序等)页衙;
4)從不是空的桶里把排好序的數(shù)據(jù)拼接起來。

③ 動圖演示

④ 代碼實(shí)現(xiàn)

public static int[] bucketSort(int[] array){
    int max = Integer.MIN_VALUE;
    int min = Integer.MAX_VALUE;
    for(int i = 0; i < array.length; i++){
        max = Math.max(max, array[i]);
        min = Math.min(min, array[i]);
    }
    
    /*桶映射函數(shù):自己設(shè)計(jì),要保證桶 i 的數(shù)均小于桶 j (i < j)的數(shù)廉侧,
      即必須桶間必須有序段誊,桶內(nèi)可以無序。這里桶映射函數(shù)為:(i - min) / arr.length*/
    int bucketNum = (max - min) / array.length + 1;
    ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
    for(int i = 0; i < bucketNum; i++){
        bucketArr.add(new ArrayList<Integer>());
    }
    
    //將每個(gè)元素放入桶
    for(int i = 0; i < array.length; i++){
        int num = (array[i] - min) / (array.length);
        bucketArr.get(num).add(array[i]);
    }
    
    //對每個(gè)桶進(jìn)行排序
    for(int i = 0; i < bucketArr.size(); i++){
        Collections.sort(bucketArr.get(i));
    }
    
   int k = 0;
   for(int i = 0; i < bucketArr.size(); i++){
      for(int j = 0;j < bucketArr.get(i).size();j++) {
           array[k++] = bucketArr.get(i).get(j);
      }
  }
  return array;
}

⑤ 時(shí)間復(fù)雜度
桶排序平均時(shí)間復(fù)雜度為O(n + k),最好時(shí)間復(fù)雜度為O(n + k)潜腻,最壞時(shí)間復(fù)雜度為O(n2)砾赔。

⑥ 空間復(fù)雜度
桶排序空間復(fù)雜度為O(n + k)妓盲。

⑦ 穩(wěn)定性
桶排序是穩(wěn)定的。

10筋粗、基數(shù)排序(Radix Sort)

① 基本思想
基數(shù)排序是按照低位先排序,然后收集买决;再按照高位排序督赤,然后再收集;依次類推没卸,直到最高位。有時(shí)候有些屬性是有優(yōu)先級順序的,先按低優(yōu)先級排序瑰煎,再按高優(yōu)先級排序酒甸。最后的次序就是高優(yōu)先級高的在前,高優(yōu)先級相同的低優(yōu)先級高的在前农尖。

② 算法描述
1)取得數(shù)組中的最大數(shù)助隧,并取得位數(shù)并村;
2)array 為原始數(shù)組哩牍,從最低位開始取每個(gè)位組成 radix 數(shù)組;

  1. 對 radix 進(jìn)行計(jì)數(shù)排序(利用計(jì)數(shù)排序適用于小范圍數(shù)的特點(diǎn))外潜;

③ 動圖演示

④ 代碼實(shí)現(xiàn)

    /**
     * 基數(shù)排序
     * @param array
     * @return
     */
public static int[] RadixSort(int[] array) {
     if (array == null || array.length < 2)
        return array;
        // 1.先算出最大數(shù)的位數(shù);
    int max = Integer.MIN_VALUE;
    for (int i = 0; i < array.length; i++) {
            max = Math.max(max, array[i]);
    }
    int maxDigit = 0;
    while (max != 0) {
            max /= 10;
            maxDigit++;
    }
    int div = 1;
    ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
    for (int i = 0; i < 10; i++)
        bucketList.add(new ArrayList<Integer>());
        //2.進(jìn)行maxDigit趟分配
    for (int i = 0; i < maxDigit; i++,div *= 10) {
            for (int j = 0; j < array.length; j++) {
                int num = (array[j] / div) % 10;
                bucketList.get(num).add(array[j]);
            }
        //3.收集
            int index = 0;
            for (int j = 0; j < bucketList.size(); j++) {
                for (int k = 0; k < bucketList.get(j).size(); k++)
                    array[index++] = bucketList.get(j).get(k);
                bucketList.get(j).clear();
            }
   }
   return array;
}

⑤ 時(shí)間復(fù)雜度
基數(shù)排序平均時(shí)間復(fù)雜度為O(n * k),最好時(shí)間復(fù)雜度為O(n * k)哆致,最壞時(shí)間復(fù)雜度為O(n * k)摊阀。

⑥ 空間復(fù)雜度
基數(shù)排序空間復(fù)雜度為O(n + k)。

⑦ 穩(wěn)定性
基數(shù)排序是穩(wěn)定的漱牵。

三刁赦、各排序算法應(yīng)用場景及選擇

1)若 n 較猩趼觥(如n ≤ 50)時(shí),可采用直接插入或簡單選擇排序波闹。
2)若元素初始狀態(tài)基本有序(正序)精堕,直接插入、冒泡或快速排序?yàn)橐恕?br> 3)若 n 較大庄撮,則應(yīng)采用時(shí)間復(fù)雜度為O(nlogn)的排序方法:快速排序、堆排序或歸并排序烙如。
快速排序是目前基于比較的內(nèi)部排序中被認(rèn)為是最好的方法亚铁,當(dāng)待排序的關(guān)鍵字是隨機(jī)分布時(shí)徘溢,快速排序的平均時(shí)間最短。
堆排序所需的輔助空間少于快速排序,并且不會出現(xiàn)快速排序可能出現(xiàn)的最壞情況雌隅。這兩種排序都是不穩(wěn)定的修械。
若要求排序穩(wěn)定肯污,則可選用歸并排序。但本文介紹的從單個(gè)記錄起進(jìn)行兩兩歸并的歸并排序算法并不值得提倡,通吵荩可以將它和直接插入排序結(jié)合在一起使用。先利用直接插入排序求得較長的有序數(shù)列灰伟,然后再兩兩歸并之袱箱。因?yàn)橹苯硬迦肱判蚴欠€(wěn)定的,所以改進(jìn)后的歸并排序仍是穩(wěn)定的了讨。
4)當(dāng)范圍已知,且空間不是很重要的情況下可以考慮使用桶排序男杈。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肤无,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌业岁,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異右犹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)姚垃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門念链,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人积糯,你說我怎么就攤上這事掂墓。” “怎么了看成?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵吃嘿,是天一觀的道長。 經(jīng)常有香客問我斗塘,道長搓侄,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上隆判,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布融撞。 她就那樣靜靜地躺著急前,像睡著了一般呻征。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上吵聪,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼敛苇。 笑死,一個(gè)胖子當(dāng)著我的面吹牛藤滥,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼碟摆,長吁一口氣:“原來是場噩夢啊……” “哼典奉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤滔以,失蹤者是張志新(化名)和其女友劉穎凭迹,沒想到半個(gè)月后撼唾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掌呜,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碍侦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年疤估,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了贷岸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡航闺,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出猴誊,到底是詐尸還是另有隱情潦刃,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布懈叹,位于F島的核電站乖杠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏澄成。R本人自食惡果不足惜胧洒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一畏吓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧卫漫,春花似錦菲饼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至包吝,卻和暖如春饼煞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背漏策。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工派哲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人掺喻。 一個(gè)月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓芭届,卻偏偏與公主長得像,于是被迫代替她去往敵國和親感耙。 傳聞我的和親對象是個(gè)殘疾皇子褂乍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容