1. 引言

一个程序员一生中可能会邂逅各种各样的算法,但总有那么几种,是作为一个程序员一定会遇见且大概率需要掌握的算法。今天就来聊聊这些十分重要的"必抓!"算法吧~

作为程序员,了解和掌握各种算法对于解决各种计算机科学问题至关重要。以下是几个常见而重要的算法。

下表汇总了本文将要介绍的 7 个核心算法,涵盖所属类别、典型应用场景与复杂度信息,方便读者快速建立整体认知:

算法名称所属类别典型应用场景平均时间复杂度空间复杂度
冒泡排序排序小规模数据排序、教学演示O(n²)O(1)
快速排序排序大规模数据排序、系统库排序函数O(n log n)O(log n)
二分搜索搜索有序数组中的快速查找O(log n)O(1)
Dijkstra 算法非负权图中的单源最短路径(如导航、路由)O((V + E) log V)O(V)
0-1 背包问题动态规划资源分配、投资组合、装载优化O(n × capacity)O(n × capacity)
活动选择问题贪心会议室排期、任务调度O(n log n)O(n)
KMP 算法字符串匹配文本检索、关键词匹配、DNA 序列比对O(n + m)O(m)

下面是本文 7 个核心算法的分类总览图,帮助读者快速建立整体认知:

核心算法

排序算法

搜索算法

图算法

动态规划

贪心算法

字符串匹配

冒泡排序

快速排序

二分搜索

Dijkstra 算法

0-1 背包问题

活动选择问题

KMP 算法

2. 排序算法

排序算法用于将一组数据按照一定的顺序进行排列。常见的排序算法包括冒泡排序、选择排序、插入排序、归并排序、快速排序等。

下表对比了常见排序算法的时间复杂度与空间复杂度:

排序算法最好时间复杂度平均时间复杂度最坏时间复杂度空间复杂度
冒泡排序O(n)O(n²)O(n²)O(1)
选择排序O(n²)O(n²)O(n²)O(1)
插入排序O(n)O(n²)O(n²)O(1)
归并排序O(n log n)O(n log n)O(n log n)O(n)
快速排序O(n log n)O(n log n)O(n²)O(log n)

下面是冒泡排序的算法流程示意图:

输入待排序数组

初始化 i = 0

i < n - 1?

输出排序结果

初始化 swapped = False

j < n - 1 - i?

swapped 为 False?

i = i + 1

arr[j] > arr[j+1]?

交换 arr[j] 与 arr[j+1]

swapped = True

j = j + 1

下面给出冒泡排序和快速排序的 Python 实现,并附上复杂度说明:

冒泡排序

def bubble_sort(arr):
    """
    冒泡排序
    时间复杂度:最好 O(n),平均 O(n²),最坏 O(n²)
    空间复杂度:O(1)
    """
    n = len(arr)
    for i in range(n - 1):
        swapped = False
        for j in range(n - 1 - i):
            if arr[j] > arr[j + 1]:
                arr[j], arr[j + 1] = arr[j + 1], arr[j]
                swapped = True
        # 若本轮无交换,说明已有序,提前结束
        if not swapped:
            break
    return arr

# 示例
print(bubble_sort([64, 34, 25, 12, 22, 11, 90]))

快速排序

def quick_sort(arr):
    """
    快速排序
    时间复杂度:最好 O(n log n),平均 O(n log n),最坏 O(n²)
    空间复杂度:O(log n)(递归调用栈)
    """
    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)

# 示例
print(quick_sort([64, 34, 25, 12, 22, 11, 90]))

下面是快速排序的分治流程示意图:

输入待排序数组

数组长度 ≤ 1?

直接返回数组

选取基准元素 pivot

划分:left < pivot

划分:middle = pivot

划分:right > pivot

递归排序 left

递归排序 right

合并结果

输出排序结果

3. 搜索算法

搜索算法用于在数据集中查找特定的元素或解决特定的问题。常见的搜索算法包括线性搜索、二分搜索、广度优先搜索、深度优先搜索等。

下面给出二分搜索的 Python 实现,并附上复杂度说明:

二分搜索

