Python3从入门到实战
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.8 实战

2.8.1 二分查找

1.顺序查找

若要查找一个序列中是否存在某个元素,一般可以采用遍历的方法查找。例如:

arr=[12,46,25,43,58,7,37,5,80,34,105,82]
v=int(input("输入你要查找的数"))
ok=False
for e in arr:
    if e==v:
      ok=True
      break
if ok:
    print(’查找成功!')
else:
    print(’查找失败!')

执行:

输入你要查找的数6
查找成功!

分析这个程序的时间效率。如果一个序列中数据元素的个数为n,且每个元素被查找的概率是均等的(1/n),在查找成功的情况下,第1、2、3、…、n元素的比较次数分别为1、2、3、…、n,因此,查找成功的情况下查找一个元素的平均比较次数是1(1/n)+2(1/n)+...+n(1/n)=(n+1)/2,即平均需要比较的次数是表长的一半。当n趋向于无穷大时,(n+1)/2和n在一个数量级,所以称其时间复杂度是O(n),即查找时间和n是同阶无穷大量。

假如n=1024,则成功查找其中的一个元素需要平均比较512次。但如果这个序列是有序的,那么可以使用“二分查找”的算法,平均只需要log2(1024)次,即10次就可以找到该元素。

2.二分查找

“二分查找”的思想很简单。例如,在一个有序序列alist中查找某个元素item,则可以让item首先和alist的中间元素比较:

● 如果相等,则成功;

● 如果小于中间元素,则在alist的左半区间查找;

● 如果大于等于中间元素,则在alist的右边半区间查找。

如图2-3所示,是在一个有序序列中查找25的过程。

图2-3 查找25的过程

以上查找过程是利用一个循环迭代过程,通过不断更新区间的左右位置L、H和中间位置Middle这3个指示器来查找。程序如下:

def binarySearch(alist, value):
    L=0                          #区间左端点
    H=len(alist)-1               #区间右端点
    found=False
    while L<=H:                    #区间(L, H)存在
      Middle=(L+H)//2           #Middle指向区间的中点
      if alist[Middle]==value: #等于Middle指向的元素,找到了
          return Middle
      else:
          if value < alist[Middle]:
              H=Middle-1         #在左区间查找
          else:
              L=Middle+1         #在右区间查找
    return -1

testlist=[5,7,12,25,34,37,43,46,58,80,82,105]
print(binarySearch(testlist, 25))
print(binarySearch(testlist, 13))

输出:

3
-1

2.8.2 冒泡排序和简单选择排序

1.冒泡排序

冒泡排序的思想是对相邻元素比较大小,如果逆序就交换它们。冒泡排序如图2-4所示,对于一个序列,通过这种两两相邻元素的比较与交换,可以将最大值(或最小值)放在最后一个位置,这一过程称为“一趟冒泡”。“一趟冒泡”只是在一个序列中选出了一个最大值(或最小值)并放在了其最终位置。对于剩余元素构成的序列,再进行“一趟冒泡”,又可以在剩余元素序列中选出一个最大值(或最小值)并放在剩余元素序列的最终位置(如倒数第二个位置)。重复这个过程,直到剩余一个元素位置。对于n个元素的序列,需要进行n-1趟冒泡。程序如下:

图2-4 冒泡排序

alist=[49,38,27,97,76,13,27,49]
debug=True

for i in range(len(alist)-1,0, -1):
      for j in range(i):
          if alist[j]>alist[j+1]:
              #temp=alist[j]
              #alist[j]=alist[j+1]
              #alist[j+1]=temp
              alist[j], alist[j+1]=alist[j+1], alist[j]  #交换两个元素
      if debug:
          print(alist)
print(alist)

输出:

[38, 27, 49, 76, 13, 27, 49, 97]
[27, 38, 49, 13, 27, 49, 76, 97]
[27, 38, 13, 27, 49, 49, 76, 97]
[27, 13, 27, 38, 49, 49, 76, 97]
[13, 27, 27, 38, 49, 49, 76, 97]
[13, 27, 27, 38, 49, 49, 76, 97]
[13, 27, 27, 38, 49, 49, 76, 97]
[13, 27, 27, 38, 49, 49, 76, 97]

2.简单选择排序

简单选择排序的思想是:在整个序列中选出一个最值(如最小值)放在序列的开头(或结束)位置。这个过程称为“一趟选择”。对于剩余元素构成的序列重复这个过程,又选出一个最值放在剩余元素序列的开头(或结束)位置,即“第二趟选择”。这个过程一直进行下去,直到剩余序列只有一个元素。请读者根据这个思想写出简单选择排序的程序。

2.8.3 Floyd最短路径算法

1.最短(最佳)路径问题

最短路径问题是日常生活和科学研究中广泛应用的问题。例如,一个人在一个陌生城市,要从某一个地点前往另外一个地点,此时他会打开手机中的导航软件,导航软件会按照不同代价(时间、路程、花费)给出不同的最佳(最短)路径。再如,计算机网络通信需要在不同计算机之间发送数据包,网络路由算法会采用最短路径算法计算出最佳的数据包发送路径。

如图2-5(a)所示的一个城市公路图。其中,顶点表示城市,而边表示城市之间的公路及其距离,现在要求出任何两个城市之间的最短路径。最短路径属于图论的一个基本问题,针对这个问题有很多最短路径算法,其中的Floyd算法可以求出任意两个顶点之间的最短路径。

图2-5 城市公路图及其邻接矩阵

2.Floyd算法

Floyd算法的基本思想是:用一个二维矩阵(如图2-5(b)所示)表示任意两个顶点之间的距离,初始时,这个矩阵的数据元素的值表示的是两个城市的直达距离,值是无穷大∞则表示没有直达距离。

直达距离不一定是最短距离(顶点0到顶点2的直达距离6不是顶点0到顶点2的最短距离),因此,Floyd算法每次考虑绕道一个顶点,查看是否有两个顶点的距离会因为绕道这个顶点变得更近。

假如当前的距离矩阵为D,现在绕道顶点w,查看顶点u到顶点v之间的距离D[u][v]会不会因为绕道顶点w变得更短。如果更短,则更新D[u][v]=D[u][w]+ D[w][v],即:

if   D[u][w]+ D[w][v] < D[u][v]:
      D[u][v]= D[u][w]+ D[w][v]

对这个绕道顶点w,检查所有其他的顶点u、v是否因为绕道w而变得更短,从而更新距离矩阵D。

不断重复绕道不同的顶点,并更新这个距离矩阵D,直到所有顶点都绕道完为止,得到的最终矩阵就是这两个顶点的最短距离。

为了记录路径,需要用一个和D一样大小的二维矩阵P记录任意两个顶点之间的当前距离对应的路径上的倒数第二个顶点(路径上终点之前的那个顶点)。例如:

if  D[u][w]+ D[w][v] < D[u][v] :
    D[u][v]= D[u][w]+ D[w][v]
    P[u][v]= P[w][v]       #P[u][v]= P[u][w]+ P[w][v]

下面是Floyd算法的Python代码实现:

n=4
INFINITY=100000.0                 #假设这是一个很大的数值

D=[[0,2,6,4],                       #距离矩阵
  [INFINITY,0,3, INFINITY],
  [7, INFINITY,0,1],
  [5, INFINITY,12,0]]

P=[[0,0,0,0], [0,0,0,0], [0,0,0,0], [0,0,0,0]]   #路径矩阵
//初始化路径矩阵,即P[u][v]记录直达路径uv的终点v前面的顶点u
for u in range(0, n):
    for v in range(0, n):
      P[u][v]=u

print(*D, sep="\n")     #*D是"解封参数列表"传递参数方式
                          #sep="\n"表示每个输出元素之间的分割符是换行符"\n"
print()
print(*P, sep="\n")
print()

for w in range(0, n):                                #绕道顶点w
    for u in range(0, n):
      for v in range(0, n):
          #其他顶点u、v会不会因为绕道w距离变得更短呢?
          if w!=u and w!=v and D[u][w] + D[w][v] < D[u][v] :
              D[u][v]=D[u][w] + D[w][v]
              P[u][v]=P[w][v]

print(*D, sep="\n")
print()
print(*P, sep="\n")

输出结果:

[0, 2, 6, 4]
[100000.0, 0, 3, 100000.0]
[7, 100000.0, 0, 1]
[5, 100000.0, 12, 0]

[0, 0, 0, 0]
[1, 1, 1, 1]
[2, 2, 2, 2]
[3, 3, 3, 3]

[0, 2, 5, 4]
[9, 0, 3, 4]
[6, 8, 0, 1]
[5, 7, 10, 0]

[0, 0, 1, 0]
[3, 1, 1, 2]
[3, 0, 2, 2]
[3, 0, 1, 3]

根据路径矩阵P,对于任何一对顶点u、顶点v,其路径可以从终点倒过来追踪到起点,即终点是v,其前一个顶点是P[u][v],再前一个顶点是P[u][ P[u][v] ]、...

for u in range(0, n):
    for v in range(0, n):
      if u==v continue
      print(u, ’到’, v, ’的逆向路径是:', end=' ')
      print(v, end=', ')
      w=P[u][v]
      while w!=u:
          print(w, end=', ')
          w=P[u][w]
      print(u)

输出结果:

0到1的逆向路径是:1,0
0到2的逆向路径是:2,1,0
0到3的逆向路径是:3,0
1到0的逆向路径是:0,3,2,1
1到2的逆向路径是:2,1
1到3的逆向路径是:3,2,1
2到0的逆向路径是:0,3,2
2到1的逆向路径是:1,0,3,2
2到3的逆向路径是:3,2
3到0的逆向路径是:0,3
3到1的逆向路径是:1,0,3
3到2的逆向路径是:2,1,0,3