n 维数组

映像函数 设 n 维数组的基址为 `a`, 每个元素占 `L` 个单元, `b_i` 是第 `i` 维的长度, 则元素 a[j_(n-1)]...[j_2][j_1][j_0] 的存储位置是 `a + L(sum_(i=1)^(n-1) j_i prod_(k=0)^(i-1) b_i)`. 数组具有随机存储结构: 计算各元素存储位置的时间相等, 存取任一元素的时间也相等. 下面的函数计算存储位置:

offset(n, bound, index):
	ret = 0
	size = [1]	# size[i] 表示 0..i-1 维总大小
	for i = 0 to n-1:
		if index[i] < 0 or index[i] >= bound[i]:
			return OVERFLOW
		ret += size[i] * index[i]
		size[i+1] = size[i] * bound[i]
	# 最后 size[n] 表示整个数组的大小
	return ret

矩阵的压缩存储

下面总假定矩阵的行列下标是从 0 开始的.

对称矩阵的下三角存储

N 维对称矩阵的下三角元 (包括主对角线) 按行存储在数组 compmat[N(N+1)/2] 中, 用映射 at(i, j) 来获取矩阵的 ij 元. 类似可以设计出上 (下) 三角矩阵的压缩存储方案. 当矩阵为严格下三角时, 可以存储在数组 compmat[N(N-1)/2] 中, 且 at(i, j) 返回 compmat[i*(i-1)/2+j].

at(i, j):
	if i lt j:
		swap(i, j)
	return compmat[i*(i+1)/2 + j]

2n+1 对角矩阵的压缩存储

n = 0, 1, ... 时, 2n+1 对角矩阵是指只有主对角线以及主对角线上, 下各 n 条对角线非零, 其它元素都为 0 的矩阵. 我们可以按行序或按对角线顺序压缩存储之.

稀疏矩阵的三元组表示法

假设 rows*cols 矩阵中, 有 t 个非零元, 则称 δ = t/(rows*cols) 为矩阵的稀疏因子. 通常认为 δ ≤ 1/20 时为稀疏矩阵.

稀疏矩阵的三元组表示法(val, row, col) 的三元组表示一个 rows*cols 稀疏矩阵. 所有非零元按行序排列. 当操作过程中, 矩阵的非零元个数和位置变化较大时, 就不宜采用顺序存储的三元组表示法.

struct TripleSparseMat:
	int rows, cols		# 行数, 列数
	int cnt				# 非零元个数
	Type val[rows*cols]	# val[i] 是第 i 个非零元
	int row[rows*cols]	# row[i] 是第 i 个非零元的行号
	int col[rows*cols]	# col[i] 是第 i 个非零元的列号

三元组表示法下的矩阵转置 naive 算法依次在矩阵 this 中寻找位于第 0..cols-1 列的元素, 将其存入矩阵 m, 时间复杂度为 O(cols*cnt).
改进后的算法预先计算 this 每一列的第一个非零元在结果矩阵 m 中的序号, 将其保存在数组 next[cols+1] 中. 接下来按行处理矩阵 this, 可将结果直接放到 m 的对应位置上. 每处理一个元素, 都应使 next[col] 增加 1. 改进后的时间复杂度为 O(cols+cnt).

naive_transpose(TripleSparseMat &m):
	m.cnt, m.rows, m.cols = cnt, rows, cols
	if cnt == 0:
		return
	i = 0
	for c = 0 to cols-1:
		for j = 0 to cnt-1:
			if col[j] == c:
				(m.val, m.row, m.col)[i++] = val[j], col[j], row[j]

int next[cols+1]

fast_transpose(TripleSparseMat &m):
	m.cnt, m.rows, m.cols = cnt, rows, cols
	if cnt == 0:
		return
	# next[i+1] 初始化为矩阵 this 第 i 列的非零元数
	for c = 0 to cols:
		next[c] = 0
	for i = 0 to cnt-1:
		++next[col[i]+1]
	# 累加得到各列第一个非零元的序号
	for c = 1 to cols:
		next[c] += next[c-1]
	# 按行处理 this
	for j = 0 to cnt-1:
		i = next[col[j]]++
		(m.val, m.row, m.col)[i] = val[j], col[j], row[j]

稀疏矩阵的行逻辑链接表示法

行逻辑链接表示法 RowLinkedSparseMat 其实就是在三元组表示法中, 增加一个指示每行第一个非零元序号的向量 row_head[rows+1], 其中最后一个单元 row_head[rows] == cnt+1 作哨兵. 显然 row_head[0] == 0.

