查找算法之二分查找(折半查找)
对于顺序表中已经排好序的表来说,除了上篇文章中的可以比较大小外,还可以有另外一种思路,就是二分查找。因为已经排好序了,可以直接比较中间值(中位数),如果中位数比要查找的数大,那么要查找的数可能在前半部分(至少不可能在后半部分),因为后半部分一定比中位数大,也一定比要查找的数大。继续,把前半部分当做一个新的有序表,进一步比较中位数,直到找到要查找的数或者不存在。
def binary_search(alist, item):
first = 0
last = len(alist) - 1
mid = 0
found = False
while first <= last and not found:
mid = (first + last) // 2
if alist[mid] == item:
found = True
elif alist[mid] < item: # 说明item一定在当前列表的后半部分,所以筛选范围向后移动
first = mid + 1
else:
last = mid - 1 # 一定在前半部分
if found:
return mid
else:
return False
# 注意列表一定是有序的。
testlist = [0,1,2,8,13,17,19,32,42]
print(binary_search(testlist, 32))
print(binary_search(testlist, 17))
当然也可以用递归的形式做,在前面叙述中可以看出来,每次分割后,都把要比较的子列表当做一个新的列表进一步比较,其实这就是递归的过程。因为递归每次的变量都会重复,这里如果要输出要查找元素的下标,需要注意。
# 二分查找的递归形式
def binary_search(alist, item):
first = 0
last = len(alist) - 1
if first <= last:
mid = (first + last) // 2
if alist[mid] == item:
# 注意返回的mid是此时的first和last的中间值
return mid
elif alist[mid] < item:
# 注意,因为本函数中每次都是初始化first和last
# 所以递归调用时,返回的miid是当前子列表的中间值
# 要想得到原始的中间值,需要起始位置
first = mid + 1
return first + binary_search(alist[first:], item)
else:
# 此时的开始的相对位置没有改变,改变的只是结束位置
# 而结束位置并不影响子列表的中间值的相对原来的位置
# 所以此处不需要加上起始位置first。
last = mid - 1
return binary_search(alist[:last+1], item)
else:
return False
testlist = [0,1,2,8,13,17,19,32,42]
print(binary_search(testlist, 13))
print(binary_search(testlist, 8))
接下来再看一下时间复杂度。每一次比较,那么剩下的还需要比较的就是n/2个元素(最坏的情况下),做第二次比较时,还剩下n/4个元素,也就是n/(2^i)。
如果还剩一个元素时,就只需要比较一次就知道结果了,所以n/(2^i)=1时就是最终结束时,可以看出来i=log(n),成对数形式的复杂度,比线性的复杂度要低一点,优化了一些。