八种基本排序算法和Python实现-----未完!!!!!!!!

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiaozhu_you/article/details/89536739

                                 数据结构之排序及python实现(第九章)


排序是数据处理中经常使用的一种重要运算。排序就是将文件中的记录进行整理,使之按照关键字进行递增或递减的顺序排列起来。排序的定义如下:

假设含有n个记录的序列为

                                         {R1, R2, ..., Rn}

其相应的关键字的顺序为

                                        {K1, K2, ..., Kn}

需确定1,2,...,n的一种排列p1,p2, ...,pn,使得其相应的关键字满足以下的非递增(或非递减)的关系

                                        Kp1 <= Kp2 <= ... <=Kpn

即n个记录的序列成为一个按关键字有序的序列

                                        {Rp1, Rp2, ..., Rpn}

需要注意的是,用来进行排序依据的关键字,可以是数字类型,也可以是字符类型,关键字的选择需要根据问题的具体要求而定。

当待排序记录的关键均不相同时,排序的结果是唯一的,否则排序结果不唯一。在排序过程中,若整个文件都是放在内存中处理,排序时不涉及数据的内、外存交换,则称为内部排序,简称为内排序;反之,若排序过程中要进行数据的内、外存交换,则称为外部排序,简称外排序。

在计算机及其应用系统中,排序上所花费的时间在系统运行时间中所占比例较大;并且排序本身对推动算法分析的发展也起着很大作用。

 

                                                                 9.1   插 入 排 序

顾名思义,插入排序算法就是将记录插入到适当的位置,当所有记录都插入到适当的位置时,记录就排好序了。根据选择位置的策略不同,插入排序又分为直接插入排序、2路插入排序和希尔排序。

9.1.1 直接插入排序

直接插入排序(straight  insertion  sort)是最简单的排序方法之一,它的基本操作是将一个记录插入到有序表中的恰当位置,从而得到一个记录增 1 的有序表。对于第一个记录,直接放置就可,从第二个记录开始执行其基本操作,直到所有记录都插入到有序表中,则排序结束。

将数组 record 中的记录进行从小到大的顺序排序。对于前两个元素,即 record[0]和record[1]。如果它们的次序颠倒,则交换,得到一个有序表,比如,{record[1], record[0]}。对于record[2],则需要比较 record[2]是否小于record[1],若小于则将 record[2]插入到 record[1]的前面,得到有序表 {record[2], record[1], record[0]},否则需要比较 record[2]是否小于record[0]。对后续记录用同样的方法插入到有序表中,从而完成排序。

例如,已有有序表{1, 3, 7, 13, 16},需要插入的数为9,从左到右寻找第一个比9大的数13,将13以及后面的记录依次后移一位,将9插入到空位;或者从右到左寻找第一个比9小的数7,将后面的数依次后移一位,将9插入到空位,得到有序表{1, 3, 7, 9, 13, 16}。在寻找插入位置时,为了防止数组下标越界,不让数组的第一个位置 record[0]存放有序表的记录,应设置监视哨。一趟直接插入排序的算法如程序清单 9-1 所示。

程序清单 9-1     一趟直接插入排序的算法

"""
一 9.1 一趟直接插入排序算法
lis_old为有序表,i为待插入lis_old的记录,length=len(lis_old) - 1 ,lis_new为长度为len(lis_old) + 1,值为任意元素的列表
"""
def straipass(lis_old, i, lis_new, length):

    lis_new[0] = i  #设置监视哨    备注:完整算法中并没有用到
    #从右到左查找第一个比i小的位置
    while i < lis_old[length]:
        if length==len(lis_old) -1:  #为了严密性,一趟算法才多了if else 的if子句。其实完整的算法无需此if子句
            lis_old.insert(len(lis_old), i)
        else:
            lis_new[length +1] = lis_old[length]
        length-=1
    lis_new[length +1] = i  #将i插入到合适的位置。注释:此时的lis_old的length位置元素 是从右往左比较的 一个比i小
#测试
lis_old =[1, 3, 7, 13, 16]
i = 9
length=len(lis_old) - 1
lis_new=[None for j in range(len(lis_old) + 1)]
straipass(lis_old, i, lis_new, length)
print('lis_new---------:',lis_new)

#结果:
#lis_new---------: [9, None, None, 9, 13, 16]

整个排序过程为 n-1 趟插入,第一个记录可以看作是一个有序表的子序列,所以从第二个记录开始执行插入,直到第n个记录被插入到有序表中。

