排序雙雄:歸并排序與快速排序

1.歸并排序
核心思想:先把數(shù)組從中間分成前后兩部分,然后對(duì)前后兩部分分別排序忿危,再將排好序的兩部分合并在一起达箍,這樣整個(gè)數(shù)組就有序了。使用了分治的處理思想與遞歸的編程技巧铺厨。
其實(shí)歸并排序是先歸再并缎玫,“歸”就是分解,遞歸地將數(shù)組從中間一分為二解滓,直至子數(shù)組長度為1赃磨,這個(gè)過程并不涉及數(shù)組元素之間的比較。而“并”洼裤,則是將被分解的子數(shù)組兩兩合并(從最細(xì)分的長度為1的子數(shù)組開始合并煞躬,而長度為1的數(shù)組可被認(rèn)為就是有序的,這樣每次合并逸邦,子數(shù)組都已經(jīng)是有序的了)恩沛,這個(gè)過程涉及子數(shù)組 A 與子數(shù)組 B 之間的元素比較。

歸并排序分解圖(來源極客幫)

代碼如下:

 // 將兩個(gè)已經(jīng)排好序的子數(shù)組合并
    // left左子數(shù)組缕减,right右子數(shù)組
    const mergeArr=(left,right)=>{
        let temp=[];
        let leftIndex=0;
        let rightIndex=0;
        let leftLen=left.length;
        let rightLen=right.length;
        while(leftIndex<leftLen && rightIndex<rightLen){
            // 判斷兩個(gè)子數(shù)組的元素大小雷客,依次插入到臨時(shí)數(shù)組temp
            // 下面這個(gè)判斷條件使得歸并排序是穩(wěn)定排序
            if(left[leftIndex]<=right[rightIndex]){
                temp.push(left[leftIndex++]);
            }
            else{
                temp.push(right[rightIndex++]);
            }
        }
        // 此時(shí)某一數(shù)組可能還有剩余元素未插入temp
        return temp.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
    }

    const mergeSort=(arr)=>{
        if(arr.length<=1)
            return arr;
        let mid=Math.floor(arr.length/2);
        const left=arr.slice(0,mid);
        const right=arr.slice(mid);
        return mergeArr(mergeSort(left),mergeSort(right));
    }

    console.log(mergeSort([2,8,7,6,1,5,3,7]));//[1, 2, 3, 5, 6, 7, 7, 8]

歸并排序性能:
(1)穩(wěn)定的排序算法,值相等的元素桥狡,在排序前后相對(duì)先后順序不變搅裙。(原因在合并函數(shù) mergeArr 中皱卓,自己看代碼就知道了)。
(2)時(shí)間復(fù)雜度:O(nlogn)部逮,歸并排序的執(zhí)行效率與要排序的原始數(shù)組的有序程度無關(guān)娜汁,所以其時(shí)間復(fù)雜度是非常穩(wěn)定的,不管是最好情況兄朋、最壞情況掐禁,還是平均情況,都是O(nlogn)颅和。(這里的時(shí)間復(fù)雜度分析涉及數(shù)學(xué)公式推導(dǎo)傅事,很有代表性,具體見極客幫王爭老師的課程)峡扩。
(3)空間復(fù)雜度:歸并排序不是原地排序蹭越,時(shí)間復(fù)雜度為 O(n)。(合并兩個(gè)有序數(shù)組為一個(gè)有序數(shù)組時(shí)教届,需要借助額外的存儲(chǔ)空間响鹃,即代碼中的 temp 臨時(shí)數(shù)組,長度不會(huì)超過原始數(shù)組長度案训,且每個(gè)合并操作結(jié)束买置,自動(dòng)回收 temp 占用的內(nèi)存,下次合并再新建 temp 數(shù)組萤衰,所以占用的額外空間并不會(huì)累加)。

