算法与数据结构回顾 – 广义表
图源:吟游诗人-狼娘 月巴小鹅 111776313
简述
广义表是线性表的一种推广。线性表要求表中元素拥有统一的类型,而广义表无此限制。需要注意的是,广义表和线性表长度一般认为是有限的,但广义表的深度可以是无限的(即递归表)。
广义表是一种数据结构。对广义表的常见操作包括:
- 创建
- 从字符串形式创建
- 销毁
- 复制
- 取表头
- 取表尾
- 判空
- 求长度
- 求深度
- 从表头插入
- 从表头删除
- 遍历
广义表是Lisp语言的基本数据结构。后文以Common Lisp和C++给出各基本操作的示例。
名词注解
长度
指广义表中的元素个数。举例如下:
例一
| 1 | (a, b) | 
该表中共有两个元素:原子项a,原子项b。因而该表的长度为2。
例二
| 1 | (a, (b, c)) | 
该表中共有两个元素:原子项a,广义表(b, c)。因而该表的长度为2。
深度
指广义表中子表的嵌套层数。举例如下:
例一
| 1 | (a, b) | 
该表中没有子表,因而该表的深度为1。
例二
| 1 | (a, (b, c)) | 
该表中嵌套了一个无嵌套的子表,因而该表的深度为2。
例三
| 1 | ((a, b), (c, d)) | 
该表中嵌套了两个无嵌套的子表,因而该表的深度为2。
例四
| 1 | (a, (b, (c, d))) | 
该表中嵌套了1个子表,子表中嵌套了1个无嵌套的子表,因而该表的深度为3。
可理解为广义表字符串形式中括号的层数。
子表
嵌套在广义表中的广义表叫做该广义表的字表。例如:
| 1 | (a, (b, c)) | 
称(b, c)是(a, (b, c))的字表。
原子项
不可作为广义表再分的元素。例如:
| 1 | (1, (a, b)) | 
上表中1不是广义表,对广义表来说不可再分,因此是原子项。
上表中(a, b)是广义表,可以被分割为(a)与(b),因此不是原子项。
字符串形式(书写形式)
一种广义表的序列化方法。即将广义表写成由括号、原子项的值、(逗号)等组成的字符串。例如:
| 1 | (1 2 (A B)) | 
与
| 1 | (1, 2, (A, B)) | 
都是常见的广义表的书写形式。
Lisp 中的实现
使用SBCL Lisp环境。
创建
使用cons函数可以创建列表。例如创建一个只包含数字1(表头为原子项整数1,表尾为空)的列表:
| 1 | (cons 1 nil) | 
或者:
| 1 | (list 1) | 
也可以根据已有的表头和表尾创建列表:
| 1 | (cons 1 '(2 5)) | 
也可以使用符号。
| 1 | (cons 'a '(b 2)) | 
设置变量X为列表(1 A B),作为接下来的示例:
| 1 | (let ((x (cons 1 '(a b)))) (prin1 x)) | 
输出为:
| 1 | (1 A B) | 
复制
可以理解为变量传值。
| 1 | (let ((x (cons 1 '(a b)))) | 
输出为:
| 1 | (1 A B) | 
取表头
使用car函数。
| 1 | (let ((x (cons 1 '(a b)))) (car x)) | 
输出为:
| 1 | 1 | 
此时输出为原子项。
取表尾
使用cdr函数。
| 1 | (let ((x (cons 1 '(a b)))) (cdr x)) | 
输出为:
| 1 | (A B) | 
此时输出为广义表。
求长度
使用length函数。
例一
有广义表(1 A B),求长度。
| 1 | (length '(1 A B)) | 
得到结果为3。
例二
有广义表(1 (A B)),求长度。
| 1 | (length '(1 (A B))) | 
得到结果为2。
判空
广义表的长度为0即为空。
| 1 | (if (= (length '()) 1) t nil) | 
得到结果为NIL。
从表头插入
反复应用函数cons可以实现从表头将数据插入广义表。
| 1 | (let ((x (cons 1 '(A B)))) | 
输出为:
| 1 | (3 2 1 A B) | 
从表头删除
反复应用函数cdr可以实现从表头删除数据。
| 1 | (let ((x (cons 1 '(a b)))) | 
输出为:
| 1 | (B) | 
求深度
基本思路:递归求解。
- 定义函数list-depth,接受变量list作为参数
- 判断list是否为广义表,不是则直接返回0
- 若list是广义表,则:- 对该广义表中的每一个元素调用list-depth,使用mapcar实现这一操作。返回值组成一张广义表。
- 取该广义表的最大值。
- (赋初始值,可选项)
- 将已有的最大深度值 +1。
 
- 对该广义表中的每一个元素调用
| 1 | (defun list-depth (list) | 
调用:
| 1 | (list-depth '(1 (2 3))) | 
输出为:
| 1 | 2 | 
C++ 实现
由于广义表的特性,其很难使用顺序结构进行存储。因此广义表多建立在链式存储的结构之上。
讨论只含有两种可能结点的广义表:
- int类型的原子项结点
- 由int类型构成的广义表结点;此时相当于该结点是指向子表头结点的指针
因此可以设计“表结构”和“结点结构”。“表结构”中保存该表的头结点指针,及表的长度。当表为空表时,其头结点指针为nullptr。“结点结构”存储了具体的数据。具体存储结构如下:
| 1 | 
 | 
其中,Node.type指明了结点的类型:为false时代表该结点是原子项结点,为true时代表该结点为子表结点。
创建
把表和各结点都放在堆里。
| 1 | List::List(): length(0), head(nullptr) | 
销毁
| 1 | List::~List() | 
判空
利用“表结构”中的length即可。
| 1 | bool List::CheckEmpty() | 
求长度
同上。
| 1 | demo.length; | 
[TODO]
