一、计数排序
1、概述
是通过统计元素的数量来实现排序的,通常用于整数数组
2、原理
- 遍历数组,找出待排序的数组中最大和最小的值
- 统计数组中每个元素的出现次数,存到数组A中,元素大小为下标
- 对数组做前缀和操作(每次加上前面一项)
- 再填充目标数组,按照下标顺序和出现次数来
3、代码
# 计数排序
def Counting_Sort(nums):
Max_Num = max(nums)
Counter = [0] * (Max_Num + 1) # 记录每个元素出现的次数
for i in nums:
Counter[i] += 1
for j in range(Max_Num): # 计算前缀和
Counter[j + 1] += Counter[j]
Lenth = len(nums)
res = [0] * Lenth
for q in range(Lenth - 1, -1, -1): # 倒叙遍历num,将元素填入结果数组res
num = nums[q]
res[Counter[num] - 1] = num # 将值放到对应的索引处
Counter[num] -= 1
for p in range(Lenth): # 使用res数组覆盖原数组
nums[p] = res[p]
if __name__ == '__main__':
tmp = [100, 1, 3, 5, 64, 2, 54, 89, 2, 56, 10]
Counting_Sort(tmp)
print(tmp)
结果:[1, 2, 2, 3, 5, 10, 54, 56, 64, 89, 100]
4、特征
时间复杂度:O(n + m)
空间复杂度:O(n + m)
稳定排序
适用于数据量大但是数据范围小的情况
二、 堆排序
1、概述
输入数组创建大顶堆,此时最大的元素再堆顶,再不断的进行出堆操作,依次记录数据,即可得到从小到大排列的序列。
2、原理
-
将初始待排列的序列构建成大顶堆,创建出初始的无序区
-
将堆顶的元素和最后一个元素交换,就会得到一个新的无序区,和新的有序区,并且此时无序区域内的元素都小于有序区的元素
-
交换完的新堆顶可能会违反堆的性质,所以需要对无序区重新进行构建,构建完成后再重复上述动作,直到排序完成
3、代码
# 堆排序
def Max_Heapify(nums, Lenth, i): # 堆的长度为Lenth,从节点i开始,建堆操作
while True:
Left = 2 * i + 1
Right = 2 * i + 2
tmp = i
if Left < Lenth and nums[Left] > nums[tmp]:
tmp = Left
if Right < Lenth and nums[Right] > nums[tmp]:
tmp = Right
# 如果节点i最大或者索引l,r越界,则说明不用继续堆化直接跳出即可
if tmp == i:
break
# 交换节点
nums[i], nums[tmp] = nums[tmp], nums[i]
# 继续向下堆化
i = tmp
def Heap_Sort(nums):
Lenth = len(nums)
# 建立堆
for i in range(Lenth // 2 - 1, -1, -1):
Max_Heapify(nums, Lenth, i)
# 从堆中提取最大的元素,一直循环
for i in range(Lenth - 1, 0, -1):
# 交换根节点和最右叶节点
nums[0], nums[i] = nums[i], nums[0]
#重新堆化
Max_Heapify(nums, i, 0)
if __name__ == '__main__':
tmp = [100, 1, 3, 5, 64, 2, 54, 89, 2, 56, 10]
Heap_Sort(tmp)
print(tmp)
结果:[1, 2, 2, 3, 5, 10, 54, 56, 64, 89, 100]
4、特征
时间复杂度:O(nlogn)
空间复杂度:O(1)
非稳定排序
三、 希尔排序
1、概述
在插入排序上做了改进,先将待排序序列分割成若干子序列分别进行插入序列,等到整个序列基本有序时,再整体做插入排序。
2、原理
-
选择一个增量序列t1,t2,…,tk,其中ti > tj, tk = 1
-
按增量序列个数K,对序列进行K趟排序
-
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各个子表直接进行插入排序,仅增量因子为1时,整个序列作为一个表来处理,表长即序列长
3、代码
def Insert_Sort(arr, start, gap): # 插入排序
Lenth = len(arr)
# 外循环是已经排序的区间
for i in range(start + gap, Lenth, gap):
Value = arr[i]
Position = i
# 内循环将值插入到已经排序区间的正确位置
while Position >= gap and arr[Position - gap] > Value:
arr[Position] = arr[Position - gap]
Position = Position - gap
arr[Position] = Value
def Shell_Sort(arr):
count = len(arr) // 2 # 切割子序列的步长
while count > 0: # 持续切割直到不能切割为止
# 遍历每个子序列
for i in range(count):
Insert_Sort(arr, i, count)
count //= 2 # 改变步长,继续切割
if __name__ == '__main__':
tmp = [100, 1, 3, 5, 64, 2, 54, 89, 2, 56, 10]
tmp_1 = [0.44, 0.45, 0.21, 0.11, 0.32, 0.95, 0.31]
Shell_Sort(tmp)
print(tmp)
4、特征
时间复杂度:O(n logn)
空间复杂度:O(1)
不稳定排序
四、 桶排序
1、概述
本质是计数排序的升级版本,假设输入的数据均匀分布,将数据分到有限个桶内,再在每个桶内进行排序,那么如果想要排序更加高效,在额外内存足够的情况下需要尽可能的增大桶的数量。
2、原理
-
设定一个定量的数组作为空桶
-
遍历数组,将数据放到对应的桶中
-
对每个不是空的桶进行排序
-
从每个非空桶中取出数据进行拼接,得到完整的排序数据
3、代码
# 排序数组为浮点数时
def Bucket_Sort(nums):
# 初始化tmp = n/2个桶子,平均每个桶分配两个元素
Lenth = len(nums)
tmp = Lenth // 2
bucksts = [[] for _ in range(tmp)]
# 将数组分配到每个桶中
for num in nums:
i = int(num * tmp) # 因为是浮点型,所以需要对区间进行放大
# 将num添加进桶i
bucksts[i].append(num)
# 对各个桶执行排序
for buckst in bucksts:
buckst.sort()
# 遍历桶合并结果
i = 0
for buckst in bucksts:
for num in buckst:
nums[i] = num
i += 1
if __name__ == '__main__':
tmp = [100, 1, 3, 5, 64, 2, 54, 89, 2, 56, 10]
tmp_1 = [0.44, 0.45, 0.21, 0.11, 0.32, 0.95, 0.31]
Bucket_Sort(tmp_1)
print(tmp_1)
结果:[0.11, 0.21, 0.31, 0.32, 0.44, 0.45, 0.95]
上述代码是针对于浮点数的,所以需要对区间进行放大,当数据是int类型时一般不使用缩放
def Bucket_Sort(nums):
# 初始化tmp = (max - min)/len + 1个桶子
Lenth = len(nums)
tmp = (max(nums) - min(nums)) // Lenth + 1
bucksts = [[] for _ in range(tmp)]
# 将数组分配到每个桶中
for num in nums:
i = (num - min(nums)) // Lenth
# 将num添加进桶i
bucksts[i].append(num)
# 对各个桶执行排序
for buckst in bucksts:
buckst.sort()
# 遍历桶合并结果
i = 0
for buckst in bucksts:
for num in buckst:
nums[i] = num
i += 1
if __name__ == '__main__':
tmp = [100, 1, 3, 5, 64, 2, 54, 89, 2, 56, 10]
tmp_1 = [0.44, 0.45, 0.21, 0.11, 0.32, 0.95, 0.31]
Bucket_Sort(tmp)
print(tmp)
结果:[1, 2, 2, 3, 5, 10, 54, 56, 64, 89, 100]
上述两种各自适用于不同场景,在实际使用过程中,需要对区间进行合理话安排,该算法的优势才能最大体现。
4、特征
时间复杂度:O(n + k) # k是指桶数量
空间复杂度:O(n + k)
桶排序是否稳定,取决于内部依赖的排序是否稳定
五、 基数排序
1、概述
类似于计数排序,通过统计个数来时间排序,在此基础上利用数字之间的递进关系,依次对每位数据进行排序,最终得到排序结果。
2、原理
-
获得数组中最大的数,获取其位数
-
arr为原始数组,从最低位开始取每个位组成arr_tmp数组
-
对arr_tmp进行计数排序
3、代码
def Digit(nums, exp):# 获取元素nums的位置
return (nums // exp) % 10
def Digit_Counting_Sort(nums, exp):
# 十进制范围为0-9,所以创建空间为10的数组
Counter = [0] * 10
Lenth = len(nums)
# 统计0-9每个数字出现的次数
for i in range(Lenth):
d = Digit(nums[i], exp)
Counter[d] += 1
# 求前缀和,将出现个数转化为数组索引
for i in range(1, 10):
Counter[i] += Counter[i - 1]
res = [0] * Lenth
# 倒序遍历,根据桶内统计结果,将各元素填入res
for i in range(Lenth - 1, -1, -1):
d = Digit(nums[i], exp)
j = Counter[d] - 1# 获取d在数组中的索引
res[j] = nums[i] # 将当前元素填入索引
Counter[d] -= 1 # 索引d数量-1
# 覆盖原始数组
for i in range(Lenth):
nums[i] = res[i]
def Radix_Sort(nums):
Max_Nums = max(nums)
# 按照从低到高的顺利遍历
exp = 1
while exp <= Max_Nums:
# 对数组的第k位做计数排序
Digit_Counting_Sort(nums, exp)
exp *= 10
if __name__ == '__main__':
tmp = [100, 1, 3, 5, 64, 2, 54, 89, 2, 56, 10]
tmp_1 = [0.44, 0.45, 0.21, 0.11, 0.32, 0.95, 0.31]
Radix_Sort(tmp)
print(tmp)
结果:[1, 2, 2, 3, 5, 10, 54, 56, 64, 89, 100]
4、特征
时间复杂度:O(nk)
空间复杂度:O(n + d)
是否稳定要取决于其中的计数排序
适用于数字范围较大的情况,但是前提是数据必须可以表示为固定位数的格式,且位数不能过大
六、总结
排序算法 | 平均时间复杂度 | 空间复杂度 | 排序方式 | 是否稳定 |
---|---|---|---|---|
计数排序 | O(n + m) | O(n + m) | 外部排序 | 稳定 |
堆排序 | O(nlogn) | O(1) | 内部排序 | 非稳定 |
希尔排序 | O(nlogn) | O(1) | 内部排序 | 非稳定 |
桶排序 | O(n + k) | O(n + k) | 外部排序 | 取决于内部排序方式 |
基数排序 | O(nk) | O(n + d) | 外部排序 | 取决于内部排序方式 |