二分查找题目汇总

二分查找的提示

  • 数组有序、或部分有序
  • 查找指定元素、或查找某个满足条件的元素
  • 时间复杂度要求 O ( l o g n ) O(logn) O(logn)

69. x 的平方根

class Solution {
public:
    int mySqrt(int x) {
        // 方法:二分查找

        // /* 我的迭代实现V1(不work)
        // 思路:二分查找的变体问题,在有序数组中找最后一个mid * mid <= x的mid
        // 问题:平方会上溢;
        // */
        // int low = 0, high = x; // high这里不能是x/2,得是x,不然1会无法通过
        // while(low <= high)
        // {
        //     int mid = low + (high - low) / 2; // C++里整除是/而不是//,除以2这样写不会出错
        //     if(mid * mid <= x) // runtime error: signed integer overflow: 1073697799 * 1073697799 cannot be represented in type 'int'
        //     {
        //         if((mid == high) || (mid + 1) * (mid + 1) > x) return mid;
        //         low = mid + 1;
        //     }
        //     else
        //     {
        //         high = mid - 1;
        //     }
        // }
        // return -1;

        /* 我的迭代实现V2(推荐写法1,不容易写错)
        思路:二分查找的变体问题,在有序数组中找最后一个mid <= x/mid 的mid
        Time:O(logn),执行用时:4 ms, 在所有 C++ 提交中击败了48.33%的用户
        Space:O(1),内存消耗:5.8 MB, 在所有 C++ 提交中击败了64.62%的用户
        */
        if(x == 0) return 0;

        int low = 1, high = x; // high这里不能是x/2,得是x,不然1会无法通过
        while(low <= high)
        {
            int mid = low + (high - low) / 2; // 除以2这样写不会出错
            if(mid <= x / mid)
            {
                if(mid == high || (mid + 1) > x / (mid + 1)) return mid; // mid是最后一个值,或者(mid+1) > x/(mid+1)了
                low = mid + 1;
            }
            else
            {
                high = mid - 1;
            }
        }
        return -1;


        /* leetcode101标准答案(推荐写法2,更快一些):
            1. 将求平方,转换为求整除
            2. 通过整除将二分查找的变体问题,转换为标准的二分查找问题:找的是mid == x/mid的mid;但若无解,返回的不是-1,而是较小值(退出循环后,较小值是high)!!!
            3. 考虑边界条件,把0单独处理,避免除以0
        */

        if(x == 0) return 0;

        int low = 1, high = x;
        while(low <= high)
        {
            int mid = low + (high - low) / 2;
            int target = x / mid;
            if(mid == target) return mid;
            else if(mid > target) high = mid - 1; // 注意这里是>,就单纯把x/mid当作一个目标值target,不要考虑target里的mid
            else low = mid + 1;
        }
        return high; // 不是-1,因为这道题永远有解


        /* 牛顿迭代法
        牛顿迭代法是找f(x)=0的近似最优解x的方法,方法是迭代求解x(n+1) = x(n) - f(x(n))/f'(x(n));
        这道题对应的f(x) = x^2 - C,递推公式x(n+1) = (x(n) + C/x(n)) / 2。
        Time: O(logn)
        Space: O(1)
        牛顿迭代法的详细介绍见本题官方题解:https://leetcode.cn/problems/sqrtx/solution/x-de-ping-fang-gen-by-leetcode-solution/
        */
        if(x == 0) return 0;

        long res = x; // 看详细介绍,初始化为C
        // unsigned int表示范围是[0, 2^32-1]
        // int表示范围是[-2^31, 2^31-1]
        // 因为下面有res + xxx的操作,为了防止res超出int的表示范围,所以得定义为long类型

        while(res > x / res)
        {
            res = (res + x / res) / 2; // 看详细介绍,是x(n+1)是在x(n)的左边
        }

        return res;
    }
};

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

class Solution {
public:

    /* 我的实现V1,不work,问题:把找left和找right写在一起、没有办法return中断while,导致nums={1} target=1 时,改不了low和high,跳不出循环 */
    // vector<int> searchRange(vector<int>& nums, int target) {
    //     // 方法:二分查找

    //     /* 我的实现: 
    //         1. 二分查找的变体问题:在有序数组中找出第一个和最后一个等于指定值的位置
    //         2. 要留意的错误:low、high是下标,比较大小时是nums[low]和nums[high]
    //     */ 
    //     int low = 0, high = nums.size() - 1, mid;
    //     int left = -1, right = -1;

