常见排序算法python实现
1. 前言
在刷leetcode题目的时候,发现秋招也会出现一些常见排序算法实现的问题,因此这里进行统一实现,方便后期复习。
实现语言:Python。
目录
文章目录
-
- 常见排序算法python实现
-
- [1. 前言](#1. 前言)
- [2. 冒泡排序](#2. 冒泡排序)
-
- [2.1 原理](#2.1 原理)
- [2.2 实现](#2.2 实现)
- [3. 选择排序](#3. 选择排序)
-
- [3.1 原理](#3.1 原理)
- [3.2 实现](#3.2 实现)
- [4. 插入排序](#4. 插入排序)
-
- [4.1 原理](#4.1 原理)
- [4.2 实现](#4.2 实现)
- [5. 快速排序](#5. 快速排序)
-
- [5.1 原理](#5.1 原理)
- [5.2 实现](#5.2 实现)
- [6. 归并排序](#6. 归并排序)
-
- [6.1 原理](#6.1 原理)
- [6.2 实现](#6.2 实现)
- [7. 堆排序](#7. 堆排序)
-
- [7.1 原理](#7.1 原理)
- [7.2 实现](#7.2 实现)
- [8. 总结](#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 实现
通过上面的算法,发现是一个双重循环,第一重循环就是从开头到末尾,第二重循环就是从开头到后面已经排序后的前一个,然后再执行循环过程中,判断两者元素大小,是否需要交换位置即可,很简单:
从小到大排序
python
# 冒泡排序:从小到大
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]
从大到小排序
注意的区别就是判断大小,与上面的唯一区别就是:
python
if arr[j] > arr[j+1]:
## 变为
if arr[j] < arr[j+1]:
3. 选择排序
3.1 原理
每次从未排序部分选择最小(或最大)元素放到已排序部分的末尾。
- 时间复杂度:O(n²)
- 空间复杂度:O(1)
3.2 实现
首先,第一重循环肯定是从头到尾(索引为i),第二重循环就是从i+1到末尾,然后更新其中的最大或者最小值,然后移动即可。
实现:
python
# 选择排序
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循环实现即可。
python
# 插入排序
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] = key
return 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的时候,肯定自然而然有序,就结束了
- 子问题:对于当前数组的排序,变为基准元素、左子数组、右子数组排序的问题
- 基准元素:这里我们定义为中间元素为基准元素
python
# 快速排序
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))
而实现从大到小排序的关键就是:
python
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两个列表,需要把两者拼接起来,并且大小顺序对应即可。
开始实现:
python
# 归并排序
def merge_sort(arr):
# 整体框架
if len(arr) <= 1:
return arr
# 开始处理
mid = len(arr)//2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
# 合并两者
return merge(left,right)
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
# 把两者剩下的部分合并,因为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))
实现从大到小的排列,就是改变:
python
if left[i] < right[j]:
## 变为
if left[i] > right[j]:
7. 堆排序
7.1 原理
堆排序是一种基于二叉堆数据结构的比较排序算法。它结合了插入排序和归并排序的优点:像归并排序一样时间复杂度为O(n log n),像插入排序一样是原地排序。
堆是一种特殊的完全二叉树,满足:
- 最大堆:每个节点的值都大于或等于其子节点的值
- 最小堆:每个节点的值都小于或等于其子节点的值
因此,算法流程(构建从小到大):
- 构建最大堆:将无序数组构建成最大堆,这样最大的元素就在堆顶(数组的第一个位置)。
- 排序:
- 将堆顶元素(最大值)与堆的最后一个元素交换
- 堆的大小减1(排除已排序的最大值)
- 对新的堆顶元素进行"堆化"(下沉操作),恢复最大堆性质
- 重复步骤1-3,直到堆大小为1
7.2 实现
首先,我们得把数组构建成一个最大堆,即当前结点值大于其所有的子节点值,然后再把最大堆构建为最小堆,即可实现从小到大排序。
首先定义一个函数,去实现最大堆和最小堆,再定义一个函数,去实现二叉树的替换,保证成功构建最大堆。
python
# 堆排序
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,也就是c
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)
return arr
def 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. 总结
可以多写几次,达到默写的境界,不过建议结合理解算法本身去实现,更容易长久记住。