-
快速排序
-
快速排序
-
快速排序就是一个二叉树的前序遍历
-
快速排序是先将一个元素(p)排好序,然后再将剩下的元素排好序
-
排序完成其实形成的 是一棵二叉搜索树,如此可以理解为,快速排序的过程是一个构造 二叉搜索树的过程
即如下二叉搜索树
-
二叉搜索树的构造时,二叉搜索树不平衡的极端情况下二叉搜索树会退化成一个链表,导致操作效率大幅降低,快速排序中也有类似的情况,如图所示
因此要引入随机性:可采用进行排序前对整个数组执行 洗牌算法 进行打乱,或者在 partition函数中随机选择数组元素作为切分点,我倾向于使用洗牌算法
-
-
快速排序的代码框架
void sort(int[] nums, int lo, int hi) { if (lo >= hi) { return; } // 对 nums[lo..hi] 进行切分 // 使得 nums[lo..p-1] <= nums[p] < nums[p+1..hi] int p = partition(nums, lo, hi); // 去左右子数组进行切分 sort(nums, lo, p - 1); sort(nums, p + 1, hi); }
-
快速排序完整代码
public static void quickSort(int[] nums) { shuffle(nums); quickSort(nums, 0, nums.length - 1); } private static void quickSort(int[] nums, int low, int high) { //一个元素或者没有元素不再需要排序 if (high <= low) return; int j = partition(nums, low, high); quickSort(nums, low,j - 1); quickSort(nums,j + 1,high); } private static int partition(int[] nums, int low, int high) { int i = low, j = high + 1; int value = nums[low]; while (true) { //此while结束时恰好nums[i] >= value,即找到一个元素大于value while (nums[++i] - value < 0) { if (i == high) { break; } } //此while结束时恰好nums[j] <= value,即找到一个元素小于value while (nums[--j] - value > 0) { if (j == low) { break; } } if (i >= j) { break; } //将大于value和小于value的两个元素原地交换 swap(nums, i, j); } //j指针指向的就是切分点 swap(nums, low, j); return j; } //洗牌算法,将输入的数组随机打乱 private static void shuffle(int[] nums) { Random random = new Random(); int n = nums.length; for (int i = 0; i < n; i++) { //生成[i, n - 1]的随机数, 即索引 int r = i + random.nextInt(n - i); swap(nums, i, r); } } //原地交换数组中的两个元素 private static void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; }
-
快速排序的时间复杂度
假设数组元素个数为
N
,那么二叉树每一层的元素个数之和就是O(N)
;分界点分布均匀的理想情况下,树的层数为O(logN)
,所以理想的总时间复杂度为O(NlogN)
。 -
与归并排序的比较
快速排序是 不稳定排序,归并排序是 稳定排序
在实际工程中我们经常会将一个复杂对象的某一个字段作为排序的
key
,所以应该关注编程语言提供的 API 底层使用的到底是什么排序算法,是稳定的还是不稳定的,这很可能影响到代码执行的效率甚至正确 -
快速选择算法(Quick Select)
是快速排序的变体,效率更高O(N),看LeetCode215
-
-
LeetCode215 数组中的第K个最大元素
-
思路分析
-
可以使用优先级队列:Java的 PriorityQueue 默认实现是小顶堆,将pq看成一个筛子,较大的元素会沉淀下去,较小的元素会浮上来;当堆大小超过k个元素时,我们就删掉堆顶元素,因为这个元素小;当nums中的所有元素都过了一遍之后,堆中留下了最大的k个元素,堆顶元素就是第k大的元素
二叉堆插入和删除的时间复杂度和堆中的元素个数有关,在这里我们堆的大小不会超过
k
,所以插入和删除元素的复杂度是O(logk)
,再套一层 for 循环,假设数组元素总数为N
,总的时间复杂度就是O(Nlogk)
。 -
可以使用快速选择算法:非常像二分搜索框架
-
快速选择算法是快速排序的变体,效率更高
-
题目问第k大的元素,相当于数组升序排序后找索引为n - k的元素,即k = n - k
-
在partition函数中会将nums[p]放到正确的位置,使得nums[lo…p-1] < nums[p],nums[p+1…hi] > nums[p], 这个时候虽然没有把整个数组排好序,但是nums[p]左边的元素都比nums[p]小了,因此我们知道了nums[p]的排名,因此我们找索引为k的元素,等价于比价k和p的大小
- 如果 k == p,恭喜你,找到了
- 如果 k < p,则表示k在nums[lo…p-1]中,则使 hi = p - 1
- 如果 k > p,则表示k在nums[p+1…hi]中,则使lo = p + 1
-
为什么循环条件时 lo <= hi, 即lo > hi时退出,而不再是快速排序中的lo >= hi
在快速排序中,lo == hi, 我们默认只有一个元素的时候他是有序的,不需要再调用partition
而在快速选择算法中,我们需要知道lo == hi是不是所要找的k
-
注意partition中与快速排序不同点
因为快速选择算法 结束循环条件是 lo > hi,lo == hi时循环继续
为了后面nums[i++]不越界,且一个元素也不需要partition,所以要单独判断lo是否等于hi
if (lo == hi) {
return lo;
} -
时间复杂度为O(N)
-
-
-
代码实现
-
优先级队列
-
快速选择算法
class Solution { public int findKthLargest(int[] nums, int k) { shuffle(nums); int lo = 0; int hi = nums.length - 1; //第k个最大元素,即升序找索引为n - k int reverseK = nums.length - k; //条件为lo <= hi的原因,lo == hi时,区间有一个元素,需要判断该元素即该切分点与reverseK的关系,否则就漏了 while (lo <= hi) { int p = partition(nums, lo, hi); if (p < reverseK) { //即reverseK在切分点右侧 lo = p + 1; } else if (p > reverseK) { //即reverseK在切分点左侧 hi = p - 1; } else { //找到 return nums[p]; } } return -1; } int partition(int[] nums, int lo, int hi) { int i = lo, j = hi + 1; int value = nums[lo]; while (true) { while (nums[++i] - value < 0) { if (i == hi) { break; } } while (nums[--j] - value > 0) { if (j == lo) { break; } } if (i >= j) { break; } swap(nums, i, j); } swap(nums, lo, j); return j; } void shuffle(int[] nums) { Random random = new Random(); int n = nums.length; for (int i = 0; i < n; i++) { //生成[0, n-1] int r = i + random.nextInt(n - i); //交换nums[i]和nums[r] swap(nums, i, r); } } void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } } class Solution { public int findKthLargest(int[] nums, int k) { shuffle(nums); int lo = 0; int hi = nums.length - 1; //第k个最大元素,即升序找索引为n - k int reverseK = nums.length - k; //条件为lo <= hi的原因,lo == hi时,区间有一个元素,需要判断该元素即该切分点与reverseK的关系,否则就漏了 while (lo <= hi) { int p = partition(nums, lo, hi); if (p < reverseK) { //即reverseK在切分点右侧 lo = p + 1; } else if (p > reverseK) { //即reverseK在切分点左侧 hi = p - 1; } else { //找到 return nums[p]; } } return -1; } int partition(int[] nums, int lo, int hi) { //因为快速选择算法 结束循环条件是 lo > hi,lo == hi时循环继续 //为了后面nums[i++]不越界,要单独判断lo是否等于hi //一个元素也不需要partition if (lo == hi) { return lo; } int i = lo, j = hi + 1; int value = nums[lo]; while (true) { while (nums[++i] - value < 0) { if (i == hi) { break; } } while (nums[--j] - value > 0) { if (j == lo) { break; } } if (i >= j) { break; } swap(nums, i, j); } swap(nums, lo, j); return j; } void shuffle(int[] nums) { Random random = new Random(); int n = nums.length; for (int i = 0; i < n; i++) { //生成[0, n-1] int r = i + random.nextInt(n - i); //交换nums[i]和nums[r] swap(nums, i, r); } } void swap(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } }
-
-
快速排序及快速选择算法详解
最新推荐文章于 2024-01-26 10:38:54 发布