二分查找

参考链接:https://www.bilibili.com/video/BV1Ft41157zW

 

 

一,算法思想

  假设我们要找的值在区间 [l, r] 中,我们根据区间的中间位置的值 a[m] 与目标值的关系,更新 l 和 r 的取值,从而对区间 [l, r] 进行二分来缩小区间。而当区间缩小到足够小时,即可确定目标值。

 

 

二,代码(以查询大于等于k且下标最小的数字的下标为例)

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#define N 100
int a[N], k;
int bsearch(int l, int r) // 循环
{
    while (l + 1 < r)
    {
        int m = l + (r - l) / 2;
        if (a[m] < k)
            l = m;
        else
            r = m;
    }
    return r;
}
int find(int l, int r) // 递归
{
    if (l + 1 >= r)
        return r;

    int m = l + (r - l) / 2;
    if (a[m] < k)
        return find(m, r);
    else
        return find(l, m);
}
int main(void)
{
    int n;
    while (scanf("%d%d", &n, &k) != EOF)
    {
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        printf("%d\n", bsearch(-1, n));
        printf("%d\n", find(-1, n));
    }
    system("pause");
    return 0;
}
/*
7 1
1 2 3 4 5 6 7
7 7
1 2 3 4 5 6 7
7 4
1 2 3 4 5 6 7
7 4
1 2 4 4 4 6 7
7 4
1 2 3 5 6 7 8
7 0
1 2 3 4 5 6 7
7 8
1 2 3 4 5 6 7

*/
View Code

 

 

三,代码解析(以查询大于等于k且下标最小的数字的下标为例)

1, l 与 r 的指向

  ① 算法进行时

    l:小于 k 的值的坐标

    r:大于等于 k 的值的下标

   ② 算法结束时

    l:小于 k 且下标最大的值的下标

    r:大于等于 k 且下标最小的值的下标

 

2,为什么算法结束后, l 与 r 的指向会是这样?

  因为

    ① 算法进行时,l 必须满足小于 k 的坐标;r 必须满足大于等于 k 的值的坐标。

    ② 算法终止的条件是 l + 1 >= r,即 l 与 r 位置刚好相邻。

  所以,只有一种情况,算法才有可能终止:

    l:小于 k 且下标最大的值的下标;r:大于等于 k 且下标最小的值的下标。此时,l 和 r 相邻

 

3,注意点

  ① 数组中未必有 k

  ② 注意边界

      当数组为 [1, 3, 5, 6],目标值为 0 时,预期的答案是 0,但如果 l 的初值为 0 的话,最终的答案只能为 1。

    因为 l 无法再小了,所以 r 最小也之能到 1。

      当数组为 [1, 3, 5, 6],目标值为 7 时,预期的答案是 4,但如果 r 的初值为 3 的话,最终的答案只能为 1。

    因为 r 无法再大了,所以 r 最大也之能到 3。

    而且,但 l 和 r 的初始值为 -1 和 n 时,该下标对应的值 (虽然没有对应的值) 是不会被动用的

      因为,但 l == -1 时,且要找的值下溢出,此时动的只有 r,且当 r 移动到 0 时,算法结束,没有动用过 -1 对应的值。

      因为,但 r == n 时,且要找的值上溢出,此时动的只有 l,且当 l 移动到 n-1 时,算法结束,没有动用过 n 对应的值。

  ③ 区间 <==> 结束条件

    如果区间决定了,则结束条件也相应的确定了。反之亦然。(见例题⑤)     

 

4,递归说明

  在搜索阶段,通过判断区间的中间位置的值 a[m] 与目标值的关系,更新 l 和 r 的取值,从而对区间 [l, r] 进行二分来缩小区间。

  在回溯阶段,不断将搜索的结果 return 下去。

 

 

