vlambda博客
学习文章列表

​查找与排序:选择排序、二分查找

选择排序

还有一种简单的排序,叫选择排序。思想很简单,就是第一趟从N个数据中选最小的那个放到序列第一位,第二趟从剩下的N-1个数据中选最小的那个放到序列第二位,一直循环。
有了冒泡的基础,选择排序很类似,不详细解释了,直接上代码:

def selectionsort(arr):
    for i in range(len(arr)-1): #对每一个元素
        min_index = i
        for j in range(i + 1, len(arr)): #后面的元素逐个比较,找到最小的元素
            if arr[j] < arr[min_index]:
                min_index = j
        # 把最小交换到前面位置
        arr[min_index], arr[i] = arr[i], arr[min_index]
    return arr

测试一下:

print(selectionsort([5,2,9,15,3,5,11,17,18,10]))

运行结果:

[235591011151718]

它的性能也是比较不好的,跟冒泡排序一样,都是O(N2)。

二分查找(做一个字典)

现在看看再一个字典里面怎么查找单词。
想都不用想,如果字典是杂乱无章的,那就只能从头到尾一个一个找了。所以我这里说的字典其实都是有序的,怎么排序,上面试过最傻的方式,后面会再介绍两种好一点的方式。有了这些铺垫,我们的问题明确成了:在一个有序的列表中查找数据。
实际的字典不管是纸质的还是在线的,都是有序的,我们找一个单词,不会从头到尾找的。好多字典的侧面都会印出首字母来,所以我们一般是先找单词的首字母,这样就一下子把范围缩小很多了,极大地提高了查找效率。
如果没有首字母的标记,我们会怎么找呢?MIT麻省理工学院有一个教授讲了一堂公开课,他拿出一本厚厚的字典,让一个学生上讲台这么找单词:不管什么单词,先翻到字典的中间那一页,然后看这一页的第一个单词,跟要查找的单词比大小,如果小,说明要查找的单词在中间那一页的后面,否则就在前面,于是教授让学生当场把字典从中间那一页撕开成两半,只留下所在的那一半,然后继续这个过程,每次撕开留一半丢一半,直到最后找到这个单词或者压根找不到。
教授其实在讲一个算法,叫做二分查找,这是用得很多的一个算法,思路很简单也很好用。我们先上程序:

word = input("Search word: ")
engwords=["abandon","abbreviation","abeyance","abide","ability","abnormal"]
chiwords=["放弃","缩写","缓办","遵守","能力","反常的"]

L=len(engwords)
bottom=0
up=L-1
pos=(bottom+up)//2
found=0

while up>=bottom:
    if engwords[pos]==word:
        found=1
        break;
    elif engwords[pos]>word:
        up=pos-1
        pos=(bottom+up)//2
    elif engwords[pos]<word:
        bottom=pos+1
        pos=(bottom+up)//2

if found==1:
    print ("pos:"pos)
    print (chiwords[pos])
else:
    print ("noipe")

首先注意的是字典在程序里面是如何表示的,我们这里用了两个列表:engwords和chiwords,按照位置一一对应,这个表示法跟我们习惯的不一样,一般习惯“word:单词”这种写在一起的排列,我们先不管,后面再调整成这个格式。还要注意的是,这个字典已经是排好序的,不排好序,这个算法是不工作的。
然后我们用了三个位置变量来记录特殊位置,bottom记录最小的位置,up记录最大的位置,pos记录中间的位置。初始的时候,bottom=0,up=5,pos=(bottom+up)//2=2,我们图示字典及位置如下:

abandon

abbreviation

abeyance

abide

ability

abnormal

0

1

2

3

4

5

bottom


pos



up

程序的核心思想就是每次按照pos分两半,然后定位所在的那一半,继续细分,直到找到或者up的位置小于bottom位置了(说明没有找到)为止。
假定我们要查找的单词是abide,这时engwords[pos]为abeyance,小于abide,那程序会执行下面的语句:

    elif engwords[pos]<word:
        bottom=pos+1
        pos=(bottom+up)//2

三个位置变化为bottom=3,pos=4,up=5,通过这个方式实现了把查找范围限定在后面一半。图示如下:

abandon

abbreviation

abeyance

abide

ability

abnormal

0

1

2

3

4

5




bottom

pos

up

然后继续比对,这时engwords[pos]为ability,大于abide,那程序会执行下面的语句:
   elif engwords[pos]>word:
       up=pos-1
       pos=(bottom+up)//2
三个位置变化为bottom=3,pos=3,up=3,通过这个方式实现了把查找范围限定在前面一半。图示如下:

