王清欢Randy 王清欢Randy
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
首页
  • 编程语言

    • C/C++ 学习笔记
    • Golang 学习笔记
  • 算法分析

    • LeetCode 刷题笔记
  • 操作系统

    • Linux 基础
    • Vim 实用技巧
    • Shell 脚本编程
    • GDB 学习笔记
  • 开发工具

    • Git 学习笔记
  • 分布式理论

    • 共识算法
    • 分布式事务
  • 数据库内核

    • PostgreSQL
    • Postgres-XL
  • hidb
  • pgproxy
  • 实用技巧
  • 学习方法
  • 资源分享
GitHub (opens new window)
  • 基础算法

    • 双指针

      • 双指针基础
      • 碰撞指针
      • 快慢指针
      • 滑动窗口
    • 二分查找

      • 基础应用
        • 01 二分查找的基础应用
          • 二分查找简介
          • 二分查找的基础应用
          • 35 搜索插入位置
          • 69 Sqrt(x)
          • 74 搜索二维矩阵
          • 540 有序数组中的单一元素
        • 参考资料
      • 边界收缩
      • 局部有序
    • 排序算法

      • 01八大排序
      • 02快速排序
      • 03归并排序
      • 04桶排序
      • 05堆排序
    • 优先搜索

      • 01递归
      • 02网格结构深度优先搜索
      • 03树结构深度优先搜索
      • 04图结构深度优先搜索
      • 05网格结构广度优先搜索
      • 06树结构广度优先搜索
      • 07图结构广度优先搜索
    • 回溯算法

      • 01递归
      • 02 子集问题
      • 03 全排列问题
      • 04 组合问题
      • 05 回溯搜索问题
  • 基础数据结构

    • 线性表与哈希表

      • 01数组
      • 02栈和队列
      • 03单调栈
      • 04优先队列
      • 05双端队列
      • 06哈希表
      • 07多重集合
      • 08前缀和
      • 09数据结构设计
    • 字符串

      • 01字符串比较
      • 02回文字符串
      • 03字符串匹配
      • 04字符串算术表达式
    • 单链表

      • 01链表基础操作
      • 02链表遍历
    • 二叉树

      • 01二叉树的属性
      • 02二叉树的操作
      • 03层次遍历
      • 04前中后序遍历
      • 05二叉搜索树的属性
      • 06二叉搜索树的操作
      • 07字典树
      • 08二叉搜索树BST
    • 图

      • 01二分图
      • 02拓扑排序
      • 03并查集
      • 04最小生成树
      • 05最短路径
  • 进阶算法

    • 贪心算法

      • 01跳跃游戏
      • 02分配问题
      • 03区间问题
    • 分治策略

    • 动态规划

      • 01一维动态规划
      • 02二维动态规划
      • 03分割型动态规划
      • 04子序列问题
      • 05背包问题
      • 06字符串编辑问题
      • 07股票交易问题
  • 其他内容

    • 数学问题

      • 01公倍数与公因数
      • 02质数问题
      • 03进制转换问题
      • 04数字字符串求和问题
      • 05众数问题
      • 06中位数问题
      • 07数字处理问题
      • 08随机数问题
    • 位运算

      • 01位运算基础
      • 02妙用异或运算
      • 03二进制特性
  • LeetCode刷题笔记
  • 基础算法
  • 二分查找
王清欢
2023-03-24
目录

基础应用

# 01 二分查找的基础应用

# 二分查找简介

​ 二分查找也常被称为二分法或者折半查找,它是一种在有序数组中查找某一特定元素的查找算法。这种查找方法将查找的时间复杂度从原本的线性时间提升到了对数时间范围,大大缩短了搜索时间。对于一个长度为 O(n) 的数组,二分查找的时间复杂度为 O(log n)。

​ 举例来说,给定一个排好序的数组 {3,4,5,6,7},我们希望查找 4 在不在这个数组内。第一次折半时考虑中位数 5,因为 5 大于 4, 所以如果 4 存在于这个数组,那么其必定存在于 5 左边这一半。于是我们的查找区间变成了 {3,4,5}。根据具体情况和您的刷题习惯,这里的 5 可以保留也可以不保留,并不影响时间复杂度的级别。第二次折半时考虑新的中位数 4,正好是我们是需要查找的数字。于是我们发现,对于一个长度为 5 的数组,我们只进行了 2 次查找。如果是遍历数组,最坏的情况则需要查找 5 次。

