從簡單的快速排序說起-Partition-ThreePartition-TopK

tfzh發表於2022-04-30

1. 簡單快速排序

快速排序是一個簡單,易於理解的排序演算法,我們先來看看一個入門級別的快排:

function QuickSort(nums){
    if(nums.length <= 1){
        return nums
    }
    let pivot = nums[random(0, nums.length - 1)]
    let left  = []
    let right = []
    let mid   = []
    for (let i = 0; i < nums.length; i++) {
        if(nums[i] < pivot){
            left.push(nums[i])
        }else if(nums[i] == pivot){
            mid.push(nums[i])
        }else{
            right.push(nums[i])
        }
    }
    return QuickSort(left).concat(mid, QuickSort(right))
}

這種寫法的優點是邏輯非常清晰,很好理解;缺點是每次遞迴都產生了新的陣列,最後再合併陣列,空間複雜度略高

2.原地排序 Partition演算法

Partition演算法是一種原地分割排序的的演算法,選定一個基準值,將每一個小於基準值的交換到左邊,將每一個大於基準值的交換到右邊,最後基準值居中,是一種原地分割的演算法,除了常數級的變數外,沒有使用額外的空間

2.1 公共函式

我們先定義幾個常用的公共函式

//陣列交換
function swap(nums, i ,j){
    tmp = nums[j]
    nums[j] = nums[i]
    nums[i] = tmp
}
//範圍內隨機數
function random(minNum,maxNum){
    return parseInt(Math.random()*(maxNum-minNum+1)+minNum,10)
}

2.2 從左向右一次遍歷-Partition演算法

function Partition(nums, l = 0, r = nums.length - 1) {
    //隨機選取一個作為基值值
    let random_i   = random(l, r)
    let pivot      = nums[random_i]
    swap(nums, l, random_i)
    let pos        = l
    for (let i = l + 1; i <= r; i++) {
        if(nums[i] <= pivot){
            pos++
            if(i != pos){
                swap(nums, i, pos)
            }
        }
    }
    swap(nums, l, pos)
    return pos
}

pos是分割線,pos(含本身)右邊都是小於等於基準值的,右邊都是大於基準值的

2.3 雙指標-Partition演算法

function Partition(nums, l = 0, r = nums.length - 1)
{
    let pivot = nums[l]
    while(l < r)
    {
        while(l < r && nums[r] >= pivot){
            r--
        }
        nums[l] = nums[r]
        while(l < r && nums[l] <= pivot){
            l++
        }
        nums[r] = nums[l]
    }
    nums[begin] = pivot;
    return begin;
}

雙指標的演算法比較不太好理解,精妙在沒有使用交換函式,通過pivot暫存了基準值,然後使用基準值,左右端點的關係,完成了分割,感興趣可以打上斷點跑一下看看

3.使用Partition演算法的快排

function QuickSort(nums, l = 0, r = nums.length - 1){
    if(r - l <= 0){
        return nums
    }
    let pos = Partition(nums, l, r)
    QuickSort(nums, l, pos - 1)
    QuickSort(nums, pos + 1, r)
    return nums
}

使用Partition演算法的快排,沒有建立新的陣列,在原陣列上交換,完成了排序

4. Three-Partition演算法

Three-Partition演算法是Partition演算法的延伸,把無序陣列分為3份,小於基準值,等於基準值,大於基準值,非常適合重複資料很多的陣列分割排序

4.1 確定左右邊界的Three-Partition演算法

function ThreePartition(nums, l = 0, r = nums.length - 1) {
    //隨機選取一個作為基準值
    let mid        = random(l, r)
    let pivot      = nums[mid]    
    let p = l//這裡的p就是左邊界,p(含p)左邊都是小於基準值的
    for (let i = l; i <= r; i++) {
        while(i <= r && nums[i] > pivot){
            swap(nums, i, r)//這裡的r就是右邊界,r右邊都是大於基準值的
            r--            
        }
        if(nums[i] < pivot){
            swap(nums, i, p)
            p++           
        }
    }    
    return [p,r]
}

這種演算法我覺得比 4.2確定左中邊界的Three-Partition演算法好理解些,遇到每一個大於基準值的,都交換到右邊,同時右邊界r--;遇到小於基準值的,都交換到左邊,同時左邊界p++,更符合常見思維。

4.2 確定左中邊界的Three-Partition演算法

function ThreePartition(nums, l = 0, r = nums.length - 1) {
    //隨機選取一個作為基準值
    let mid        = random(l, r)
    let pivot      = nums[mid]
    
    let p0 = p1 = l//p0 0的最右邊界   //p1中間值的最右邊界
    for (let i = l; i <= r; i++) {
        if(nums[i] < pivot){
            swap(nums, i, p0)
            //因為首先是連續的左值+連續的基準值+連續的右值
            //如果p1 > p0則會把一個基準值交換到了i,這不是我們期望的
            //這時候我們需要把i和基準值的右邊界p1交換
            if(p0 < p1){
                swap(nums, i, p1)
            }
            p0++
            p1++
        }else if(nums[i] == pivot){
            swap(nums, i, p1)
            p1++
        }
    }
    return [p0,p1-1]
}

這種演算法是基於確定左中邊界,每次都會左值交換到左邊界,把基準值交換到基準值的右邊界,但當基準值的右邊界p1增長過快時,超過p0,此時就需要把i和基準值的右邊界p1交換

5 使用Three-Partition演算法的快排

function QuickSort(nums, l = 0, r = nums.length - 1){
    if(r - l <= 0){
        return nums
    }
    let pos = ThreePartition(nums, l, r)
    QuickSort(nums, l, pos - 1)
    QuickSort(nums, pos + 1, r)
    return nums
}

如果無序陣列裡沒有重複值,那麼完全等價於Partition演算法,如果陣列記憶體在重複值,重複的越多,分佈越集中,此演算法效率越高

6. 使用Partition演算法求解TopK問題

function findKthNumber(nums, k){
    let l = 0;
    let r = nums.length - 1;
    while (l <= r){
        let pos = Partition(nums, l ,r)
        let r_len = r - pos
        let l_len = pos
        if(k == r_len + 1){
            return nums[pos]
        }else if(k <= r_len){
            l = pos + 1
        }else{
            r = pos - 1
            k -= (r_len + 1)
        }
    }
    return 0
 }

效率還是非常高效的

image.png

7. 使用Three-Partition演算法求解TopK問題

function findKthNumber(nums, k){
    let l = 0;
    let r = nums.length - 1;
    while (l <= r){
        let pos = Partition(nums, l ,r)
        let r_len = r - pos
        let l_len = pos
        if(k == r_len + 1){
            return nums[pos]
        }else if(k <= r_len){
            l = pos + 1
        }else{
            r = pos - 1
            k -= (r_len + 1)
        }
    }
    return 0
 }

Three-Partition演算法效率也是非常高效的,分佈越收斂,越集中,效果則越好

image.png

相關文章