欢迎白嫖 但也殷切希望大家 关注+收藏+点赞 继续加油!!!
本文主要介绍了数据结构中常见的排序算法:冒泡排序(鸡尾酒排序) 快速排序(分治法) 堆排序(大顶堆) 计数排序 桶排序 (使用的是Python语言);以及各个算法之间的比较(稳定性 时间复杂度 空间复杂度等)
目录
4.1 分类
根据时间复杂度的不同,主流的排序算法可以分为3大类:
1.时间复杂度为O(n^2)
冒泡排序
选择排序
插入排序
希尔排序(它的性能略优于O(n^2),O(nlogn),姑且把它归为此类)
2.O(nlogn)
快速排序
归并排序
堆排序
3.时间复杂度为线性
基数排序
归并排序
基数排序
冒泡排序
4.2 冒泡排序及其改进(鸡尾酒排序)
4.2.1 冒泡排序
`numpy` 是 Python 中一个非常重要的库,它提供了大量的数学函数操作,特别是针对数组的操作。
# 冒泡排序
# 生成一个随机列表
import numpy as np
list_pao = np.random.randint(100,size = 6)
print('随机生成的列表为:',list_pao)
count = len(list_pao)
# i表示一共进行了多少轮,j表示每一轮比较了多少次(每一轮比较的元素)
for i in range(count-1):
for j in range(count-i-1):
if list_pao[j] > list_pao[j+1]:
list_pao[j],list_pao[j+1] = list_pao[j+1],list_pao[j]
print('排列后的列表为:',list_pao)
4.2.2 鸡尾酒排序
冒泡算法的每一轮都是从左到右一次比较元素,单项进行,
鸡尾酒排序是像摆钟一样,左右循环进行(感觉有点像指针),没有元素进行位置交换,排序结束。
# 鸡尾酒排序算法
def cocktail_sort(arr):
n = len(arr)
swapped = True
start = 0
end = n - 1
while swapped:
# 重置swapped标志,如果这轮遍历中没有发生交换,则数组已经排序完成
swapped = False
# 从左向右遍历数组
for i in range(start, end):
if arr[i] > arr[i + 1]:
# 交换元素
arr[i], arr[i + 1] = arr[i + 1], arr[i]
swapped = True
# 如果这一轮没有发生交换,则数组已经排序完成
if not swapped:
break
# 否则,重置swapped标志,并准备从右向左遍历
swapped = False
# 从右向左遍历数组,注意此时不需要再比较最后一个元素(因为它已经在上一轮与倒数第二个元素比较过了)
end -= 1
for i in range(end - 1, start - 1, -1):
if arr[i] > arr[i + 1]:
# 交换元素
arr[i], arr[i + 1] = arr[i + 1], arr[i]
swapped = True
# 下一轮从左向右遍历时,由于最小的元素已经被移到了最左边,所以从start+1开始
start += 1
# 测试代码
arr = [7, 3, 5, 8, 2, 9, 4, 1, 6]
cocktail_sort(arr)
print("Sorted array is:", arr)
4.3 快速排序
4.3.1 分治法 基准元素的选择
在每一轮挑选一个基准元素,并让其他比他大的元素移动到数列的一边,比它小的元素移动到数列的另一边,从而把数列拆解成两个部分。这种思想叫做分治法
时间复杂度为O(nlogn),最坏的情况下是O(n^2)
基准元素的选择:
一般情况下选取的是第一个元素,特殊情况,随机选择一个元素作为基准元素.
4.3.3 元素的交换
1.双边循环法
# 双边循环实现快速排序
# 递归
def quicksort(arr):
if len(arr) <= 1:
return arr
pivot = arr[len(arr) // 2] # 选择中间元素作为基准
left, right = 0, len(arr) - 1
# 类似于荷兰国旗问题的三向切分
lt, gt = left, right # lt是小于pivot的区域的右边界,gt是大于pivot的区域的左边界
i = left # 从左向右遍历数组
while i <= gt:
if arr[i] < pivot:
arr[i], arr[lt] = arr[lt], arr[i] # 交换,将小于pivot的元素移到左边
lt += 1
i += 1
elif arr[i] > pivot:
arr[i], arr[gt] = arr[gt], arr[i] # 交换,将大于pivot的元素移到右边
gt -= 1
else:
i += 1 # 当前元素等于pivot,不做交换,继续向右移动
# 递归排序小于和大于pivot的部分
quicksort(arr[:lt])
quicksort(arr[gt + 1:])
# 注意:这里的递归方式没有直接修改原数组,而是对切片进行递归。
# 若要直接修改原数组,可以考虑使用索引而非切片进行递归,或使用辅助函数。
# 示例
arr = [10, 7, 8, 9, 1, 5]
quicksort(arr)
print(arr) # 注意:由于原数组被直接修改,所以打印的是排序后的数组
# 这里的问题在于,由于切片操作,原数组arr并未被直接修改。
# 若要修改原数组,请采用索引进行递归或调整函数逻辑。
# 修正为直接修改原数组的版本(使用索引递归)
def quicksort_inplace(arr, low, high):
if low < high:
pi = partition(arr, low, high)
quicksort_inplace(arr, low, pi - 1)
quicksort_inplace(arr, pi + 1, high)
def partition(arr, low, high):
pivot = arr[high] # 也可以选择其他位置的元素作为pivot
lt, gt = low, high - 1
i = low
while i <= gt:
if arr[i] < pivot:
arr[i], arr[lt] = arr[lt], arr[i]
lt += 1
i += 1
elif arr[i] > pivot:
arr[i], arr[gt] = arr[gt], arr[i]
gt -= 1
else:
i += 1
arr[lt], arr[high] = arr[high], arr[lt]
return lt
# 使用修正后的版本
arr_inplace = [10, 7, 8, 9, 1, 5]
quicksort_inplace(arr_inplace, 0, len(arr_inplace) - 1)
print(arr_inplace) # 现在直接打印修改后的原数组
2.单边循环法
# 单边排序
def quick_sort(arr):
if len(arr) <= 1:
return arr
else:
pivot = arr[len(arr) // 2] # 选择中间元素作为基准
left = [x for x in arr if x < pivot] # 所有小于基准的元素
middle = [x for x in arr if x == pivot] # 所有等于基准的元素
right = [x for x in arr if x > pivot] # 所有大于基准的元素
return quick_sort(left) + middle + quick_sort(right)
# 如果你想要一个单边循环的版本(即直接在原数组上进行操作的版本),下面是一个例子:
def quick_sort_inplace(arr, low, high):
if low < high:
# partition_index 是分区后基准元素的正确位置
partition_index = partition(arr, low, high)
# 递归地对基准元素左边和右边的子数组进行排序
quick_sort_inplace(arr, low, partition_index - 1)
quick_sort_inplace(arr, partition_index + 1, high)
def partition(arr, low, high):
pivot = arr[high] # 选择最右边的元素作为基准
i = low - 1 # 小于基准的元素的最后一个索引
for j in range(low, high):
# 如果当前元素小于或等于基准
if arr[j] <= pivot:
i += 1
arr[i], arr[j] = arr[j], arr[i] # 交换
arr[i + 1], arr[high] = arr[high], arr[i + 1] # 把基准元素放到中间
return i + 1
# 示例
arr = [10, 7, 8, 9, 1, 5]
quick_sort_inplace(arr, 0, len(arr) - 1)
print("Sorted array is:", arr)
4.4 堆排序(不稳定排序)
4.4.1 堆排序认识
-
最大堆的堆顶是整个堆中最大元素
-
最小堆的堆顶是整个堆中最小元素
堆排序算法的步骤:
-
把无序数组构建成二叉堆。(时间复杂度是O(n))需要从小到大进行排序,则构建成最大堆;需要从大到小进行排序,则构成最小堆。
-
循环删除堆顶元素,替换到二叉堆的末尾,调整堆产生新的堆顶。(时间复杂度为O(nlogn))
-
大顶堆:每一个树的孩子节点值均小于父节点
4.4.2 堆排序的代码实现
堆排序的空间复杂度是O(1),时间复杂度为O(nlogn)
def heapify(arr, n, i):
"""
将索引i处的元素调整为堆顶
:param arr: 列表
:param n: 列表的长度
:param i: 当前调整元素的索引
"""
largest = i # 初始化最大值为根
l = 2 * i + 1 # 左子节点
r = 2 * i + 2 # 右子节点
# 如果左子节点大于根
if l < n and arr[l] > arr[largest]:
largest = l
# 如果右子节点大于目前的最大值
if r < n and arr[r] > arr[largest]:
largest = r
# 如果最大值不是根 说明进行了索引值交换
if largest != i:
arr[i], arr[largest] = arr[largest], arr[i] # 交换
# 递归地调整受影响的子树
heapify(arr, n, largest)
def heapSort(arr):
n = len(arr)
# 构建一个最大堆
for i in range(n // 2 - 1, -1, -1): #从最后一个非叶子节点开始,到顶点结束,递减进行
heapify(arr, n, i)
# 一个个从堆顶取出元素
#取出来堆顶元素与最后一个叶子结点的值进行交换,,然后再进行堆性质
for i in range(n - 1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # 交换
heapify(arr, i, 0)
# 测试代码
if __name__ == "__main__":
arr = [12, 11, 13, 5, 6, 7]
heapSort(arr)
n = len(arr)
print("排序后的数组是")
for i in range(n):
print("%d" % arr[i], end=" ")
4.5 计数排序和桶排序
计数排序;利用数组下标来确定元素的正确位置。它适用于一定范围内的整数排序。在取值范围不是很大的情况下,它的性能甚至快过那些时间复杂度为O(nlogn)的排序。
4.5.1 计数排序
介绍 文心一言
原始数组:arr
计数数组:count 生成一个长度为原始数组中最大值的数组,记录原始数组中相应元素出现的次数
累计数组:count 计数数组的值 + 累计数组上一个的值。例:1+0=1 2+1=3...
最终结果:output 在累计数组中找到原始数组元素,将累计数组-1(累计数组也要-1)得到最终结果的排序
代码表示
# 计数排序
def counting_sort(arr):
# 找到数组中的最大值和最小值
max_val = max(arr)
min_val = min(arr)
# 数组的长度
range_of_elements = max_val - min_val + 1
# 初始化计数数组,长度为范围+1,并全部填充为0
count_arr = [0] * range_of_elements
# 计算每个元素的出现次数
for num in arr:
count_arr[num - min_val] += 1
# 根据计数数组重构原数组
index = 0
for i in range(range_of_elements):
while count_arr[i] > 0:
arr[index] = i + min_val
index += 1
count_arr[i] -= 1
return arr
# 测试代码
if __name__ == "__main__":
arr = [4, 2, 2, 8, 3, 3, 1]
sorted_arr = counting_sort(arr)
print("Sorted array is:", sorted_arr)
适用范围
-
当数列的最大和最小值差距过大时,并不适用于计数排序(例:给出20个随机数,范围在0~1亿之间,这时如果使用计数排序,需要构建长度为1亿的数组,严重浪费空间,时间复杂度也会随之变高)
-
当数列不是整数时,也不适用计数排序(例:如果数列中的元素都是小数,显然无法进行计数排序)
-
对于这些局限性,另一种限行时间排序算法做出弥补,这种排序叫做桶排序。
4.5.2 桶排序
桶排序需要创建若干个桶来协助排序。
代码表示
# 桶排序
def bucket_sort(arr):
if len(arr) <= 1:
return arr
# 1. 找到数组中的最大值和最小值
max_val = max(arr)
min_val = min(arr)
# 2. 计算桶的数量和桶的边界
bucket_range = max_val - min_val + 1
bucket_count = len(arr) if len(arr) < bucket_range else bucket_range
# 3. 初始化桶
buckets = [[] for _ in range(bucket_count)]
# 4. 将数组中的元素分配到各个桶中
for i in arr:
index = int((i - min_val) * bucket_count / bucket_range)
buckets[index].append(i)
# 5. 对每个桶进行排序,这里简单使用Python内置的排序
# 对于每个非空桶,可以使用更高效的排序算法,比如插入排序,
# 因为桶内数据量不大时,插入排序的效率可能更高
for i in range(bucket_count):
buckets[i].sort()
# 6. 合并桶中的数据
sorted_arr = []
for bucket in buckets:
sorted_arr.extend(bucket)
return sorted_arr
# 示例
arr = [0.78, 0.17, 0.39, 0.26, 0.72, 0.94, 0.21, 0.12, 0.23, 0.68]
sorted_arr = bucket_sort(arr)
print("Sorted array:", sorted_arr)
4.6 小结 排序算法归纳
根据算法的时间复杂度、空间复杂度、是否稳定等多维度来做一个归纳。