​ 具体到代码上,二分查找时区间的左右端取开区间还是闭区间在绝大多数时候都可以,因此有些初学者会容易搞不清楚如何定义区间开闭性。这里我提供两个小诀窍,第一是尝试熟练使用一种写法,比如左闭右开(满足 C++、Python 等语言的习惯)或左闭右闭(便于处理边界条件),尽量只保持这一种写法;第二是在刷题时思考如果最后区间只剩下一个数或者两个数,自己的写法是否会陷入死循环,如果某种写法无法跳出死循环,则考虑尝试另一种写法。我们给出左闭右开的二分查找基本程序结构如下:

int basicBinarySearch(vector<int> arr, int target){
    int left = 0;
    int right = arr.size();
    while(left < right){
        int mid = left + (right-left)/2;
        if(arr[mid] == target){
            return mid;
        }
        else if (arr[mid] > target){
        	right = mid; 
        }
        else{
            left = mid + 1;
        }
    }
    return -1;
}

​ 二分查找也可以看作双指针的一种特殊情况,但我们一般会将二者区分。双指针类型的题,指针通常是一步一步移动的,而在二分查找里,指针每次移动半个区间长度。

# 二分查找的基础应用

​ 二分查找最为基础的应用就是在有序数组中查找某一特定元素,即查找数组中的某个值。

# 35 搜索插入位置 (opens new window)

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

输入一个数组和一个目标值,输出一个整数表示目标值插入的位置

输入: nums = [1,3,5,6], target = 2
输出: 1

解析:

​ 题目要求使用时间复杂度为 O(log n) 的算法。我们可以考虑使用二分查找来寻找合适位置,计算中间位置 mid,有如下三种情况:

  • nums[mid] == target 那么 mid 的位置就是插入位置
  • nums[mid] > target 那么其插入位置在 mid 的左侧
  • nums[mid] < target 那么其插入位置在 mid 的右侧
class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0, right = nums.size();
        int pos = -1;
        while(left<right){
            int mid = left + (right-left)/2;
            if(nums[mid] == target){
                return mid;
            }else if(nums[mid] > target){
                right = mid;
            }else{
                left = mid + 1;
            }
        }
        return left;
    }
};

# 69 Sqrt(x) (opens new window)

给定一个非负整数 x ,计算并返回 x 的 算术平方根 ,由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

输入一个整数,输出一个整数。

输入:x = 8
输出:2
解释:8 的算术平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。

解析:

​ 我们可以把这道题想象成,给定一个非负整数 a,求 f (x) = x^2 − a = 0 的解。因为我们只考虑 x ≥ 0,所以 f (x) 在定义域上是单调递增的。考虑到 f (0) = −a ≤ 0, f (a) = a^2 − a ≥ 0,我们可以对 [0, a] 区间使用二分法找到 f (x) = 0 的解。

​ 值得注意的是,为了防止除以 0,我们把 a = 0 的情况单独考虑,然后对区间 [1, a] 进行二分查找;同时,我们采用左闭右开的写法所以 a = 1 时会出现结果处理不一致的情况也要单独考虑。

class Solution {
public:
    int mySqrt(int x) {
        if(!x) return 0;
        int left = 1, right = x;
        if(left==right) return left;
        while(left<right){
            int mid = left + (right-left)/2;
            int sqrt = x/mid;
            if(mid == sqrt){
                return mid;
            }else if(mid > sqrt){
                right = mid;
            }else{
                left = mid+1;
            }
        }
        return left-1;
    }
};

# 74 搜索二维矩阵 (opens new window)

给定一个 m x n 二维矩阵,已知每行从左到右按升序排列,尝试设计一个快速搜索一个数字是否在矩阵中存在的算法。

输入是一个二维整数矩阵,和一个待搜索整数。输出是一个布尔值,表示这个整数是否存在于矩阵中。

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

解析:

​ 本题使用二分查找有两种思路,一种是先对列二分查找然后对行二分查找,这种两次查找完成搜索;另一种则是将二维数组展平为一个一维数组,然后在该基础上进行二分查找。

