二分查找
是在执行检索时高效简单的算法,但是其是否在任何情形下都适用呢?本将简单介绍二分法,并以此引入一些算法的基本概念。
1 算法
算法
即一组完成任务的指令。
1.1例子
举个例子:
对公差为
d
d
d的等差数列
a
n
a_n
an有:
a
n
=
a
1
+
(
n
−
1
)
d
a_n=a_1+(n-1)d
an=a1+(n−1)d
换一种表示方法:
a
n
∈
{
a
1
,
a
2
,
…
…
,
a
n
}
a_n\in \lbrace a_1,a_2,……,a_n \rbrace
an∈{a1,a2,……,an}
对于数列的前
n
n
n项的和有:
S
n
=
n
a
1
+
n
(
n
−
1
)
⋅
d
2
S_{n}=na_1+\frac{n(n-1)\cdot d}{2}
Sn=na1+2n(n−1)⋅d
同样也有第二种表示形式:
S
n
=
∑
i
=
1
n
a
n
;
a
n
=
a
1
+
(
n
−
1
)
d
S_{n}=\sum_{i=1}^{n}a_n;~~a_n=a_1+(n-1)d
Sn=i=1∑nan; an=a1+(n−1)d
这时,对于
S
n
S_n
Sn的计算,我们就有了两种算法:
a
n
a_n
an计算
+
S
n
+S_n
+Sn求和;求和公式
S
n
S_n
Sn求和。
假设我们要计算的
S
n
S_n
Sn中
n
=
10000000
,
d
=
2
,
a
1
=
1
n=10000000,d=2,a_1=1
n=10000000,d=2,a1=1,使用python
实现即为:
(1) a n a_n an计算 + S n +S_n +Sn求和:
a1 = 1
d = 2
n = 10000001 #此处n为索引,需排除0索引
an = []
for n in range(1,n):
a_n = a1+(n-1)*2
an.append(a_n)
Sn = sum(an)
print("Sn:\t",Sn)
##结果:
# Sn: 100000000000000
# [Finished in 2.7s]
(2)求和公式 S n S_n Sn求和:
a1 = 1
d = 2
n = 10000000
Sn = n*a1 + 1/2 * (n*(n - 1)*d)
print("Sn:\t",Sn)
##结果:
# Sn: 100000000000000
# [Finished in 80ms]
可以看出,同样的结果,程序的运行时间却天差地别,是什么导致了这样的结果呢?这时就需要引入算法的一个重要概念:操作数
。
1.2 操作数与大O运行时间
上述的例子比较极端,算法(1)每次进行的操作数为 n + 1 n+1 n+1次,时间复杂度为 O n O_{n} On,而算法(2)的操作数与数列长度 n n n无关,仅为1次,时间复杂度为 O 1 O_{1} O1。当数列长度为1时,二者的运行时间几乎一致。
a1 = 1
d = 2
n = 1
Sn = n*a1 + 1/2 * (n*(n - 1)*d)
print("Sn:\t",Sn)
##结果:
# Sn: 1.0
# [Finished in 91ms]
a1 = 1
d = 2
n = 2 #此处n为索引,需排除0索引
an = []
for n in range(1,n):
a_n = a1+(n-1)*2
an.append(a_n)
Sn = sum(an)
print("Sn:\t",Sn)
##结果:
# Sn: 1
# [Finished in 81ms]
造成这一结果的原因是二者的运行时间增速
不同,那么在此如何表示运行时间的增速呢?大O表示法
很好的解决了之一问题。
“O”即“operate”,其表示方法为:
O
(
n
)
,
n
=
操作次数
O_{(n)},n=操作次数
O(n),n=操作次数
对于上述两个算法,算法(1)的运行时间可表示为
O
(
n
)
O_{(n)}
O(n),即线性时间
,程序的运行时间与参数的大小呈线性关系;而后者可表示为
O
1
O_{1}
O1,即常量时间
,不管参数如何,程序运行的次数始终为常数。
2 二分查找
2.1 算法python实现
介绍了一些基本概念,我们就可以开始二分法查找
算法的学习了。
还是利用上文提及的数列为例:假设我们需要在数列的前10000项中找到特定大小的项对应的
n
n
n值。
线性查找
通过逐个对比数组(集)中的元素,知道找到目标元素为止:
def basic_search(an, item):
for i in range(0,len(an)):
if an[i] == item:
return i+1
else:
pass
二分查找
二分查找
通过直接将目标元素的值与数组的中间值进行比对,并判断其与中间值的大小关系,以此来排除50%的非目标元素,以此类推直到找到目标元素为止:
def binary_search(an, item):
low = 0
high = len(an)-1
while low <= high:
mid = int((low + high)/2)
guess = an[mid]
if guess == item:
return mid+1
if guess > item:
high = mid - 1
else:
low = mid + 1
pass
计算运行时间:
import time #计算时间的模块
# 生成数列
a1 = 1
d = 2
n = 100000001 #此处n为索引,需排除0索引
an = []
for n in range(1,n):
a_n = a1+(n-1)*2
an.append(a_n)
# 线性查找
ba_start_time = time.time()
ba= basic_search(an, 199999999)
ba_end_time = time.time()
ba_time = ba_end_time - ba_start_time
print("basic search:\t", ba,"\n\t\ttime:\t",ba_time,"s")
# 二分查找
bi_start_time = time.time()
bi = binary_search(an, 199999999)
bi_end_time = time.time()
bi_time = bi_end_time - bi_start_time
print("binary search:\t", bi,"\n\t\ttime:\t",bi_time,"s")
程序运行结果:
basic search: 100000000
time: 4.624472379684448 s
binary search: 100000000
time: 0.0 s
[Finished in 27.7s]
可以看出二分查找
对于线性查找的速度优势是成几何数量级的,现在我们来计算其运行时间,并以大O表示法
进行表示:
{
线性查找:
O
(
n
)
,即线性时间,操作次数与数列的长度成正比;
二分查找:
O
(
l
o
g
2
n
)
,
即对数时间,操作次数与数列的长度成对数关系。
\begin{cases} 线性查找:O_{(n)},即线性时间,操作次数与数列的长度成正比;\\ 二分查找:O_{(log_{2}n)},即对数时间,操作次数与数列的长度成对数关系。 \end{cases}
{线性查找:O(n),即线性时间,操作次数与数列的长度成正比;二分查找:O(log2n),即对数时间,操作次数与数列的长度成对数关系。
通过计算可以得出上述两种算法所需得操作次数:
{
线性查找:
n
O
l
=
100000000
二分查找:
n
O
b
=
l
o
g
2
100000000
≈
27
\begin{cases} 线性查找:n_{Ol}=100000000\\ 二分查找:n_{Ob}=log_{2}100000000\approx27 \end{cases}
{线性查找:nOl=100000000二分查找:nOb=log2100000000≈27
二者的操作次数是成几何级数的差异的!
\color{red}{二者的操作次数是成几何级数的差异的!}
二者的操作次数是成几何级数的差异的!
2.2 大O表示法
只是指出了最坏的情景
通过观察代码不难发现,线性查找是通过按序遍历整个数组(或列表)来找到目标元素的,那么如果我们的目标元素位于数组比较靠前的位置会发生什么呢?
ba_start_time = time.time()
ba= basic_search(an, 1999)
ba_end_time = time.time()
ba_time = ba_end_time - ba_start_time
print("basic search:\t", ba,"\n\t\ttime:\t",ba_time,"s")
bi_start_time = time.time()
bi = binary_search(an, 1999)
bi_end_time = time.time()
bi_time = bi_end_time - bi_start_time
print("binary search:\t", bi,"\n\t\ttime:\t",bi_time,"s")
结果:
basic search: 1000
time: 0.0 s
binary search: 1000
time: 0.0 s
是的,线性查找的操作次数变少了,这是否说明我们之前表示的运行时间是错误或者有歧义的呢,需要指出的是:大O表示法
表示的只是最坏的情况下所需要的运行时间,因为谁也不能保证用户在使用你编写的程序时始终遇不到最坏的情况,我们需要提供最坏的情况供用户参考。
以上就是全部内容,仅为个人理解,如有错误,还望在评论区指出