python实现6大基本排序算法

        在开始今天的文章之前,我想很多小伙伴可能有一个疑惑,就是在python中我们有内置的sort和sorted函数可以快速实现排序了,为什么我们还要会用Python来编写排序算法呢?而且就算我们学会了如何用Python去编写排序算法,在真正写程序时我也不可能真的自己去编写一个排序函数然后去调用啊?的确,在多数情况下我们肯定是会首选内置的sort函数去解决问题,那么我在这里想说是,我写这篇文章的目的:其一,这是我自己在学习数据结构预算法中的总结,同时可能会有小伙伴和我一样需要在pta或者其他网站上用python实现这些算法来完成作业,所以希望可以通过这篇文章给小伙伴提供参考;其二,排序算法可以说是一个学计算机的人所必备的基础算法(而在我看来基础是很重要的,以为的好高骛远而忘记最根本的东西往往会在某些时刻吃亏);其三,学习的过程是一个刨根问底的过程,我们不仅仅看到某个事物的表面,更应该去挖掘其深层。而且很多时候,我们学习一个知识的目的不仅仅是为了记住它,而是更应该去理解学习其中所蕴含的思维方法思维模式来开拓我们思维;其四,书到用时方恨少,我们不能保证我们所学过的在将来都能发挥到作用,但我们要做有充分的准备,等到有一天需要它的时候我们能拿的出手,不至于到那个时候再去后悔。接下来就将详细介绍(以从小到大排序为例,从大到小类似)。

1.简单选择排序

1.1 算法思想

        简单选择排序的算法思想是,循环遍历列表,每次遍历找到未排序的序列中最小的那个数将其与未排序的序列中首个元素交换位置。

#未排序的序列
3 1 5 4
#第一次选择排序,1是最小的,所以把3,1交换位置
1 3 5 4
#此时未排序序列是3 5 4
#3是最小的不动
1 3 5 4
#此时未排序的序列是5 4
#4是最小的,4 5交换位置
1 3 4 5
#排序完成

1.2 时间复杂度

        在选择排序中,不论你的输入如何,由于每一次我们都需要从未排序的序列中找到最小的,而计算机时无法一眼看出谁最小,必须要一个一个去比较,因此其比较次与我们输入无关。其时间复杂度为O(n^2)

1.3 代码实现

s=[int(i) for i in input().split()]
l=len(s)
min_num=0
#初始话min用来记录最小数的位置
for i in range(l-1):
#这里用l-1,范围为[0,l-2]因为在第二层循环中j从i+1开始,当i=l-2时j=l-1就不会超出索引范围而出错
    min_num=i
#每次循环前假设未排序序列中第一个就是最小的,并用min来记录其位置
    for j in range(i+1,l):
#遍历未排序序列,如果是s[j]更小我们就更新最小数的位置
        if s[j]<s[min_num]:
            min_num=j
#将最小数和未排序列表第一个数交换
    s[min_num],s[i]=s[i],s[min_num]

2.简单插入排序

2.1 算法思想

        简单插入排序的算法思想是,将一个序列分为已排序部分和未排序部分,初始时认为第一个数就为已排序序列,然后取未排序序列中的第一个元素,将其插入到已排序序列中合适位置,如此循环。

#序列
4 2 1 6 5 8
#初始时4为已排序序列,2为未排序序列第一个元素,我们应该将2放到4前面
#第一次插入
2 4 1 6 5 8
#此时2 4为已排序序列,1为未排序序列第一个元素,应该将1插到2前面
#第二次插入
1 2 4 6 5 8
#此时1 2 3为已排序序列,6为未排序序列的=第一个元素,6位置正确
#第三次插入
1 2 4 6 5 8
#此时已排序序列1 2 4 6,此时应该将5插入
#第四次插入
1 2 4 5 6 8
#此时已排序为1 2 4 5 6,将8插入
#第五次
1 2 4 5 6 8
#完成

2.2 时间复杂度

        插入排序的时间复杂度和所给的输入有关。如果输入的序列越趋向于有序,每次插入时所需要移动和比较的次数就越少,时间也就越少。其平均算法使时间复杂度为O(n^2)。

2.3 代码实现

        插入排序代码的实现主要是解决如何插入的问题,我们采用将要插入的元素与已排序序列中元素倒序一 一比较,当遇到某个元素小于要插入的元素时,我们就把要插入的元素放到该元素后面一个位置即可:

s=[int(i) for i in input().split()]
l=len(s)
for i in range(1,s):
    t=s[i]
#用t暂存要插入的元素
    j=i-1
#j表示已排序序列末尾的元素
    while s[j]>t and j>=0:
#只要当已排序序列中元素s[j]大于要插入的元素,将s[j]往后移动,目的是为了留出位置给要插入的元素
#同时这也是为什么上面要用t去暂存要插入的元素,如果不暂存,s[j]后移时会覆盖住要插入的元素
#我们就找不到要插入的元素了
        s[j+1]=s[j]
        j-=1
