常见算法实现系列01 - 排序算法

常见排序算法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 原理

​ 快速排序的主要思想是分治法,将一个大问题分割成小问题,解决小问题后再合并它们的结果。

  1. 选择基准:从数组中选择一个元素作为"基准"
  2. 分区操作:将数组重新排列,所有比基准小的元素放在基准前面,所有比基准大的元素放在基准后面
  3. 递归排序:递归地对基准前后的子数组进行快速排序
  4. 当所有子数组都有序时,整个数组就自然有序了。
  • 时间复杂度:平均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 原理

​ 归并排序是一种基于分治策略的高效排序算法,核心思想是"分而治之":

  1. :将数组递归地分成两半,直到每个子数组只有一个元素
  2. :将两个已排序的子数组合并成一个有序数组
  3. :重复合并过程,直到整个数组有序
  • 时间复杂度: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. 构建最大堆:将无序数组构建成最大堆,这样最大的元素就在堆顶(数组的第一个位置)。
  2. 排序:
  • 将堆顶元素(最大值)与堆的最后一个元素交换
  • 堆的大小减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. 总结

​ 可以多写几次,达到默写的境界,不过建议结合理解算法本身去实现,更容易长久记住。

相关推荐
2301_764441332 小时前
Python常见的排序算法及其特点和实现代码
python·算法·排序算法
ones~2 小时前
Python 简单算法题精选与题解汇总
数据结构·python·算法
Madison-No72 小时前
【C++】string类的常见接口的使用
开发语言·c++·算法
一只雄牧慕3 小时前
【C++】哈希表
开发语言·数据结构·c++·散列表
不枯石3 小时前
Matlab通过GUI实现点云的统计滤波(附最简版)
开发语言·图像处理·算法·计算机视觉·matlab
koddnty4 小时前
数据结构:字符串匹配 kmp算法
算法
-一杯为品-4 小时前
【足式机器人算法】#1 强化学习基础
算法·机器人
叫我詹躲躲4 小时前
🌟 回溯算法原来这么简单:10道经典题,一看就明白!
前端·算法·leetcode
LAOLONG-C5 小时前
LeetCode算法“无重复字符的最长子串”哈希表+滑动窗口+贪心
算法·哈希算法·散列表