【前言】
二分算法又称二分查找法,是一种在有序数组中查找特定元素的高效算法。
一、基本原理
- 首先,确定一个有序数组以及要查找的目标元素。
- 接着,取数组的中间元素,将其与目标元素进行比较。
1)如果中间元素等于目标元素,查找成功,算法结束。
2)如果中间元素大于目标元素,则在数组的前半部分继续查找。
3)如果中间元素小于目标元素,则在数组的后半部分继续查找。- 重复上述步骤,直到找到目标元素或者确定目标元素不在数组中。
二、时间复杂度
二分算法的时间复杂度为O(logN) ,其中 N 是数组的长度。这是因为每次查找都将问题规模减半,所以查找次数与数组长度的对数成正比。
二分算法的难点:有很多细节问题,容易出现死循环
一、朴素二分查找
首先给出一个例子来理解二分查找是如何操作的。如下,对于一个有序数列:
元素: | 1 | 5 | 6 | 8 | 10 | 13 | 14 |
下标: | iLeft | iMid | iRight |
如上有序数列,如果要寻找目标数字 13。 iMid 的所代表的元素(8)小于目标元素(13),则令 iLeft 更新为 iMid + 1,再更新iMid,更新为左右区间的中间位置。如下表格。
元素: | 1 | 5 | 6 | 8 | 10 | 13 | 14 |
下标: | iLeft | iMid | iLeft | iMid | iRight |
通过上述例子,我们可以把这个问题抽象为一个通用的问题:
如下图,target 将数据分为三段:①< target ②target ③>tagert
其实,对于数列来说,要寻找目标元素target,只要这段数据能被target划分为两段不同属性的数据,就可以用二分查找的方式,不一定是要在数值上有某种规律。
特别提醒:中间点的计算
iMid = (iLeft + iRight) / 2 or iMid = (iLeft + iRight + 1) / 2 这样的算逻辑上是可行的,但是 (iLeft + iRight) 可能有溢出的风险。
推荐:iMid = iLeft + (iLeft - iRight) / 2 or iMid = iLeft + (iLeft - iRight + 1) / 2
二、寻找左端点
同样的,先看一个例子。如下数据,寻找目标区间为:以 13 开头,13结尾的区间,即[ 13, 13, 13 ]。我们可以先通过二分查找找到左端点值。
1 | 5 | 6 | 7 | 13 | 13 | 13 | 14 |
iLeft | iMid | iRight |
寻找左端点:(如下表格,标红的“13”即为需要寻找的左端点)
1 | 5 | 6 | 7 | 13 | 13 | 13 | 14 |
iLeft | iMid | targetL | iRight |
以 13 为界限,可以将数据划分为两段,一段为 < 13 的元素,另一段为 ≥ 13 的元素。
1 | 5 | 6 | 7 | 13 | 13 | 13 | 14 |
iLeft | iMid | iRight |
如果要找的元素在 iMid 后面,则 iLeft 向 iMid 缩进,如下表:(注意!虽然此时 iMid 位置代表的元素值等于要找的元素值,但不是实际上我们要找到的值,这是跟朴素二分查找很不一样的地方)
1 | 5 | 6 | 7 | 13 | 13 | 13 | 14 |
iLeft | iMid | iLeft | iMid | iRight |
如果要找的元素在 iMid 前面,则 iRight 向 iMid 缩进,如下表:
1 | 5 | 6 | 7 | 13 | 13 | 13 | 14 |
iLeft iMid | iRight | iRight |
如果要找的元素在 iMid 所在的位置,则则 iRight 向 iMid 缩进,如下表:
1 | 5 | 6 | 7 | 13 | 13 | 13 | 14 |
iLeft iMid iRight | iRight |
如上,当 iLeft == iRight 时,即为结束。
接下来,将上述这个例子抽象为一个通用的解法:
从上面的两种情况可知,情况一:iRight -> iMid 移动;情况二:iLeft -> iMid + 1. 如此判断并循环这个操作。
⭐循环结束的情况
下图说明了三种在给定数据区间内寻找target的情况:①能在给定数据中找到目标值;②目标值大于所有给定数据;③目标值小于所有给定数据👇
当 iLeft == iRight 时结束循环,因此循环应写作 while(iLeft < iRight){...}
👉 区别与朴素二分查找(因为如果写成 while(iLeft <= iRight){...} 会有陷入死循环的可能。)
在朴素二分查找中,是通过直接的数值比较是否相等来确认是否找到,在寻找左端点的情况下,通过数值相等来判断已经不适用了。这实际上是通过不断缩短左右区间,最后当区间缩短到只剩一个元素的时候,再去判断其值是否与目标元素的值相等。
⭐iMid的选择
上文有说到 “推荐:iMid = iLeft + (iLeft - iRight) / 2 or iMid = iLeft + (iLeft + iRight + 1) / 2 ”。基于此,分析下面两种极端的情况:
1 | 5 | 6 | 7 | 13 | 13 | 13 | 14 |
iLeft | iRight | ||||||
iMid = iLeft + (iLeft - iRight) / 2 | iMid = iLeft + (iLeft - iRight + 1) / 2 |
如上表格,当 iMid = iLeft + (iLeft - iRight + 1) / 2 时,由于更新的左右区间端点的方式为:情况一:iRight -> iMid 移动;此时,总满足:iRight == iMid && iLeft < iRight. 因此左右端点不会再被更新,循环的条件总是满足,则会陷入死循环。
因此,寻找左端点时,应选择 iMid = iLeft + (iLeft - iRight) / 2 来更新中间点的位置。
三、寻找右端点
寻找右端点的分析思路同寻找左端带的方式是一样,不再赘述。下面直接给出结论。
- 更新左右区间端点的方式:情况一:iRight -> iMid - 1 移动;情况二:iLeft -> iMid
- 循环结束的条件:iLeft == iRight
- iMid的选择:iMid = iLeft + (iLeft + iRight + 1) / 2
结尾总结:陷入死循环的本质是循环条件总是满足。循环条件总是满足有两种情况:1)数据一直不更新;2)循环条件设置错误,不管数据如何更新总符合条件。
END.