算法复习之二分查找

二分查找

二分查找作为程序员的一项基本技能,是面试官最常使用来考察程序员基本素质的算法之一,也是解决很多查找类题目的常用方法,它可以达到O(log n)的时间复杂度。

一般而言,当一个题目出现以下特性时,你就应该立即联想到它可能需要使用二分查找:

待查找的数组有序或者部分有序
要求时间复杂度低于O(n),或者直接要求时间复杂度为O(log n)
二分查找有很多种变体,使用时需要注意查找条件,判断条件和左右边界的更新方式,三者配合不好就很容易出现死循环或者遗漏区域,本篇中我们将介绍常见的几种查找方式的模板代码,包括:

  1. 标准的二分查找
  2. 二分查找左边界
  3. 二分查找右边界
  4. 二分查找左右边界
  5. 二分查找极值点

本文的内容来自于笔者个人的总结,事实上二分查找有很多种等价的写法,本文只是列出了笔者认为的最容易理解和记忆的方法。

例1(LeetCode.704 二分查找)

题面:给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例1描述
输入nums = [-1,0,3,5,9,12], target = 9
输出4
说明9 出现在 nums 中并且下标为 4

思路:经典的二分查找

二分查找的做法是,定义查找的范围[left,right]初始查找范围是整个数组。每次取查找范围的中点 mid,比较num[mid]与target的大小,若相等则返回,不相等可以根据关系缩小一半的查找范围

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

分析二分查找的一个技巧是:不要出现else,而是把所有情况用else if写清楚,这样可以清楚地展现所有细节些。

例2(LeetCode.278 第一个错误的版本)

你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。

假设你有 n 个版本 [1, 2, …, n],你想找出导致之后所有版本出错的第一个错误的版本。

你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数。

示例1描述
输入n = 5,bad=4
输出4
说明调用 isBadVersion(3) -> false 调用 isBadVersion(4) -> true,所以,4 是第一个错误的版本。

思路:整体上使用二分查找,但是需要注意一个细节:如果midfalse,那么第一个错误可能为自己。但如果midtrue,则第一个错误一定在[mid+1,right]
ps:不要把isBadVersion当成测试是否成功

// The API isBadVersion is defined for you.
// bool isBadVersion(int version);

class Solution {
public:
    int firstBadVersion(int n) {
        int left=1;
        int right=n;
        while(left<right)
        {
            int mid = (right - left) /2 + left;
            if(isBadVersion(mid))right=mid;
            else left=mid+1;
        }
        return left;
    }
};

例3(LeetCode.35 搜索插入位置)

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为O(log n) 的算法。

示例1描述
输入nums = [1,3,5,6], target = 5
输出2

这一部分一定要分析清楚:

  • 题目要找的元素是:第一个大于等于 target 的元素的下标。
  • 数组的长度 len 也有可能是问题的答案。
    上面 2 个小点,都需要仔细分析题意和几个示例得到,任何模板都不能回答这样的问题。
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int l=0,r=n-1;
        while(l<=r){
            int mid=l+(r-l)/2;
            if(nums[mid]<target)
                l=mid+1;
            else r=mid-1;
        }
        return l;
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值