二分查找应该都会,那么二分查找的变体呢?




0. 前言

你们好,我是多选参数的程序锅,一个正在”捣鼓“操做系统、学数据结构和算法以及 Java 的硬核菜鸡。node

二分查找你们估计都会,可是二分查找的变体你们会吗?我相信大佬都是会的,可是我这个菜鸡就是不会了。还记得,在学习二分查找变体的时候,我像发现了新大陆通常,很开森,很开森,很开森。git

为了整个知识的相对完整,下面仍是从最基本的二分查找开始讲解,以后讲解二分查找的变体,这个变体在刷 Leetcode 的有些题目的时候也会用到。最后对二分查找这种算法进行总结。另外,这个数据结构和算法系列的代码都在 github 仓库中能够找到:https://github.com/DawnGuoDev/algos 。github

1. 二分查找及其变体

二分查找针对的是一个有序的数据集合(必须是有序),查找思想有点相似分治思想。每次都经过跟区间的中间元素对比,将待查找的区间缩小为以前的一半(或者说剔除了另外一半数据),直到找到要查找的元素,或者区间被缩小为 0。web

因为通过一次查找,会剔除一半数据而剩下另外一半数据,所以通过 k 次查找以后,剩下的数据个数为  ,整个二分查找当剩下一个元素的时候中止,所以须要通过  次查找,时间复杂度也就是  算法

1.1. 最基础的实现

这边先讲解不存在重复元素的有序数组中,查找值等于给定值的元素的状况(PS:全文的讲解都以数据是从小到大排列为前提)。数组

1.1.1. 非递归的方式

public int bsearch(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);
if (array[mid] == value) {
return mid;
} else if (array[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}

return -1;
}

在实现非递归算法时,须要注意如下几个关键点:微信

  • 循环的条件是 low <= high,而不是 low < high。由于可能 low 和 high 重合的时候正是须要查询的值,好比 1,2,3 那么假如我要查询 3 这个值的位置时,是在 low 等于 high 的时候才查询到的。
  • mid = (low+high)/2 这种写法不太严谨,由于 low 和 high 比较大的时候,可能就会溢出。因此,改进的方法是 mid = low +(high-low)/2。固然为了追求性能的极致,那么能够将这里的除以 2 改成移位操做。由于移位操做比除法运算来讲,计算机处理前者会更快。最终为 mid = low + ((high-low)>>1)。须要注意的是,考虑到移位操做和加法的优先级,这边的括号必需要这样。
  • low 和 high 值的更新,这边必定要记得 +1 和 -1,不然的话可能会进入死循环。假如没有+1 或者 -1 的操做,那么 1,2,3 我要查询的是 3 这个值,第一步 low=0, high=2;第二步 low=1,high=2;第三步仍是 low=1,high=2。

1.1.2. 递归的方式

public int bsearchInternally(int[] array, int low, int high, int value) {
if (low > high) {
return -1;
}

int mid = low + ((high - low) >> 1);
if (array[mid] == value) {
return mid;
} else if (array[mid] < value) {
return bsearchInternally(array, mid + 1, high, value);
} else {
return bsearchInternally(array, low, mid - 1, value);
}
}

这边的注意点与非递归的注意点是一一对应的,递归方式注意的是循环的条件,非递归方式注意的则是递归终止的条件,这边须要 low>high 而不是 low >= high,理由是同样的,本身举例看一下。其余两个注意事项是同样的。数据结构

回忆一下递归方式编写代码的技巧:1.是先写出递归式;2.肯定终止条件;3.翻译成代码。app

1.2. 查找第一个等于给定值的元素所在的 index

接下去讲解二分查找的变体,主要考虑几种典型的状况。首先,将不存在重复元素的有序数组进行通常化,即有序数组集合中存在重复的数据。那么咱们该如何找到第一个等于给定值的数据的 index 呢?数据结构和算法

假如按照最简单的方式来实现查找的话(即上述的实现),那么获得的结果将不必定正确。好比下面这个存在重复数据的有序数组集合。假设要查找的数据是 8 ,那么先拿 8 和第 4 个数据 6 进行比较,发现 8 比 6 大,因而在下标 5-9 之间寻找。结果发现第 7 个数据 8 正好是要查找的数据,而后将 index 7 返回,可是实际上第一个 8 的 index 应该是 5。

1  3  4  5  6  8  8  8  11  18

所以,对于这个变形问题,咱们须要改造一下以前的代码。改造以后的代码以下所示:

public int bsearchFirstEqual(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);
if (array[mid] < value) {
low = mid + 1;
} else if (array[mid] > value) {
high = mid - 1;
} else {
if (mid == 0 || array[mid - 1] != value) {
return mid;
}
high = mid - 1;
}
}
return -1;
}

这边稍微解析一下代码。a[mid]跟要查找的 value 的大小关系有三种状况:大于、小于、等于。对于 a[mid] >value的状况,说明等于状况位于 low-mid 之间,因此 high = mid-1。对于 a[mid]<value 的状况,说明等于状况位于 mid-high 之间,因此 low = mid+1。对于 a[mid]=value的状况,咱们须要确保 mid 这个 index 是否是第一个等于 value 的 index。所以,先判断 mid 等不等于 0,假如等于的话,那么确定是第一个了;以后判断 mid-1 位置的元素等不等于 value,若是不等于 value,那么说明 mid 是第一个等于 value 的 index。假如 mid-1 位置的元素等于 value,那么说明第一个等于 value 在 mid 以前,因此 high=mid-1

1.3. 查找最后一个等于给定值的元素所在的 index

前面是查找第一个值等于给定值的元素,如今将问题稍微改一下,查找最后一个值等于定值的元素的 index。相应的实现代码其实和前面的相似。

public int bsearchLastEqual(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);

