快速排序都会写, 但这几个问题你回答得出来么?

896 阅读2分钟

912. 快速排序

虽然大家都能默写出快速排序, 但是这几个问题是我在今天一个等于符号错误的时候引申出来的, 记录一下, 本文现讲正确性, 再谈优化

为什么一定要先从右边开始?? 能不能先从左边开始?

其实不一定要先从右边开始, 当你要升序排序的时候, 从右边开始, 反之亦然

从右边开始是因为要保证left指向的值在退出循环时永远小于等于pivot, 保证随后的中枢值归位swap(nums, pivot_index, left);的正确性.

正确性

partition判断时的等号,什么时候取,为什么?

先别往下看, 可以试着猜想下

image.png

image.png

image.png

image.png

这个问题我在没有debug之前想了很久, 后来发现, 其实简单地在nums[low]pivot 作判断的那一行打上断点, 就能轻松解决.

错误的: image.png

我们发现, 如果判断条件是 nums[low] < pivot的话, 这个条件其实永远不会满足, 因为 low 初始时指向的是pivot, 也就是 nums[low] = pivot 成立, 自己和自己比较, 自己当然不会比自己小, 所以 nums[low] < pivot 的写法是错误的!!!

有的同学可能会说, 如果是因为自身和自身比较的话, 那我先把left++是不是就是可以了呢? 其实这样更是大错特错, 因为把partition的区间无缘由地缩小了1, 抛弃了最左边的元素.

设想这样的情形, 原数组是有序的, 即选取了最小的数作为pivot, 如果left++后, 退出循环时left为最小的数右边, 最后还要进行交换swap(nums, pivot_index, left);, 直接把最小的数换到第二位去了, 更是不可取.

关于 nums[high]pivot 作比较的情况, leetcode 提交验证显示都能通过, 我们可以这样想, 如果nums[high]pivot 作比较时加了=号, 即while (left < right && nums[right] > pivot), 那么等于pivot的元素分布于partition操作的两端, 否则只会出现在左半部分.

按照语义来讲, 应该采用的是:

 public int partition(int[] nums, int left, int right) {
        int pivot_index = left;
        int pivot = nums[pivot_index];
        while (left < right) {
            while (left < right && nums[right] > pivot)
                right--;
            while (left < right && nums[left] <= pivot)
                left++;
            if (left < right)
                swap(nums, left, right);
        }
        swap(nums, pivot_index, left);
        return left;
    }

partition策略? 快慢指针, 左右指针?

左右指针就是上面那四个图的做法, 其实还有一种做法, 是快慢指针, 如下

public int partition(int[] nums, int left, int right) {
        int pivot = nums[left];
        int smaller_tail_index = left;
        for (int i = left + 1; i <= right; i++) {
            if (nums[i] < pivot) {
                smaller_tail_index++;
                swap(nums, smaller_tail_index, i);
            }
        }
        swap(nums, smaller_tail_index, left);
        return smaller_tail_index;
    }

Time / Spcace Complexity

复杂度分析:

  • 时间复杂度:O(NlogN),这里 N 是数组的长度, 最坏情况下为O(N^2)
  • 空间复杂度:O(logN),这里占用的空间来自递归函数的栈空间

什么时候最坏?

每次partition操作都导致左右分割极不均衡, 递归树的深度由O(logN)变为链表的O(N)

之后优化也就是在递归树上做文章

优化

partition_random的优化? random函数?

使用random()函数来进行优化非常简单, 只需要随机选一个数, 和最左边的元素交换位置即可, 之后进入正常的partition操作

 public int partition_random(int[] nums, int left, int right) {
        int random_index = new Random().nextInt(right - left + 1) + left;
        swap(nums, random_index, left);
        return partition(nums, left, right);
    }

partition_random? 三数取和?

有了随机选取, 为什么还要 三数取和?

虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴, 它每次选取的一定不会是最坏的情况, 反而每次都是最好的情况

可以这么理解, 三个数, 排序

  1. 首先排序最右边两个数, middleright, 选出大的, 放到最右边
  2. 然后, 我们排序一个数left右边两个数, 因为右边两个数中较大者已经在末尾了, 所以我们比较leftright 即可
  3. 最后, 我们把三个数中中间的那个数放到最开头left的位置. 因为此时三个数中最大的数已经放到right位置上了, leftmiddle中较大的数既是三个数中中间大的数, 我们比较leftmiddle即可
    public int partition_random(int[] nums, int left, int right) {
        int middle = left + (right - left) >> 1;
        if (nums[middle] > nums[right]) swap(nums, middle, right);
        if (nums[left] > nums[right]) swap(nums, left, right);
        if (nums[middle] > nums[left]) swap(nums, left, middle);
        return partition(nums, left, right);
    }

使用三数取中选择枢轴优势还是很明显的,但是还是处理不了重复数组

优化相同的数, 放到数组的中间, 之后递归时直接不需要再次参与递归

此代码纯原创, 跑过了leetcode, 还是很开心的呀hhh!

思路如下:

  1. 使用same_as_pivot_num记录和pivot相同的数的个数, 并把相同的数都放到数组左边

  2. 把和pivot相同的数(位于数组的左边)交换到和pivot相邻的左边

  3. 然后使用数组, 把和pivot相同的数的个数same_as_pivot_numpivot都传参传递出去

  4. 向左边递归时即可跳过相同的元素!!!

有注释, 基于之前的代码结构改变, 看懂前面的看这个小 case

public void quickSort(int[] nums, int start, int end) {
        if (end <= start) {
            return;
        }
        int partition[] = partition(nums, start, end);
        int partition_index = partition[0];
        int same_as_pivot_num = partition[1];
        quickSort(nums, start, partition_index - same_as_pivot_num - 1);
        quickSort(nums, partition_index + 1, end);
    }


    public int[] partition(int[] nums, int left, int right) {
        int pivot_index = left;
        int pivot = nums[pivot_index];
        int same_as_pivot_num = 0;
        while (left < right) {
            while (left < right && nums[right] > pivot)
                right--;
            while (left < right && nums[left] <= pivot)
                left++;
            if (left < right) {
                swap(nums, left, right);
                // 注意这里left和right已经交换了, 且交换前只有left有可能==pivot, 所以现在只需要判断right就行, 并把他们放到数组刚开始的位置
                if (nums[right] == pivot)
                    swap(nums, left + 1 + same_as_pivot_num++, left);
            }
        }
        swap(nums, pivot_index, left);
        for (int i = 0; i < same_as_pivot_num; i++) {
            swap(nums, left + i, pivot_index - 1 - i);
        }
        return new int[]{left, same_as_pivot_num};
    }

更多优化措施

设置阈值, 结合插入排序

设置阈值, 小于则使用插入排序

这个很简单, 就不写了

疑似尾递归优化???查了资料, 发现并不是严格的尾递归优化策略, 这点存疑, 请大佬解答