二分查找

目录

博主对leetcode中可以使用二分查找解题的题目作了总结。

题目来源leetcode,点击题目即可直达leetcode对应题目。

35. 搜索插入位置

题目

题目链接:35. 搜索插入位置

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

请必须使用时间复杂度为 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

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

解法

首先定义target所在区间是左闭右闭的,即[left, right]。

所以:

  • while中的条件是(left <= right),而不是left < right;
  • 当nums[mid] != target而给left或right重新赋值时,应赋mid + 1或mid - 1。

代码如下:

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        int mid;
        while(left <= right)
        {
            mid = left + (right - left) / 2; // 防止溢出,等效于(left + right)/2
            if(nums[mid] < target)
            {
                left = mid + 1; // 新的区间为[mid + 1, right]
            }
            else if(nums[mid] > target)
            {
                right = mid - 1; // 新的区间为[left, mid - 1]
            }
            else if(nums[mid] == target)
                return mid;
        }
        return left;
    }
};

检验一下所有可能的输入:

  1. target等于其中一个值,if(nums[mid] == target) return mid,显然正确;
  2. target比所有值都小,应该插到最左边:此时最后一次进循环是left = right = 0,执行if(nums[mid] > target) right = mid - 1; return left; 即返回0,确实插到了最左边;
  3. target比所有值都大,应该插到最右边:此时最后一次进循环是left = right =nums.size() - 1,执行if(nums[mid] < target) left = mid + 1; return left; 即返回right =nums.size(),确实插到了最右边;
  4. target夹在两个值之间,left仍能指向比target值大的最小数,也正确插入。

时间复杂度:O(log n)

空间复杂度:O(1)

704. 二分查找

题目

题目链接:704. 二分查找

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

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:
你可以假设 nums 中的所有元素是不重复的。
n 将在 [1, 10000]之间。
nums 的每个元素都将在 [-9999, 9999]之间。

解法

把上面那题的left改成-1就行了。

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0, right = nums.size() - 1;
        int mid;
        while(left <= right)
        {
            mid = left + (right - left) / 2; // 防止溢出,等效于(left + right)/2
            if(nums[mid] < target)
            {
                left = mid + 1; // 新的区间为[mid + 1, right]
            }
            else if(nums[mid] > target)
            {
                right = mid - 1; // 新的区间为[left, mid - 1]
            }
            else if(nums[mid] == target)
                return mid;
        }
        return -1;
    }
};

时间复杂度:O(log n)

空间复杂度:O(1)

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

题目

题目链接:34. 在排序数组中查找元素的第一个和最后一个位置

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

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

进阶:

你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗?

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

提示:
0 <= nums.length <= 10^5
-10^9 <= nums[i] <= 10^9
nums 是一个非递减数组
-10^9 <= target <= 10^9

解法

将查找左边界和查找右边界分开,使代码更清晰。

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        int leftBorder = getLeftBorder(nums, target);
        int rightBorder = getRightBorder(nums, target);
        // 情况一 target在数组的左边或右边
        if (leftBorder == -2 || rightBorder == -2) return {-1, -1};
        // 情况三 target在数组中
        if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1};
        // 情况二 target在数组范围中但不存在
        return {-1, -1};
    }
private:
     int getRightBorder(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] > target) {
                right = middle - 1;
            } else { // 寻找右边界,nums[middle] == target的时候更新left
                left = middle + 1;
                rightBorder = left;
            }
        }
        return rightBorder;
    }
    int getLeftBorder(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
                right = middle - 1;
                leftBorder = right;
            } else {
                left = middle + 1;
            }
        }
        return leftBorder;
    }
};

时间复杂度:O(log n)

空间复杂度:O(1)

此方法来自代码随想录

69. x 的平方根

题目

题目链接:69. x 的平方根

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

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

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

示例 1:

输入:x = 4
输出:2

示例 2:

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

提示:
0 <= x <= 2^31 - 1

解法

