Contents

数据结构与算法-5其他

Beautiful landscape

Nature's Beauty

Explore the wonders of the world

概述

本章节收录了一些在基础数据结构与算法之外的重要主题,涵盖位运算技巧高级数据结构(线段树)、搜索算法A*)、随机数生成(梅森旋转)、字符串匹配(KMP)、集合合并(并查集)等内容。

一、位运算

位运算是一种直接对二进制数进行操作的运算方式,具有极高的执行效率,常用于优化算法和系统底层编程。

1.1 基础位运算符

运算符 名称 说明
& 两位都为 1 时结果为 1
| 任一位为 1 时结果为 1
^ 异或 相同为 0,不同为 1
<< 左移 所有位向左移动指定的位数
>> 右移 所有位向右移动指定的位数
~ 取反 所有位取反(0 变 1,1 变 0)

1.2 常用位运算技巧

判断奇偶性

1
n & 1  # 结果为 1 表示奇数,0 表示偶数

可以用来将某些位设置为 0
例如 n = n & (n-1) 可以用于判断一个数是否为 2 的幂
假设 n 的二进制表示为 1000,则 n-1 的二进制表示为 0111
1000 & 0111 的结果为 0000 即 0,说明 n 是 2 的幂

还可以用 n & 1 来判断一个数的奇偶性
如果结果为 1,则说明 n 是奇数
如果结果为 0,则说明 n 是偶数

判断是否为 2 的幂

1
2
3
4
# 2 的幂的二进制表示只有一个 1
n = 8  # 1000
n - 1 = 7  # 0111
n & (n - 1) == 0  # True,n 是 2 的幂

获取二进制最右边的 1

1
2
3
n = 12  # 二进制:1100
-n = -12  # 补码:0100
n & -n  # 0100,获取最右边 1 的位置

-n 是 n 的补码,即 n 的反码加 1
假设 n = 12,其二进制表示为 1100
12 的反码为 0011,故补码为:0011 + 0001 = 0100
n & -n = 1100 & 0100 = 0100 即获取到了 1100 的最右边的 1

交换两个变量(无需临时变量)

1
2
3
4
a ^= b
b ^= a
a ^= b
# 等价于:a = a ^ b, b = b ^ a, a = a ^ b

原理:异或运算满足 a ^ a = 0a ^ 0 = a 的特性。第一步 a = a ^ b,第二步 b = b ^ (a ^ b) = a,第三步 a = (a ^ b) ^ a = b

将最低位设置为 1

1
2
n | 1  # 将 n 的二进制最低位设为 1
# 例如:1000 | 1 = 1001

1.3 位移运算的数学含义

1
2
n << k   # 等价于 n * 2^k
n >> k   # 等价于 n // 2^k(整数除法)

二、线段树

2.1 基本概念

线段树是一种用于维护区间信息的二叉树结构,常见应用包括:

  • 区间求和
  • 区间最大值/最小值
  • 区间更新(如给区间中每个数加 1)

树的结构:根节点代表整个区间 [l, r],子节点递归代表 [l, mid][mid+1, r]。若根节点编号为 1,则左子节点编号为 2*id,右子节点为 2*id+1

2.2 懒标记(Lazy Propagation)

线段树的核心优化是懒标记,它将更新操作分为两个阶段:

  1. 下推(Push Down):更新时不直接修改子节点,而是将信息暂存在父节点的懒标记中
  2. 上提(Pull Up):查询时若遇到懒标记,先将其应用到子节点,再清除

2.3 完整实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
class SegNode:
    def __init__(self):
        self.l = 0           # 区间左端点
        self.r = 0           # 区间右端点
        self.sum = 0         # 区间和
        self.lazytag = False # 懒标记(区间翻转场景)


