浅谈二分

二分查找

前言

视频链接:二分查找入门_哔哩哔哩

本质

逐步缩小搜索区间,减治思想,排除法

如何使用二分查找

将求值问题转化为判断问题

前提

使用二分查找的前提:

  • 访问的值具有随机访问特性(如数组可以根据下标 \(O(1)\) 的时间复杂度访问元素值,而链表需要遍历)
  • "有序"(不是有序的数组也可以使用 二分查找,这一类题是可以根据某一位置的值,来推测出两侧元素的性质,进而缩小搜索区间,被称作二分答案

基本思路

  1. 确定 查找区间范围 \([left,right]\)。任何时刻 \([left,right]\) 都有可能是目标结果。
  2. 确定 \(while\) 循环条件,它代表着循环可以继续的条件
    • \(while\) 循环条件为 while(left<right) 时,循环退出 \(left=right\),它们指向同一个元素,即目标元素
    • \(while\) 循环条件为 while(left<=right) 时,循环退出 \(left>right\),它们指向不同的元素,此时无对应结果
  3. 循环体内是两个判断还是三个判断?见疑问。
  4. 确定对应判断如何缩小搜索区间,即确定下一搜索区间(\(mid\) 位置是否应该划入下一搜索区间)。
  5. 根据下一搜索区间判断 \(mid\) 取值是否要 \(+1\),即上取整。
  6. \(mid\) 是否会整型溢出。若会整型溢出,则将其改为 \(mid=left+\left\lfloor\dfrac{right-left}{2}\right\rfloor\)\(mid=left+\left\lfloor\dfrac{right-left+1}{2}\right\rfloor\)
    Java语言可以使用int mid = left + right >>> 1无符号右移来规避

疑问

判断条件个数

问:什么时候循环体内写成三个判断,什么时候写成两个?

  1. 三个判断(如折半查找):
    • 可以在循环体内返回结果(有一定概率程序提前终止)
    • 循环体内可以分为三种情况讨论。\(mid\) 左半部分,\(mid\) 部分, \(mid\) 右半部分。
  2. 两个判断(如求某个值第一次出现或最后一次出现的位置):
    • 必须要退出循环后才可以返回结果(因为无法设计出一个条件 表示 第一次或最后一次出现)
    • 只把区间分成两个部分,这是因为:只有这样,退出循环的时候才有 \(left\)\(right\) 重合,我们才敢说,找到了问题的答案。

mid 是否上取整

问:\(mid\) 取值何时要 \(+1\) (即上取整)?

结论:我们只需要在编写完二分查找,判断区间元素剩余 \(2\) 个时是否会死循环,如果死循环就将其调整为上取整。

  1. 当区间内个数有偶数个元素时,\(mid=\left\lfloor\dfrac{right+left}{2}\right\rfloor\) 只能取到位于左边的中位数,想要取到右边的中位数就需要 \(mid=\left\lfloor\dfrac{right+left+1}{2}\right\rfloor\)
  2. 为什么要取右边的中位数?这是因为当区间之中只有 \(2\) 个元素时,把区间划分成 \([left,mid-1]\)\([mid,right]\), 对于 \(mid=\left\lfloor\dfrac{right+left}{2}\right\rfloor\) 取左边中位数并不能缩小搜索区间
  3. 上取整的目的:只是为了 避免死循环

上取整的正确性:

\(mid\) 是为了求中位数:

  1. 对于区间内元素个数为奇数个,中位数个数唯一,则上取整与下取整相同。
  2. 对于区间内元素个数为偶数个,中位数个数有两个,选择左边中位数与右边中位数均可。

例题

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

题目链接:https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/

class Solution {
    // 查找元素的第一个位置
    int findFirstPosition(int[] nums, int left, int right, int target) {
        while (left < right) {
            int mid = left + right >>> 1;
            if (nums[mid] >= target){
                // 区间向左收缩,且mid可能为答案
                right = mid;
            } else {
                // 区间向右收缩,且mid不可取
                left = mid + 1;
            }
        }
        return left;
    }

    // 查找元素的最后一个位置
    int findLastPosition(int[] nums, int left, int right, int target) {
        while (left < right) {
            // 会死循环,选择上取整
            int mid = left + right + 1 >>> 1;
            if (nums[mid] > target){
                right = mid - 1;
            }else {
                left = mid;
            }
        }
        return left;
    }

    public int[] searchRange(int[] nums, int target) {
        int len = nums.length;
        if (len == 0) return new int[]{-1, -1};
        int start = findFirstPosition(nums, 0, len-1, target);
        if (nums[start] != target) return new int[]{-1, -1};
        int end = findLastPosition(nums, 0, len-1, target);
        return new int[]{start, end};
    }
}

35. 搜索插入位置

题目链接:https://leetcode.cn/problems/search-insert-position/

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0, right = nums.length;// 可能插入到length位置
        while (left < right) {
            int mid = left + right >> 1;
            if (nums[mid] == target) {
                //找到了插入位置
                return mid;
            } else if (nums[mid] < target) {
                left = mid + 1;
            } else {
              right = mid;// mid 位置可能为结果
            }
        }
        return left;
    }
}

