数据结构之排序算法 - 实践
一.冒泡排序
对于一个数组,从前往后依次比较相邻的两个元素,将较大的元素放在较小的元素的后面。
假设数组的长度为n,第一次比较需要比较n-1次,第二次在剩下的n-1个元素里面找最大值,需要比较n-2次......直到比较到还剩下一个元素,这时剩下的这个元素就一定是最小的,从小到大的排序任务就完成了。这个过程中一共要遍历数组n-1次(只剩下一个元素时这种情况不用比较,也就不用遍历,所以遍历数组的次数是n-1),而且每次遍历数组时要遍历的长度都在变小,第一次需要全部遍历,第一次遍历完后已经找到了n个元素中的最大值并把它放到了数组的末尾,第二次就不需要比较这个最大的元素了,只需要在剩下的n-1个元素里找次大的,所以第二次遍历数组时不用全部都遍历,只需要遍历n-1个长度,即从第1个元素遍历到倒数第二个元素(第n-1个);然后依次类推
def bubble_sort(arr):
n = len(arr)
for i in range(n-1):
for j in range(n-i-1):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
def bubble_sort_short(arr):
"""短冒泡排序"""
n = len(arr)
for i in range(n-1):
swapped = False # 优化:提前终止标志 值为False说明该轮比较中没有元素交换
for j in range(1, n-i):
if arr[j] < arr[j-1]:
arr[j], arr[j-1] = arr[j-1], arr[j]
swapped = True
if not swapped: # 如果本轮比较没有交换,则说明数组已经有序,直接退出循环
break
if __name__ == "__main__":
result = bubble_sort([2,3,1,0,6,-4,9,-6])
print(result)
a_list = [4,2,-1,9,45,76,-99,3]
bubble_sort_short(a_list) # 进行原地排序
print(a_list)
输出:
[-6, -4, 0, 1, 2, 3, 6, 9]
[-99, -1, 2, 3, 4, 9, 45, 76]
时间复杂度分析:
最好情况下只需比较1次,时间复杂度为o(1)
最坏情况下要遍历n-1次,比较次数依次为:n-1, n-2, n-3, ... , 3, 2, 1 。共要比较次,所以时间复杂度为o(
)
平均情况下要比较 除以 (n-1)也就是
次,所以时间复杂度为o(n)
二.选择排序
选择排序是在冒泡排序的基础上进行了一些改进。改进的地方在于每次遍历数组时只交换一次
要实现这一点,就要在每次遍历数组时找出数组的最大值,然后在该次遍历完之后把这个最大值放在合适的位置上
假设数组元素个数为n,只需遍历n-1次,找出n-1个较大值,剩下的那个元素自然就有序了
def selected_sort_min(arr):
"""找最小值,把最小值放到数组的第一个位置"""
n = len(arr)
for i in range(n-1):
min_idx = i
for j in range(i+1,n):
if arr[j] < arr[min_idx]:
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr
def selected_sort_max(arr):
"""找最大值,把最大值放到数组的末尾"""
n = len(arr)
for i in range(n-1,0,-1): # 序列为(n-1, n-2, n-3, ..., 2, 1)
max_idx = i
for j in range(i):
if arr[j] > arr[max_idx]:
max_idx = j
if max_idx != i: # 只有位置不同时才交换,位置相同说明该次遍历中没有元素比该元素大
arr[i], arr[max_idx] = arr[max_idx], arr[i]
return arr
if __name__ == "__main__":
result = selected_sort_min([1,5,6,2,-6,7,-9,4])
print(result)
result = selected_sort_max([3,1,9,0,4,-5,5])
print(result)
输出:
[-9, -6, 1, 2, 4, 5, 6, 7]
[-5, 0, 1, 3, 4, 5, 9]
时间复杂度分析:
最坏情况下:同冒泡排序一样,为o()
三.插入排序
基本思想:
- 将数组分为“已排序”和“未排序”两部分
- 每次从数组中的“未排序”部分取出一个元素
- 将取出的这个元素插入到“已排序部分”的正确位置
- 重复这个过程直到“未排序”部分的所有元素都被插入到“已排序”部分中
def insertion_sort(arr):
n = len(arr)
for i in range(1,n):
key = arr[i]
j = i - 1
while j >= 0 and arr[j] > key:
arr[j+1] = arr[j]
j -= 1
arr[j+1] = key
return arr
def insertion_sort_swap(arr):
n = len(arr)
for i in range(1,n):
j = i
while j > 0 and arr[j] < arr[j-1]:
arr[j], arr[j-1] = arr[j-1], arr[j]
j -= 1
return arr
def binary_insertion_sort(arr):
"""二分插入排序 - 减少比较次数"""
n = len(arr)
for i in range(1, n):
key = arr[i]
# 使用二分查找找到插入位置
left, right = 0, i - 1
while left <= right:
mid =(left + right) // 2
if arr[mid] > key:
right = mid - 1
else:
left = mid + 1
# 将元素向右移动,为插入腾出空间
for j in range(i - 1, left - 1, -1):
arr[j + 1] = arr[j]
# 插入元素
arr[left] = key
return arr
if __name__ == "__main__":
result = insertion_sort([2,4,1,5,-9,8,3])
result_1 = insertion_sort_swap([1,4,98,-6,0,3,-9])
result_2 = binary_insertion_sort([3,5,9,0,7,-5,2,6])
print(result)
print(result_1)
print(result_2)
四.希尔排序
插入排序的改进版本,也称为“缩小增量排序”
核心思想:通过将原始列表分割为多个子序列,分别对这些子序列进行插入排序,从而使得整个列表逐渐趋于“基本有序”,最后再对整个列表进行一次插入排序。
这样做的原理在于插入排序在对“基本有序”的列表进行排序时,效率非常高,接近o(n)
工作原理:
- 选择增量序列:首先确定一个增量序列。增量是一个间隔,用于将列表划分为子序列。最常用且最简单的增量是初始增量=列表长度/2,然后每次再减半,直到增量为1
- 按增量分组并排序:根据当前增量,将列表分割成多个子序列。每个子序列由相隔“增量”位置的元素组成,然后对每个子序列分别进行插入排序
- 缩小增量:减小增量的值(如减半),重复步骤二
- 最终排序:当增量减小到1时,整个列表被视为一个子序列,进行最后一次插入排序。此时,由于列表已经基本有序,这次插入排序会非常快
举例说明:
对数组[8,3,5,1,4,7,2,6]进行希尔排序
初始数组为[8,3,5,1,4,7,2,6]
长度n=8,选择增量序列gap=n/2, n/4, n/8,.... 即4,2,1
第一轮循环:
增量为4
子序列1:[8,4] ---> [4,8]
子序列2:[3,7] ---> [3,7]
子序列3:[5,2] ---> [2,5]
子序列4:[1,6] --- > [1,6]
分别对这四个子序列进行插入排序,得到右边的排好序的子序列
在将这些排好序的子序列按照原来的位置放回原始数组
得到:
[4,3,2,1,8,7,5,6]
第二轮循环:
增量为2
子序列分别为[4,2,8,5] [3,1,7,6]
分别对这两个子序列进行插入排序得到排好序的子序列:[2,4,5,8] [1,3,6,7]
第二轮循环得到的数组:[2,1,4,3,5,6,8,7]
第三轮循环:
增量为1,此时整个数组就是一个子序列
再对其进行插入排序,得到最终的排好序的数组:[1,2,3,4,5,6,7,8]
def shell_sort(arr):
n = len(arr)
# 初始增量
gap = n // 2
# 循环直到gap缩小到0
while gap > 0:
# 从gap开始,对每个子序列进行插入排序
for i in range(gap,n):
temp = arr[i] # 当前要插入的元素
j = i
# 对子序列进行插入排序
# j >= gap 确保不越界
# arr[j - gap] > temp 是插入排序的比较条件
while j >= gap and arr[j - gap] > temp:
arr[j] = arr[j - gap] # 向后移动元素
j -= gap
arr[j] = temp # 插入到正确位置
# 缩小增量
gap //= 2
return arr
if __name__ == "__main__":
print(shell_sort([2,7,4,-7,9,6,-9,5]))
稳定性:希尔排序不稳定,在排序过程中,相等的原始可能会因为分属不同的子序列而改变相对次序
最坏情况下的时间复杂度:o(n^2)
最好情况下的时间复杂度: o(nlogn)
六.归并排序
核心思想:分而治之
把一个复杂的大问题,分解成若干个简单的小问题,然后逐个解决,最后将解决的小问题合并起来,就得到了大问题的答案
具体到排序上,分为三个步骤:
- 分:将待排序的数组递归地“分割”成两个子数组,直到每个子数组只剩下一个元素(一个元素的数组本身就是有序的)
- 治:将两个已经有序子数组“合并”成一个新的有序数组
- 合:不断地进行合并操作,直到最终合并成一个完整的有序数组
分布详解:
假设要排序的数组为[38, 27, 43, 3, 9, 82, 10]
第一步:分(Split)
不断地从中间将数组拆分成两半,直到每个子数组只有一个元素
初始数组:[38, 27, 43, 3, 9, 82, 10]
第一次分:[38, 27, 43]和[3,9,82,10]
第二次分: [38] [27, 43] 和 [3, 9] [82, 10]
第三次分:[38] [27] [43] 和 [3] [9] [82] [10]
现在,我们得到了7个只有一个元素的子数组:[38] [27] [43] [3] [9] [82] [10]
关键点:一个元素的数组天然就是有序的。所以现在我们有7个已经“排好序”的小数组
第二步:治(Merge)- 核心操作
现在开始“合并”,合并的过程就是排序的过程。如何将两个有序的数组合并成一个新的有序数组呢?
合并两个有序数组的算法:
1.创建一个临时数组,用来存放合并后的结果
2.设定两个指针,分别指向两个待合并数组的起始位置
3.比较两个指针所指的元素,将较小的那个元素放入临时数组,并将该指针向右移动一位
4.重复步骤3,直到其中一个数组的所有元素都被取完
5.将另一个数组的剩余所有元素直接追加到临时数组的末尾
6.将临时数组拷贝回原数组的对应位置
第三步:合(Combine)- 逐层合并
我们从最底层的一个元素数组开始合并,凉凉合并
第一层合并(合并单个元素)
合并[38]和[27] --> 比较38和27,27小,所以结果是[27, 38]
合并[43]和[3] --> 比较43和3,3小,所以结果是[3, 43]
合并[9]和[82] --> [9, 82]
[10]暂时没有可合并的,留到下一轮
此时数组状态:[27, 38], [3, 43], [9, 82], [10]
第二层合并:
合并[27, 38]和[3, 43] --> 比较27和3,取3;比较27和43,取27;比较38和43,取38;剩余[43],直接追加 --> 结果是:[3,27,38,43]
合并[9, 82]和[10] --> 比较9和10,取9;比较82和10,取10;剩余[82],直接追加到临时数组中 --> 结果是:[9, 10, 82]
此时数组状态:[3, 27, 38, 43] [9, 10, 82]
第三次合并(最终合并):
合并[3, 27, 38, 43] 和 [9, 10, 82] --> 比较3和9,取3;比较27和9,取9;比较27和10,取10;比较27和82,取27;比较38和82,取38;比较43和82,取43;剩余[82],直接追加 --> 最终结果:[3, 9, 10, 27, 38, 43, 82]
def merge_sort(arr):
"""
归并排序主函数
"""
# 递归终止函数:数组长度为1或0时,已经是有序的
if len(arr) <= 1:
return arr
# 1.分:找到中间点,分割数组
mid = len(arr) // 2
left_arr = arr[:mid] # 左半部分
right_arr = arr[mid:] # 右半部分
# 递归地对左右两部分进行排序
left_sorted = merge_sort(left_arr)
right_sorted = merge_sort(right_arr)
# 2.治:合并两个有序数组
return merge(left_sorted,right_sorted)
def merge(left,right):
"""
合并两个有序数组
"""
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
# 将剩余元素添加到结果数组中
# 以下两个while循环只会执行其中一个
while i < len(left):
result.append(left[i])
i += 1
while j < len(right):
result.append(right[j])
j += 1
return result
# 测试示例
if __name__ == "__main__":
# 测试用例
test_arr = [38, 27, 43, 3, 9, 82, 10]
print("原始数组:", test_arr)
sorted_arr = merge_sort(test_arr)
print("排序结果:", sorted_arr)
# 更多测试
test_cases = [
[5, 2, 4, 6, 1, 3],
[1],
[],
[3, 1, 2],
[9, 8, 7, 6, 5, 4, 3, 2, 1]
]
for i, case in enumerate(test_cases):
print(f"测试用例 {i+1}: {case} -> {merge_sort(case)}")
#### 上面的版本会创建很多新数组,下面是一个更优化的原地排序版本:
def merge_sort_inplace(arr):
"""
原地归并排序版本(修改原数组)
"""
if len(arr) <= 1:
return arr
# 使用辅助函数进行递归
_merge_sort_helper(arr,0,len(arr)-1)
return arr
def _merge_sort_helper(arr,left,right):
"""
递归辅助函数
"""
if left < right:
mid = (left + right) // 2
# 递归排序左右两部分
_merge_sort_helper(arr,left,mid)
_merge_sort_helper(arr,mid+1,right)
# 合并已排序的两部分
_merge_inplace(arr,left,mid,right)
def _merge_inplace(arr,left,mid,right):
"""
原地合并两个有序子数组arr[left:mid]和arr[mid+1:right]
"""
# 创建临时数组存放要合并的数据
temp = []
i,j = left,mid+1
# 比较并合并
while i <= mid and j <= right:
if arr[i] <= arr[j]:
temp.append(arr[i])
i += 1
else:
temp.append(arr[j])
j += 1
# 添加剩余元素
while i <= mid:
temp.append(arr[i])
i += 1
while j <= right:
temp.append(arr[j])
j += 1
# 将临时数组的内容复制回原数组
for k in range(len(temp)):
arr[left+k] = temp[k]
# 测试原地排序版本
if __name__ == "__main__":
print("\n=== 原地排序版本测试 ===")
test_arr = [38, 27, 43, 3, 9, 82, 10]
print("原始数组:", test_arr)
# 注意:这个版本会修改原数组
original = test_arr.copy()
merge_sort_inplace(test_arr)
print("排序结果:", test_arr)
print("原数组已被修改:", original != test_arr)

浙公网安备 33010602011771号