常见算法实现系列01 - 排序算法
常见排序算法python实现
1. 前言
在刷leetcode题目的时候,发现秋招也会出现一些常见排序算法实现的问题,因此这里进行统一实现,方便后期复习。
实现语言:Python。
目录
文章目录
- 常见排序算法python实现
- 1. 前言
- 2. 冒泡排序
- 2.1 原理
- 2.2 实现
- 3. 选择排序
- 3.1 原理
- 3.2 实现
- 4. 插入排序
- 4.1 原理
- 4.2 实现
- 5. 快速排序
- 5.1 原理
- 5.2 实现
- 6. 归并排序
- 6.1 原理
- 6.2 实现
- 7. 堆排序
- 7.1 原理
- 7.2 实现
- 8. 总结
2. 冒泡排序
2.1 原理
冒泡排序是一种简单直观的排序算法,它重复地遍历要排序的列表,比较相邻的元素,如果它们的顺序错误就交换它们。
比如:
原本:5 4 6 1 3 2,从小到大排列
迭代第一次:
(1) 4 5 6 1 3 2,判断依据:4<5,换位置
(2) 4 5 6 1 3 2,判断依据:5<6,不换位置
(3) 4 5 1 6 3 2,判断依据:1<6,换位置
(4) 4 5 1 3 6 2,判断依据:3<6,换位置
(5) 4 5 1 3 2 6,判断依据:2<6,换位置
这就是第一次,然后继续迭代即可
- 时间复杂度:O(n²),因为是双重循环
- 空间复杂度:O(1),因为没有创建新的遍历,利用的原数组修改
2.2 实现
通过上面的算法,发现是一个双重循环,第一重循环就是从开头到末尾,第二重循环就是从开头到后面已经排序后的前一个,然后再执行循环过程中,判断两者元素大小,是否需要交换位置即可,很简单:
从小到大排序
# 冒泡排序:从小到大
def bubble_sort(arr):n = len(arr)for i in range(n):# 二重循环:访问到还没排好的位置即可for j in range(0,n-i-1):if arr[j] > arr[j+1]:# 交换位置arr[j],arr[j+1] = arr[j+1],arr[j]return arr# 尝试
arr = [64, 34, 25, 12, 22, 11, 90]
print(bubble_sort(arr))
# 输出:[11, 12, 22, 25, 34, 64, 90]
从大到小排序
注意的区别就是判断大小,与上面的唯一区别就是:
if arr[j] > arr[j+1]:
## 变为
if arr[j] < arr[j+1]:
3. 选择排序
3.1 原理
每次从未排序部分选择最小(或最大)元素放到已排序部分的末尾。
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
3.2 实现
首先,第一重循环肯定是从头到尾(索引为i),第二重循环就是从i+1到末尾,然后更新其中的最大或者最小值,然后移动即可。
实现:
# 选择排序
def selection_sort(arr):n = len(arr)# 第一重循环for i in range(n):mn = i # or mx = i# 第二重循环for j in range(i+1,n):# 如果遇到更小的,就更新if arr[j] < arr[mn]: # or arr[j] > arr[mx]mn = j# 此时,找到了最小的,更新位置arr[i],arr[mn] = arr[mn],arr[i]return arr# 尝试
arr = [64, 34, 25, 12, 22, 11, 90]
print(selection_sort(arr))
# 输出
[11, 12, 22, 25, 34, 64, 90]
4. 插入排序
4.1 原理
将数组分为"已排序"和"未排序"两部分,每次从未排序部分取出一个元素,插入到已排序部分的正确位置,直到所有元素都排序完成。
它的工作方式类似于我们整理扑克牌:每次拿起一张牌,插入到手中已排序牌组的正确位置。
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
4.2 实现
分析一下这个算法。
首先,肯定是双重循环,第一重循环仍然是从头到尾遍历(索引为i),第二重循环,需要不停遍历,找到当前元素i在已经排序的列表中的正确位置,这个肯定通过一个while循环实现即可。
# 插入排序
def insertion_sort(arr):# 第一重遍历n = len(arr)for i in range(1,n):# 当前要插入的元素key = arr[i]# 第二重循环j = i-1# 开始找到正确位置:从小到大排序while j >= 0 and key < arr[j]:arr[j+1] = arr[j] # 向后移动j -= 1# 找到了,交换arr[j+1] = keyreturn arr# 尝试
arr = [64, 34, 25, 12, 22, 11, 90]
print(insertion_sort(arr))
5. 快速排序
5.1 原理
快速排序的主要思想是分治法,将一个大问题分割成小问题,解决小问题后再合并它们的结果。
- 选择基准:从数组中选择一个元素作为"基准"
- 分区操作:将数组重新排列,所有比基准小的元素放在基准前面,所有比基准大的元素放在基准后面
- 递归排序:递归地对基准前后的子数组进行快速排序
- 当所有子数组都有序时,整个数组就自然有序了。
- 时间复杂度:平均O(n log n),最坏O(n²)
- 空间复杂度:O(log n)
5.2 实现
简单来说就是一个递归算法,既然是递归,肯定得有子问题、边界条件。
- 边界条件:从上面不难看出,当子数组的长度是1的时候,肯定自然而然有序,就结束了
- 子问题:对于当前数组的排序,变为基准元素、左子数组、右子数组排序的问题
- 基准元素:这里我们定义为中间元素为基准元素
# 快速排序
def quick_sort(arr):# 边界条件if len(arr) <= 1:return arr# 基准元素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)# 尝试
arr = [64, 34, 25, 12, 22, 11, 90]
print(quick_sort(arr))
而实现从大到小排序的关键就是:
return quick_sort(left) + middle + quick_sort(right)
## 改为
return quick_sort(right) + middle + quick_sort(left)
6. 归并排序
6.1 原理
归并排序是一种基于分治策略的高效排序算法,核心思想是"分而治之":
- 分:将数组递归地分成两半,直到每个子数组只有一个元素
- 治:将两个已排序的子数组合并成一个有序数组
- 合:重复合并过程,直到整个数组有序
- 时间复杂度:O(n log n)
- 空间复杂度:O(n):创建了一个result列表存放结果
6.2 实现
这个原理和思路简单易懂,但是实现起来比较复杂。
首先,整体分为两个部分:拆分部分 + 合并部分。
然后,理清整个递归的流程:
- 边界条件:当划分到只有一个元素的时候,返回当前列表
- 递归问题:左半部分元素 + 右半部分元素,然后将两者组合
然后,关于组合问题:传入的是left、right两个列表,需要把两者拼接起来,并且大小顺序对应即可。
开始实现:
# 归并排序
def merge_sort(arr):# 整体框架if len(arr) <= 1:return arr# 开始处理mid = len(arr)//2left = merge_sort(arr[:mid])right = merge_sort(arr[mid:])# 合并两者return merge(left,right)def merge(left,right):# 合并两者result = []i = j = 0while i < len(left) and j < len(right):if left[i] < right[j]:result.append(left[i])i += 1else:result.append(right[j])j += 1# 把两者剩下的部分合并,因为left、right的长度可能不相同,因此while结束后# 可能left、right中还有剩余元素result.extend(left[i:])result.extend(right[j:])return result# 尝试
arr = [64, 34, 25, 12, 22, 11, 90]
print(merge_sort(arr))
实现从大到小的排列,就是改变:
if left[i] < right[j]:
## 变为
if left[i] > right[j]:
7. 堆排序
7.1 原理
堆排序是一种基于二叉堆数据结构的比较排序算法。它结合了插入排序和归并排序的优点:像归并排序一样时间复杂度为O(n log n),像插入排序一样是原地排序。
堆是一种特殊的完全二叉树,满足:
- 最大堆:每个节点的值都大于或等于其子节点的值
- 最小堆:每个节点的值都小于或等于其子节点的值
因此,算法流程(构建从小到大):
- 构建最大堆:将无序数组构建成最大堆,这样最大的元素就在堆顶(数组的第一个位置)。
- 排序:
- 将堆顶元素(最大值)与堆的最后一个元素交换
- 堆的大小减1(排除已排序的最大值)
- 对新的堆顶元素进行"堆化"(下沉操作),恢复最大堆性质
- 重复步骤1-3,直到堆大小为1
7.2 实现
首先,我们得把数组构建成一个最大堆,即当前结点值大于其所有的子节点值,然后再把最大堆构建为最小堆,即可实现从小到大排序。
首先定义一个函数,去实现最大堆和最小堆,再定义一个函数,去实现二叉树的替换,保证成功构建最大堆。
# 堆排序
def heap_sort(arr):n = len(arr)# 构建最大堆# 从最后一个非叶子节点开始,依次向下调整# [a,b,c,d,e,f,g],变成二叉树,就是# a - b\c# b - d\e# c - f\g# 因此,最后一个非叶子节点就是n//2-1,也就是cfor 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)return arrdef heapify(arr,n,i):"""堆化函数:确保以i为根的子树满足最大堆性质n: 堆的大小i: 当前节点的索引"""largest = i # 初始化最大值为当前节点left = 2 * i + 1 # 左子节点right = 2 * i + 2 # 右子节点# 如果左子节点存在且大于根节点if left < n and arr[left] > arr[largest]:largest = left# 如果右子节点存在且大于当前最大值if right < n and arr[right] > arr[largest]:largest = right# 如果最大值不是当前节点,需要交换并递归堆化if largest != i:arr[i], arr[largest] = arr[largest], arr[i]# 递归堆化受影响的子树heapify(arr, n, largest)# 尝试
arr = [64, 34, 25, 12, 22, 11, 90]
print(heap_sort(arr))
8. 总结
可以多写几次,达到默写的境界,不过建议结合理解算法本身去实现,更容易长久记住。