探索算法系列 - 二分查找算法

目录

二分查找(原题链接)

在排序数组中查找元素的第一个和最后一个位置(原题链接)

搜索插入位置(原题链接)

[x 的平方根(原题链接)](#x 的平方根(原题链接))

山脉数组的峰顶索引(原题链接)

寻找峰值(原题链接)

寻找旋转排序数组中的最小值(原题链接)

点名(原题链接)


二分查找(原题链接)

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

解题思路

该算法实现的是二分查找算法,用于在一个有序数组中查找目标值的位置。二分查找的核心思想是每次将查找范围缩小一半,从而在对数时间内找到目标值。
步骤说明

  1. 初始化指针

    • left 指向数组的起始位置,right 指向数组的末尾位置。这样定义指针是为了确定搜索区间。
  2. 进入循环

    • left 指针小于等于 right 指针时,继续循环。循环的目的是不断缩小查找区间,直到找到目标值或区间无效。
  3. 计算中间位置

    • 计算 mid 的位置作为当前查找的中间位置,使用 (left + right) / 2 的方式,但为了避免整数溢出,改用 left + (right - left) / 2
  4. 判断中间值

    • 如果 nums[mid] 等于目标值 target,直接返回 mid,表示找到了目标值。
    • 如果 nums[mid] 大于目标值,说明目标值在左半部分,将 right 指针移动到 mid - 1
    • 如果 nums[mid] 小于目标值,说明目标值在右半部分,将 left 指针移动到 mid + 1
  5. 返回结果

    • 如果在循环结束后仍未找到目标值,则返回 -1,表示数组中不存在目标值。
      具体代码

    class Solution
    {
    public:
    int search(vector<int>& nums, int target)
    {
    // 初始化 left 与 right 指针
    int left = 0, right = nums.size() - 1;
    // 由于两个指针相交时,当前元素还未判断,因此需要取等号
    while (left <= right)
    {
    // 先找到区间的中间元素
    int mid = left + (right - left) / 2; //防止溢出
    // 判断中间元素是否为目标值
    if (nums[mid] == target)
    return mid;
    // 如果中间元素大于目标值,缩小右边界
    else if (nums[mid] > target)
    right = mid - 1;
    // 如果中间元素小于目标值,缩小左边界
    else
    left = mid + 1;
    }
    // 如果未找到目标值,返回 -1
    return -1;
    }
    };

在排序数组中查找元素的第一个和最后一个位置(原题链接)

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

解题思路

该算法的目的是找到目标值在有序数组中的起始位置和结束位置。为了达到这个目的,算法使用了二分查找的方法,分别寻找目标值的左端点和右端点。由于二分查找在有序数组中可以高效地定位目标值,因此这种方法非常适合这个问题。
步骤说明

  1. 处理边界情况

    • 如果数组为空,直接返回 {-1, -1}
  2. 查找左端点

    • 使用二分查找找到目标值的第一个出现位置(左端点)。如果 nums[mid] 小于目标值,说明左半部分不可能包含目标值,移动左指针。否则,右半部分可能包含目标值或正好是目标值,因此右指针移动到 mid
    • 最后检查 nums[left] 是否等于目标值来验证是否找到目标值。
  3. 查找右端点

    • 使用二分查找找到目标值的最后一个出现位置(右端点)。为了保证找到的右端点是最后一个目标值的位置,计算 mid 时取 (left + (right - left + 1) / 2)。如果 nums[mid] 小于等于目标值,左指针移动到 mid;否则,右指针移动到 mid - 1
    • 由于二分查找结束时 left 指向目标值的最后一个位置,因此返回 {begin, right},其中 begin 是左端点,right 是右端点。
      具体代码

    class Solution
    {
    public:
    vector<int> searchRange(vector<int>& nums, int target)
    {
    // 处理边界情况:如果数组为空,直接返回 {-1, -1}
    if (nums.empty())
    return {-1, -1};

         int begin = 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; // 目标值在左半部分
         }
         // 返回目标值的起始和结束位置
         return {begin, right};
     }
    

    };

搜索插入位置(原题链接)

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

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

解题思路

这个算法的目标是找到目标值 target 应该插入到有序数组 nums 的位置,使得插入后的数组仍然保持有序。这个问题可以通过修改的二分查找算法来解决。算法会找到目标值插入的位置,确保所有小于目标值的元素在它前面,所有大于目标值的元素在它后面。
步骤说明

  1. 初始化指针

    • 使用两个指针 leftright 来表示当前查找的区间,初始值分别为数组的起始位置和末尾位置。
  2. 二分查找

    • left 小于 right 时,计算中间位置 mid
    • 如果 nums[mid] 小于目标值 target,说明目标值在右半部分,因此将 left 指针移动到 mid + 1
    • 如果 nums[mid] 大于或等于目标值,说明目标值在左半部分或正好是中间值,因此将 right 指针移动到 mid
  3. 确定插入位置

    • 循环结束时,left 应该是目标值应该插入的位置。如果 nums[left] 小于目标值,则 right + 1 为插入位置;否则,right 为插入位置。
      具体代码

    class Solution
    {
    public:
    int searchInsert(vector<int>& nums, int target)
    {
    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;     // 目标值在左半部分或正好是中间值
         }
         
         // 确定目标值的插入位置
         // 如果 nums[left] 小于目标值,插入位置是 right + 1
         // 否则,插入位置是 right
         if (nums[left] < target)
             return right + 1;
         return right;
     }
    

    };