if (array[mid] < value) {
low = mid + 1;
} else if (array[mid] > value) {
high = mid - 1;
} else {
if (mid == len -1 || array[mid + 1] != value) {
return mid;
}
low = mid + 1;
}
}

return -1;
}

这里咱们就不分析了,分析思路跟上面的那种状况相似。

1.4. 查找第一个大于等于给定值的元素所在的 index

看完查找值相等的状况以后,接下去咱们查找值不相等的状况。在有序数组中(可含重复元素),查找第一个大于等于给定值的元素的 index。好比针对序列:三、四、六、七、10,查找第一个大于等于 5 的元素,那就是 6 ,index 是 2。

public int bsearchFirstMore(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);

if (array[mid] < value) {
low = mid + 1;
} else {
if (mid == 0 || array[mid - 1] < value) {
return mid;
}
high = mid - 1;
}
}

return -1;
}

若是 mid 位置所在的元素小于 value,那么第一个大于等于 value 的值的 index 是在 [mid+1, high] 之间,因此 low=mid+1。若是 mid 位置所在的元素已经大于 value,那么须要判断 mid 是否是第一个大于等于 value 的 index。假如 mid == 0 ,那么确定是第一个了;或者 mid 前面的那个元素小于 value,那么 mid 也是第一个大于等于 value 的 index。若是两个条件都不知足,那么第一个大于等于 value 的 index,是在 [low, mid-1] 之间,所以将 high 进行更新。

1.5. 查找最后一个小于等于给定值的元素所在的 index

如今将问题变成查找最后一个小于等于给定值的元素的 index。好比针对序列:三、五、六、八、九、10,最后一个小于等于给定值 7 的元素是 6, index 是 2 。代码的实现思路与上述状况类似。

public int bsearchLastLess(int[] array, int len, int value) {
int low = 0;
int high = len - 1;

while (low <= high) {
int mid = low + ((high - low) >> 1);

if (array[mid] > value) {
high = mid - 1;
} else {
if (mid == len - 1 || array[mid + 1] > value) {
return mid;
}
low = mid + 1;
}
}

return -1;
}

这里咱们就不分析了,分析思路跟上面的那种状况相似。

2. 总结

2.1. 二分查找的局限性

虽然二分查找的时间复杂度是 O(logn),查找效率极高,可是二分查找却不是完美的,这种查找方法存在一些局限性。

  • 二分查找依赖的是顺序表结构,简单点说就是数组。

    二分查找可否依赖其余数据结构呢?好比链表。答案是不能够的,主要缘由是二分查找算法是按照下标随机访问元素的,好比咱们访问 mid 这个位置的数据就是经过下标随机访问的,这个时间复杂度是 O(1)。假如使用链表方式的话,须要遍历到 mid 这个位置,那么时间复杂度为 O(n)。因此,若是数据使用链表存储,二分查找的时间复杂度会变得高。

  • 二分查找针对的是有序数据,在动态变化的数据集合中不适用

    二分查找的时候要求查找的数据序列必须是有序的。若是数据不是有序的,那么须要先排序才能查找。在使用时间复杂度为 O(nlogn)的排序算法的状况下。若是一组静态的数据,没有频繁地插入、删除等操做,二分查找仍是能够接受的。由于咱们能够进行一次排序,屡次二分查找。这样排序的成本就会被均摊。可是,若是咱们的数据集合有频繁的插入和删除操做的话,要想二分查找。那么每次插入、删除以后都须要进行排序,从而反正数据序列的有序。这种状况下,维护有序的时间成本时很高的。

    综上,二分查找只能用于插入、删除操做不频繁,一次排序屡次查找的状况。针对动态变化的数据集合,二分查找将再也不适合。

  • 数据量过小不适合二分查找

    要处理的数据量很小的话,彻底没有必要用二分查找,顺序遍历就能够了。好比要在 10 个有序的数组中查找一个元素,无论使用顺序遍历仍是二分查找,查找速度都查很少。可是这种状况下有个例外,就是若是比较操做很是耗时的话,那么也请用二分查找,由于虽然二者次数差很少,可是这种状况下咱们是须要尽量减小比较的次数。显然,二分查找的次数还会更少一点。

  • 数据量太大也不适合二分查找

    二分查找的底层须要依赖数组这种数据结构,而数组这种数据结构要求内存空间的连续。假如数据量太大,好比有 1GB 大小的数据,若是使用数组来存储,那么就须要 1GB 的连续内存空间。因此当要查找的数据集合特别大的时候二分查找也会不太适合。

2.2. 二分查找的优点

  • 二分查找在内存使用上更节省

    虽然大部分状况下,用二分查找的方式能够解决的问题,散列表、二叉树均可以解决。可是,无论是散列表仍是二叉树都须要额外的内存空间。而二分查找依赖的是数组,除了数据自己以外,不须要存储额外的其余信息。因此当二分查找须要 100MB 内存的状况下,散列表或二叉树须要的内存空间会更大(不止 100MB)。显然,在这三种方式中二分查找是最省内存空间的。

  • 二分查找更适合用在“近似”查找问题。

    在这类问题上,二分查找的优点更加明显,就好比这几种变体。而查找“等于给定值”的问题,更适合散列表或二叉树。这种变体的二分查找算法比较难写,尤为是细节上若是处理很差容易产生BUG,这些出错的细节有:终止条件、区间上下界更新方法、返回值选择。

后台回复【AI资料】和【学习资料】便可获取优质的学习资料


纯分享 | 全网推荐的 AI 视频教程和书籍分享


另外附上整个《拿下数据结构与算法》系列准备完成的思惟导图(不含详细内容)



不甘于「本该如此」,多选参数 值得关注




本文分享自微信公众号 - 多选参数(zhouxintalk)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索