2. 快速排序:待排序數(shù)組下標(biāo)從 p 到 r猜旬,選擇 p 到 r 之間的一個(gè)元素作為分區(qū)點(diǎn)(pivot)脆栋。然后遍歷數(shù)組元素,將小于 pivot 的元素放到左邊洒擦,將大于 pivot 的元素放到右邊椿争。一次遍歷結(jié)束,數(shù)組就被分成了3個(gè)部分:(1)p 到 q-1 之間全小于 pivot熟嫩;
(2)處于 q 位置處的 pivot秦踪;
(3)q+1 到 r 之間全大于 pivot。

快排分區(qū)示意圖--源自極客幫

根據(jù)分治掸茅、遞歸的處理思想椅邓,我們可以按照以上操作遞歸排序下標(biāo)從 p 到 q-1 之間的數(shù)據(jù)和下標(biāo)從 q+1 到 r 之間的數(shù)據(jù),直到區(qū)間長度縮小為 1昧狮,就說明所有的數(shù)據(jù)都有序了景馁。
代碼如下:

     // 獲取 pivot 交換完后的index
    const partition = (arr, pivot, left, right) => {
        //注意 pivot 與 startIndex,不能取區(qū)間同一端點(diǎn)逗鸣,而是一個(gè)取開頭合住,另一個(gè)就取結(jié)尾
        const pivotVal = arr[pivot]
        let startIndex = left
        for (let i = left; i < right; i++) {
            if (arr[i] < pivotVal) {
                [arr[i],arr[startIndex]]=[arr[startIndex],arr[i]]
                startIndex++
            }
        }
        [arr[startIndex],arr[pivot]]=[arr[pivot],arr[startIndex]]
        return startIndex
    }

    const quickSort = (arr, begin, end) => {
        if(begin>=end){
            return ;
        }
        let pivotIndex=partition(arr,end,begin,end);
        quickSort(arr,begin,pivotIndex-1);
        quickSort(arr,pivotIndex+1,end);
        return arr;
}

這里補(bǔ)充一個(gè)借助雙指針確定pivot位置的分區(qū)函數(shù)绰精,partition2。參考

 //方法二 借助雙指針
    const partition2=(arr,pivot,left,right)=>{
        const pivotVal = arr[pivot];//pivot是選取的基準(zhǔn)原始下標(biāo)
        let l=left;//左指針
        let r=right;//右指針
        //左右指針相遇的時(shí)候退出掃描循環(huán)
        while(l < r) {
            //注意透葛,如果基準(zhǔn)選子區(qū)間最后一個(gè)元素笨使,那么先從左指針開始掃描
            // 如果基準(zhǔn)選子區(qū)間第一個(gè)元素,就從右指針開始掃描
            //左指針從左向右掃描僚害,碰到第一個(gè)大于基準(zhǔn)數(shù)的時(shí)候停住
            while(l < r && arr[l] <= pivotVal){
                l ++;
            }
            //右指針從右向左掃描硫椰,碰到第一個(gè)小于基準(zhǔn)數(shù)的時(shí)候停住
            while(l < r && arr[r] >= pivotVal){
                r --;
            }
            //交換左右指針?biāo)N恢玫臄?shù)
            [arr[l], arr[r]] = [arr[r], arr[l]];
        }
        //最后交換基準(zhǔn)數(shù)與指針相遇位置的數(shù)
        [arr[pivot], arr[l]] = [arr[l], arr[pivot]];
        return l;
    }

