PHP 递归快速排序问题分析及优化方案

6 篇文章 0 订阅
博客分析了快速排序的递归实现存在的问题,包括最坏情况下的时间复杂度为O(N^2),空间复杂度在数据重复时呈指数级增长,以及调用栈深度可能导致溢出。通过将递归转换为循环压栈的方式进行了优化,解决了这些问题。优化后的算法在最坏情况下仍保持O(n*logn)的时间复杂度,空间复杂度稳定在N,消除了栈深问题。测试表明,优化后的算法能有效处理相同数据,避免内存不足。
摘要由CSDN通过智能技术生成

最近研究了下快速排序,发现网上很多用递归实现,但是有很多极端问题,分析记录一波

首先给出快排递归实现(网上copy)

$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个相同数据)测试,一点问题没有,随机生成的数据也是可以的

总结:递归的逻辑比较清晰,在特定情况下是速度会比循环快,但是缺点非常突出,极度容易内存溢出或者调用栈溢出,循环的方式无论从时间复杂度还是空间复杂度都是优于递归的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值