二分查找算法(一)

一、力扣704——二分查找

1.暴力解法:直接for遍历一遍数组,找到值就返回下标,未找到,返回-1。O(N)

2.二分查找: 具有二段性,根据某规律,能将数组分成两部分,能够砍掉数组的一部分

暴力解法的缺陷,每次查找时,只能消除掉其中的一个数,利用二分查找可以砍掉一部分。

 

代码如下:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0, right = n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target) {
                right = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
};

二、力扣34——在排序数组中查找元素的第一个和最后一个位置

 

题目解析

非递减顺序:表示要么是递增的,要么是平衡的(数值相同),找到就返回值所在的区间,找不到就返回[-1, -1]。空数组也返回[-1, -1]

算法原理

1.暴力解法:直接遍历数组,找到数值第一次出现的位置begin,保存下来,继续寻找,找到最后一个出现的位置end,再保存,得到区间[begin, end],O(N)

2.二分查找

如果用朴素二分,会导致,最后的时间复杂度变为暴力解法的时间复杂度

1.查找区间的左端点

注意:当mid所处位置的值>=target,right 不能再等于mid的左边值了,而是直接 == mid,因为有可能mid所处的位置就是target

难点:细节处理

1.循环条件

left <= right 错误

left < right

1.left == right的时候,就是最终结果,无需判断

2.如果判断,就会死循环

三种情况讨论:

  • 有结果,right一定不会越过ret ,当left == right 时,就是最终的结果,无序判断

  • 全大于target

只会命中第二个条件,left不会动,right一直向右边移动,直到left和right相遇为止就结束,最后判断left处的值是否等于target。不等于 返回 -1

  • 全小于target,只会命中第一个条件,right不会动,left一直向右边移动,直到left和right相遇为止就结束。不能判断,判断就会死循环。

 2.求中点的操作,如何求mid

left + (right - left)/2

left + (right - left + 1)/2 --->  会陷入死循环

2.查找区间的右端点

类似查找左端点

1.循环条件 -> 同查找左端点: left < right 

2.求中点的方式:

left + (right - left) / 2

left + (right - left + 1) / 2

注意此时只剩两个元素,left指向mid,如果命中第一个条件,left就一直指向mid,

使用第一种方式: left + (right - left) / 2 = left 就会导致陷入死循环.

选择第二种方式 :mid = left + (right - left + 1) / 2 = left + 1 ,left = mid =  left + 1,就会和right指向同一个元素,就能终止循环

代码如下:

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.size() == 0) return {-1, -1};
        int begin = 0, end = 0;
        // 1.查找二分左端点
        int left = 0, right = nums.size() - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        if (nums[left] != target)
            return {-1, -1};
        else
            begin = left;
 
        // 2.二分右端点
        left = 0, right = nums.size() - 1;
        while (left < right) {
            int mid = left + (right - left + 1) / 2;
            if (nums[mid] <= target) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        end = right;
        return {begin, end};
    }
};

 

总结二分模版(查找区间左端点的模版,和查找区间右端点的模版)

左端点

 while (left < right) {
            int mid = left + (right - left) / 2;
            if (......) {
                left = mid + 1; //记这里
            } else {
                right = mid;
            }
        }

右端点

 while (left < right) {
            int mid = left + (right - left + 1) / 2;
            if (......) {
                left = mid;
            } else {
                right = mid - 1;  //记这里
            }
        }

如何记忆:当下面出现 - 1的时候上面就 +1

三、力扣69——x的平方根

题目解析

返回一个比真实值小的最近整数。

算法原理

1.暴力解法:

依次从1开始算他们的平方数,如果没有刚好等于的,就返回刚大于x的前一个数的原始值,也就是找到这个刚大于x的数后-1

2.二分查找:

具有二段性,不朴素的二分查找:

准备left ,right,  mid:left从1开始,right就为x(因为可能x = 1):

1.mid * mid <= x,left = mid

2.mid * mid > x   ,  right = mid - 1

代码编写 

注意long long防止溢出

class Solution {
public:
    int mySqrt(int x) {
        if (x < 1)
            return 0; // 处理边界情况
        long long left = 1, right = x;
        while (left < right) {
            long long mid = left + (right - left + 1) / 2;
            if (mid * mid <= x) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        return left;
    }
};

四、力扣35——搜索插入位置

算法原理

二分查找:

1.恰好能找到这个值

2.找插入位置

        在中间:左边是小于 t ,右边都是大于 t 的