def binary_search(arr, target):
    """
    二分搜索
    适用条件:数组必须是有序的(升序或降序)
    时间复杂度:O(log n)
    空间复杂度:O(1)
    """
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1  # 未找到

# 示例(数组必须有序)
sorted_arr = [11, 12, 22, 25, 34, 64, 90]
print(binary_search(sorted_arr, 25))  # 输出: 3
print(binary_search(sorted_arr, 100)) # 输出: -1

下面是二分搜索的查找流程示意图:

输入有序数组与目标值

初始化 left = 0, right = n - 1

left ≤ right?

返回 -1(未找到)

计算 mid = (left + right) / 2

arr[mid] == target?

返回 mid

arr[mid] < target?

left = mid + 1

right = mid - 1

4. 图算法

图算法用于解决与图相关的问题,比如最短路径问题、最小生成树问题和网络流问题等。常见的图算法包括 Dijkstra 算法、Bellman-Ford 算法、Kruskal 算法、Prim 算法和 Floyd-Warshall 算法等。

下面给出 Dijkstra 算法的 Python 实现,使用邻接表表示图,并通过优先队列(最小堆)优化,附上复杂度说明:

Dijkstra 算法

import heapq

def dijkstra(graph, start):
    """
    Dijkstra 最短路径算法(优先队列优化)
    适用条件:图中边的权重必须为非负值
    时间复杂度:O((V + E) log V),其中 V 为顶点数,E 为边数
    空间复杂度:O(V)
    """
    # 初始化距离字典,起点距离为 0,其余顶点为无穷大
    distances = {node: float('inf') for node in graph}
    distances[start] = 0

    # 优先队列(最小堆),存储 (距离, 顶点)
    pq = [(0, start)]

    while pq:
        current_dist, current = heapq.heappop(pq)

        # 若当前距离大于已记录的最短距离,则跳过
        if current_dist > distances[current]:
            continue

        # 遍历当前顶点的所有邻接顶点
        for neighbor, weight in graph[current].items():
            distance = current_dist + weight
            # 若找到更短路径,则更新并加入优先队列
            if distance < distances[neighbor]:
                distances[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor))

    return distances

# 示例:使用邻接表表示图
graph = {
    'A': {'B': 1, 'C': 4},
    'B': {'A': 1, 'C': 2, 'D': 5},
    'C': {'A': 4, 'B': 2, 'D': 1},
    'D': {'B': 5, 'C': 1}
}

# 运行示例:计算从 A 出发到各顶点的最短距离
print(dijkstra(graph, 'A'))
# 输出: {'A': 0, 'B': 1, 'C': 3, 'D': 4}

下面是 Dijkstra 算法的执行流程示意图:

初始化:起点距离为 0,其余为无穷大

将起点加入优先队列

优先队列为空?

输出所有顶点的最短距离

弹出距离最小的顶点 u

u 的距离已过期?

遍历 u 的所有邻接顶点 v

dist[u] + w < dist[v]?

更新 dist[v] 并入队

所有邻接顶点处理完毕

5. 动态规划算法

动态规划算法用于解决具有重叠子问题性质的问题,在这种问题中,可以通过将其分解为较小的子问题来有效地解决。常见的动态规划算法包括背包问题、最长公共子序列问题和最优矩阵链乘法等。

下面给出经典的 0-1 背包问题的 Python 实现,包含状态转移方程说明,并附上复杂度说明:

0-1 背包问题

def knapsack(weights, values, capacity):
    """
    0-1 背包问题(动态规划)
    状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j - weights[i-1]] + values[i-1])
    其中 dp[i][j] 表示前 i 件物品在容量为 j 的背包中能获得的最大价值
    时间复杂度:O(n * capacity),其中 n 为物品数量
    空间复杂度:O(n * capacity),可优化为 O(capacity)
    """
    n = len(weights)
    # dp[i][j] 表示前 i 件物品放入容量为 j 的背包的最大价值
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            if weights[i - 1] <= j:
                # 放得下:取「不放」与「放」的较大值
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
            else:
                # 放不下:只能继承前 i-1 件物品的结果
                dp[i][j] = dp[i - 1][j]

    return dp[n][capacity]

# 示例:物品重量与价值
weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
capacity = 5

