912. 快速排序
虽然大家都能默写出快速排序, 但是这几个问题是我在今天一个等于符号错误的时候引申出来的, 记录一下, 本文现讲正确性, 再谈优化
为什么一定要先从右边开始?? 能不能先从左边开始?
其实不一定要先从右边开始, 当你要升序排序的时候, 从右边开始, 反之亦然
从右边开始是因为要保证left
指向的值在退出循环时永远小于等于pivot
, 保证随后的中枢值归位swap(nums, pivot_index, left);
的正确性.
正确性
partition判断时的等号,什么时候取,为什么?
先别往下看, 可以试着猜想下
这个问题我在没有debug之前想了很久, 后来发现, 其实简单地在nums[low]
和 pivot
作判断的那一行打上断点, 就能轻松解决.
错误的:
我们发现, 如果判断条件是 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),要缓解这种情况,就引入了三数取中选取枢轴, 它每次选取的一定不会是最坏的情况, 反而每次都是最好的情况
可以这么理解, 三个数, 排序
- 首先排序最右边两个数,
middle
和right
, 选出大的, 放到最右边 - 然后, 我们排序一个数
left
和右边两个数
, 因为右边两个数
中较大者已经在末尾了, 所以我们比较left
和right
即可 - 最后, 我们把三个数中中间的那个数放到最开头
left
的位置. 因为此时三个数中最大的数已经放到right
位置上了,left
和middle
中较大的数既是三个数中中间大的数, 我们比较left
和middle
即可
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!
思路如下:
-
使用
same_as_pivot_num
记录和pivot
相同的数的个数, 并把相同的数都放到数组左边 -
把和
pivot
相同的数(位于数组的左边)交换到和pivot
相邻的左边 -
然后使用数组, 把和
pivot
相同的数的个数same_as_pivot_num
和pivot
都传参传递出去 -
向左边递归时即可跳过相同的元素!!!
有注释, 基于之前的代码结构改变, 看懂前面的看这个小 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};
}
更多优化措施
设置阈值, 结合插入排序
设置阈值, 小于则使用插入排序
这个很简单, 就不写了