码迷,mamicode.com
首页 > 其他好文 > 详细

面试手撕系列:「二分法」

时间:2020-12-19 12:31:22      阅读:1      评论:0      收藏:0      [点我收藏+]

标签:二分   假设   复杂   循环   高效   区域   例子   依次   关注   

最近春招开始了,面试面着面着一言不合就开始手撕代码
技术图片

手撕就手撕,接下来我打算写几个专题讲讲面试中手撕的常见题目
这些都是LeetCode上有的题目 手撕无非就是 树、链表、二分、字符串这些常用的数据结构
所以接下来请关注我们的专题吧

二分法

二分法查找,也称为折半法,是一种在有序数组中查找特定元素的搜索算法。
二分法查找的思路如下:
(1)首先,从数组的中间元素开始搜索,如果该元素正好是目标元素,则搜索过程结束,否则执行下一步。
(2)如果目标元素大于/小于中间元素,则在数组大于/小于中间元素的那一半区域查找,然后重复步骤(1)的操作。
(3)如果某一步数组为空,则表示找不到目标元素。
二分法查找的时间复杂度O(logn)。
技术图片

感觉很抽象的样子 举一个简单通俗易懂的例子
你不知道自己长得怎么样,然后去找了10个人 ,评分分别为1-10分
首先把5号拉出来,让大家看看是不是这个人和你比哪个优秀
如果你比五号选手长得帅,那么你就不用和1-4号比了,这个区间就缩短成[5-10]
如果不幸的比五号选手长得丑,那么这个区间就缩短成了[1-4]
这里介绍两个二分法的模板 主要区别在于当前数是被划分到左区间还是右区间

模板

