双路快排的问题

来源:3-5 三路快排partition思路的应用 Sort Color

讲武德的年轻人

2022-07-20

bobo老师,借这个地方请教您在“算法与数据结构”体系课中的双路快排的问题。在partition函数中,您实现的版本中,在while循环的前后都需要用swap函数来调整pivot元素的位置。我修改了一下partition函数,不需要进行这样的调整,相应的while循环也进行了修改,即当左指针i > 右指针j的时候,才退出循环 (您的版本中是i >= j退出)。这样,partition函数需要返回了一个int[]型数组,记录j和i的位置,在quickSort中,只需要对[l, j]和[i, r]分别进行快排就行了。我用了测试用例认为应该是对的?想请教您这算不算是一种常数级别的优化呢?当然,我觉得书写方法还是看个人习惯,本质并无优劣。

public class Solution {

    public void sortIntegers2(int[] a) {

        Random rnd = new Random();
        quickSort(a, 0, a.length - 1, rnd);

    }

    private void quickSort(int[] a, int l, int r, Random rnd) {
        if (l >= r)
            return;

        int[] p = partition(a, l, r, rnd);

        quickSort(a, l, p[0], rnd);
        quickSort(a,  p[1], r, rnd);
    }

    private int[] partition(int[] a, int l, int r, Random rnd) {
    
        int p = rnd.nextInt(r - l + 1) + l;
        int pivot = a[p];
        
        int i = l;
        int j = r;

        while (true) {
        
            while (i <= j && a[i] < pivot)
            i++;

            while (i <= j && a[j] > pivot)
            j--;

            if (i > j)
            break;

            swap(a, i, j);
            i++;
            j--;
        }

        return new int[]{j, i};

    }

    private void swap(int[] a, int i, int j) {
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}


写回答

1回答

liuyubobobo

2022-07-20

逻辑是正确的。


我的测试结果,你的这版代码是比课程代码慢的,测试结果如下:(其中 QuickSort2Ways2 是你的这版代码)

https://img.mukewang.com/szimg/62d7d76209077c4e13921250.jpg


我认为核心原因是:partition 中使用了 new。new 是很慢的一个操作。因为 Java 原生不支持返回多个函数值,对于不支持某类操作的语言,“绕弯子”达成这个目的肯定会牺牲性能。


我个人不是很推崇在某个语言中“绕弯子”以达到某个并不支持的语言特性;而更推崇“绕弯子”以不使用这个语言特性(毕竟它不支持)。这就是为什么,对于课程中的三路排序,我选择了不单独写 partition 函数,而将 partition 的过程和 sort 融合起来。因为对于三路快排,单独写 partition,就牵扯到了要返回两个函数值的问题。


但这个原则只是我个人的一个原则,也并无好坏。在很多时候,这个性能的影响也并不大。(比如你的这版代码,对 100 万级别的数据,差距在 ms 级别,完全在承受范围里。)所以根据自己的喜好来就好。



赞一下换一个方式去实现,相信你完全理解了这个算法的原理:)


==========


补充两点:


1)

这个问题在 C++ 或者 Python 中都不存在。Python 原生支持返回多个函数值;C++ 中可以使用传入引用的方式解决。注意:C++ 中的引用和 Java 中说的引用不是一个概念。说白了,是因为 C++ 支持对地址的操作,可以直接给出一个地址,去读写这个地址中的内容。所以我怀疑用 C++ 实现你这个思路,性能差距不大,但是懒得实验了。


我个人其实不喜欢纠结太过细碎的常数级“优化”:),因为其实对于现代编译器而言,很多这种“非常细碎的”常数级的优化,编译器就给做了,在上层代码上做这种工作,通常收益很低。


(但要注意,我强调的是“非常细碎的”常数级优化。但有的常数级优化不是如此。还是要具体问题具体分析。另外,在特定场合下,这类优化是有意义的,比如偏底层,或者低延迟高并发的场景。但是如果对性能这么敏感,或许本身不应该选择使用 Java 语言。我知道有人专门针对 C/C++ 语言研究这类优化,有个挺知名的 tech blogger,一时想不起来,在这方面做了很多工作。但很少听说 Java 工程师专门研究这种语言层面的细碎优化。)


2)

因为你特意强调了你的版本的代码是 i > j 退出,我的代码是 i >= j 退出,所以我怀疑你认为 i > j 是优于 i >= j 的。但实际上不是如此。虽然我们说 i >= j 是 i > j 或者 i == j,好像多了一个判断,但是在计算机底层,>= 是一个指令,用汇编表示就是 JGE,而 > 就是 JG。更往底层看,从数字逻辑的角度,JGE 就是一个电路。(JG 也是一个电路)


也就是逻辑上,i >= j 和 i > j || i == j 是一样的,但是性能上,这二者完全不同。前者的性能是优于后者的。而 i >= j 和 i > j 的性能是相同的。这一点看汇编码很容易看出来。(对于 Java 来说,看字节码。)


(但是!这里还是要有但是:对于现代编译器,是能直接把  i > j || i == j 优化成 i >= j 的。我用 C++ 实验了一下,是如此。)


继续加油!:)

0
1
讲武德的年轻人
好的,谢谢老师!
2022-07-20
共1条回复

玩转算法面试-- Leetcode真题分门别类讲解

课程配套大量BAT面试真题,高频算法题解析,强化训练

7408 学习 · 1150 问题

查看课程