二分查找——算法基础

文章介绍了二分查找算法的高效性和适用场景,通过与线性查找的对比,阐述了大O表示法在描述算法时间复杂度上的作用。在给定的例子中,二分查找在大规模数据查找上展现出显著的优势,其时间复杂度为O(log2n),而线性查找的时间复杂度为O(n)。同时,文章强调大O表示法描述的是最坏情况下的运行时间,为用户提供参考。
摘要由CSDN通过智能技术生成

二分查找是在执行检索时高效简单的算法,但是其是否在任何情形下都适用呢?本将简单介绍二分法,并以此引入一些算法的基本概念。

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+(n1)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(n1)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=1nan;  an=a1+(n1)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=10000000d=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=log210000000027
二者的操作次数是成几何级数的差异的! \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表示法表示的只是最坏的情况下所需要的运行时间,因为谁也不能保证用户在使用你编写的程序时始终遇不到最坏的情况,我们需要提供最坏的情况供用户参考。

以上就是全部内容,仅为个人理解,如有错误,还望在评论区指出

Ending!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Odd_guy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值