矩阵在图形学、工程计算等领域占有重要地位。在数据结构中,关注的重点并非矩阵本身的数学性质及其运算,而是如何以最小的内存空间高效存储矩阵,并支持对元素的便捷访问。
3.4.1 数组的定义
数组是由 个相同类型的数据元素组成的有限序列。每个元素称为数组元素,其在线性序列中的位置由下标标识,下标的取值范围称为维界。
数组与线性表的关系:数组是线性表的推广。一维数组可视为一个线性表;二维数组可视为其元素为定长一维数组的线性表;更高维数组以此类推。数组一旦定义,其维数与维界即固定不变。因此,除初始化和销毁外,数组仅支持存取元素和修改元素两种基本操作。
3.4.2 数组的存储结构
大多数编程语言提供数组类型,逻辑上的数组通常映射为内存中一段连续的存储空间。以一维数组 A[0...n-1]为例,设每个元素占 L 个存储单元,则元素 A[i]的地址为
多维数组在内存中需按某种顺序展开为一维。常见方式有按行优先和按列优先。
按行优先:先行后列,先存储行号较小的元素;同一行内,先存储列号较小的元素。
设二维数组行下标与列下标范围分别为 与 ,则元素 A[i][j] 的地址为
按列优先:先列后行,先存储列号较小的元素;同一列内,先存储行号较小的元素。其对应的地址公式为
3.4.3 特殊矩阵的压缩存储
压缩存储:为多个值相同的元素只分配一个存储空间,对零元素不分配存储空间。
特殊矩阵:指具有大量相同元素或零元素,且这些元素分布具有规律性的矩阵。常见的特殊矩阵有对称矩阵、上(下)三角矩阵、对角矩阵等。
特殊矩阵的压缩存储方法:通过分析矩阵中相同元素的分布规律,仅存储一份实际数据,其余的可通过下标映射访问,从而将原本冗余的数据压缩到一个共享的空间中,显著节省内存。
1. 对称矩阵
若 阶矩阵 中任意元素 均满足 (),则称其为对称矩阵。其元素可分为三部分:上三角区、主对角线和下三角区。
由于上三角区与下三角区完全对称,若仍用二维数组存储,将近一半空间被浪费。为此可将n阶对称矩阵A压缩存储于一维数组 中,通常仅存放下三角部分(含主对角线)。
对于元素 ( ),其在数组 B 中的位置由其前方的元素个数决定
第1行:1个元素 。
第2行:2个元素 。
…
第 i-1 行:i-1 个元素 。
第i行:j-1个元素 。
因此,元素 在数组 B 中的下标 (数组下标从0开始)。元素下标之间的对应关系如下:
若数组下标从1开始,则可采用类似的推导方法,请读者自行思考。
NOTE二维数组
A[n][n]与A[0...n-1][0...n-1]写法等价,表示下标从 0 开始。若写作A[1...n][1...n],则表示下标从 1 开始。矩阵元素通常记为 ,其行号 i 和列号 j 通常从 1 开始。
2. 三角矩阵
在下三角矩阵中,上三角区的所有元素均为同一常量。其存储思想与对称矩阵的类似,但是需要额外存储该常量一次。因此,可以将 n 阶下三角矩阵 A 压缩存储在 B[n(n+1)/2+1] 中。
对于元素 (),其在数组 B 中的下标为
在上三角矩阵中,下三角区所有元素均为同一常量。只需存储主对角线、上三角区上元素及该常量一次,同样将其压缩存储在 B 中。
对于元素 ( ),其在数组 B 中前方的元素个数为
第1行:n个元素
第2行:n-1个元素
…
第i-1行:n-i+2个元素
第i行:j-i个元素
故元素 在数组 B 中的下标 。元素下标之间的对应关系如下:
以上推导均假设数组下标从0开始。若下标指定从1开始,则要相应地调整映射关系。
3. 三对角矩阵
若 阶矩阵 中的任意元素 ,当 时均有 (),则称为三对角矩阵。非零元素仅集中在以主对角线为中心的 3 条对角线的区域。
可将三对角上的元素按行优先顺序存入一维数组 B,且 存于 B[0]。
| … |
|---|
三对角上的元素 ()在一维数组 B 中的下标 。
反之,已知元素存于B[k]中时,,。例如,当 时,,,存放的是 ;当 时,,,存放的是 ;当 时,,,存放的是 。
3.4.4 稀疏矩阵
若矩阵中非零元素的个数 t 相对于总元素个数 s 来说非常少,即 ,则称该矩阵为稀疏矩阵。例如,一个 的矩阵中只有不到 100 个非零元素。
为避免空间浪费,稀疏矩阵通常仅存储非零元素。然而,由于非零元素的分布通常是无规律的,仅存储其值是不够的,还需记录它们所在的行和列。为此,将每个非零元素及其对应的行列位置组合成一个三元组(行标 i,列标 j,值 )。这些三元组可以按某种顺序排列成线性表进行存储。稀疏矩阵压缩存储后便失去了随机存取特性。
稀疏矩阵的三元组表可通过多种方式存储,常见的有:数组存储将所有三元组按某种顺序存储在一维数组中,这种方式简单直接,但插入和删除操作的效率较低;十字链表存储(见6.2节)通过链表的方式组织三元组,适用于频繁插入和删除操作的场景。无论采用哪种方式,都需要额外保存稀疏矩阵的行数、列数和非零元素的个数,以便支持后续的各种操作。
归纳总结
本章所介绍的几种数据结构是线性表的应用与推广,考试中主要以选择题形式考查,但栈和队列仍可能出现在算法设计题中。不少读者看到教材中列出大量操作函数时容易产生畏难情绪:如果考试中出现了栈或队列相关的算法大题,是否需要完整写出每个操作函数?
其实,在算法设计题中,栈和队列通常作为辅助工具用于解决其他问题,无须严格按照模块化方式分函数实现。我们完全可以将其声明和核心操作写得简洁明了。以顺序栈为例:
(1) 声明并初始化栈:
Elemtype~stack[maxSize];int top=-1;// 两句代码用来声明和初始化(2) 入栈操作:
stack[++top]=x;\qquad// 一句代码实现入栈操作(3) 出栈操作:
X=stack[top--]; //单目运算符在变量之前表示“先运算后使用”,之后则相反对于链式栈,同样只需定义一个结构体,然后根据需要从常规操作中摘取关键语句,直接嵌入到自己的解题代码中即可,无须完整实现所有接口。此外,在考研真题中,链式栈出现的概率远低于顺序栈,因此大家应有所侧重,多训练与顺序栈相关的题目。
思维拓展
设计一个栈,使它可以在 的时间复杂度内实现 Push、Pop 和 min 操作。所谓 min 操作,是指得到栈中最小的元素。
TIP使用双栈,两个栈是同步关系。主栈是普通栈,用来实现栈的基本操作 Push 和 Pop;辅助栈用来记录同步的最小值 min,例如元素 x 入栈,则辅助栈 stack_min[top++] = (x < min) ? x : min; 即在每次 Push 中,都将当前最小元素放到 stack_min 的栈顶。在主栈中 Pop 最小元素 y 时,stack_min 栈中相同位置的最小元素 y 也会随着 top—而出栈。因此 stack_min 的栈顶元素必然是 y 之前入栈的最小元素。本题是典型的以空间换时间的算法。
