硅基计划4.0 算法 二分查找

硅基计划4.0 算法 二分查找


129563571_p0



一、二分查找

题目链接
这道题就是最简单的二分查找题,在正式解题之前,我想先来讲讲二分查找算法
可能大家之前已经了解过了这个算法,我之前文章也有介绍过,但是我想说,二分查找不止于此

我们接下来先讲讲最普通的二分查找算法
二分查找,本质上是二段性,不一定要以1/2作为分割点,1/31/4等等也可以
但是为什么推荐用1/2作为分割点呢,因为这样子时间复杂度是最小的,证明可以去网上搜搜

好,我们二分查找的核心是不是设立两个指针,一个在最左边一个在最右边,然后相互靠拢,当两个指针位置互换了循环就结束了
好,那我的循环条件是写成left<=right还是写成left<right呢?
我们推荐写成left<=right,为什么?
你想,我们每一次二分查找,区间都是未知的,到最后的时候,即使区间收缩成一个点,这个点我们还是未知的,我们还是要进行判断的

好,那我们如何去寻找中间元素呢?
我们为了避免超出数据范围即溢出的风险,我们采用left+(right-left)/2,即左指针加上整体长度的一半
当然,我们求中间元素还有left+(right-left+1)/2
这两个有什么区别呢,我们看到它们区别就是+1的问题

在奇数个元素下,求的中间节点都是一样的,而在偶数个元素下
上面那个求中间元素,求的中间元素是在中间的第一个元素,即[0,1,2,3]中间元素是1
下面那个求中间元素,求的中间元素是在中间的第二个元素,即[0,1,2,3]中间元素是2

好,我们最后来分析时间复杂度,求一次二分,排查剩下 n 2 \frac{n}{2} 2n个元素
求第二次二分,排查剩下 n 4 \frac{n}{4} 4n个元素…求第x次二分,排查剩下 1 2 x \frac{1}{2^x} 2x1
因此等差数列求和后是 2 x = n 2^x=n 2x=n,化简成 x = l o g n x=logn x=logn

class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length-1;
        while(left <= right){
            int middle = left+(right-left)/2;
            if(nums[middle] > target){
                right = middle-1;
            }else if(nums[middle] < target){
                left = middle+1;
            }else{
                return middle;
            }
        }
        return -1;
    }
}

二、排序数组中查找指定元素第一个位置和最后一个位置

题目链接
这道题我们看到是一个有序的数组,那我们就可以利用单调性使用二分查找了

1. 先找左端点

我们把原数组划分成两个区域,一个区域是<target,一个区域是>target

  • 当中间元素值比target小的时候,不可能是我们的最终结果,此时我们的左指针要去中间元素右边寻找,即left = middle +1
  • 当中间元素值>=target时候,可能是我们的最终结果,即我们要找的左端点,也可能不是,因此我们的right = middle

为什么我们的right不能去middle左边呢?因为middle可能正好是我们要找的左端点

好,我们来讨论细节问题

  1. 为什么循环终止条件是left < right呢?
    你想,我们最后如果能找到结果,此时一定left = right,那我们都把周边区域排查完了,此时我们就没必要排查了,这一点和普通二分查找不一样
    还有最重要的是,当我们的原始数组内所有的值都比target目标值大,此时right就会一直不断向左移,直到和left相遇,此时就是我们的最后结果
    当我们的原始数组值都比target小,left就会向右移,直到和right相遇
    上述两种情况,当它们相遇的时候,由于循环是left <= right,它们会一直原地判断,导致死循环
  2. 为什么求中点使用left+(right-left)/2而不使用left+(right-left+1)/2呢?
    image-20250818111138670

2. 再找右端点

跟刚刚找左端点一样,是反着来的

  • 当中间元素值>target的时候,不可能是我们的最终结果,此时我们的右指针要去中间元素左边寻找,即right = middle -1
  • 当中间元素值<=target时候,可能是我们的最终结果,即我们要找的右端点,也可能不是,因此我们的left = middle

为什么我们的left不能去middle右边呢?因为middle可能正好是我们要找的右端点

好,我们继续来讨论细节问题

  1. 为什么循环终止条件还是left < right呢?
    原因和刚刚一样的,相等的时候就是最终结果,无需判断,也避免死循环
  2. 为什么求中点使用left+(right-left+1)/2而不使用left+(right-left)/2呢?
    这里就和刚刚不一样了,我们还是用画图来演示一下
    image-20250818111719431

3. 编写代码

class Solution {
    public int[] searchRange(int[] nums, int target) {
        int [] ret = {-1,-1};
        int length = nums.length;
        if(length == 0){
            return ret;
        }
        if(target > nums[length-1]){
            return ret;
        }
        int left = 0;
        int right = length-1;
        //找左端点
        while(left < right){
            int middle = left+(right-left)/2;
            if(nums[middle] < target){
                left = middle+1;
            }else{
                right = middle;
            }
        }
        //判断左端点是否找到了结果
        if(nums[left] == target){
            ret[0] = left;
        }else{
            return ret;
        }
        //找右端点
        right = length-1;
        while(left < right){
            int middle = left+(right-left+1)/2;
            if(nums[middle] > target){
                right = middle-1;
            }else{
                left = middle;
            }
        }
        ret[1] = right;
        return ret;
    }
}

三、x平方根

题目链接
这一题和上一题类似,我们把数组划分成两个区域

  • middle*middle <= 目标值left = middle
  • middle*middle > 目标值right = middle-1
class Solution {
    public int mySqrt(int x) {
        if(x < 1){
            return x;
        }
        long left = 0;
        long right = x;
        while(left < right){
            long middle = left+(right-left+1)/2;
            if(middle*middle > x){
                right = middle-1;
            }else{
                left = middle;
            }
        }
        return (int)left;
    }
}