# 运行示例
print(knapsack(weights, values, capacity))
# 输出: 7(选择重量 2 和 3 的物品,价值 3 + 4 = 7)

6. 贪心算法

贪心算法通过在每个阶段都做出当前最优选择来解决问题。尽管贪心算法通常无法获得全局最优解,但它们经常用于解决一些特定类型的问题,如活动选择问题和霍夫曼编码问题等。

下面给出经典的活动选择问题的 Python 实现,包含问题描述、算法思路与复杂度说明:

活动选择问题

def activity_selection(start, finish):
    """
    活动选择问题(贪心算法)
    问题描述:给定 n 个活动,每个活动 i 有开始时间 start[i] 和结束时间 finish[i],
              同一时间只能进行一个活动,求能参加的最多活动数量。
    算法思路:按结束时间升序排序,每次选择结束最早且与已选活动不冲突的活动,
              从而为后续活动留下尽可能多的时间。
    时间复杂度:O(n log n)(排序),若已按结束时间排序则为 O(n)
    空间复杂度:O(n)(存储所选活动)
    """
    n = len(start)
    # 按结束时间升序排序(若输入未排序)
    activities = sorted(zip(start, finish), key=lambda x: x[1])

    selected = [activities[0]]  # 第一个活动必选
    last_finish = activities[0][1]

    for i in range(1, n):
        # 若当前活动开始时间不早于上一个已选活动的结束时间,则选择
        if activities[i][0] >= last_finish:
            selected.append(activities[i])
            last_finish = activities[i][1]

    return selected

# 示例:活动编号 0~5,对应 (开始时间, 结束时间)
start = [1, 3, 0, 5, 8, 5]
finish = [2, 4, 6, 7, 9, 9]

# 运行示例
result = activity_selection(start, finish)
print("选择的活动(开始, 结束):", result)
print("最多可参加的活动数量:", len(result))
# 输出: 选择的活动(开始, 结束): [(1, 2), (3, 4), (5, 7), (8, 9)]
#       最多可参加的活动数量: 4

7. 其他重要算法

此外,还有许多其他重要的算法,如字符串匹配算法(如 KMP 算法、Boyer-Moore 算法)、压缩算法(如哈夫曼压缩、LZW 压缩)、图像处理算法(如边缘检测算法、图像分割算法)等。

下面给出 KMP 字符串匹配算法的 Python 实现,包含 next 数组的构建过程与复杂度说明:

KMP 字符串匹配算法

def build_next(pattern):
    """
    构建 next 数组(部分匹配表)
    next[i] 表示 pattern[:i] 的最长相等前后缀长度
    时间复杂度:O(m),其中 m 为模式串长度
    """
    m = len(pattern)
    next_arr = [0] * m
    j = 0  # 当前最长相等前后缀长度

    for i in range(1, m):
        # 若失配,则回退到上一个最长相等前后缀位置
        while j > 0 and pattern[i] != pattern[j]:
            j = next_arr[j - 1]
        # 若匹配,则最长相等前后缀长度加 1
        if pattern[i] == pattern[j]:
            j += 1
        next_arr[i] = j

    return next_arr


def kmp_search(text, pattern):
    """
    KMP 字符串匹配算法
    算法思路:利用 next 数组在失配时跳过已匹配的前缀,避免回溯主串指针
    时间复杂度:O(n + m),其中 n 为主串长度,m 为模式串长度
    空间复杂度:O(m)(存储 next 数组)
    """
    n, m = len(text), len(pattern)
    if m == 0:
        return 0

    next_arr = build_next(pattern)
    j = 0  # 模式串指针

    for i in range(n):
        # 失配时,利用 next 数组回退模式串指针
        while j > 0 and text[i] != pattern[j]:
            j = next_arr[j - 1]
        # 匹配成功,模式串指针前进
        if text[i] == pattern[j]:
            j += 1
        # 完全匹配,返回主串中的起始下标
        if j == m:
            return i - m + 1

    return -1  # 未找到


# 示例
text = "ABABDABACDABABCABAB"
pattern = "ABABCABAB"

