🎯 算法精讲:二分查找(一)—— 基础原理与实现 🔍

🎯 算法精讲:二分查找(一)------ 基础原理与实现 🔍

二分查找是一种高效的查找算法,时间复杂度为 O(log n)。

它的核心思想是通过不断地将查找范围缩小一半,来快速定位目标元素。

今天由我带领大家一起学习、探讨二分查找的基础原理与实现。

作者:无限大

推荐阅读时间:15 分钟

引言

为什么二分查找是程序员的「效率神器」? ⚡️

在编程的世界中,效率往往意味着时间、资源和代码复杂度的平衡。当你面对一个庞大的数据集,比如一个包含数百万条记录的用户数据库,或者是一个需要频繁查找的排序列表时,如何快速找到所需的信息,就成为了一个关键问题。想象一下,如果你每次都要从头开始逐个检查元素,那无疑会像在一本厚重的电话簿中寻找一个名字一样耗时费力。而二分查找,正是这样一种减而治之的神器,它通过每次将搜索范围缩小一半,极大地提升了查找效率。

二分查找的核心思想并不复杂,却极具智慧。它基于一个简单的前提:数据必须是有序 的。一旦数据被排序,我们就可以利用这种顺序性,通过比较中间元素与目标值的关系,快速判断目标值是在当前区间的左侧还是右侧,从而将搜索范围缩小到一半。 这个过程不断重复,直到找到目标值或确定其不存在。这种策略不仅适用于静态的数据结构,如数组,也可以在某些动态场景中发挥重要作用,尽管在插入和删除操作时可能会带来一些限制。

在实际开发中,二分查找的应用场景非常广泛。例如,在搜索引擎中,当用户输入关键词时,系统需要在海量的索引中快速定位相关结果;在数据库查询中,二分查找可以帮助快速定位满足条件的记录;甚至在日常的编程任务中,如查找数组中的最大值、最小值或特定值的位置,二分查找都能提供高效的解决方案。此外,随着智能化开发工具的普及,如 InsCode AI IDE 等,开发者可以借助这些工具轻松实现并优化二分查找算法,进一步提升开发效率。

然而,二分查找并非万能。它依赖于数据的有序性这意味着在使用前必须对数据进行排序,而排序本身也需要一定的计算成本。此外,二分查找通常适用于顺序存储结构,如数组,而链表等非顺序结构则无法直接应用该算法。因此,在选择算法时,程序员需要根据具体场景权衡利弊,确保所选方法既能满足性能需求,又能适应数据结构的特点。

二分查找之所以被称为程序员的"效率神器",是因为它在处理有序数据时展现出的惊人效率。无论是面对庞大的数据集,还是需要频繁查找的场景,二分查找都能以 O(log n)的时间复杂度,显著优于线性查找的 O(n)表现。它不仅是一种算法,更是>一种思维方式。


一、🧩 二分查找的核心要点与适用场景

其实在引言部分已经强调过,二分查找的核心是数组必须是有序的,是单调的,但仍有一些其他的相关法则、要点,跟随我的步伐,一起去看看吧。

二分查找的三大黄金法则

1. 单调性:数组必须有序

二分查找的核心在于有序这一前提。它依赖于数据的有序性,通过不断比较中间元素与目标值,将搜索范围缩小一半。如果数组未排序,二分查找将无法正确工作,甚至可能陷入死循环或返回错误结果。

提示:在实际开发中,如果遇到无序数组,可以先使用排序算法(如快速排序、归并排序)对其进行排序,然后再进行二分查找。这样可以结合两种算法的优势,提高查找效率。

2. 边界清晰:定义区间是关键

在实现二分查找时,如何定义 leftright 的边界至关重要。

常见的有两种方式:左闭右闭 (即 [left, right])和左闭右开 (即 [left, right))。

这两种方式会影响循环的终止条件和指针的移动逻辑。例如,在左闭右闭区间中,循环条件为 left <= right,而在左闭右开区间中,循环条件为 left < right。因此,在编写代码时,必须明确选择一种区间定义方式,并在整个过程中保持一致。