四、搜索插入位置

题目链接
这道题唯一需要注意的是如果数组末尾的数(最大数)比目标值还要小的话,说明我们插入的数要插入到数组末尾,即left+1位置(此时left会走到最后一个位置)
否则我们就正常放入left循环结束后的位置就好

class Solution {
    public int searchInsert(int[] nums, int target) {
        int left = 0;
        int right = nums.length-1;
        while(left <= right){
            int middle = left+(right-left)/2;
            if(nums[middle] > target){
                right = middle-1;
            }else if(nums[middle] < target){
                left = middle+1;
            }else{
                return middle;
            }
        }
        //此时说明不存在数组中,看最后一个值是否大于目标值
        return nums[nums.length-1] > target ? right+1 : left;
    }
}

五、山脉数组峰顶索引

题目链接
根据单调性去解决问题就好,直接二分查找就行,不过多赘述

class Solution {
    public int peakIndexInMountainArray(int[] arr) {
        int left = 0;
        int right = arr.length-1;
        while(left < right){
            int middle = left+(right-left)/2;
            if(arr[middle] >= arr[middle+1]){
                right = middle;
            }else{
                left = middle+1;
            }
        }
        return left;
    }
}

六、寻找峰值

题目链接
这题因为只需要返回一个峰值就好,和上一题一模一样的代码

class Solution {
    public int findPeakElement(int[] nums) {
        int left = 0;
        int right = nums.length-1;
        while(left < right){
            int middle = left+(right-left)/2;
            if(nums[middle] >= nums[middle+1]){
                right = middle;
            }else{
                left = middle+1;
            }
        }
        return left;
    }
}

七、寻找旋转排序数组最大值

题目链接
这题数组特点就是从头到尾先增大,后迅速减小,又增大,像两个坡
但是你观察到,因为旋转之前是有序的数组,因此我们可以很明确的直到,数组最左边的值一定是大于数组最右边的值的,不信您可以看看示例
我们可以利用这个特性,以数组末尾的数作为参照,我们分为两种情况
image-20250818113019320

class Solution {
    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length-1;
        // 先处理数组未旋转的情况
        if(nums[left] <= nums[right]) {
            return nums[left];
        }
        while(left < right) {
            int mid = left + (right-left)/2;
            // 与第一个元素比较
            if(nums[mid] >= nums[0]) {
                // 中间值在左半部分(较大段)
                left = mid+1;
            } else {
                // 中间值在右半部分(较小段)
                right = mid;
            }
        }
        return nums[left];
    }
}

你说,我们以数组起始位置为参考点可以吗,可以,但是有一种特殊情况
就是如果数组是完全有序的,那我们最后left会变到middle+1位置,并不是起始位置了

class Solution {
    public int findMin(int[] nums) {
        int left = 0;
        int right = nums.length-1;
        // 先处理数组未旋转的情况
        if(nums[left] <= nums[right]) {
            return nums[left];
        }
        while(left < right) {
            int mid = left + (right-left)/2;
            // 与第一个元素比较
            if(nums[mid] >= nums[0]) {
                // 中间值在左半部分(较大段)
                left = mid+1;
            } else {
                // 中间值在右半部分(较小段)
                right = mid;
            }
        }
        return nums[left];
    }
}

八、0~n-1的缺失数字

题目链接
这一题就是说数组值和下标值相同,如果出现错位,请你找出那个缺失的值
这一我们可以用好多种方法,我们先用二分查找方法
有个细节要注意,如果是[0,1,2,3]这种情况,看起来有序,其实它缺失的是数字4,但是4超出了数组范围,因此我们要返回left+1

1. 二分查找

class Solution {
    public int takeAttendance(int[] records) {
        int left = 0;
        int right = records.length-1;
        while(left < right){
            int middle = left+(right-left)/2;
            //根据下标确定
            if(records[middle] - middle == 0){
                left = middle+1;
            }else{
                right = middle;
            }
        }
        //left+1针对是是完全有序的数组,最后left会落在数组末尾
        //但是缺失的数是末尾数值+1,因此返回left+1,否则正常返回left
        return left == records[left] ? left+1 : left;
    }
}

2. 模拟哈希表

class Solution {
    public int takeAttendance(int[] records) {
        boolean[] exists = new boolean[records.length + 1];
        for (int num : records) {
            if (num < exists.length) {
                exists[num] = true;
            }
        }
        for (int i = 0; i < exists.length; i++) {
            if (!exists[i]) {
                return i;
            }
        }
        return -1; // 不会执行
    }
}

3. 位运算

class Solution {
    public int takeAttendance(int[] records) {
        int result = records.length; // 初始化结果为n
        for (int i = 0; i < records.length; i++) {
            result ^= i;
            result ^= records[i];
        }
        return result;
    }
}

4. 高斯求和

class Solution {
    public int takeAttendance(int[] records) {
        int n = records.length;
        long total = (long) n * (n + 1) / 2; // 0到n的总和
        long sum = 0;
        for (int num : records) {
            sum += num;
        }
        return (int) (total - sum);
    }
}

5. 直接遍历

class Solution {
    public int takeAttendance(int[] records) {
        int n = records.length;
        for (int i = 0; i < n; i++) {
            if (records[i] != i) {
                return i;
            }
        }
        return n; // 缺失的是最后一个数
    }
}

6.综合分析

方法时间复杂度空间复杂度
直接遍历O(n)O(1)
高斯求和O(n)O(1)
位运算O(n)O(1)
模拟哈希表O(n)O(1)
二分查找O(log n)O(1)

因此对于本题数组的有序性,使用二分查找是最优解


希望本篇文章对您有帮助,有错误您可以指出,我们友好交流

END
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值