    //     while(low <= high)
    //     {
    //         mid = low + (high - low) / 2;
    //         if(nums[mid] == target)
    //         {
    //             if(mid == 0 || nums[mid - 1] < target) left = mid;
    //             else high = mid - 1; // 从左数不是第一个

    //             if(mid == nums.size() - 1 || nums[mid + 1] > target) right = mid;
    //             else low = mid + 1; // 从右数不是第一个
    //         }
    //         else if(nums[mid] > target)
    //         {
    //             high = mid - 1;
    //         }
    //         else
    //         {
    //             low = mid + 1;
    //         }
    //     }
    //     return vector<int> {left, right};
    // }



    int searchRangeLeft(vector<int>& nums, int target) 
    {
        int low = 0, high = nums.size() - 1, mid, left = -1;
        while(low <= high)
        {
            mid = low + (high - low) / 2;
            if(nums[mid] == target)
            {
                if(mid == 0 || nums[mid - 1] < target)
                {
                    left = mid;
                    return left;
                }
                else high = mid - 1; // 从左数不是第一个
            }
            else if(nums[mid] > target)
            {
                high = mid - 1;
            }
            else
            {
                low = mid + 1;
            }
        }
        return -1;
    }

    int searchRangeRight(vector<int>& nums, int target) {
        int low = 0, high = nums.size() - 1, mid, right = -1;
        while(low <= high)
        {
            mid = low + (high - low) / 2;
            if(nums[mid] == target)
            {
                if(mid == nums.size() - 1 || nums[mid + 1] > target)
                {
                    right = mid;
                    return right;
                }
                else low = mid + 1; // 从右数不是第一个
            }
            else if(nums[mid] > target)
            {
                high = mid - 1;
            }
            else
            {
                low = mid + 1;
            }
        }
        return -1;
    }


    vector<int> searchRange(vector<int>& nums, int target) {
        // 方法:二分查找

        /* 我的实现: 
            1. 二分查找的变体问题:在有序数组中找出第一个和最后一个等于指定值的位置
            2. 要留意的错误:low、high是下标,比较大小时是nums[low]和nums[high]

            Time: O(logn)
            Space: O(1)
        */ 
        
        int left = searchRangeLeft(nums, target);
        int right = searchRangeRight(nums, target);

        return vector<int> {left, right};
    }
};

33. 搜索旋转排序数组(数组中无重复元素)

class Solution {
public:
    int search(vector<int>& nums, int target) {
        // 二分查找

        /* 旋转数组
        1. 旋转数组的特性:
            - 以数组中间点为分区,会将数组分成一个有序数组和一个旋转数组
            - 如果nums[mid]>=nums[left],说明前半部分是有序的,后半部分是旋转数组;如果nums[mid]<nums[left],说明后半部分是有序的,前半部分是旋转数组
            - 判断目标元素是否有序数组的两端范围内,在的话收缩边界到有序数组,不在的话收缩边界到另一半旋转数组范围内
        */

        // 第二遍写
        if(nums.size() == 0) return -1;
        int l = 0, r = nums.size() - 1, mid;
        while(l <= r)
        {
            mid = l + (r - l) / 2;
            if(nums[mid] == target) return mid;
            if(nums[mid] >= nums[l]) // 等号在判断左侧有序这边,因为整除的原因mid有可能=0
            {
                if(nums[l] <= target && target < nums[mid]) // 在有序范围内
                {
                    r = mid - 1;
                }
                else
                {
                    l = mid + 1;
                }
            }
            else // 右侧有序
            {
                if(nums[mid] < target && target <= nums[r]) // 在有序范围内
                {
                    l = mid + 1;
                }
                else // 不在有序范围内
                {
                    r = mid - 1;
                }
            }
        }

        return -1;



        // // 第一遍写
        // if(nums.size() == 0) return -1;

        // int left = 0, right = nums.size() - 1, mid;
        // while(left <= right)
        // {
        //     mid = left + (right - left) / 2;
            
        //     if(nums[mid] == target) return mid;

        //     // 判断左半有序还是右半有序,如果nums[0] <= nums[mid],则左半有序、右半旋转
        //     if(nums[0] <= nums[mid]) // 左半有序,虽然无重复元素也得是<=,不能是<,因为有可能mid = 0
        //     {
        //         // 确认target是否在有序区间的数值范围内
        //         if(nums[left] <= target && target < nums[mid]) // 注意是<=,不是<;nums[left]也可以换nums[0]
        //         {
        //             // 可能在有序区间
        //             right = mid - 1;
        //         }
        //         else
        //         {
        //             // 可能在无序区间
        //             left = mid + 1;
        //         }
        //     }
        //     else // 右半有序
        //     {
        //         // 确认target是否在有序区间的数值范围内
        //         if(nums[mid] < target && target <= nums[right]) // // 注意是>=,不是>;nums[right]也可以换nums[nums.size()-1]
        //         {
        //             // 可能在有序区间
        //             left = mid + 1;
        //         }
        //         else
        //         {
        //             // 可能在无序区间
        //             right = mid - 1;
        //         }
        //     }

        // }

        // return -1;
    }
};

