資料結構與演算法知識點總結(4)各類排序演算法

LyAsano發表於2022-04-18

0.先言

  排序演算法是資料結構與演算法中最基本的演算法之一。

  排序演算法可以分為內部排序和外部排序,內部排序是資料記錄在記憶體中進行排序,而外部排序是因排序的資料很大,一次不能容納全部的排序記錄,在排序過程中需要訪問外存。常見的內部排序演算法有:插入排序、希爾排序、選擇排序、氣泡排序、歸併排序、快速排序、堆排序、基數排序等。用一張圖概括:、

資料結構與演算法知識點總結(4)各類排序演算法

資料結構與演算法知識點總結(4)各類排序演算法

0.1 名詞解釋

  • n:資料規模
  • k:"桶"的個數
  • In-place:佔用常數記憶體,不佔用額外記憶體
  • Out-place:佔用額外記憶體
  • 穩定性:排序後 2 個相等鍵值的順序和排序之前它們的順序相同

1. 插入排序

1.1 直接插入排序

  直接插入排序的特點:

  • 時空效率: 時間複雜度為O(n^2),空間複雜度為O(1)。最好情況下是元素基本有序,此時每插入一個元素,只需比較幾次而無需移動,時間複雜度為O(n)

  • 穩定性: 保證相等元素的插入相對位置不會變化,穩定排序

    void insertion_sort_int(int *arr,int n) {
        for(int i = 1;i < n;i++) {
            int key = arr[i];
            int j = i - 1;
            for(; j >= 0 && arr[j] > key;j--) {
                arr[j + 1] = arr[j];
            }
            arr[j + 1] = key;
        }
    }

1.2 折半插入排序

  在查詢插入位置時改為折半查詢,即為折半插入排序。

void binary_insertion_sort_int(int *arr,int n) {
    for(int i = 1;i < n;i++) {
        int key = arr[i];
        int left = 0;
        int right = i - 1;
        while(left <= right) {
            int mid = (left + right) / 2;
            if(key < arr[mid]) 
                right = mid - 1;
             else
                 left = mid + 1;
        }
        for(int j = i - 1; j >= left; j--)
            arr[j + 1] = arr[j];
        arr[left] = key;
    }
}

1.3 希爾排序

  首先取長度的一半作為增量的步長d1,把表中全部記錄分成d1個組,把步長隔d1的記錄放在同一組中再進行直接插入排序;然後再取d1的一半作為步長,重複插入排序操作。

  • 一般預設n在某個特定範圍時,希爾排序的時間複雜度為O(n^1.3)

  • 穩定性: 當相同關鍵字對映到不同的子表中,可能會改變相對次序,不穩定排序。例如(3,2,2)就會改變2的相對次序

void shell_sort_int(int *arr,int n) {
    for(int dk = n / 2; dk >= 1; dk >>= 1) {
        for(int i = dk; i < n;i++) {
            int key = arr[i];
            int j = i - dk;
            for(; j >=0 && arr[j] > key; j -= dk) {
                arr[j + dk] = arr[j];
            }
            arr[j + dk] = key;
        }
    }
}

2. 歸併排序

2.1 二路歸併排序

  二路歸併排序是分治法的應用,模式如下:

  • 分解: n個元素分解成兩個n/2的子序列
  • 解決: 用歸併排序對兩個子序列進行遞迴地排序
  • 合併: 合併兩個有序的子序列得到排序結果

  關於兩個有序列表之間的合併,只需要複製到輔助陣列中,根據大小關係兩兩比較再放入原始陣列中,這種合併方法有很多變式。二路歸併排序的特點如下:

  • 時間效率: 有lgn趟歸併,每次歸併時間為O(n),則時間複雜度為O(nlgn)

  • 空間效率: merge操作需要輔助空間O(n),建議在merge操作外分配一個大的陣列(注意也有O(1)的合併演算法)

  • 穩定性: 是穩定排序,不改變相同關鍵字記錄的相對次序

