15~16 | 二分查找

二分查找相关性质 :

二分查找针对一个有序的数据集合,每次通过和区间的中间元素对比,将待查区间缩小为之前的一半,知道查到要查找的元素,或者区间缩小为0。

查询的时间复杂度:O(logn)

待查区间是:n,n/2,n/4,n/8,...,n/2^k。其中当n/2^k=1时,查询了k = log2n次,并且每次查询只涉及两个数据的大小比较操作,所以时间复杂度是O(logn)

 

如何写一个正确无误的二分查找:

二分法的写法有很多种,以为你low和high的初始化会影响后面整个二分停止条件,以及mid的赋值,low和high的更新,所以我的写法以下标为准

int low = 0,high = n-1;

要注意三个容易出错的地方:

  • 循环退出条件

注意是 low<=high,而不是 low

  • mid 的取值

mid = low + (high - low ) /2 防止当low和high都很大的时候,直接相加除以2的方法时的溢出。另外如果想继续优化性能,mid = low+((high-low)>>1),注意后面要加个括号,因为优先级的缘故。

  • low和high的更新
low = mid+1;
high = mid-1;

这里很好理解,因为二分的核心是拿中间的数取和low,high下标对应的数取比较,如果比较完成了之后,肯定要抛去中间这个数了。(对于二分的变形问题不适用,最好的方法就是解决不同变形问题时候画图想想)

 

二分法应用场景的局限性

  • 二分法依赖的是顺序表结构(数组),来完成它根据下标的O(1)快速查找。

如果你想利用链表来实现,链表的随机访问时间复杂度很高,这样会导致二分查找的时间复杂度变高。

  • 二分查找针对有序的数据;静态场景下少次插入删除,多次查找的数据。

如果是一组无序的静态数据,没有频繁的插入,删除,我们进行一次排序多次查找,这样排序的时间复杂度被均摊,二分查找效率就会很高。相反,如果数据集涉及到频繁的插入和删除,要想用二分查找,同时还要调用多次排序来维护有序性,这样的时间复杂度就很高。

  • 数据量较小,直接使用顺序查找即可,因为体现不出来二分查找的优势。

不过也有例外,当数据之间的比较非常耗时时,不管数据量大小,都推荐使用二分查找从而来降低比较的次数。

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

前面提到,二分法依赖顺序表结构,就是要开辟一块连续的内存。如果申请不到一块很大的内存空间就没法来存储这些数据。

 

思考题:

如何在 1000 万个整数中快速查找某个整数?内存限制是100MB。

每个数据大小是8B,直接将数据存在数组中,消耗内存80MB。对1000万个数据从小到大排序,然后利用二分查找,可以快速查找到想要的数据。

至于为什么要排序,因为如果要多次查找呢?如果不排序,每次都要O(n)的时间复杂度,但是排序了之后利用二分查找大大降低了需要的时间。并且,大部分情况下二分查找可以解决的问题,用哈希表和二叉树都可以解决,但是会需要比较多的额外空间。所以100MB存储不下。数组除了存储数据本身之外,不需要额外存储其他信息,是最省空间的存储方式。

 

课后思考:

  • 如何编程实现“求一个数的平方根”?要求精确到小数点后 6 位。

https://blog.csdn.net/qq_34269988/article/details/97179601

  • 如果数据使用链表存储,二分查找的时间复杂就会变得很高,那查找的时间复杂度究竟是多少呢?如果你自己推导一下,你就会深刻地认识到,为何我们会选择用数组而不是链表来实现二分查找了。

假设链表长度为n,第一次查找的区域为[0,n/2),指针需要移动n/2次;第二次需要移动n/4次;直到第k次需要移动1次。总共指针移动次数(查找次数) = n/2 + n/4 + n/8 + ...+ 1。sum = n -1。时间复杂度为O(n)。

 

二分查找的四个变形问题:

  • 查找第一个值等于给定值的元素在数组中的位置
int search(int *a, int n, int value) {
	int low = 0, high = n - 1;
	while (low <= high) {
		int mid = low + (high - low) / 2;
		if (a[mid]>value) {
			high = mid - 1;
		}
		else if (a[mid]< value) {
			low = mid + 1;
		}
		else {
			if (mid == 0 || a[mid - 1] != value)	return mid;
			high = mid - 1;
		}
	}
	return -1;
}
  • 查找最后一个值等于给定值的元素在数组中的位置
int search(int *a, int n, int value) {
	int low = 0, high = n - 1;
	while (low <= high) {
		int mid = low + (high - low) / 2;
		if (a[mid]>value) {
			high = mid - 1;
		}
		else if (a[mid]< value) {
			low = mid + 1;
		}
		else {
			if (mid == n-1 || a[mid + 1] != value)	return mid;
			low = mid + 1;
		}
	}
	return -1;
}
  • 查找最后一个小于等于给定值的元素在数组中的位置
int search(int *a, int n, int value) {
	int low = 0, high = n - 1;
	while (low <= high) {
		int mid = low + (high - low) / 2;
		if (a[mid] >= value) {
			if (mid == 0 || a[mid - 1] < value)    return mid;
			high = mid - 1;
		}
		else {
			low = mid + 1;
		}
	}
	return -1;
}
  • 查找最后一个小于等于给定值的元素在数组中的位置 
int search(int *a, int n, int value) {
	int low = 0, high = n - 1;
	while (low <= high) {
		int mid = low + (high - low) / 2;
		if (a[mid] <= value) {
			if (mid == n - 1 || a[mid + 1] > value)    return mid;
			low = mid + 1;
		}
		else {
			high = mid - 1;
		}
	}
	return -1;
}

思考题: 如何快速定位IP对应的省份地址?

[202.102.133.0, 202.102.133.255]  山东东营市 
[202.102.135.0, 202.102.136.255]  山东烟台 
[202.102.156.34, 202.102.157.255] 山东青岛 
[202.102.48.0, 202.102.48.255] 江苏宿迁 
[202.102.49.15, 202.102.51.251] 江苏泰州 
[202.102.56.0, 202.102.56.255] 江苏连云港

将所有的ip区间的起始地址进行排序,然后根据二分查找,找到最后一个小于等于目标ip的起始ip,然后在这个区间内查找,如果在,我们就取出对应的归属地显示;如果不在,就返回未查找到。

总结

        凡是用二分查找能解决的,绝大部分我们更倾向于用散列表或者二叉查找树。即便是二分查找在内存使用上更节省,但是毕竟内存如此紧缺的情况并不多。那二分查找真的没什么用处了吗?实际上,上一节讲的求“值等于给定值”的二分查找确实不怎么会被用到,二分查找更适合用在“近似”查找问题,在这类问题上,二分查找的优势更加明显。比如今天讲的这几种变体问题,用其他数据结构,比如散列表、二叉树,就比较难实现了。

课后思考:如果有序数组是一个循环有序数组,比如 4,5,6,1,2,3。针对这种情况,如何实现一个求“值等于给定值”的二分查找算法呢?

先找到分界下标,然后在两个区间做二分查找。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值