x 的平方根(原题链接)

给你一个非负整数 x ,计算并返回 x算术平方根

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

注意: 不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

解题思路

要找到一个非负整数 x 的平方根,我们可以使用二分查找算法。目标是找到最大的整数 y,使得 y^2 小于或等于 x。在有序的整数区间 [0, x] 上进行二分查找可以有效地找到这个整数 y
步骤说明

  1. 处理边界情况

    • 如果 x 小于 1,则平方根应该是 0(这是因为平方根在负数或零范围内的定义)。
  2. 初始化指针

    • left 初始化为 1(对于 x >= 1 的情况),right 初始化为 x。这些指针定义了二分查找的区间。
  3. 二分查找

    • 使用二分查找来找到最大整数 y,使得 y^2 <= x。计算中间位置 mid 时使用 (left + (right - left + 1) / 2) 来避免整数溢出。
    • 如果 mid * mid 小于或等于 x,说明 mid 可以是平方根,或者在 mid 右侧的值也有可能是平方根,因此移动左指针到 mid
    • 否则,目标平方根只能在 mid 的左侧,因此移动右指针到 mid - 1
  4. 返回结果

    • 循环结束时,left 就是最大整数,使得 left^2 小于或等于 x,即所求的平方根。
      具体代码

    class Solution
    {
    public:
    int mySqrt(int x)
    {
    // 处理边界情况:x 小于 1 的平方根是 0
    if (x < 1)
    return 0;

         int left = 1, right = x;
         
         // 二分查找平方根
         while (left < right) 
         {
             // 防止中间值计算溢出
             long long mid = left + (right - left + 1) / 2;
             
             // 检查 mid^2 是否小于等于 x
             if (mid * mid <= x)
                 left = mid;   // mid 可能是答案,尝试右边
             else
                 right = mid - 1; // mid 不是答案,调整右边界
         }
         
         // 返回找到的最大平方根整数
         return left;
     }
    

    };

山脉数组的峰顶索引(原题链接)

给定一个长度为 n 的整数 山脉 数组 arr ,其中的值递增到一个 峰值元素 然后递减。

返回峰值元素的下标。

你必须设计并实现时间复杂度为 O(log(n)) 的解决方案。

解题思路

山脉数组的特性是存在一个峰值元素,使得在峰值左边的元素严格递增,右边的元素严格递减。我们可以利用二分查找来高效地找到这个峰值。通过比较中间元素与其相邻的元素,我们可以判断当前元素是在上升序列中还是下降序列中,从而调整搜索区间。
步骤说明

  1. 初始化指针

    • left 初始化为 0,right 初始化为数组的最后一个索引。这两个指针用于定义搜索区间。
  2. 二分查找

    • 计算中间位置 mid,为了防止溢出,使用 left + (right - left + 1) / 2
    • 判断 arr[mid]arr[mid - 1] 的关系:
      • 如果 arr[mid] > arr[mid - 1],说明 mid 处于山脉的上升部分,峰值在 mid 右侧或者就是 mid,因此移动 left 指针到 mid
      • 如果 arr[mid] <= arr[mid - 1],说明 mid 处于山脉的下降部分,峰值在 mid 左侧,因此移动 right 指针到 mid - 1
  3. 返回结果

    • left == right 时,leftright 指向的就是峰值元素的索引。
      具体代码

    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;
             
             // 判断 mid 是否处于上升部分
             if (arr[mid] > arr[mid - 1])
                 left = mid;  // 峰值在右侧或就是 mid
             else
                 right = mid - 1; // 峰值在左侧
         }
         
         // 返回找到的峰值索引
         return left;
     }
    

    };

寻找峰值(原题链接)

峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞

你必须实现时间复杂度为 O(log n)的算法来解决此问题。

解题思路

在一个无序数组中寻找峰值元素,可以利用二分查找的思想。通过比较中间元素与其右侧元素的大小,可以判断峰值所在的区域,从而逐步缩小查找范围。最终找到一个峰值元素的索引。
步骤说明

  1. 初始化指针

    • left 初始化为数组的起始位置 0,right 初始化为数组的末尾位置 nums.size() - 1
  2. 二分查找

    • left 小于 right 的情况下,继续循环。
    • 计算中间位置 mid,为防止溢出,使用 left + (right - left) / 2
    • 判断 nums[mid]nums[mid + 1] 的关系:
      • 如果 nums[mid] > nums[mid + 1],说明在 mid 位置的左侧可能存在峰值(包括 mid),因此移动 right 指针到 mid
      • 如果 nums[mid] <= nums[mid + 1],说明在 mid 位置的右侧存在峰值,因此移动 left 指针到 mid + 1
  3. 返回结果

    • leftright 相等时,循环结束,left 即为峰值元素的索引。
      具体代码

    class Solution
    {
    public:
    int findPeakElement(vector<int>& nums)
    {
    int left = 0, right = nums.size() - 1;

         // 二分查找峰值元素
         while (left < right) 
         {
             // 防止溢出计算中间位置
             int mid = left + (right - left) / 2;
             
             // 判断 mid 是否处于下降坡度
             if (nums[mid] > nums[mid + 1])
                 right = mid; // 峰值在左侧,包括 mid 位置
             else
                 left = mid + 1; // 峰值在右侧
         }
         
         // 返回找到的峰值索引
         return left;
     }
    

    };

