阅读 3226

快速排序详解

主要介绍快速排序,和3个快排的优化,最后引出三路快速排序。

原文请访问我的技术博客番茄技术小站

定义(百度百科)

快速排序由C. A. R. Hoare在1962年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

算法步骤(wiki)

  • 从数列中挑出一个元素,称为"基准"(pivot),
  • 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  • 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

快速排序代码实现(basic)

分析

paste image

  • 首先寻找基准元素;
  • 分区(partition):所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面;
  • 递归的对左右两边的子数列进行递归排序

图示(初始条件很重要)

partition的某个时刻

paste image

  • 当e>v时, i直接i++

paste image

  • 当e<v时, arr[j+1] <=> e互换, j++, i++

paste image

paste image

  • 最后的状态为:(返回j)
    paste image

php代码实现

最重要的是实现partition函数,而实现partition函数最重要的是初始化状态值(j =l,i=l+1)

//对arr[l...r]部分进行partition操作
// 返回p,使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
function partition(&$arr, $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 __quickSort(&$arr, $l, $r){
	if ($l >= $r) {
		return;
	}

	//优化点1
	// if($r - $l <= 15){
	// 	insertSortCom($arr, $l, $r);
	// 	return;
	// }

	$p = partition($arr, $l, $r);

	__quickSort($arr, $l, $p-1);
	__quickSort($arr, $p+1, $r);
}

function quickSort(&$arr, $n){
	__quickSort($arr, 0, $n-1);
}
复制代码

执行时间

mergeSort运行的时间为:0.031744003295898s
quickSort运行的时间为:0.02904486656189s
复制代码

优化点1

还是在数列小于16的时候直接对数列进行插入排序

function __quickSort(&$arr, $l, $r){
	// if ($l >= $r) {
	// 	return;
	// }

	//优化点1
	if($r - $l <= 15){
		insertSortCom($arr, $l, $r);
		return;
	}

	$p = partition($arr, $l, $r);

	__quickSort($arr, $l, $p-1);
	__quickSort($arr, $p+1, $r);
}
复制代码

优化点2

对于数列基本有序的情况

// //main
$n = 100000;
// $arr = generateRandomArray($n, 0, $n);
$arr = generateNearlyOrderedArray($n, 100);
$copy_arr = $arr;
testSort("mergeSort", "mergeSort", $copy_arr, $n);
testSort("quickSort", "quickSort", $arr, $n);
复制代码

归并排序和快速排序算法的时间对比

mergeSort运行的时间为:0.32358503341675s
quickSort运行的时间为:18.838504076004s
复制代码

分析

当数组近乎有序的情况下,快速排序每次划分的数列极其不平衡,几乎退化成O(N*N)时间复杂度的算法

paste image

有没有解决方法,有的!只要每次随机的选取“基准数“即可。

问题解决(代码实现)

只需要在partition代码中加入swap($arr, $l, rand($l, $r));

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;
}
复制代码

执行时间

mergeSort运行的时间为:0.21585202217102s
quickSort运行的时间为:0.34276294708252s
复制代码

优化点3

对于数列中有大量重复元素的情况

$n = 100000;
$arr = generateRandomArray($n, 0, 10);
$copy_arr = $arr;
testSort("mergeSort", "mergeSort", $copy_arr, $n);
testSort("quickSort", "quickSort", $arr, $n);
复制代码

运行时间

mergeSort运行的时间为:0.89260506629944s
quickSort运行的时间为:19.236886024475s
复制代码

分析

当数列中存在大量重复元素时,我们发现快排速度很慢,我们有理由怀疑它可能又蜕化成O(n*n)时间复杂度的算法了。我们的代码中,对于arr[i]等于v的情况都放到了右边,所以会造成partition造成极度不平衡:

paste image

问题解决(代码实现)

** 场景分析**

  • 初始场景

    paste image

  • arr[i] < e, 直接i++

paste image

  • arr[j] > v, 直接j--