建议:建议初学者统一使用"左闭右闭"区间,因为这种方式更直观,也更容易理解。一旦掌握了这种模式,再尝试"左闭右开"区间,会更容易适应不同语言或框架下的实现方式。

3. 不重不漏:确保每个元素都被考虑

在二分查找过程中,必须确保不会遗漏任何可能包含目标值的元素。这要求我们在每次循环中,根据比较结果正确地更新 leftright 的值。例如,当 nums[mid] > target 时,说明目标值在 mid 的左侧,因此应将 right 设置为 mid - 1(不包含 mid,针对左闭右闭区间的,若为左闭右开区间,则关注下方的说明内容);反之,若 nums[mid] < target,则应将 left 设置为 mid + 1。如果处理不当,可能会导致死循环或漏掉目标值。

说明

左闭右闭区间 [left, right ]

  • right = nums.size() - 1;// 初始值

  • right = mid - 1; // 继续搜索左半边

左闭右开区间 [left, right)

  • right = nums.size(); // 注意 right 是 nums.size()

  • right = mid; // 目标在左半边,包括中间

法则总结表
法则 描述 常见错误
单调性 数组必须有序 📈 忘记排序直接查找
边界清晰 明确左右指针含义 循环条件写成 low <= high
不重不漏 确保每个元素都被考虑 中间值计算溢出

什么时候不适合使用二分查找

  1. 数组未排序:二分查找要求输入必须是有序数组;如果数组未排序,使用该算法将得不到正确结果。

  2. 小规模数据:对于小数组(通常 n<100),线性查找可能更有效,因为它的开销较小,实现简单。

  3. 不适合链表:在链表中访问元素的时间是 O(n),因此二分查找无法有效利用其时间复杂度优势。

  4. 动态数据结构:频繁对数组进行插入或删除操作会影响其顺序,导致维护排序的开销过大。

  5. 重复元素问题:当数组中存在重复元素时,二分查找无法保证找到首个或最后一个目标值的准确性。

  6. 特殊数据类型:对于某些复杂或自定义的数据结构,二分查找也可能不适用。

关键点

  • 二分查找仅适用于有序数组 ,且数组中元素不应重复

  • 二分查找的核心思想是通过不断缩小查找区间来定位目标值。

  • 有两种边界定义方式:左闭右闭区间左闭右开区间,影响代码逻辑。

    • 在左闭右闭区间中,循环条件为 left <= right
    • 在左闭右开的区间中,循环条件为 left < right
  • 二分查找的时间复杂度为 O(log n),空间复杂度为 O(1)。

  • 对于初学者,常出现不清晰定义区间导致的代码错误,需要坚持循环不变量规则。


二、💻 代码实现:两种区间定义方式(Java)

上面的内容我们已经介绍了二分查找的核心要点和适用场景,下面我们来看看如何使用代码实现二分查找。

前提:数组有序,无重复元素。

注意:

一般的 mid 都是 (left + right) / 2,但是推荐使用 left + (right - left) / 2

使用 mid = left + (right - left) / 2;是为了避免整型溢出的问题。leftright 的值很大时,直接相加可能导致计算结果超出 int 能表示的范围,而先计算差值再进行加法,就可以确保整个过程在安全的数值范围内进行。这种做法让代码更健壮,有助于避免潜在的错误。

方式一:左闭右闭区间 [left, right]

  • 初始条件left = 0, right = nums.length - 1

  • 循环条件left <= right

  • 右边界处理right = mid - 1

  • 原因

    • 包括右边界 :需要包括右边界的元素,因此当 leftright 相等时,mid 仍可能是目标值,所以不能立即跳出循环。
    • 处理退出条件 :当 left 超过 right 时,确认查找结束,并且目标值未在这个范围内。
  • 适用场景

    • 需要包括右边界元素的查找或范围判断。
    • 例如,想要查找目标值或最后一个小于或等于目标值的元素,并带有全区间的遍历。