class SegTree:
    def __init__(self, nums):
        n = len(nums)
        self.arr = [SegNode() for _ in range(n * 4)]
        self.build(1, 0, n - 1, nums)

    def build(self, id, l, r, nums):
        """构建线段树"""
        self.arr[id].l, self.arr[id].r = l, r
        if l == r:
            self.arr[id].sum = nums[l]
            return
        mid = (l + r) >> 1
        self.build(id * 2, l, mid, nums)
        self.build(id * 2 + 1, mid + 1, r, nums)
        self.arr[id].sum = self.arr[id * 2].sum + self.arr[id * 2 + 1].sum

    def pushdown(self, id):
        """下推懒标记到子节点"""
        if self.arr[id].lazytag:
            left, right = id * 2, id * 2 + 1
            # 翻转子节点的区间和
            self.arr[left].sum = (self.arr[left].r - self.arr[left].l + 1) - self.arr[left].sum
            self.arr[right].sum = (self.arr[right].r - self.arr[right].l + 1) - self.arr[right].sum
            self.arr[left].lazytag = not self.arr[left].lazytag
            self.arr[right].lazytag = not self.arr[right].lazytag
            self.arr[id].lazytag = False

    def modify(self, id, l, r):
        """区间修改"""
        if self.arr[id].l >= l and self.arr[id].r <= r:
            self.arr[id].sum = (self.arr[id].r - self.arr[id].l + 1) - self.arr[id].sum
            self.arr[id].lazytag = not self.arr[id].lazytag
            return
        self.pushdown(id)
        mid = (self.arr[id].l + self.arr[id].r) >> 1
        if self.arr[id * 2].r >= l:
            self.modify(id * 2, l, r)
        if self.arr[id * 2 + 1].l <= r:
            self.modify(id * 2 + 1, l, r)
        self.arr[id].sum = self.arr[id * 2].sum + self.arr[id * 2 + 1].sum

    def query(self, id, l, r):
        """区间查询"""
        if self.arr[id].l >= l and self.arr[id].r <= r:
            return self.arr[id].sum
        if self.arr[id].r < l or self.arr[id].l > r:
            return 0
        self.pushdown(id)
        res = 0
        if self.arr[id * 2].r >= l:
            res += self.query(id * 2, l, r)
        if self.arr[id * 2 + 1].l <= r:
            res += self.query(id * 2 + 1, l, r)
        return res

    def sum_range(self, left, right):
        return self.query(1, left, right)

    def reverse_range(self, left, right):
        self.modify(1, left, right)

复杂度:单次区间更新/查询的时间复杂度为 O(log n),空间复杂度为 O(n)


三、A* 寻路算法

3.1 算法原理

A* 算法是一种启发式搜索算法,用于在图形或网格中找到两点之间的最短路径。它结合了 Dijkstra 算法的完备性和最佳优先搜索的高效性。

  • 算法步骤
  1. 将起点加入开放列表
  2. 循环直到找到终点或开放列表为空: a. 从开放列表中取出 f 值最小的节点作为当前节点 b. 将当前节点移到关闭列表 c. 如果当前节点是终点,则回溯路径 d. 遍历当前节点的所有相邻节点: i. 如果不可通过或在关闭列表中,跳过 ii. 如果不在开放列表中,加入开放列表并计算其 f,g,h 值 iii. 如果在开放列表中,检查是否通过当前节点到达它有更小的 g 值
  3. 如果开放列表为空且未找到终点,则路径不存在

核心公式: $$f(n) = g(n) + h(n)$$

符号 含义
f(n) 节点 n 的综合优先级(越小越优先)
g(n) 从起点到节点 n 的实际路径成本
h(n) 从节点 n 到终点的估计成本(启发式函数)

关键数据结构

  • 开放列表(Open Set):待检查的节点集合
  • 关闭列表(Closed Set):已检查的节点集合

3.2 启发式函数选择

