Python数据结构与算法—排序

文章目录

  • 一、列表排序
  • 二、常见排序算法
    • 1.算法分类
    • 2.算法复杂度
  • 三、排序LowB三人组
    • 1、冒泡排序
      • 1.1算法描述
      • 1.2代码实现
      • 1.3算法分析
    • 2、选择排序
      • 2.1算法描述
      • 2.2代码实现
      • 2.3算法分析
    • 3、插入排序
      • 3.1算法描述
      • 3.2代码实现
      • 3.3算法分析
  • 四、排序NB三人组
    • 1、快速排序
      • 1.1算法描述
      • 1.2代码实现
      • 1.3算法分析
    • 2、堆排序
      • 2.1知识补充(树与二叉树)
        • 2.1.1树的一些基本概念
        • 2.1.2树的存储结构
        • 2.1.3二叉树
          • 2.1.3.1特殊二叉树
          • 2.1.3.2二叉树的存储结构
        • 2.1.4大根堆与小根堆
      • 2.2算法描述
      • 2.3代码实现
      • 2.4堆排序扩展
      • 2.5TopK问题
    • 3、归并排序
      • 3.1算法描述
      • 3.2代码实现
      • 3.3算法分析
    • 4、NB三人组小结
  • 五、其他排序
    • 1、希尔排序
      • 1.1算法描述
      • 1.2代码实现
      • 1.3算法分析
    • 2、计数排序
      • 2.1算法描述
      • 2.2代码实现
      • 2.3算法分析
    • 3、桶排序
      • 3.1算法描述
      • 3.2代码实现
      • 3.3算法分析
    • 4、基数排序
      • 4.1算法描述
      • 4.2代码实现
      • 4.3算法分析


一、列表排序

1.排序:将一组“无序”的记录序列调整为“有序”的记录序列
2.列表排序:将无序列表变为有序列表
3.升序与降序
4.python内置排序函数:sort()

二、常见排序算法

1.排序lowB三人组: 冒泡排序 选择排序 插入排序
2.排序NB三人组: 快速排序 堆排序 归并排序
3.其他排序: 希尔排序 基数排序 计数排序 桶排序

1.算法分类

十种常见排序算法可以分为两大类:

比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。

非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
Python数据结构与算法—排序_第1张图片

2.算法复杂度

Python数据结构与算法—排序_第2张图片


三、排序LowB三人组

1、冒泡排序

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1算法描述

1.比较相邻的元素。如果第一个比第二个大,就交换它们两个;
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
3.针对所有的元素重复以上的步骤,除了最后一个;
4.重复步骤1~3,直到排序完成。

Python数据结构与算法—排序_第3张图片
Python数据结构与算法—排序_第4张图片

1.2代码实现

# __author__: PPPsych
# date: 2021/1/2

def bubble_sort(li):
    for i in range(len(li) - 1):  # 第i趟
        exchange = False
        for j in range(len(li) - i - 1):  # 当前所指向的值
            if li[j] > li[j + 1]:
                li[j], li[j + 1] = li[j + 1], li[j]
                exchange = True
        print(li)
        if not exchange:
            return