上面兩個(gè)分區(qū)函數(shù)基準(zhǔn)值選取的都是區(qū)間端點(diǎn),不過要注意若基準(zhǔn)取右端點(diǎn)贡珊,那么 partition1 中的 startIndex 取左端點(diǎn)最爬,然后向右掃描;partition2 則先從左指針開始向右掃描门岔。這個(gè)順序不能錯(cuò)爱致,否則結(jié)果不對(duì)。
上述代碼中比較核心的就是 partition函數(shù)寒随,它用來獲取 pivot 在數(shù)組中的下標(biāo)糠悯,確保左邊元素均小于它,右邊元素均大于它妻往。由于 partition 函數(shù)設(shè)計(jì)巧妙互艾,只涉及部分?jǐn)?shù)組元素的交換,并沒有占用額外的內(nèi)存讯泣,所以快排是原地排序纫普。
不過由于在 partition 中涉及交換操作,比如數(shù)組 6,8,7,6,2,4好渠,若取 4 為 pivot昨稼,那么第一次分區(qū)操作完畢,兩個(gè) 6 的相對(duì)前后順序會(huì)改變拳锚,所以快排并不是穩(wěn)定排序假栓。
快排的時(shí)間復(fù)雜度較為麻煩,如果每次分區(qū)的操作結(jié)果都能將數(shù)組分成長度接近相等的兩個(gè)小區(qū)間(對(duì)應(yīng)最好情況復(fù)雜度)霍掺,那快排的時(shí)間復(fù)雜度就和歸并排序相同匾荆,為 O(nlogn)。
然而杆烁,分區(qū)結(jié)果很難如此均勻牙丽。一個(gè)極端的例子,就是原先已經(jīng)完全有序的數(shù)組1,3,5,7,9兔魂,如果每次都選區(qū)間最后一個(gè)元素作為 pivot 剩岳,那每次分區(qū)都極不平衡。如果是對(duì) 長度為 n 的完全有序數(shù)組進(jìn)行同樣處理入热,那約需 n 次分區(qū)操作拍棕,每次分區(qū)平均處理約 n/2 個(gè)元素晓铆,這時(shí)快排的時(shí)間復(fù)雜度(最壞)退化為 O(n平方)。
利用遞歸樹求解快排時(shí)間復(fù)雜度绰播,其在大部分情況下的時(shí)間復(fù)雜度為 O(nlogn)骄噪,極端情況下退化到 O(n平方)。
快排的優(yōu)化就是盡量使得分區(qū)結(jié)果均勻蠢箩,即分區(qū)點(diǎn)前后兩個(gè)子區(qū)間元素?cái)?shù)量差不多链蕊,避免時(shí)間復(fù)雜度退化為 O(n平方)。
兩個(gè)常見快排優(yōu)化方法:
(1)三數(shù)取中法:從區(qū)間的首谬泌、尾滔韵、中間,分別取出一個(gè)數(shù)掌实,然后對(duì)比大小陪蜻,取這 3 個(gè)數(shù)的中間值作為分區(qū)點(diǎn)。這樣每間隔某個(gè)固定的長度贱鼻,取數(shù)據(jù)出來比較宴卖,將中間值作為分區(qū)點(diǎn)的分區(qū)算法,肯定要比單純?nèi)∧骋粋€(gè)數(shù)據(jù)更好邻悬。如果要排序的數(shù)組長度很大症昏,那么就要多取幾個(gè)數(shù),比如“5數(shù)取中”父丰、“十?dāng)?shù)取中”肝谭。
(2)隨機(jī)法:每次從要排序的區(qū)間中,隨機(jī)選擇一個(gè)元素作為分區(qū)點(diǎn)蛾扇。這種方法并不能保證每次分區(qū)點(diǎn)都選的比較好攘烛,但是從概率的角度來看,也不大可能會(huì)出現(xiàn)每次分區(qū)點(diǎn)都選的很差的情況屁桑,所以平均情況下医寿,這樣選的分區(qū)點(diǎn)是比較好的栏赴。

快排通過與基準(zhǔn)值比較遞歸地將區(qū)間分為左右子區(qū)間蘑斧,這個(gè)過程中,左子區(qū)間可視為左子樹须眷,右子區(qū)間可視為右子樹竖瘾,而基準(zhǔn)可視為根節(jié)點(diǎn)。因而可以利用快排的思想來構(gòu)建二叉搜索樹(Binary Search Tree)花颗。
代碼如下:

 //構(gòu)建BST
    const createBST=function (arr,begin,end) {
        if(begin>end){
            return;
        }
        let pivotIndex=partition(arr,end,begin,end);
        let root=new Node(arr[pivotIndex]);
        root.leftChild=createBST(arr,begin,pivotIndex-1);
        root.rightChild=createBST(arr,pivotIndex+1,end);
        return root;
    }

