天天看點

快速排序 golang快速排序 golang

快速排序 golang

快速排序(英語:Quicksort),又稱劃分交換排序(partition-exchange sort),簡稱快排,一種排序算法,最早由東尼·霍爾提出。在平均狀況下,排序 個項目要(大O符号)次比較。在最壞狀況下則需要次比較,但這種狀況并不常見。

快排應用

  • 快排是一般語言内置排序包中的實作,當然在數組大小不同的情況下會有不同的選擇,但是整體以快排為主,為了防止出現一般采用随機選擇中間節點的方式來實作。
  • 作為算法題,要求實作。

分治法

分而治之,先将原問題的規模下降,分解為子問題,此所謂“分”,然後解決子問題,此為“治”。

分治法的基本思想是将一個規模為n的原問題分解為k個規模較小的子問題,這些子問題互相獨立且與原問題相同。遞歸地解這些子問題,然後将子問題的解合并為原問題的解。
           

算法描述

步驟為:

  1. 從數列中挑出一個元素,稱為”基準”(pivot),
  2. 重新排序數列,所有比基準值小的元素擺放在基準前面,所有比基準值大的元素擺在基準後面(相同的數可以到任何一邊)。在這個分區結束之後,該基準就處于數列的中間位置。這個稱為分區(partition)操作。
  3. 遞歸地(recursively)把小于基準值元素的子數列和大于基準值元素的子數列排序。

遞歸到最底部時,數列的大小是零或一,也就是已經排序好了。這個算法一定會結束,因為在每次的疊代(iteration)中,它至少會把一個元素擺到它最後的位置去。

一般采用的都是原地排序版本,這樣不需要配置設定額外的空間,對速度有提升。

僞代碼如下:

procedure quicksort(a, left, right)
     if right > left
         select a pivot value a[pivotIndex]
         pivotNewIndex := partition(a, left, right, pivotIndex)
         quicksort(a, left, pivotNewIndex-1)
         quicksort(a, pivotNewIndex+1, right)
           

實作

單路快排

遞歸版本實作

  1. 首先通過随機數選取比較的點
  2. 周遊 head + 1-> tail
    • 大于mid 則和tail交換 并且tail–
    • 小于等于 則交換head 并且head++ i++

疑問:

  • 為什麼在大于的情況下 index i 沒有移位?

因為交換後的tail,并沒有保證一定會大于mid元素,是以需要再次進行比較

  • 為什麼在小于的情況下 index i 需要移位?

因為第一次交換的時候是arr[0] -> mid,是以交換後一定滿足arr[i] <= mid,是以可以移位操作。否則周遊無法結束

func QSortRecursion(arr []int){
    arrLen := len(arr)
    if arrLen <= 1{
        return
    }

    randNum := getRandNum(arrLen -1)
    arr[randNum], arr[0] = arr[0],arr[randNum]
    mid := arr[0]//取出用于比較的元素

    head, tail := 0, arrLen -1

    for i := 1; i <= tail;{// 從 head + 1 到 tail 周遊 case 大于比較元素 換 else 小于等于 換 并且head++ 維護左邊的數組
        if arr[i] > mid{
            arr[i], arr[tail] = arr[tail], arr[i]
            tail--
        }else{
            arr[i], arr[head] = arr[head], arr[i]
            head++
            i++
        }
    }

    QSortRecursion(arr[:head])
    QSortRecursion(arr[head+1:])
}

func getRandNum(totalNum int)int{
    rand.Seed(time.Now().Unix())// 設定随機種子
    return rand.Intn(totalNum)
}
           

雙路快排

上面的實作方法是單路快排,常用的我們會用雙路快排的方式,更加快。

可以看到當資料比較多重複的時候,單路快排會有非常備援的swap操作,雙路快排避免了這一點。

/*
    雙路快排
    從左向右找到大于pivot的元素
    從右向左找小于pivot的元素
    判斷是否越界 交換元素 head++ tail--
*/
func QSortTwoWay(arr []int){
    arrLen := len(arr)
    if arrLen <= 1{
        return
    }

    randNum := getRandNum(arrLen -1)
    arr[randNum], arr[0] = arr[0],arr[randNum]
    mid := arr[0]//取出用于比較的元素

    head, tail := 0, arrLen -1

    for {
        for head <= tail && arr[head] < mid{
            head ++
        }

        for tail > head && arr[tail] > mid{
            tail --
        }

        if head > tail{
            break
        }

        swap(arr, head,tail)
        head++
        tail--
    }

    QSortTwoWay(arr[:head])
    QSortTwoWay(arr[head+1:])
}
           

三路快排

當遇到非常多重複元素的時候,會使用三路快排的方式解決。

快速排序 golang快速排序 golang

三路快排複雜的地方就在于對指定的變量的描述以及邊界問題上。

快速排序 golang快速排序 golang

由于時間關系暫時來一版c語言實作 詳細資訊情況下文參看連結

private void quickSort(int[] a, int left, int right) {
    if (right <= left)
        return;

    /* 
     * 工作指針
     * p指向序列左邊等于pivot元素的位置
     * q指向序列右邊等于Pivot元素的位置
     * i指向從左向右掃面時的元素
     * j指向從右向左掃描時的元素
     */
    int p, q, i, j;
    int pivot;// 錨點
    i = p = left;
    j = q = right - 1;
    /*
     * 每次總是取序列最右邊的元素為錨點
     */
    pivot = a[right];
    while (true) {
        /*
         * 工作指針i從右向左不斷掃描,找小于或者等于錨點元素的元素
         */
        while (i < right && a[i] <= pivot) {
            /*
             * 找到與錨點元素相等的元素将其交換到p所訓示的位置
             */
            if (a[i] == pivot) {
                swap(a, i, p);
                p++;
            }
            i++;
        }
        /*
         * 工作指針j從左向右不斷掃描,找大于或者等于錨點元素的元素
         */
        while (left <= j && a[j] >= pivot) {
            /*
             * 找到與錨點元素相等的元素将其交換到q所訓示的位置
             */
            if (a[j] == pivot) {
                swap(a, j, q);
                q--;
            }
            j--;
        }
        /*
         * 如果兩個工作指針i j相遇則一趟周遊結束
         */
        if (i >= j)
            break;

        /*
         * 将左邊大于pivot的元素與右邊小于pivot元素進行交換
         */
        swap(a, i, j);
        i++;
        j--;
    }
    /*
     * 因為工作指針i指向的是目前需要處理元素的下一個元素
     * 故而需要退回到目前元素的實際位置,然後将等于pivot元素交換到序列中間
     */
    i--;
    p--;
    while (p >= left) {
        swap(a, i, p);
        i--;
        p--;
    }
    /*
     * 因為工作指針j指向的是目前需要處理元素的上一個元素
     * 故而需要退回到目前元素的實際位置,然後将等于pivot元素交換到序列中間
     */
    j++;
    q++;
    while (q <= right) {
        swap(a, j, q);
        j++;
        q++;
    }

    /*
     * 遞歸周遊左右子序列
     */
    quickSort(a, left, i);
    quickSort(a, j, right);
}
           

參考:維基百科

三路快排

繼續閱讀