​ 这里介绍第二种方法,当然我们不需要真的去做二维数组到一维数组的转化,而是用一维数组的索引去映射二维数组的元素位置。例如,一维数组中索引为 6 的元素映射到 3X4 的二维数组中的位置为matrix[6/4][6%4] 即 matrix[1][2]。又数组整体为递增关系,所以可以方便的采用二分查找搜索元素。当然这种方法中,如果二维矩阵不是对齐的就无法正确应用。

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
        int m = matrix.size(), n = matrix[0].size();
        int left = 0, right = m*n;
        while(left<right){
            int mid = left + (right-left)/2;
            if(matrix[mid/n][mid%n] == target){
                return true;
            }else if(matrix[mid/n][mid%n] > target){
                right = mid;
            }else{
                left = mid + 1;
            }
        }
        return false;
    }
};

​ 除了二分查找,对于这种元素呈现从左上角向右下角递增的排列方式,我们可以直接利用其特性进行快速剪枝搜索目标元素。从矩阵右上角开始搜索 target,如果 target 大于当前值就向下移动,当前行被剪枝;如果 target 小于当前值就向左移动,当前列被剪枝。通过这种快速剪枝的策略可以快速完成搜索。

class Solution {
public:
    bool searchMatrix(vector<vector<int>>& matrix, int target) {
       if(matrix.empty() || matrix[0].empty()){
           return false;
       } 
       int m = matrix.size(), n = matrix[0].size();
       int i = 0, j = n-1;
       while(i<m && j>=0){
           if(target == matrix[i][j]){
                return true;
            }else if(target < matrix[i][j]){
                --j;
            }else{
                ++i;
            }
       }
       return false;
    }
};

# 540 有序数组中的单一元素 (opens new window)

给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。

输入一个数组,输出一个整型值表示数组中唯一仅出现一次的元素。

输入: nums = [1,1,2,3,3,4,4,8,8]
输出: 2

解析:

​ 借助本题我们可以进一步抽象二分查找的应用场景。一般的二分查找应用场景都是根据目标值来将整个数组划分为两部分,一部分整体大于等于目标值,另一部分整体小于目标值。

​ 我们可以进一步抽象二分法为:将数组整体划分为两个局部连续区间,这两个局部区间具有相对的性质。

​ 在本题中采用二分法,我们可以将两个局部区间分为区间长度为偶数和奇数两部分,而奇数区间内就包含着那个唯一出现一次的元素。

​ 使用二分法解决本题的步骤如下:

  • 计算 mid,并判断当前元素是出现一次还是两次
  • 如果仅出现一次即 nums[mid] != nums[mid-1] && nums[mid] != nums[mid+1],直接返回当前元素
  • 如果出现两次就要考虑两种情况,即nums[mid] == nums[mid-1] 还是 nums[mid] == nums[mid+1]。如果是前者,判断[0,mid]区间长度是偶数还是奇数,偶数则在 mid 右侧区间找分界点即left = mid + 1,奇数则在 mid 左侧区间找分界点即right = mid - 1;同理,如果是后者判断[0,mid-1]区间长度为奇数还是偶数,并进行相似操作。
class Solution {
public:
    int singleNonDuplicate(vector<int>& nums) {
        int left = 0, right = nums.size();
        while(left<right){
            int mid = left + (right-left)/2;
            // 元素出现两次的情况 nums[mid] == nums[mid-1]
            if(mid > 0 && nums[mid] == nums[mid-1]){
                // 判断区间长度的奇偶性,与运算,奇数与0001进行与运算结果为1,偶数为0。mid表示的是元素位置
                if(mid&1){
                    left = mid + 1;
                }else{
                    right = mid - 1;
                }
            // 元素出现两次的情况 nums[mid] == nums[mid+1]
            }else if(mid < nums.size()-1 && nums[mid] == nums[mid+1]){
                // 判断区间长度的奇偶性
                if(mid&1){
                    right = mid;
                }else{
                    left = mid + 2;
                }
            }else{
                return nums[mid];
            }
        }
        return -1;
    }
};

​ 当然本题也可以使用异或运算的特性快速解决,与自身进行异或运算结果为0,与0进行异或运算的结果为自身x^x=0, x^0=x。利用这一特性我们将数组元素逐一进行异或运算就可以得到只出现一次的元素了。

class Solution {
public:
    int singleNonDuplicate(vector<int>& nums) {
        int ans = 0;
        for(const auto num:nums){
            ans ^= num;
        }
        return ans;
    }
};

# 参考资料

一文带你刷遍二分查找(视频+绘图) (opens new window)

上次更新: 2023/11/19, 12:55:48
滑动窗口
边界收缩

← 滑动窗口 边界收缩→

Theme by Vdoing | Copyright © 2023-2024 Wang Qinghuan | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式