你真的理解二分的写法吗 - 二分写法详解

标签: 二分
1043人阅读 评论(0) 收藏 举报
分类:

说实话,我之前也不完全理解二分查找的各种写法,导致在写各种二分的边界时我总是弄不清边界值,于是我只能通过暴力枚举这些边界值,去一个一个试,这样子效率真的很低下。于是,痛定思痛,一定要把二分的写法吃透,就有了这篇文章。

二分写法的种类

二分写法的种类很多,最常见的就是二分查找了的最普遍写法了。代码如下:

bool bFind(int a[], int left, int right, int tag)
{
    for (; left <= right; ) {
        int mid = (left + right) >> 1;
        if (a[mid] == tag)
            return true;
        else
            a[mid] < tag ? l = mid + 1 : r = mid - 1;
    }
    return false;
}

正如我刚才说的,这只是最普通的二分,实际上,二分还可以实现非等值查找,例如平时所说的二分求上界,二分求下界之类的。

下面我用乘法原理来告诉你们二分查找有多少种写法。

  • 区间的开闭 - [开区间还是闭区间]
  • 左右端点 - [这个端点是和上面区间开闭联系的,具体表现为左开还是左闭,右开还是右闭]
  • 中点是取整还是进一 - [在计算中点的时候到底是(left + right) >> 1还是(left + right + 1) >> 1]
  • 大于还是小于 - [这个对应上下界问题]
  • 取不取等于 - [是大于等于还是小于等于]
  • 第一个还是最后一个 - [找第一个大于目标的位置还是找最后一个大于目标的位置]

每个选项都是两种可能,于是二分写法一共有26=64种写法。也就是说,从这六个选项中的每个选项中任意挑选一个就可以组成一个二分的问题。

那么这么多种类的二分,是不是每种二分都要去记呢?肯定不要啊,不然我还写这博客干嘛。接下来我会告诉你一个通用的方法。

先看一个题目吧,有题目才有切入点。

题目背景

统计一个数字在排序数组中出现的次数.

题目解析

做法参见剑指Offer66题之每日6题 - 第七天中的第一题。

代码

class Solution {
public:
    int GetNumberOfK(vector<int> data ,int k) {
        int pre, last, l, r;
    // 下界
        for (l = 0, r = data.size() ;l < r; ) {
            int mid = (l + r) / 2;
            if (data[mid] >= k)
                r = mid;
            else
                l = mid + 1;
        }
        pre = r;

    // 上界
        for (l = -1, r = (int)data.size() - 1; l < r; ) {
            int mid = (l + r + 1) / 2;
            if (data[mid] <= k)
                l = mid;
            else
                r = mid - 1;
        }
        last = l;
        return last - pre + 1;
    }
};

详解

这个题目里,要用一个二分去求下界,一个二分求上界。

求下界

下界的定义是什么,就是找到一个数,这个数是第一个大于等于k的数,这个数的位置就是下界;

注意到二分的边界我设成了l = 0, r = data.size(),这是一个左闭右开的区间,中点处我用的是mid = (l + r) / 2,二分结束后,l等于r

下界的位置靠近左端点,所以我们从左端点开始找,因此,可以看到,二分中l的位置在一步一步向右端点靠近(因此要加一),而r只是起到缩小范围的作用;

右端点是个开端点,这是为了处理有序数组中没有一个数字比k大的情况,因此,如果查找失败,lr可以指向一个空的位置,也就是数组的最后一个位置的后一个位置,这个程序设计中的“左闭右开”区间的思想是一样的;

至于中点处为什么要向下取整,原因是这样的:如果这个题要你顺序查找这个有序数组找到下界,你会从哪里开始找?肯定是左边第一个元素开始找啊,你总不可能从第二个元素开始找吧,这就是为什么要向下取整的原因,向下取整可以避免漏掉最优解;