void merge(int *arr,int *help,int left,int mid,int right) {
    for(int i = left; i <= right;i++)
        help[i] = arr[i]; //把A中元素複製到B中,藉助B處理
    int i = left;
    int j = mid + 1;
    int k = left;
    while(i <= mid && j <= right) { //記錄有多少個共同的
        if(help[i] < help[j]) 
            arr[k++] = help[i++]; //較小值複製到A中
        else 
            arr[k++] = help[j++];
    }
    while(i <= mid)   arr[k++] = help[i++]; //某表未檢測完直接複製
    while(j <= right) arr[k++] = help[j++];
}


void merge_sort_int(int *arr,int *help,int left,int right) {
    if(left < right) {
        int mid = ((right - left) >> 1) + left;
        merge_sort_int(arr,help,left,mid);
        merge_sort_int(arr,help,mid + 1,right);
        merge(arr,help,left,mid,right);
    }
}

2.2 原地歸併排序

  原地歸併排序不需要輔助陣列就可以歸併,關鍵在於merge函式。思路是:

  • 遍歷i找到第一個arr[i] > arr[j](start = j),確定i的位置。也就是說在這之前的元素都小於從j開始的元素
  • 遍歷j找到第一個元素arr[j] > arr[i](end = j),確定j的位置,則說明在這之前的元素小於從i開始的元素

  上面操作說明第二個序列(start,end-1)的元素都應該在i之前-採用迴圈移位的辦法移到i前面。
  例如0 1 5 6 9 | 2 3 4 7 8:

  • 第一次遍歷確定了5和7,然後就把2 3 4迴圈移位到5 6 9前面,
  • i再從5的新位置開始作為第一個序列,j的位置是第二個序列開始
