0
点赞
收藏
分享

微信扫一扫

归并排序&快速排序(Java)

老牛走世界 2022-04-20 阅读 47

1.归并排序

1.1递推公式

归并排序分解图
在这里插入图片描述

1.2代码实现

    /**
     * 归并排序
     *
     * @param value
     */
    public static void mergeSort(int[] value) {
        mergeSort(value, 0, value.length - 1);
    }

    //将两个排好序的合并到一起
    public static void mergeSort(int[] value, int m, int n) {
        //终止条件
        if (m >= n) {
            return;
        }
        mergeSort(value, m, (m + n) / 2);
        mergeSort(value, (m + n) / 2 + 1, n);
        //将两个子排序合并到一起 用临时数组
        merge(value, m, n);
    }

    /**
     * 合并函数
     *
     * @param value
     * @param m
     * @param n
     */
    private static void merge(int[] value, int m, int n) {
        int[] tem = new int[n - m + 1];
        
        int i = m;
        int j = (m + n) / 2 + 1;
        for(int x = 0; x <= n - m; x++) {
            if (j <= n && value[j] < value[i] || i > (m + n) / 2) {
                tem[x] = value[j];
                j++;
            } else {
                tem[x] = value[i];
                i++;
            }
        }
        for (x--; x >= 0; x--, n--) {
            value[n] = tem[x];
        }
    }

如何简化merge函数

    /**
     * 简化merge函数
     * todo 如何用哨兵简化代码
     *
     * @param value
     * @param m
     * @param n
     */
    private static void merge1(int[] value, int m, int n) {
        int[] tem = new int[n - m + 1];
        int x = 0;

        int i = m;
        int j = (m + n) / 2 + 1;
        while (x <= n - m) {
            if (j <= n && value[j] < value[i] || i > (m + n) / 2) {
                tem[x++] = value[j++];
            } else {
                tem[x++] = value[i++];
            }
        }
        while (--x >= 0) {
            value[x + m] = tem[x];
        }
    }

1.3算法分析

1.3.1稳定性

归并排序稳不稳定关键要看 merge() 函数,也就是两个有序子数组合并成一个有序数组的那部分代码。
在合并的过程中,如果 A[p…q]和 A[q+1…r]之间有值相同的元素,那我们可以像伪代码中那样,先把 A[p…q]中的元素放入 tmp 数组。这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法。

1.3.2时间复杂度

不仅递归求解的问题可以写成递推公式,递归代码的时间复杂度也可以写成递推公式。
我们假设对 n 个元素进行归并排序需要的时间是 T(n),那分解成两个子数组排序的时间都是 T(n/2)。我们知道,merge() 函数合并两个有序子数组的时间复杂度是 O(n)。所以,套用前面的公式,归并排序的时间复杂度的计算公式就是:

T(1) = C;   n=1时,只需要常量级的执行时间,所以表示为CT(n) = 2*T(n/2) + n; n>1
T(n) = 2*T(n/2) + n
     = 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
     = 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
     = 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
     ......
     = 2^k * T(n/2^k) + k * n
     ......

通过这样一步一步分解推导,我们可以得到 T(n) = 2k * T(n/2k) + k*n。当 T(n/2k)=T(1) 时,也就是 n/2k=1,我们得到 k=log2n 。

我们将 k 值代入上面的公式,得到 T(n)=C*n+n*log2n 。如果我们用大 O 标记法来表示的话,T(n) 就等于 O(nlogn)。所以归并排序的时间复杂度是 O(nlogn)。

1.3.3空间复杂度

归并排序的合并函数,在合并两个有序数组为一个有序数组时,需要借助额外的存储空间。

如果我们继续按照分析递归时间复杂度的方法,通过递推公式来求解,那整个归并过程需要的空间复杂度就是 O(nlogn)。不过,类似分析时间复杂度那样来分析空间复杂度,这个思路对吗?

实际上,递归代码的空间复杂度并不能像时间复杂度那样累加。刚刚我们忘记了最重要的一点,那就是,尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了。在任意时刻,CPU 只会有一个函数在执行,也就只会有一个临时的内存空间在使用。临时内存空间最大也不会超过 n 个数据的大小,所以空间复杂度是 O(n)

2.快速排序

2.1递推公式

在这里插入图片描述

2.2代码实现

    /**
     * @param value
     * @param m
     * @param n
     */
    public static void quickSort(int[] value, int m, int n) {
        //终止条件
        if (m >= n) {
            return;
        }
        //找到分区点, 并分区
        int pivot = partition(value, m, n);

        quickSort(value, m, pivot - 1);
        quickSort(value, pivot + 1, n);
    }

    /**
     * 分区函数, 找到分区点, 并分区(移动分区点)
     * @param value
     * @param m
     * @param n
     * @return
     */
    private static int partition(int[] value, int m, int n) {
        int pivot = m;
        for (int i = m + 1; i <= n; i++) {
            //把比分区点小的放到分区点左边, 分区点右移一位, 分区点右边>=分区点的不动
            // (数组中分区点右边的数换到比分区点小的数字位置)
            if (value[i] < value[pivot]) {
                int tem = value[i];
                value[i] = value[pivot + 1];
                value[pivot + 1] = value[pivot];
                value[pivot++] = tem;
            }
        }
        return pivot;
    }

2.3算法分析

2.3.1稳定性

因为分区的过程涉及交换操作,如果数组中有两个相同的元素,比如序列 6,8,7,6,3,5,9,4,在经过第一次分区操作之后,两个 6 的相对先后顺序就会改变。所以,快速排序并不是一个稳定的排序算法。

2.3.2时间复杂度

快排也是用递归来实现的。对于递归代码的时间复杂度,我前面总结的公式,这里也还是适用的。如果每次分区操作,都能正好把数组分成大小接近相等的两个小区间,那快排的时间复杂度递推求解公式跟归并是相同的。所以,快排的时间复杂度也是 O(nlogn)。

T(1) = C;   n=1时,只需要常量级的执行时间,所以表示为CT(n) = 2*T(n/2) + n; n>1

但是,公式成立的前提是每次分区操作,我们选择的 pivot 都很合适,正好能将大区间对等地一分为二。但实际上这种情况是很难实现的。我举一个比较极端的例子。如果数组中的数据原来已经是有序的了,比如 1,3,5,6,8。如果我们每次选择最后一个元素作为 pivot,那每次分区得到的两个区间都是不均等的。我们需要进行大约 n 次分区操作,才能完成快排的整个过程。每次分区我们平均要扫描大约 n/2 个元素,这种情况下,快排的时间复杂度就从 O(nlogn) 退化成了 O(n2), 即最坏情况时间复杂度。

那快排的平均情况时间复杂度是多少呢?
实际上,递归的时间复杂度的求解方法除了递推公式之外,还有递归树,在树那一节我再讲,这里暂时不说。我这里直接给你结论:T(n) 在大部分情况下的时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化到 O(n2)。而且,我们也有很多方法将这个概率降到很低

2.3.3空间复杂度

通过元素交换的写法,快排的空间复杂度是O(1),属于原地排序算法。

举报

相关推荐

0 条评论