带你直观地感受二分查找
写在前面
二分查找(Binary Search)被认为是极具效率的查找算法,其 O ( l o g n ) O(logn) O(logn) 的时间复杂度在大规模数据下具有极高的效率。比如,试想要从70亿人中找到1人,计算次数可以被降到32次。这使得二分查找成为了一个相当具有研究价值的算法。
本文就二分查找进行了少量优化,并利用可视化的软件包对实验结果进行直观展示。
本文的是以学习二分查找算法原理,直观呈现并感受其效率为目的撰写。
1. 二分查找
1.1 原理
二分查找的基本思想,是一种分治的思想。
二分查找的基本原理:
为一个有序可随机访问的抽象数据结构创建三个指针,高位指针、低位指针与中间指针。
具体来讲,以有序数组为例,我们创建三个整数,看作指针,分别称作高位、低位、中间位。
我们有一个目标数 t t t
每次将 t t t 与中间位进行比较,不断压缩查找范围。
这样就可以提高查找效率,并达到
O
(
l
o
g
n
)
O(logn)
O(logn) 的时间复杂度。
(图片来源:极客时间)
1.2 流程
2. 实验
2.0 实验环境
Anaconda 3.0
Ubuntu 18.04.4 LTS (GNU/Linux 4.15.0-101-generic x86_64)
JupyterLab
2.1 核心代码
'''
A: 输入数组
k: 目标数字
'''
def binary_search(A , k):
left = 0
right = len(A) - 1
find = False
while left<=right:
# 利用移位操作,提高运算效率
mid = int(left +((right - left)>>1))
if k < A[mid]:
right = mid -1
continue
elif k > A[mid]:
left = mid + 1
continue
else:
if mid==0 or A[mid-1]!=k:
return mid
else:
right = mid - 1
return -1
2.2 实验代码
- 导入软件包
from matplotlib import pyplot as plt
import numpy as np
import time as t
import pandas as pd
from scipy.optimize import curve_fit
import math
- 产生一个随机的指定大小的numpy数组
def generateArray(n):
A = np.random.randint(0,n,size=n)
A = np.sort(A,axis=-1,kind='quicksort',order=None)
return A
- 核心的实验代码,测试各个数量级的数据,每组100次
# 结果存入数组
size=100000000
log_size=int(math.log(size,10))
n_time=100
print(log_size)
rs = np.zeros([log_size,n_time])
i =1
m = 0
while i<size:
#i:数据规模
#确定一个目标值
k = np.random.randint(0,i)
print(i)
# j:测试次数(每个数据规模测100次,暂时)
for j in range(n_time):
A = generateArray(i)
# profile CPU time
t1 = float(t.time())
binary_search(A,k)
t2 = float(t.time())
delta_time = t2 - t1
rs[m][j]= delta_time
i=i * 10
m = m+1
2.3 绘图
问题
在绘图时,由于是采样实验,所绘制出的曲线可能会出现一定的波动,导致结果的展示不够清晰。
解决
导入 scipy 软件包,利用软件包的 scipy.optimize 组件的 curve_fit 方法
由于本实验的期望曲线呈对数类型,在利用curve_fit方法时,需要一些特殊拟合方法,具体请参考StackOverflow的回答或官方文档。
以下给出代码:
#对数拟合
def func(x,a,b):
print(x)
y = np.log(x)+b
return y
# draw the graph
x = np.arange(log_size)
y = rs
# 行:数据规模 列:实验次数
df = pd.DataFrame(y,index=x,columns=np.arange(n_time))
df["mean"] = df.mean(axis=1)
df['var'] = df.std(axis=1)
fig = plt.figure()
# x坐标
x =[1,1e1,1e2,1e3,1e4,1e5,1e6,1e7]
# y坐标是均值序列
y = df["mean"]
# popt返回拟合优化后的参数序列
popt, pcov = curve_fit(func,x,y)
# 这里放大了 标准差 使得图上的表达更加直观
plt.errorbar(x,func(x,popt[0],0)*1e-5,yerr=df["var"]*2,fmt='bo:',
label='proformance in differenct data sizes')
plt.legend()
结果
可以看到,运行时间的曲线,在不同数量级的数据规模下基本呈现类似对数函数的形状。
3. 结论
3.1 算法上的反思
二分查找本身虽然在查找效率上拥有极高的效率,但是也存在诸多局限:
- 需要以支持随机查找顺序表结构为基础。
- 需要有序的数据结构,因此在面对无序数据时,需要消耗时间进行排序的预处理。
- 数据量过小时,无法发挥其效能。
- 数据量过大时,由于其以顺序表的基础,需要连续的内存空间,导致二分查找在这个场景下不能很好地应用。
3.2 实验上的反思
本次实验的收获主要体现在如下几点:
首先,对二分查找的高效率有了直观清晰的理解;
其次,在代码实践中学会了数据参数的优化、拟合,errorbar曲线的绘制;
最后,想感谢程振波老师提供的此次机会,让本人能够以偏向于学术的方式研究一个算法的效能,从中学到的不仅是算法思想与技术。
更多的,是科学研究的方法论。
参考
《Design and Analysis of Algorithm Using Python》 程振波等著 清华大学出版社