1.排序:将一组“无序”的记录序列调整为“有序”的记录序列
2.列表排序:将无序列表变为有序列表
3.升序与降序
4.python内置排序函数:sort()
1.排序lowB三人组: 冒泡排序 选择排序 插入排序
2.排序NB三人组: 快速排序 堆排序 归并排序
3.其他排序: 希尔排序 基数排序 计数排序 桶排序
十种常见排序算法可以分为两大类:
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.比较相邻的元素。如果第一个比第二个大,就交换它们两个;
2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
3.针对所有的元素重复以上的步骤,除了最后一个;
4.重复步骤1~3,直到排序完成。
# __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]
关于冒泡排序的时间复杂度,在上面python实现的代码中时间复杂度是图片 ,当然可以再考虑一下极端的情况:当队列已经从小到大排好序或者从大到小排好序,从小到大排好顺序时可以只扫描一遍就结束排序,此时时间复杂度为O(n),如果是从大到小,那么就需要扫描n-1次,同时需要比较交换n-1次,时间复杂度为图片 。
选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
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个的新无序区;
# __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]
表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:
1.从第一个元素开始,该元素可以认为已经被排序;
2.取出下一个元素,在已经排序的元素序列中从后向前扫描;
3.如果该元素(已排序)大于新元素,将该元素移到下一位置;
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5.将新元素插入到该位置后;
6.重复步骤2~5。
# __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]
插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
插入排序的可应用于这样的场景:需要合并两个有序序列,并且合并后的序列依旧有序,此时插入排序可以排上用场。
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。
最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。因而,插入排序不适合对于数据量比较大的排序应用
但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:
1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
# __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.快速排序会存在极端坏的情况,此时的时间复杂度为O(n^2)
2.快速排序中存在递归的使用,而递归会消耗系统资源并且在python中存在递归最大深度问题
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
树是n个结点的有限集,n=0时称为空树,在任意一颗非空树中:
1.有且只有一个特定的称为根的结点(下图中的结点A),
2.当n>1时,其余结点可分为m个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树。
结点: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去掉,剩下的三棵互不相交树就是森林。
1.顺序存储结构
树的顺序存储结构中最简单直观的是双亲存储结构,用一维数组即可实现。双亲结点就是用双亲的信息来存储数据,比如结点2、3、4的双亲是1,结点5、6、7的双亲是3,结点1是根节点,无双亲,令其等于-1。
2.链式存储结构
树的链式存储中最常用的两种结构主要是孩子存储结构、孩子兄弟存储结构。
孩子存储结构就是让每个结点由一个数据域+若干个指针域组成,指针域的个数等于孩子的个数,让每个指针指向一个孩子。
孩子兄弟存储结构是每个结点有两个指针,一个指针指向该结点的其中一个孩子(长子),另一个指针指向该结点的兄弟。
二叉树是由n个结点的有限集合,该集合或者为空集,或者由一个根节点和两颗互不相交的、分别称为根节点的左子树和右子树的二叉树组成。
二叉树有如下特征:
1.每个结点最多只有两颗子树,即二叉树中结点的度最高不能超过2。
2.子树的左右顺序之分,不能颠倒。
满二叉树:在一颗二叉树中,如果所有的分支结点都有左孩子和右孩子结点,并且叶子节点都集中在二叉树的最下一层,则这样的二叉树称为满二叉树。
完全二叉树:如果对一颗深度为k,有n个结点的二叉树进行编号后,各结点的编号与深度为k的满二叉树中相同位置上的结点的编号均相同,那么这棵二叉树就是一颗完全二叉树。
一颗完全二叉树其实就是由一颗满二叉树从右至左从下至上的,挨个删除结点以后所得到的。
1.顺序存储结构
顺序存储即用一个数组来存储一颗二叉树,具体存储方法为将二叉树中的结点进行编号,然后按编号依次将结点值存入到一个数组中,即完成了一颗二叉树的顺序存储。这种存储结构比较适合存储完全二叉树,用于存储一般的二叉树会浪费大量的存储空间。
注:在堆排序中使用顺序储存结构
2链式存储结构
顺序结构有一定的局限性,不便于存储任意形态的二叉树。通过二叉树的形态,可以发现一个根节点与两颗子树有关系,因此设计一个含有一个数据域和两个指针域的链式结点结构,具体如下:
data表示数据域,用于存储对应的数据元素;lchild和rchild分别表示左指针域和右指针域,分别用于存储左孩子结点和右孩子结点的位置,如果没有右孩子结点,则右指针为空。这种存储结构称为二叉链表存储结构。定义如下:
大根堆:一颗完全二叉树,满足任意节点都比其孩子节点大
小根堆:一颗完全二叉树,满足任意节点都比其孩子节点小
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,则整个排序过程完成。
# __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]
内置模块heapq
常用函数:heapify(li),heappush(heap,item),heappop(heap)
问题描述:现有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]
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
1.把长度为n的输入序列分成两个长度为n/2的子序列;
2.对这两个子序列分别采用归并排序;
3.将两个排序好的子序列合并成一个最终的排序序列。
分解合并如下:
# __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]
归并排序的空间复杂度为O(n)
其特点是需要开辟新的待存储的列表,需要额外的内存开销
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
希尔排序是插入排序的一种又称“缩小增量排序”,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。
希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
希尔排序的核心是对步长的理解,步长是进行相对比较的两个元素之间的距离,随着步长的减小,相对元素的大小会逐步区分出来并向两端聚拢,当步长为1的时候,就完成最后一次比较,那么序列顺序就出来了。
如上面实例:第一次排序步长为5,那么需要比较的元素对为:9-4 1-8 2-6 5-3 7-5,只需要将这几组元素比比较并交换位置;然后开始第二轮的比较。
# __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.找出待排序的数组中最大和最小的元素;
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
# __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]
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
1.设置一个定量的数组当作空桶;
2.遍历输入数据,并且把数据一个一个放到对应的桶里去;
3.对每个不是空的桶进行排序;
# __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]
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
1.取得数组中的最大数,并取得位数;
2.arr为原始数组,从最低位开始取每个位组成radix数组;
3.对radix进行计数排序(利用计数排序适用于小范围数的特点);
# __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]
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。