07.2-排序算法之快速排序思想相关算法

37 阅读4分钟

前言

我们熟悉了快速排序的思想与其实现原理之后,可以在我们实际开发中带来很多的启发与灵感,借助快速排序双指针的游走的思想,我们也可以解决很多类快排问题。现在就来借助一些算法题巩固一下对于讨论的快排思想。

刷题

剑指 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;
};