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 算法

其他算法

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

下面是一张平均时间复杂度对比柱状图,直观展示 7 个核心算法的复杂度差异(纵轴为相对耗时量级,数值越大表示耗时越高):

7 个核心算法平均时间复杂度对比 冒泡排序 快速排序 二分搜索 Dijkstra 0-1 背包 活动选择 KMP 100 90 80 70 60 50 40 30 20 10 0 相对耗时量级

说明:图中柱高表示各算法平均时间复杂度的相对量级(非精确数值)。其中二分搜索 O(log n) 最快,冒泡排序 O(n²) 最慢;快速排序与活动选择均为 O(n log n),Dijkstra 为 O((V + E) log V),KMP 为 O(n + m),0-1 背包为 O(n × capacity)。需要注意的是,不同算法的输入规模含义不同(如 n、V、E、capacity、m),柱状图仅用于直观感受量级差异,实际选型还需结合具体问题规模与约束条件。

选型建议

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

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

9. 综合实战案例:城市交通调度系统

前面我们分别介绍了排序、搜索、图、动态规划、贪心和字符串匹配 6 大类共 7 个核心算法。但在真实业务中,这些算法往往不是孤立使用的,而是需要组合起来解决一个完整的问题。下面我们以「城市交通调度系统」为例,展示如何将本文的 7 个算法协同应用。

9.1 业务场景与算法映射

假设某城市需要调度一批出租车完成乘客的出行请求,系统需要依次解决以下子问题:

业务子问题使用的算法作用
按出发时间整理请求快速排序将请求按时间排序,便于后续处理
在有序请求中快速定位某条记录二分搜索快速查找指定时间段的请求
计算司机到乘客的最短路径Dijkstra 算法在路网图中求最短行驶距离
为司机分配载客容量0-1 背包问题在车辆容量限制下选择最优载客组合
安排司机接单顺序活动选择问题在时间冲突下安排最多接单任务
校验乘客目的地名称KMP 算法在地址库中快速匹配目的地关键词

9.2 完整 Python 实现

import heapq

# ---------- 1. 快速排序:按出发时间整理请求 ----------
def quick_sort(arr):
    """快速排序,按出发时间升序排列请求"""
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x[0] < pivot[0]]
    middle = [x for x in arr if x[0] == pivot[0]]
    right = [x for x in arr if x[0] > pivot[0]]
    return quick_sort(left) + middle + quick_sort(right)

# ---------- 2. 二分搜索:在有序请求中定位某条记录 ----------
def binary_search(arr, target_time):
    """二分搜索,在按时间排序的请求中查找指定出发时间"""
    left, right = 0, len(arr) - 1
    while left <= right:
        mid = (left + right) // 2
        if arr[mid][0] == target_time:
            return mid
        elif arr[mid][0] < target_time:
            left = mid + 1
        else:
            right = mid - 1
    return -1

# ---------- 3. Dijkstra 算法:计算司机到乘客的最短路径 ----------
def dijkstra(graph, start):
    """Dijkstra 最短路径算法(优先队列优化)"""
    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

# ---------- 4. 0-1 背包问题:在车辆容量限制下选择最优载客组合 ----------
def knapsack(weights, values, capacity):
    """0-1 背包问题(动态规划),选择价值最高的乘客组合"""
    n = len(weights)
    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:
                dp[i][j] = dp[i - 1][j]
    return dp[n][capacity]

# ---------- 5. 活动选择问题:安排司机接单顺序 ----------
def activity_selection(start, finish):
    """活动选择问题(贪心算法),在时间冲突下安排最多接单任务"""
    activities = sorted(zip(start, finish), key=lambda x: x[1])
    selected = [activities[0]]
    last_finish = activities[0][1]
    for i in range(1, len(start)):
        if activities[i][0] >= last_finish:
            selected.append(activities[i])
            last_finish = activities[i][1]
    return selected