81. 搜索旋转排序数组 II(数组中有重复元素)

class Solution {
public:
    bool search(vector<int>& nums, int target) {
        // 二分查找

        /* 旋转数组
        1. 无重复元素的旋转数组查找元素见leetcode 33
        2. 有重复元素的旋转数组查找元素,不同的是:nums[mid] >= nums[left]无法保证左侧有序,这时需要将left++
        */
        
        if(nums.size() == 0) return false;

        int left = 0, right = nums.size() - 1, mid;
        while(left <= right)
        {
            mid = left + (right - left) / 2;
            if(nums[mid] == target) return true;
            if(nums[mid] == nums[left]) ++left; // 旋转数组中存在重复元素时,无法判断左侧有序还是右侧有序,将左端点右移一位再来判断
            else if(nums[mid] > nums[left]) // 左侧有序
            {
                if(nums[left] <= target && target < nums[mid])
                {
                    right = mid - 1;
                }
                else
                {
                    left = mid + 1;
                }
            }
            else // 右侧有序
            {
                if(nums[mid] < target && target <= nums[right])
                {
                    left = mid + 1;
                }
                else
                {
                    right = mid - 1;
                }
            }
        }

        return false;
    }
};

153. 寻找旋转排序数组中的最小值(无重复元素)

class Solution {
public:
    int findMin(vector<int>& nums) {
        // 二分查找

        /* 无重复元素的旋转数组查找最小值
        1. 如何确定哪半是排序数组、哪半是旋转数组,方法同leetcode 33
        2. 如何找最小值:先将最小值定为有序区间的左端点,再继续搜旋转数组区间看有没有更小的
        */

        int l = 0, r = nums.size() - 1, mid, res = 5001;

        while(l <= r)
        {
            mid = l + (r - l) / 2;
            if(nums[mid] >= nums[l]) // 左侧有序
            {
                res = min(res, nums[l]); // 把最小值设为有序区间的最小值
                // 继续搜旋转区间
                l = mid + 1;
            }
            else // 右侧有序
            {
                res = min(res, nums[mid]); // 把最小值设为有序区间的最小值
                // 继续搜旋转区间
                r = mid - 1;
            }
        }

        return res;
    }
};

154. 寻找旋转排序数组中的最小值 II(有重复元素)

class Solution {
public:
    int findMin(vector<int>& nums) {
        // 二分查找

        /* 有重复元素的旋转数组查找最小值
        注意:要给res赋初始值、nums[mid] == nums[l]时也要给res赋值
        */
        int l = 0, r = nums.size() - 1, mid, res = 5001; // res要预先给一个较大值

        while(l <= r)
        {
            mid = l + (r - l) / 2;
            if(nums[mid] == nums[l])
            {
                res = min(res, nums[l]); // 这种情况下也要给res赋值!!!不然输入[1]时输出5001
                ++l;
            }
            else if(nums[mid] > nums[l]) // 左侧是有序数组
            {
                res = min(res, nums[l]);
                l = mid + 1;
            }
            else // 右侧是有序数组
            {
                res = min(res, nums[mid]);
                r = mid - 1;
            }
        }

        return res;
    }
};

540. 有序数组中的单一元素