/*三個輔助函式*/
void swap(int *a,int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

/*長度為n的陣列翻轉*/
void reverse(int *arr,int n) {
    int i = 0;
    int j = n - 1;
    while(i < j) {
        swap(&arr[i],&arr[j]);
        i++;
        j--;
    }
}

/**
 * 將含有n個元素的陣列左迴圈移位i位
 */
void rotation_left(int *arr,int n,int i) {
    reverse(arr,i);
    reverse(arr + i,n - i);
    reverse(arr,n);
}

void merge_inplace(int  *arr,int left,int mid,int right) {
    int i = left,j = mid + 1, k = right;
    while(i < j && j <= k) {
        while(i < j && arr[i] <= arr[j]) i++;
        int step = 0;
        while(j <= k && arr[j] <= arr[i]) {
            j++;
            step++;
        }
        //arr+i為子陣列,j-i表示子陣列元素個數,j-i-step表示左迴圈移位的位數(把前面的元素移到後面去)
        rotation_left(arr + i,j- i,j - i - step);
    }
}


void merge_sort_inplace(int *arr,int left,int right) {
    if(left < right) {
        int mid = (left + right) / 2;
        merge_sort_inplace(arr,left,mid);
        merge_sort_inplace(arr,mid+1,right);
        merge_inplace(arr,left,mid,right);
    }
}

3. 交換排序

3.1 氣泡排序

  氣泡排序總共要進行n-1趟遍歷,每次都能確定一個元素最終的位置(剩餘序列中的最值)。在一個序列中,正向遍歷能確定一個最大值,反向遍歷確定一個最小值,每次序列減少一個元素。它的特點如下:

  • 時空效率: 平均時間複雜度為O(n^2),空間複雜度為O(1),產生的有序子序列一定是全域性有序的

  • 穩定性: 穩定的排序方法

void bubble_sort_int(int *arr,int n) {
    for(int i = 0; i < n - 1; i++) { //n-1趟冒泡,每次確定一個元素,每次確定小值元素
        bool flag = false;
        for(int j = n- 1; j > i; j--) {
            if(arr[j - 1] > arr[j]) { //有逆序
                swap(&arr[j - 1],&arr[j]);
                flag = true; //發生交換
            }
        }
        if(flag == false)
            return ; //表明本次遍歷沒有交換,則表已經有序
    }
}

3.2 快速排序

  快速排序的核心在於劃分操作,它的效能也取決於劃分操作的好壞,快排是所有內部排序演算法中平均效能最好的。它的特點如下:

  • 時間效率: 快排的執行時間與劃分是否對稱有關,平均時間複雜度為O(nlgn),最壞為O(n^2)

  • 為了提高演算法效率: 在元素基本有序時採取直接插入排序(STL就這麼幹,給定一個閾值)

  • 為了保證劃分的平衡性,例如三數取中位數或者隨機選擇pivot元素甚至Tukey ninther選取策略,這樣使得最壞情況在實際情形中不太可能發生

  • 空間效率: 採用遞迴工作棧,空間複雜度最好情況下為O(lgn),最壞為O(n)

  • 穩定性: 在劃分演算法中,若右側有兩個相同的元素,在小於pivot時交換到左側相對位置就會發生變化,表明快排是一種不穩定排序,但每趟排序能將一個元素位置確定下來(pivot)。注意使用有些劃分演算法能夠保證區域性穩定性

  快速排序的主框架如下:

void quick_sort_int(int *arr,int left,int right) {
    if(left < right) {
        int pos = partition1(arr,left,right); //劃分
        quick_sort_int(arr,left,pos - 1);
        quick_sort_int(arr,pos + 1,right);
    }
}

3.2.1 首尾指標向中間掃描法

  兩指標分別從首尾向中間掃描。這裡主要考慮的是選取pivot的策略,可能是首元素或尾元素,甚至是三數取中或者隨機取某個數。思路是將資料以pivot作劃分,右側小於pivot移到左側,左側大於pivot移到右側

  此方法可以用來確定第k大或第k小的元素。

int partition(int *arr,int left,int right) {
    int pivot = arr[left];
    while(left < right) {
        while(left < right && pivot <= arr[right])
            right--;
        arr[left] = arr[right]; //比pivot值小的移到左端
        while(left < right && pivot >= arr[left])
            left++;
        arr[right] = arr[left]; //比pivot值大的移到右端
    }
    arr[left] = pivot; //left == right
    return left;
}

  其實還有一種交換策略: 把左右端不滿足條件的元素交換,最後迴圈終止的元素應該與開始元素進行交換。這種方法結合前後兩個策略: 既雙向遍歷又使用了交換操作。

 

int partition1(int *arr,int left,int right) {
    int start = left;
    int pivot = arr[left];
    while(left < right) {
        while(left < right && pivot <= arr[right])
            right--;
        while(left < right && pivot >= arr[left])
            left++;
        swap(&arr[left],&arr[right]); 
    }
    swap(&arr[start],&arr[left]);
    return left;
}

3.2.2 一前一後指標向後掃描法

  採用單向遍歷: 兩指標索引一前一後逐步向後掃描。它的策略是: 記錄一個last指標,用於儲存小於或等於pivot的元素。從左往右掃描,每遇見一個元素小於等於pivot即將它儲存於last指標裡,所以一趟劃分過後last之前的元素(包括last,這取決於last的初始化)小於等於pivot。

  可以看出該劃分演算法的特點: 從前向後遍歷,last記錄的是小於等於pivot的元素,這些元素相對順序不變。若要大於等於pivot的元素相對順序不變,可從後向前遍歷,pivot取首元素。

int partition2(int *arr,int left,int right) {
    int pivot = arr[right];
    int last = left -1;
    for(int i = left; i < right; i++) {
        if(arr[i] <= pivot) {
            ++last;
            swap(&arr[last],&arr[i]);
        }
    }
    swap(&arr[last + 1],&arr[right]);
    return last + 1;
}

  如果pivot不方便取,例如Dijkstra提出的荷蘭三色旗問題要保證0,1,2三者有序,使用單純的單向遍歷會存在0和1順序亂的情況,並且元素的重複次數比較多,所以Dijkstra提出了一種簡單快速的三向劃分法。

3.2.3 三向劃分法

  Dijkstra三向快速切分: 用於處理有大量重複元素的情形,減少遞迴時重複元素的比較的次數。即遍歷陣列一次,維護三個指標lt、cur、gt

  • lt使得arr[0..lt-1]的元素小於v
  • gt使得arr[gt+1..n-1]的元素大於v
  • cur使得arr[lt..i-1]的元素等於v,arr[i..gt]的元素仍不確定

  這和荷蘭三色旗是類似的問題,思路比較簡單。要學會掌握用一個工作指標結合兩個邊界指標進行一次線性遍歷,同樣有很多變式。完整的三向劃分快速排序程式碼如下:

void quick_sort_threeway(int *arr,int left,int right) {
    if(left < right) {
        int lt = left;
        int cur = left;
        int gt = right;
        int pivot = arr[left];
        while(cur <= gt) {
            if(arr[cur] == pivot) {
                cur++;
            } else if(arr[cur] < pivot) {
                swap(&arr[cur],&arr[lt]);
                lt++;
                cur++;
            } else {
                swap(&arr[cur],&arr[gt]);
                gt--;
            }
        } //cur > gt則退出,直接形成了left..lt-1 lt..gt gt+1..right三個區間
        quick_sort_threeway(arr,left,lt - 1);
        quick_sort_threeway(arr,gt + 1,right);
    }
    
}

  不過這種方法唯一的缺點就是在陣列中重複元素不多的情況下比標準的二分法多使用了很多次交換。90年代,John Bently等人用一個聰明的方法解決了此問題,使得三向切分的快排比一般的排序方法都要快。

3.2.4 快速三向劃分法

  Bently用重複元素放置於子陣列兩端的方式實現了一個資訊量最優的演算法,這裡額外的交換隻用於和切分的pivot元素相等的元素,上面的額外交換是用於切分不相等的元素。

  思路如下:

  • 維護兩個索引p、q,使得arr[lo..p-1]和arr[q+1..hi]的元素都和 a[lo]相等(記為v)
  • 使用兩個索引i、j,使得a[p..i-1]小於v,a[j+1..q]大於v。
  • 若在前面遇到等於v的元素與p交換,後面的與q交換(類似操作)

  如圖:

資料結構與演算法知識點總結(4)各類排序演算法

  它的程式碼如下:

void quick_sort_threeway_fast(int *arr,int left,int right) {
    if(left < right) {
        int p = left , q = right + 1; 
        int pivot = arr[left];
        int i = left , j = right + 1;
        while(true) {
            while(arr[++i] < pivot) 
                if(i == right) 
                    break;
            while(arr[--j] > pivot) 
                if(j == left)
                    break;

            if(i == j && arr[i] == pivot)
                swap(&arr[++p],&arr[i]);
            if(i >= j) break;

            swap(&arr[i],&arr[j]);
            if(arr[i] == pivot)
                swap(&arr[++p],&arr[i]);
            if(arr[j] == pivot)
                swap(&arr[--q],&arr[j]);
        }
        i = j + 1;
        for(int k = left; k <= p; k++) swap(&arr[k],&arr[j--]);
        for(int k = right; k >= q;k--) swap(&arr[k],&arr[i++]);
        quick_sort_threeway_fast(arr,left,j);
        quick_sort_threeway_fast(arr,i,right);
    }
}

3.3 最優化的排序演算法

  在元素個數比較小的時候使用直接插入排序,元素個數較多的適合主要在於pivot元素的選取策略。程式碼如下:

/**
 * 三數取中策略: 返回陣列的下標
 */
int median3(int *arr,int i,int j,int k) {
    if(arr[i] < arr[j]) {
        if(arr[j] < arr[k])
            return j;
        else {
            return (arr[i] < arr[k])? k : i;
        }
    } else {
        if(arr[k] < arr[j])
            return j;
        else {
            return (arr[k] < arr[i])? k : i;
        }
    }
}

void optimal_sort_int(int *arr,int left,int right) {
    int n = right - left + 1;
    if(n <= THRESHOLD) {
        insertion_sort_int(arr,n);
    } else if(n <= 500) { //用三數取中排序
        int pos = median3(arr,left, left + n / 2,right);
        swap(&arr[pos],&arr[left]);
    } else {//採取Tukey ninther 作為pivot元素
        int eighth = n / 8;
        int mid = left + n / 2;
        int m1 = median3(arr,left,left + eighth,left + eighth * 2);
        int m2 = median3(arr,mid - eighth,mid,mid + eighth);
        int m3 = median3(arr,right - eighth * 2,right - eighth,right);
        int ninther = median3(arr,m1,m2,m3);
        swap(&arr[ninther],&arr[left]);
    }
    quick_sort_threeway_fast(arr,left,right);
}

4. 選擇排序

4.1 簡單選擇排序

  選擇排序的基本思路: 每一趟要在後面的元素中確定一個最值元素,重複n - 1趟。它的特點如下:

  • 時間效率: 注意元素的比較次數與初始序列無關,始終要比較n(n-1)/2,時間複雜度為O(n^2)
  • 空間效率為O(1)
  • 穩定性:不穩定排序,例如6 8 6 5
void selection_sort_int(int *arr,int n) {
    int min;
    for(int i = 0; i < n - 1;i++) {//n-1趟選擇排序,每次選擇一個最小值
        min = i;
        for(int j = i + 1; j < n;j++) {
            if(arr[min] < arr[j]) {
                min = j; //更新最小元素位置
            }
        }
        if(min != i) swap(&arr[i],&arr[min]); //更新到的最小值與i位置交換
    }
}

4.2 堆排序和優先順序佇列

  堆排序是一種樹形選擇排序,利用完全二叉樹中父節點和子節點的關係選擇最值的元素。排序的步驟如下:

  • 先構造大根堆(這也是一個反覆向下調整滿足最大堆性質的操作)
  • 然後再把堆頂元素與堆底元素交換,此時根結點不再滿足堆的性質,堆頂元素向下調整重複操作,直到堆中剩一個元素為止,要進行n-1趟交換和調整操作

  堆排序的特點是:

  • 時空效率: 在最好、平均和最壞情況下,堆排序的時間複雜度均為O(nlgn);空間複雜度: O(1)

  • 穩定性: 不穩定排序

  堆排序的主程式如下(注意0號不儲存元素,實際儲存從1開始,陣列長度為len + 1):

void heap_sort_int(int *arr,int len) {
    build_max_heap(arr,len);
    for(int i = len; i > 1;i--) {
        swap(&arr[i],&arr[1]); //和堆頂元素交換,並輸出堆頂元素
        sink_down(arr,1,i - 1); //注意還剩餘i-1個元素調整堆
    }
}

4.2.1 堆調整操作

  A 下標為k的堆的自頂向下操作

  這種操作是為了保持根為下標k的元素的子樹滿足最大堆性質。如果某個節點比它們的兩個子節點或之一更小,則以該節點為根的子樹不滿足最大堆性質。

  調整思路: 把更小的元素由上至下下沉,以類似的方式維持其子節點的堆狀態性質直到某子節點元素滿足最大堆的狀態。向下調整的時間與樹高有關為O(h)

  此操作用於構造堆,堆排序和刪除操作(delMax),程式碼如下:

void sink_down(int *arr,int k,int len) {
    while(2 * k <= len) {
        int j = 2 * k;
        if(j <len && arr[j] < arr[j + 1]) j++; //取更大的元素
        if(arr[k] > arr[j]) break; //根元素大於子節點,則滿足最大堆性質無需調整
        swap(&arr[k],&arr[j]);
        k = j;
    }
}

B 下標為k的堆自底向上操作

  這種操作是因為該節點的大小比其父節點更大,則需要進行向上調整,注意終止條件是k=1且父節點更大。

  該操作用於在堆底插入新元素

void swim_up(int *arr,int k) {
    while(k > 1 && arr[k] > arr[k / 2]) {
        swap(&arr[k],&arr[k / 2]);
        k = k / 2;
    }
}

  總結起來堆和優先順序佇列的插入、刪除和取最值還是比較簡單的,關鍵還是在於這兩個堆調整操作,程式碼量不大,但需要理解其執行邏輯。

4.2.2 構造堆操作

  以無序陣列自底向上構造出一個最大堆,時間複雜度為O(n),實際儲存從下標1開始,陣列長度為len + 1。從n=len/2開始進行自頂向下調整操作

void build_max_heap(int *arr,int len) {
    for(int i = len / 2; i >= 1;i--)
        sink_down(arr,i,len);
}

  大根堆一般可用於求海量資料中最小的k個數: 即先讀取k個元素構造最大堆,再依次讀入資料。若當前資料比堆頂小,則替換堆頂;若當前資料比較大,不可能是最小的k個數 。對應地求最大的k個數一般用小根堆。

  如下分別是用快排的劃分演算法和堆的性質取得最小的k個數

void print(int *arr,int left,int right) {
    for(int i = left; i<= right;i++)
        printf("%d ",arr[i]);
    printf("\n");
}

/*基於partition演算法取最小的k個數*/
void getleastk(int *arr,int n,int k) {
     int left = 0;
    int right = n - 1;
    int pos = partition(arr,left,right);
    while(pos != k - 1) {
        if(pos > k - 1) {
            right = pos - 1;
            pos = partition(arr,left,right);
        } else {
            left = pos + 1;
            pos = partition(arr,left,right);
        }
    }
    print(arr,0,k-1);
}

/*基於最大堆求最小的k個數*/
void getmink(int *arr,int n,int k) {
    int b[k+1];
    for(int i = 1; i <= k;i++) {
            b[i] = arr[i - 1];        
    }

    build_max_heap(b,k);
    for(int i = k; i < n;i++) {
        if(arr[i] > b[1])
            continue;
        else {
            b[1] = arr[i];
            sink_down(b,1,k);
        }
    }
    heap_sort_int(b,k);
    print(b,1,k);
}

5. 線性時間排序

5.1 計數排序

  計數排序的核心在於將輸入的資料值轉化為鍵儲存在額外開闢的陣列空間中。作為一種線性時間複雜度的排序,計數排序要求輸入的資料必須是有確定範圍的整數。

  當輸入的元素是 n 個 0 到 k 之間的整數時,它的執行時間是 Θ(n + k)。計數排序不是比較排序,排序的速度快於任何比較排序演算法。

  由於用來計數的陣列C的長度取決於待排序陣列中資料的範圍(等於待排序陣列的最大值與最小值的差加上1),這使得計數排序對於資料範圍很大的陣列,需要大量時間和記憶體。例如:計數排序是用來排序0到100之間的數字的最好的演算法,但是它不適合按字母順序排序人名。但是,計數排序可以用在基數排序中的演算法來排序資料範圍很大的陣列。

  通俗地理解,例如有 10 個年齡不同的人,統計出有 8 個人的年齡比 A 小,那 A 的年齡就排在第 9 位,用這個方法可以得到其他每個人的位置,也就排好了序。當然,年齡有重複時需要特殊處理(保證穩定性),這就是為什麼最後要反向填充目標陣列,以及將每個數字的統計減去 1 的原因。

   演算法的步驟如下:

  • (1)找出待排序的陣列中最大和最小的元素
  • (2)統計陣列中每個值為i的元素出現的次數,存入陣列C的第i項
  • (3)對所有的計數累加(從C中的第一個元素開始,每一項和前一項相加)
  • (4)反向填充目標陣列:將每個元素i放在新陣列的第C(i)項,每放一個元素就將C(i)減去1
public class CountingSort implements IArraySort {

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 對 arr 進行拷貝,不改變引數內容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        int maxValue = getMaxValue(arr);

        return countingSort(arr, maxValue);
    }

    private int[] countingSort(int[] arr, int maxValue) {
        int bucketLen = maxValue + 1;
        int[] bucket = new int[bucketLen];

        for (int value : arr) {
            bucket[value]++;
        }

        int sortedIndex = 0;
        for (int j = 0; j < bucketLen; j++) {
            while (bucket[j] > 0) {
                arr[sortedIndex++] = j;
                bucket[j]--;
            }
        }
        return arr;
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }

}