在进行插入排序时不需要将有序表用新的数组来存放,可以复用原来存放待排序记录的数组。算法如程序清单 9-2 所示。

程序清单 9-2   直接插入排序的算法

#一 9.2 直接插入排序算法        备注:因为待排序列表是数值型列表,所以每趟的关键字即是每趟要插入的元素
#把第一个记录先看成是有序子序列,然后从第二个记录开始,逐次与前面子序列中的所有记录比较
def straight_sort(lis):
    counts=len(lis)
    for i in range(1, counts):
        key=lis[i]
        j=i-1
        while key < lis[j]:
            lis[j+1]=lis[j]
            j-=1
            if j < 0:
                break
        lis[j+1]=key

        print('第{i}趟关键字:{key}\t\t{lis}'.format(i=i, key=key ,lis=lis))

#测试
lis=[4, 3, 1, 19, 7, 12, 13, 16]
print('原列表lis---------:',lis)
straight_sort(lis)
print('排序结束lis--------:',lis)

#结果
'''
原列表lis--------------: [4, 3, 1, 19, 7, 12, 13, 16]
第1趟关键字:3		[3, 4, 1, 19, 7, 12, 13, 16]
第2趟关键字:1		[1, 3, 4, 19, 7, 12, 13, 16]
第3趟关键字:19		[1, 3, 4, 19, 7, 12, 13, 16]
第4趟关键字:7		[1, 3, 4, 7, 19, 12, 13, 16]
第5趟关键字:12		[1, 3, 4, 7, 12, 19, 13, 16]
第6趟关键字:13		[1, 3, 4, 7, 12, 13, 19, 16]
第7趟关键字:16		[1, 3, 4, 7, 12, 13, 16, 19]
排序结束lis------------: [1, 3, 4, 7, 12, 13, 16, 19]
'''

或者

直接插入排序算法:

#一趟直接插入排序算法:
def straightpass(lis, key, leng):
    index=leng-1
    while key < lis[index]:
        lis[index + 1]=lis[index]
        index -=1
        if index == -1:
            break
    lis[index + 1]=key

#直接插入排序完整算法
def straight_sort(lis):
    for i in range(1, len(lis)):
        straightpass(lis, lis[i], i)

#测试
lis=[4, 3, 1, 19, 7, 12, 13, 16]
print('原列表lis---------:',lis)
straight_sort(lis)
print('排序结束lis--------:',lis)
#结果
"""
原列表lis---------: [4, 3, 1, 19, 7, 12, 13, 16]
排序结束lis--------: [1, 3, 4, 7, 12, 13, 16, 19]
"""

从图 9.1 可以看到,直接插入排序算法非常简洁,也很容易理解。下面从效率方面对直接插入排序进行分析。

(1)从空间来看,它需要一个记录的辅助空间用于存放监视哨。

(2)从时间来看,排序的基本操作为比较记录的关键字大小和移动记录。下面来看一下一趟插入排序,比较记录关键字的次数取决于当前需要插入的第i个与已经排好序的前 i-1 个元素的关系。最好的情况是第i个记录比前 i-1 个记录都大,则只需要进行依次比较;最坏情况是第i个记录比前 i-1 个记录都小。最好情况是待排序序列按非递减有序排列(即正序)给出,则保证每一趟插入都只是比较一次,无需移动记录,整个过程需要比较 n-1 次, 无需移动记录。最坏情况是待排序序列按非递增有序排列(即逆序),则每一趟插入要比较i次(i-1次和前 i-1 个记录的比较,以及1次和监视哨的比较),移动数据 i-1 次,整个排序过程需要比较\sum_{i=2}^{n}i次,移动数据\sum_{i=2}^{n}(i-1)次。如果待排序序列以完全随机方式给出,可以去两者的平均值,所以直接插入排序算法的时间复杂度为O(n^{2})。

9.1.2  折半插入排序

前面讲到直接插入排序的算法比较简洁,易于实现,当待排序的记录数不大时,这种算法是一种不错的选择,但当待排序的记录数很大时,则不宜再用直接插入排序,需要更快的排序算法。直接插入排序中的两个基本操作,比较记录的关键字和移动记录。折半排序就是针对比较记录的关键字的整个基本操作进行改进。

在直接插入排序算法中查找插入位置时,采用的顺序查找。前面讲到的顺序表的查找方法中,折半查找方法要由于顺序查找方法,所以现在讲到的折半插入排序就是用折半查找方法代替直接插入算法中顺序查找方法。一趟折半插入排序算法如程序清单 9-3所示。

程序清单 9-3      一趟折半插入排序的算法

