数据结构与稀疏矩阵基础:二叉搜索树、堆与矩阵运算
1. 二叉搜索树(Binary Search Trees)
二叉搜索树(BST)是一种重要的数据结构,对于同一组元素,可以构建出不同形态的 BST。不同的 BST 在搜索元素所需的时间上存在差异,这种差异可以通过树的高度来量化。
树中节点的高度定义为该节点到根节点的唯一路径长度。例如,在某 BST 中,标签为 27 的节点高度可能为 1,而在另一个 BST 中,该节点高度可能为 4。BST 的高度则定义为其所有节点高度的最大值。
搜索操作
bfs_search()
的时间复杂度为 $O(h)$,其中 $h$ 是 BST 的高度。如果 BST 的高度为 $O(log N)$($N$ 为 BST 中的元素数量),则称该 BST 是平衡的。在平衡 BST 中,访问、插入、删除和搜索操作的平均时间复杂度为 $O(log N)$。平衡 BST 非常适合存储需要频繁访问和更新的数据,并且在许多图算法中都有应用。
2. 二叉堆(Binary Heaps)
在许多应用中,需要快速找出一组元素中的最大值或最小值。例如,在 Dijkstra 算法中,每次都需要从未访问节点中选择距离源节点最近的节点。如果将未访问节点存储在数组中,每次搜索最小距离元素需要扫描整个数组,时间复杂度为 $O(N)$;若将数组按距离升序排序,每次更新距离后都需要重新排序,时间复杂度为 $O(N log N)$,这比在未排序数组上的线性搜索更糟糕。
二叉堆是解决此类问题的有效数据结构,它可以保证以 $O(1)$ 的时间复杂度访问和提取集合中的最小(或最大)元素,并以 $O(log N)$ 的操作来更新数据结构。如果一个二叉堆能在常数时间内访问集合中的最大(最小)元素,则称其为最大堆(Max-Heap)或最小堆(Min-Heap)。
2.1 最大堆(Binary Max-Heap)的性质
- 每个节点都关联一个数值键,键不一定唯一。
- 树有且仅有一个根节点。
- 除根节点外,每个节点都有一个父节点,即离根节点更近的邻居。
- 每个节点最多有两个子节点,分别称为左子节点和右子节点。
- 树是完全的,即除最后一层外,所有层都被完全填充,最后一层从左到右填充。
- 最大堆性质:任何节点的键都大于或等于其所有子节点的键。
最小堆的定义与最大堆类似,只是最大堆性质变为任何节点的键小于或等于其所有子节点的键。
例如,给定三个具有相同元素集合的树,只有满足上述所有性质的树才是最大堆。
2.2 堆的操作
二叉堆通常支持以下两种基本操作:
-
heap_extract()
:移除堆的根节点,并重新排列元素以确保剩余结构仍然是堆。
-
heap_insert()
:向堆中插入一个新元素。
可以使用数组来实现堆,具体规则如下:
- 数组中位置 $i$ 的节点的左子节点位于位置 $2 × i + 1$。
- 数组中位置 $i$ 的节点的右子节点位于位置 $2 × i + 2$。
- 数组中位置 $i$ 的节点的父节点位于位置 $\lfloor(i - 1)/2\rfloor$。
以下是
heap_extract()
和
sift_down()
的伪代码:
Algorithm 5 heap_extract()
Input: H, N
Output: H, N, max_H
1: max_H ← H[0]
2: H[0] ← H[N - 1]
3: N ← N - 1
4: sift_down(H, N, 0)
5: return max_H
Algorithm 6 sift_down()
Input: H, N, current
Output: H (with max-heap property respected)
1: largest ← current
2: left ← 2 × current + 1
3: right ← 2 × current + 2
4: if left < N and H[left] > H[largest] then
5: largest ← left
6: end if
7: if right < N and H[right] > H[largest] then
8: largest ← right
9: end if
10: if largest ≠ current then
11: swap (H[current], H[largest])
12: sift_down(H, N, largest)
13: return
14: end if
以下是
heap_insert()
和
sift_up()
的伪代码:
Algorithm 7 heap_insert()
Input: H, N, new_key
Output: N, H (with new_key added and max-heap property respected)
1: N ← N + 1
2: H[N - 1] ← new_key
3: sift_up(H, N, N)
4: return H
Algorithm 8 sift_up()
Input: H, N, current
Output: H (with max-heap property respected)
1: parent ← ⌊(current - 1)/2⌋
2: while H[parent] < H[current] do
3: swap(H[parent], H[current])
4: current ← parent
5: parent ← ⌊(current - 1)/2⌋
6: end while
7: return H
heap_extract()
操作的流程如下:
1. 读取堆的最大值(存储在数组的第一个位置)并将其放入返回变量
max_H
。
2. 将数组的最后一个元素移到第一个位置。
3. 调用
sift_down()
函数来恢复最大堆性质。
heap_insert()
操作的流程如下:
1. 增加堆的元素数量。
2. 将新元素插入到堆的最后一个位置。
3. 调用
sift_up()
函数来确保新元素找到合适的位置,以满足最大堆性质。
heap_extract()
和
heap_insert()
的时间复杂度均为 $O(log N)$。
3. 稀疏矩阵(Sparse Matrices)
矩阵是一种常见的数学结构,一个 $m×n$ 的矩阵 $A$ 是由 $m$ 行 $n$ 列的实数组成的表格,矩阵元素用 $a_{ij}$ 表示。矩阵的加法、数乘和乘法运算有特定的定义。矩阵的转置是通过交换行和列得到的,如果一个矩阵等于其转置,则称该矩阵为对称矩阵。
在处理与图相关的矩阵(如邻接矩阵和拉普拉斯矩阵)时,由于现实世界网络中的矩阵通常是稀疏的,即矩阵的非零元素数量 $M$ 远小于 $N^2$($N$ 为矩阵的阶数),因此需要使用特殊的表示方法来提高存储和运算效率。
3.1 ij - 形式(ij - Form)
$N × N$ 矩阵 $A$ 的 ij - 形式使用三个长度为 $M$(矩阵非零元素数量)的向量 $i$、$j$ 和 $s$。向量 $i$ 和 $j$ 分别存储矩阵非零元素的行和列索引,向量 $s$ 存储这些非零元素的值。如果 $A$ 是加权图的邻接矩阵,向量 $s$ 存储边的权重。
通过这些基础的数据结构和矩阵运算知识,我们可以更高效地处理各种算法问题,尤其是在图论和网络分析领域。二叉搜索树和二叉堆为我们提供了高效的数据存储和检索方法,而稀疏矩阵的表示则有助于优化矩阵运算的性能。在实际应用中,我们可以根据具体需求选择合适的数据结构和算法,以提高程序的效率和性能。
下面是一个简单的 mermaid 流程图,展示
heap_extract()
的操作流程:
graph TD;
A[开始] --> B[读取最大值];
B --> C[将最后元素移到首位];
C --> D[调用 sift_down()];
D --> E[结束];
同时,我们可以用表格总结二叉堆的操作复杂度:
| 操作 | 时间复杂度 |
| ---- | ---- |
|
heap_extract()
| $O(log N)$ |
|
heap_insert()
| $O(log N)$ |
| 查找最大值(最大堆) | $O(1)$ |
| 查找最小值(最小堆) | $O(1)$ |
数据结构与稀疏矩阵基础:二叉搜索树、堆与矩阵运算
4. 压缩行存储格式(Compressed Row Storage Format)
除了 ij - 形式,压缩行存储(CRS)格式也是一种常用的稀疏矩阵表示方法。在 CRS 格式中,需要三个数组:
row_ptr
、
col_idx
和
values
。
-
row_ptr:长度为 $m + 1$,其中 $m$ 是矩阵的行数。row_ptr[i]表示第 $i$ 行的非零元素在col_idx和values数组中的起始位置。 -
col_idx:存储所有非零元素的列索引。 -
values:存储所有非零元素的值。
例如,对于一个 $3\times3$ 的稀疏矩阵:
[
A =
\begin{bmatrix}
1 & 0 & 2 \
0 & 3 & 0 \
4 & 0 & 5
\end{bmatrix}
]
其 CRS 格式表示如下:
-
row_ptr = [0, 2, 3, 5]
-
col_idx = [0, 2, 1, 0, 2]
-
values = [1, 2, 3, 4, 5]
通过
row_ptr
数组,我们可以方便地定位每一行的非零元素。例如,第 1 行的非零元素从
col_idx[row_ptr[0]]
到
col_idx[row_ptr[1] - 1]
,即
col_idx[0]
和
col_idx[1]
,对应列索引为 0 和 2,值分别为
values[0]
和
values[1]
,即 1 和 2。
4.1 从 ij - 形式转换到 CRS 格式
将矩阵从 ij - 形式转换到 CRS 格式的步骤如下:
1. 初始化
row_ptr
数组,
row_ptr[0] = 0
。
2. 遍历
i
向量(ij - 形式中的行索引向量),统计每一行的非零元素数量。
3. 根据每一行的非零元素数量,计算
row_ptr
数组的后续元素。
4. 初始化
col_idx
和
values
数组。
5. 再次遍历
i
、
j
和
s
向量,将列索引和元素值分别存储到
col_idx
和
values
数组中。
以下是一个简单的 Python 代码示例,展示如何进行转换:
def ij_to_crs(i, j, s, m):
row_ptr = [0] * (m + 1)
for idx in i:
row_ptr[idx + 1] += 1
for k in range(1, m + 1):
row_ptr[k] += row_ptr[k - 1]
col_idx = [0] * len(i)
values = [0] * len(i)
for idx in range(len(i)):
row = i[idx]
pos = row_ptr[row]
col_idx[pos] = j[idx]
values[pos] = s[idx]
row_ptr[row] += 1
# 恢复 row_ptr 的原始值
for k in range(m, 0, -1):
row_ptr[k] = row_ptr[k - 1]
row_ptr[0] = 0
return row_ptr, col_idx, values
4.2 从 CRS 格式转换到 ij - 形式
将矩阵从 CRS 格式转换到 ij - 形式的步骤如下:
1. 初始化
i
、
j
和
s
向量。
2. 遍历
row_ptr
数组,对于每一行,根据
row_ptr
数组确定该行非零元素的范围。
3. 在该范围内,将行索引、列索引和元素值分别添加到
i
、
j
和
s
向量中。
以下是一个简单的 Python 代码示例,展示如何进行转换:
def crs_to_ij(row_ptr, col_idx, values, m):
i = []
j = []
s = []
for row in range(m):
start = row_ptr[row]
end = row_ptr[row + 1]
for pos in range(start, end):
i.append(row)
j.append(col_idx[pos])
s.append(values[pos])
return i, j, s
5. 稀疏矩阵的矩阵 - 向量乘法
稀疏矩阵的一个重要应用是矩阵 - 向量乘法。对于一个 $m\times n$ 的矩阵 $A$ 和一个 $n$ 维向量 $x$,矩阵 - 向量乘法 $y = Ax$ 的结果是一个 $m$ 维向量 $y$。
在稀疏矩阵的情况下,使用 ij - 形式或 CRS 格式可以更高效地执行矩阵 - 向量乘法。
5.1 使用 ij - 形式进行矩阵 - 向量乘法
使用 ij - 形式进行矩阵 - 向量乘法的步骤如下:
1. 初始化结果向量 $y$ 为零向量。
2. 遍历
i
、
j
和
s
向量。
3. 对于每一个非零元素 $a_{ij}$(存储在
s
向量中,行索引为
i
,列索引为
j
),将 $a_{ij}x_j$ 累加到 $y_i$ 中。
以下是一个简单的 Python 代码示例:
def sparse_matrix_vector_multiply_ij(i, j, s, x, m):
y = [0] * m
for idx in range(len(i)):
row = i[idx]
col = j[idx]
value = s[idx]
y[row] += value * x[col]
return y
5.2 使用 CRS 格式进行矩阵 - 向量乘法
使用 CRS 格式进行矩阵 - 向量乘法的步骤如下:
1. 初始化结果向量 $y$ 为零向量。
2. 遍历每一行。
3. 对于每一行,根据
row_ptr
数组确定该行非零元素的范围。
4. 在该范围内,将非零元素与向量 $x$ 对应元素的乘积累加到 $y$ 的相应行中。
以下是一个简单的 Python 代码示例:
def sparse_matrix_vector_multiply_crs(row_ptr, col_idx, values, x, m):
y = [0] * m
for row in range(m):
start = row_ptr[row]
end = row_ptr[row + 1]
for pos in range(start, end):
col = col_idx[pos]
value = values[pos]
y[row] += value * x[col]
return y
6. 总结与应用场景
通过上述内容,我们了解了二叉搜索树、二叉堆和稀疏矩阵的相关知识。这些数据结构和算法在不同的场景中有着重要的应用。
- 二叉搜索树 :适用于需要频繁进行插入、删除和搜索操作的场景,尤其是当数据需要有序存储时。平衡二叉搜索树在处理大规模数据时能保证较好的时间复杂度。
- 二叉堆 :在需要快速找到最大值或最小值的场景中表现出色,如 Dijkstra 算法、优先队列等。堆操作的时间复杂度较低,能有效提高算法效率。
- 稀疏矩阵 :在处理大规模稀疏矩阵时,ij - 形式和 CRS 格式能显著减少存储空间,并提高矩阵运算的效率。在图论、机器学习等领域,稀疏矩阵的应用非常广泛。
下面是一个 mermaid 流程图,展示稀疏矩阵从 ij - 形式到 CRS 格式的转换流程:
graph TD;
A[开始] --> B[初始化 row_ptr];
B --> C[统计每行非零元素数量];
C --> D[计算 row_ptr 后续元素];
D --> E[初始化 col_idx 和 values];
E --> F[遍历 i, j, s 向量];
F --> G[填充 col_idx 和 values];
G --> H[恢复 row_ptr 原始值];
H --> I[结束];
同时,我们可以用表格总结稀疏矩阵不同表示方法的特点:
| 表示方法 | 优点 | 缺点 | 适用场景 |
| ---- | ---- | ---- | ---- |
| ij - 形式 | 简单直观,易于实现 | 存储效率相对较低 | 小规模稀疏矩阵或需要快速构建矩阵的场景 |
| 压缩行存储格式 | 存储效率高,适合矩阵 - 向量乘法 | 构建过程相对复杂 | 大规模稀疏矩阵和需要频繁进行矩阵 - 向量乘法的场景 |
综上所述,掌握这些数据结构和算法对于解决实际问题,特别是在图论、网络分析和机器学习等领域,具有重要的意义。我们可以根据具体的应用场景选择合适的数据结构和算法,以达到最佳的性能和效率。
超级会员免费看
33

被折叠的 条评论
为什么被折叠?