        在边缘:全部大于t ,直接返回 0 ,全部小于t,直接返回nums.end()

所要找的值,如果是要刚好等于,返回mid,插入的位置一定是在刚好大于target的这个位置,因此我们需要分的是小于和大于等于这二段

理解:

如果我们的nums[mid]值小于target,那么我们需要找的下标一定不会再这个区间内,因此是left 

= left + 1 ,如果nums[mid]值大于等于target,那么需要找的值就一定在这个区间内right = mid

代码如下:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0,right = nums.size() - 1;
        int max = nums[right];
        while(left < right)
        {
            int mid = left +(right - left) / 2;
            if(nums[mid] < target) left = mid + 1;
            else right = mid;
        }
        if(nums[left] < target) return left + 1;
        else return left;
        
    }
};

五、力扣852——山脉数组的峰顶索引 

算法原理

二分查找:

当arr[mid] > arr[mid - 1]的时候,说明这是上坡,不一定到转折点

arr[mid] < arr[mid - 1]的时候,说明这是下坡,前面一定有一个转折点也就是山坡

这就是这道题的二义性

代码如下:

class Solution {
public:
    int peakIndexInMountainArray(vector<int>& arr) {
        int left = 0,right = arr.size() - 1;
        while(left < right)
        {
            int mid = left + (right - left + 1) / 2;
            if(arr[mid] > arr[mid - 1]) left = mid;
            else right = mid - 1;
        }
        return left;
    }
};

六、力扣162——寻找峰值

题目解析

遇上题不同的是,有多个峰值,最左和最右都是比所有峰值小的

算法原理

1.暴力解法:

从第一个位置开始,一直向后走,分情况讨论,O(N),这道题的数据范围比较小,直接暴力解法也是可以的,峰值有可能在最后

 2.二分查找:优化暴力解法

这里还是类似于上一道题 这道题特殊在

 

 

上图即为我们会遇到的山峰

  • 1是遇到递减,说明之前一定有个山峰。 
  • 2是递增在递减,中间一定有个山峰。
  • 3是一直递增,但因为nums[n] = 负无穷,所以在后面一定会的碰到一个递减,在此之间有一个山峰。

代码如下:

class Solution {
public:
    int findPeakElement(vector<int>& nums) {
        int left = 0,right = nums.size() - 1;
        while(left < right)
        {
            int mid = left +(right - left) / 2;
            if(nums[mid] < nums[mid + 1]) left = mid + 1;
            else right = mid;
        }
        return left;
    }
};

七、力扣153寻找旋转排序数组中的最小值

如何旋转:

旋转n次后,nums原来就是一个升序排列的数组,传给我们的是经过n次旋转的,我们需要得到原始的升序排列的数组 ,并且返回数组中最小的元素,注意数组元素互不相同。

算法原理

1.暴力查找最小值:从前往后遍历,找最小值。慢在,没有使用这个数组的特性

2.二分查找:

这个数组一定存在两段数值上升的递增区间。

二段性:以D为参照点,会发现,左边的AB段的数组一定都是大于D的值的,而CD这个区域的所有值都是小于等于D的,而我们最终要找的值,就是C点。

如果mid落在A~B这个区间:nums[mid] < nums[n - 1](C ~ D)

如果mid落在C~D这个区间:nums[mid] >= nums[n - 1](C ~ D)

 代码如下:

class Solution {
public:
    int findMin(vector<int>& nums) {
        int left = 0,right = nums.size() - 1;
        int x = nums[right];
        while(left < right)
        {
            int mid = left + (right - left) / 2;
            if(nums[mid] > x) left = mid + 1;
            else right = mid;
        }
        return nums[left];
    }
};

八、力扣LCR 173——点名

题目解析

就是一共有9个人,但是有一个人的学号不在,因此数组长度只会有8个,就要找到这个不在的人

算法原理

  1. 解法一:高斯求和公式:原值求和 - 现有值的和 = 缺失的数 
  2. 解法二:哈希表 ,建立一个 n+1大小的哈希表,遍历原始数组往里填,最后查看哈希表哪一个位置没有填,就是缺失的数字
  3. 解法三:直接遍历,for循环找,不等于则返回这个值
  4. 解法四:位运算,将题目所给数组,和一个完整数组的元素进行位运算异或。最后异或的结果就是位运算的结果。

前四种的时间复杂度都是O(N);

解法五:二分查找:

发现二段性:
从缺失值处开始划分,左边的值和下标都是一一对应的,当遇到不对应的值的时候就返回下标

细节问题:如果数组是【0,1,2,3】,缺失的值是4,但是在我们的二分查找里面并没有得到这个值

因此在最后还需要判断一下records[left] == records[right] == records.size() - 1,的话,就返回left + 1

代码编写

class Solution {
public:
    int takeAttendance(vector<int>& records) {
        //二分
        int left = 0,right = records.size() - 1;
        int cnt = records[right];
        while(left < right)
        {
            int mid = left + (right - left + 1) / 2;
            if(records[mid] == mid) left = mid;
            else right = mid - 1;
        }
        if(records[left] == left) return left + 1;
        else  return left;
    }
};

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值