博主秋招提前批已拿百度、字节跳动、拼多多、顺丰等公司的offer,可加微信:pcwl_Java 一起交流秋招面试经验,可获得博主的秋招简历和复习笔记。
1、二分查找:是一种查询效率非常高的查询算法,又称为折半查找。
2、要求:(1)必须采用顺序存储结构,一般都是使用数组;
(2)必须按关键字大小有序排列。
3、基本思想:假设表中元素按照升序排列,将表的中间位置记录的关键字与要查找的关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于要查找的关键字,则在前子表中查找,否则在后子表中查找,以此类推,直到找到满足条件的记录,查找成功,或者二分到子表不存在为止,则要查找的关键字不在表中。
二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想。每次都通过跟区间的中间元素对比,将待查找的区间缩小为之前的一半,直到找到要查找的元素,或者区间被缩小为0。
4、复杂度分析
(1)时间复杂度分析
我们假设数据大小为n,每次查找后数据都会缩小为原来的一半,也就是会除以2,最坏情况下,直到查找区间被缩小为空,才停止。所以被查找区间的大小变化如上图所示。其中n/=1时,k的值就是总共缩小的次数。而每一次缩小操作只涉及两个数据大小的比较(待查找元素和mid位置上元素)。所以,经过了k次区间缩小操作,时间复杂度就是O(k)。通过n/=1,我们可以求得k=,所以时间复杂度就是O(logn)。
其实logn是一个非常“恐怖”的数量级,即便n非常大,对应的logn也很小。比如当n等于2的32次方时,大约是42亿。也就是说,如果我们要在42亿个数据中使用二分查找一个数据,最多也只需要比较32次。而且在此mark下一个坑,我们用大O标记时间复杂度的时候,会省略掉常数、系数和低阶。对于常量级时间复杂度的算法来说,O(1)有可能表示的是一个非常大的常量值,比如O(10000)、O(100000)。所以,常量级时间复杂度的算法有时候可能还没有O(logn)的算法执行效率高。
(2)空间复杂度分析
算法的空间复杂度并不是计算实际的占用空间,而是计算整个算法的辅助空间单元的个数,即额外空间开销。
辅助空间是常数级别的,所以二分查找的空间复杂度为:O(1)。
5、优缺点
优点:比较次数少,查找速度快,平均性能好,占用系统内存较少;
缺点:要求待查表为有序表,且插入删除困难,因此,二分查找方法适用于不经常变动而查找频繁的有序列表。
6、二分查找应用场景的局限性
二分查找的时间复杂度虽然是O(logn),查找数据的效率非常高。但是并不是什么情况下都适合二分查找,它的应用场景是有很大的局限性的,下面列举几条:
(1)二分查找依赖的是顺序表结构,简单点说就是数组,其他数据结构无法使用二分查找;
(2)二分查找针对的是有序数组,如果是无序数组,还需要先排序,再使用二分查找。因此,二分法针对的是一组静态的数据,没有频繁的插入和删除操作,进行一次排序,多次查找;
(3)数据量太小不适合二分查找。当数据量比较小的时候,使用遍历就可以了;
(4)数据量太大也不适合二分查找。二分查找底层依赖数组这种数据结构,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。比如,我们有1GB大小的数据,如果希望使用数组来存储,那就需要1GB的连续内存空间来存储。
说明:下面两种实现过程均为:有序数组中不存在重复元素的情况下。
public class BinarySearch_Circulate {
public static int binarySearch(int[] arr,int value){
int low = 0;
int high = arr.length - 1;
// 注意:终止循环条件为low<=high,等于不要漏掉了
while(low <= high){
int mid = low + (high - low) / 2;
if(arr[mid] == value){
return mid;
}else if(arr[mid] < value){
low = mid + 1; // 在后半部分查找
}else{
high = mid - 1; // 在前半部分查找
}
}
return -1; // 未查找到
}
// 测试
public static void main(String[] args) {
int arr[] = {2, 5, 6, 34, 65, 79, 88, 106, 127};
int index = binarySearch(arr, 88); // index = 6
System.out.println(index);
}
}
public class BinarySearch_Recursion {
public static int binarySearch(int[] arr, int n, int val){
return bSearch(arr, 0, n - 1, val);
}
private static int bSearch(int[] arr, int low, int high, int val) {
if(low > high){
return -1;
}
// 将除以2的操作转化为位运算,相比较除法运算,计算机处理位运算的速度要快得多
int mid = low +((high - low) >> 1);
if(arr[mid] == val){
return mid;
}else if(arr[mid] < val){
return bSearch(arr, mid + 1, high, val);
}else{
return bSearch(arr, low, mid - 1, val);
}
}
// 测试
public static void main(String[] args) {
int arr[] = {2, 5, 6, 34, 65, 79, 88, 106, 127};
int index = binarySearch(arr, 9, 88); // index = 6
System.out.println(index);
}
}
Mark:在这里说明一个问题:我们常说当用mid = (low + high) / 2 时,会提及到如果low和high比较大的时候,两者之和就有可能会溢出,这是因为int是四个字节,有一定的范围,如果两个其中一个非常大,相加就很容易超出这个范围,改成相减则可以在一定成度上避免这种情况,或者直接使用位运算的方法,还可以提升效率。
上面讲的二分查找是最基本的,即有序数据集合中不存在重复的数据,我们在其中查找等于某个给定值的数据。那么接下来,我们介绍几种二分查找的变形,可以在有重复元素的有序数据集合中进行查找操作。
我们先来分析下,上面介绍的简单二分查找为什么不能处理有重复数据的情况:
假设有这样的一个数组: int array = {1, 3, 4, 5, 6, 8, 8, 8, 11, 18},其中a[5],a[6],a[7]的值都等于8,是重复的数据,现在希望查找第一个等于8的数据,也就是下标为5的元素。如果用上面的二分查找方法,首先拿8与区间的中间值a[4]进行比较,8比6大,于是在下标5到9之间继续查找。下标5到9的中间位置是下标7,而a[7]正好等于8,所以找到就返回了。尽管a[7]也等于8,但它并不是我们想要找的一个等于8的元素,因为第一个值等于8的元素是数组下标为5的元素。
因此,我我们需要改善原始的二分查找方法以适应在有重复数据的有序数据集合中查找数据。
4种常见的二分查找变形问题:
1、查找第一个值等于给定值的元素;
2、查找最后一个值等于给定值的元素;
3、查找第一个大于等于给定值的元素;
4、查找最后一个小于等于给定值的元素。
public class BinarySearch_Improved1 {
public static int binarySearch(int[] arr, int n, int val){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = low + ((high - low) >> 1);
if(arr[mid] > val){
high = mid - 1;
}else if(arr[mid] < val){
low = mid + 1;
}else{
// 如果a[mid]=val,且a[mid - 1] != val,则a[mid]就是我们要找的第一个值为val的元素
if(mid == 0 || (arr[mid - 1]) != val){
return mid;
}else{
high = mid - 1;
}
}
}
return -1;
}
// 测试
public static void main(String[] args) {
int arr[] = {2, 5, 6, 34, 65, 79, 88, 88,88,106, 999};
int index = binarySearch(arr, 9, 88); // index = 6
System.out.println(index);
}
}
// 二分查找变形:查找最后一个值等于给定值的元素
public static int binarySearch(int[] arr, int n, int val){
int low = 0;
int high = n - 1;
while(low <= val){
int mid = low + ((high - low) >> 1);
if(arr[mid] > val){
high = mid - 1;
}else if(arr[mid] < val){
low = mid + 1;
}else{
if(mid == n - 1 || (arr[mid + 1] != val)){
return mid;
}else{
low = mid + 1;
}
}
}
return -1;
}
// 测试
public static void main(String[] args) {
int arr[] = {2, 5, 6, 34, 65, 79, 88, 88, 88, 106, 999};
int index = binarySearch(arr, 9, 88); // index = 8
System.out.println(index);
}
// 查找第一个大于等于给定值的元素
public static int binarySearch(int[] arr, int n, int val){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = low + ((high - low) >> 1);
if(arr[mid] >= val){
// 如果(mid == 0) || (arr[mid - 1] < val,则mid位置上的元素则是第一个大于val的元素
if((mid == 0) || (arr[mid - 1] < val)){
return mid;
}else{
high = mid - 1;
}
}else{
low = mid + 1;
}
}
return -1;
}
// 测试
public static void main(String[] args) {
int arr[] = {2, 5, 6, 34, 65, 79, 88, 88, 88, 106, 999};
// 第一个大于8的数为arr[3] = 34;
int index = binarySearch(arr, 9, 8); // index = 3
System.out.println(index);
}
// 查找最后一个小于等于给定值的的元素
public static int binarySearch(int[] arr, int n, int val){
int low = 0;
int high = n - 1;
while(low <= high){
int mid = low + ((high - low) >> 1);
if(arr[mid] > val){
high = mid - 1;
}else{
// 如果(mid == n - 1) || (arr[mid + 1] > val),则mid位置上的元素为最后一个小于等于给定值val的元素
if((mid == n - 1) || (arr[mid + 1] > val)){
return mid;
}else{
low = mid + 1;
}
}
}
return -1;
}
// 测试
public static void main(String[] args) {
int arr[] = {2, 5, 6, 34, 65, 79, 88, 88, 88, 106, 999};
int index = binarySearch(arr, 9, 88); // index = 8
System.out.println(index);
}
1、360百科--二分查找
2、Java实现二分查找--两种方式
3、Java实现二分查找法
学习不是单打独斗,如果你也是做 Java 开发,可以加我微信:pcwl_Java,一起分享学习经验!