動畫:一篇文章快速學會計數排序

內容介紹

計數排序簡介

咱們知道目前排序速度最快的是時間複雜度爲O(nlogn)的排序算法,如快速排序、歸併排序和堆排序。其中O(logn)是利用了分治思想進行數據二分遠距離比較和交換元素的位置。以前的算法都是基於元素比較的,有沒有一種算法,它的時間複雜度小於O(nlogn)呢?這樣的算法是存在的。計數排序就是一個非基於比較的排序算法,該算法於1954年由 Harold H. Seward提出。它的優點在於在對必定範圍內的整數排序時,它的複雜度爲Ο(n+k)(其中k是整數的範圍),快於任何比較排序算法。 固然這是一種犧牲空間換取時間的作法。java

計數排序適用於數據量很大,可是數據的範圍比較小的狀況。好比對一個公司20萬人的年齡進行排序,要排序的數據量很大,可是年齡分佈在0 ~ 134歲之間(最大年齡數據來源吉尼斯世界記錄)。算法

計數排序的思想

使用一個輔助數組,遍歷待排序的數據,待排序數據的值就是輔助數組的索引,輔助數組索引對應的位置保存這個待排序數據出現的次數。最後從輔助數組中取出待排序的數據,放到排序後的數組中。編程

計數排序動畫演示

通常沒有特殊要求排序算法都是升序排序,小的在前,大的在後。數組由{6, 8, 9, 5, 3, 2, 1, 7, 8, 5} 這10個無序元素組成。 數組

計數排序分析

經過上面的動畫演示,咱們能夠將計數排序分爲兩個過程:微信

  1. 統計過程
  2. 排序過程。

假設咱們要排序的數據是10個0到9的隨機數字,例如{6, 8, 9, 5, 3, 2, 1, 7, 8, 5} 這10個數據,以下圖所示: 優化

  1. 統計過程 取出元素6,放到輔助數組索引6的地方,輔助數組記錄數據6出現1次,效果以下圖:

取出元素8,放到輔助數組索引8的地方,輔助數組記錄數據8出現1次,效果以下圖: 動畫

取出元素9,放到輔助數組索引9的地方,輔助數組記錄數據9出現1次,效果以下圖: 3d

依次類推。中間省略一部分。code

再次取出元素8,放到輔助數組索引8的地方,輔助數組記錄數據8出現2次,效果以下圖: blog

最終輔助數組效果以下:

這個輔助數組統計了每一個數據出現的次數,最後遍歷這個輔助數組,輔助數組中的索引就是元素的值,輔助數組中索引對應的值就是這個數據出現的次數,放到排序後的數組中。

  1. 排序過程 輔助數組索引1的對應的數據1放到排序後數組中,效果以下:

輔助數組索引2的對應的數據2放到排序後數組中,效果以下:

依次類推,取出統計數組中的數據放到排序後的數組中,中間省略部分。

輔助數組索引5的對應的數據5放到排序後數組中,效果以下:

再次將輔助數組索引5的對應的數據5放到排序後數組中,效果以下:

依次類推,中間省略部分。

最終排序後的效果以下:

計數排序代碼編寫

public class CountSortTest {
    public static void main(String[] args) {
        int[] arr = new int[] {6, 8, 9, 5, 3, 2, 1, 7, 8, 5};

        countSort(arr);
        System.out.println("排序後:" + Arrays.toString(arr));
    }

    // 假設咱們要排序的數據是10個0到9的隨機數字
    public static void countSort(int[] arr) {
        // 1.獲得待排序數據的最大值
        int max = arr[0];
        for (int i = 1; i < arr.length; i++) {
            int num = arr[i];
            if (num > max)
                max = num;
        }

        // 2.建立一個輔助數組長度是最大值+1
        int[] countArr = new int[max+1];

        // 3.遍歷待排序的數據,放到輔助數組中進行統計
        for (int i = 0; i < arr.length; i++) {
            // arr[i]取出待排序的數據,假設是5
            // countArr[arr[i]] 就是 countArr[5]
            // countArr[5]++;
            countArr[arr[i]]++; // 取出待排序的數據,找到數據在輔助數組對應的索引,數量加1
        }

        // 4.遍歷輔助數據,將統計到的待排序數據放到已排序的數組中
        int index = 0; // 用於記錄當前數據放到已排序數組的哪一個位置
        // 已排序數組和待排序數組同樣長
        int[] sortedArr = new int[arr.length];
        for (int i = 0; i < countArr.length; i++) {
            while (countArr[i] > 0) {
                sortedArr[index++] = i;
                countArr[i]--;
            }
        }

        // 5.將已排序的數據放到待排序的數組中
        for (int i = 0; i < sortedArr.length; i++) {
            arr[i] = sortedArr[i];
        }
    }
}