paste image

  • 直到arr[i] >= e,arr[j] <= e, swap(arr[i], arr[j]);这时候,与$v重复的元素就均匀的分布在数列的两边

paste image

  • 最终状态

paste image

代码实现

/*--------------------第二种算法开始-------------------*/


//对arr[l...r]部分进行partition操作
// 返回p,使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
function partition2(&$arr, $l, $r){

	swap($arr, $l, rand($l, $r));

	$v = $arr[$l];
	// arr[l+1...i) <= v; arr(j...r] >= v
	$i = $l+1;
	$j = $r;

	while (true) {
		while($arr[$i] < $v && $i <= $r){
			$i++;
		}
		while($arr[$j] > $v && $j >= $l+1){
			$j--;
		}
		if ($i > $j) {
			break;
		}
		swap($arr, $i, $j);
		$i++;
		$j--;
	}
	swap($arr, $l, $j);
	return $j;
}


function quickSort2(&$arr, $n){
	__quickSort2($arr, 0, $n-1);
}

function __quickSort2(&$arr, $l, $r){
	// if ($l >= $r) {
	// 	return;
	// }

	//优化点1
	if($r - $l <= 15){
		insertSortCom($arr, $l, $r);
		return;
	}

	$p = partition2($arr, $l, $r);

	__quickSort2($arr, $l, $p-1);
	__quickSort2($arr, $p+1, $r);
}


/*--------------------第二种算法结束-------------------*/
复制代码

运行时间比较

mergeSort运行的时间为:1.1809809207916s
quickSort运行的时间为:25.186847925186s
quickSort2运行的时间为:0.20458602905273s
复制代码

可以看出,优化后的快排时间复杂度又回到了O(N*logN)了。从优化2算法其实可以引出三路快排算法。

三路快速排序算法

定义

三路快速排序是快速排序的的一个优化版本, 将数组分成三段, 即小于基准元素、 等于 基准元素和大于基准元素, 这样可以比较高效的处理数组中存在相同元素的情况,其它特征与快速排序基本相同。

图示

  • 初始情况

paste image

  • 如果arr[i] == v, 直接i++

paste image

  • 如果arr[i] < v, swap(arr[lt+1],arr[i]),i++, $lt++

paste image

  • 如果arr[i] > v, swap(arr[gt-1],arr[i]),gt--, $i++

paste image

  • 最终状态

paste image

代码实现

/*--------------------第三种算法开始-------------------*/

//对arr[l...r]部分进行partition操作
// 返回p,使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p]
function partition3(&$arr, $l, $r){

	swap($arr, $l, rand($l, $r));

	$v = $arr[$l];
	$lt = $l;
	$i = $l + 1;
	$gt = $r + 1;

	while($i < $gt){
		if ($arr[$i] == $v) {
			$i++;
		}elseif ($arr[$i] < $v) {
			swap($arr, $lt+1, $i);
			$lt++;
			$i++;
		}else{
			swap($arr, $gt-1, $i);
			$gt--;
		}
	}
	swap($arr, $l, $lt);
	return array("lt" => $lt, "gt" => $gt);
}


function quickSort3(&$arr, $n){
	__quickSort3($arr, 0, $n-1);
}

function __quickSort3(&$arr, $l, $r){
	// if ($l >= $r) {
	// 	return;
	// }

	//优化点1
	if($r - $l <= 15){
		insertSortCom($arr, $l, $r);
		return;
	}


	$rt = partition3($arr, $l, $r);
	$lt = $rt['lt'];
	$gt = $rt['gt'];

	__quickSort3($arr, $l, $lt -1);
	__quickSort3($arr, $gt, $r);
}

/*--------------------第三种算法结束-------------------*/
复制代码

时间结果

quickSort2运行的时间为:0.43507099151611s
quickSort3运行的时间为:0.19537496566772s
复制代码

可以看出3路快速排序的算法还是很快的。


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

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

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

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

番茄技术小栈

文章分类
后端
文章标签