# ---------- 6. KMP 算法:校验乘客目的地名称 ----------
def build_next(pattern):
    """构建 next 数组(部分匹配表)"""
    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]
        if pattern[i] == pattern[j]:
            j += 1
        next_arr[i] = j
    return next_arr

def kmp_search(text, pattern):
    """KMP 字符串匹配算法,在地址库中匹配目的地关键词"""
    n, m = len(text), len(pattern)
    if m == 0:
        return 0
    next_arr = build_next(pattern)
    j = 0
    for i in range(n):
        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

# ---------- 主流程:城市交通调度系统 ----------
def traffic_dispatch_system():
    print("=" * 50)
    print("城市交通调度系统 - 综合实战")
    print("=" * 50)

    # 1. 原始请求列表:(出发时间, 目的地, 乘客人数, 预估收益)
    raw_requests = [
        (8, "中央车站", 2, 50),
        (6, "机场", 3, 80),
        (9, "科技园区", 1, 30),
        (7, "市中心", 4, 100),
        (5, "火车站", 2, 60),
    ]

    # 2. 快速排序:按出发时间整理请求
    sorted_requests = quick_sort(raw_requests)
    print("\n[1] 快速排序 - 按出发时间整理请求:")
    for req in sorted_requests:
        print(f"    出发时间 {req[0]:>2} 点 -> {req[1]}({req[2]} 人,预估收益 {req[3]} 元)")

    # 3. 二分搜索:查找出发时间为 7 点的请求
    target_time = 7
    idx = binary_search(sorted_requests, target_time)
    print(f"\n[2] 二分搜索 - 查找出发时间 {target_time} 点的请求:")
    if idx != -1:
        req = sorted_requests[idx]
        print(f"    找到:{req[1]}({req[2]} 人,预估收益 {req[3]} 元)")
    else:
        print("    未找到该时间段的请求")

    # 4. Dijkstra 算法:计算司机从「调度中心」到各乘客上车点的最短距离
    road_graph = {
        '调度中心': {'中央车站': 5, '机场': 12, '火车站': 8},
        '中央车站': {'调度中心': 5, '市中心': 3, '科技园区': 6},
        '机场': {'调度中心': 12, '火车站': 10},
        '火车站': {'调度中心': 8, '机场': 10, '市中心': 4},
        '市中心': {'中央车站': 3, '火车站': 4, '科技园区': 2},
        '科技园区': {'中央车站': 6, '市中心': 2},
    }
    distances = dijkstra(road_graph, '调度中心')
    print("\n[3] Dijkstra 算法 - 从调度中心到各上车点的最短距离:")
    for dest, dist in distances.items():
        if dest != '调度中心':
            print(f"    到 {dest}:{dist} 公里")

    # 5. 0-1 背包问题:车辆容量为 4 人,选择收益最高的乘客组合
    passenger_counts = [req[2] for req in sorted_requests]
    revenues = [req[3] for req in sorted_requests]
    capacity = 4
    max_revenue = knapsack(passenger_counts, revenues, capacity)
    print(f"\n[4] 0-1 背包问题 - 车辆容量 {capacity} 人时的最大收益:")
    print(f"    最大收益:{max_revenue} 元")

    # 6. 活动选择问题:安排司机在时间冲突下接单
    start_times = [req[0] for req in sorted_requests]
    finish_times = [req[0] + 2 for req in sorted_requests]  # 假设每单耗时 2 小时
    selected = activity_selection(start_times, finish_times)
    print("\n[5] 活动选择问题 - 时间不冲突的接单安排:")
    for s, f in selected:
        print(f"    出发 {s} 点 -> 结束 {f} 点")

    # 7. KMP 算法:校验目的地名称是否在地址库中
    address_db = "中央车站,机场,火车站,市中心,科技园区"
    print("\n[6] KMP 算法 - 校验目的地名称:")
    for req in sorted_requests:
        dest = req[1]
        pos = kmp_search(address_db, dest)
        status = "地址有效" if pos != -1 else "地址无效"
        print(f"    {dest}:{status}(匹配位置 {pos})")

    print("\n" + "=" * 50)
    print("调度系统运行完毕!")
    print("=" * 50)


