歸併和快速排序思想的延伸

吳軍旗發表於2018-01-10

前面學習了歸併和快速排序演算法,現在來了解歸併和快速排序演算法背後的演算法思想:分治思想,並對歸併和快速排序進行擴充套件,解決經典演算法問題:逆序對和第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
複製程式碼

-------------------------華麗的分割線--------------------

看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。

個人部落格番茄技術小棧掘金主頁

想了解更多,歡迎關注我的微信公眾號:番茄技術小棧

番茄技術小棧

相關文章