0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

以非遞歸的形式來寫快速排序

算法與數(shù)據(jù)結(jié)構(gòu) ? 來源:碼農(nóng)的荒島求生 ? 作者:陸小風(fēng) ? 2022-11-08 17:01 ? 次閱讀

今天給大家講解一道非常有趣的算法面試題,以非遞歸的形式來寫快速排序。

其實這也可以衍生出更多同類問題,非遞歸二叉樹的前序、中序、后序遍歷等等,這些問題的背后的思想是一致的,那就是用棧來手動模擬遞歸調(diào)用。

道理很簡單有沒有,一句話就能說清楚,但問題是你真的理解了嗎?該怎樣用棧來手動模擬遞歸調(diào)用呢?你的大腦在面對這個問題時有一個清晰的思路嗎?

別著急,我們先從最簡單的快排開始。

快排,quick sort

快速排序想必大家都知道,我們以數(shù)組中的某個數(shù)字為基準(zhǔn),通常是數(shù)組的第一個或者最后一個(當(dāng)然也可以是其它選擇方式),這里假設(shè)以數(shù)組的最后一個元素為基準(zhǔn):

7091ae22-5f3e-11ed-8abf-dac502259ad0.png

然后將數(shù)組中小于該基準(zhǔn)的數(shù)字放在左邊、將大于該數(shù)字的放在右邊:

70afb958-5f3e-11ed-8abf-dac502259ad0.png

經(jīng)過這一次處理后base就被放到了最終的位置上并得到了兩個子數(shù)組:base左邊的數(shù)組和base右邊的數(shù)組,以同樣的方式處理這兩個子數(shù)組即可。

用代碼表示就是這樣:

voidquick_sort(vector&arr,intb,inte){
if(b>=e)return;
inti=b-1;
for(intk=b;k

其中參數(shù)中的b和e表示begin和end,也就是范圍。

可以看到,最終使用遞歸的方式編寫的代碼非常簡潔,也很容易理解,遞歸是計算機(jī)科學(xué)中一個極其重要的概念,遞歸對于理解編譯原理、編程語言、分而治之算法思想、排序以及動態(tài)規(guī)劃等等有重要的意義。

遞歸版本很簡單有沒有,如果讓你用非遞歸的方式來實現(xiàn)呢?

非遞歸手寫快速排序

想一想這個問題!如果你真正理解遞歸的話那么就應(yīng)該能寫出來。

我們再來看看這個遞歸寫法。

首先會得到一個問題quick_sort(arr, b, e),我們利用base進(jìn)行一次劃分后得到兩個子問題:

quick_sort(arr, b, i - 1)

quick_sort(arr, i + 1, e)

在遞歸版本中這兩個子問題的狀態(tài)(所謂的狀態(tài)就是要解決哪個子問題,這里用參數(shù)中的begin和end來界定)是隨著函數(shù)的調(diào)用自動保存在棧幀中的,而我們需要用棧這種數(shù)據(jù)結(jié)構(gòu)來模擬這個過程。

接下來,我們用變量task來表示要處理的子問題,也就是說入棧出棧的都是task,task可以這樣定義:

pair

表示要對哪一段數(shù)組進(jìn)行排序,因此使用了pair來記錄這段數(shù)組的開始和結(jié)尾。

由于需要使用棧來追蹤問題的解決順序,因此我們最終這樣定義棧:

stack>tasks;

一切準(zhǔn)備就緒,是時候創(chuàng)建些任務(wù)了,任務(wù)的起源是什么呢?很簡單,就是數(shù)組本身:

intsize=arr.size();
tasks.push(pair(0,size-1));

接下來就是最重要的部分了:

while(!tasks.empty()){
//取出棧頂元素
//處理
//是否有新的子任務(wù)需要push到棧中
}

整體的框架就是這樣,接下來的三個問題就是:

取出棧頂元素

處理

是否有新的子任務(wù)需要push到棧中,如果有則push到棧中

第一個問題很簡單,沒什么可說的;第二個問題是說我們該怎樣處理一個子問題,其實也很簡單,就是用base將數(shù)組劃分為兩個子數(shù)組。

第三個問題是重點,我們該怎么知道接下來是否有新的子任務(wù)需要push到棧中呢?

想一想這個問題。。。

如果用base對數(shù)組進(jìn)行劃分后發(fā)現(xiàn)數(shù)組已經(jīng)是有序的那么就沒有必要創(chuàng)建子任務(wù)了,因為當(dāng)前的數(shù)組已經(jīng)有序了嘛!否則我們就需要創(chuàng)建子任務(wù)。

因此我們必須知道對數(shù)組進(jìn)行劃分后數(shù)組是不是已經(jīng)排好序。

基于上述討論,我們可以這樣實現(xiàn)劃分函數(shù)partition:

intpartition(vector&arr,intb,inte,bool*sorted){
if(b>e||b==e)return-1;

inti=b-1;
for(intj=b;j

這其實和開始遞歸版本中quick_sort函數(shù)里的劃分部分代碼沒什么區(qū)別,變化的部分僅在于我們將一次劃分后base所在的下標(biāo)以及判斷一次劃分后數(shù)組是否有序記錄在參數(shù)sorted中

一次劃分后如果sorted的值為true也就是數(shù)組已經(jīng)有序那么我們無需再創(chuàng)建新的子問題,一次劃分后我們得到兩個新的更小的子問題,即:

boolsorted=true;
intp=partition(arr,top.first,top.second,&sorted);

if(sorted){
continue;
}else{
tasks.push(pair(p+1,top.second));
tasks.push(pair(top.first,p-1));
}

所有問題分析完畢,完整的代碼為:

voidquick_sort(vector&arr){
intsize=arr.size();
if(size==0||size==1)return;
stack>tasks;
tasks.push(pair(0,size-1));

intb=0;

while(!tasks.empty()){
autotop=tasks.top();
tasks.pop();

boolsorted=true;
intp=partition(arr,top.first,top.second,&sorted);

if(sorted){
continue;
}else{
tasks.push(pair(p+1,top.second));
tasks.push(pair(top.first,p-1));
}
}
}

運行一下,it works like magic,有沒有!

這段代碼是怎樣運行的?

No,其實一點都不magic,接下來我們仔細(xì)看看這段代碼是怎么運行的。

假設(shè)當(dāng)前棧頂元素為(2,9),我們獲取棧頂元素,并將其從中pop掉:

70f63d60-5f3e-11ed-8abf-dac502259ad0.png

此時我們要對數(shù)組下標(biāo)2到9的元素進(jìn)行排序,把末尾的base作為基準(zhǔn)進(jìn)行劃分:

71187c36-5f3e-11ed-8abf-dac502259ad0.png

假設(shè)劃分后base放到了下標(biāo)為5的位置,這樣我們得到了兩個子問題(2,3)以及(4,9):

713b77c2-5f3e-11ed-8abf-dac502259ad0.png

由于經(jīng)過base的劃分后我們判斷出該數(shù)組不是有序的(partition函數(shù)中sorted參數(shù)的作用),因此我們需要將兩個子問題(2,3)以及(4,9)放到棧中:

715f5ade-5f3e-11ed-8abf-dac502259ad0.png

就這樣,我們解決了子任務(wù)(2,9),并得到了兩個更小的子問題(2,3)以及(4,9),接著while循環(huán)繼續(xù)從棧中彈出任務(wù)并重復(fù)上述過程,當(dāng)棧為空時我們一定能確信數(shù)據(jù)已經(jīng)有序了。

這個過程“完全”模擬了上述遞歸函數(shù)的調(diào)用,這里之所以加了引號,是因為我們的迭代快排版本進(jìn)行了一點點小小的優(yōu)化,這個優(yōu)化是什么呢?

尾遞歸

依然假設(shè)遞歸調(diào)用到函數(shù)quick_sort(2,9),此時的函數(shù)棧幀為:

717f73e6-5f3e-11ed-8abf-dac502259ad0.png

基于base劃分后依然得到:

713b77c2-5f3e-11ed-8abf-dac502259ad0.png

根據(jù)遞歸版本的quick_sort實現(xiàn)接著我們需要調(diào)用quick_sort(2,3),此時的棧幀為:

71bb9722-5f3e-11ed-8abf-dac502259ad0.png

看到非遞歸版本與遞歸版本的不同了吧:

728a3cbc-5f3e-11ed-8abf-dac502259ad0.png

在非遞歸版本下,對處理子任務(wù)(2,9)時會將該任務(wù)從棧中pop出來,而遞歸版本則不會pop出quick_sort(2,9)的棧幀,函數(shù)quick_sort(2,3)執(zhí)行完后還會再次回到函數(shù)quick_sort(2,9),然后接著調(diào)用函數(shù)quick_sort(4,9)。

而之所以非遞歸實現(xiàn)可以提前將子任務(wù)(2,9)從棧中彈出是因為遞歸版本下所有遞歸調(diào)用都位于函數(shù)的末尾,這就是所謂的“尾遞歸”。

尾遞歸是一種比較常見的現(xiàn)象,二叉樹的前序遍歷遞歸實現(xiàn)也是這樣:

voidtree_travel(Tree*t){
if(t){
print(t->value);
tree_travel(t->left);
tree_travel(t->right);
}
}

你可以使用和本文一樣的套路將上述遞歸代碼轉(zhuǎn)為非遞歸代碼,但是如果是二叉樹的中序遍歷或者后序遍歷呢?

voidtree_travel(Tree*t){
if(t){
tree_travel(t->left);
print(t->value);
tree_travel(t->right);
}
}

此時,本文中講解的套路就失效了,因此我們需要一種更加通用的方法將此類非尾遞歸代碼轉(zhuǎn)為遞歸代碼,這種通用的方法是什么呢?





審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 算法
    +關(guān)注

    關(guān)注

    23

    文章

    4577

    瀏覽量

    92352

原文標(biāo)題:字節(jié)一面:非遞歸手寫快速排序

文章出處:【微信號:TheAlgorithm,微信公眾號:算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    FPGA排序-冒泡排序介紹

    排序算法是圖像處理中經(jīng)常使用一種算法,常見的排序算法有插入排序、希爾排序、選擇排序、冒泡排序、歸
    發(fā)表于 07-17 10:12 ?1018次閱讀
    FPGA<b class='flag-5'>排序</b>-冒泡<b class='flag-5'>排序</b>介紹

    matlab實現(xiàn)快速排序法(原創(chuàng))

    使用快速排序法進(jìn)行排序,本以為很簡單就可以實現(xiàn),但搜索了一下help文檔,只有C中的qsort存在,況且調(diào)用比較麻煩,其實在數(shù)據(jù)結(jié)構(gòu)中,快速排序
    發(fā)表于 08-15 11:33

    matlab快速排序算法實現(xiàn)

    只有C中的qsort存在,調(diào)用比較麻煩,其實在數(shù)據(jù)結(jié)構(gòu)中,快速排序法是經(jīng)典排序之一,上網(wǎng)搜了一下簡介,把對應(yīng)的VC程序改了一下,做成了下面的matlab代碼:%快速
    發(fā)表于 02-29 15:58

    快速排序

    // 快速排序package algorithmsimport "fmt"http:// 第一種寫法func quickSort(values []int, left, right int
    發(fā)表于 10-17 19:05

    簡述計算機(jī)排序

    不用交換,低指針指向的小于樞軸的元素不用交換。直到高低指針指向小于等于或者大于等于的元素后直接交換元素。一趟過后,在低子分區(qū)和高子分區(qū)中繼續(xù)進(jìn)行遞歸快速排序方式。(一種優(yōu)化方法是三位選中,第二種是在最后高低分區(qū)中,high –
    發(fā)表于 12-26 23:07

    《C Primer Plus》讀書筆記——遞歸

    必須包含可以終止遞歸調(diào)用的語句(如if)。尾遞歸最簡單的遞歸形式。把遞歸調(diào)用語句放在函數(shù)結(jié)尾(return語句之前)。舉個栗子: 計算n的階
    發(fā)表于 02-05 20:06

    數(shù)組快速排序及索引vi

    分享一個數(shù)組快速排序及索引的vi,lv里面的某些集成功能vi還是比較好用的,善于調(diào)用的話可以節(jié)約大家的編寫時間。
    發(fā)表于 05-06 09:11

    C#實現(xiàn)快速排序

    快速排序法是對冒泡排序的一種改進(jìn)。它的基本思想是,通過一趟排序將待排序記錄分割成獨立的兩部分,其中一部分記錄的關(guān)鍵字均比另一部分記錄的關(guān)鍵字
    發(fā)表于 08-09 17:57 ?16次下載

    快速排序是一種交換排序

    快速排序在每次分割的過程中,需要 1 個空間存儲基準(zhǔn)值。而快速排序的大概需要 Nlog2N次的分割處理,所以占用空間也是 Nlog2N 個。
    的頭像 發(fā)表于 07-27 14:49 ?2846次閱讀
    <b class='flag-5'>快速</b><b class='flag-5'>排序</b>是一種交換<b class='flag-5'>排序</b>

    C語言排序快速排序的技巧

    快速排序是由東尼·霍爾所發(fā)展的一種排序算法。在平均狀況下,排序 n 個項目要Ο(n log n)次比較。在最壞狀況下則需要Ο(n2)次比較,但這種狀況并不常見。事實上,
    的頭像 發(fā)表于 07-29 15:14 ?2421次閱讀
    C語言<b class='flag-5'>排序</b>中<b class='flag-5'>快速</b><b class='flag-5'>排序</b>的技巧

    所有遞歸代碼都可以轉(zhuǎn)為遞歸代碼

    之所以所有的遞歸都能轉(zhuǎn)為迭代算法是因為遞歸借助函數(shù)調(diào)用,函數(shù)調(diào)用本身就是基于調(diào)用棧這種結(jié)構(gòu)實現(xiàn)的,只不過這一切都是自動完成的,我們當(dāng)然也可以用代碼手動模擬出來。
    的頭像 發(fā)表于 04-19 15:02 ?1993次閱讀

    遞歸代碼都轉(zhuǎn)為遞歸可以嗎

    之所以所有的遞歸都能轉(zhuǎn)為迭代算法是因為遞歸借助函數(shù)調(diào)用,函數(shù)調(diào)用本身就是基于調(diào)用棧這種結(jié)構(gòu)實現(xiàn)的,只不過這一切都是自動完成的,我們當(dāng)然也可以用代碼手動模擬出來。
    的頭像 發(fā)表于 02-17 14:35 ?698次閱讀
    <b class='flag-5'>遞歸</b>代碼都轉(zhuǎn)為<b class='flag-5'>非</b><b class='flag-5'>遞歸</b>可以嗎

    2分鐘看懂快速排序的算法

    之前有同學(xué)提出想要復(fù)習(xí)一下排序算法,那我們今天就挑一個難度中等的,快速排序。
    的頭像 發(fā)表于 02-25 09:32 ?755次閱讀

    排序算法有哪些

    1. 歸并排序遞歸版) 歸并排序(MERGE-SORT)是利用歸并的思想實現(xiàn)的排序方法,該算法采用經(jīng)典的分治策略,即分為兩步:分與治。 分:先遞歸
    的頭像 發(fā)表于 10-11 15:49 ?558次閱讀
    <b class='flag-5'>排序</b>算法有哪些

    遞歸神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu)形式主要分為

    遞歸神經(jīng)網(wǎng)絡(luò)(Recurrent Neural Networks,簡稱RNN)是一種具有時間序列處理能力的神經(jīng)網(wǎng)絡(luò),其結(jié)構(gòu)形式多樣,可以根據(jù)不同的需求進(jìn)行選擇和設(shè)計。本文將介紹遞歸神經(jīng)網(wǎng)絡(luò)的幾種主要
    的頭像 發(fā)表于 07-05 09:32 ?385次閱讀