計數排序優化1

對任意指定範圍內的數字進行排序。

剛纔咱們的計數排序規定數據是 0 ~ 9這十個範圍內的數字。有可能排序的數據不是從0開始,例以下面這個數{68, 65, 72, 74, 73, 72, 70, 71, 69, 70, 67, 70, 66}是65 ~ 74這十個範圍內的數字。咱們按照剛纔的代碼輔助數組的長度須要爲75,其實這是沒有必要的,咱們能夠看到0 ~ 64這個範圍內根本沒有數字。浪費了數組上0 ~ 63索引位置上的存儲空間。咱們能夠把65放到索引0,66放到索引1,依次類推,效果以下圖:

經過上圖能夠看到,咱們須要找到待排數據中的最小值和最大值,使用(最大值-最小值+1)來做爲輔助數組的長度。最小值放到輔助數組0索引的位置,依次日後推。

輔助數組的長度=10 (74-65+1)

69元素在輔助數組的位置=4 (69-65)

優化後代碼:

public class CountSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {68, 65, 72, 74, 73, 72, 70, 71, 69, 70, 67, 70, 66};

        countSort(arr);
        System.out.println("排序後:" + Arrays.toString(arr));
    }

    // 假設咱們要排序的數據是10個65到74的隨機數字
    public static void countSort(int[] arr) {
        // 1.獲得待排序數據的最大/最小值
        int max = arr[0];
        int min = arr[0];
        for (int i = 1; i < arr.length; i++) {
            int num = arr[i];
            if (num > max)
                max = num;
            else if (num < min)
                min = num;
        }

        // 2.建立一個輔助數組長度是最大值+1
        int[] countArr = new int[max-min+1];

        // 3.遍歷待排序的數據,放到輔助數組中進行統計
        for (int i = 0; i < arr.length; i++) {
            // arr[i]取出待排序的數據,假設是69
            // countArr[arr[i]-min] 就是 countArr[69-65]
            // countArr[4]++;
            countArr[arr[i]-min]++; // 取出待排序的數據,找到數據在輔助數組對應的索引,數量加1
        }

        // 4.遍歷輔助數據,將統計到的待排序數據放到已排序的數組中
        int index = 0; // 用於記錄當前數據放到已排序數組的哪一個位置
        // 已排序數組和待排序數組同樣長
        int[] sortedArr = new int[arr.length];
        for (int i = 0; i < countArr.length; i++) {
            while (countArr[i] > 0) {
                sortedArr[index++] = min + i;
                countArr[i]--;
            }
        }

        // 5.將已排序的數據放到待排序的數組中
        for (int i = 0; i < sortedArr.length; i++) {
            arr[i] = sortedArr[i];
        }
    }
}

計數排序優化2

保證計數排序的穩定性。

到目前爲止,咱們的計數排序能夠實現必定範圍內的排序,可是還存在一個問題,相同的數據咱們排序時沒有保證順序,也就是說如今的計數排序時不穩定的排序,以下圖所示:

從上圖能夠看到待排序中有3個相同的數據70,經過計數排序後,這3個70的位置改變了。那麼如何保證計數排序的穩定性呢?我要先分析一下爲何會形成不穩定,上面說過計數排序分紅兩個過程:1.統計過程,2.排序過程。問題就出如今這兩個過程當中。

咱們先來看一下統計的過程:

第一次統計紫色數字70,效果以下:

第二次統紅色計數字70,效果以下:

第三次統計灰色數字70,效果以下:

咱們再來看一下排序的過程:

第一次排序,排序的是第三個灰色數字70,效果以下:

第二次排序,排序的是第二個紅色數字70,效果以下:

第三次排序,排序的是第一個紫色色數字70,效果以下:

經過上面的分析咱們就知道致使計數排序是不穩定排序的緣由了,統計時最後一個灰色的數字70在排序時被第一個取出排序,第一個紫色的70被最後一次取出排序。總結就是統計時的順序和排序時的順序不對應。知道緣由瞭解決就好辦了。

要讓計數排序是穩定排序,只要保證統計時和排序時操做相同數字的順序是對應的(後統計的先參與排序)。以下圖所示:

統計時第三個灰色的數字70第一次取出放到合適的地方,以下圖:

統計時第二個紅色的數字70第二次取出放到合適的地方,以下圖:

統計時第一個紫色的數字70第三次取出放到合適的地方,以下圖:

如何作到上圖中的相同數據後統計的先參與排序,這個地方有點繞,要注意啦!咱們須要保證兩點:

  1. 統計時,計算相同數據具體保存的位置。
  2. 排序時,從待排序數組倒序遍歷,從後往前獲取數據。

咱們先看第一點:統計時,計算相同數據具體保存的位置。 輔助數組在統計當前元素數量時加上以前元素的數量,就能夠肯定當前元素所在的位置,效果以下:

