归并排序、快速排序

本文介绍了冒泡排序、插入排序和选择排序的基本概念,重点比较了归并排序和快速排序,包括它们的时间复杂度(O(nlogn)),空间复杂度(归并排序需要O(n),快速排序原地),以及稳定性(归并排序稳定,快速排序不稳定)。

冒泡排序、插入排序、选择排序这三种排序算法,它们的时间复杂度都是 O(n²),比较高,适合小规模数据的排序。归并排序和快速排序时间复杂度为 O(nlogn),适合大规模的数据排序。

归并排序和快速排序都用到了分治思想。

归并排序

归并排序的原理

如果要排序一个数组,先从中间的位置将数组分割成两个子数组,然后再对这两个子数组从中间位置分割,如此直到分割之后的子数组只有一个元素。然后将最终分割的数组按大小顺序合并到一个新的数组中,如此递归地合并直到所有子数组合并完毕,就可以得到一个有序的数组了。

例子

假设要对下面这组数据利用归并排序方法进行排序:

$arr = [3, 5, 100, 6, 78];

第一次分割成下面两个数组:

$arr1 = [3, 5, 100]; $arr2 = [6, 78];

第二次分割:

// $arr1 划分成
[3] [5, 100]
// $arr2 划分成
[6] [78]

按递归的思想会先处理 $arr1 分割出来的两个数组:

[3] 里面只有一个元素,无需再分割,[5, 100] 还需要再次分割成 [5][100],分割完成就开始合并了,合并的时候,需要创建一个大小等于两个需要合并的数组大小总和的数组 temp,然后将小的元素先放入 temp,如此直到所有元素都放进了 temp 之后,这次合并操作就完成了,就得到了一个有序的数组 temp

所以 $arr1 最终合并得到:

[3, 5, 100]

$arr2 最终合并得到:

[6, 78]

再将 $arr1$arr2 处理的结果合并,也就是将 [3, 5, 100][6, 78] 合并,即可得到:

[3, 5, 6, 78, 100]

PHP 实现

function merge_sort($arr, $p, $r)
{
    if ($p >= $r) return [$arr[$r]];

    $q = intval(($p + $r) / 2);

    $arr1 = merge_sort($arr, $p, $q);
    $arr2 = merge_sort($arr, $q + 1, $r);

    return merge($arr1, $arr2);
}

function merge($arr1, $arr2)
{
    $arr = [];
    $n1 = count($arr1);
    $n2 = count($arr2);

    $k = 0;
    $i = 0;
    $j = 0;
    while ($i < $n1 && $j < $n2) {
        if ($arr1[$i] <= $arr2[$j]) {
            $arr[$k++] = $arr1[$i++];
        } else {
            $arr[$k++] = $arr2[$j++];
        }
    }

    while ($i < $n1) {
        $arr[$k++] = $arr1[$i++];
    }

    while ($j < $n2) {
        $arr[$k++] = $arr2[$j++];
    }

    return $arr;
}

算法分析

  1. 归并排序是否稳定?

merge 实现里面对相等的元素,我们可以将 $arr1 的元素先合并到 $arr,这样即可实现排序算法的稳定性。

  1. 时间复杂度是多少?

归并排序的排序过程中不会受原始数据的有序度影响,都是固定的不断拆分成两个数组,直到最终拆分到只有一个元素的时候进行合并,小的在前面,大的在后面。也就是说,归并排序的时间复杂度是固定的 O(nlogn),非常稳定。

  1. 空间复杂度是多少?

在将原始数据一分为二的时候,我们不需要开辟额外的空间来存储数据,但是在合并的过程中,我们需要开辟一个临时数组来存储排序后的数据,然后将这个数组覆盖掉原始数组的那一段范围的数据。

但由于某一时刻 CPU 中最多只有一个合并的操作在进行,所以最大的空间复杂度为 O(n)

快速排序

快速排序原理

快速排序的过程中,我们会先从原始数组中随机选一个数 pivot,以这个数为中点,将剩余的其他 n-1 个数划分为两个部分,小于 pivot 的交换到 pivot 的左边,大于 pivot 的交换到 pivot 的右边。然后再对左边和右边两部分的子数组递归进行此操作,直到最后只有一个元素为止,这个时候我们的整个数组就排序完毕了。

例子

利用快速排序对以下数组进行排序:

$arr = [5, 1, 7, 8, 6];
  1. 选择最后一个数(6)作为中点,小于 6 的交换到 $arr 的左边,中间为 6,大于 6 的交换到 $arr 的右边。

  2. 经过上一步的交换后,$arr[5, 1, 6, 8, 7],这个时候 6 其实就是最终排序完的位置了。

  3. 再对 6 左边和右边的子数组 [5, 1][8, 7] 执行第一步操作,如此直到子数组只有一个元素,结束排序。

我们可以发现,快速排序的过程,其实就是一次次地确定最终排序数组中间元素的过程。

PHP 实现

function quick_sort(&$arr, $p, $r)
{
    if ($p >= $r) return;

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

    quick_sort($arr, $p, $q - 1);
    quick_sort($arr, $q + 1, $r);
}

$array = [1, 5, 3, 7, 9, 4, 20, 100, 55, 67, 66];

// 分区+排序
function partition(&$arr, $p, $r)
{
    // 返回分区点
    $pivot = $arr[$r];

    $i = $p;
    for ($j = $p; $j < $r - 1; $j++) { // $r - 1 的值为 pivot,中间值
        if ($arr[$j] < $pivot) {
            $temp = $arr[$i];
            $arr[$i] = $arr[$j];
            $arr[$j] = $temp;
            $i++;
        }
    }

    // $i 永远不大于 $j,$i <= $j
    // $i == $j 的情况下不交换位置
    // 情况一:只有一个数,不影响
    // 情况二:数组里面全部都是小于 $pivot 的元素,这种情况不需要再互换了
    if ($i < $j) {
        $arr[$r] = $arr[$i];
        $arr[$i] = $pivot;
    }

    return $i;
}

quick_sort($array, 0, count($array) - 1);
var_export($array);

算法分析

  1. 快速排序是否是稳定的排序算法?

由于快速排序的过程中,会发生元素前后的交换,所以不能保证稳定性,所以是不稳定的排序算法。

  1. 快速排序的时间复杂度是多少?

平均时间复杂度是 O(nlogn),但可能比这个大

  1. 快速排序的空间复杂度是多少?

快速排序是原地排序算法,排序过程不需要开辟新的空间来处理。

归并排序和快速排序对比

  1. 归并排序是稳定的排序算法,快速排序不是稳定的排序算法

  2. 归并排序的过程需要开辟临时的空间来进行合并操作,快速排序是原地排序算法,归并排序的空间复杂度太高

  3. 归并排序的时间复杂度是固定的,快速排序的时间复杂度会受实际数据有序度影响

  4. 实际使用中,快速排序用得更多,因为是原地排序算法,但是不能保证排序结果的稳定性

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

张无忌打怪兽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值