java 复制代码
public class BinarySearch {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1; // 定义target在左闭右闭的区间里,[left, right]

        while (left <= right) { // 当left == right时,区间[left, right]仍然有效
            int mid = left + (right - left) / 2; // 防止溢出,等同于(left + right)/2

            if (nums[mid] == target) {
                return mid; // 找到目标值,返回下标
            } else if (nums[mid] < target) {
                left = mid + 1; // target在右区间,所以[mid+1, right]
            } else {
                right = mid - 1; // target在左区间,所以[left, mid-1]
            }
        }
        return -1; // 未找到目标值
    }
}

方式二:左闭右开区间 [left, right)

  • 初始条件left = 0, right = nums.length

  • 循环条件left < right

  • 右边界处理right = mid

  • 原因

    • 不包括右边界right 代表的是不包含的上界,因此当 left 等于 right 时,说明查找结束。
    • 允许缩小范围 :可以在每次迭代中使用 right 来控制 mid 的计算,将其排除在下次迭代中。
  • 适用场景

    • 当需要查找一个可能的插入位置,且保证 right不包含已经查找的元素。
    • 适于返回某个值应该被插入的位置,而不会影响既有元素的顺序。
java 复制代码
public class BinarySearch {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length; // 定义target在左闭右开的区间里,[left, right)

        while (left < right) { // 当left == right时,区间[left, right)为空,应终止循环
            int mid = left + (right - left) / 2; // 防止溢出

            if (nums[mid] == target) {
                return mid; // 找到目标值,返回下标
            } else if (nums[mid] < target) {
                left = mid + 1; // target在右区间,所以[mid+1, right)
            } else {
                right = mid; // target在左区间,所以[left, mid)
            }
        }
        return -1; // 未找到目标值
    }
}

两种区间定义方式的对比

区间定义 循环条件 right 初始值 右边界更新 左边界更新
[left, right] left <= right nums.length - 1 right = mid - 1 left = mid + 1
[left, right) left < right nums.length right = mid left = mid + 1

两种方式没有绝对的优劣,关键是要坚持循环不变量原则,在整个二分查找过程中保持区间定义的一致性。


三、📊 二分查找的效率分析:为什么它这么快?

二分查找之所以高效,主要得益于其对数级的时间复杂度 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( log ⁡ n ) O(\log n) </math>O(logn),这使得它在处理大规模数据时远优于线性查找。具体来说,每次比较后,查找范围都会减半,因此查找次数与 <math xmlns="http://www.w3.org/1998/Math/MathML"> log ⁡ 2 n \log_2 n </math>log2n 成正比。

想象一下,如果你要对 100 万个元素的数据集,线性查找最坏情况下需要 100 万次比较,而二分查找仅需约 20 次 。这种效率优势在数据量极大时尤为明显,例如在 10 亿个元素中查找目标,线性查找需要 10 亿次比较,而二分查找仅需约 30 次 !这就是对数级复杂度的威力 💥

时间复杂度对比

数据规模 顺序查找 (O(n)) 二分查找 (O(log n))
100 100 次 7 次
10,000 10,000 次 14 次
1,000,000 1,000,000 次 20 次
1,000,000,000 1,000,000,000 次 30 次

时间复杂度分析

二分查找的时间复杂度推导过程:

设数据规模为 n,查找次数为 k:

  1. 第 1 次查找后剩余:n/2
  2. 第 2 次查找后剩余:n/4
  3. ...
  4. 第 k 次查找后剩余:n/(2^k)

当 n/(2^k) ≤ 1 时停止,解得:

bash 复制代码
k ≥ log₂n
∴ 时间复杂度为 O(log n)

在 Java 实现代码中(以左闭右闭区间为例):

java 复制代码
public int search(int[] nums, int target) {
    int left = 0, right = nums.length - 1; // O(1)
    while(left <= right) {                 // 循环次数 O(log n)
        int mid = left + (right - left)/2; // O(1)
        // ...比较逻辑...
    }
    return -1;                            // O(1)
}

