二分查找
二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想.每次都通过跟区间的中间元素对比,将待查找的数据的区间缩小为原先的一半 直到找到要查找的元素,或者区间被缩小为0.
O(logn)的查找速度.
二分查找是一种非常高效的查找算法,时间复杂度是O(logn).这种对数时间复杂度,是一种极其高效的时间复杂度,有的时候甚至比常量级O(1)的算法还要高效
二分查找的递归与非递归实现
最简单的情况就是有序数组中不存在重复元素.
1 | //非递归 |
容易出错的地方
- 循环退出条件 注意是low <= height,而不是low < height
- mid的取值,实际上mid=(low+height)/2 这种写法是有问题的.因为如果low和height比较大的话两者之和可能就会溢出.改进方式是将mid的计算方式改为low+(height-low)/2.更近一步,我们可以将除以2的操作转化成位运算low+((height-low)>>1).
- low 和 height 的更新.注意这里的+1和-1,如果直接写成low=mid或者height=mid,就可能会发生死循环.
1 | //递归方式 |
二分查找的局限性
首先,二分查找依赖的是顺序表结构,简单说就是数组.
二分查找能否依赖其他数据结构呢?比如说链表呢?答案是否定的.主要原因是二分查找需要按照下标随机访问元素.数组随机访问时间复杂度是O(1),而链表是O(n).如果数据使用链表存储,二分查找时间复杂度会变的很高.
其次,二分查找针对的是有序数据
二分查找的数据必须是有序的.如果无序,我们需要先排序,排序的最低时间复杂度是O(nlogn).所以,如果我们针对的是一组静态数据,没有频繁的插入,删除,我们可以进行一次排序,多次二分查找.
但是,如果数据集合有频繁的插入和删除,想要使用二分查找,要么每次操作之后,保证数据依然有序;要么每次查找前都先排序.
所以,二分查找只能用在插入,删除操作不频繁,一次排序多次查找的场景中.针对动态变化的数据,二分查找,并不适用.
数据量太少不适合二分查找
如果数据量太很少,完全没必要用二分查找,顺序遍历就够了. 但是,如果数据之间比较操作非常耗时,不管数据量大小都推荐适用二分查找.
数据量太大也不适合二分查找
二分查找底层需要依赖数组这种数据结构,而数组为了支持随机访问特性,要求内存空间需要连续.太大的数据需要更多的连续内存空间,数据存储比较吃力,所以不推荐.
二分查找变体
变体1,查找一个值等于给定值的元素
有序集合中存在重复数据,我们希望第一个值等于给定值的顺序.
1 | public static int binarySearch1(int[] arr, int value){ |
arr[mid]跟要查找的value的大小关系有三种情况:大于,小于,等于.大于,小于情况下的代码好理解.等于的情况下代码怎么理解呢?
我们重点看等于情况下的代码,如果mid=0,那这个元素就是数据的第一个元素那肯定是我们要找的;如果mid不等于0,但arr[mid]的前一个元素arr[mid-1]不等于value,那说明arr[mid]是我们要找的.
如果arr[mid-1]也等于value,那arr[mid]肯定不是我们要找的.那我们就更新height=mid - 1.因为要找的元素肯定在[low,mid-1]之间.
变体2,查找最后一个值等于给定的元素
跟变体1相似.
1 | public static int binarySearch2(int[] arr,int value){ |
变体3,查找第一个大于等于给定值的元素
例如: a[] = {1,2,3,5,6}.如果查找第一个大于等于4的元素,那就是5.
1 | public static int binarySearch2(int[] arr,int value){ |
变体4, 查找最后一个小于等于给定值的元素
1 | public static int binarySearch2(int[] arr,int value){ |