《演算法導論》 快速排序

2021-08-10 17:04:28 字數 3981 閱讀 1633

最近和朋友聊天,聊到企業面試時考了一道鍊錶的快排,這道題在leetcode上也刷到過,而且對於陣列的快排,真是從大學到研究生一直都要求必須掌握的重點知識,然而自己不斷的思考,卻發現所謂的快排,比我以前想象的要複雜的多,因為快排的思想非常幹練,但是形式,或者說具體實現千變萬化,為了達到萬變不離其宗,筆者決定好好**下它的各種實現,以下是筆者反覆測試的過程。

首先翻開《演算法導論》,裡面第七章就非常簡潔的將快排的偽**展示出了如下;

// 快排主演算法

quicksort(arr, p, q)

if p < q

r = partition(arr, p, q)

quicksort(arr, p, r-1)

quicksort(arr, r+1, q)

// 哨兵元素關鍵位置的獲取

partition(arr, p, q)

value = arr[q]

index = p-1

for i=p to q-1

if arr[i] <= value

index ++

exchange(arr, index, i)

exchange(arr, ++ index, q)

return index

看懂這幾條語句並不難,而且我**實現過,完美執行如下:

private

static

void

quicksort(int arr, int left, int right)

}/**

* 選取最右邊元素作為哨兵,並從左邊開始堆積;但從左邊開始遍歷

*@param arr

*@param left

*@param right

*@return

*/private

static

intpartition(int arr, int p, int q)

算導的思想非常優美簡練,選取最右邊元素為哨兵,從左邊往右邊遍歷,遇到乙個比哨兵小的,就index的後一位元素進行交換,這個index是什麼?是乙個下標,它代表著在index本身以及前面的所有元素都小於等於哨兵,而後一位的元素肯定是大於該元素的,所以拿來交換是完全可以的,交換完了之後,index下標加了1,這樣繼續保持著index本身及前面的所有元素都小於等於哨兵;

在遍歷了一便之後,index成了乙個分界點,index元素一定是小於等於哨兵的,而index後面的元素必然是大於哨兵的,那麼應該如何處置哨兵元素呢?就只需要將index元素後一位的元素和哨兵所在位置交換即可,最後返回的就是index+1也即完成了原址重排的哨兵的位置下標;

但是一如往常的,看完這幾條語句,不禁陷入深深的思考,比如:

private

static

intpartition(int arr, int p, int q)

private

static

intpartition(int arr, int p, int q)

這裡做個變體,如果此時要求i從p取值到q,程式還能正常執行麼?看了前一點思考,可能會想到,應該不能把,這時候i可以取到q,是不是得把for後面的交換語句去掉才行啊,恰恰相反,留著交換語句才可以正常執行,反而去掉交換語句,程式報錯,為什麼?因為此時的arr[i]只能是小於value元素,所以當i取到q值時,也即哨兵元素的位置時,此時的arr[i]=value,不滿足if條件,所以不會發生交換,此刻退出for,如果沒有交換語句,那麼哨兵元素就不會交換到index的後一位,所以程式報錯,所以此時的exchange方法必須呼叫

private

static

intpartition(int arr, int p, int q)

輸入是;

輸出是;

很明顯不行!

而且不管你怎麼調整i的初始位置,判斷條件,抑或是if裡的判斷條件,都無法得出正確的結果。其實認真分析一下,就可以知道,哨兵元素在最右邊元素,而index依然選擇從左邊開始計數,所以當從右往左遍歷時,比如遍歷到arr[i],此時如果它比value要小,好,我們將index加1,然後將這個位置的元素arr[index]和arr[i]交換了,然後就繼續遍歷,stop,沒發現有什麼不對麼?你怎麼知道arrindex]就一定大於等於value呢?如果此時的arr[index]也小於value呢?當然有可能!,這就是問題的關鍵,而i選擇繼續遍歷之後,i減去1,直接忽略了被交換了位置以後的arr[index],它此刻站在第i+1個位置上,但它是有可能比value小的,這樣得到的結果能是最後以index為分界點,右邊元素都大於等於value麼?不能

那麼剛才選擇以從左往右遍歷卻可以,為什麼?因為當index在左,i也從左往右遍歷時,將先發現那些小於value的元素,設想,當arr[i]小於value,然後此時的index加1,並將此時的arr[index]和arr[i]交換,此時的arr[index]可能小於value麼?只有一種情況下會小於,那就是此時的index=i的情況下,其實也就用不著交換了,當i>index時,arr[index]只會大於等於value,絕對不可能比value小,否則i在遍歷過程中跳過了乙個小於value是值,矛盾;

綜上的一點思考得出的結論:那就是當index選取從哪邊開始,那麼此時的遍歷方式也得選擇從這一邊開始,否則就會漏掉要比較的元素!

private

static

intpartition(int arr, int p, int q)

經過筆者多次測試,只有上面的**能正常執行,一方面,此時的哨兵元素可以保持在原位置不動,最後方可以順利和index位置的元素交換(q不變,依然是最低位置的元素);

快速排序想必大家都特比熟悉了,快速排序的主要思想是:

1)選定乙個基準元素

2)經過一趟排序,將所有元素分成兩部分

3)分別對兩部分重複上述操作,直到所有元素都已排序成功

但是對於鍊錶而言,和陣列的做法相似,但是卻又有所不同。根據思想,可以很快寫出類似下面的**。

//此處的begin和end都分別代表單鏈表初始狀態下的頭和尾結點

public

void

quicksort(listnode begin, listnode end)

}second = second.next;

}//判斷,有些情況是不用換的,提公升效能

if (begin != first)

//前部分遞迴

quicksort(begin, first);

//後部分遞迴

quicksort(first.next, end);

}

但是還有一種更加簡潔的基於快排思想的解法,**如下

public

static listnode sortlist(listnode head) else

}//此刻之後,head前面的都比head小,head後面的都比head大

last = head.next; //然後把head(即快排裡面的比較元素的下乙個元素)取出,好遞迴進行排序

head.next = null; //同時切斷整個鍊錶為兩份

pre = sortlist(pre); //對前面部分進行快排, 下同

last = sortlist(last);

head.next = last; //因為head是肯定還在中間的(細想可知),所以這裡直接繼續從中間接上即可。

return pre; //返回頭節點

}

快速排序 演算法導論

對於包含n個數的輸入陣列來說,快速排序是一種最壞情況時間複雜度為o n 的排序演算法。雖然最壞情況時間的複雜度很差,但是快速排序通常是實際排序應用中最好的選擇,因為它的平均效能非常好 它的期望時間複雜度是o nlgn 而且o nlgn 中隱含的常數因子非常小,另外,它還能夠進行原址排序,甚至在虛存環...

演算法導論 快速排序

既然敢叫 快速排序 必然有其過人之處。事實上,它確實是最快的通用內部排序演算法。它由hoare於1962年提出,相對歸併排序來說不僅速度快,並且不需要輔助空間。對於包含n個數的輸入陣列來說,快速排序是一種最壞情況時間複雜度為o n 2 的排序演算法。雖然最壞情況時間複雜度差,但是快速排序通常是實際排...

演算法導論 快速排序

昨天阿里3面,被問了快速排序,我看看之前的部落格,這個排序漏了 快排的基本思想 1.分而治之的思路,選取乙個哨兵節點 2.將小於key的元素放在哨兵的左邊,將大於key的元素放在右邊 快排的時間複雜度為on logn 最差的情況為o n2 即所有元素都是有序的。每次只能走一位元素。快排是不穩定的演算...