# 运行示例
print("next 数组:", build_next(pattern))
print("匹配起始下标:", kmp_search(text, pattern))
# 输出: next 数组: [0, 0, 1, 2, 0, 1, 2, 3, 4]
#       匹配起始下标: 10

8. 算法对比与选型指南

面对不同的业务场景,选择合适的算法往往比实现算法本身更重要。下表汇总了本文 7 个核心算法的适用场景、优缺点与不适用场景,帮助读者快速做出决策:

算法名称适用场景优点缺点不适用场景
冒泡排序小规模数据排序、教学演示实现简单、稳定、原地排序平均 O(n²) 效率低大规模数据排序
快速排序大规模数据排序、系统库排序函数平均 O(n log n) 效率高、原地排序最坏 O(n²)、不稳定、递归占用栈空间数据基本有序或对稳定性有要求
二分搜索有序数组中的快速查找O(log n) 极快、空间 O(1)要求数组必须有序无序数组、频繁插入删除的动态数据集
Dijkstra 算法非负权图中的单源最短路径(导航、路由)可求出到所有顶点的最短路径、配合堆优化效率高不能处理负权边含负权边的图(应改用 Bellman-Ford)
0-1 背包问题资源分配、投资组合、装载优化可求得精确最优解、思路通用O(n × capacity) 复杂度较高物品可分割(应改用贪心)、容量极大时
活动选择问题会议室排期、任务调度思路简单、O(n log n) 高效仅适用于满足贪心选择性质的问题需要全局最优且不满足贪心性质的问题
KMP 算法文本检索、关键词匹配、DNA 序列比对O(n + m) 线性时间、主串指针不回溯需额外 O(m) 空间构建 next 数组模式串极短、单次匹配(朴素匹配即可)

下面是一张算法选型流程图,根据问题类型与关键条件(数据规模、是否有序、是否含负权边)引导读者快速锁定合适的算法:

开始:判断问题类型

排序问题?

数据规模大?

快速排序

冒泡排序

搜索问题?

数组是否有序?

二分搜索

线性搜索

图的最短路径?

是否含负权边?

Dijkstra 算法

Bellman-Ford 算法

组合优化问题?

满足贪心选择性质?

贪心算法(如活动选择)

动态规划(如 0-1 背包)

字符串匹配?

KMP 算法

其他算法

说明:流程图从「问题类型」出发,依次结合数据规模、是否有序、是否含负权边等关键条件进行分支判断,最终落到具体的算法选择;其中「组合优化问题」若满足贪心选择性质(如活动选择)可优先用贪心,否则用动态规划保证全局最优。

选型建议

  • 数据量小时选冒泡排序:当数据规模很小(如几十个元素)且对稳定性有要求时,冒泡排序实现简单、代码直观,足够胜任。
  • 大规模数据排序选快速排序:面对海量数据,快速排序平均 O(n log n) 的性能优势明显,是绝大多数系统库排序函数的默认选择。
  • 有序数组用二分搜索:只要数据是有序且静态的,二分搜索 O(log n) 的查找效率远高于线性扫描,是查找场景的首选。
  • 非负权图用 Dijkstra:在导航、路由等非负权图场景中,Dijkstra 配合优先队列能在 O((V + E) log V) 内求出单源最短路径;若图中存在负权边,则应改用 Bellman-Ford 算法。
  • 组合优化问题用动态规划:当问题具有重叠子问题与最优子结构性质(如 0-1 背包)时,动态规划能保证求得全局最优解。
  • 满足贪心性质的问题用贪心算法:活动选择这类满足贪心选择性质的问题,贪心算法能以最低的复杂度快速得到最优解。
  • 字符串匹配用 KMP:在长文本中反复匹配模式串时,KMP 的 O(n + m) 线性复杂度能避免朴素匹配的回溯开销。

小结:选型时先判断问题类型(排序/搜索/图/优化/匹配),再结合数据规模、是否有序、是否含负权边等约束条件,即可快速锁定合适的算法。

8. 总结

需要强调的是,具体需要掌握哪些算法取决于程序员所从事的领域和项目需求。因此,不同的程序员可能会更加专注于某些特定类型的算法,并对它们进行更深入的学习和研究。

Logo

一站式 AI 云服务平台

更多推荐