包括以下内容:
- 二分查找key在数组中的位置
- 二分查找数组中第一个大于或等于key的位置
- 二分查找数组中第一个大于key的位置
变量解释:int[] arr1; 记录查找表,所有元素都是唯一的
int[] arr2; 记录查找表,元素不唯一
测试用例:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
arr1[] | 01 | 05 | 07 | 09 | 10 | 15 | 18 | 22 | 25 |
arr2[] | 01 | 01 | 07 | 07 | 15 | 15 | 15 | 22 | 25 |
一. 查找key在数组中的位置, 查找不成功则返回-1;
迭代实现:
1 int binary_search1(int arr[], int low, int high, int key){ 2 while(low<=high){ 3 int mid = low + (high-low)/2; 4 if(arr[mid]<key) low=mid+1; 5 else if(arr[mid]>key) high=mid-1; 6 else return mid; 7 } 8 return -1; 9 }
递归实现:
1 int binary_search(int arr[], int low, int high, int key){ 2 int mid = low+(high-low)/2; 3 if(low>high) return -1; 4 if(arr[mid]<key) return binary_search(arr, mid+1, high, key); 5 if(arr[mid]>key) return binary_search(arr, low, mid-1, key); 6 return mid; 7 }
这里对递归实现,做一定的解释:
首先这个函数的功能是在查找表arr1[]中查找key所在的位置, 如果查找成功则返回所在位置, 否则返回-1,标志未查找成功; 二分查找的前提的查找表有序,这里以查找表递增为例实现二叉查找;
每次调用函数,是对整个数组查找, 需要查找表数组arr, 查找范围的下限low,和上限high, 以及要查找的值key; 为了统一,我们这里把上限+1,后面做解释
二分查找的思路:先拿key和查找表最中间的值比较:
1.如果比中间值大,则key只可能在查找表的右半边, 在查找表的右边进行二叉查找
2. 如果key比中间的值小,那么key只可能在查找表的左半边,在查找表的左边进行二叉查找
3.如果相等, 则找到key所在key的位置
以查找7为例,
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
1 | 5 | 7 | 9 | 10 | 15 | 18 | 22 | 25 | ||
low | mid | high | 7比10大,在查找表左边进行二叉查找, high=mid-1 | |||||||
low | mid | high | 7比5大,在查找表的右边进行二叉查找, low=mid+1 | |||||||
low | mid | high | 找到7,返回7所在位置 |
有几个需要注意的地方:
- while的循环条件:应该是low<=high, 而不是low<high;
- 每次循环如果没有找到key所在的位置,要修改low=mid-1,或者high=mid+1; 不能是low=mid, 或者是high=mid,否则会导致死循环
- 只适用于递增的查找表
二叉查找,相当于在一颗平衡二叉树中进行查找, 平衡二叉树的高度h=[logn]+1, n是查找表的长度, 所以使用二叉树查找值得时候,在O(logn)的时间内就能找到所需要查找的值,及时查找失败, 比较次数也不会超过查找表对于的平衡二叉树的深度
二叉查找并不能保证比顺序查找的更优 , 对于查找概率相同的的key来说二叉树更优,当查找表前面的值查找频率较高的时候,顺序查找的效率可能比二叉查找更优
二. 查找表中第一个大于等于key的位置
1 int lower_bound(int arr[], int low, int high, int key){ 2 while(low<high){ 3 int mid = low + (high-low)/2; 4 if(key>arr[mid]) low=mid+1; 5 else high=mid; 6 } 7 return low; 8 }
函数解释:在不减的查找表里面找第一个大于或者等于key的位置, 如果key在查找表中不存在, 返回的是key在数组中应该在的位置, 比如在1,3,5中查找2, 返回的值是1, 意思就是如果查找表中有2这个值,他的位置应该在1这个位置, 如果key在查找表中, 返回的值就是key所在的位置; 解释一下上面的上限为什么要+1,任然以1,3,5为例,如果在表中查找8,传入2作为上限, 返回的值就是2,但这是错误的,8的位置应该在3;所以上限加一在于解决查找值key比查找表中所有元素还大的情况, 而对其他值得查找不会有影响;
这个函数和上面的函数很相似,整体的框架类似, 但是存在一些细微的差别:
- while的循环判断条件不同
- high的修改条件不同
- 返回值不同
下面对这三点做出解释
- 在二分查找时,有一个隐含条件是查找值key必须在查找表中,当low==high以及确定了一个唯一的位置,但是需要验证该位置的值是否等于key;但是在这个函数中,我们要找的是第一个大于或等于key的位置,如果key存在于查找表中,那么当low==hight时,查找表的值一定为key,不存在也没关系,该位置即为key在查找表中应该所在的位置
- 因为我们要找的是第一个大于或者等于key所在位置,那么当arr[mid]<key时, key的位置肯定在mid的右边,不可能在mid上,因而low=mid+1; 当arr[mid]>key时,key的位置可能在mid的左边也可能就在mid上,因为要找的是大于或等于key的值, 当arr[mid]==key的时候, key的位置就在mid上, 让high=mid,经过几次迭代就能让low==high,从而退出循环, 可以发现后面两种情况是可以合并的, 将其合并在一起,让代码精简一些
- 如果你手写实现一下该迭代过程,就会发现终止循环的的情况一定是low==high,这种情况下确定了一个唯一的位置, 返回low和high其实都一样;因为每一次迭代low的最大值即为low=[(low+high)/2]+1, 改值一定是不大于high的当low等于high退出循环,当low小于high,进行下一次迭代
以查找不存在的8为例, 正确的位置应该在4;
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
1 | 1 | 7 | 7 | 15 | 15 | 15 | 23 | 25 | ||
low | mid | high | 8小于15,在mid左边进行查找, high=mid | |||||||
low | mid | high | 8大于7,在mid右边进行查找,low=mid+1 | |||||||
low, mid | high | 8大于7,在mid右边进行查找,low=mid+1 | ||||||||
low, high | low<high条件不满足,返回low找到key应该在的位置 |
三.查找第一个大于key所在的位置
int upper_bound(int arr[], int low, int high, int key){ while(low<high){ int mid = low + (high-low)/2; if(key>=arr[mid]) low=mid+1; else high=mid; } return low; }
思路和查找第一个大于或等于key的位置的函数一样, 这里只需要把等于的条件放在左边即可; 这里不再做解释
lower_bound和upper_bound组合使用能很方便的得到查找表中所有值等于key的左闭右开区间
通过相同的思路能能实现找到第一个小于或等于key的位置, 第一个等于key的位置;