#一趟折半插入排序的算法
#lis是子序表,key是要插入lis的记录的关键字(这里就是记录本身)
def one_banpass(lis,key):

    leng=len(lis)

    #初始化搜索的边界,即子序表lis的索引范围
    low=0
    high=leng-1

    #查找带插入的位置
    while low <= high:
        middle=(low + high)//2
        if key < lis[middle]:
            high=middle-1
        else:
            low=middle+1
    #循环结束后的low或(high + 1)就是插入位

    #循环结束后,后移lis中插入位置之后(包括插入位置)的所有记录,留出空位。 备注:从最后的记录开始后移
    for j in range(leng-1, low-1,-1): #即索引: leng-1, leng-2, ..., low
        lis[j+1]=lis[j]

    #将记录插入空位
    lis[low]=key
    #或
    #lis[high+1]=key

折半查找插入位置的分析过程

 

移位分析:
循环结束,说明找到了插入位置,随后进行移位:因为要插入的位置是low,所以子序表从low索引开始都要后移一位,并且是从最后一位元素开始后移。

整个排序和直接插入排序基本相同,只是将直接插入排序的一趟排序算法由折半插入排序的算法一趟排序算法来替换。算法入程序清单 9-4 所示:

 程序清单 9-4   折半插入排序的算法

def banpass (lis):

    #一般把第一个记录先看成是有序子序列,然后从第二个记录开始,逐次与前面子序列中的所有记录去比较,折半插入
    for leng in range(1,8): #leng即是记录要插入的子序表的长度,同时也是 key 在 lis 中的索引位置
        key = lis[leng]
        low = 0
        high = leng -1
        #当 low>high,则找到了插入位置,为 low或者 high+1
        while (low <= high) :
            #//循环计算 middle的值
            middle=(low + high)//2
            #待插入记录和 mid进行比较
            if (key < lis[middle]) :
                #改变 high
                high = middle - 1
            else:
                #//改变 low
                low = middle + 1

        #循环结束,说明找到了插入位置,进行移位:
        #  因为要插入的位置是low,所以子序表从low索引开始都要后移一位,并且是从最后一位元素开始移

        for j in range(leng -1, low -1 ,-1):
            lis[j + 1] = lis[j]

        #//插入
        lis[low] = key
        #或
        # list[high + 1] = key

#测试:
lis=[4, 3, 1, 19, 7, 12, 13, 16]
banpass(lis)
print('折半插入排序后的lis:',lis)
"""
结果:
折半插入排序后的lis: [1, 3, 4, 7, 12, 13, 16, 19]
"""



#一趟折半插入排序的算法
#lis是子序表,key是要插入lis的记录的关键字(这里就是记录本身)
def one_banpass(lis,key):

    leng=len(lis)

    #初始化搜索的边界
    low=1
    high=leng

    #查找带插入的位置
    while low <= high:
        middle=(low + high)//2
        if key < lis[middle]:
            high=middle-1
        else:
            low=middle+1
    #循环结束后的low或(high + 1)就是插入位

    #循环结束后,后移lis中插入位置之后(包括插入位置)的所有记录,留出空位。 备注:从最后的记录开始后移
    for j in range(leng-1, low-1,-1): #即索引: leng-1, leng-2, ..., low
        lis[j+1]=lis[j]

    #将记录插入空位
    lis[low]=key
    #或
    #lis[high+1]=key

从 9-4 程序清单  9-4  所示的算法可以看到算法的两个基本操作:比较记录关键字通过利用折半查找算法得到了改善;移动记录的次数和直接插入排序一样,所以折半插入排序的时间复杂度还是O(n^{2})。

或者 

程序清单如下:

#一趟折半插入排序算法:
def one_banpass(lis, key, leng):

    low=0
    high=leng-1

    while (low <= high):
        middle = (low + high) // 2
        if (key < lis[middle]):
            high = middle - 1
        else:
            low = middle + 1

    for j in range(leng - 1, low - 1, -1):
        lis[j + 1] = lis[j]

    lis[low] = key

#折半插入排序完整算法:
def banpass(lis):
    for i in range(1, len(lis)):
        one_banpass(lis, lis[i], i)

#测试:
lis=[4, 3, 1, 19, 7, 12, 13, 16]
banpass(lis)
print('折半插入排序后的lis:',lis)
"""
结果
折半插入排序后的lis: [1, 3, 4, 7, 12, 13, 16, 19]
"""

9.1.3   2路插入排序

