【算法专题训练】38、二分查找算法

1、二分查找

  • 在一个长度为n的数组中要查找一个数字,逐一扫描数组中的每个数字,需要O(n)的时间
  • 如果数组是排序的,那么可以采用二分查找算法进行优化,采用分治思想,将将数组分为三个区域,判断中间值是否命中,如果没命中,继续在其中的一个区域继续进行分治查询

2、LCR 068. 搜索插入位置

题目信息:

cpp 复制代码
给定一个排序的整数数组 nums 和一个整数目标值 target ,请在数组中找到 target ,并返回其下标。如果目标值不存在于数组中,
返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。

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

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

示例 3:
输入: nums = [1,3,5,6], target = 7
输出: 4

示例 4:
输入: nums = [1,3,5,6], target = 0
输出: 0

示例 5:
输入: nums = [1], target = 0
输出: 0

提示:
1 <= nums.length <= 104
-104 <= nums[i] <= 104
nums 为无重复元素的升序排列数组
-104 <= target <= 104

解题思路:

  • 1、审题:
  • 输入一个升序排序的整数数组和一个目标值target,要求在数组中查找是否存在目标值,如果存在返回该目标值在数组中的下标位置,
    • 如果不存在,返回插入该数值应该所在的位置
  • 2、解题:题目要求使用二分查找法
  • 使用左右指针left,right分别标记查找范围,取中间值mid
  • 如果mid下标值等于目标值,则返回mid
  • 如果mid值大于目标值,且mid-1小于目标值,则查找到插入位置,如果mid已经是小标0了,也直接返回
  • 如果mid值小于目标值,继续在后半部分查找, left = mid + 1
  • 如果还是没有找到,说明数组中所有元素都大小目标值,应该插入到数组最末尾端

代码实现:

cpp 复制代码
int searchInsert(vector<int> &nums, int target)
{
    int left = 0;
    int right = nums.size() - 1;
    int mid = 0;

    while (left <= right)
    {
        mid = left + (right - left) / 2;
        if (nums[mid] == target)
        {
            return mid;
        }

        if (nums[mid] > target)
        {
            if (mid == 0 || nums[mid - 1] < target)
            {
                return mid;
            }
            right = mid - 1;
        }
        else
        {
            left = mid + 1;
        }
    }
    return nums.size();
}

3、LCR 069. 山脉数组的峰顶索引

题目信息:

cpp 复制代码
符合下列属性的数组 arr 称为 山峰数组(山脉数组) :
arr.length >= 3
存在 i(0 < i < arr.length - 1)使得:
arr[0] < arr[1] < ... arr[i-1] < arr[i]
arr[i] > arr[i+1] > ... > arr[arr.length - 1]
给定由整数组成的山峰数组 arr ,返回任何满足 arr[0] < arr[1] < ... arr[i - 1] < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i ,即山峰顶部。

示例 1:
输入:arr = [0,1,0]
输出:1

示例 2:
输入:arr = [1,3,5,4,2]
输出:2

示例 3:
输入:arr = [0,10,5,2]
输出:1

示例 4:
输入:arr = [3,4,5,1]
输出:2

示例 5:
输入:arr = [24,69,100,99,79,78,67,36,26,19]
输出:2

提示:
3 <= arr.length <= 104
0 <= arr[i] <= 106
题目数据保证 arr 是一个山脉数组

进阶:很容易想到时间复杂度 O(n) 的解决方案,你可以设计一个 O(log(n)) 的解决方案吗?

解题思路:

  • 1、审题:输入一个山峰数组,就是数据前半部分排序不断升高,后半部分不断降低,要求找出这个山峰的封顶位置
  • 2、解题:
  • 普通解法:遍历整个数组元素,峰顶元素肯定是比前后两个元素都大的数据,需要O(n)算法复杂度
  • 二分查找解法:使用两个指针left,right标记数组的前后两个元素位置,不断计算得出中间元素位置mid
  • 如果mid下标元素,比前后两个元素都大,则命中
  • 如果mid下标元素,比前一个元素大,处于升序部分,则峰顶元素在数组后半部分,反之峰顶元素在前半部分
  • 山峰数组最后有三个元素组成,left与right可以从第二个位置元素开始遍历查找

代码实现:

cpp 复制代码
int peakIndexInMountainArray(vector<int> &arr)
{
    int left = 1;
    int right = arr.size() - 2;
    int mid;
    while (left <= right)
    {
        mid = left + (right - left) / 2;
        if (arr[mid - 1] < arr[mid] && arr[mid] > arr[mid + 1])
        {
            return mid;
        }
        // 升序部分
        if (arr[mid - 1] < arr[mid])
        {
            left = mid + 1;
        }
        else
        {
            right = mid - 1;
        }
    }
    return -1;
}

4、LCR 070. 有序数组中的单一元素

题目信息:

cpp 复制代码
给定一个只包含整数的有序数组 nums ,每个元素都会出现两次,唯有一个数只会出现一次,请找出这个唯一的数字。

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

示例 2:
输入: nums =  [3,3,7,7,10,11,11]
输出: 10

提示:
1 <= nums.length <= 105
0 <= nums[i] <= 105
进阶:采用的方案可以在 O(log n) 时间复杂度和 O(1) 空间复杂度中运行吗?

解题思路:

  • 二分查找解法
  • 使用异或操作的解法时间复杂度为O(n),没有使用有序数组的特性
  • 将数组元素从最开始位置,两两合在一起判断,查找其中的两个元素是否相等,如果相同则不同的元素在后面
  • 如果不同,需要判断是否是第一组不相同的数组组合,如果不是则继续遍历前面的组合,前面的数组组合刚好两个元素相同则命中。