class Solution {
public:
    int singleNonDuplicate(vector<int>& nums) {
        // 二分查找

        // // 第一次自己实现,不work,会逐渐细分条件,其实是没想清楚
        // if(nums.size() == 1) return nums[0]; // 边界条件

        // int l = 0, r = nums.size() - 1, mid;
        // while(l <= r)
        // {
        //     mid = l + (r - l) / 2;
        //     if (mid % 2 == 0 && (mid == 0 || nums[mid] == nums[mid - 1])) r = mid - 2; // 在左侧
        //     else if (mid % 2 == 1 && (mid == nums.size() - 1 || nums[mid] == nums[mid + 1])) r = mid - 1; // 在左侧
        //     else if (mid % 2 == 1 && (mid == 0 || nums[mid] == nums[mid - 1])) l = mid + 1; // 在右侧
        //     else l = mid + 2;
        // }
        // return -1;


        /*
            如果每个数字都出现两次,就一定会满足对于所有的(偶数下标,奇数下标)这样的小单元,值都相等。
            二分查找确定区间的方法是:对于mid判断(偶数下标,奇数下标)这样的小单元的值是否相等,如果相等,说明单元素不会在左半段,只会在右半段;如果不相等,说明单元素在左半段
        */

        int l = 0, r = nums.size() - 1, mid; // 注意在二分查找时,r是要-1的!!!
        while(l < r) // r和l缩到同一个位置上的时候,就是那个单元素了
        {
            mid = l + (r - l) / 2;
            if(mid % 2 == 0) // mid是偶数
            {
                if(nums[mid] == nums[mid + 1]) // 单元素在右侧
                {
                    l = mid + 2; // 跳过双元素的部分,依旧保持所搜索数组满足只有一个单元素的特性
                }
                else // 单元素在左侧
                {
                    r = mid;
                }
            }
            else // mid是奇数
            {
                if(nums[mid - 1] == nums[mid]) // 单元素在右侧
                {
                    l = mid + 1;
                }
                else // 单元素在左侧
                {
                    r = mid;
                }
            }
        }
        return nums[r]; // l和r都可

    }
};

4. 寻找两个正序数组的中位数

class Solution {
public:
    double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
        // 二分查找 官方答案 看懂阶段

        // 选较短的数组来分割,另一个数组的分割由此决定
        if (nums1.size() > nums2.size()) {
            return findMedianSortedArrays(nums2, nums1);
        }

        int m = nums1.size();
        int n = nums2.size();
        int left = 0, right = m;
        // median1:前一部分的最大值
        // median2:后一部分的最小值
        int median1 = 0, median2 = 0;

        while (left <= right) {
            // 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
            // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
            int i = (left + right) / 2;
            int j = (m + n + 1) / 2 - i; // 两个数组的中位数是两个数组从小到大排序后的第k = (m + n + 1) / 2 个数的位置上,这对奇偶长度的都成立???

            // nums_im1, nums_i, nums_jm1, nums_j 分别表示nums1左段最大值nums1[i-1], nums1右段最小值nums1[i], nums2左段最大值nums2[j-1], nums2右段最小值nums2[j]
            int nums_im1 = (i == 0 ? INT_MIN : nums1[i - 1]); 
            int nums_i = (i == m ? INT_MAX : nums1[i]);
            int nums_jm1 = (j == 0 ? INT_MIN : nums2[j - 1]);
            int nums_j = (j == n ? INT_MAX : nums2[j]);

            /* 找到中位数的条件是:nums_im1 <= nums_j && nums_jm1 <= nums_i
               所以:
                   1) 如果nums_im1 <= nums_j,可以对于nums1继续向右找一下
                   2) 如果nums_im1 > nums_j,可以对于nums继续向左找一下
               直到left > right
            */
            if (nums_im1 <= nums_j) {
                median1 = max(nums_im1, nums_jm1);
                median2 = min(nums_i, nums_j);
                left = i + 1;
            } else {
                right = i - 1;
            }
        }

        // 中位数是什么要看nums长度的奇偶:如果是奇数,median就是左侧最大;如果是偶数,就是左侧最大和右侧最小的平均?
        return (m + n) % 2 == 0 ? (median1 + median2) / 2.0 : median1;
    }
};

剑指 Offer 04. 二维数组中的查找

class Solution {
public:
    bool findNumberIn2DArray(vector<vector<int>>& matrix, int target) {
        /* 二分查找(的思想)
            1. 先找到中间的数,通过跟target比对,排除一部分不可能的解,缩小查找范围。然而在本题中,如果同时找行、列中间位置,是不好排除的。所以要思考中间位置是什么,能排除的是什么
            2. 可以看出,矩阵的左下角or右上角元素是中间元素,通过与target比对,可以排除一行或一列。例如对于左下角元素x:
            (1)如果x==target, return true
            (2)如果x<target, 列+1
            (3)如果x>target, 行-1
        */
        if(matrix.empty() || matrix[0].empty()) return false;

        int i = matrix.size() - 1, j = 0; // 从左下角元素开始找
        while(i >= 0 && j < matrix[0].size())
        {
            if(matrix[i][j] == target) return true;
            if(matrix[i][j] < target) ++j;
            else --i;
        }
        return false;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值