#循环完成将要插入的元素插入
    s[j+1]=t

3.希尔排序

3.1 算法思想

        希尔排序可以说是简单插入排序的进阶版。在简单插入排序中,我们每一次是按步长为1去比较,如1 3 4 5 2,要将2插入,依次和5,4 ,3,1进行比较跨度为1,而在希尔排序中我们尝试每次以步长为i去比较,试图改善排序效率。比如i=2。对于1 3 4 5 6 7 2,要将2插入,依次和6 4 1比较,每次比较都跨过两个数。在希尔排序中我们将每个步长i所组成的集合叫做希尔增量序列。先通过大的步长使要排序的序列整体趋向于有序,然后步长慢慢减小直至为1,及回到简单选择排序。所以简但选择排序可以理解为增量序列d=[1]的特殊的希尔排序。

3.2 时间复杂度

        希尔排序的时间和增量序列的选取有关,具体增量序列的选取要基于实际情况考虑,目前还没有统一的方法或者说最优的序列。经过上网查询,希尔排序最差时间复杂度是O(n^2),最好是O(n^3/4)。同时研究表明,希尔排序对大量数据使用时会表现出很好的效率

3.3 代码实现

d = [260609, 146305, 64769, 36289,
         16001, 8929, 3905, 2161, 929,
         505, 209, 109, 41, 19, 5, 1] # Sedgewick增量序列
s=[int(i) for i in input().split()]
l=len(s)
l1=len(d)
#当我们的增量大于列表长度时肯定是无效的,这一步我们找出有效的增量序列
for i in range(l1):
    if d[i]<l:
        break
nd=d[i:]
for i in nd:
#与插入排序相比,希尔排序就是多了最外面这一层循环,当i==1时里面循环代码就是插入排序代码
#因此该代码记忆很简单只要把i看作是1当成插入排序去写即可
#我们这里内层循环从i开始因为当索引小于i时,以步长为i去比较,这个索引不在列表索引范围内了
    for j in range(i,l):
        t=s[j]
        k=j-i
#循环每次跨越i个数去比较
        while k>=0 and s[k]>t:
            s[k+i]=s[k]
            k-=i
        s[k+i]=t
        

 

4.冒泡排序

4.1 算法思想

        冒泡排序是最简单的交换排序。我们通过循环n-1次(n为列表长度),每次循环比较相邻两数的大小,如果后一个数比前一个数小,交换位置,否则不做操作。这样每次循环完成,最大的数都会被交换到最后。也正是因为每次最大的都被交换到最后,所以每次循环时我们就可以不用考虑已经被交换到最后的数组成的序列了,每次循环遍历的列表长度随之减少。

这是一次冒泡,可以看见经过一次后,,最大的数已经再最后了,所以之后对前面的数进行同样的交换操作即可。 

4.2 时间复杂度

        冒泡排序的时间复杂的与输入的序列有关,如果给定序列越取向与有序我们交换的次数也就越少,所耗时也就越少(因为这里采用了f开关变量优化,当没有交换时表示序列已经有序,停止循环,从而避免了不必要的循环,减少时间)。总而言之,冒泡排序平均算法复杂的是O(n^2)。

4.3 代码实现

s=[int(i) for i in input().split()]
l=len(s)
for i in range(l-1):
#外层循环控制循环次数,长的为l的列表循环l-1次即可
#f为开关变量,目的是对冒泡排序做一个小的优化,当内层循环完后f还是为0,也就是说该次
#循环没有交换发生,及序列已经是有序的了,此时就可以退出循环,不必要经行多余的循环了
    f=0
    for j in range(l-i-1):
#内存循环要注意的就是range中的写法,因为内层中有j+1所以我们必须保证j+1不超出索引范围
#当i=0时范围[0,l-2],此时j+1=l-1没有超出索引范围
#至于为什么要-i是因为每个循环后最大的数都被交换到最后了,所以我们不用再去比较他们
        if s[j+1]<s[j]:
            s[j+1],s[j]=s[j],s[j+1]
            f=1
    if f==0:
        break

5.快速排序

5.1 算法思想

        快速排序的算法思想是,首先从待排序序列中找到一个基准,然后遍历列表,将比基准小的全部放到左边,比基准大的全部放到右边。然后把要排序的序列一分为2,对两个子序列经行同样操作。之后对每个子序列又将其一分二,做同样处理......依次类推。

        所以我们可以发现,对一个序列进行快速排序,可以将其分解为多个规模相当,解决方法一样的子问题,且对每个子问题求解后,及将每个子序列排完后,原序列也有序了,这完全符合分治递归的思想,因此我们采用递归来实习代码。同时研究表明,当数据量较小时,快速排序的效率 并不是很高,因此我们设置一个阈值,当子问题规模小于阈值时,采用简单插入排序解决。这种采用不同排序方法混合求解的方式,能大大改善我们的算法性能。

        在实现快速排序之前,我们首先要解决的问题就是基准的选取。一个好的基准的选取能改善我们算法的性能,而一个坏的基准的选取可能时我们某次的递归变为无用功,比如如果我们选的基准恰好是列表中最大的数,那么这次快排后序列不会发生任何改变。接下来来介绍1种常见的基准选取方法:       

