最近研究了下快速排序,发现网上很多用递归实现,但是有很多极端问题,分析记录一波
$a = array(2,13,42,34,56,23,67,365,87665,54,68,3);
function quick_sort($a)
{
// 判断是否需要运行,因下面已拿出一个中间值,这里<=1
if (count($a) <= 1) {
return $a;
}
$middle = $a[0]; // 中间值
$left = array(); // 接收小于中间值
$right = array();// 接收大于中间值
// 循环比较,注意这里下标从1开始,也就是默认处理了相等的情况
for ($i=1; $i < count($a); $i++) {
if ($middle < $a[$i]) {
// 大于中间值
$right[] = $a[$i];
} else {
// 小于中间值
$left[] = $a[$i];
}
}
// 递归排序划分好的2边
$left = quick_sort($left);
$right = quick_sort($right);
// 合并排序后的数据,别忘了合并中间值
return array_merge($left, array($middle), $right);
}
问题分析
1:时间复杂度,最坏的情况(就是所有数据都相等时) 时间复杂度是O(NN/2) 跟冒泡差不多,最好情况,没有重复数据,且取数据都取到中间值,时间复杂度为O(N*logn);
2:空间复杂度,最差情况(就是每个元素相同),保存的数据大概是(n*n/2),这样假如有1w 个重复数据,那最少得保存5000w个数据,内存消耗是指数级的
3:调用栈深度,最差情况(所有元素相同),调用栈深度就是n 层,这是非常可怕的,语音调用栈一般不超过5000,也就是5000个相容的数,用上面的方法就可能出现调用栈溢出的问题
试验:生成5000个相同的数,用上面的方法排序
结果:内存不够…
优化:上面所有的问题根本原因都是递归造成的,递归导致方法内临时变量内存没法释放,最后溢出,这里有我之前研究的递归跟循环转换的关系,我们可以用循环压栈方式实现上面的算法:
递归与循环互转关系看这里
1:外层任务栈处理方法
public function quickSort(){
$data=[1,6,3,5,7,8,44,65,44,15,33,99,67,22,16,6,3,65,99,1];
//任务栈
$task=[];
//第一次处理,往任务栈添加
$this->dealTask($data,$task);
//保存最后排好序的数据
$lastData=[];
//消费task
while (!empty($task)){
//这里根据入栈顺序跟排序方式 取最后或者最前一个,这里小的后入所以要先取,达到从小到大排列
$temp=array_pop($task);
//继续往下拆,最后为单个数据,第一个为最左边的数据即最小的
$data= $this->dealTask($temp,$task);
//判断是否需要继续拆分
if (false!==$data){
//这里出来的数据就是有序的了
array_push($lastData,$data);
}
}
//排序完成
echo json_encode($lastData);
}
2:拆分数组方法
/* 往任务栈 task 里压入拆分的数组,直到数组为单个
* @param $data ,数组
* @param $task ,任务栈
* @return bool ,返回false或者最后拆分的数据
*/
private function dealTask($data,&$task){
if (count($data)==1){
//拆分完成,返回数据
return $data[0];
}
//获取一个数据
$need=$data[0];
//拆分数据
$left=[];
$right=[];
$mid=[];
foreach ($data as $k=> $datum) {
if ($datum<$need){
$left[]=$datum;
}elseif ($datum==$need){
//相等情况需要独立放进一个数组,不然无法拆完
$mid[]=[$need];
}else{
$right[]=$datum;
}
}
//添加到task,这个顺序必须先添加右边的,处理会优先处理后面的,达到从小到大排列,要从大到小只需要调整入栈顺序即可
if (!empty($right)){
array_push($task,$right);
}
//这个得放中间,里面应该是多个相等的数据
if (!empty($mid)){
$task= array_merge($task,$mid);
}
if (!empty($left)){
array_push($task,$left);
}
//未拆分完,返回false 继续拆分
return false;
}
## 转成循环后问题分析:
缺点:
1:多一层操作,就是压栈跟出栈,不管数据组成怎样,这是必须要操作的;
优点:
1:从时间复杂度来说,这样的方式最差情况就是数据完全不一致,最优时间复杂度为O(n*logn)就是上面最好的情况,这里最好情况就是上面的最差情况即所有元素相同,这里只需要一次遍历+出栈就完成了
2:空间复杂度,由于每次压栈后都会先出栈数据,最终栈内的数据都是一个完整原始的数据数组,所以不管怎样,这里的空间复杂度都是 n
3:栈深问题,这里就不存在了
测试:用上面的测试数据(5000个相同数据)测试,一点问题没有,随机生成的数据也是可以的
总结:递归的逻辑比较清晰,在特定情况下是速度会比循环快,但是缺点非常突出,极度容易内存溢出或者调用栈溢出,循环的方式无论从时间复杂度还是空间复杂度都是优于递归的