四,百花

  根据三种结束条件,二分法可以有三种写法。(其中,l + 1 < r 最简单)

    ① l + 1 < r;② l < r;③ l <= r; 

  根据实现的方法不同,每种二分法可以有两种写法

    ① 循环;② 递归

  根据 l 和 r 的判断条件,每种二分可以用来求解两种问题

    ① 求小于等于 k 且下标最大的数字的下标;

    ② 求大于等于 k 且下标最小的数字的下标;

下面给出求解大于等于 k 且下标最小的数字的下标的问题的代码,另外一个问题可以自己尝试着自己推导一下。

① l + 1 < r && 求大于等于 k 且下标最小的数字的下标 && 包含循环和递归的写法

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#define N 100
int a[N], k;
int bsearch(int l, int r) // 循环
{
    while (l + 1 < r)
    {
        int m = l + (r - l) / 2;
        if (a[m] < k)
            l = m;
        else
            r = m;
    }
    return r;
}
int find(int l, int r) // 递归
{
    if (l + 1 >= r)
        return r;

    int m = l + (r - l) / 2;
    if (a[m] < k)
        return find(m, r);
    else
        return find(l, m);
}
int main(void)
{
    int n;
    while (scanf("%d%d", &n, &k) != EOF)
    {
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        printf("%d\n", bsearch(-1, n));
        printf("%d\n", find(-1, n));
    }
    system("pause");
    return 0;
}
/*
7 1
1 2 3 4 5 6 7
7 7
1 2 3 4 5 6 7
7 4
1 2 3 4 5 6 7
7 4
1 2 4 4 4 6 7
7 4
1 2 3 5 6 7 8
7 0
1 2 3 4 5 6 7
7 8
1 2 3 4 5 6 7

*/
View Code

② l < r && 求大于等于 k 且下标最小的数字的下标 && 包含循环和递归的写法

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 100
int a[N], k;
int find(int l, int r) // 递归
{
    if (l >= r)
        return r;

    int m = l + (r - l) / 2;
    if (a[m] < k)
        return find(m + 1, r);
    else
        return find(l, m);
}
int bsearch(int l, int r) // 循环
{
    while (l < r)
    {
        int m = l + (r - l) / 2;
        if (a[m] < k)
            l = m + 1;
        else
            r = m;
    }
    return r;
}
int main(void)
{
    int n;
    while (scanf("%d%d", &n, &k) != EOF)
    {
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        printf("%d\n", bsearch(0, n));
        printf("%d\n", find(0, n));
    }
    system("pause");
    return 0;
}
/*
7 1
1 2 3 4 5 6 7
7 7
1 2 3 4 5 6 7
7 4
1 2 3 4 5 6 7
7 4
1 2 4 4 4 6 7
7 4
1 2 3 5 6 7 8
7 0
1 2 3 4 5 6 7
7 8
1 2 3 4 5 6 7

*/
View Code

③ l <= r && 求大于等于 k 且下标最小的数字的下标 && 包含循环和递归的写法

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 100
int a[N], k;
int find(int l, int r) // 递归
{
    if (l > r)
        return l;

    int m = l + (r - l) / 2;
    if (a[m] < k)
        return find(m + 1, r);
    else
        return find(l, m - 1);
}
int bsearch(int l, int r) // 循环
{
    while (l <= r)
    {
        int m = l + (r - l) / 2;
        if (a[m] < k)
            l = m + 1;
        else
            r = m - 1;
    }
    return l;
}
int main(void)
{
    int n;
    while (scanf("%d%d", &n, &k) != EOF)
    {
        for (int i = 0; i < n; i++)
            scanf("%d", &a[i]);
        printf("%d\n", bsearch(0, n - 1));
        printf("%d\n", find(0, n - 1));
    }
    system("pause");
    return 0;
}
/*
7 1
1 2 3 4 5 6 7
7 7
1 2 3 4 5 6 7
7 4
1 2 3 4 5 6 7
7 4
1 2 4 4 4 6 7
7 4
1 2 3 5 6 7 8
7 0
1 2 3 4 5 6 7
7 8
1 2 3 4 5 6 7

*/
View Code

 

 