# 运行综合实战案例
traffic_dispatch_system()

9.3 运行结果

==================================================
城市交通调度系统 - 综合实战
==================================================

[1] 快速排序 - 按出发时间整理请求:
    出发时间  5 点 -> 火车站(2 人,预估收益 60 元)
    出发时间  6 点 -> 机场(3 人,预估收益 80 元)
    出发时间  7 点 -> 市中心(4 人,预估收益 100 元)
    出发时间  8 点 -> 中央车站(2 人,预估收益 50 元)
    出发时间  9 点 -> 科技园区(1 人,预估收益 30 元)

[2] 二分搜索 - 查找出发时间 7 点的请求:
    找到:市中心(4 人,预估收益 100 元)

[3] Dijkstra 算法 - 从调度中心到各上车点的最短距离:
    到 中央车站:5 公里
    到 机场:12 公里
    到 火车站:8 公里
    到 市中心:8 公里
    到 科技园区:10 公里

[4] 0-1 背包问题 - 车辆容量 4 人时的最大收益:
    最大收益:140 元

[5] 活动选择问题 - 时间不冲突的接单安排:
    出发 5 点 -> 结束 7 点
    出发 7 点 -> 结束 9 点
    出发 9 点 -> 结束 11 点

[6] KMP 算法 - 校验目的地名称:
    火车站:地址有效(匹配位置 4)
    机场:地址有效(匹配位置 3)
    市中心:地址有效(匹配位置 8)
    中央车站:地址有效(匹配位置 0)
    科技园区:地址有效(匹配位置 11)

==================================================
调度系统运行完毕!
==================================================

9.4 案例小结

通过这个综合实战案例可以看到,一个真实的业务系统往往需要多种算法协同配合:

  • 快速排序负责将无序的请求数据整理为有序结构,为后续的二分搜索奠定基础;
  • 二分搜索在有序数据上实现 O(log n) 的快速定位;
  • Dijkstra 算法解决路网中的最短路径规划问题;
  • 0-1 背包问题在容量约束下求得最优收益组合;
  • 活动选择问题用贪心策略在时间冲突下安排最多任务;
  • KMP 算法在线性时间内完成字符串匹配校验。

这正是算法学习的价值所在——理解每种算法的适用场景与复杂度特性,才能在真实业务中做出正确的选型与组合。

10. 常见面试题与易错点

掌握了 7 个核心算法的原理与实现后,面试中往往还会考察对算法细节的理解与边界情况的处理。下面针对每个算法各设计 1 道典型面试题,包含题目描述、考察点与易错提示,并给出简要解答思路。

10.1 冒泡排序

题目描述:给定一个数组,用冒泡排序将其升序排列,并说明在什么情况下冒泡排序的时间复杂度可以优化到 O(n)。

考察点:冒泡排序的基本实现、提前终止优化、最好/最坏时间复杂度。

易错提示:忘记设置 swapped 标志位,导致已有序数组仍执行完整 O(n²) 循环;内层循环边界写成 n - 1 而非 n - 1 - i,造成多余比较。

解答思路:每轮比较相邻元素并交换逆序对,若某一轮没有任何交换,说明数组已有序,可提前结束。当输入数组本身有序时,第一轮即无交换,时间复杂度为 O(n)。

10.2 快速排序

题目描述:快速排序在最坏情况下时间复杂度为 O(n²),请说明触发最坏情况的条件,并给出一种避免方法。

考察点:快速排序的分治思想、基准元素选择对复杂度的影响。

易错提示:误以为快速排序最坏情况是「数据量很大」;忽略基准选择策略(如固定取首元素)在数据基本有序时会导致极度不平衡的划分。

解答思路:当每次划分都极度不平衡(如基准恰好是最大或最小值)时触发最坏情况,常见于数据基本有序且固定取首元素为基准。可通过随机选取基准、三数取中法或洗牌预处理来避免,使期望复杂度稳定在 O(n log n)。