69. x 的平方根 - 力扣

题目链接:https://leetcode.cn/problems/sqrtx/description/

class Solution {
    public int mySqrt(int x) {
        if (x <= 1) return x;
        int left = 1, right = x / 2;
        while (left < right) {
            int mid = left + right + 1 >>> 1;
            // 如果mid*mid大于x,则下一个搜索区间为[left,mid-1]
            // 因为乘法可能溢出,使用除法计算
            if (mid > x / mid) right = mid - 1;
            // 如果如果mid*mid小于等于x,则下一个搜索区间为[mid,right]
            else left = mid;
        }
        return left;
    }
}

162. 寻找峰值

题目链接:https://leetcode.cn/problems/find-peak-element/

public class Solution {

    public int findPeakElement(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        // 在 nums[left..right] 中查找峰值
        while (left < right) {
            int mid = left + right >>> 1;
            
            if (nums[mid] < nums[mid + 1]) {
                // 上升,右侧一定有峰值
                // 下一轮搜索的区间 [mid + 1..right]
                left = mid + 1;
            } else {
                // 下降,左侧一定有峰值,且 mid 可能为峰值
                // 下一轮搜索的区间 [left..mid]
                right = mid;
            }
        }
        return left;
    }

}

278. 第一个错误的版本- 力扣

题目链接:https://leetcode.cn/problems/first-bad-version/description/

/* The isBadVersion API is defined in the parent class VersionControl.
      boolean isBadVersion(int version); */
public class Solution extends VersionControl {
    public int firstBadVersion(int n) {
        int left = 1, right = n, mid;
        while (left < right) {
            mid = left + right >>> 1;
            if (isBadVersion(mid)) right = mid;
            else left = mid + 1;
        }
        return left;
    }
}

287. 寻找重复数

题目链接:https://leetcode.cn/problems/find-the-duplicate-number/