class Solution {
public:
    int mySqrt(int x) {
        int l = 0, r = x, ans = -1;
        while (l <= r) {
            int mid = l + (r - l) / 2;
            if ((long long)mid * mid <= x) { 
                // int和long表示范围约为正负20亿(正负10^10)
                // long long表示范围约为正负900亿亿(正负10^19)
                ans = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return ans;
    }
};

时间复杂度:O(log x)

空间复杂度:O(1)

其它解法

袖珍计算器算法

「袖珍计算器算法」是一种用指数函数exp 和对数函数ln 代替平方根函数的方法。

√x = x ^ 1/2 = (e ^ ln x) ^ 1/2 = e ^ (1/2 * ln x)

由于计算机无法存储浮点数的精确值,而指数函数和对数函数的参数和返回值均为浮点数,因此运算过程中会存在误差。例如当 x = 2147395600时,e ^ (1/2 * ln x)的计算结果与正确值 4634046340 相差 10^(-11),这样在对结果取整数部分时,会得到 4633946339 这个错误的结果。因此在得到结果的整数部分ans 后,我们应当找出ans 与ans+1 中哪一个是真正的答案。

class Solution {
public:
    int mySqrt(int x) {
        if (x == 0) {
            return 0;
        }
        int ans = exp(0.5 * log(x));
        return ((long long)(ans + 1) * (ans + 1) <= x ? ans + 1 : ans);
    }
};

时间复杂度:O(1),由于内置的log和exp函数一般都很快

空间复杂度:O(1)

:这个leetcode官方的解法有争议,因为题目中说:“不允许使用任何内置指数函数和算符”,而解法中出现了exp函数。

牛顿迭代

原理:用(x, f(x))处的切线来逼近方程x^2 - a = 0的根。f(x) = x^2 - a的导数为2x。即(x, f(x))处切线的斜率为2x,由下图:

可知x - f(x) / 2x是一个比x更接近的近似值。将f(x) = x^2 - a带入该式,可得近似值为x- (x^2 - a) / 2x,化简得近似值为(x + a / x) / 2。观察此式,近似值为x与a/x的平均数。

class Solution {
public:
    int mySqrt(int x) {
        if (x == 0) {
            return 0;
        }

        double a = x, x0 = x;
        while (true) {
            double xi = 0.5 * (x0 + a / x0);
            if (fabs(x0 - xi) < 1e-7) { // 误差 < 10^(-7)
                break;
            }
            x0 = xi;
        }
        return int(x0);
    }
};

时间复杂度:O(log x),比二分法更快

空间复杂度:O(1)

此方法来自牛顿迭代

367. 有效的完全平方数

题目

题目链接:367. 有效的完全平方数

给定一个 正整数 num ,编写一个函数,如果 num 是一个完全平方数,则返回 true ,否则返回 false 。

进阶:不要 使用任何内置的库函数,如 sqrt 。

示例 1:

输入:num = 16
输出:true

示例 2:

输入:num = 14
输出:false

提示:
1 <= num <= 2^31 - 1

解法

class Solution {
public:
    bool isPerfectSquare(int num) {
        long long target = num;
        long long left = 0, right = num, mid;
        while(left <= right)
        {
            mid = left + (right - left) / 2;
            if(mid * mid == target) // mid即为√num
                return true;
            else if(mid * mid < target)
                left = mid + 1;
            else if(mid * mid > target)
                right = mid - 1;
        }
        return false;
    }
};

时间复杂度:O(log num)

空间复杂度:O(1)

其它解法

牛顿迭代

class Solution {
public:
    bool isPerfectSquare(int num) {
        double x0 = num;
        while (true) {
            double x1 = (x0 + num / x0) / 2;
            if (x0 - x1 < 1e-6) {
                break;
            }
            x0 = x1;
        }
        int x = (int) x0;
        return x * x == num;
    }
};

时间复杂度:O(log num)

空间复杂度:O(1)

总结

  • 对n个有序数使用二分法查找的时间复杂度为log n
  • while循环中的条件判断时,一定要注意:left = mid + 1; right = mid - 1,不要把加减号弄错了,否则while会无限循环。
商业转载请联系博主获得授权,非商业转载请注明出处!

分享结束,大家辛苦了。散会!

-->