五,例题

① 链接:https://leetcode-cn.com/problems/search-insert-position/

求解思路:

  求插入的位置就是要求 > k 的值的下标;求目标值就是求 == k 的值的下标。综上就是求大于等于 k 且下标最小的数字的下标。

class Solution {
public:
    int k;
    int find(vector<int>& a, int l, int r) // 递归
    {
        if (l+1 >= r)
            return r;

        int m = l + (r - l) / 2;
        if (a[m] < k)
            return find(a, m, r);
        else
            return find(a, l, m);
    }
    int bsearch(vector<int>& a, int l, int r) // 循环
    {
        while (l + 1 < r)
        {
            int m = l + (r - l) / 2;
            if (a[m] < k)
                l = m;
            else
                r = m;
        }
        return r;
    }

    int searchInsert(vector<int>& nums, int target) {
        k = target;
        return find(nums, -1, nums.size());
        return bsearch(nums, -1, nums.size());
    }
};
View Code

 

 

② 链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/

求解思路:

  主要是没有找到的判断条件:

    数组为空 return -1;

    找到的值超出边界,return -1;(要是 u 或者 d == -1 最后就返回 -1 了,所以这里可以不用判断)

    找到的值不等于边界值,return -1;

class Solution {
public:
    int k;
    int findU(vector<int>& a, int l, int r)
    {
        if (l + 1 >= r)
            return r;

        int m = l + (r - l) / 2;
        if (a[m] < k)
            return findU(a, m, r);
        else
            return findU(a, l, m);
    }
    int findD(vector<int>& a, int l, int r)
    {
        if (l + 1 >= r)
            return l;

        int m = l + (r - l) / 2;
        if (a[m] <= k)
            return findD(a, m, r);
        else
            return findD(a, l, m);
    }

    vector<int> searchRange(vector<int>& nums, int target) {
        if(nums.empty())
            return {-1, -1};

        k = target;
        int u = findU(nums, -1, nums.size());
        int d = findD(nums, -1, nums.size());
        if(u == nums.size() || d == nums.size())
            return {-1, -1};
        if(nums[u] != target || nums[d] != target)
            return {-1, -1};
        return {u, d};
    }
};
View Code

 

 

③ 链接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/

求解思路:

  以最小值为分界线,左边的值大于 nums.back(),右边的值小于 nums.back(),最小值在 >= nums.back() 且下标最小的数字的下标。

  所以,可以设 k 为 nums.back(),令 l 指向大于 nums.back() 的值的下标,令 r 指向小于等于 nums.back() 的值的下标。

  于是,算法结束后 r 就指向最小值的下标。

class Solution {
public:
    int find(vector<int>& nums, int l, int r)
    {
        if(l + 1 >= r)
            return r;
        int m = l + (r - l) / 2;
        if(nums[m] > nums.back())
            return find(nums, m, r);
        else
            return find(nums, l, m);
    }
    int findMin(vector<int>& nums) {
        return nums[find(nums, -1, nums.size())];
    }
};
View Code

 

 

④ 链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array/

求解思路:

  在上一题的基础上,先求出最小值的下标,在进行目标值与 nums.back() 的大小比较。

  若大于 nums.back(),则对区间 [0, m-1] 进行二分;

  若小于 nums.back(),则对区间 [m, n-1] 进行二分。

class Solution {
public:

    int findM(vector<int>& nums, int l, int r)
    {
        if(l + 1 >= r)
            return r;
        int m = l + (r - l) / 2;
        if(nums[m] > nums.back())
            return findM(nums, m, r);
        else
            return findM(nums, l, m);
    }

