【leetcode】153.寻找旋转排序数组中的最小值

87 阅读4分钟

leetcode-153.png

错误的解法

这里利用mid还有left所处位置的值来进行比较 在这里忽略掉了一个最基本的测试用例,即没有旋转过的数组,此时nums[mid] > nums[left]成立,但是向后面寻找目标,这里就错了。

var findMin = function (nums) {
    let left = 0,
        right = nums.length - 1;
    while (left < right) {
        let mid = Math.floor((left + right) / 2);
        if (nums[mid] > nums[left]) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return nums[left];
};

改进后的解法

这里就更细节的判断了是不是没有旋转的数组,在内部进行返回
第六行,>= 是为了 确保 mid === left
第七行,不能去掉 =,否则会误判只有一个元素的情况,也无法正确识别某些未旋转数组。

var findMin = function (nums) {
    let left = 0,
        right = nums.length - 1;
    while (left < right) {
        let mid = Math.floor((left + right) / 2);
        if (nums[mid] >= nums[left]) {
            if (nums[left] <= nums[right]) return nums[left];
            left = mid + 1;
        } else {
            right = mid;
        }
    }
    return nums[left];
};

判断mid与right

  • 判断最小值所在区间

    • 如果 nums[mid] > nums[right],说明最小值在右半部分,因此将 left 指针右移到 mid + 1
    • 否则,最小值在左半部分(包含 mid),因此将 right 指针左移到 mid
  • 循环条件

    • 使用 left < right,确保在最小值区域时,left 指针不会越界。
  • 返回结果

    • 当循环结束时,left 指针指向数组中的最小值。
var findMin = function(nums) {
    let left = 0, right = nums.length - 1;
    while (left < right) {
        let mid = Math.floor((left + right) / 2);
        // 如果中间值大于最右值,说明最小值在右半部分
        if (nums[mid] > nums[right]) {
            left = mid + 1;
        } else {
            // 否则最小值在左半部分
            right = mid;
        }
    }
    return nums[left]; // 最小值
};

跟之前见到过的二分查找的标准模版不同

// 1
var binarySearch = function(nums, target) {
    let left = 0, right = nums.length - 1;

    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        
        // 找到目标值
        if (nums[mid] === target) {
            return mid;
        }

        // 如果中间值大于目标值,则目标值在左半部分
        if (nums[mid] > target) {
            right = mid - 1;
        } else {
            // 否则目标值在右半部分
            left = mid + 1;
        }
    }

    // 没找到返回 -1
    return -1;
};

// 2
var binarySearch = function(nums, target) {
    let left = 0, right = nums.length;

    while (left < right) {
        let mid = Math.floor((left + right) / 2);
        
        // 如果中间值小于目标值,则向右侧查找
        if (nums[mid] < target) {
            left = mid + 1;
        } else {
            // 否则继续向左侧查找(包括相等情况)
            // 左开右闭,所以不是 mid - 1Ï
            right = mid;
        }
    }

    // 最终 left == right,判断是否找到目标值
    return (left < nums.length && nums[left] === target) ? left : -1;
};

场景 1:查找目标元素

  1. 第一种写法 while(left <= right)

    • 当目标元素在数组中时,它能够正确地找到目标元素的索引。
    • 在所有迭代中,左右边界始终保持有效,当 left == right 时依然可以继续查找,因为 right 是有效边界。
    • 若目标元素不存在,会在 left > right 时退出循环。
  2. 第二种写法 while(left < right)

    • 查找目标元素时,right 是不包含的,因此边界处理稍微复杂。终止条件是 left == right,也就是数组被缩减到一个元素。
    • 如果要查找第一个大于等于目标值的元素或者目标值首次出现的位置,这种方式更适合。

场景 2:查找第一个满足条件的元素(如重复元素)

  1. 第一种写法 while(left <= right)

    • 对于包含重复元素的数组,第一种方式可能无法返回第一个目标值的索引,因为其目的是找到任意目标元素,而不是第一个或最后一个。
    • 它在左右边界的调整上,更偏向于通用二分查找。
  2. 第二种写法 while(left < right)

    • 由于 right 的边界是不包含的,因此这一方式可以有效找到数组中第一个满足条件的元素。比如,当 nums[mid] == target 时,继续将 right = mid 缩小范围,以保证找到第一个出现的目标值。

场景 3:效率与边界处理

  1. 第一种写法

    • 对于较标准的二分查找需求,第一种写法足够简洁,并且直接。由于它包含了整个数组范围,处理边界问题会更加直观。
  2. 第二种写法

    • 对于查找第一个满足条件的场景,第二种写法更为高效,并且由于 right 是不包含的边界,它可以在某些特殊查找需求中节省迭代步骤,特别是对查找第一个或最后一个元素的场景。

总结:

  • while(left <= right)  更适用于标准的二分查找,查找的是某个特定目标值的位置,处理左右闭合的数组区间。适合在找特定目标值时使用。
  • while(left < right)  更适合查找满足条件的第一个位置(如第一个大于等于目标值的位置),处理左闭右开区间。适合在处理有重复元素时寻找第一个或最后一个目标元素