li = [9, 8, 7, 1, 2, 3, 4, 5, 6]
print(li)
bubble_sort(li)
——————————————————————————————————————————————————————————
输出:
[9, 8, 7, 1, 2, 3, 4, 5, 6]
[8, 7, 1, 2, 3, 4, 5, 6, 9]
[7, 1, 2, 3, 4, 5, 6, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

1.3算法分析

关于冒泡排序的时间复杂度,在上面python实现的代码中时间复杂度是图片 ,当然可以再考虑一下极端的情况:当队列已经从小到大排好序或者从大到小排好序,从小到大排好顺序时可以只扫描一遍就结束排序,此时时间复杂度为O(n),如果是从大到小,那么就需要扫描n-1次,同时需要比较交换n-1次,时间复杂度为图片 。


2、选择排序

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

1.初始状态:无序区为R[1…n],有序区为空;

2.第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;

3.n-1趟结束,数组有序化了。
Python数据结构与算法—排序_第5张图片

2.2代码实现

# __author__: PPPsych
# date: 2021/1/2

def select_sort(li):
    for i in range(len(li) - 1):  # i为第i趟
        min_loc = i
        for j in range(i + 1, len(li)):
            if li[j] < li[min_loc]:
                min_loc = j
        if min_loc != i:
            li[i], li[min_loc] = li[min_loc], li[i]
        print(li)


li = [3, 4, 1, 2, 6, 9, 5, 8, 7]
print(li)
select_sort(li)
————————————————————————————————————————————————————————————————————————
输出:
[3, 4, 1, 2, 6, 9, 5, 8, 7]
[1, 4, 3, 2, 6, 9, 5, 8, 7]
[1, 2, 3, 4, 6, 9, 5, 8, 7]
[1, 2, 3, 4, 6, 9, 5, 8, 7]
[1, 2, 3, 4, 6, 9, 5, 8, 7]
[1, 2, 3, 4, 5, 9, 6, 8, 7]
[1, 2, 3, 4, 5, 6, 9, 8, 7]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

2.3算法分析

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。


3、插入排序

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

1.从第一个元素开始,该元素可以认为已经被排序;
2.取出下一个元素,在已经排序的元素序列中从后向前扫描;
3.如果该元素(已排序)大于新元素,将该元素移到下一位置;
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5.将新元素插入到该位置后;
6.重复步骤2~5。

Python数据结构与算法—排序_第6张图片

3.2代码实现

# __author__: PPPsych
# date: 2021/1/2

def insert_sort(li):
    for i in range(1, len(li)):  # i表示摸到的牌的下标
        temp = li[i]
        j = i - 1  # j指的是手里的牌的下标
        while j >= 0 and li[j] > temp:  # 找插入的位置
            li[j + 1] = li[j]
            j -= 1
        li[j + 1] = temp
        print(li)


li = [3, 2, 4, 1, 5, 7, 9, 6, 8]
print(li)
insert_sort(li)
—————————————————————————————————————————————————————————————————————————————
输出:
[3, 2, 4, 1, 5, 7, 9, 6, 8]
[2, 3, 4, 1, 5, 7, 9, 6, 8]
[2, 3, 4, 1, 5, 7, 9, 6, 8]
[1, 2, 3, 4, 5, 7, 9, 6, 8]
[1, 2, 3, 4, 5, 7, 9, 6, 8]
[1, 2, 3, 4, 5, 7, 9, 6, 8]
[1, 2, 3, 4, 5, 7, 9, 6, 8]
[1, 2, 3, 4, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

3.3算法分析

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

插入排序的可应用于这样的场景:需要合并两个有序序列,并且合并后的序列依旧有序,此时插入排序可以排上用场。

如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。

最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用

但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。


四、排序NB三人组

1、快速排序

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

1.1算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
Python数据结构与算法—排序_第7张图片

1.2代码实现

# __author__: PPPsych
# date: 2021/1/4


def partition(li, left, right):
    temp = li[left]  # 将左边第一个值存入temp中给列表留下待填空位
    while left < right:
        while left < right and li[right] >= temp:  # 从右边找比temp小的数
            right -= 1  # 找到比temp大的数则往左走一步
        li[left] = li[right]  # 将右边的比temp小的数填入左边空位
        print(li)
        while left < right and li[left] <= temp:  # 从左边找比temp大的数
            left += 1  # 找到比temp小的数则往右走一步
        li[right] = li[left]  # 将左边比temp大的数填入右边空位
        print(li)
    li[left] = temp  # 将temp归位
    print(li)
    return left


def quick_sort(li, left, right):
    if left < right:  # 至少存在两个元素
        mid = partition(li, left, right)
        quick_sort(li, left, mid - 1)  # 中间值左边递归
        quick_sort(li, mid + 1, right)  # 中间值右边递归


li = [5, 7, 4, 6, 3, 1, 2, 9, 8]
print(li)
quick_sort(li, 0, len(li) - 1)
print(li)
——————————————————————————————————————————————————————————
输出:
[5, 7, 4, 6, 3, 1, 2, 9, 8]
[2, 7, 4, 6, 3, 1, 2, 9, 8]
[2, 7, 4, 6, 3, 1, 7, 9, 8]
[2, 1, 4, 6, 3, 1, 7, 9, 8]
[2, 1, 4, 6, 3, 6, 7, 9, 8]
[2, 1, 4, 3, 3, 6, 7, 9, 8]
[2, 1, 4, 3, 3, 6, 7, 9, 8]
[2, 1, 4, 3, 5, 6, 7, 9, 8]
[1, 1, 4, 3, 5, 6, 7, 9, 8]
[1, 1, 4, 3, 5, 6, 7, 9, 8]
[1, 2, 4, 3, 5, 6, 7, 9, 8]
[1, 2, 3, 3, 5, 6, 7, 9, 8]
[1, 2, 3, 3, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 8, 8]
[1, 2, 3, 4, 5, 6, 7, 8, 8]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

1.3算法分析

快排故名思意在于一个快字,也就是这种算法可以通过一些方式提高速度,如使用并行的元素分区,或者在超大数据中使用多机进行联合排序,或者将两个分区拆分为更多分区,还有很多可以深入的地方。但是快速排序存在两个问题:
1.快速排序会存在极端坏的情况,此时的时间复杂度为O(n^2)
2.快速排序中存在递归的使用,而递归会消耗系统资源并且在python中存在递归最大深度问题


2、堆排序

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

2.1知识补充(树与二叉树)

2.1.1树的一些基本概念

树是n个结点的有限集,n=0时称为空树,在任意一颗非空树中:

1.有且只有一个特定的称为根的结点(下图中的结点A),

2.当n>1时,其余结点可分为m个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树。
Python数据结构与算法—排序_第8张图片
结点:A、B、C、D等都是结点,结点不仅包含数据元素,而且包含指向子树的分支。比如,结点A不仅包含数据元素A,还包含指向结点B、C、D的指针

结点的度:结点拥有的子树个数或者分支的个数。例如,A结点有B、C、D3棵子树,所以A结点的的度为3。

树的深度或高度:树中各结点度的最大值称为树的深度。

结点的深度:结点的深度是从根结点到该结点路径上的结点个数。

结点的高度:从某一结点往下走可能到达的多个叶子结点,对应了多条通往这些叶子节点的路径,其中最长那条路径的长度即为该结点在树中的深度。根节点的高度为树的高度。

叶子结点:是指结点的度为0的结点,即不指向任何结点的结点。比如结点F、G、M、I、J。

孩子:某一结点指向的结点,比如A结点的孩子就是B、C、D结点。

双亲:与孩子相对应,B、C、D的双亲就是结点A。

兄弟:同一双亲的孩子之间互称为兄弟。B、C、D结点互称为兄弟。

堂兄弟:双亲在同一层次的结点互为堂兄弟。G、H、F互为堂兄弟。

祖先:从根结点到具体某节点的路径上的所有结点,都是这个结点的祖先。结点K的祖先是A、B、E。

子孙:与祖先的概念相对应,以某结点为根的子树中的所有结点,都是该结点的子孙。结点D的子孙为H、I、J、M。

层次:从根开始,根为第一层,根的孩子为第二层,根的孩子的孩子为第三层,以此类推。

无序树:如果将树中结点的各子树看成是从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。

森林:是若干颗互不相交的树的集合。如果把根节点A去掉,剩下的三棵互不相交树就是森林。

2.1.2树的存储结构

1.顺序存储结构
树的顺序存储结构中最简单直观的是双亲存储结构,用一维数组即可实现。双亲结点就是用双亲的信息来存储数据,比如结点2、3、4的双亲是1,结点5、6、7的双亲是3,结点1是根节点,无双亲,令其等于-1。
Python数据结构与算法—排序_第9张图片

2.链式存储结构
树的链式存储中最常用的两种结构主要是孩子存储结构、孩子兄弟存储结构。

孩子存储结构就是让每个结点由一个数据域+若干个指针域组成,指针域的个数等于孩子的个数,让每个指针指向一个孩子。

孩子兄弟存储结构是每个结点有两个指针,一个指针指向该结点的其中一个孩子(长子),另一个指针指向该结点的兄弟。

2.1.3二叉树

二叉树是由n个结点的有限集合,该集合或者为空集,或者由一个根节点和两颗互不相交的、分别称为根节点的左子树和右子树的二叉树组成。

二叉树有如下特征:

1.每个结点最多只有两颗子树,即二叉树中结点的度最高不能超过2。

2.子树的左右顺序之分,不能颠倒。

2.1.3.1特殊二叉树

满二叉树:在一颗二叉树中,如果所有的分支结点都有左孩子和右孩子结点,并且叶子节点都集中在二叉树的最下一层,则这样的二叉树称为满二叉树。

完全二叉树:如果对一颗深度为k,有n个结点的二叉树进行编号后,各结点的编号与深度为k的满二叉树中相同位置上的结点的编号均相同,那么这棵二叉树就是一颗完全二叉树。

一颗完全二叉树其实就是由一颗满二叉树从右至左从下至上的,挨个删除结点以后所得到的。
Python数据结构与算法—排序_第10张图片

2.1.3.2二叉树的存储结构

1.顺序存储结构
顺序存储即用一个数组来存储一颗二叉树,具体存储方法为将二叉树中的结点进行编号,然后按编号依次将结点值存入到一个数组中,即完成了一颗二叉树的顺序存储。这种存储结构比较适合存储完全二叉树,用于存储一般的二叉树会浪费大量的存储空间。
Python数据结构与算法—排序_第11张图片

注:在堆排序中使用顺序储存结构

2链式存储结构
顺序结构有一定的局限性,不便于存储任意形态的二叉树。通过二叉树的形态,可以发现一个根节点与两颗子树有关系,因此设计一个含有一个数据域和两个指针域的链式结点结构,具体如下:
在这里插入图片描述
data表示数据域,用于存储对应的数据元素;lchild和rchild分别表示左指针域和右指针域,分别用于存储左孩子结点和右孩子结点的位置,如果没有右孩子结点,则右指针为空。这种存储结构称为二叉链表存储结构。定义如下:
Python数据结构与算法—排序_第12张图片

2.1.4大根堆与小根堆

Python数据结构与算法—排序_第13张图片
大根堆:一颗完全二叉树,满足任意节点都比其孩子节点大
小根堆:一颗完全二叉树,满足任意节点都比其孩子节点小

2.2算法描述

1.将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;

2.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];

3.由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

2.3代码实现

# __author__: PPPsych
# date: 2021/1/4


def sift(li, low, high):
    """
    对堆向下调整
    :param li: 列表
    :param low: 堆的根节点位置
    :param high: 堆的最后一个元素的位置
    :return:
    """
    i = low  # i最开始指向根节点
    j = 2 * i + 1  # j开始指向i的左孩子
    temp = li[low]  # 将堆顶存入temp
    while j <= high:  # 只要j的位置有元素
        if j + 1 <= high and li[j + 1] > li[j]:  # 如果i的右孩子存在并且比左孩子大
            j = j + 1  # j指向i的右孩子
        if li[j] > temp:  # 如果i的孩子大于temp中的值
            li[i] = li[j]  # 将j指向的元素放到i指向的位置
            i = j  # 往下一层
            j = 2 * i + 1
        else:  # temp更大,则将temp放到i的位置上
            li[i] = temp  # 把temp放到某一节点上
            break
    else:
        li[i] = temp  # 如果j指向的位置没有元素则直接将temp放到叶子节点上


def heap_sort(li):
    """ 堆排序 """
    n = len(li)
    # 1.构造大顶堆
    for i in range((n - 2) // 2, -1, -1):
        # i表示构造堆时需调整的部分的根的下标
        sift(li, i, n - 1)
    # 大顶堆构造完成

    # 2.挨个出数
    for i in range(n - 1, -1, -1):
        # i指向当前堆的最后一个元素
        li[0], li[i] = li[i], li[0]
        sift(li, 0, i - 1)  # i-1是新的high


li = [5, 7, 4, 6, 3, 1, 2, 9, 8]
print(li)
heap_sort(li)
print(li)
————————————————————————————————————————————————————————
输出:
[5, 7, 4, 6, 3, 1, 2, 9, 8]
[1, 2, 3, 4, 5, 6, 7, 8, 9]

2.4堆排序扩展

内置模块heapq
常用函数:heapify(li),heappush(heap,item),heappop(heap)

2.5TopK问题

问题描述:现有n个数,设计算法得到前k大的数
(类似取热搜榜前几问题)

解决思路:
1.排序后切片
2.排序LowB三人组
3.堆排序

解决方法选择:考虑时间复杂度,方法1为O(nlogn)、方法2为O(kn)、方法3为O(nlogk),因此最优解为堆排序方法。

TopK问题:
1.使用小根堆记录前k个最大值
2.如果新元素大于堆顶,则移除堆顶并插入新元素。然后进行堆排序/构建堆。

代码示例:

# __author__: PPPsych
# date: 2021/1/4


def sift(li, low, high):
    i = low
    j = 2 * i + 1
    temp = li[low]
    while j <= high:
        if j + 1 <= high and li[j + 1] < li[j]:  # 如果i的右孩子存在并且比左孩子小
            j = j + 1
        if li[j] < temp:  # 如果i的孩子小于temp中的值
            li[i] = li[j]
            i = j
            j = 2 * i + 1
        else:
            li[i] = temp
            break
    else:
        li[i] = temp


def topk(li, k):
    heap = li[0:k]
    # 1.建堆
    for i in range((k - 2) // 2, -1, -1):
        # i表示构造堆时需调整的部分的根的下标
        sift(heap, i, k - 1)

    # 2.遍历
    for i in range(k, len(li) - 1):
        if li[i] > heap[0]:
            heap[0] = li[i]
            sift(heap, 0, k - 1)

    # 3.出数
    for i in range(k - 1, -1, -1):
        heap[0], heap[i] = heap[i], heap[0]
        sift(heap, 0, i - 1)

    return heap


import random

li = list(range(1000))
random.shuffle(li)
print(topk(li, 10))
———————————————————————————————————————————————————————————
输出:
[999, 998, 997, 996, 995, 994, 993, 992, 991, 990]

3、归并排序

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

3.1算法描述

1.把长度为n的输入序列分成两个长度为n/2的子序列;

2.对这两个子序列分别采用归并排序;

3.将两个排序好的子序列合并成一个最终的排序序列。
Python数据结构与算法—排序_第14张图片
分解合并如下:
Python数据结构与算法—排序_第15张图片

3.2代码实现

# __author__: PPPsych
# date: 2021/1/4


def merge(li, low, mid, high):
    i = low
    j = mid + 1
    ltmp = []
    while i <= mid and j <= high:
        if li[i] <= li[j]:
            ltmp.append(li[i])
            i += 1
        else:
            ltmp.append(li[j])
            j += 1
    while i <= mid:
        ltmp.append(li[i])
        i += 1
    while j <= high:
        ltmp.append(li[j])
        j += 1
    li[low:high + 1] = ltmp


def merge_sort(li, low, high):
    if low < high:  # 至少有两个元素
        mid = (low + high) // 2
        merge_sort(li, low, mid)
        merge_sort(li, mid + 1, high)
        merge(li, low, mid, high)
        print(li[low:high + 1])


li = list(range(10))
import random

random.shuffle(li)
print(li)
merge_sort(li, 0, len(li) - 1)

———————————————————————————————————————————————————————————
输出:
[9, 2, 8, 4, 3, 6, 0, 7, 1, 5]
[2, 9]
[2, 8, 9]
[3, 4]
[2, 3, 4, 8, 9]
[0, 6]
[0, 6, 7]
[1, 5]
[0, 1, 5, 6, 7]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

3.3算法分析

归并排序的空间复杂度为O(n)
其特点是需要开辟新的待存储的列表,需要额外的内存开销


4、NB三人组小结

Python数据结构与算法—排序_第16张图片


五、其他排序

1、希尔排序

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

1.1算法描述

希尔排序是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。

希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

希尔排序的核心是对步长的理解,步长是进行相对比较的两个元素之间的距离,随着步长的减小,相对元素的大小会逐步区分出来并向两端聚拢,当步长为1的时候,就完成最后一次比较,那么序列顺序就出来了。
Python数据结构与算法—排序_第17张图片
如上面实例:第一次排序步长为5,那么需要比较的元素对为:9-4 1-8 2-6 5-3 7-5,只需要将这几组元素比比较并交换位置;然后开始第二轮的比较。

1.2代码实现

# __author__: PPPsych
# date: 2021/1/5


def insert_sort_gap(li, gap):
    for i in range(gap, len(li)):  # i表示摸到的牌的下标
        temp = li[i]
        j = i - gap  # j指的是手里的牌的下标
        while j >= 0 and li[j] > temp:  # 找插入的位置
            li[j + gap] = li[j]
            j -= gap
        li[j + gap] = temp


def shell_sort(li):
    d = len(li) // 2
    while d >= 1:
        insert_sort_gap(li, d)
        d //= 2
        print(li)


li = [9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
print(li)
shell_sort(li)
print(li)
———————————————————————————————————————————————————————————
输出:
[9, 1, 2, 5, 7, 4, 8, 6, 3, 5]
[4, 1, 2, 3, 5, 9, 8, 6, 5, 7]
[2, 1, 4, 3, 5, 6, 5, 7, 8, 9]
[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
[1, 2, 3, 4, 5, 5, 6, 7, 8, 9]

1.3算法分析

不需要大量的辅助空间,和归并排序一样容易实现。希尔排序是基于插入排序的一种算法,在此算法基础之上增加了一个新的特性,提高了效率。希尔排序没有快速排序算法快,因此中等大小规模数据排序中表现良好,对规模非常大的数据排序不是最优选择。

希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差,几乎任何排序工作在开始时都可以用希尔排序,本质上讲,希尔排序算法是直接插入排序算法的一种改进,减少了其复制的次数,速度要快很多。


2、计数排序

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

2.1算法描述

1.找出待排序的数组中最大和最小的元素;

2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;

3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);

4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
Python数据结构与算法—排序_第18张图片

2.2代码实现

# __author__: PPPsych
# date: 2021/1/5


def count_sort(li, max_count=100):
    count = [0 for _ in range(max_count + 1)]
    for value in li:
        count[value] += 1
    li.clear()
    for index, value in enumerate(count):
        for i in range(value):
            li.append(index)


import random

li = [random.randint(0, 10) for _ in range(20)]
print(li)
count_sort(li)
print(li)
———————————————————————————————————————————————————————————
输出:
[5, 8, 8, 6, 4, 0, 1, 8, 4, 7, 10, 8, 7, 9, 6, 6, 6, 7, 10, 5]
[0, 1, 4, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 8, 8, 8, 8, 9, 10, 10]

2.3算法分析

计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。


3、桶排序

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

3.1算法描述

1.设置一个定量的数组当作空桶;

2.遍历输入数据,并且把数据一个一个放到对应的桶里去;

3.对每个不是空的桶进行排序;

4.从不是空的桶里把排好序的数据拼接起来。
Python数据结构与算法—排序_第19张图片

3.2代码实现

# __author__: PPPsych
# date: 2021/1/5


def bucket_sort(li, n=100, max_num=10000):
    buckets = [[] for _ in range(n)]  # 创建桶
    for value in li:
        i = min(value // (max_num // n), n - 1)
        # i表示value放到几号桶里
        buckets[i].append(value)  # 把value加到桶里
        for j in range(len(buckets[i]) - 1, 0, -1):  # 保持桶内的数据的顺序
            if buckets[i][j] < buckets[i][j - 1]:
                buckets[i][j], buckets[i][j - 1] = buckets[i][j - 1], buckets[i][j]
            else:
                break
    sorted_li = []
    for buc in buckets:
        sorted_li.extend(buc)
    return sorted_li


import random

li = [random.randint(0, 10000) for i in range(20)]
print(li)
li = bucket_sort(li)
print(li)
———————————————————————————————————————————————————————————
输出:
[6418, 2373, 2738, 8994, 1757, 134, 5018, 7629, 5762, 469, 1318, 3262, 9216, 9333, 9620, 5535, 7219, 8941, 7515, 4335]
[134, 469, 1318, 1757, 2373, 2738, 3262, 4335, 5018, 5535, 5762, 6418, 7219, 7515, 7629, 8941, 8994, 9216, 9333, 9620]

3.3算法分析

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。


4、基数排序

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

4.1算法描述

1.取得数组中的最大数,并取得位数;

2.arr为原始数组,从最低位开始取每个位组成radix数组;

3.对radix进行计数排序(利用计数排序适用于小范围数的特点);
Python数据结构与算法—排序_第20张图片

4.2代码实现

# __author__: PPPsych
# date: 2021/1/5


def radix_sort(li):
    max_num = max(li)  # 取最大值,99则分两次,999则分三次,10000则分五次
    it = 0
    while 10 ** it <= max_num:
        buckets = [[] for _ in range(10)]
        for val in li:
            digit = (val // 10 ** it) % 10
            buckets[digit].append(val)
            # 分桶完成
        li.clear()
        for buc in buckets:
            li.extend(buc)
            # 把数据重新写回li
        it += 1
        print(li)

import random

li = list(range(20))
random.shuffle(li)
print(li)
radix_sort(li)
———————————————————————————————————————————————————————————
输出:
[11, 2, 18, 14, 8, 7, 1, 6, 4, 10, 3, 15, 16, 17, 9, 19, 5, 13, 12, 0]
[10, 0, 11, 1, 2, 12, 3, 13, 14, 4, 15, 5, 6, 16, 7, 17, 18, 8, 9, 19]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

4.3算法分析

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。


你可能感兴趣的:(Python基础,数据结构,算法,排序算法)