归并和快速排序思想的延伸

前面学习了归并和快速排序算法,现在来了解归并和快速排序算法背后的算法思想:分治思想,并对归并和快速排序进行扩展,解决经典算法问题:逆序对和第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
复制代码

-------------------------华丽的分割线--------------------

看完的朋友可以点个喜欢/关注,您的支持是对我最大的鼓励。

个人博客番茄技术小栈掘金主页

想了解更多,欢迎关注我的微信公众号:番茄技术小栈

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值