算法刷题-二分查找

文章目录

    • [1、704 二分查找](#1、704 二分查找)
    • [2、35 搜索插入位置](#2、35 搜索插入位置)
    • [3、34 在排序数组中查找元素的第一个和最后一个位置](#3、34 在排序数组中查找元素的第一个和最后一个位置)
    • [4、69 x的平方根](#4、69 x的平方根)
      • 另解牛顿迭代法
      • [1. 核心原理:几何直观](#1. 核心原理:几何直观)
      • [2. 公式推导](#2. 公式推导)
      • [3. C 语言代码实现](#3. C 语言代码实现)
      • [4. 牛顿迭代法 vs 二分查找](#4. 牛顿迭代法 vs 二分查找)
      • [5. 为什么在力扣 69 题中常用?](#5. 为什么在力扣 69 题中常用?)
    • 5、有效的完全平方数
    • 关于第4题和第5题
      • [1. 核心属性对比](#1. 核心属性对比)
      • [2. 存储结构的区别](#2. 存储结构的区别)
      • [3. 为什么在刷题(如 LeetCode)中很重要?](#3. 为什么在刷题(如 LeetCode)中很重要?)
        • [情况 A:判断 `mid * mid == num`](#情况 A:判断 mid * mid == num)
        • [情况 B:溢出风险](#情况 B:溢出风险)
      • [4. 形象的类比](#4. 形象的类比)
      • 总结建议

1、704 二分查找

题目

代码

c 复制代码
int search(int* nums, int numsSize, int target) {
    int left = 0;
    int right = numsSize-1;
    int mid=0;
    while(left <= right){
        //mid = (left + right)/2;
        //为了防止上溢
        mid = left + (right-left)/2;
        if(nums[mid] > target){
            right = mid-1;
        }
        else if(nums[mid] < target){
            left = mid + 1;
        }
        else{
            return mid;
        }
    }
    return -1;
}

时间复杂度:O(logn)

空间复杂度:O(1)

2、35 搜索插入位置

题目:

代码

c 复制代码
int searchInsert(int* nums, int numsSize, int target) {
    int left = 0;
    int right = numsSize-1;
    int mid=0;
    while(left <= right){
        mid = left + (right-left)/2;
        if(nums[mid] > target){
            right = mid-1;
        }
        else if(nums[mid] < target){
            left = mid + 1;
        }
        else{
            return mid;
        }
    }
    if(nums[mid] > target){
        return mid;
    }
    else{
        return mid + 1;
    }

}

时间复杂度:O(logn)

空间复杂度:O(1)
优化:

在二分查找结束(即 while 循环退出)时,如果没找到 target,left 指针其实已经指向了该元素应该被插入的位置。

为什么可以直接return left?

left 的定义在循环逻辑中非常纯粹:它是搜索范围的左边界。 当搜索失败时,它代表了"搜索范围中第一个不再小于 target 的位置",也就是插入点。

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

题目

代码

c 复制代码
/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int* searchRange(int* nums, int numsSize, int target, int* returnSize) {
    int* a= (int*)malloc(sizeof(int)*2);//准备返回结果
    *returnSize = 2;//必须告诉力扣返回数组的大小
    a[0] = -1;
    a[1] = -1;//初始化
    int left = 0;
    int right = numsSize-1;
    int mid = 0;
    //第一次二分:找左边界
    while(left <= right){
        mid = left + (right-left)/2;
        if(nums[mid] >= target){
            if(nums[mid] == target){//关键:等号放在这里,不断向左逼近
                a[0] = mid;
            }
            right = mid - 1;
        }
        else{
            left = mid + 1;
        }
    }
    //第二次二分,找右边界
    left = 0,right = numsSize-1;
    while(left <= right){
        mid = left + (right - left)/2;
        if(nums[mid] <= target){
            if(nums[mid] == target){//关键:等号放在这里,不断向右逼近
                a[1] = mid;
            }
            left = mid+1;
        }
        else{
            right = mid - 1;
        }
    }
    return a;
}

在C 语言中如何返回数组?

动态分配内存:你需要使用 malloc 申请一个大小为 2 的 int 数组。

c 复制代码
int* a= (int*)malloc(sizeof(int)*2);//准备返回结果

设置返回长度:你必须给 *returnSize 赋值为 2,否则评测系统不知道你返回了多少个元素。

c 复制代码
*returnSize = 2;//必须告诉力扣返回数组的大小

为什么这样能行?(逻辑要点)

  • 寻找左边界时:即使找到了 target,我们依然执行 right = mid - 1。这相当于在强制向左收缩区间,直到 while 循环结束。在这个过程中,最后一次记录的 mid 一定是最左边的那个。
  • 寻找右边界时:同理,找到 target 后执行 left = mid + 1,强制向右收缩,记录最右边的一个。
    时间复杂度:O(logn)
    空间复杂度:O(1)

4、69 x的平方根

题目:

代码:

c 复制代码
int mySqrt(int x) {
    int res=0;
    int left = 0;
    int right =x;
    double mid = 0;
    while(left <= right){
        mid = left + (right - left) / 2;
        if(mid * mid <= x){
            res = mid;
            left = mid+1;
        }
        else{
            right = mid - 1;
        }
    }
    return res;
}

求平方根其实也是一种"搜索"过程。我们要在一个有序的范围( 0 0 0 到 x x x)内,找到一个整数 k k k,使得 k 2 ≤ x k^2 \le x k2≤x 且 ( k + 1 ) 2 > x (k+1)^2 > x (k+1)2>x。

判断条件:

  • 如果 m i d 2 = = x mid^2 == x mid2==x,说明刚好找到了,直接返回 m i d mid mid。
  • 如果 m i d 2 < x mid^2 < x mid2<x,说明 m i d mid mid 有可能是答案,但可能还有更大的数也满足条件,所以我们先记录下这个 m i d mid mid(作为备胎答案),然后去右半区间继续找:left = mid + 1。
  • 如果 m i d 2 > x mid^2 > x mid2>x,说明 m i d mid mid 太大了,去左半区间找:right = mid - 1。
    优化版
    对0和1单独处理,使用除法防止mid*mid溢出
c 复制代码
int mySqrt(int x) {
    if(x < 2) return x;//0和1直接返回
    int left = 0,right = x;
    int ans = 0;
    while(left <= right){
        int mid = left + (right - left) / 2;
        //使用除法防止mid*mid溢出
        //相当于判断mid <= x / mid
        if(mid <= x / mid){
            ans = mid;//暂时记录答案
            left = mid + 1;//尝试找更大的
        }
        else{
            right = mid - 1;//太大了,往小了找
        }
    }
    return ans;
}

另解牛顿迭代法

牛顿迭代法(Newton's Method)是一种在数值分析中用于近似求解方程根的方法。

对于力扣 69 题求 ,实际上就是求方程 的正根。


copy from gimini嘿嘿

1. 核心原理:几何直观

牛顿迭代法的精髓在于:用切线逼近曲线

  1. 在曲线上随机找一个点 。
  2. 做曲线在该点处的切线
  3. 找到这条切线与 轴(横轴)的交点,记为 。
  4. 这个交点 会比之前的 更接近方程的真实根。
  5. 重复这个过程,直到精度满足要求。

2. 公式推导

这就是著名的平方根迭代公式


3. C 语言代码实现

牛顿迭代法的收敛速度极快(平方收敛),通常只需要几次迭代就能达到很高的精度。

c 复制代码
int mySqrt(int x) {
    if (x == 0) return 0;

    double C = x, y = x;
    while (1) {
        double next_y = 0.5 * (y + C / y);
        // 如果两次迭代的结果非常接近,说明已经收敛
        if (fabs(y - next_y) < 1e-7) {
            break;
        }
        y = next_y;
    }
    
    // 题目要求返回整数部分,强制转换即可
    return (int)y;
}

4. 牛顿迭代法 vs 二分查找

特性 二分查找 (Binary Search) 牛顿迭代法 (Newton's Method)
原理 逐步缩小搜索区间 利用导数(切线)寻找零点
收敛速度 较慢(对数级) 极快(平方级)
适用性 只要函数单调即可 函数需要可导,且起始值选取会影响收敛
代码难度 简单,注意边界 简单,需理解公式

5. 为什么在力扣 69 题中常用?

虽然二分查找已经足够通过,但在工程和科学计算(如 sqrt 的标准库实现)中,牛顿迭代法或其变体(如倒数平方根算法)更为常见。

一个有趣的知识点:

著名的《雷神之锤 III》代码中有一个"魔数" 0x5f3759df,它就是通过一种极其巧妙的方式为牛顿迭代法提供了一个完美的初始猜测值,从而在极短时间内算出了平方根倒数。

时间复杂度:O(logn)

空间复杂度:O(1)

5、有效的完全平方数

是第四题的变种,会第四题的话这题应该也没问题~
题目

代码:

c 复制代码
bool isPerfectSquare(int num) {
    int left = 0;
    int right = num;
    double mid = 0;
    while(left <= right){
        mid = left + (right-left) / 2;
        if(mid * mid == num){
            return true;
        }
        else if(mid * mid > num){
            right = mid-1;
        }
        else{
            left = mid + 1;
        }
    }
    return false;
}

时间复杂度:O(logn)

空间复杂度:O(1)

关于第4题和第5题

我错误的使用了double,正确应该使用long long

以下是gimini老师给出的关于他两的区别的回答

在编程(尤其是 C 语言)中,long longdouble 的区别本质上是 "如何用二进制位来表示数字" 的区别。一个是为了绝对精确 而生,一个是为了范围极其巨大而生。

以下是它们的详细对比:

1. 核心属性对比

特性 long long (长长整型) double (双精度浮点型)
数字类型 整数(没有小数点) 实数(带有小数点)
占用空间 8 字节 (64 bits) 8 字节 (64 bits)
精确度 100% 精确 存在精度误差(约 15-17 位有效数字)
表示范围 约 (范围极大)
运算速度 通常更快(CPU原生支持整数运算) 略慢(涉及阶码和尾数解析)

2. 存储结构的区别

这是两者最根本的不同:

  • long long:直球存储
    它直接将数字转为二进制补码。每一位都代表一个确定的数值。
  • double:科学计数法存储 (IEEE 754 标准)
    它把 64 位拆成三部分:符号位 + 指数位 + 尾数位
    就像把数字记作 。这种方式能表示天文数字(比如宇宙中原子的数量),但代价是无法精确存储每一个具体的数字。

3. 为什么在刷题(如 LeetCode)中很重要?

情况 A:判断 mid * mid == num
  • **使用 long long**:如果 mid * mid 的结果是 12345678901234567,它存的就是这个数,判断相等是可靠的。
  • **使用 double**:由于精度限制,double 可能无法区分 1234567890123456712345678901234568。在处理大整数时,double== 判断可能会给你错误的结果。
情况 B:溢出风险
  • int 的上限大约是 。
  • int * int 很容易超过这个范围。
  • long long 的上限大约是 ,足以应对绝大多数算法题中的平方运算。

4. 形象的类比

  • long long 就像一把刻度精细到 1 毫米的尺子。你可以用它测量非常精确的长度,但如果东西太长(超过尺子长度),你就能量不动了(溢出)。
  • double 就像一个带有刻度的可伸缩望远镜。你可以看到极远星系的距离,也可以看到桌上的蚂蚁,但你量出来的数值永远是"大概"、"约等于"。

总结建议

  1. 处理数组下标、计数、大整数乘法 :永远优先选择 long long
  2. 处理除法(有小数)、几何计算、物理模拟 :选择 double
  3. 在 LeetCode 367(有效完全平方数)中 :请务必使用 long long,因为这道题考察的是整数的精确匹配。
相关推荐
李慕婉学姐29 分钟前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
じ☆冷颜〃42 分钟前
黎曼几何驱动的算法与系统设计:理论、实践与跨领域应用
笔记·python·深度学习·网络协议·算法·机器学习
数据大魔方1 小时前
【期货量化实战】日内动量策略:顺势而为的短线交易法(Python源码)
开发语言·数据库·python·mysql·算法·github·程序员创富
POLITE31 小时前
Leetcode 23. 合并 K 个升序链表 (Day 12)
算法·leetcode·链表
楚来客1 小时前
AI基础概念之八:Transformer算法通俗解析
人工智能·算法·transformer
Echo_NGC22372 小时前
【神经视频编解码NVC】传统神经视频编解码完全指南:从零读懂 AI 视频压缩的基石
人工智能·深度学习·算法·机器学习·视频编解码
会员果汁2 小时前
leetcode-动态规划-买卖股票
算法·leetcode·动态规划
奋进的芋圆2 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——二进制求和
算法·leetcode·哈希算法·散列表·结构与算法
sxlishaobin2 小时前
设计模式之桥接模式
java·设计模式·桥接模式