几乎刷完了力扣全部的二分题,我发现了这些

前言

你们好,我是 lucifer。今天给你们带来的是《二分》专题。先上下本文的提纲,这个是我用 mindmap 画的一个脑图,以后我会继续完善,将其余专题逐步完善起来。算法

你们也可使用 vscode blink-mind 打开源文件查看,里面有一些笔记能够点开查看。源文件能够去个人公众号《力扣加加》回复脑图获取,之后脑图也会持续更新更多内容。vscode 插件地址: https://marketplace.visualstu...

本系列包含如下专题:编程

<!-- more -->数组

本专题预计分两部分两进行。第一部分主要讲述基本概念一个中心。有了这些基础知识以后,第二部分咱们继续学习两种二分类型四大应用数据结构

本文内容已经同步到个人刷题插件的 RoadMap 中,结合刷题插件食用味道更佳哦~ 插件的获取方式能够在个人公众号力扣加加中回复插件查看。ide

刷题插件

若是以为文章有用,请点赞留言转发一下,让我有动力继续作下去。

前言

为了准备这个专题,我不只肝完了力扣的全部二分题目,还肝完了另一个 OJ 网站 - Binary Search 的全部二分题目,一共100 多道。你们看完若是以为有用,能够经过点赞转发的方式告诉我,若是喜欢的人多,我继续尽快出下篇哦~学习

二分查找又称折半搜索算法。 狭义地来说,二分查找是一种在有序数组查找某一特定元素的搜索算法。这同时也是大多数人所知道的一种说法。实际上, 广义的二分查找是将问题的规模缩小到原有的一半。相似的,三分法就是将问题规模缩小为原来的 1/3。测试

本文给你们带来的内容则是狭义地二分查找,若是想了解其余广义上的二分查找能够查看我以前写的一篇博文 从老鼠试毒问题来看二分法网站

尽管二分查找的基本思想相对简单,但细节能够使人难以招架 ... — 高德纳

当乔恩·本特利将二分搜索问题布置给专业编程课的学生时,百分之 90 的学生在花费数小时后仍是没法给出正确的解答,主要由于这些错误程序在面对边界值的时候没法运行,或返回错误结果。1988 年开展的一项研究显示,20 本教科书里只有 5 本正确实现了二分搜索。不只如此,本特利本身 1986 年出版的《编程珠玑》一书中的二分搜索算法存在整数溢出的问题,二十多年来无人发现。Java 语言的库所实现的二分搜索算法中一样的溢出问题存在了九年多才被修复。spa

可见二分查找并不简单, 本文就试图带你走近 ta,明白 ta 的底层逻辑,并提供模板帮助你们写书 bug free 的二分查找代码。看完讲义后建议你们结合 LeetCode Book 二分查找 练习一下。插件

基本概念

首先咱们要知道几个基本概念。这些概念对学习二分有着很重要的做用,以后遇到这些概念就再也不讲述了,默认你们已经掌握。

解空间

解空间指的是题目全部可能的解构成的集合。好比一个题目全部解的多是 1,2,3,4,5,但具体在某一种状况只能是其中某一个数(便可能是 1,2,3,4,5 中的一个数)。那么这里的解空间就是 1,2,3,4,5 构成的集合,在某一个具体的状况下多是其中任意一个值,咱们的目标就是在某个具体的状况判断其具体是哪一个。若是线性枚举全部的可能,就枚举这部分来讲时间复杂度就是 $O(n)$。

举了例子:

若是让你在一个数组 nums 中查找 target,若是存在则返回对应索引,若是不存在则返回 -1。那么对于这道题来讲其解空间是什么?

很明显解空间是区间 [-1, n-1],其中 n 为 nums 的长度。

须要注意的是上面题目的解空间只多是区间 [-1,n-1] 之间的整数。而诸如 1.2 这样的小数是不可能存在的。这其实也是大多数二分的状况。 但也有少部分题目解空间包括小数的。若是解空间包括小数,就可能会涉及到精度问题,这一点你们须要注意。

好比让你求一个数 x 的平方根,答案偏差在 $10^-6$ 次方都认为正确。这里容易知道其解空间大小可定义为 [1,x](固然能够定义地更精确,以后咱们再讨论这个问题),其中解空间应该包括全部区间的实数,不只仅是整数而已。这个时候解题思路和代码都没有太大变化,惟二须要变化的是:

  1. 更新答案的步长。 好比以前的更新是 l = mid + 1,如今可能就不行了,所以这样可能会错过正确解,好比正确解刚好就在区间 [mid,mid+1] 内的某一个小数。
  2. 判断条件时候须要考虑偏差。因为精度的问题,判断的结束条件可能就要变成 与答案的偏差在某一个范围内

