最近开始学习王争老师的《数据结构与算法之美》,通过总结再加上自己的思考的形式记录这门课程,文章主要作为学习历程的记录。
课前问题:假设有1000万个整数数据,每个数据占8个字节,如何设计数据结构与算法,快速判断某个整数是否出现在这1000万数据中,同时又希望这个功能内存不要超过100M?
这个问题就引入了二分查找,举个例子来理解二分查找:假设一个数组为0-99,要查找元素23在数组中的位置
次数 | 猜测范围 | 中间数 | 对比大小 |
---|---|---|---|
1 | 0-99 | 49 | 49>23 |
2 | 0-48 | 24 | 24>23 |
3 | 0-23 | 11 | 11<23 |
4 | 12-23 | 17 | 17<23 |
5 | 18-23 | 20 | 20<23 |
6 | 21-23 | 22 | 22<23 |
7 | 23 | √ |
利用二分思想,每次都与区间中间数据比对大小,缩小查找区间的范围,二分查找针对的是一个有序的数据集合,查找思想有点类似分治思想,每次都通过跟区间的中间元素对比,将待查找的区间缩小为原来的一半,直到找到要查找的元素,或者区间被缩小为0。
二分查找查找效率非常高。假设数据大小为n,每次查找后数据都会缩小为原来的一半,也就是会除以2,最坏的情况下,直到查找区间被缩小为空才停止。
被查找区间的大小变化:
n n n , n / 2 n/2 n/2 , n / 4 n/4 n/4 , n / 8 n/8 n/8 , … , n / 2 k n/2^k n/2k 当 n / 2 k n/2^k n/2k=1时,k的值就是缩小的次数。
时间复杂度为O(k) = O( l o g 2 n log_2{n} log2n),故时间复杂度为O(logn)
最简单的二分查找就是有序数组中不存在重复元素,代码:
def binary_search(l,n):
low = 0
high = len(l)-1
while(low<=high):
mid = low+((high-low)>>1)
if n == l[mid]:
return mid
elif nl[mid]:
low = mid+1
下面有几点需要注意:
1.循环退出条件:
注意是low<=high,而不是low
2.mid的取值:
mid = (low+high)/2的写法是有问题的,low+high可能会发生溢出,改进的方法是将mid的计算方式写出low+(high-low)/2.需要进一步优化的话,可将除以2的操作转化为位运算,即low+((high-low)>>1),速度要快得多。
3.low和high的更新
low = mid+1, high = mid -1,如果直接写成low = mid或者high = mid就会发生死循环。
实际上,二分查找除了可以用循环,还可以采用递归实现。
def binary_search(l,n,low,high):
while(high>=low):
mid = ((high-low)>>1)+low
if l[mid]==n:
return mid
elif l[mid]>n:
return binary_search(l,n,low,mid-1)
elif l[mid]
二分查找不适合用于链表,主要原因在于二分查找算法需要按照下标随机访问元素。数组按照下标随机访问数据的时间复杂度为O(1),而链表是O(n)。因此数据使用链表存储的话,二分查找时间复杂度就会很高。
二分查找要求数据必须是有序的。排序的时间复杂度最低是O(nlogn)。因此,针对的是一组静态的数据,没有频繁地插入、删除,我们可以进行一次排序,多次二次查找,这样排序的成本可以被均摊,二次查找的边际成本就会比较低。
数据量很小,直接进行顺序遍历。只有数据量大时,二分查找的优势才比较明显。但若是数据之间的比较操作非常耗时,不管数据量大小,都更推荐使用二分查找。
二分查找的底层需要依赖数组,而数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻。因此太大的数据用数组存储就比较吃力,不能用二分查找。
补充一个思考题,编程实现“求一个数的平方根,保留6位有效小数”
def sqrt(t):
low = 0
high = t
mid = t/2
while(abs(t-mid**2)>0.00001):
if mid**2>t:
high = mid
elif mid**2
一、查找第一个值等于给定值的元素
这个问题的难度在于当查找到对应的值时,不一定是第一个值,需要进行判断。若mid=0,则元素已经是第一个值了,即我们查找的。若mid不等于0,但s[mid]的前一个元素s[mid-1]不等于t,那么s[mid]就是我们要找的第一个等于给定值的元素。如果发生s[mid-1]也等于t,则令high = mid - 1,因为要找的元素肯定在[low,mid-1]中。
def bin_find(s,t): #查找第一个等于给定值的元素
low = 0
high = len(l)-1
while(high>=low):
mid = low + ((high-low)>>1)
if s[mid]t:
high = mid-1
elif s[mid]==t:
if mid==0 or s[mid-1]!=t:
return mid
else:
high = mid-1
二、查找最后一个等于给定值的元素
与变体一类似
def bin_find(s,t): #查找最后一个等于给定值的元素
low = 0
high = len(l)-1
while(high>=low):
mid = low + ((high-low)>>1)
if s[mid]t:
high = mid-1
elif s[mid]==t:
if mid==0 or s[mid+1]!=t:
return mid
else:
low = mid+1
三、查找第一个大于等于给定值的元素
难度在于s[mid]>=t。我们要首先看一下这个s[mid]是不是我们要找的第一个大于等于给定值的元素。如果s[mid]前面无元素或前一个元素小于要查找的t,那s[mid]即为要找的元素。如果s[mid-1]也大于等于要找的t,说明要查找的元素在[low,mid-1]之间,所以要将high更新为mid-1.
def bin_find(s,t): #查找第一个大于等于给定值的元素
low = 0
high = len(l)-1
while(high>=low):
mid = low + ((high-low)>>1)
if s[mid]=t:
if mid==0 or s[mid-1]
四、查找最后一个小于等于给定值的元素
与变体三类似
def bin_find(s,t): #查找最后一个小于等于给定值的元素
low = 0
high = len(l)-1
while(high>=low):
mid = low + ((high-low)>>1)
if s[mid]<=t:
if mid == 0 or s[mid+1]>t:
return mid
else:
low = mid+1
elif s[mid]>t:
high = mid-1
参考资料:王争《数据结构与算法之美》