前言
我们熟悉了快速排序的思想与其实现原理之后,可以在我们实际开发中带来很多的启发与灵感,借助快速排序双指针的游走的思想,我们也可以解决很多类快排问题。现在就来借助一些算法题巩固一下对于讨论的快排思想。
刷题
剑指 Offer 21. 调整数组顺序使奇数位于偶数前面
解题思路
这道题的要点就是我们要找到数组中的奇偶数,并将他们按照规则放到他们改在的地方。我们可以借助快排中的双指针思想,让左右游标不断在数组中游走,左游标遇到偶数和右游标遇到奇数时,我们就可以让左右游标对应的值交换,这样就可以实现左边奇数,右边偶数的排序了。
代码实现
function exchange(nums: number[]): number[] {
// 判断边界
if(nums.length===0) return nums;
// 定义双指针
let x = 0, y = nums.length-1;
// 由于我们进行了边界判断,因此,初始时x一定是大于y的,因此此处使用do...while循环,可以减少一些条件的判断
do {
// 左游标没有到达有边界并且左游标所指向的元素是奇数的话,就让左游标一直向右移,直到遇到偶数位置
// num[x] & 1 利用二进制的与运算判断奇偶数,效率会高一些,与num[x]%2等效
while(x < nums.length && (nums[x] & 1)) x++;
// 同理,当右游标没有到达左边界并且右游标所指向的值为偶数时,让右游标一直向左移,直到遇到奇数
while(y >= 0 && (nums[y] & 1) === 0) y--;
// 经过上面两次移动过后,我们的左右游标已经分别停留在了偶数的位置和奇数的位置,如果这两个游标没有错开,即
// 左游标依然没有超过右游标的话,我们就交换左右游标所对应数组值,这样就可以让奇数放到左边,偶数放到右边
// 交换完之后,只需要让左游标向右走一步,右游标想左走一步,然后进入下一次循环继续处理即可
if(x <= y) {
[nums[x], nums[y]] = [nums[y], nums[x]];
x++;
y--;
}
} while (x <= y);
return nums;
};
75. 颜色分类
解题思路
这道题还是可以利用我们的快排思想,找到中间值作为基准值,然后设立左右指针作为哨兵静待守候,当我们遍历到的元素值是小于基准值时,让左哨兵与其交换,当遍历到的值大于基准值时,让右哨兵与其交换,交换完后别忘了让左右哨兵向中间靠拢。如果遇到等于基准值时,什么都不干,继续遍历下一个元素
代码演示
/*
* @lc app=leetcode.cn id=75 lang=typescript
*
* [75] 颜色分类
*/
// @lc code=start
/**
Do not return anything, modify nums in-place instead.
*/
function sort(arr, l = 0, r = arr.length-1, mid = 1) {
// 如果左右指针重合或越界则直接退出
if(l >= r) return;
// 定义两个哨兵游标,x在最左侧元素的前一位,y在最右侧元素的后一位
// 这样我们就可以少很多条件判断,不管是不是第一个元素,直接x++,
// 不管是不是最后一个元素,直接y--,有点类似我们处理链表问题的哨兵节点
let x = -1, y = r + 1, i = l;
while(i < y) {
if(arr[i] < mid) { // 0
// 如果当前元素是0则让左侧哨兵节点右移一位
x++;
// 然后交换当前节点与哨兵节点所指向的值
[arr[i], arr[x]] = [arr[x], arr[i]];
// 处理完之后,当前节点向右移
i++;
} else if(arr[i] === mid) {
// 如果当前节点是1,那我们就不管他,因为我们只需要把0放在1的左边
// 把2放在1的右边就可以了,1就原地不动,直接右移到下一个节点
i++;
} else if(arr[i] > mid) {
// 如果当前元素是2,则让右边的哨兵向左走一步
y--;
// 然后交换当前节点与哨兵节点的位置
[arr[i], arr[y]] = [arr[y], arr[i]];
}
}
}
function sortColors(nums: number[]): void {
sort(nums);
};
// @lc code=end
面试题 17.14. 最小K个数
解题思路
这题之前我们使用堆排序实现过,现在使用快排思想来解决这个问题,我们只需要对数组进行k轮的快排(注意:无需像数组完整快排,只需要按题意快排k轮,那么数组头k个数就是我们要的答案)
代码演示
// 三点取中间
function getMid(a: number, b: number, c: number): number {
if(a > b) [a, b] = [b, a];
if(a > c) [a, c] = [c, a];
if(b > c) [b, c] = [c, b];
return b;
}
// 使用快排思想实现的快速选择方法
function quickSelect(arr, k, left = 0, right = arr.length-1) {
// 左游标与右游标相遇或错过时退出
if(left >= right) return;
// 获取中间值
let x = left, y = right, mid = getMid(arr[left], arr[Math.floor((left+right)/2)], arr[right]);
do {
// 左边的值小于基准值则继续右移
while (arr[x] < mid) x++;
// 右边的值大于基准值则继续左翼
while (arr[y] > mid) y--;
// 左右指针相遇时交换左右指针对应的值
if(x <= y) {
[arr[x], arr[y]] = [arr[y], arr[x]];
x++,y--;
}
} while (x <= y);
// 我们只需要排序k位就行了,剩下的无需排序
if(y - left === k - 1) return;
if(y - left >= k) { // 做区间数量大于k,扩大范围
quickSelect(arr, k, left, y);
} else {// 做区间数量大于k,缩小范围
quickSelect(arr, k - x + left, x, right);
}
}
function smallestK(arr: number[], k: number): number[] {
// 进行k轮快排
quickSelect(arr, k);
// 取前k位作为答案输出
return arr.slice(0, k);
};
11. 盛最多水的容器
解题思路
这道题也是利用双指针思想来做。我们先来捋一下思路,首先,我们想让接水的面积最大,就要保证宽和高都尽可能大,我们利用双指针,通过左右游标的移动来确定宽的大小(宽就是左右游标中间的长度,也就是右游标的索引减去左游标的索引),由于左右游标都是一直向中间靠拢的,所以宽度肯定是一直在变小的,因此我们的面积大小就取决于高的大小了,我们的高就是左右游标对应的值,又因为实际高取决于比较短的哪一条,因此需要取左右两个游标的最小值与宽计算出面积,在与上一次计算的对比取最大值,这样,循环一遍后,就得到了最大面积了。
代码实现
function maxArea(height: number[]): number {
// res为最大面积,初始为0,l为左游标,r为右游标
let res = 0, l = 0, r = height.length - 1;
// 左右游标没有相遇
while(l < r) {
// 计算面积并与上一次的面积比较取最大值
res = Math.max(res, (r - l) * Math.min(height[l], height[r]));
// 为了确保高尽可能的大,如果左边的高比较小,则让左游标右移,换一个左高
if(height[l] < height[r]) l++;
// 否则右游标左移
else r--;
}
return res;
};