    int k;
    int find(vector<int>& nums, int l, int r)
    {
        if(l + 1 >= r)
            return r;

        int m = l + (r - l) / 2;
        if(nums[m] < k)
            return find(nums, m, r);
        else
            return find(nums, l, m);
    }

    int search(vector<int>& nums, int target) {
        if(nums.empty())
            return -1;

        k = target;
        int m = findM(nums, -1, nums.size());

        int l, r;
        if(target <= nums.back())
            l = m - 1, r = nums.size();
        else
            l = -1, r = m;
        int x = find(nums, l, r);
        if(nums.size() == x || nums[x] != target)
            return -1;
        return x;
    }
};
View Code

 

 

⑤ 链接:https://leetcode-cn.com/problems/find-peak-element/

求解思路:

  ① 若 a[m] < a[m+1],则说明 m+1 ~ n-1 的区间必存在峰值。

  ② 若 a[m] > a[m+1],则说明 0 ~ m  的区间必存在峰值。

  证 ①:

    若 m+2 ~ n-1 的值皆小于 a[m+1],则 a[m+1] 为峰值;

    若 m+2 ~ n-1 的值皆大于 a[m+1],则分为两种情况:

      若  m+2 ~ n-1 的值是单调递增的,则 a[n-1] 为峰值;

      若  m+2 ~ n-1 的值是不具有单调性的,则其中某个值必为峰值;

    综上,① 得证。

   注意点:

    这一题的结束条件不能用 l + 1 >= r,因为:

      定性分析:因为当最后 l 和 r 指向最后剩下的两个时,并不一定是 l 或者是 r 的指向是峰值,它们的较大值才是峰值。所以 l 和 r 必须指向同一个值才能判定哪个值是峰值,即结束条件只能是 l >= r

      定量分析:因为判断条件后执行的语句只能是 l = m + 1 或者 r = m,所以结束条件只能是 l >= r

    所以说,区间决定结束条件。

class Solution {
public:
    int find(vector<int>& nums, int l, int r)
    {
        if (l >= r)
            return r;
        int m = l + (r - l) / 2;
        if (nums[m] < nums[m + 1])
            return find(nums, m + 1, r);
        else
            return find(nums, l, m);
    }
    int findPeakElement(vector<int>& nums) {
        return find(nums, 0, nums.size() - 1);
    }
};
View Code

 

 

⑥ 链接:https://leetcode-cn.com/problems/h-index-ii/

求解思路:

  问题概述:设 h 满足:在升序序列中 h 个数大于 h,求 h 的最大值。

  因为需要在数组取 h 个数,所以 h ~ [0, n],

  所以,可以用二分在 [0, n] 内查找:

    设 m 为 h

      如果 nums[ nums.size() - m ] >= m,则说明 nums.size() - m ~ nums.size() - 1 之间 m 个都是大于等于 m,

        所以此时 h 一定在区间 [m, n ] 之间;

      如果 nums[ nums.size() - m ] < m,则说明大于等于 m 的个数小于 m,

        所以此时 h 一定在区间 [0, m-1 ] 之间;

   注意点:

      ① 还是区间决定结束条件,所以结束条件还是只能用 l >= r

      ② 因为 h ~ [0, n],所以取中间值要用 l + r + 1 >> 1

class Solution {
public:
    int find(vector<int>& nums, int l, int r)
    {
        if(l>=r)
            return r;
        int m = r + l + 1 >> 1;
        if(nums[nums.size()-m]>=m)
            return find(nums, m,r);
        else
            return find(nums, l,m-1);
    }
    
    int hIndex(vector<int>& citations) {
        return find(citations, 0, citations.size());
    }
};
View Code

 

 

========== ========== ======== ======= ====== ====== ==== === == =

     问刘十九  白居易(唐)

绿蚁新醅酒,红泥小火炉。

晚来天欲雪,能饮一杯无?

 

posted @ 2020-01-03 10:51  叫我妖道  阅读(219)  评论(0编辑  收藏  举报
~~加载中~~