《劍指offer》Partition函式及其Partition函數週邊
技術標籤:資料結構劍指offerleetcodepartition中位數
Partion函式最早出現於快速排序演算法,但其代表的分治思想,在《劍指offer》這本書中求陣列中位數(面試題39,陣列中出現次數超過一半的數字)中也有所涉及,將其彙總在一起,稱之為Partition函式及其周邊。
快速排序完整程式碼Github連線
陣列中位數完整Github連線
若開啟Github網址有困難,可以參考我的這篇文章程式設計師常用網站加速方法
1. 快速排序
1.1 快排之Partion函式
Partition函式是快速排序的核心。下面程式碼是我基於c++語言對快速排序演算法的實現。
#include <iostream>
#include <ctime>
// exch array[i] and array[j]
void Exch(int *array, int i, int j);
void RandShuffle(int *array, int lo, int hi);
//find j, and make array[i] <= array[j], when 0 <=i <= j-1
// make array[i] >= array[j], when j < j+1 < len -1
int Partition(int * array, int len, int lo, int hi)
{
int i = lo;
int j = hi+1 ;//後撤一步
while (true)
{
while (array[++i] <= array[lo] && i < hi);//停在“大”數字,或者遞減陣列,一直到hi
while (array[--j] >= array[lo] && j > lo);//停在“小”數字,或者遞增陣列,一直到lo
if (i >= j) break;//此處i和j的最終停止範圍,j<i,除非陣列本身遞減此時i=j
Exch(array, i, j);
}
Exch(array, j, lo);//此處i和j不等價,因為99%的情形下,j=i-1;1%的情形始,i=j=hi(遞減)
return j;
}
int QuickSort(int *array, int len, int lo, int hi)
{
if (lo >= hi)
return 1;//遞迴終止條件,遞迴子問題的陣列已經變成一個元素,此時就可以return了。
RandShuffle(array, lo, hi);//每一次打亂遞迴子陣列的順序,使得達到一種均衡效率。
//遞迴呼叫必須有一個父問題---》(簡單)子問題的過程
int target = Partition(array, len, lo, hi);
QuickSort(array, len, lo, target - 1);
QuickSort(array, len, target + 1, hi);
return 1;
}
int main(int argv, char * argc)
{
int array[5] = { 0, 4, 3, 2, 1};
int len = sizeof(array) / sizeof(int);
//陣列輸入檢查
if (array == nullptr || len < 1)
{
return -1;
}
//主體
int lo = 0;
int hi = len - 1;
QuickSort(array, len, lo, hi);
...
return 0;
}
1.2 快排演算法中,最終狀態下i和j的位置關係分析
首先,要強調一點最終是lo和j指向的元素進行,因為J最終會停留在小於等於array[lo]的位置,而i則大部分情形下會停留在大於等於array[lo]的位置,就像這樣。
array的lo和hi段普通情形:滿足
j
<
i
j<i
j<i,且
j
=
i
−
1
j=i-1
j=i−1
array的lo和hi段遞減情形:此時
i
=
j
=
h
i
i=j=hi
i=j=hi
array的lo和hi段遞增情形:此時
j
<
i
j<i
j<i,且
j
=
i
−
1
j=i-1
j=i−1
綜上所述,i和j並非等價的關係,大部分情形下,i=j+1,且j代表的總是小於等於a[lo]的元素,因此將其與a[lo]交換,這樣符合交換後,小—target—大的順序。
1.3 快排演算法的計算複雜度
網上已經有不少的證明計算複雜度的部落格 ,我認為只需大概理解理想情況下的每次等分的證明情形即可。
然後需要有一個直觀反射弧,如果
T
(
l
)
=
2
T
(
l
−
1
)
+
n
T(l)=2T(l-1)+n
T(l)=2T(l−1)+n這樣的推導通式,其計算複雜度為
o
(
n
l
o
g
n
)
o(nlogn)
o(nlogn)。再有就是,記住
T
(
1
)
=
0
T(1)=0
T(1)=0,演算法的迭代次數為二叉樹的高度
l
o
g
n
logn
logn,可以有助於自己必要時刻隨時推出該計算複雜度。
2. Partition在求陣列中位數中的應用
面試題39:陣列中出現次數超過一半的數字。
分析,出現次數超過一半的數字,等同於該陣列的中位數。分析見下圖
-
若陣列元素個數n為奇數:對於排完序陣列
-
若陣列元素個數n為偶數:對於排完序陣列
但如果仔細分析,上述排序陣列,並非需要嚴格有序,如果我們去先排序的話,最低的時間複雜度也需要
o
(
n
l
o
g
n
)
o(nlogn)
o(nlogn),但事實上,只要保證[0+len-1]/2處元素,大於其左邊元素,小於其右邊元素,此時其即為中位數,這個要求顯然比嚴格有序要低,後面會證明,達到這種效果,最快的演算法為
o
(
n
)
o(n)
o(n)。
看到“保證[0+len-1]/2處元素,大於其左邊元素,小於其右邊元素”這種要求,應該可以聯絡到我們已有的一些"武器庫" : Partition函式。
只不過這次我們不再呼叫遞迴演算法,而是採用while迴圈,這樣當找到中位數時理解可以中止程式(遞迴比較適用於把所有的元素都理順)。
核心程式碼如下:
int target = hi / 2;
int return_value = Partition(array, lo, hi);
while (return_value != target)
{
if (return_value > target)
{
lo = lo;
hi = return_value-1;
return_value = Partition(array, lo, hi);
}
else
{
lo = return_value+1;
hi = hi;
return_value = Partition(array, lo, hi);
}
}
2.2 Partition函式求陣列的中位數補充知識
時間複雜度時o(n)。基於Partition函式的演算法的時間複雜度的分析不是很直觀,本書限於篇幅不作詳細討論,感興趣的讀者可以參考《演算法導論》等書籍的相關章節。
該Partition函式會修改陣列的內容。事實上對於“陣列中超過一半的數字”除了從中位數的角度出發,還可以從原始超過一半來出發,我把這種解法稱之為“動物世界”,具體見《劍指offer》雜篇