一、冒泡排序
简介
冒泡排序是一种简单的排序算法,它通过重复地遍历待排序的数列来实现排序。在每次遍历时,算法会比较相邻的两个元素,如果它们的顺序错误(即前一个元素大于后一个元素),则交换它们的位置。这个过程会一直持续到整个数列再无需要交换的元素为止,即数列已经完全排序。
代码实现
def bubble_sort(arr):
# 遍历列表中的每一个元素
for i in range(len(arr)):
# 初始化一个标志位,用于记录本次遍历是否有元素交换
swapped = False
# 从列表的最后一个元素开始向前遍历,每次递减,直到i(因为i后面的元素已经排好序了)
for j in range(len(arr) - 1, i, -1):
# 比较相邻的两个元素
if arr[j - 1] > arr[j]:
# 如果前一个元素大于后一个,则交换它们的位置
arr[j - 1], arr[j] = arr[j], arr[j - 1]
# 设置标志位为True,表示发生了交换
swapped = True
# 如果在整个遍历过程中没有发生任何交换,则说明列表已经是有序的,可以提前结束循环
if not swapped:
break
# 示例调用函数
test_list = [3, 2, 1]
bubble_sort(test_list)
print(test_list) # 输出应该是 [1, 2, 3]
代码讲解
这个函数bubble_sort
接受一个列表arr
作为参数,并试图对其进行升序排序。如果列表在某次遍历中没有发生任何交换,则认为列表已经排序完毕,此时可以直接跳出循环,这样可以提高效率。这种方法在最好情况下(即输入列表已经是排序好的)的时间复杂度为O(n),但在最坏情况下的时间复杂度为O(n^2)。
通过引入一个布尔变量swapped
来记录是否发生了交换,可以避免不必要的迭代,从而在某些情况下提升算法的效率。如果在整个遍历过程中没有发生任何交换,则说明列表已经是有序的,可以提前终止排序过程。
具体步骤如下:
- 外层循环:遍历列表中的每一个元素。
- 内层循环:从列表的最后一个元素开始向前遍历,每次递减,直到当前外层循环的索引
i
(因为i
后面的元素已经排好序了)。 - 比较与交换:比较相邻的两个元素,如果前一个元素大于后一个,则交换它们的位置,并设置
swapped
为True
。 - 提前终止:如果在整个遍历过程中没有发生任何交换,则说明列表已经是有序的,可以提前结束循环。
通过这种方式,冒泡排序能够有效地处理列表排序问题,尽管它的时间复杂度在最坏情况下较高。然而,在实际应用中,冒泡排序因其简单易懂的实现方式仍然有一定的使用价值,尤其是在数据量不大或部分有序的情况下。
二、选择排序
简介
选择排序的基本思想是在未排序序列中找到最小(或最大)的元素,并将其存放到已排序序列的起始位置。这个过程会重复进行,直到所有元素都被排序完毕。选择排序是一种简单直观的比较排序算法。
选择排序的时间复杂度为O(n^2),其中n是列表的长度。尽管其效率低于诸如快速排序或归并排序等更高效的算法,但由于其实现简单,对于小型数据集仍然是一个可行的选择。此算法的一个优点是它仅执行O(n)次交换,而其他一些O(n^2)排序算法如冒泡排序可能执行更多次交换。
代码实现
def selection_sort(arr):
# 遍历列表中的每一个元素
for i in range(len(arr)):
# 假设当前位置i就是最小值的位置
min_index = i
# 内层循环从当前位置i+1开始到最后,寻找最小值
for j in range(i + 1, len(arr)):
# 如果找到比当前位置i更小的值,则更新最小值的位置
if arr[min_index] > arr[j]:
min_index = j
# 如果最小值的位置不是当前位置i,则交换两者
if min_index != i:
arr[i], arr[min_index] = arr[min_index], arr[i]
# 示例调用函数
test_list = [64, 34, 25, 12, 22, 11, 90]
selection_sort(test_list)
print(test_list) # 应输出排序后的列表 [11, 12, 22, 25, 34, 64, 90]
代码讲解
在上面的代码中,我们首先假设当前位置i是最小值的位置,然后通过内层循环找到实际的最小值位置。如果最小值的位置不是当前位置i,则将最小值与当前位置i的值交换。这个过程会一直进行,直到所有元素都被排序完毕。
具体步骤如下:
- 外层循环:遍历列表中的每一个元素。
- 假设最小值位置:假设当前位置i就是最小值的位置。
- 内层循环:从当前位置i+1开始到最后,寻找实际的最小值。
- 更新最小值位置:如果找到比当前位置i更小的值,则更新最小值的位置。
- 交换元素:如果最小值的位置不是当前位置i,则交换两者的位置。
通过这种方式,选择排序逐步将列表中的最小元素移动到已排序部分的末尾,直到整个列表都排序完毕。
值得注意的是,这段代码仅展示了选择排序的一个方向(从小到大排序),如果需要从大到小排序,可以将比较操作符>
改为<
。例如,如果要实现降序排序,只需将内层循环中的判断条件改为if arr[min_index] < arr[j]
。
三、插入排序
简介
插入排序的基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。插入排序是一种简单直观的比较排序算法,它逐步构建有序序列,对于部分有序的数据尤其有效。
插入排序的时间复杂度为O(n^2),其中n是列表的长度。当列表部分有序或者几乎有序时,插入排序的性能会非常好,因为它只需要移动少量的元素就能达到排序的目的。此外,插入排序是一个稳定的排序算法,这意味着相等的元素之间的相对顺序不会改变。
代码实现
def insertion_sort(arr):
# 从第二个元素开始,即索引为1的元素
for i in range(1, len(arr)):
# 假设当前元素为key
key = arr[i]
# 初始化一个指针j,用于与当前元素比较
j = i - 1
# 当指针j大于等于0且当前元素小于其前一个元素时,进行交换
while j >= 0 and key < arr[j]:
# 将前一个元素向后移动一位
arr[j + 1] = arr[j]
j -= 1
# 将当前元素插入到正确的位置
arr[j + 1] = key
# 示例调用函数
test_list = [64, 34, 25, 12, 22, 11, 90]
insertion_sort(test_list)
print(test_list) # 应输出排序后的列表 [11, 12, 22, 25, 34, 64, 90]
代码讲解
这段代码实现了一个简单的插入排序过程,它不断地将每个元素插入到其左侧已经排序好的子列表中,直到整个列表都变得有序。当遇到一个元素比它的前一个元素小时,就会发生交换,并且索引回退一步,这样可以确保每个元素都能找到其正确的位置。当索引不再满足条件(即当前元素大于等于其前一个元素或到达列表头部)时,循环停止。
在这个实现中,我们使用了一个变量key
来存储当前要插入的元素,同时使用指针j
来追踪已经排序的部分。如果当前元素小于其前一个元素,就将前一个元素向后移动一位,直到找到正确的位置。这样可以确保每次循环结束时,当前元素都位于其正确的排序位置。
具体步骤如下:
- 初始化:从第二个元素开始,即索引为1的元素。
- 选取关键元素:将当前元素赋值给变量
key
。 - 比较与移动:使用指针
j
从当前元素的前一个位置开始,向左遍历已排序部分,如果当前元素小于其前一个元素,则将前一个元素向后移动一位。 - 插入:当找到正确的位置后,将
key
插入到该位置。
通过这种方式,插入排序逐步构建了一个有序序列,直到整个列表排序完成。
插入排序适合处理小型数据集或部分有序的数据,因为在这种情况下,它只需要较少的比较和移动操作就能完成排序。虽然插入排序的时间复杂度为O(n^2),但是在最佳情况下(即输入列表已经是排序好的),其时间复杂度可以降低到O(n),因为此时内部循环不会执行。
四、快速排序
简介
快速排序是一种高效的排序算法,采用分治法的策略来进行排序。快速排序的基本思想是选择一个基准元素,然后将数组分为两部分,一部分包含小于基准的所有元素,另一部分包含大于基准的所有元素。接着,递归地对这两部分进行相同的排序操作,直到整个数组排序完成。
快速排序的时间复杂度平均情况下为O(n log n),但是在最坏的情况下(例如初始列表已经完全有序或逆序),时间复杂度会退化到O(n^2)。为了改善这种情况,通常会使用随机选择基准元素或其他技术来优化快速排序算法。
代码实现
def quick_sort(arr, start=0, end=None):
# 参数说明:
# arr: 待排序的列表
# start: 列表的起始索引,默认为0
# end: 列表的结束索引,默认为列表长度减1
if end is None:
end = len(arr) - 1 # 如果end未提供,则设置为列表最后一个元素的索引
if start >= end: # 如果起始索引大于等于结束索引,说明当前区间只有一个元素或为空,无需排序
return
pivot_index = partition(arr, start, end) # 对当前子数组进行划分
# 递归地对基准元素左侧和右侧的子数组进行快速排序
quick_sort(arr, start, pivot_index - 1) # 对左侧子数组进行排序
quick_sort(arr, pivot_index + 1, end) # 对右侧子数组进行排序
# 辅助函数,用于对子数组进行划分
def partition(arr, start, end):
pivot = arr[start] # 选择基准元素
i = start + 1 # 初始化左侧指针
j = end # 初始化右侧指针
while True:
# 从左侧开始,寻找大于基准的元素
while i <= j and arr[i] <= pivot:
i += 1
# 从右侧开始,寻找小于基准的元素
while i <= j and arr[j] >= pivot:
j -= 1
if i <= j:
# 交换元素
arr[i], arr[j] = arr[j], arr[i]
else:
# 结束循环
break
# 将基准元素放置在其最终位置
arr[start], arr[j] = arr[j], arr[start]
return j # 返回基准元素的索引
# 示例调用函数
test_list = [64, 34, 25, 12, 22, 11, 90]
quick_sort(test_list)
print(test_list) # 应输出排序后的列表 [11, 12, 22, 25, 34, 64, 90]
代码讲解
这段代码使用递归来实现快速排序的核心逻辑:选择一个基准元素,然后将数组分为两部分,一部分包含小于基准的所有元素,另一部分包含大于基准的所有元素。然后递归地对这两部分进行同样的操作,直到整个数组排序完成。
在这个实现中:
quick_sort
函数负责递归地对数组进行排序,它接收三个参数:数组arr
、起始索引start
以及结束索引end
。默认情况下,start
为0,end
为数组最后一个元素的索引。partition
函数负责将数组划分为两部分,并返回基准元素的索引。基准元素的选择在此例中为数组的第一个元素。
partition
函数的工作原理如下:
- 初始化:选择基准元素
pivot
为数组的第一个元素,初始化左侧指针i
为start + 1
,右侧指针j
为end
。 - 比较与交换:
- 从左侧开始,寻找大于基准的元素;
- 从右侧开始,寻找小于基准的元素;
- 当左侧指针
i
和右侧指针j
相遇时,将基准元素放置在其最终位置,并返回该位置的索引。
- 循环结束:当左侧指针
i
和右侧指针j
不再满足条件时,循环结束。
通过这种方式,快速排序逐步将数组划分为更小的部分,并对这些部分进行排序,直到整个数组完全有序。
快速排序的性能通常优于其他O(n^2)的排序算法,特别是在数据随机分布的情况下。为了进一步优化快速排序,可以考虑随机选择基准元素或使用三数取中法等技巧来避免最坏情况的发生。
五、计数排序
简介
计数排序是一种非比较型整数排序算法,适用于一定范围内的整数排序。它通过计算各个数值出现的次数来实现排序,适用于数据范围不是特别大的情况。计数排序的时间复杂度为O(n + k),其中n是列表的长度,k是列表中数值的范围大小(最大值与最小值之差加一)。因此,当k不是特别大时,计数排序非常高效。然而,当k远大于n时,计数排序的空间复杂度较高,可能不如其他算法(如快速排序)有效。
特点:
- 计数排序适用于整数排序,并且它是稳定的排序算法,这意味着相等的元素在排序后的顺序与排序前相同。
- 这种排序方法最适合于数据范围较小的情况,并且数据都是非负整数。
- 如果数据中有负数,则需要先调整数据使得最小值为0,然后再应用计数排序。
代码实现
def counting_sort(arr):
# 确定列表中的最小值和最大值
min_val, max_val = min(arr), max(arr)
# 创建一个计数数组,长度为最大值减去最小值加1
# 每个索引对应于一个数值,初始化为0
count = [0] * (max_val - min_val + 1)
# 计算每个数值在原列表中出现的次数
for num in arr:
count[num - min_val] += 1
# 重新构建原列表
index = 0
for i in range(len(count)):
while count[i] > 0:
arr[index] = i + min_val
index += 1
count[i] -= 1
# 示例调用函数
test_list = [4, 2, 2, 8, 3, 3, 1]
counting_sort(test_list)
print(test_list) # 应输出排序后的列表 [1, 2, 2, 3, 3, 4, 8]
代码讲解
这段代码首先确定列表中的最小值和最大值,然后创建一个长度为最大值减去最小值加一的计数数组,用来统计每个数值出现的次数。
具体步骤如下:
- 确定最小值和最大值:使用
min()
和max()
函数找到列表中的最小值和最大值。 - 创建计数数组:根据最小值和最大值创建一个计数数组
count
,数组长度为max_val - min_val + 1
。 - 计算计数:遍历原列表中的每个元素,更新计数数组中相应位置的计数。
- 重建原列表:通过索引
index
来跟踪当前应该放置的位置,并在每次放置后更新计数数组中的计数。
通过这种方式,计数排序能够有效地处理一定范围内的整数排序问题,特别是在数据范围较小的情况下表现优异。
当数据范围较大或数据类型不是整数时,计数排序的适用性和效率可能会受到限制。此外,如果列表中包含负数,可以在应用计数排序之前,通过将所有元素加上一个合适的常数(通常是列表中的绝对最小值的相反数)来调整数据,使得最小值变为非负数。
总结来说,计数排序是一种非常有效的排序算法,特别是当数据范围有限且数据量适中时。它的稳定性和效率使其在某些特定的应用场景下具有优势。
六、桶排序
简介
桶排序是一种分布式的排序算法,它将待排序的数据分散到若干个被称为“桶”的数组中,然后对每个桶分别进行排序(通常使用其他的排序算法),最后把各个桶中的数据合并起来得到排序的结果。
桶排序的时间复杂度通常为O(n + k),其中n是列表的长度,k是桶的数量。理想情况下,当输入数据均匀分布时,桶排序的时间复杂度可以达到O(n)。但是,如果输入数据分布不均匀,某些桶可能会包含大量数据,导致排序这些桶的时间增加,从而使整体时间复杂度上升。
桶排序适用于数据分布均匀且数据量较大的场景。它特别适合处理具有特定分布模式的数据,例如正态分布的数据。此外,桶排序通常用于浮点数排序,也可以用于整数排序,特别是当整数的范围不是特别大时。
代码实现
import math # 导入数学模块,用于计算平方根
def bucket_sort(arr):
# 确定列表中的最小值和最大值
min_val, max_val = min(arr), max(arr)
# 计算桶的数量,这里使用了sqrt方法来决定桶的数量
# 基本思想是让桶的数量与待排序元素的平方根成比例
num_buckets = int(math.sqrt(len(arr)))
# 创建num_buckets个空桶
buckets = [[] for _ in range(num_buckets)]
# 计算桶的范围大小
bucket_range = (max_val - min_val) / num_buckets
# 将每个元素放入对应的桶中
# 元素分配到桶中的索引是元素值减去最小值后除以桶的范围
for num in arr:
# 防止索引越界
index = min(int((num - min_val) / bucket_range), num_buckets - 1)
buckets[index].append(num)
# 对每个桶进行排序
# 这里使用Python内置的sorted()函数进行排序
sorted_arr = []
for bucket in buckets:
sorted_bucket = sorted(bucket)
sorted_arr.extend(sorted_bucket)
# 合并所有桶中的元素
arr[:] = sorted_arr # 将排序后的桶元素赋值给原始列表
# 示例调用函数
test_list = [29, 25, 3, 45, 99, 100, 1, 0, 23, 50]
bucket_sort(test_list)
print(test_list) # 应输出排序后的列表 [0, 1, 3, 23, 25, 29, 45, 50, 99, 100]
代码讲解
这段代码首先确定列表中的最小值和最大值,然后根据数据范围的平方根来计算桶的数量。这样做是为了确保桶的数量适中,既不会太多也不会太少。
具体步骤如下:
- 确定最小值和最大值:使用
min()
和max()
函数找到列表中的最小值和最大值。 - 计算桶的数量:桶的数量由数据范围的平方根决定,这里使用
math.sqrt(len(arr))
来估计桶的数量。 - 创建空桶:创建相应数量的空桶。
- 计算桶的范围大小:桶的范围大小由
(max_val - min_val) / num_buckets
计算得出。 - 分配元素到桶中:遍历每个元素,计算其应分配到哪个桶中,并将元素加入到相应的桶中。
- 对每个桶进行排序:使用Python内置的
sorted()
函数对每个桶进行排序,并将排序后的桶合并到一个新的列表中。 - 合并所有桶中的元素:将排序后的元素赋值给原始列表,以便保持原列表的引用不变。
通过这种方式,桶排序能够有效地处理数据分布较为均匀的情况,特别是在数据量较大且数据范围适中的情况下表现良好。当数据分布不均匀或数据范围过大时,桶排序的效果可能会受到影响。
通过以上实现,桶排序能够有效地处理各种数据分布情况,特别是在数据分布均匀且数据量较大的时候。
七、基数排序
简介
基数排序是一种非比较型整数排序算法,它按照整数的不同数位逐位进行排序。基数排序通常用于对整数或固定长度的字符串进行排序。
基数排序的时间复杂度为O(d * (n + b)),其中d是最大的位数,n是列表的长度,b是基数(在这里为10,因为考虑的是十进制数)。基数排序适用于整数排序,并且是稳定的排序算法,这意味着相等的元素在排序后的顺序与排序前相同。
基数排序的主要优势在于它不需要比较操作,这对于特定类型的数据(如整数或固定长度的字符串)来说是非常有效的。然而,基数排序的缺点是它需要额外的空间来存储临时的桶数组,并且只适用于整数或字符串等固定长度的键值排序。如果数据的范围非常大或位数很多,基数排序的效率可能会受到影响。
代码实现
def radix_sort(arr):
# 确定列表中的最大值
max_num = max(arr)
# 确定最大数的位数
place = 1
while max_num >= 10 ** place:
place += 1
# 逐位进行排序
for i in range(place):
# 创建10个空桶,用于存放0-9这十个数字
buckets = [[] for _ in range(10)]
# 将列表中的每个数字按当前位数的值放入对应的桶中
for num in arr:
# 计算当前位上的数字
radix = int(num / (10 ** i) % 10)
buckets[radix].append(num)
# 重置列表arr,将桶中的元素依次放回列表中
j = 0
for k in range(10):
for num in buckets[k]:
arr[j] = num
j += 1
# 示例调用函数
test_list = [170, 45, 75, 90, 802, 24, 2, 66]
radix_sort(test_list)
print(test_list) # 应输出排序后的列表 [2, 24, 45, 66, 75, 90, 170, 802]
代码讲解
在这段代码中,首先计算出最大数的位数,然后逐位进行排序。每一轮排序都会根据当前位的值将数字分配到相应的桶中,最后再将桶中的数字按顺序放回原列表。这样,经过几轮排序之后,列表就会完全排序好了。
具体步骤如下:
- 确定最大数的位数:通过计算最大值来确定需要排序的位数。
- 创建桶:创建10个空桶,用于存放0到9这十个数字。
- 分配元素到桶中:对于当前位数i,计算每个数字在该位上的值,并将数字放入对应的桶中。
- 收集桶中的元素:将桶中的元素依次放回原列表中。
通过这种方式,基数排序能够有效地处理整数排序问题,特别是在数据范围不是特别大且位数有限的情况下表现优异。当数据范围非常大或位数很多时,基数排序的空间复杂度可能会成为一个问题。
八、希尔排序
简介
希尔排序是插入排序的一种改进版本,它通过允许交换距离较远的元素来加速插入排序的过程。希尔排序的基本思想是先将整个待排序的记录序列分割成若干子序列分别进行直接插入排序,具体分割的方法是通过设定一个增量序列,然后按照增量对序列进行部分排序,最后当增量减小到1时,整个序列就变成了基本有序的状态,此时再进行一次插入排序即可完成排序。
希尔排序的时间复杂度取决于所使用的增量序列。在最佳情况下,希尔排序的时间复杂度可以接近O(n^(3/2)),而在最坏情况下则可能达到O(n^(4/3))。希尔排序的性能高度依赖于增量序列的选择。常见的增量序列有希尔原始增量序列(每次除以2),以及其他如斐波那契数列等。
希尔排序的一个主要优点是它可以在一定程度上避免插入排序在最坏情况下的性能下降。通过首先对较远的元素进行交换,希尔排序能够在更短的时间内使列表部分有序,从而在后续的插入排序中提高效率。
代码实现
def shell_sort(arr):
# 初始化增量dn为列表长度的一半
gap = len(arr) // 2
# 不断减少增量gap,直到gap为1
while gap >= 1:
# 对当前增量gap下的子序列进行插入排序
for i in range(gap, len(arr)):
# 记录当前元素的位置
j = i
# 当前元素小于其gap个位置之前的元素时,进行交换
while j >= gap and arr[j - gap] > arr[j]:
# 交换元素
arr[j], arr[j - gap] = arr[j - gap], arr[j]
# 将指针向前移动gap个位置
j -= gap
# 减少增量gap
gap //= 2
# 示例调用函数
test_list = [64, 34, 25, 12, 22, 11, 90]
shell_sort(test_list)
print(test_list) # 应输出排序后的列表 [11, 12, 22, 25, 34, 64, 90]
代码讲解
在这段代码中,增量gap
最初设置为列表长度的一半,然后逐步减少到1。在每次增量下,都对列表进行类似于插入排序的操作,只不过比较和交换的距离不再是相邻的元素,而是相隔gap
个位置的元素。这样逐步减少增量,直到增量为1时,列表基本上已经有序,这时再进行一次插入排序即可完成排序。
具体步骤如下:
- 初始化增量:将
gap
初始化为列表长度的一半。 - 减少增量:不断将
gap
除以2,直到gap
为1。 - 插入排序:对于当前的
gap
,从索引gap
开始,对每个元素执行插入排序。如果当前元素小于其gap
个位置之前的元素,则进行交换,并将指针向前移动gap
个位置,直到当前元素到达正确位置或到达列表头部。 - 重复上述步骤:直到
gap
为1,此时列表已经基本有序。
通过这种方式,希尔排序能够有效地处理列表排序问题,特别是在数据量较大且初始状态较为无序的情况下。当增量序列选择得当时,希尔排序可以显著提高排序效率。
九、归并排序
简介
归并排序是一种分治算法,它将一个大的问题分成两个或更多的相同或相似的小问题来解决。在归并排序中,原始列表被不断地分割成更小的子列表,直到每个子列表只有一个元素为止。然后,这些单元素子列表被逐步合并成更大的有序列表,直到整个列表完全有序。
归并排序的时间复杂度为O(n log n),其中n是列表的长度。这是因为在每次分割中,列表长度减半,而每次合并都需要线性时间来完成。归并排序是一种稳定的排序算法,这意味着相等的元素在排序后的顺序与排序前相同。
需要注意的是,归并排序在实际应用中可能会消耗较多的内存空间,因为它需要创建新的列表来保存中间结果。如果内存是一个限制因素,可以考虑使用原地排序算法,如快速排序或堆排序。
代码实现
def merge_sort(arr):
# 如果列表长度小于等于1,直接返回列表本身
if len(arr) <= 1:
return arr
# 找到列表的中间位置
mid = len(arr) // 2
# 递归地对左右两半部分进行归并排序
left = merge_sort(arr[:mid]) # 左半部分
right = merge_sort(arr[mid:]) # 右半部分
# 初始化结果列表
result = []
# 初始化左右两个子列表的索引
i = j = 0
# 合并两个有序子列表
while i < len(left) and j < len(right):
if left[i] < right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
# 如果左子列表还有剩余元素,将它们追加到结果列表末尾
if i == len(left):
result.extend(right[j:])
# 如果右子列表还有剩余元素,将它们追加到结果列表末尾
else:
result.extend(left[i:])
return result
# 示例调用函数
test_list = [64, 34, 25, 12, 22, 11, 90]
sorted_list = merge_sort(test_list)
print(sorted_list) # 应输出排序后的列表 [11, 12, 22, 25, 34, 64, 90]
代码讲解
在这段代码中,首先检查列表长度是否小于等于1,如果是,则直接返回列表(单个元素的列表本身就是有序的)。接着,将列表分割成左右两部分,并递归地对这两个部分进行排序。最后,将排序好的两个子列表合并成一个有序的大列表,并返回这个列表。
具体步骤如下:
- 基本情况检查:如果列表长度小于等于1,则直接返回列表。
- 分割列表:找到列表的中间位置,并将列表分割成左右两部分。
- 递归排序:对左右两部分递归地进行归并排序。
- 合并有序子列表:将排序好的左右两部分合并成一个有序的大列表。
通过这种方式,归并排序能够有效地处理列表排序问题,并且保证了排序的稳定性。尽管它需要额外的空间来存储中间结果,但对于大多数应用场景而言,归并排序的高效性和稳定性使其成为一种非常实用的排序算法。
十、堆排序
简介
堆排序是一种基于二叉堆(通常是最大堆)的比较排序算法。堆排序将待排序的序列构造成一个最大堆,然后依次取出最大元素放到序列的末尾,再重新调整剩余元素构成的最大堆,如此反复直至所有元素都排好序。
堆排序的时间复杂度为O(n log n),其中n是列表的长度。这是因为构建初始堆的时间复杂度为O(n),而每次调整堆的时间复杂度为O(log n),总共需要调整n次。
堆排序是一种原地排序算法,除了辅助函数所需的栈空间外,不需要额外的存储空间。然而,由于堆排序涉及到频繁的元素交换,因此在实际应用中,它可能不如归并排序或快速排序那样快。但是,堆排序的优势在于它是一个稳定的算法,并且可以很好地利用缓存。
代码实现
def heap_sort(arr):
# 辅助函数,用于维护最大堆的性质
def big_heap(array, f, r):
root = f # 当前节点的索引
child = root * 2 + 1 # 当前节点的左子节点索引
# 当左子节点索引不超过范围时,循环进行调整
while child <= r:
# 如果右子节点存在并且值大于左子节点,则移动到右子节点
if child + 1 <= r and array[child] < array[child + 1]:
child += 1
# 如果当前节点小于其子节点中的最大值,则交换它们
if array[root] < array[child]:
array[root], array[child] = array[child], array[root]
root = child # 更新当前节点为刚刚交换过的子节点
child = root * 2 + 1 # 更新当前节点的左子节点索引
else:
break # 如果不需要交换,退出循环
# 构建初始的最大堆
first = len(arr) // 2 - 1
for start in range(first, -1, -1):
big_heap(arr, start, len(arr) - 1)
# 排序过程
for end in range(len(arr) - 1, 0, -1):
# 将当前最大元素移到数组末尾
arr[0], arr[end] = arr[end], arr[0]
# 调整剩余元素构成的最大堆
big_heap(arr, 0, end - 1)
return arr # 返回排序后的列表
# 示例调用函数
test_list = [64, 34, 25, 12, 22, 11, 90]
sorted_list = heap_sort(test_list)
print(sorted_list) # 应输出排序后的列表 [11, 12, 22, 25, 34, 64, 90]
代码讲解
在这段代码中,首先定义了一个辅助函数big_heap
,用于维护最大堆的性质。接下来,通过从最后一个非叶子节点开始,逐个调整节点,将整个列表构建成一个最大堆。然后,通过不断将最大元素(即堆顶元素)交换到数组末尾,并调整剩余元素构成的最大堆,直至所有元素都排好序。
具体步骤如下:
- 构建最大堆:从最后一个非叶子节点开始,逐个调整节点,直到根节点,构建出一个最大堆。
- 排序过程:每次将堆顶元素(即当前最大元素)与堆的最后一个元素交换,并将新堆顶元素下沉至合适的位置,以此类推,直到所有元素都排好序。
通过这种方式,堆排序能够有效地处理列表排序问题,并且保证了排序的稳定性。尽管它涉及到频繁的元素交换,但对于大多数应用场景而言,堆排序的高效性和原地排序特性使其成为一种非常实用的排序算法。