class Solution {
    public int findDuplicate(int[] nums) {
        //  查找[left,right]范围内的重复的数
        int left = 1, right = nums.length - 1;
        while (left < right) {
            int mid = left + right >>> 1;
            // 抽屉(鸽巢)原理:n个抽屉,n+1个苹果,一定有一个抽屉放了不止一个苹果
            int sum = 0;
            for (int v : nums) {
                if (v <= mid) {
                    ++sum;
                }
            }
            // [1...mid]个数 大于 mid
            // 则说明 [left,mid] 内有重复数
            if (sum > mid) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
}

367. 有效的完全平方数

题目链接:https://leetcode.cn/problems/valid-perfect-square/

class Solution {
    public boolean isPerfectSquare(int num) {
        if(num == 1) return true;
        int left = 1, right = num / 2;
        // 找第一个平方大于等于num的整数
        while (left < right) {
            int mid = left + right >>> 1;
            if (mid >= num / mid) {
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return (long)left * left == num;
    }
}

374. 猜数字大小

题目链接:https://leetcode.cn/problems/guess-number-higher-or-lower/

/**
 * Forward declaration of guess API.
 *
 * @param num your guess
 * @return -1 if num is higher than the picked number
 * 1 if num is lower than the picked number
 * otherwise return 0
 * int guess(int num);
 */

public class Solution extends GuessGame {
    public int guessNumber(int n) {
        //[left,right]猜测主持人选择的数字
        int left = 1, right = n;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            int res = guess(mid);
            if (res == 0) {
                return mid;
            } else if (res == -1) {
                //下一轮搜索区间为[left,mid-1]
                right = mid - 1;
            } else {
                //下一轮搜索区间为[mid+1,right]
                left = mid + 1;
            }
        }
        return -1;
    }
}

441. 排列硬币 - 力扣

题目链接:https://leetcode.cn/problems/arranging-coins/

求使得 \(1+2+3+...+x\leq n\) 的最大的正整数 \(x\)

化简得:\(\dfrac{x\times (1+x)}{2}\leq n\)

class Solution {
    public int arrangeCoins(int n) {
        //由于n大于1,所以最少能有一行
        int left = 1, right = n;
        while (left < right) {
            int mid = left + right + 1 >>> 1;
            //数值太大,使用long类型
            long sum = (1l + mid) * mid >> 1;
            if (sum > n) right = mid - 1;
            else left = mid;
        }
        return left;
    }
}

540. 有序数组中的单一元素

题目链接:https://leetcode.cn/problems/single-element-in-a-sorted-array/

设仅出现一个数的下标为 \(x\),则其左边的数均出现了两次,右边的数也是如此

由于数组下标从 \(0\) 开始,则 \(x\) 一定为偶数

  1. 对于 \(x\) 左边的数,两个相邻且相等的数的下标为 偶+奇
  2. 对于 \(x\) 右边的数,两个相邻且相等的数的下标为 奇+偶
class Solution {
    public int singleNonDuplicate(int[] nums) {
        int len = nums.length;
        // if (len == 1) return nums[0]; 当只有一个元素时可归为下列情况
        int left = 0, right = len - 1;
        // 最终结果一定位于偶数位置
        while (left < right) {
            int mid = left + right >> 1;
            if (mid % 2 == 0){
                //两个相邻且相等的数的下标为 偶+奇 则 结果在右侧
                if (mid + 1 < len && nums[mid] == nums[mid + 1]) left = mid + 2;
                else right = mid; // mid 可能为答案所在位置
            } else {
                //两个相邻且相等的数的下标为 奇+偶 则 结果在左侧
                if (mid + 1 < len && nums[mid] == nums[mid + 1])  right = mid - 1;
                else left = mid + 1; // mid 不可能为答案所在位置
            }
        }
        return nums[left];
    }
}

744. 寻找比目标字母大的最小字母

题目链接:https://leetcode.cn/problems/find-smallest-letter-greater-than-target/

class Solution {
    public char nextGreatestLetter(char[] letters, char target) {
        int left = 0, right = letters.length - 1;
        while (left < right){
            int mid = left + right >>> 1;
            if (letters[mid] > target) {
                right = mid;
            } else {
                // 字符小于等于target,mid不可取
                left = mid + 1;
            }
        }
        return letters[left] > target ? letters[left] : letters[0];
    }
}

852. 山脉数组的峰顶索引

题目链接:https://leetcode.cn/problems/peak-index-in-a-mountain-array/

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        // 题目保证为山脉数组,则顶点在[1,len-2]
        int left = 1, right = arr.length - 2;
        while (left < right) {
            int mid = left + right >>> 1;
            if (arr[mid] > arr[mid+1]) {
                // 下降趋势,顶点在左侧,mid可能为顶点
                right = mid;
            } else {
                left = mid + 1;
            }
        }
        return left;
    }
}

参考资料

8-1 「二分查找」的基本思想 - liweiwei - 哔哩哔哩

写对二分查找不是套模板并往里面填空,需要仔细分析题意 - liweiwei1419

二分查找 - 算法吧

posted @ 2022-11-30 12:06  Cattle_Horse  阅读(56)  评论(0编辑  收藏  举报