boolcheck(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
intbsearch_1(int l, int r){
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
intbsearch_2(int l, int r){
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}

二分法作为一种搜索上快速的算法,可以把复杂度从O(N)变成O(log N),
要满足二分法搜索的条件是搜索区间内满足单调性
为什么会有两个模板呢
可以看到在区分id的时候可以分为mid=(l+r)/2和(l+r)/2+1 与此同时 带来了当前数字索引划分区间的不同
话不多说先来两个题目试一试水

69 X的平方根

实现 int sqrt(int x) 函数。
计算并返回 x 的平方根,其中 x 是非负整数。
由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。

示例 1:
输入: 4
输出: 2
示例 2:
输入: 8
输出: 2
说明: 8 的平方根是 2.82842...,
     由于返回类型是整数,小数部分将被舍去。

这个题目使用二分法还是比较明显的
使用二分法搜索平方根的思想很简单,就类似于小时候我们看的电视节目中的“猜价格”游戏,高了就往低了猜,低了就往高了猜,范围越来越小。
因此,使用二分法猜算术平方根就很自然。
存在三种情况:

  1. 平方小于目标数 往高走
  2. 平方大于目标数 往低走
  3. 平方和恰好等于目标数 跳出循环
//采用二分模板 mid划到了左区间
    long left=1;
    long right=x;
    while(left<right){
            long mid=(left+right)/2+1;
            if(mid*mid>x)//平方大于目标数 往低走
                    right=mid-1;
            else if(mid*mid<x)//平方大于目标数 往低走
                    left=mid;
            else {//平方和恰好等于目标数 跳出循环
                    left=mid;
                    break;
            }
    }
    return (int)(left);

74 搜索二维矩阵

编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性:
每行中的整数从左到右按升序排列。每行的第一个整数大于前一行的最后一个整数。

输入:
matrix = [
  [1,   3,  5,  7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]
target = 3
输出: true
输入:
matrix = [
  [1,   3,  5,  7],
  [10, 11, 16, 20],
  [23, 30, 34, 50]
]
target = 13
输出: false

这个题目也可以采用二分 先找一下规律发现 每行的第一个整数大于前一行的最后一个整数 所以这个数组 从上到下 从左到右是依次递增的 所以我们可以采用两次二分法
第一次二分的目的是锁定这个数在哪一行上

int left=0;
int right=Hang-1;
while(left<right){
        int mid=(left+right)/2+1;
        if(matrix[mid][0]>target)//当前这个行首元素大于target 说明不可能在这一行
                right=mid-1;
        else if(matrix[mid][0]<=target)//小于或者等于的时候 说明可能是在这一行上
                left=mid;
    }//最后这个left就是具体哪一行

第二次二分的目的是锁定这个数在这一行的哪一列上
主要判断索引为(Hang,mid)这个元素与target的关系

Hang = left;
left =0;
right=Lie-1;
while(left<right){
        int mid=(left+right)/2+1;
        if(matrix[Hang][mid]>target)//如果这一行上坐标为(Hang,mid)的元素大于target 说明目标数可能在左边
                right=mid-1;
        else if(matrix[Hang][mid]<target)//如果这一行上坐标为(Hang,mid)的元素小于target 说明目标数可能在右边
                left=mid;
        else //相等的话就是True
                return true;

}
//最后找到这个数但是不一定是和目标数相等 要判断一下
if(matrix[Hang][left]==target)
        return true;
else    
        return false;

查找元素的第一个和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是 O(log n) 级别。
如果数组中不存在目标值,返回 [-1, -1]。

输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]

因为数组已经排过序了,我们可以使用二分查找的方法去定位左右下标。
1,使用二分法判断是否存在目标值
2,使用二分法找到第一个 大于等于 target 的位置i
3,使用二分法找到最后一个 小于等于 target的位置j
这个过程类似什么 类似我们高中学过的夹逼准则吧
技术图片

由于数组有序且从小到大排序 那么找到的[i,j]区间里必定是目标区间 当然还得判断目标区间是否存在
使用二分法找到第一个 大于等于 target 的位置i

int left=0;
int right=nums.length-1;

while(left<right){
        int mid=(left+right)/2+1;
        if(nums[mid]<=target)
                left=mid;
        else   
                right=mid-1;
        }

使用二分法找到最后一个 小于等于 target 的位置j

left=0;
right=nums.length-1;

while(left<right){
    int mid=(left+right)/2;

    if(nums[mid]>=target)
            right=mid;
    else
            left=mid+1;
}

最后验证区间是否符合我们的要求即可

287 寻找重复数

给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。

输入: [1,3,4,2,2]
输出: 2
输入: [3,1,3,4,2]
输出: 3

说明:
不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n^2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。
这个题目看上去可以用暴力的做法去做,但是暴力做法的复杂度是在O(n^2),肯定是不行的
既然要小于O(n^2) 灵机一动 要不我先排个序

技术图片

且慢 这题排序好像帮助也不大 上头了上头了抽根烟冷静一下

技术图片

n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n)
那就可以保证至少有一个数是重复的
慢着 这不就是抽屉原理嘛
所谓抽屉原理就是
n+1个苹果 放到n个抽屉里 肯定有一个抽屉是放了两个苹果以上 而且题目中说只有一个抽屉是放了两个苹果 其他抽屉放了一个苹果
好了我先生成一个抽屉 把苹果放进去不就好了

且慢 这个题不能使用额外空间

我们回到刚开始的暴力做法 暴力做法是指从1遍历到n看看1-n上哪个数是出现两次的
假设这个重复出现的数是x+1
那么1-x 出现的次数就是x
1-(x+1)出现的次数就是x+2
好像可以看到二分的性质了二分的依据就是 1-x 出现的次数是不是x

  1. 如果是 那么就说明1-x上每个数都只出现了一次 目标数还得往大找
  2. 如果不是 那么说明1-x上有一个数是出现了两次 目标数得往小了找

那么我们要找出这个引起质变的数即可

int n=nums.length;
int l=0;
int r=n-1;
while(l<r){
        int mid=(l+r)/2;
        int cnt=0;
        for(int i=0;i<nums.length;i++){//计算1-mid 上每个数出现的次数
                if(nums[i]<=mid)
                        cnt+=1;
        }
        if(cnt<=mid)//如果是每个数都出现了一次 那么往大找
                l=mid+1;
        else//否则往小找
                r=mid;
}
return l;

162 寻找峰值

峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞。

例 1:
输入: nums = [1,2,3,1]
输出: 2
解释: 3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入: nums = [1,2,1,3,5,6,4]
输出: 1 或 5
解释: 你的函数可以返回索引 1,其峰值元素为 2;或者返回索引 5, 其峰值元素为 6。

说明:
你的解法应该是 O(logN) 时间复杂度的。

这个题目算是二分法里相对抽象的模型了 O(logN) 也在提示你往二分方面想

不过还好题目给了点提示 nums[-1] = nums[n] = -∞
由于峰值元素是指其值大于左右相邻值的元素
我们可以把这个数组的最左端和最右端看作是负无穷
那么只要是任意一个实数 他小于两边的元素 或者大于两边的元素 他就是峰值

举个例子
数组[1] 它的峰值就是1 因为 nums[-1] = nums[n] = -∞
数组[1,2] 它的峰值就是2 因为1<2< -∞
给大家画一个山峰 可以知道山峰的确定和他的绝对高度 无关 和他的相对高度有关

只要他是比它的邻居都高 那它就是山峰

技术图片

抽象成二分法怎么理解 如这个图所示 我就比较nums[mid]和nums[mid+1]之间的关系
无非就是只有两个情况
nums[mid]<nums[mid+1] 也就是右边的元素大于左边的 那从左往右看就是上坡 上坡路必定存在一个顶峰

nums[mid]>nums[mid+1] 也就是右边的元素小于等于左边的 那从右往左看就是一个平滑的下坡,那这个顶峰必定是在mid这个位置之前,不然我也不可能有下坡的空间

所以抽象成二分模型就是

int l=0;
int r=nums.length-1;
while(l<r){
        int mid=(l+r)/2;
        if(nums[mid]<nums[mid+1])//如果上坡 继续往前走 看看顶峰在哪里
                l=mid+1;
        else//如果下坡了 往回走 直到找到顶峰
                r=mid;
}
return l;

最后推荐几道题目,也是使用二分的,相信你看到这里也对二分有了更高的认识,不再是有序才可以用二分
技术图片

Leetcode-278 第一个错误的版本
Leetcode-275 H指数II
Leetcode-35 搜索插入位置
Leetcode-53 寻找旋转排序数组中的最小值

 往期精彩回顾

三月算法岗面经

阿里笔试题两道

扫码关注我,加入每日一题算法交流群/秋招备战群吧
请备注 学校+昵称 才可以加群噢

技术图片

技术图片

面试手撕系列:「二分法」

标签:二分   假设   复杂   循环   高效   区域   例子   依次   关注   

原文地址:https://blog.51cto.com/15054042/2563932

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!