10.3 二分搜索

题目描述:在有序数组 [1, 3, 5, 7, 9, 11, 13] 中查找目标值 7,写出二分搜索过程,并说明循环终止条件。

考察点:二分搜索的区间维护、left/right 指针更新、循环不变量。

易错提示:mid 计算写成 (left + right) // 2 在极端大数下可能溢出(Python 无此问题,但其他语言需注意);更新边界时写成 left = mid 或 right = mid 导致死循环;忘记处理未找到返回 -1 的情况。

解答思路:维护 [left, right] 闭区间,mid = (left + right) // 2。若 arr[mid] == target 返回 mid;若 arr[mid] < target 则 left = mid + 1,否则 right = mid - 1。循环条件为 left <= right,退出时返回 -1。本题中 7 位于下标 3。

10.4 Dijkstra 算法

题目描述:Dijkstra 算法为什么不能处理含负权边的图?请举例说明。

考察点:Dijkstra 的贪心策略本质、负权边导致贪心失效的原因。

易错提示:只回答「因为算法假设非负权」而不举例;混淆 Dijkstra 与 Bellman-Ford 的适用场景。

解答思路:Dijkstra 每次从优先队列中弹出当前距离最小的顶点并标记为已确定,其正确性依赖于「已确定顶点的最短路径不会再被更新」这一性质。若存在负权边,可能出现「先经过一个较大正权路径到达某顶点,再通过负权边绕回更短」的情况,导致已确定的距离被推翻。例如 A→B 权 5、A→C 权 2、C→B 权 -3,从 A 出发 Dijkstra 会先确定 C(距离 2),再确定 B(距离 5),但实际 A→C→B 距离为 -1,比 5 更短。因此含负权边时应改用 Bellman-Ford。

10.5 0-1 背包问题

题目描述:0-1 背包与完全背包的区别是什么?0-1 背包的状态转移方程如何推导?

考察点:动态规划的状态定义、状态转移方程、两类背包问题的差异。

易错提示:混淆 0-1 背包(每件物品最多选一次)与完全背包(每件物品可无限选);忘记 dp[i][j] 中 i 表示前 i 件物品、j 表示容量;内层循环方向写错导致物品被重复选取。

解答思路:0-1 背包每件物品只能选 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 下的最大价值。完全背包则允许重复选取,内层循环需正序遍历。0-1 背包若用一维数组优化,内层循环必须倒序遍历,避免覆盖上一轮结果。

10.6 活动选择问题

题目描述:给定若干活动的开始与结束时间,贪心算法为什么按「结束时间最早」选择而不是「开始时间最早」或「持续时间最短」?

考察点:贪心选择性质的证明思路、反例构造能力。

易错提示:凭直觉回答「结束早留的时间多」而不给出反例;混淆活动选择与区间调度问题的变体。

解答思路:按结束时间最早选择,能保证为剩余活动留下尽可能多的可用时间,且该选择一定包含在某个最优解中(贪心选择性质)。反例:活动 A(1, 10)、B(2, 3)、C(3, 4),若按开始时间最早选 A,则只能选 1 个;按结束时间最早选 B、C,可选 2 个。按持续时间最短也可能选到冲突的组合,无法保证最优。

10.7 KMP 算法

题目描述:KMP 算法中 next 数组的含义是什么?为什么失配时主串指针不回溯?

考察点:next 数组(部分匹配表)的语义、KMP 避免回溯的原理。

易错提示:把 next 数组理解为「前缀函数」却说不清具体含义;失配时错误地回退主串指针;构建 next 数组时边界条件处理错误。

解答思路:next[i] 表示模式串 pattern[:i] 的最长相等前后缀长度。当主串与模式串在位置 j 失配时,模式串指针回退到 next[j-1],主串指针 i 不回溯,因为已匹配部分的后缀与模式串前缀相同,无需重新比较。这样主串只需扫描一遍,总时间复杂度为 O(n + m)。

8. 总结

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

Logo

一站式 AI 云服务平台

更多推荐