空间复杂度分析

迭代实现的二分查找具有常数级空间复杂度:

scss 复制代码
空间消耗 = 3个整型变量(left/right/mid) = O(1)

与递归实现的对比:

实现方式 空间复杂度 百万数据内存消耗
迭代实现 O(1) 12 字节
递归实现 O(log n) 20 层调用栈 ≈ 1KB

优势总结:

  1. 内存占用固定,适合嵌入式设备等资源受限场景
  2. 无递归调用栈开销,避免栈溢出风险
  3. 更适合处理超大规模数据(十亿级以上)

内存使用说明

  • 迭代实现仅需存储数组指针和 3 个整型变量(left/right/mid)
  • 递归实现需要维护调用栈,深度为 log n,在 n=100 万时深度为 20 层

从实现角度来看,二分查找的迭代版本空间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1),因为它仅需几个变量(如 left、right 和 mid)来跟踪搜索范围。而递归版本由于调用栈的深度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> log ⁡ n \log n </math>logn,因此空间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( log ⁡ n ) O(\log n) </math>O(logn)。这种低空间占用使其非常适合内存受限的场景,如嵌入式系统或处理传感器数据的应用 。

  • 迭代实现:空间复杂度为 O(1),只需要几个额外变量
  • 递归实现:空间复杂度为 O(log n),因为需要递归调用栈

四、🏃 力扣算法题实战

1. 力扣704. 二分查找(基础版)

原题链接

题目描述: 给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,返回目标值的下标,否则返回 -1。

算法思路

  • 基于数组有序特性,每次比较中间元素与目标值
  • 缩小搜索范围时需严格遵守区间定义
  • 时间复杂度:O(log n),空间复杂度:O(1)

解法一:左闭右闭区间 [left, right]

java 复制代码
class Solution {
    public int search(int[] nums, int target) {
        // 初始化区间为[left, right],包含两端
        int left = 0, right = nums.length - 1;

        // 当left == right时,区间仍然有效
        while(left <= right) {
            // 防溢出写法,等价于(left + right)/2
            int mid = left + (right - left)/2;

            if(nums[mid] == target) {
                return mid; // 找到目标立即返回
            } else if(nums[mid] < target) {
                // 目标在右区间 [mid+1, right]
                left = mid + 1;
            } else {
                // 目标在左区间 [left, mid-1]
                right = mid - 1;
            }
        }
        return -1; // 搜索区间为空时返回-1
    }
}

解法二:左闭右开区间 [left, right)

java 复制代码
class Solution {
    public int search(int[] nums, int target) {
        // 初始化区间为[left, right),右边界不包含
        int left = 0, right = nums.length;

        // 当left == right时区间无效
        while(left < right) {
            int mid = left + (right - left)/2;

            if(nums[mid] == target) {
                return mid;
            } else if(nums[mid] < target) {
                // 目标在右区间 [mid+1, right)
                left = mid + 1;
            } else {
                // 目标在左区间 [left, mid)
                right = mid;
            }
        }
        return -1;
    }
}

关键点对比

  1. 初始右边界:右闭区间用 nums.length-1,右开区间用 nums.length
  2. 循环条件:右闭用 left <= right,右开用 left < right
  3. 右边界更新:右闭用 mid-1,右开用 mid

2. 力扣35. 搜索插入位置

原题链接

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

算法思路

  • 在标准二分基础上增加插入位置判断
  • 当循环结束时,left指针即为插入位置
  • 时间复杂度:O(log n),空间复杂度:O(1)

解法一:左闭右闭区间

java 复制代码
class Solution {
    public int searchInsert(int[] nums, int target) {
        // 初始化区间为[left, right]
        int left = 0, right = nums.length - 1;
    
        while(left <= right) {
            int mid = left + (right - left)/2;
        
            if(nums[mid] == target) {
                return mid; // 找到直接返回下标
            } else if(nums[mid] < target) {
                left = mid + 1; // 搜索右区间
            } else {
                right = mid - 1; // 搜索左区间
            }
        }
        // 循环结束时left指向第一个大于target的位置
        return left; 
    }
}