对于搜索类题目,解空间必定是有限的,否则问题不可解。对于搜索类问题,第一步就是须要明确解空间,这样你才可以在解空间内进行搜索。这个技巧不只适用于二分法,只要是搜索问题均可以使用,好比 DFS,BFS 以及回溯等。只不过对于二分法来讲,明确解空间显得更为重要。若是如今还不理解这句话也不要紧,看完本文或许你就理解了。

定义解空间的时候的一个原则是: 能够大但不能够小。由于若是解空间偏大(只要不是无限大)无非就是多作几回运算,而若是解空间太小则可能错失正确解,致使结果错误。好比前面我提到的求 x 的平方根,咱们固然能够将解空间定义的更小,好比定义为 [1,x/2],这样能够减小运算的次数。但若是设置地过小,则可能会错过正确解。这是新手容易犯错的点之一。

有的同窗可能会说我看不出来怎么办呀。我以为若是你实在拿不许也彻底没有关系,好比求 x 的平方根,就能够甚至为 [1,x],就让它多作几回运算嘛。我建议你给上下界设置一个宽泛的范围。等你对二分逐步了解以后能够卡地更死一点

序列有序

我这里说的是序列,并非数组,链表等。也就是说二分法一般要求的序列有序,不必定是数组,链表,也有多是其余数据结构。另外有的序列有序题目直接讲出来了,会比较容易。而有些则隐藏在题目信息之中。乍一看,题目并无有序关键字,而有序其实就隐藏在字里行间。好比题目给了数组 nums,而且没有限定 nums 有序,但限定了 nums 为非负。这样若是给 nums 作前缀和或者前缀或(位运算或),就能够获得一个有序的序列啦。

更多技巧在四个应用部分展开哦。

虽然二分法不意味着须要序列有序,但大多数二分题目都有有序这个显著特征。只不过:

  • 有的是题目直接限定了有序。这种题目一般难度不高,也容易让人想到用二分。
  • 有的是须要你本身构造有序序列。这种类型的题目一般难度不低,须要你们有必定的观察能力。

好比Triple Inversion。题目描述以下:

Given a list of integers nums, return the number of pairs i < j such that nums[i] > nums[j] * 3.

Constraints: n ≤ 100,000 where n is the length of nums
Example 1
Input:
nums = [7, 1, 2]
Output:
2
Explanation:
We have the pairs (7, 1) and (7, 2)

这道题并无限定数组 nums 是有序的,可是咱们能够构造一个有序序列 d,进而在 d 上作二分。代码:

class Solution:
    def solve(self, A):
        d = []
        ans = 0

        for a in A:
            i = bisect.bisect_right(d, a * 3)
            ans += len(d) - i
            bisect.insort(d, a)
        return ans

若是暂时不理解代码也不要紧,你们先留个印象,知道有这么一种类型题便可,你们能够看完本章的全部内容(上下两篇)以后再回头作这道题。

极值

相似我在堆专题 提到的极值。只不过这里的极值是静态的,而不是动态的。这里的极值一般指的是求第 k 大(或者第 k 小)的数。

堆的一种很重要的用法是求第 k 大的数,而二分法也能够求第 k 大的数,只不过两者的思路彻底不一样。使用堆求第 k 大的思路我已经在前面提到的堆专题里详细解释了。那么二分呢?这里咱们经过一个例子来感觉一下:这道题是 Kth Pair Distance,题目描述以下:

Given a list of integers nums and an integer k, return the k-th (0-indexed) smallest abs(x - y) for every pair of elements (x, y) in nums. Note that (x, y) and (y, x) are considered the same pair.

Constraints:n ≤ 100,000 where n is the length of nums
Example 1
Input:
nums = [1, 5, 3, 2]
k = 3
Output:
2
Explanation:

Here are all the pair distances:

abs(1 - 5) = 4
abs(1 - 3) = 2
abs(1 - 2) = 1
abs(5 - 3) = 2
abs(5 - 2) = 3
abs(3 - 2) = 1

Sorted in ascending order we have [1, 1, 2, 2, 3, 4].