函数类型 公式 适用场景
曼哈顿距离 |x₁ - x₂| + |y₁ - y₂| 四方向移动的网格
欧几里得距离 √((x₁-x₂)² + (y₁-y₂)²) 八方向或任意角度移动
对角线距离 (dx + dy) + (√2 - 2) * min(dx, dy) 对角线移动

3.3 Python 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import heapq
import math

class Node:
    def __init__(self, x, y, walkable=True):
        self.x, self.y = x, y
        self.walkable = walkable
        self.g = 0  # 实际成本
        self.h = 0  # 启发式估计
        self.f = 0  # 综合成本
        self.parent = None

    def __lt__(self, other):
        return self.f < other.f


def heuristic(node, end, mode='manhattan'):
    """计算启发式估计成本"""
    dx, dy = abs(node.x - end.x), abs(node.y - end.y)
    if mode == 'manhattan':
        return dx + dy
    elif mode == 'euclidean':
        return math.sqrt(dx**2 + dy**2)
    elif mode == 'diagonal':
        return (dx + dy) + (math.sqrt(2) - 2) * min(dx, dy)
    return 0


def a_star(grid, start, end, heuristic_mode='manhattan'):
    """A* 寻路算法"""
    open_set = []
    closed_set = set()
    heapq.heappush(open_set, start)

    while open_set:
        current = heapq.heappop(open_set)

        if current == end:
            path = []
            while current:
                path.append((current.x, current.y))
                current = current.parent
            return path[::-1]

        closed_set.add(current)

        for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]:
            x, y = current.x + dx, current.y + dy
            if not (0 <= x < len(grid) and 0 <= y < len(grid[0])):
                continue

            neighbor = grid[x][y]
            if not neighbor.walkable or neighbor in closed_set:
                continue

            new_g = current.g + 1
            if new_g < neighbor.g or neighbor not in open_set:
                neighbor.g = new_g
                neighbor.h = heuristic(neighbor, end, heuristic_mode)
                neighbor.f = neighbor.g + neighbor.h
                neighbor.parent = current
                if neighbor not in open_set:
                    heapq.heappush(open_set, neighbor)

    return None  # 无路径


# 使用示例
def create_grid(w, h, obstacles):
    return [[Node(x, y, (x, y) not in obstacles) for y in range(h)] for x in range(w)]


# 创建10x10网格,设置一些障碍物
grid = create_grid(10, 10, [(1, 1), (2, 1), (3, 1), (4, 1), (5, 1),
                           (5, 2), (5, 3), (5, 4), (5, 5)])
path = a_star(grid, grid[0][0], grid[7][7])
print("路径:", path)

3.4 优化与变体

变体 特点
双向 A* 从起点和终点同时搜索,可加速
动态 A* 适应环境变化
JPS(跳点搜索) 优化规则网格的路径查找
Theta* 支持任意角度移动

四、梅森旋转算法

4.1 基本概念

梅森旋转算法(Mersenne Twister)是用于生成高质量伪随机数的算法,被广泛应用于科学计算、游戏开发等领域。Python 的 random 模块即基于此算法。

核心参数

  • 状态长度 N = 624
  • 周期 M = 397
  • 字长 W = 32 位