咱們再看第二點:排序時,從待排序數組倒序遍歷,從後往前獲取數據,能夠保證後統計的數據先參與排序。動畫效果以下:

優化後代碼以下:

public class CountSortTest3 {
    public static void main(String[] args) {
        int[] arr = new int[] {68, 65, 72, 74, 73, 72, 70, 71, 69, 70, 67, 70, 66};

        countSort(arr);
        System.out.println("排序後:" + Arrays.toString(arr));
    }

    // 假設咱們要排序的數據是10個65到74的隨機數字
    public static void countSort(int[] arr) {
        // 1.獲得待排序數據的最大/最小值
        int max = arr[0];
        int min = arr[0];
        for (int i = 1; i < arr.length; i++) {
            int num = arr[i];
            if (num > max)
                max = num;
            else if (num < min)
                min = num;
        }

        // 2.建立一個輔助數組長度是最大值+1
        int[] countArr = new int[max-min+1];

        // 3.遍歷待排序的數據,放到輔助數組中進行統計
        for (int i = 0; i < arr.length; i++) {
            // arr[i]取出待排序的數據,假設是69
            // countArr[arr[i]-min] 就是 countArr[69-65]
            // countArr[4]++;
            countArr[arr[i]-min]++; // 取出待排序的數據,找到數據在輔助數組對應的索引,數量加1
        }

        // 4.對輔助數組進行加工處理
        for (int i = 1; i < countArr.length; i++) {
            countArr[i] += countArr[i-1];
        }
        System.out.println("Arrays.toString() = " + Arrays.toString(countArr));

        // 5.倒序遍歷源數組
        // 已排序數組和待排序數組同樣長
        int[] sortedArr = new int[arr.length];
        for (int i = arr.length-1; i >= 0; i--) {
            // 獲得這個待排序的數據`arr[i]`,去輔助數組中找到合適的位置`arr[i]-min`,放到已排序數組中`countArr[arr[i]-min]`
            // arr[i]: 待排序的數據
            // arr[i]-min: 待排序的數據在輔助數組中的位置
            // countArr[arr[i]-min-1]: 待排序數據再已排序數組的位置
            sortedArr[countArr[arr[i]-min]-1] = arr[i];
            // 輔助數組中該數據的數量減一,也就是後續相同數據放到前面一個位置
            countArr[arr[i]-min]--;
        }

        // 6.將已排序的數據放到待排序的數組中
        for (int i = 0; i < sortedArr.length; i++) {
            arr[i] = sortedArr[i];
        }
    }
}

計數排序的複雜度

假設數據規模爲n,數據範圍爲k。

計數排序的空間複雜度:輔助數組須要m個空間,排序後的數組和待排序數組是同樣長的,因此總的空間複雜度是O(n+m)

計數排序的時間複雜度:1.獲得待排序數據的最大/最小值遍歷一次源數組操做次數爲n,3.遍歷待排序的數據,放到輔助數組中進行統計操做次數爲n,4.對輔助數組進行加工處理操做次數爲k,5.倒序遍歷源數組操做次數爲n,6.將已排序的數據放到待排序的數組中操做次數爲n,總操做次數爲:4n+m。因此總的時間複雜度爲O(n+k)

計數排序的侷限性

  1. 待排序數據範圍過大不適用於計數排序。假設有100個整數,他們的範圍是0到兩千萬,若是使用計數排序須要一個長度爲一千萬零一的數組,其中只有100位置存儲了數據,剩餘都是沒有存儲數據,浪費空間。若是有負整數,能夠加上一個固定的常數使得待排序列的最小值爲0。

  2. 待排序數據不是整數不適用於計數排序。假設有100個小數,他們的範圍是0到1,可是0到1之間的小數有無數個,沒法使用計數排序進行排序,由於連開闢多大的輔助數組都不能肯定。

總結

計數排序就是一個非基於比較的排序算法,它的優點在於在對必定範圍內的整數排序時,它的複雜度爲Ο(n+k)(其中k是整數的範圍),快於任何比較排序算法。

計數排序適用於數據量很大,可是數據的範圍比較小的狀況。好比對一個公司20萬人的年齡進行排序,要排序的數據量很大,可是年齡分佈在0 ~ 134歲之間(最大年齡數據來源吉尼斯世界記錄)。

計數排序的思想:使用一個輔助數組,遍歷待排序的數據,待排序數據的值就是輔助數組的索引,輔助數組索引對應的位置保存這個待排序數據出現的次數。最後從輔助數組中取出待排序的數據,放到排序後的數組中。


原創文章和動畫製做真心不易,您的點贊就是最大的支持! 想了解更多文章請關注微信公衆號:表哥動畫學編程

相關文章
相關標籤/搜索