简单来讲,题目就是给的一个数组 nums,让你求 nums 第 k 大的任意两个数的差的绝对值。固然,咱们可使用堆来作,只不过使用堆的时间复杂度会很高,致使没法经过全部的测试用例。这道题咱们可使用二分法来降维打击。

对于这道题来讲,解空间就是从 0 到数组 nums 中最大最小值的差,用区间表示就是 [0, max(nums) - min(nums)]。明确了解空间以后,咱们就须要对解空间进行二分。对于这道题来讲,能够选当前解空间的中间值 mid ,而后计算小于等于这个中间值的任意两个数的差的绝对值有几个,咱们不妨令这个数字为 x。

  • 若是 x 大于 k,那么解空间中大于等于 mid 的数都不多是答案,能够将其舍弃。
  • 若是 x 小于 k,那么解空间中小于等于 mid 的数都不多是答案,能够将其舍弃。
  • 若是 x 等于 k,那么 mid 就是答案。

基于此,咱们可以使用二分来解决。这种题型,我总结为计数二分。我会在后面的四大应用部分重点讲解。

代码:

class Solution:
    def solve(self, A, k):
        A.sort()
        def count_not_greater(diff):
            i = ans = 0
            for j in range(1, len(A)):
                while A[j] - A[i] > diff:
                    i += 1
                ans += j - i
            return ans
        l, r = 0, A[-1] - A[0]

        while l <= r:
            mid = (l + r) // 2
            if count_not_greater(mid) > k:
                r = mid - 1
            else:
                l = mid + 1
        return l

若是暂时不理解代码也不要紧,你们先留个印象,知道有这么一种类型题便可,你们能够看完本章的全部内容(上下两篇)以后再回头作这道题。

一个中心

二分法的一个中心你们必定紧紧记住。其余(好比序列有序,左右双指针)都是二分法的手和脚,都是表象,并非本质,而折半才是二分法的灵魂

前面已经给你们明确了解空间的概念。而这里的折半其实就是解空间的折半。

好比刚开始解空间是 [1, n](n 为一个大于 n 的整数)。经过某种方式,咱们肯定 [1, m] 区间都不多是答案。那么解空间就变成了 (m,n],持续此过程知道解空间变成平凡(直接可解)。

注意区间 (m,n] 左侧是开放的,表示 m 不可能取到。

显然折半的难点是根据什么条件舍弃哪一步部分。这里有两个关键字:

  1. 什么条件
  2. 舍弃哪部分

几乎全部的二分的难点都在这两个点上。若是明确了这两点,几乎全部的二分问题均可以迎刃而解。幸运的是,关于这两个问题的答案一般都是有限的,题目考察的每每就是那几种。这其实就是所谓的作题套路。关于这些套路,我会在以后的四个应用部分给你们作详细介绍。

二分法上篇小结

上篇主要就是带你们了解几个概念,这些概念对作题极为重要,请务必掌握。接下来说解了二分法的中心 - 折半,这个中心须要你们作任何二分都要放到脑子中。

若是让我用一句话总结二分法,我会说二分法是一种让未知世界无机可乘的算法。即二分法不管如何咱们均可以舍弃一半解,也就是不管如何均可以将解空间砍半。难点就是上面提到的两点:什么条件舍弃哪部分。这是二分法核心要解决的问题。

以上就是《二分专题(上篇)》的全部内容。若是以为文章有用,请点赞留言转发一下,让我有动力继续出下集。

下集预告

上集介绍的是基本概念。下一集咱们介绍两种二分的类型以及四种二分的应用。

下集目录:

  • 两种类型

    • 最左插入
    • 最右插入
  • 四大应用

    • 能力检测二分
    • 前缀和二分
    • 插入排序二分(不是你理解的插入排序哦)
    • 计数二分

其中两种类型(最左和最右插入)主要解决的的是:解空间已经明确出来了,如何用代码找出具体的解

而四大应用主要解决的是:如何构造解空间。更多的状况则是如何构建有序序列。

这两部分都是实操性很强的内容,在理解这两部份内容的同时,请你们务必牢记一个中心折半。那咱们下篇见喽~

以上就是本文的所有内容了, 你们对此有何见解,欢迎给我留言,我有时间都会一一查看回答。我是 lucifer,维护西湖区最好的算法题解,Github 超 40K star 。你们也能够关注个人公众号《力扣加加》带你啃下算法这块硬骨头。

相关文章
相关标签/搜索