被忽視的 partition 演算法

2021-09-18 02:28:07 字數 3133 閱讀 7890

如果你學習過演算法,那麼肯定聽說過快速排序的大名,但是對於快速排序中用到的 partition 演算法,你了解的夠多嗎?或許是快速排序太過於光芒四射,使得我們往往會忽視掉同樣重要的 partition 演算法。

partition 可不只用在快速排序中,還可以用於 selection algorithm(在無序陣列中尋找第k大的值)中。甚至有可能正是這種通過一趟掃瞄來進行分類的思想激發 edsger dijkstra 想出了 three-way partitioning,高效地解決了 dutch national flag problem 問題。接下來我們一起來探索 partition 演算法。

快速排序中用到的 partition 演算法思想很簡單,首先從無序陣列中選出樞軸點 pivot,然後通過一趟掃瞄,以 pivot 為分界線將陣列中其他元素分為兩部分,使得左邊部分的數小於等於樞軸,右邊部分的數大於等於樞軸(左部分或者右部分都可能為空),最後返回樞軸在新的陣列中的位置。

partition 的乙個直觀簡單實現如下(這裡取陣列的第乙個元素為pivot):

// do partition in arr[begin, end), with the first element as the pivot.

int partition(vector&arr, int begin, int end)}}

swap(arr[begin], arr[pos]);

return pos;

}

如果原始陣列為[5,9,2,1,4,7,5,8,3,6],那麼整個處理的過程如下圖所示:

這種實現思路比較直觀,但是其實並不高效。從直觀上來分析一下,每個小於pivot的值基本上(除非到現在為止還沒有遇見大於pivot的值)都需要一次交換,大於pivot的值(例如上圖中的數字9)有可能需要被交換多次才能到達最終的位置。

如果我們考慮用 two pointers 的思想,保持頭尾兩個指標向中間掃瞄,每次在頭部找到大於pivot的值,同時在尾部找到小於pivot的值,然後將它們做乙個交換,就可以一次把這兩個數字放到最終的位置。一種比較明智的寫法如下:

int partition(vector&arr, int begin, int end)

arr[begin] = pivot;

return begin;

}

如果是第一次看到上面的**,那麼停下來,好好品味一下。這裡沒有用到 swap 函式,但其實也相當於做了 swap 操作。以前面的陣列為例,看看以這種方法來做的話,整個處理的流程。

直觀上來看,賦值操作的次數不多,比前面單向掃瞄的swap次數都少,效率應該會更高。這裡從理論上對這兩種方法進行了分析,有興趣可以看看。

我們都知道經典的快速排序就是首先用 partition 將陣列分為兩部分,然後分別對左右兩部分遞迴進行快速排序,過程如下:

void quick_sort(vector&arr, int begin, int end)

int pos = partition(arr, begin, end);

quick_sort(arr, begin, pos);

quick_sort(arr, pos+1, end);

}

雖然快排用到了經典的分而治之的思想,但是快排實現的前提還是在於 partition 函式。正是有了 partition 的存在,才使得可以將整個大問題進行劃分,進而分別進行處理。

除了用來進行快速排序,partition 還可以用 o(n) 的平均時間複雜度從無序陣列中尋找第k大的值。和快排一樣,這裡也用到了分而治之的思想。首先用 partition 將陣列分為兩部分,得到分界點下標 pos,然後分三種情況:

下面給出基於迭代的實現:

int find_kth_number(vector&arr, int k)

else if(pos > k-1)

else

}return target_num;

}

該演算法的時間複雜度是多少呢?考慮最壞情況下,每次 partition 將陣列分為長度為 n-1 和 1 的兩部分,然後在長的一邊繼續尋找第 k 大,此時時間複雜度為 o(n^2 )。不過如果在開始之前將陣列進行隨機打亂,那麼可以盡量避免最壞情況的出現。而在最好情況下,每次將陣列均分為長度相同的兩半,執行時間 t(n) = n + t(n/2),時間複雜度是 o(n)。

接下來先考慮這樣乙個問題,給定紅、白、藍三種顏色的小球若干個,將其排成一列,使相同顏色的小球相鄰,三種顏色先後順序為紅,白,藍。這就是經典的 dutch national flag problem。

我們可以針對紅,藍,白三種顏色的球分別計數,然後根據計數結果來重新放球。不過如果我們將問題進一步抽象,也就是說將乙個陣列按照某個target值分為三部分,使得左邊部分的值小於 target,中間部分等於 target,右邊部分大於 target,這樣就不能再用簡單的計數來確定排序後的結果。這時候,就可以用到另一種 partition 演算法:three-way-partition。它的思路稍微複雜一點,用三個指標將陣列分為四個部分,通過一次掃瞄最終將陣列分為 的三部分,如下圖所示:

可以結合下面**來理解具體的邏輯:

// assume target is in the arr.

void three_way_partition(vector&arr, int target)

else if(arr[next_scan_pos] > target)

else}}

這裡的主要思想就是在一遍掃瞄中,通過交換不同位置的數字,使得陣列最終可以維持一定的順序,和前面快排中用到的 partition 思想一致。區別在於快排按照 pivot 將陣列分為兩部分,左右部分中的值都可能等於 pivot,而 three-way-partition 將陣列分為 的三部分。

algorithms 4.0: demopartitioning

dutch national flag problem

time complexity of quick-sort in detail

quicksort partitioning: hoare vs. lomuto

數學之美番外篇:快排為什麼那樣快

被忽視的 partition 演算法

如果你學習過演算法,那麼肯定聽說過快速排序的大名,但是對於快速排序中用到的 partition 演算法,你了解的夠多嗎?或許是快速排序太過於光芒四射,使得我們往往會忽視掉同樣重要的 partition 演算法。partition 可不只用在快速排序中,還可以用於 selection algorith...

快排光芒下被忽視的Partition函式

看到這篇標題,沒有學過快排的人自然是不知道partition函式的意思和作用,這裡附上學習的連線 lantian的快排總結 我們現在都是被快排蒙蔽了雙眼,沒有意識到快最核心的劃分函式partition,當然partition函式也就不止於快排這裡,本文就從多方面來為展示partition函式的本質和...

被忽視的IT金飯碗

談高校學子對軟體測試認識的三大誤區 軟體測試是保障軟體質量的重要環節,企業對於軟體質量意識的逐步增強,促使國內軟體測試人員的地位不斷提公升。但中國高等教育往往落後企業實際發展3 5年,所以在高校學子心中,重開發 輕測試的思想依然嚴重。許多優秀的畢業生甚至對測試工作存在偏見,致使軟體測試人才缺口進一步...