行逻辑链接表示法下的矩阵乘法 依次取出 m1 中的元素 (val, row, col), 在 m2 中找到第 col 行元素与之相乘. 如此逐行得到结果 m = m1*m2. 每得到 m 的一行, 都进行压缩存储操作.
算法的时间复杂度等于初始化 tmp 的时间 O(m.rows*m.cols), 计算 m 的所有非零元的时间 O(m1.cnt*m2.cnt/m2.rows) (矩阵 m2 每行平均有 m2.cnt/m2.rows 个非零元) 和挑出非零元的时间 O(m.rows*m.cols) 之和, 即 O(m.rows*m.cols + m1.cnt*m2.cnt/m2.rows). 当矩阵足够稀疏时, 近似为 O(m.rows*m.cols).

tmp[m2.cols]

multiply(m1, m2, &m):
	if m1.cols != m2.rows
		return ERROR
	m.cnt, m.rows, m.cols = 0, m1.rows, m2.cols
	if m1.cnt == 0 or m2.cnt == 0:
		return

	i = 0
	for row = 0 to m.rows-1:
		tmp = [0..]	# 清空临时行向量
		# 计算 m 的新一行
		while i < m1.row_head[row+1]:
			col = m1.col[i]
			for j = m2.row_head[col] to m2.row_head[col+1]-1:
				tmp[m2.col[j]] += m1.val[i] * m2.val[j]
			++i
		# 挑出非零元
		for col = 0 to m.cols-1:
			if tmp[col] != 0:
				(m.val, m.row, m.col)[m.cnt++] = tmp[col], row, col

稀疏矩阵的十字链表表示法

稀疏矩阵的十字链表表示法 (Orthogonal List) 每个非零元用一个向右指针 right 链接同一行中下一非零元, 用一个向下指针 down 链接同一列中下一非零元. 整个矩阵构成一个十字交叉的链表.

struct OLNode:
	Type val
	int row, col
	OLNode *right, *down

struct OList:
	int rows, cols, cnt
	OLNode *row_head[rows]
	OLNode *col_head[cols]

十字链表表示下矩阵的初始化 对输入非零元的先后次序没有要求. 时间复杂度为 O(cnt*max(rows,cols)). 如果数据以行序输入, 则算法可改写为 O(cnt) 的.

init():
	# 各行各列初始化为空链表
	row_head = col_head = [NULL..]
	while input(val, row, col):
		p = new OLNode(val, row, col)
		# 插入到行表
		if !row_head[row] or col < row_head[row]->col:
			p->right = row_head[row]
			row_head[row] = p
		else:
			q = row_head[row]
			while q->right and col > q-&right;->col:
				q = q->right
			p->right = q->right
			q->right = p
		# 插入到列表
		if !col_head[col] or row < col_head[col]:
			p->down = col_head[col]
			col_head[col] = p
		else:
			q = col_head[col]
			while q->down and row > q->down->row:
				q = q->down
			p->down = q->down
			q->down = p

十字链表表示法下的矩阵加法 将矩阵 m 加到 this 上. 此算法可类比于链表表示的一元多项式加法. 算法设指针向量 col_pre[cols], 用于指示每一列的前驱结点. 算法的时间复杂度为 O(m1.cnt+m2.cnt).

col_pre[cols]

add(m):
	if rows != m.rows or cols != m.cols:
		return ERROR
	for col = 0 to cols-1:
		col_pre[col] = col_head[col]
	for row = 0 to rows-1:
		p1 = row_head[row]
		p2 = m.row_head[row]
		row_pre = NULL
		while p2:
			col = p2->col
			if !p1 or col < p1->col: # 插入一个 *p2 的拷贝
				p = new OLNode(p2->val, row, col)
				if !row_pre:
					row_head[row] = p
				else:
					row_pre->right = p
				p->right = p1
				row_pre = p
				if !col_head[col] or row < col_head[col]->row:
					p->down = m1.col_head[col]
					col_head[col] = p
				else:
					p->down = col_pre[col]->down
					col_pre[col]->down = p
				col_pre[col] = p
				p2 = p2->right
			elif col == p1->col: # *p1 + *p2
				p1->val += p2->val
				if p1->val == 0:		# 删除零元
					if !row_pre:
						row_head[row] = p1->right
					else:
						row_pre->right = p1->right
					p = p1
					p1 = p1->right
					if p1.col_head[col] == p:
						p1.col_head[col] = col_pre[col] = p->down
					else:
						col_pre[col]->down = p->down
					delete p
				else:
					p1 = p1->right
				p2 = p2->right
			else: # col > p1->col: 跳过 *p1
				row_pre = p1
				p1 = p1->right

广义表

广义表 (General List) 广泛应用于 Lisp 语言, 一般记作 `LS = (a_1, a_2, cdots, a_n)`, 其中 `a_i` 可以是单个元素, 即原子, 也可以是广义表, 即子表. 当广义表非空时, 第一个元素 `a_1` 称为表头 (car), 其余元素组成的广义表 `(a_2, cdots, a_n)` 称为表尾 (cdr). 某种程度上, 广义表可以看作树结构. 和树不同之处在于, 广义表的元素和子表可以公用. 甚至自己可以是自己的子表 (递归表).