解法二:左闭右开区间

java 复制代码
class Solution {
    public int searchInsert(int[] nums, int target) {
        // 初始化区间为[left, right)
        int left = 0, right = nums.length;
    
        while(left < right) {
            int mid = left + (right - left)/2;
        
            if(nums[mid] == target) {
                return mid;
            } else if(nums[mid] < target) {
                left = mid + 1; // 搜索右区间
            } else {
                right = mid; // 搜索左区间
            }
        }
        // 结束时left==right,即为插入位置
        return left;
    }
}

关键点对比

  1. 右开区间初始right值为nums.length
  2. 循环条件改为left < right
  3. 右边界更新为mid而不是mid-1

3.力扣 374. 猜数字大小

原题链接

题目描述: 猜数字游戏,通过预定义接口判断猜测结果。

算法思路

  • 通过预定义接口判断猜测方向
  • 根据反馈结果调整搜索区间
  • 时间复杂度:O(log n),空间复杂度:O(1)

解法一:左闭右闭实现

java 复制代码
public class Solution extends GuessGame {
    public int guessNumber(int n) {
        // 初始化区间[1, n]
        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; // 目标更大,搜索右区间
            } else {
                right = mid - 1; // 目标更小,搜索左区间
            }
        }
        return -1; // 题目保证有解,实际不会执行
    }
}

解法二:左闭右开实现

java 复制代码
public class Solution extends GuessGame {
    public int guessNumber(int n) {
        // 初始化区间[1, n+1)
        int left = 1, right = n + 1;
    
        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; // 搜索右区间
            } else {
                right = mid; // 搜索左区间
            }
        }
        return -1;
    }
}

实现要点

  1. 右开区间初始right值为n+1
  2. 比较结果处理与区间定义严格对应
  3. 循环终止条件适配区间特性

五、🌟 总结与激励

学习路径建议

  1. 基础掌握:反复练习标准二分模板(每日 1 题,连续 3 天)
  2. 思维训练:在白纸上手写代码,模拟指针移动过程
  3. 实战提升:完成力扣「二分查找」专题的 15 道经典题

算法思维培养

  • 建立「减而治之」的思维模式,面对问题时先思考如何缩小问题规模
  • 培养边界意识,在纸上画出区间示意图辅助分析
  • 记录自己的「错误案例集」,总结常见的越界、死循环场景

无限大寄语

从看到这篇文章开始,连续 7 天每天完成 1 道二分练习题。当你坚持到第 3 天时,会发现代码中的边界条件处理变得得心应手;到第 7 天时,你的算法思维将完成一次质的飞跃。记住:每个优秀的程序员都经历过数千次的边界条件调试,你正在走向卓越的路上!

希望这篇文章能帮你真正理解二分查找的精髓!下一篇我们将深入探讨二分查找的各种变形问题和实战技巧,敬请期待! 😊

相关推荐
Flobby52914 分钟前
Go语言新手村:轻松理解变量、常量和枚举用法
开发语言·后端·golang
Warren981 小时前
Java Stream流的使用
java·开发语言·windows·spring boot·后端·python·硬件工程
程序视点2 小时前
IObit Uninstaller Pro专业卸载,免激活版本,卸载清理注册表,彻底告别软件残留
前端·windows·后端
xidianhuihui2 小时前
go install报错: should be v0 or v1, not v2问题解决
开发语言·后端·golang
进击的铁甲小宝4 小时前
Django-environ 入门教程
后端·python·django·django-environ
掘金码甲哥4 小时前
Go动态感知资源变更的技术实践,你指定用过!
后端
王柏龙5 小时前
ASP.NET Core MVC中taghelper的ModelExpression详解
后端·asp.net·mvc
无限大65 小时前
算法精讲:二分查找(二)—— 变形技巧
后端
勇哥java实战分享5 小时前
基于 RuoYi-Vue-Pro 定制了一个后台管理系统 , 开源出来!
后端