如果你还不明白,那么我举个例子你就知道了,数组为[-2], k = 3l = 0, r = 1,然后你二分的时候中点向上进一,那么mid = (0 + 1 + 1) / 2 = 1,然后你会发现mid不在数组中,怎么可能啊,mid是区间的中点,那必然会在数组中啊!这就是求下界的时候为什么中间要向下取整了;

还有一点要始终记得,二分结束后,l = r,因此最后l, r都是答案。

求上界

刚刚讲了求下界,求上界也如法炮制。

求上界求的是最后一个大于等于k的数字的位置,我们把数组反过来,以右端点作为起点开始查找,这个时候第一个小于或等于k的元素的位置就是原问题的上界;

你把求上界转化成求下界来看,是不是代码中的一切东西都理所当然了;

r = data.size() - 1, l = -1,因为把数组反过来的,故右端点就是起点,结束的位置就是第一个元素前面的一个位置;

mid = (l + r + 1) / 2,因为把数组反过来了,因此这里的向上进一其实就是反过来之后的向下取整。

总结

可以看到,求上界可以转化为求下界来做,因此,这里详细总结一下求下界的做法。

求下界第一件事就是确定左右端点范围,由于求下界是求第一个满足condition条件的位置,这里用condition是为了把这个求下界的方法一般化,求第一个满足条件的位置,因此,以左端点为起点;

最后一个元素的后一个位置作为终点,这是为了在没有满足条件的解可以得到一个合理的值(指向最后一个元素的后一个位置就是代表着没有找到下界);

中点取靠近起点的一端,根据靠近的位置选择向下取整还是向上进一;

在缩小范围的时候,如果mid满足条件,那么令r = mid, 这样子可以缩小范围([mid + 1, r)是一定不满足条件的,但mid有可能是答案,没关系,我们让l来等于mid就行,r只管缩小范围),而且,这样可以保证一定有解;

如果mid不满足条件,就令l = mid + 1, 由于[l, mid]是一定不满足条件的,故让l一步步靠近r来找到满足条件的答案。

这样,无论是64种二分中的任何一种,你都可以按照这种求下界的方法来做了。

模板

class Solution {
public:
    static bool cmp1(const int &a, const int &b) {
        return a >= b;
    }

    static bool cmp2(const int &a, const int &b) {
        return a <= b;
    }

    // 求下界
    int getDown(vector<int> data, int k, bool (*cmp)(const int &, const int &))
    {
        int l, r;
        for (l = 0, r = data.size() ; l < r; ) {
            int mid = (l + r) / 2;
            if (!cmp(data[mid], k))
                l = mid + 1;
            else
                r = mid;
        }
        return l;
    }

    // 求上界
    int getUp(vector<int> data, int k, bool (*cmp)(const int &, const int &))
    {
        int l, r;
        for (l = -1, r = data.size() - 1; l < r; ) {
            int mid = (l + r + 1) / 2;
            if (!cmp(data[mid], k))
                r = mid - 1;
            else
                l = mid;
        }
        return l;
    }