广义表的存储结构

广义表的头尾链表存储 与树的内部结点和叶子结点相对应, 有广义表的表结点和原子结点. 设 ATOM = False, LIST = True, 则可用一 bool 变量指示结点类型. car 指向表的第一个结点, cdr (读作 kuder) 指向表的其余结点组成的广义表. 故表尾非空时, cdr 总是指向一个表结点.

struct GLNode1:
	bool type
	union:
		Type atom
		struct:
			GLNode *car
			GLNode *cdr

广义表的扩展线性链表存储 与头尾链表的区别在于, 不以 cdr 指向表尾, 而以 next 指向同一层次的下一结点. 原子和表都具有 next 域.

struct GLNode2:
	bool type
	union:
		Type atom
		GLNode *car
	GLNode *next

多元多项式的广义表存储

`n` 元多项式 `P(x_1, x_2, cdots, x_n)` 可以整理为按 `x_1` 降幂的多项式, 其系数是 `x_2, cdots, x_n` 组成的 `n-1` 元多项式. 这一过程递归地进行, 最终可以化为一元多项式的情形. 如多项式 `x^10 y^3 z^2 + 2x^6 y^3 z^2 + 3x^5 y^2 z^2` `+ x^4 y^4 z + 6x^3 y^4 z + 2y z + 15` `= ((x^10 + 2 x^6)y^3 + 3x^5 y^2)z^2` `+ ((x^4 + 6x^3)y^4 + 2y)z + 15`, 用广义表表示, 其中用方括号表示系数-指数的偶对, 得: ([([([1,10], [2,6]), 3], [([3,5]), 2]), 2], [([([1,4], [6,3]), 4], [2,1]), 1], [15, 0]) 或用树表示如下, 从根到叶的每一路径指出了多项式中的一项.

z:          2                  1              (15)
y:      3       2          4       1
x:	  10  6     5        4   3    (2)
     (1) (2)   (3)      (1) (6)

可见广义表的表结点需要存储指数, 而原子结点需要存储系数. 故设计存储结构如下

多元多项式的扩展线性链表存储

struct MPolyNode:
	bool type
	int exp				# 指数
	union:
		double coef		# 系数
		MPolyNode *car
	MPolyNode *next

广义表的算法

以下假设广义表都是非递归表且无共享子表.

求广义表深度 即广义表中括弧的重数. 空表深度为 1, 原子深度为 0; 否则广义表的深度等于 1 + 所有子表的最大深度.

GList.depth(GLNode1 root):
	if !root:
		return 1	# 空表
	if root->type == ATOM:
		return 0	# 原子
	max = 0
	p = root
	while p:
		dep = depth(p->car)
		if dep > max:
			max = dep
		p = p->cdr
	return max+1

复制广义表 当原表为空时, 只需分配一空表; 否则分别复制其表头和表尾即可.

GList.copy(GLNode1 &cpy, GLNode1 root):
	if !root:
		cpy = NULL
	elif root->type == ATOM:
		cpy = new GLNode1(ATOM, root->atom)
	else:
		cpy = new GLNode1(LIST)
		copy(cpy->car, root->car)
		copy(cpy->cdr, root->cdr)

创建广义表 读取广义表的括号表达式串, 以此建立广义表.

# 取出非空串 str 第一个 "," 前的部分 car, 剩余部分赋给 str
# 假设操作前后的 str 均不含最外层的括号
sever(&str, &car):
	pushed = 0
	for i = 0 to str.length-1:
		if str[i] == '(':
			++pushed
		elif str[i] == ')':
			--pushed
		elif str[i] == ',' and pushed == 0:
			car = str[..i-1]
			str = str[i+1..]
			return
	car = ''
	swap(car, str)

# 思路1: 将广义表视为 car 和 cdr 的组合 (推荐)
GList.init(&root, str):
	if str == '()':
		root = NULL
	elif str[0] != '('
		root = new GLNode1(ATOM)
	else:
		str = remove_parenthesis(str)
		sever(str, car)
		cdr = add_parenthesis(str)
		root = new GLNode1(LIST)
		init(root->car, car)
		init(root->cdr, cdr)

# 思路2: 将广义表视为含 n 个并列子表的表
GList.init(&root, str):
	if str == '()':
		root = NULL
	elif str.length == 1:
		root = new GLNode1(ATOM, str)
	else:
		str = remove_parenthesis(str)	# 去外层括号
		root = new GLNode1(LIST)
		p = root
		while True:
			sever(str, car)
			init(p->car, car)
			if str:
				p = p->cdr = new GLNode1(LIST)
			else:
				p->cdr = NULL
				break