abandon

abbreviation

abeyance

abide

ability

abnormal

0

1

2

3

4

5




bottom/pos/up



程序继续循环,这次engwords[pos]=word了,找到了。
假设字典的这个位置不是abide而是abike,一比对发现engwords[pos]>word,会出现什么情况呢?我们跟踪一下,这个时候执行的是

    elif engwords[pos]>word:
        up=pos-1
        pos=(bottom+up)//2

那么up=pos-1=2,pos=2,bottom=3,再次循环的时候就不满足循环条件while up>=bottom了。程序跳出。
再假设字典的这个位置不是abide而是abibe,一比对发现engwords[pos]<word,会出现什么情况呢?我们跟踪一下,这个时候执行的是

    elif engwords[pos]<word:
        bottom=pos+1
        pos=(bottom+up)//2

那么up=3,pos=3,bottom=4,再次循环的时候就不满足循环条件while up>=bottom了。程序跳出。
总之,通过每次缩减一半,我们就能很快找到这个单词。我们初步估计一下,如果一本字典由1000单词,要找多少次呢?
第一次,范围缩减到500;
第二次,范围缩减到250;
第三次,范围缩减到125;
第四次,范围缩减到63;
第五次,范围缩减到32;
第六次,范围缩减到16;
第七次,范围缩减到8;
第八次,范围缩减到4;
第九次,范围缩减到2;
第十次,范围缩减到1;
也就是说,即使每次都比对不上,也只要十次。我们仔细看上面的几个数,很眼熟,1,2,4,8,16,32,64…不就是2n吗?对,反过来log2(n),这就是结论:二分查找的效率是O(log2(n))。效率真的高啊,一百万条词条也只要找20次。这就是算法的力量。

我们刚才提到了,程序里面用的数据格式不怎么习惯,我们还是觉得中英文单词写在一起比较直观,那我们就接着来试一下。我们用这个格式:
words=["abandon:放弃","abbreviation:缩写","abeyance:缓办","abide:遵守"]
这样看起来舒服多了。但是这样我们就要多处理一些问题,首先是能把“:”前后的英文中文拆得开。这个任务比较简单,我们用Python提供得split()就可以了,代码如下:

def getEng (word):
    all = word.split(":")
    eng = all[0]
    chn = all[1]
    return eng

def getChn (word):
    all = word.split(":")
    eng = all[0]
    chn = all[1]
    return chn

两段代码差不多的,把单词split开,然后分别取前后两段。
有了这个基础,我们改写一下上面的二分查找程序:

words=["abandon:放弃","abbreviation:缩写","abeyance:缓办","abide:遵守","ability:能力","abnormal:反常的"]
word=input("enter word : "

bottom=0
up=len(words)-1
pos=(bottom+up)//2
found=0

while up>=bottom:
    if getEng(words[pos])==word:
        found=1
        break
    elif getEng(words[pos])>word:
        up = pos-1
        pos=(bottom+up)//2
    elif getEng(words[pos])<word:
        bottom=pos+1
        pos=(bottom+up)//2

if found==1:
    print ("pos:",pos)
    print (getChn(words[pos]))
else:
    print ("noipe")

程序基本一样的,只是比对的时候拆分一下,拿英文部分进行比对。
但是我们到现在对这个程序肯定是不满意的,因为把字典写死在程序里面了,如果有几千个单词,程序满版都是词汇了。我们应该想办法把字典数据放在外部文件中,需要的时候调用它。用上面我们提到过的Python的文件操作就可以了。
我们建一个字典文件dict.txt:
abandon      v.抛弃,放弃
abandonment      n.放弃
abbreviation      n.缩写
abeyance      n.缓办,中止
abide      v.遵守
ability      n.能力
able      adj.有能力的,能干的
abnormal      adj.反常的,变态的
… …
到此我们可以修改二分查找程序了:

f = open("dict.txt","r",encoding="utf-8")   #设置文件对象
words=f.read().splitlines()
f.close()

word=input("enter word : "

bottom=0
up=len(words)-1
pos=(bottom+up)//2
found=0

while up>=bottom:
    if getEng(words[pos])==word:
        found=1
        break
    elif getEng(words[pos])>word:
        up = pos-1
        pos=(bottom+up)//2
    elif getEng(words[pos])<word:
        bottom=pos+1
        pos=(bottom+up)//2

if found==1:
    print ("pos:",pos)
    print (getChn(words[pos]))
else:
    print ("noipe")

点击“阅读原文”,可进入原创书籍,阅读目录