    int GetNumberOfK(vector<int> data ,int k) {
        int up = getUp(data, k, cmp2), down = getDown(data, k, cmp1);
        return up - down + 1;
        // return getUp(data, k, &cmp) - getDown(data, k, &cmp) + 1;
};

这个模版以上面那个题目为背景写的,用了一个函数指针,这样可以把这个问题一般化,这个函数指针指向一个比较函数,这个比较函数中你就写合法的条件就可以了,STL中就是这么设计它们的lower_bound的。

其他

既然讲到了STL,我就再告诉你upper_bound函数,lower_bound函数和我们上面说的求下界是一模一样的,但是,upper_bound就有点不一样了,它也是求上界,但是答案是上界的后一个位置;

利用我们刚才说的求下界,我们可以在此基础上改编一下求下界的方法,让它也能求上界,这样改编出来的函数就和upper_bound一样了,但是这样改编出来的函数只适合上面的这个题目背景,具体问题具体分析,这也是为什么STL有了lower_bound为什么还要搞一个upper_bound的原因了;

求下界的函数主体不要动,只要改一下cmp2就好了。想一下,求最后一个大于等于k的元素的位置,那我们如果求得了第一个大于k的元素的位置,那么这个位置是不是就是答案的后一个位置,所以,cmp2中的内容改成return a > b;, 然后调用getDown(data, k, cmp2)就可以求得上界的后一个位置了。

查看评论

几种情况的二分写法

之前总是对二分的边界问题把握的不是很好,以致于出现死循环等问题。所以用这篇博文进行总结。 首先,本文所用算法均为左闭右闭的算法,且数组是以非递减顺序排列的。 1、查找是否存在关键值,如有相等的,则返回...
  • Sleppypot
  • Sleppypot
  • 2017-02-23 13:48:45
  • 225

二分搜索的3种正确写法

(1)闭区间写法 b
  • lzk413
  • lzk413
  • 2014-06-28 20:55:26
  • 547

你是真的太累了么?

你不是太累。你太分心了。 “我太累了~” 我有太多还没有开始计划就流产的项目,都是因为这句话。 这是一个能够让我的任何一个计划泡汤的借口。就是这么简单的理由,但是,仅仅因为一...
  • jingxia2008
  • jingxia2008
  • 2015-10-08 10:03:24
  • 624

二分查找经典写法

二分查找 public static int binSearch(int srcArray[], int key) {          int mid;          int start = 0...
  • wzxin1234
  • wzxin1234
  • 2018-03-07 00:07:35
  • 13

你真的理解二分的写法吗 - 二分写法详解

说实话,我之前也不完全理解二分查找的各种写法,导致在写各种二分的边界时我总是弄不清边界值,于是我只能通过暴力枚举这些边界值,去一个一个试,这样子效率真的很低下。于是,痛定思痛,一定要把二分的写法吃透,...
  • FlushHip
  • FlushHip
  • 2018-02-05 16:35:24
  • 1043

你真的会写二分查找吗

看到这个标题你可能感觉二分法太简单了,谁能不会写。的确,二分法是我们学习算法和数据结构路上最早提到的算法,看一遍人人都能理解。也正是因为大家都认为这个问题太简单了,所以基本上都是看看就过去了,根本就不...
  • love_register
  • love_register
  • 2016-07-14 20:50:42
  • 606

【模板+讲解】二分答案

【模板+讲解】二分答案 !阅读须知||阅读本博文前笔者认为读者已经学会(或了解)了: 1.基础语言与算法 2.标准二分法(二分思想) 3.二分查找 定义二分答案与二分查找类似,即...
  • Mashiro_ylb
  • Mashiro_ylb
  • 2017-11-07 16:17:59
  • 415

WikiOI 1217 借教室 (NOIP2012)二分写法

题目描述 Description 在大学期间,经常需要租借教室。大到院系举办活动,小到学习小组自习讨论,都需要向学校申请借教室。教室的大小功能不同,借教室人的身份不同,借教室的手续也不一样。面对海量...
  • CodeOnce
  • CodeOnce
  • 2016-11-01 10:52:09
  • 385

二分查找法的两种写法

有序数列中查找 先和中间数查找,比较大小再和边上的查找 1. 2.利用递归: public class Erfen { public static int binarySearch(int[...
  • qq_31469369
  • qq_31469369
  • 2017-05-17 18:16:22
  • 216

二分写法

二分的正确写法整数二分int l = 0, r = n;while (l > 1; if (Judge(x, y, nMid)) { l = nMid + 1; } ...
  • govshell
  • govshell
  • 2017-03-18 20:41:14
  • 138
    个人资料
    专栏达人 持之以恒
    等级:
    访问量: 11万+
    积分: 3482
    排名: 1万+
    博客公告

    Name: FlushHip


    QQ:1299949836


    Email: FlushHip@foxmail.com


    WeChat: chenyangweixiao


    博客专栏
    最新评论