【LeetCode选讲·第十六期】「搜索旋转排序数组」「在排序数组中查找元素的第一个和最后一个位置」

141 阅读3分钟

T33 搜索旋转排序数组

题目链接:leetcode-cn.com/problems/se…

朴素解法

看到在有序数组中搜索目标数的问题,我们首先想到的就是「顺序查找」和「二分查找」。不言而喻,后者的效率相对较高,肯定是我们处理有序数组搜索问题的优先选择。

因此本题很容易想到的一个思路就是:我们先通过「顺序查找」定位旋转下标k,然后再对整个数组进行「二分查找」。

代码如下:

const search = (nums, target) => {
    const len = nums.length;
    let k = 0;
    for (let i = 0; i < len - 1; i++) {
        //如果在确定k的过程中已经发现结果,则直接返回答案
        if (nums[i] === target) {
            return i;
        }
        if (nums[i + 1] < nums[i]) {
            k = i + 1;
            break;
        }
    }
    //如果找到k之后仍未发现target,
    //则使用二分查找搜索数组中位居k右侧的元素
    let i = k;
    let j = len - 1;
    while (i <= j) {
        //将(i + j)右移一位,效果等同于Math.floor((i + j) / 2);
        let mid = (i + j) >> 1;
        let numMid = nums[mid];
        if (numMid === target) {
            return mid;
        }
        else if (numMid > target) {
            j = mid - 1;
        }
        else {
            i = mid + 1;
        }
    }
    return -1;
};

纯二分解法

实际上,我们从上面的「朴素解法」中能够发现一件很可笑的事情。

在数组长度较长的前提下,倘若转换点k在位于数组偏后方的位置,那么在顺序查找k的过程中我们基本已经将整个数组遍历了一遍,二分查找的性能优势就无法得以彰显了。

有没有办法能够保证二分查找的性能优势得以充分发挥?

答案是肯定的。事实上,二分查找的作用并不局限于在有序数列中查找某个数,这只是它的一种应用罢了。二分查找真正的威力在于,它适用于任何可以依照数组内元素某种性质被划分成两段的数组。

我们就拿本题为例。在本题中,数组nums被转换点k分割成区间分别为[0, k - 1][k, -1]的两段。很容易理解,对于前半段中的元素,满足≥ nums[0]的性质;对于后半段中的元素,满足< nums[0]的性质。我们可以根据它们在大小这个性质上的差异运用二分查找来取代顺序查找,更快地确定转换点k的位置。

代码如下:

const search = (nums, target) => {
    const len = nums.length;
    /* 第一轮二分:查找k的位置 */
    let s = 0;
    let t = len - 1;
    while (s <= t) {
        let mid = (s + t) >> 1;
        let numMid = nums[mid];
        //如果中途发现答案就直接返回
        if (numMid === target) return mid;
        //mid落在左区间内就向右查找k
        if (numMid >= nums[0]) {
            s = mid + 1;
        }
        //mid落在右区间内就向左查找k
        else {
            t = mid - 1;
        }
    }
    /* 第二轮二分:查找target的位置 */
    let i = null;
    let j = null;
    //先根据target的大小确定查找数组的哪半边
    if (target >= nums[0]) {
        i = 0;
        j = t;
    } else {
        i = s;
        j = len - 1;
    }
    while (i <= j) {
        let mid = (i + j) >> 1;
        let numMid = nums[mid];
        if (numMid === target) {
            return mid;
        }
        else if (numMid > target) {
            j = mid - 1;
        }
        else {
            i = mid + 1;
        }
    }
    return -1;
};

这里必须说明的是,二分查找只有在处理长度较大的数组时才具有明显的性能优势。因此如果我们想要进一步优化上面的代码,可以设计数组长度不同时采用不同算法进行查找的规则,这也是 V8 JavaScript Engine 常用的优化策略。但这部分内容不是本文的重点,就交由大家自行探索了~

T34 在排序数组中查找元素的第一个和最后一个位置

题目链接:leetcode-cn.com/problems/fi…

朴素解法

本题原理上与先前我们做过的「搜索插入位置」一致,我们只需要分别动用两次二分搜索确定左右端点即可。

代码如下:

const defaultAns = [-1, -1];
const searchRange = (nums, target) => {
    const end = nums.length - 1;
    /* 第一轮查找:查找左边界 */
    let i = 0;
    let j = end;
    //第二轮查找时,以第一轮查找
    //中遇见的最右侧target下标为起点。
    let s = null;
    while (i <= j) {
        let mid = (i + j) >> 1;
        let midNum = nums[mid];
        if (midNum === target) {
            j = mid - 1;
            s !== null && (s = mid);
        }
        else if (midNum > target) {
            j = mid - 1;
        }
        else {
            i = mid + 1;
        }
    }
    //如果发现数组中不存在target,直接返回结果
    if (nums[i] !== target) return defaultAns;
    /* 第二轮查找:查找右边界 */
    let t = end;
    while (s <= t) {
        let mid = (s + t) >> 1;
        let midNum = nums[mid];
        if (midNum <= target) {
            s = mid + 1;
        } else {
            t = mid - 1;
        }
    }
    return [i, t];
};

写在文末

我是来自学生组织江南游戏开发社的PAK向日葵,我们目前正在致力于开发自研的非营利性网页端同人游戏《植物大战僵尸:旅行》

我们诚挚邀请您体验我们作品。如果您喜欢TA的话,欢迎向您的同事和朋友推荐,您的支持是我们最大的动力! QQ图片20220701165008.png