寻找旋转排序数组中的最小值(原题链接)

已知一个长度为 n 的数组,预先按照升序排列,经由 1n旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:

  • 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
  • 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]

注意,数组 [a[0], a[1], a[2], ..., a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素

你必须设计一个时间复杂度为 O(log n) 的算法解决此问题。

解题思路

旋转排序数组中的最小元素是所有元素中最小的值,且它右侧的元素都大于它。利用二分查找,可以在 O(log n) 时间复杂度内找到这个最小值。关键是判断中间元素的位置,以确定缩小搜索区间的方向。
步骤说明

  1. 初始化指针

    • left 初始化为数组的起始位置 0,right 初始化为数组的末尾位置 nums.size() - 1
  2. 选择基准值

    • 选择数组末尾的元素 nums[right] 作为基准值 x。在未旋转的情况下,最小值就是这个基准值。
  3. 二分查找

    • left 小于 right 时,继续循环。
    • 计算中间位置 mid,为防止溢出,使用 left + (right - left) / 2
    • 如果 nums[mid] > x,说明最小元素在右侧,因此 left 移动到 mid + 1
    • 如果 nums[mid] <= x,说明最小元素在左侧或中间位置,因此 right 移动到 mid
  4. 返回结果

    • leftright 相等时,循环结束,此时 left 指向的元素就是最小元素。
      具体代码

    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;
             
             // 判断 mid 位置的元素与基准值的关系
             if (nums[mid] > x)
                 left = mid + 1; // 最小元素在右侧
             else
                 right = mid; // 最小元素在左侧或 mid 位置
         }
         
         // 返回最小元素
         return nums[left];
     }
    

    };

点名(原题链接)

某班级 n 位同学的学号为 0 ~ n-1。点名结果记录于升序数组 records。假定仅有一位同学缺席,请返回他的学号。

解题思路

这段代码的主要目的是在一个有序数组 records 中查找一个满足特定条件的位置。具体来说,数组 records 中的元素表示某种记录,每个元素的值表示某种"出勤"记录。在这个问题中,要求找到一个位置 i 使得 records[i] 不等于 i,并返回该位置。如果所有位置 i 都满足 records[i] == i,则返回数组的长度。
步骤说明

  1. 初始化指针 : 定义两个指针 leftright,分别指向数组的开头和结尾。

  2. 二分查找 : 使用二分查找的方法逐步缩小查找范围。通过计算 mid,即中间位置的索引,来分割数组。

  3. 条件判断:

    • 如果 records[mid] == mid,说明在 mid 及其左侧的元素都满足 records[i] == i,因此将 left 移动到 mid + 1 继续查找。
    • 如果 records[mid] != mid,则可能的解在 mid 及其左侧,因此将 right 移动到 mid
  4. 返回结果 : 最后,检查 leftrecords[left] 的关系:

    • 如果 left == records[left],则说明找到了一个最后满足 records[i] == i 的位置,返回 left + 1
    • 否则,返回 left 作为结果。
      具体代码

    class Solution
    {
    public:
    int takeAttendance(vector<int>& records)
    {
    int left = 0, right = records.size() - 1;
    // 使用二分查找方法
    while( left < right)
    {
    int mid = left + (right - left) / 2;
    if(records[mid] == mid)
    left = mid + 1; // 更新left指针,因为此时mid及左边的元素都满足条件
    else
    right = mid; // 更新right指针,因为可能的解在mid及左边
    }
    // 最终检查left位置是否满足条件,返回结果
    return left == records[left] ? left + 1 : left;
    }
    };

相关推荐
‘’林花谢了春红‘’41 分钟前
C++ list (链表)容器
c++·链表·list
搬砖的小码农_Sky2 小时前
C语言:数组
c语言·数据结构
机器视觉知识推荐、就业指导2 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
Swift社区3 小时前
LeetCode - #139 单词拆分
算法·leetcode·职场和发展
Kent_J_Truman4 小时前
greater<>() 、less<>()及运算符 < 重载在排序和堆中的使用
算法
先鱼鲨生4 小时前
数据结构——栈、队列
数据结构
一念之坤4 小时前
零基础学Python之数据结构 -- 01篇
数据结构·python
IT 青年4 小时前
数据结构 (1)基本概念和术语
数据结构·算法
Yang.994 小时前
基于Windows系统用C++做一个点名工具
c++·windows·sql·visual studio code·sqlite3