前面将的折半插入排序是针对直接插入排序中比较记录关键字这个基本操作进行的优化,而 2路插入排序则是在折半排序的基础上对移动记录这个基本操作进行的优化。

2路插入排序的做法是用待排序的序列的第一个记录record[1]将排序任务分成两部分,第一部分的所有记录都比 record[1]小,第二部分的所有记录都比record[1]大。这样将一个比较大的排序任务,分解成两个较小的排序任务,从而减小了记录的移动次数。例如,待排序的一个序列位逆序排列,序列长度为n,则移动次数为\sum_{i=2}^{n}(i-1);而分解后(假设最好情况下,record[1]为排序后处于中间位置的记录)移动次数变为2*\sum_{i=2}^{n/2}(i-1)

一趟 2路插入排序的算法如程序清单  9-5 所示。

程序清单 9-5  一趟 2路插入排序的算法

#2路插入排序一趟算法。
# 思想:tem中的后面某些位置留给比ref小的记录less用来移位,然后再把less插入空位;
        #前面的位置留给比ref的大的记录great用来移位,然后把great插入空位。
        #相当于记录可以从两端移动位置,即双向移位。相比折半插入减少了记录移动次数
#lis:待排序列表;
#key:待插入记录关键字,在这里就记录本身;

def twopass(lis, key): #!!!注意:这里的参数lis就是原待排序列表,这是与直接插入排序和折半插入排序一趟算法的不同点   
    '''备注:在完整算法中,以下两行变量定义需要注释掉'''
    #2路移动记录。    备注:每趟 tem中first和final之间不会有lis中的元素
    first=len(tem)#每趟排序后最小值在tem中的索引(初始值例外)。初始值置为tem的长度
    final=0##每趟排序后最大值在tem中的索引。初始值置0

    global first, final#在完整算法中需要新增的代码

    ref=lis[0]#参照,即假设lis排序后其处于中间位置
    #插左边
    if key < ref:
        #折半查找插入位置,即在first~(len(tem) -1 )之间找插入位
        low = first
        high = len(tem) - 1
        while low <= high:
            middle = (low + high) // 2
            if key < tem[middle]:
                high = middle - 1
            else:
                low = middle + 1
        #前移记录
        for j in range(first, low):  # 插入位置是low,所以low之前(不包括low!!!)的记录前移
            tem[j - 1] = tem[j]
        first -= 1
        #插入记录
        tem[low-1]=key

    #插右边
    else:
        #折半查找插入位置,即在0~final之间找插入位
        low = 0
        high = final
        while low<= high:
            middle = (low + high) // 2
            if key < tem[middle]:
                high = middle - 1
            else:
                low = middle + 1

        #后移记录
        for j in range(final, low -1, -1):  # 插入位置是low,所以low之后(包括low!!!)的记录后移
            tem[j + 1] = tem[j]
        #插入记录
        tem[low]=key
        final += 1

2路插入完整算法如程序清单 9-6 所示。

#2路插入排序一趟算法。
# 思想:
        #定义一个长度等于待排序lis长度的辅助空间:tem数组
        #把tem看作是循环数组,tem中的后面某些位置留给比ref(参照值,即lis[0])小的记录less用来移位,然后再把less插入空位;
        #前面的位置留给比ref的大的记录great用来移位,然后把great插入空位;
        #并且设置first和final分别记录每趟排序后tem中最小值和最大值的索引。first初始值是len(tem);fianl初始值是0。
        #相当于记录可以从两端移动位置,即双向移位。相比折半插入减少了记录移动次数
#lis:待排序列表;
#key:待插入记录关键字,在这里就记录本身;


#一趟算法
def twopass(lis,key): #!!!注意:这里的参数lis就是原待排序列表。这是与直接插入排序和折半插入排序 的一趟算法不同点

    global first, final

    ref=lis[0]

    #插左边
    if key < ref:
        #折半查找插入位置,即在first~(len(tem) -1 )之间找插入位
        low = first
        high = len(tem) - 1
        while low <= high:
            middle = (low + high) // 2
            if key < tem[middle]:
                high = middle - 1
            else:
                low = middle + 1
        #移动记录
        for j in range(first, low):  # 插入位置是low,所以low之前(不包括!!!)的记录前移
            tem[j - 1] = tem[j]
        first -= 1
        #插入记录
        tem[low-1]=key

    #插右边
    else:
        #折半查找插入位置,即在0~final之间找插入位
        low = 0
        high = final
        while low<= high:
            middle = (low + high) // 2
            if key < tem[middle]:
                high = middle - 1
            else:
                low = middle + 1

        #移动记录
        for j in range(final, low -1, -1):  # 插入位置是low,所以low之后(包括low!!!)的记录后移
            tem[j + 1] = tem[j]
        #插入记录
        tem[low]=key
        final += 1

    print('每趟排序后的first:{};final:{};辅助列表tem:{}'.format(first, final, tem))

