目录
排序算法解析
快排
快排亲兄弟:快速选择算法详解(这个比较好,更容易懂)
https://mp.weixin.qq.com/s/TRO3FOKT90Mpvn3hQWVBAQ
快排递归与非递归--Java实现
https://mp.weixin.qq.com/s/ChM5AMCpu43MIydaXMvdVw
快速选择算法比较巧妙,时间复杂度更低,是快速排序的简化版,一定要熟悉思路。
我们先从快速排序讲起。
快速排序的逻辑是,若要对nums[lo..hi]
进行排序,我们先找一个分界点p
,通过交换元素使得nums[lo..p-1]
都小于等于nums[p]
,且nums[p+1..hi]
都大于nums[p]
,然后递归地去nums[lo..p-1]
和nums[p+1..hi]
中寻找新的分界点,最后整个数组就被排序了。
快速排序的代码如下:
/* 快速排序主函数 */
void sort(int[] nums) {
// 一般要在这用洗牌算法将 nums 数组打乱,
// 以保证较高的效率,我们暂时省略这个细节
sort(nums, 0, nums.length - 1);
}
/* 快速排序核心逻辑 */
void sort(int[] nums, int lo, int hi) {
if (lo >= hi) return;
// 通过交换元素构建分界点索引 p
int p = partition(nums, lo, hi);
// 现在 nums[lo..p-1] 都小于 nums[p],
// 且 nums[p+1..hi] 都大于 nums[p]
sort(nums, lo, p - 1);
sort(nums, p + 1, hi);
}
关键就在于这个分界点索引p
的确定,我们画个图看下partition
函数有什么功效:
索引p
左侧的元素都比nums[p]
小,右侧的元素都比nums[p]
大,意味着这个元素已经放到了正确的位置上,回顾快速排序的逻辑,递归调用会把nums[p]
之外的元素也都放到正确的位置上,从而实现整个数组排序,这就是快速排序的核心逻辑。
那么这个partition
函数如何实现的呢?看下代码:
int partition(int[] nums, int lo, int hi) {
if (lo == hi) return lo;
// 将 nums[lo] 作为默认分界点 pivot
int pivot = nums[lo];
// j = hi + 1 因为 while 中会先执行 --
int i = lo, j = hi + 1;
while (true) {
// 保证 nums[lo..i] 都小于 pivot
while (nums[++i] < pivot) {
if (i == hi) break;
}
// 保证 nums[j..hi] 都大于 pivot
while (nums[--j] > pivot) {
if (j == lo) break;
}
if (i >= j) break;
// 如果走到这里,一定有:
// nums[i] > pivot && nums[j] < pivot
// 所以需要交换 nums[i] 和 nums[j],
// 保证 nums[lo..i] < pivot < nums[j..hi]
swap(nums, i, j);
}
// 将 pivot 值交换到正确的位置
swap(nums, j, lo);
// 现在 nums[lo..j-1] < nums[j] < nums[j+1..hi]
return j;
}
// 交换数组中的两个元素
void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
完整代码,自己实现的
package leetcode;
import java.util.Arrays;
import java.util.Stack;
public class QuickSort {
public static void main(String[] args) {
int a[] = new int[]{1, 2, 5, 4, 1, 2, 6};
sort(a, 0, a.length - 1);
System.out.println(Arrays.toString(a));
}
static void sort(int[] a, int left, int right) {
if (left < right) {
//主逻辑
int divide_index = divide(a, left, right);
//递归
sort(a, left, divide_index - 1);
sort(a, divide_index + 1, right);
}
}
static int divide(int[] a, int left, int right) {
int mid = left;
int pivot = a[mid];
while (left < right) {
//基准从左边开始,那右指针先走
while (left < right && a[right] >= pivot) {
right = right - 1;
}//退出a[right] < pivot
while (left < right && a[left] <= pivot) {
left = left + 1;
}//退出a[left] > pivot
//那此时两个不符合条件的位置要进行交换
if (left < right) {
swap(a, left, right);
}
}
//此时在left或者right位置上,已经满足左半边小于pivot,右半边大于pivot。
// 那我们需要把pivot放到正确的位置上(left或者right位置)
swap(a, mid, left);
return left;
}
static void swap(int[] a, int left, int right) {
int temp = a[left];
a[left] = a[right];
a[right] = temp;
}
}
数组中的第 K 个最大元素
力扣第 215 题「数组中的第 K 个最大元素」就是一道类似的题目,函数签名如下:
int findKthLargest(int[] nums, int k);
只不过题目要求找第k
个最大的元素,和我们刚才说的第k
大的元素在语义上不太一样,题目的意思相当于是把nums
数组降序排列,然后返回第k
个元素。
比如输入nums = [2,1,5,4], k = 2
,算法应该返回 4,因为 4 是nums
中第 2 个最大的元素。
这种问题有两种解法,一种是二叉堆(优先队列)的解法,另一种就是标题说到的快速选择算法(Quick Select),我们分别来看。
二叉堆解法
二叉堆的解法比较简单,实际写算法题的时候,推荐大家写这种解法,先直接看代码吧:
int findKthLargest(int[] nums, int k) {
// 小顶堆,堆顶是最小元素
PriorityQueue<Integer>
pq = new PriorityQueue<>();
for (int e : nums) {
// 每个元素都要过一遍二叉堆
pq.offer(e);
// 堆中元素多于 k 个时,删除堆顶元素
if (pq.size() > k) {
pq.poll();
}
}
// pq 中剩下的是 nums 中 k 个最大元素,
// 堆顶是最小的那个,即第 k 个最大元素
return pq.peek();
}
二叉堆(优先队列)是比较常见的数据结构,可以认为它会自动排序,我们前文 手把手实现二叉堆数据结构 实现过这种结构,我就默认大家熟悉它的特性了。
看代码应该不难理解,可以把小顶堆pq
理解成一个筛子,较大的元素会沉淀下去,较小的元素会浮上来;当堆大小超过k
的时候,我们就删掉堆顶的元素,因为这些元素比较小,而我们想要的是前k
个最大元素嘛。当nums
中的所有元素都过了一遍之后,筛子里面留下的就是最大的k
个元素,而堆顶元素是堆中最小的元素,也就是「第k
个最大的元素」。
二叉堆插入和删除的时间复杂度和堆中的元素个数有关,在这里我们堆的大小不会超过k
,所以插入和删除元素的复杂度是O(logK)
,再套一层 for 循环,总的时间复杂度就是O(NlogK)
。空间复杂度很显然就是二叉堆的大小,为O(K)
。
这个解法算是比较简单的吧,代码少也不容易出错,所以说如果笔试面试中出现类似的问题,建议用这种解法。唯一注意的是,Java 的PriorityQueue
默认实现是小顶堆,有的语言的优先队列可能默认是大顶堆,可能需要做一些调整。