4.2 Python 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class mt19937:
    def __init__(self, seed):
        self.W = 32    # 长度
        self.N = 624    # 递归长度
        self.M = 397    # 周期
        self.R = 31    # 低位掩码
        self.A = 0x9908B0DF    # bin(int('0x9908B0DF', 16))='0b10011001000010001011000011011111'
        self.F = 1812433253    # 初始化梅森旋转链所需参数
        self.U = 11

        # 位移量和掩码
        self.S = 7
        self.B = 0x9D2C5680    # '0b10011101001011000101011010000000'
        self.T = 15
        self.C = 0xEFC60000    # '0b11101111110001100000000000000000'
        self.L = 18

        self.MASK_LOWER = (1 << self.R) - 1    # 0x7FFFFFFF
        self.MASK_UPPER = (1 << self.R)    # 0x80000000

        # 初始化状态
        self.mt = [0] * self.N
        self.index = self.N
        self._initialize(seed)

    def _initialize(self, seed):
        # seed 的值作为梅森旋转链 mt 的第一个值 mt[0]
        self.mt[0] = seed

        # 递推式:mt[i] = F * (mt[i-1] xor (mt[i-1] >> (w-2))) + i
        for i in range(1, self.N):
            self.mt[i] = (self.F * (self.mt[i-1] ^ (self.mt[i-1] >> (self.W-2))) + i) & 0xFFFFFFFF
        # 注意最后的 & 0xFFFFFFFF 操作

    def _twist(self):
        mt_old = self.mt.copy()  # 使用原始状态的副本
        # 遍历旋转链 mt,对每个元素进行处理
        # mt[i] = mt[i+m] xor ((upper_mask(mt[i]) || lower_mask(mt[i+1]))A)
        for i in range(self.N):
            # 合并高位和低位:x = (高位 | 低位)
            x = (mt_old[i] & self.MASK_UPPER) | (mt_old[(i+1) % self.N] & self.MASK_LOWER)
            xA = x >> 1

            # 条件异或:当x为奇数时,异或A
            if x % 2 != 0:
                xA ^= self.A

            # 新状态计算
            self.mt[i] = mt_old[(i + self.M) % self.N] ^ xA
        self.index = 0

    def extract_u32(self):
        # 当状态耗尽时再执行 _twist
        if self.index >= self.N:
            self._twist()

        y = self.mt[self.index]
        self.index += 1

        # 温度变换,通过位移和掩码打乱输出
        y ^= (y >> self.U)
        y ^= (y << self.S) & self.B
        y ^= (y << self.T) & self.C
        y ^= (y >> self.L)

        # 返回 32 位无符号整数
        return y & 0xFFFFFFFF


rng = mt19937(123)
print(rng.extract_u32())    # 2991312382

在初始化函数中,F 是 1812433253,乘以这个数可能导致数值超过 32 位
原 C 代码中使用的是 32 位无符号整数,而 Python 的整数是任意精度的
所以 Python 在移位和掩码操作时需要注意截断,将结果限制在 32 位
可以和 0xFFFFFFFF 进行按位与操作,以保证结果在 32 位以内


五、字符串 KMP 算法

5.1 算法原理

KMP(Knuth-Morris-Pratt,三位提出者的名字)是一种高效的字符串匹配算法,用于在一个主文本串中查找一个模式串的出现位置。KMP 算法在某些情况下具有更好的性能,特别是在模式串较长或者存在重复字符的情况下。 KMP 算法的核心思想是:当发生不匹配时,利用已经匹配的部分信息,尽可能地减少比较的次数。构建一个部分匹配表,用于保存在模式串中每个位置的最长相等真前缀和后缀的长度。在主文本串中,当发生不匹配时,根据部分匹配表,可以将模式串移动到相对于主文本串的合适位置,而不是重新开始匹配。这样就能够提高匹配的效率。

部分匹配表(Prefix Table):记录模式串中每个位置的最长相等真前缀和真后缀长度。

5.2 Python 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def build_prefix_table(pattern):
    """构建前缀表(部分匹配表)"""
    m = len(pattern)
    table = [0] * m
    j = 0
    for i in range(1, m):
        while j > 0 and pattern[i] != pattern[j]:
            j = table[j - 1]
        if pattern[i] == pattern[j]:
            j += 1
        table[i] = j
    return table


def kmp_search(text, pattern):
    """KMP 字符串匹配"""
    n, m = len(text), len(pattern)
    if m == 0:
        return 0
    table = build_prefix_table(pattern)
    j = 0
    for i in range(n):
        while j > 0 and text[i] != pattern[j]:
            j = table[j - 1]
        if text[i] == pattern[j]:
            j += 1
        if j == m:
            return i - m + 1
    return -1