#2路插入完整算法

def twosort(lis):

    for i in range(1, len(lis)):
        twopass(lis,lis[i])

    # 用排序后的tem的两个子序列(即:tem[:final+1]和tem[first:]。注意:排序结束后first=final+1)的元素替换原列表lis中的元素
    n = 0
    for i in range(first, len(tem)):
        lis[n] = tem[i]
        n += 1
    m = len(tem) - first
    for i in range(0, final + 1):
        lis[m] = tem[i]
        m += 1
    print('2路插入排序后的lis:', lis)

#测试
lis=[4, 3, 1, 19, 7, 12, 13, 16]
tem=[0 for i in range(len(lis))] #用来存放每趟排序后子序列元素的辅助空间
tem[0]=lis[0]#tem的第一个值也就是参照值

first = len(tem)  # 每趟排序后最小值在tem中的索引(初始值例外)。初始值置为tem的长度
final = 0  #每趟排序后最大值在tem中的索引。初始值置0

print('游标的初始值first:{};final:{};tem的初始值:{}'.format(first, final, tem))

twosort(lis)

#结果
"""
游标的初始值first:8;final:0;tem的初始值:[4, 0, 0, 0, 0, 0, 0, 0]
每趟排序后的first:7;final:0;辅助列表tem:[4, 0, 0, 0, 0, 0, 0, 3]
每趟排序后的first:6;final:0;辅助列表tem:[4, 0, 0, 0, 0, 0, 1, 3]
每趟排序后的first:6;final:1;辅助列表tem:[4, 19, 0, 0, 0, 0, 1, 3]
每趟排序后的first:6;final:2;辅助列表tem:[4, 7, 19, 0, 0, 0, 1, 3]
每趟排序后的first:6;final:3;辅助列表tem:[4, 7, 12, 19, 0, 0, 1, 3]
每趟排序后的first:6;final:4;辅助列表tem:[4, 7, 12, 13, 19, 0, 1, 3]
每趟排序后的first:6;final:5;辅助列表tem:[4, 7, 12, 13, 16, 19, 1, 3]
2路插入排序后的lis: [1, 3, 4, 7, 12, 13, 16, 19]
"""

简洁代码如下:

def twosort(lis):
    ref = lis[0]#参照值
    tem = [0 for i in range(len(lis))]  # 用来存放每趟排序后子序列元素的辅助空间
    tem[0] = lis[0]  # tem的第一个值也就是参照值
    first = len(tem)  # 每趟排序后最小值在tem中的索引(初始值例外)。初始值置为tem的长度
    final = 0  ##每趟排序后最大值在tem中的索引。初始值置0

    for i in range(1, len(lis)):
        key=lis[i]#待插记录

        # 插左边
        if key < ref:
            low = first
            high = len(tem) - 1
            while low <= high:
                middle = (low + high) // 2
                if key < tem[middle]:
                    high = middle - 1
                else:
                    low = middle + 1
            # 前移记录
            for j in range(first, low):
                tem[j - 1] = tem[j]
            first -= 1
            # 插入记录
            tem[low - 1] = key

        # 插右边
        else:
            low = 0
            high = final
            while low <= high:
                middle = (low + high) // 2
                if key < tem[middle]:
                    high = middle - 1
                else:
                    low = middle + 1
            # 后移记录
            for j in range(final, low - 1, -1):
                tem[j + 1] = tem[j]
            # 插入记录
            tem[low] = key
            final += 1

    # 用排序后的tem的两个子序列(即:tem[:final+1]和tem[first:]。注意:排序结束后first=final+1)的元素替换原列表lis中的元素
    n = 0
    for i in range(first, len(tem)):
        lis[n] = tem[i]
        n += 1
    m = len(tem) - first
    for i in range(0, final + 1):
        lis[m] = tem[i]
        m += 1
    print('2路插入排序后的lis:', lis)

#测试
lis=[4, 3, 1, 19, 7, 12, 13, 16]
twosort(lis)

#结果
"""
2路插入排序后的lis: [1, 3, 4, 7, 12, 13, 16, 19]
"""

猜你喜欢

转载自blog.csdn.net/xiaozhu_you/article/details/89536739
今日推荐