5.2 基數排序

  基數排序是一種非比較型整數排序演算法,其原理是將整數按位數切割成不同的數字,然後按每個位數分別比較。由於整數也可以表達字串(比如名字或日期)和特定格式的浮點數,所以基數排序也不是隻能使用於整數。

  基數排序有兩種方法:

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

  1. 基數排序:根據鍵值的每位數字來分配桶;
  2. 計數排序:每個桶只儲存單一鍵值;
  3. 桶排序:每個桶儲存一定範圍的數值;
public class RadixSort implements IArraySort {

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 對 arr 進行拷貝,不改變引數內容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        int maxDigit = getMaxDigit(arr);
        return radixSort(arr, maxDigit);
    }

    /**
     * 獲取最高位數
     */
    private int getMaxDigit(int[] arr) {
        int maxValue = getMaxValue(arr);
        return getNumLenght(maxValue);
    }

    private int getMaxValue(int[] arr) {
        int maxValue = arr[0];
        for (int value : arr) {
            if (maxValue < value) {
                maxValue = value;
            }
        }
        return maxValue;
    }

    protected int getNumLenght(long num) {
        if (num == 0) {
            return 1;
        }
        int lenght = 0;
        for (long temp = num; temp != 0; temp /= 10) {
            lenght++;
        }
        return lenght;
    }

    private int[] radixSort(int[] arr, int maxDigit) {
        int mod = 10;
        int dev = 1;

        for (int i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
            // 考慮負數的情況,這裡擴充套件一倍佇列數,其中 [0-9]對應負數,[10-19]對應正數 (bucket + 10)
            int[][] counter = new int[mod * 2][0];

            for (int j = 0; j < arr.length; j++) {
                int bucket = ((arr[j] % mod) / dev) + mod;
                counter[bucket] = arrayAppend(counter[bucket], arr[j]);
            }

            int pos = 0;
            for (int[] bucket : counter) {
                for (int value : bucket) {
                    arr[pos++] = value;
                }
            }
        }

        return arr;
    }

    /**
     * 自動擴容,並儲存資料
     *
     * @param arr
     * @param value
     */
    private int[] arrayAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }
}