# 示例
text = "ABABDABACDABABCABAB"
pattern = "ABABCABAB"
print(kmp_search(text, pattern))  # 10

复杂度:预处理 O(m),匹配 O(n),总时间复杂度 O(n + m)。

5.3 结合哈希表:字母异位词分组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from collections import defaultdict

def group_anagrams(strs):
    """字母异位词分组"""
    groups = defaultdict(list)
    for s in strs:
        key = ''.join(sorted(s))
        groups[key].append(s)
    return list(groups.values())

strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
print(group_anagrams(strs))  # [['eat', 'tea', 'ate'], ['tan', 'nat'], ['bat']]

六、并查集

6.1 基本概念

并查集(Union-Find)是一种用于处理集合合并与查询的数据结构,主要支持两种操作:

  • Find(x):查找元素 x 所属集合的根节点
  • Union(x, y):合并元素 x 和 y 所在的集合

6.2 核心优化

优化策略 描述
路径压缩 查询时,将路径上所有节点直接指向根节点
按秩合并 合并时,将较小的树合并到较大的树下

6.3 Python 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class UnionFind:
    """并查集:支持路径压缩 + 按秩合并"""

    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        """查找根节点(路径压缩)"""
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        """合并集合(按秩合并)"""
        px, py = self.find(x), self.find(y)
        if px == py:
            return False
        if self.rank[px] < self.rank[py]:
            px, py = py, px
        self.parent[py] = px
        if self.rank[px] == self.rank[py]:
            self.rank[px] += 1
        return True

    def connected(self, x, y):
        """判断两个元素是否连通"""
        return self.find(x) == self.find(y)


# 示例:岛屿数量问题
def num_islands(grid):
    if not grid or not grid[0]:
        return 0
    m, n = len(grid), len(grid[0])
    uf = UnionFind(m * n)
    land_count = 0

    for i in range(m):
        for j in range(n):
            if grid[i][j] == '1':
                land_count += 1
                idx = i * n + j
                if j + 1 < n and grid[i][j + 1] == '1':
                    if uf.union(idx, idx + 1):
                        land_count -= 1
                if i + 1 < m and grid[i + 1][j] == '1':
                    if uf.union(idx, idx + n):
                        land_count -= 1

    return land_count

复杂度:近乎 O(1) 的均摊时间复杂度。


七、位运算实用技巧汇总

场景 代码 说明
判断奇偶 n & 1 奇数返回 1
判断 2 的幂 n & (n-1) == 0 是返回 True
获取最低位 1 n & -n 提取最右边 1 的值
消除最低位 1 n & (n-1) 消除最右边 1
交换两数 a ^= b; b ^= a; a ^= b 无临时变量交换
设置最低位为 1 n | 1 将二进制最低位置 1
判断是否相同 a ^ b == 0 相同返回 True

有限状态机

https://www.cnblogs.com/21207-iHome/p/6085334.html


拓展与后续学习建议

  1. 深入进阶方向

    • 树状数组(Fenwick Tree):线段树的简化版本,适合单点更新 + 区间查询
    • Trie 树:字典树,专用于字符串前缀匹配
    • 布隆过滤器(Bloom Filter):空间效率极高的概率数据结构
    • 一致性哈希:分布式系统中的负载均衡算法
  2. 算法练习建议

    • 位运算:LC 461(汉明距离)、LC 136(只出现一次的数)
    • 线段树:LC 307(区域和检索)、LC 315(计算右侧小于当前元素的个数)
    • 并查集:LC 200(岛屿数量)、LC 547(省份数量)
    • KMP:LC 28(实现 strStr)
  3. 工程实践视角

    • 游戏开发中的 A* 寻路与导航网格
    • 数据库索引中的 B+ 树与跳表
    • 编译器中的正则表达式引擎与有限自动机
    • 分布式系统中的哈希分片与一致性哈希