代码实现:

cpp 复制代码
int singleNonDuplicate(vector<int> &nums)
{
    int res = nums[0];
    int left = 0;
    int right = nums.size() - 1;
    if (right == 0)
    {
        return res;
    }
    while (left <= right)
    {
        int mid = left + (right - left) / 2;
        mid = (mid / 2) * 2;

        if (mid == 0 && nums[mid] != nums[mid + 1])
        {
            return nums[mid];
        }
        if (nums[mid] != nums[mid + 1] && nums[mid - 2] == nums[mid - 1])
        {
            return nums[mid];
        }
        if (nums[mid] == nums[mid + 1])
        {
            left = mid + 2;
        }
        else
        {
            right = mid - 2;
        }
    }
    return res;
}

5、LCR 071. 按权重随机选择

题目信息:

cpp 复制代码
给定一个正整数数组 w ,其中 w[i] 代表下标 i 的权重(下标从 0 开始),请写一个函数 pickIndex ,它可以随机地获取下标 i,选取下标 i 的概率与 w[i] 成正比。
例如,对于 w = [1, 3],挑选下标 0 的概率为 1 / (1 + 3) = 0.25 (即,25%),而选取下标 1 的概率为 3 / (1 + 3) = 0.75(即,75%)。
也就是说,选取下标 i 的概率为 w[i] / sum(w) 。

示例 1:
输入:
inputs = ["Solution","pickIndex"]
inputs = [[[1]],[]]
输出:
[null,0]
解释:
Solution solution = new Solution([1]);
solution.pickIndex(); // 返回 0,因为数组中只有一个元素,所以唯一的选择是返回下标 0。

示例 2:
输入:
inputs = ["Solution","pickIndex","pickIndex","pickIndex","pickIndex","pickIndex"]
inputs = [[[1,3]],[],[],[],[],[]]
输出:
[null,1,1,1,1,0]
解释:
Solution solution = new Solution([1, 3]);
solution.pickIndex(); // 返回 1,返回下标 1,返回该下标概率为 3/4 。
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 1
solution.pickIndex(); // 返回 0,返回下标 0,返回该下标概率为 1/4 。

由于这是一个随机问题,允许多个答案,因此下列输出都可以被认为是正确的:
[null,1,1,1,1,0]
[null,1,1,1,1,1]
[null,1,1,1,0,0]
[null,1,1,1,0,1]
[null,1,0,1,0,0]
......
诸若此类。

提示:
1 <= w.length <= 10000
1 <= w[i] <= 10^5
pickIndex 将被调用不超过 10000 次

解题思路:

  • 1、审题: 输入一个整数数组,他有一个pickIndex方法,要求该方法返回数组中的一个下标,返回数组下标的概率和该下标对应元素的值在整个数组中的占比成正比
  • 也就是说,该数组元素值越大,那么返回该元素下标的比例也越高
  • 2、解题:
  • 将上面的问题转换成累加和数组,新数组的元素值表示能够承接的元素范围
  • 使用变量total表示所有数组元素的累加和,根据该total值计算他的随机数,以随机数作为基准值取新的累加数组中获取对应的下标值
  • 要求在新数组中寻找第一个比基准值大的元素,并返回该下标,使用二分法查询方式来解题

代码实现:

cpp 复制代码
class Solution
{
public:
    Solution(vector<int> &w)
    {
        for (auto num : w)
        {
            total += num;
            nums.push_back(total);
        }
    }
    static int genRandomNum(int total)
    {
        // 随机数引擎(生成器)
        std::random_device rd;
        std::mt19937 gen(rd());

        // 定义分布范围
        std::uniform_int_distribution<> dis(0, total - 1);

        // 生成随机数
        int random_num = dis(gen);
        return random_num;
    }

    int pickIndex()
    {
        // 生成随机数
        int p = genRandomNum(total);
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right)
        {
            int mid = left + (right - left) / 2;
            if (nums[mid] > p)
            {
                if (mid == 0 || nums[mid - 1] <= p)
                {
                    return mid;
                }
                right = mid - 1;
            }
            else
            {
                left = mid + 1;
            }
        }
        return nums.size() - 1;
    }

private:
    int total = 0;
    std::vector<int> nums;
};

6、总结

  • 使用二分查找算法的前提是数组要求有序
  • 二分查找的核心思想都是将原有的有序数组分成三个区域,mid中间位置,和前后两个区域,如果没命中mid中间值,根据判断条件缩小查询范围,直接while循环结束
相关推荐
探序基因3 小时前
单细胞Seurat数据结构修改分群信息
数据结构
六义义3 小时前
java基础十二
java·数据结构·算法
四维碎片3 小时前
QSettings + INI 笔记
笔记·qt·算法
Tansmjs3 小时前
C++与GPU计算(CUDA)
开发语言·c++·算法
独自破碎E4 小时前
【优先级队列】主持人调度(二)
算法
weixin_445476684 小时前
leetCode每日一题——边反转的最小成本
算法·leetcode·职场和发展
打工的小王4 小时前
LeetCode Hot100(一)二分查找
算法·leetcode·职场和发展
Swift社区4 小时前
LeetCode 385 迷你语法分析器
算法·leetcode·职场和发展
sonadorje5 小时前
svd在图像处理中的应用
算法
挖矿大亨5 小时前
c++中的函数模版
java·c++·算法