前面學習了歸併和快速排序演算法,現在來了解歸併和快速排序演算法背後的演算法思想:分治思想,並對歸併和快速排序進行擴充套件,解決經典演算法問題:逆序對和第K大的演算法問題
原文請訪問我的技術部落格番茄技術小棧
分治演算法
顧名思義,分而治之,就是將原問題,分割成同等結構的子問題,之後將子問題逐一解決後,原問題也就得到了解決。
歸併排序演算法的延伸:逆序對
什麼是逆序對?
對於一個長度為N的整數序列A,滿足i < j 且 Ai > Aj.的數對(i,j)稱為整數序列A的一個逆序。 通常逆序對可以表示一個數列的順序程度,從小到大的數列逆序對為0,從大到小的逆序對為:(n*(n-1))/2;
分析
採用分而治之的思想,要求整個數列的逆序對,可以先求出前一半數列的逆序對,和後一半數列的逆序對,然後加上前一個數列和後一個數列所形成的逆序對,因為前後兩個數列都是有序,直接在歸併排序merge的時候求是非常簡單的。
程式碼實現
function merge(&$arr, $l, $mid, $r){
$tmp = array();
$tmp = array_slice($arr, $l, $r-$l+1, true);
$res = 0;
//tmp現在為$arr的副本,以tmp為軸,重新賦值$arr
$i = $l;
$j = $mid+1;
for ($k=$l; $k <= $r; $k++) {
if ($i > $mid) {
$arr[$k] = $tmp[$j];
$j++;
}elseif ($j > $r) {
$arr[$k] = $tmp[$i];
$i++;
}elseif($tmp[$i] <= $tmp[$j]){
$arr[$k] = $tmp[$i];
$i++;
}else{
// 此時, 因為右半部分k所指的元素小
// 這個元素和左半部分的所有未處理的元素都構成了逆序數對
// 左半部分此時未處理的元素個數為 $mid - $i + 1;
$res += $mid - $i + 1;
$arr[$k] = $tmp[$j];
$j++;
}
}
return $res;
}
/**
* [__mergeSort 對區間為[l,r]的元素進行歸併排序]
* @param [type] $arr [description]
* @param [type] $l [description]
* @param [type] $r [description]
* @return [type] [description]
*/
function __inversionCount(&$arr, $l, $r){
//此時為一個元素,不需要進行歸併
if ($l >= $r) {
return 0;
}
$mid = (int)(($l + $r) / 2);
// 求出 arr[l...mid] 範圍的逆序數
$res1 = __inversionCount($arr, $l, $mid);
// 求出 arr[mid+1...r] 範圍的逆序數
$res2 = __inversionCount($arr, $mid+1, $r);
return $res1 + $res2 + merge($arr, $l, $mid, $r);
}
function inversionCount(&$arr, $n){
$res = __inversionCount($arr, 0, $n-1);
return $res;
}
複製程式碼
結果
Array
(
[0] => 3
[1] => 0
[2] => 5
[3] => 5
[4] => 8
[5] => 0
[6] => 8
[7] => 5
)
逆序對的個數: 7
複製程式碼
快速排序演算法的延伸:數列中第k大的數
分析
- 解法1: 我們可以對這個亂序陣列按照從大到小先行排序,然後取出前k大,總的時間複雜度為O(n*logn + k)。
- 解法2: 利用選擇排序或互動排序,K次選擇後即可得到第k大的數。總的時間複雜度為O(n*k)
- 解法3: 利用快速排序的思想,從陣列S中隨機找出一個元素X,把陣列分為兩部分Sa和Sb。Sa中的元素大於等於X,Sb中元素小於X。這時有兩種情況:
- Sa中元素的個數小於k,則Sb中的第k-|Sa|個元素即為第k大數;
- Sa中元素的個數大於等於k,則返回Sa中的第k大數。時間複雜度近似為O(n)
這裡我們採用第三種解法,時間複雜度為:O(n)+O(1/2)+O(1/4)+...+O(1/n), 當n為無窮大時候,時間複雜度約為O(n)
程式碼實現
//對arr[l...r]部分進行partition操作
// 返回p,使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
function partition(&$arr, $l, $r){
swap($arr, $l, rand($l, $r));
$v = $arr[$l];
$j = $l;
for ($i=$l+1; $i <= $r ; $i++) {
if ($arr[$i] < $v) {
swap($arr, $j+1, $i);
$j++;
}
}
swap($arr, $l, $j);
return $j;
}
/**
* [__quickSort 對陣列arr[l...r]進行快速排序]
* @param [type] &$arr [description]
* @param [type] $l [description]
* @param [type] $r [description]
* @return [type] [description]
*/
function __selectK(&$arr, $l, $r, $k){
if ($l == $r) {
return $arr[$l];
}
// 如果 k == p, 直接返回arr[p]
$p = partition($arr, $l, $r, $k);
if ($p == $k) {
return $arr[$p];
}elseif($p > $k){// 如果 k < p, 只需要在arr[l...p-1]中找第k小元素即可
return __selectK($arr, $l, $p-1, $k);
}else{// 如果 k > p, 則需要在arr[p+1...r]中找第k小元素
return __selectK($arr, $p+1, $r, $k);
}
}
// 尋找arr陣列中第k小的元素
function selectK(&$arr, $n, $k){
assert($k >= 0 && $k <= $n);
return __selectK($arr, 0, $n-1, $k);
}
複製程式碼
結果
Array
(
[0] => 9
[1] => 4
[2] => 10
[3] => 4
[4] => 7
[5] => 6
[6] => 3
[7] => 10
[8] => 7
[9] => 9
)
第3小的數為: 6
複製程式碼
-------------------------華麗的分割線--------------------
看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。
想了解更多,歡迎關注我的微信公眾號:番茄技術小棧