5.3 桶排序

  桶排序是計數排序的升級版。它利用了函式的對映關係,高效與否的關鍵就在於這個對映函式的確定。為了使桶排序更加高效,我們需要做到這兩點:

  1. 在額外空間充足的情況下,儘量增大桶的數量
  2. 使用的對映函式能夠將輸入的 N 個資料均勻的分配到 K 個桶中

  同時,對於桶中元素的排序,選擇何種比較排序演算法對於效能的影響至關重要。

public class BucketSort implements IArraySort {

    private static final InsertSort insertSort = new InsertSort();

    @Override
    public int[] sort(int[] sourceArray) throws Exception {
        // 對 arr 進行拷貝,不改變引數內容
        int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);

        return bucketSort(arr, 5);
    }

    private int[] bucketSort(int[] arr, int bucketSize) throws Exception {
        if (arr.length == 0) {
            return arr;
        }

        int minValue = arr[0];
        int maxValue = arr[0];
        for (int value : arr) {
            if (value < minValue) {
                minValue = value;
            } else if (value > maxValue) {
                maxValue = value;
            }
        }

        int bucketCount = (int) Math.floor((maxValue - minValue) / bucketSize) + 1;
        int[][] buckets = new int[bucketCount][0];

        // 利用對映函式將資料分配到各個桶中
        for (int i = 0; i < arr.length; i++) {
            int index = (int) Math.floor((arr[i] - minValue) / bucketSize);
            buckets[index] = arrAppend(buckets[index], arr[i]);
        }

        int arrIndex = 0;
        for (int[] bucket : buckets) {
            if (bucket.length <= 0) {
                continue;
            }
            // 對每個桶進行排序,這裡使用了插入排序
            bucket = insertSort.sort(bucket);
            for (int value : bucket) {
                arr[arrIndex++] = value;
            }
        }

        return arr;
    }

    /**
     * 自動擴容,並儲存資料
     *
     * @param arr
     * @param value
     */
    private int[] arrAppend(int[] arr, int value) {
        arr = Arrays.copyOf(arr, arr.length + 1);
        arr[arr.length - 1] = value;
        return arr;
    }

}

相關文章