三者取中法:即选取列表首位,末尾,和中间位置的数的中位数作为基准

5.2 时间复杂度

        快排的时间复杂度分析比较复杂,有兴趣的小伙伴可以自行去研究,这里只说其平均算法复杂度是O(n\log_{2}n)。

5.3 代码实现

l,r为要排序列表最左端和最右端

#findkey函数实现寻找序列中的基准,并将基准至于列表末尾,目的是防止基准参与交换,在交换完后再将基准放入合适位子
def findkey(a, l, r):
    mid = int((r+l) / 2)
    max1=max(a[l],a[r],a[mid])
    min1=min(a[l],a[r],a[mid])
#找到三者中最大和最小的,如果某一个不是最大也不是最小则说明其是中位数
    for i in l,r,mid:
        if a[i]!=max1 and a[i]!=min1:
            break
#将基准放到最末尾
    a[i],a[r]=a[r],a[i]
#返回基准
    return a[r]

#定义选择排序函数,当子问题规模小于阈值时采用选择排序解决
def Sort(a,l,r):
    for i in range(l,r):
        t=a[i+1]
        j=i
        while a[j]>t and j>=0:
            a[j+1]=a[j]
            j-=1
        a[j+1]=t

#快排核心递归函数
def QuickSort(a, l, r):
#开始时判断子问题规模,看是否进行递归,我这里设置列表长度小于10,只是举例随便设定的
#同时这也是个递归出口
    if r - l + 1 < 10:
        Sort(a,l,r)
        return
    low = l
#high为r-1因为基准在索引为r的位置
    high = r - 1
#调用findkey函数找到基准
    key = findkey(a, l, r)
    while 1:
        while a[low] < key:
            low += 1
        while a[high] > key:
            high -= 1
#当两次循环都停止时,说明a[low]>key a[high]<key,所以要交换
        if low<high:
            a[low], a[high] = a[high], a[low]
        else:
            break
#当停止时把基准换到合适位置,因为当low>high时low位置的数肯定时大于基准的
    a[low], a[r] = a[r], a[low]
#将列表一分为二对子问题递归求解
    QuickSort(a, l, low - 1)
    QuickSort(a, low + 1, r)

6.归并排序

6.1 算法思想

        归并排序是基于归并思想实现的,首先我们先了解归并思想。归并即将两个有序序列合成一个有序的序列的过程。

        归并排序算法思想是将含有N个元素的序列分为N个子序列,然后对相邻的两个序列经行归并操作,归并成N/2个长度为2的序列,然后在对这些长度为2的序列再两两相邻归并,如此类推,最后合成一个有序序列。

        如果从分治的思想来看,我们可以自下而上的实现归并排序。我们可以将一个序列先一分为二, 然后递归的分解这两个子序列,当达到递归出口时,再依次归并子序列,合成一个完整的有序序列,这也是我们程序实现的思想

6.2 时间复杂度

        O(n\log_{2}n)        

6.3 代码实现

#核心递归函数
def MergeSort(s,l,r):
    mid=int(l+r)/2
#l <r是设置递归出口,因为当l=r时其实序列只有一个元素,不用操作
    if l<r:
        MergeSort(s,l,mid)
        MergeSort(s,mid+1,r)
        TwoWayMerge(s,l,mid,mid+1,r)

#将两个有序序列合并成一个序列,l1,r1为第一个列表头尾,l2,r2为第二个列表头尾
def TwoWayMerge(s,l1,r1,l2,r2):
#因为我们不好在原列表操作,所以我们将合并后的列表暂存在t中,最后将其还给s
    t=[]
#p,q分别指向两个要合并的列表的头部
    p=l1
    q=l2
#合并两个列表,思路为两个指针分别指向头部,比较两个指针所在位置数的大小,将小的数添加到t中
#同时对应指针后移
    while p<=l1 and q<=l2:
        if s[p]<s[q]:
            t.append(s[p])
            p+=1
        else:
            t.append(s[q])
            q+=1
#退出循环后,判断哪个列表还有剩余,将余下的全部添加到t末尾即可
    if p>l1:
        for i in s[q:r2+1]
            t.append(i)
    else:
        for i in s[p:r1+1]
            t.append(i)
#将t还给s
    s[l1:r2+1]=t[:]

s=[int(i) for i in input().split()]
l=len(s)
MergeSort(s,0,l-1,a)

7. sort函数的原理

        python中sort函数的实现是靠蒂姆排序实现的,蒂姆排序本身是一种混合排序,结合了归并排序和插入排序实现的,具体可以参考下面网站Timsort原理学习 · Sika (sikasjc.github.io)icon-default.png?t=N7T8https://sikasjc.github.io/2018/07/25/timsort/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值