利用快排還可以實(shí)現(xiàn)在 O(n) 的時(shí)間復(fù)雜度內(nèi)求無序數(shù)組的第 k 大元素捕传。(k 為正整數(shù))
我們選擇數(shù)組區(qū)間 A[0…n-1] 的最后一個(gè)元素 A[n-1] 作為 pivot,對(duì)數(shù)組 A[0…n-1] 原地分區(qū)扩劝, 將數(shù)組分成三部分庸论,A[0…p-1]职辅、A[p]、A[p+1…n-1]聂示。如果 p+1=K域携,那 A[p] 就是要求解的元素;如果 K>p+1, 說明第 K 大元素出現(xiàn)在 A[p+1…n-1] 區(qū)間鱼喉,再按照上面的思路遞歸地在 A[p+1…n-1] 這個(gè)區(qū)間內(nèi)查找秀鞭。同理,如果 K < p+1扛禽,那么就在區(qū)間 A[0,...,p-1]查找锋边。
代碼如下,其中用到了上段代碼的 partition 分區(qū)函數(shù)编曼,另外以下代碼中由于遞歸分區(qū)豆巨,每次得到的分區(qū)點(diǎn)下標(biāo)是相對(duì)于子區(qū)間的,而k是對(duì)于原始整體數(shù)組而言的灵巧,所以將子區(qū)間的分區(qū)點(diǎn)下標(biāo)與 k 比較時(shí)搀矫,加了 base 這個(gè)基礎(chǔ)量。

 // 尋找數(shù)組中的第k大元素(k為正整數(shù)且不大于數(shù)組長度)
    const findKthMax=function (arr, base, k) {
        const len=arr.length;
        let partitionIndex=partition(arr,len-1,0,len-1)+base;
        if(partitionIndex==k-1){
            return arr[partitionIndex];
        }
        else if(k-1>partitionIndex){
            return findKthMax(arr.slice(partitionIndex),partitionIndex,k);
        }
        else{
            return findKthMax(arr.slice(0,partitionIndex),base,k);
        }
    }
   const arr=[3,7,5,2,1,8];
   console.log(findKthMax(arr,0,5)); // 7

歸并和快速排序用的都是分治的思想刻肄,代碼都通過遞歸來實(shí)現(xiàn)瓤球,過程非常相似。歸并排序的重點(diǎn)是理解遞推公式和 merge() 合并函數(shù)敏弃,理解快排的重點(diǎn)是理解遞推公式卦羡,還有 partition()分區(qū)函數(shù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末麦到,一起剝皮案震驚了整個(gè)濱河市绿饵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瓶颠,老刑警劉巖拟赊,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異粹淋,居然都是意外死亡吸祟,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門桃移,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屋匕,“玉大人,你說我怎么就攤上這事借杰」牵” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵蔗衡,是天一觀的道長纤虽。 經(jīng)常有香客問我乳绕,道長,這世上最難降的妖魔是什么逼纸? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任刷袍,我火速辦了婚禮,結(jié)果婚禮上樊展,老公的妹妹穿的比我還像新娘昏兆。我一直安慰自己炫隶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著晃痴,像睡著了一般花竞。 火紅的嫁衣襯著肌膚如雪侮攀。 梳的紋絲不亂的頭發(fā)上倦沧,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音墩弯,去河邊找鬼吩跋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛渔工,可吹牛的內(nèi)容都是我干的锌钮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼引矩,長吁一口氣:“原來是場噩夢啊……” “哼梁丘!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起旺韭,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤氛谜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后区端,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體值漫,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年织盼,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了杨何。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡悔政,死狀恐怖晚吞,靈堂內(nèi)的尸體忽然破棺而出延旧,到底是詐尸還是另有隱情谋国,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布迁沫,位于F島的核電站芦瘾,受9級(jí)特大地震影響捌蚊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜近弟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一缅糟、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧祷愉,春花似錦窗宦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至订讼,卻和暖如春髓窜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背欺殿。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工寄纵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人脖苏。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓程拭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親棍潘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子哺壶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355