单链表 头指针 head
指向单链表的第一个结点,
空表的头指针为空.
也可以在单链表的第一个结点前附设一头结点,
头结点不存放数据, 或者也可存放表的长度等信息.
头结点的指针域指向第一个结点.
引入头结点的好处是, 链表第一个结点的操作和其它结点的操作变得一致了,
空表和非空表的操作也变得一致了.
因为头结点无需任何改变, 所以 head
的指向始终不变.
不过, 下面没有特别说明时,
总假定单链表不含头结点.
也可以附设单链表的尾指针, 指向最后一个结点.
struct Node: Type val Node *next Node *head
单链表的查找
# 查找第 i 个结点, 1 ≤ i ≤ len. # 如果 i 无效, 返回 NULL get(i): if i < 1: return NULL p = head while p and --i: p = p->next return p # 查找值为 val 的结点, 找不到则返回 NULL # 另外如果链表含尾指针, 可以先在末尾插入一个值 # val 的结点作哨兵, 可省去每步循环对空指针的判断 find(val): p = head while p and p->val != val: p = p->next return p
单链表的增删 下面各个删除结点的算法, 亦可以利用返回值或引用参数返回被删除结点的值.
# 在 head 所指的链表前插入一个结点, # 使用前保证 head 确实指向一链表, # 即 head == NULL, 或指向某一结点 insert_head(val): head = new Node(val, head) # 在指针 p 所指的结点后插入一值为 val 的结点 # 使用前保证 p 确实指向某一结点 # 注意此算法不能在头部插入结点 insert_after(p, val): p->next = new Node(val, p->next) # 头插法: 反向输入各结点的值, 建立单链表 init_reverse(): head = NULL while input(val): insert_head(val) # 尾插法: 正向输入各结点的值, 建立单链表 init(): head = tail = new Node(?, NULL) # 头结点 while input(val): insert_after(tail, val) tail = tail->next remove_head() # 若不需要头结点, 可以删除之
# 删除 head 所指的结点, # 使用前保证 head 确实指向某一结点 remove_head(): tmp = head head = head->next; delete tmp # 删除指针 p 所指结点的下一结点, # 使用前保证 p 确实指向某一结点. # 注意此算法不能删除头部的结点 remove_after(p) tmp = p->next if tmp: p->next = tmp->next delete tmp # 清空单链表 clear(): while head: remove_head()
对单链表的结点作增, 删操作时, 一般要求已知该结点的前驱. 但是, 也可以这样删除一个非尾结点: 先将下一结点的内容拷贝到此结点, 再删除下一结点. 同理, 为了在非尾部插入一个结点, 可以先在此结点后插入一个此结点的拷贝, 再将要插入的数据拷贝到此结点. 这两种做法分别称为单链表的前插与前删操作, 与已知前驱结点的后插/后删算法相比, 它们各自多了一次数据的拷贝.
双链表 相比于单链表, 增加了一个指向前驱结点的指针. 操作与单链表相似. 在双链表中, 寻找前驱的时间由单链表的 O(n) 降到 O(1).
struct DualNode: Type val DualNode *prev DualNode *next DualNode *head
双链表的增删
下面只列出其中两个算法. 注意指针回指前应当判断非空, 比如
p->next->prev = node
一句.
insert_before
, insert_head
的思路都类似.
# 在 p 所指的结点后插入一值为 val 的结点 # 使用前保证 p 确实指向某一结点 # 注意此算法不能在头部插入结点 insert_after(p, val): node = new Node(val, p, p->next) if p->next: p->next->prev = node p->next = node # 删除指针 p 所指结点, 使用前保证 p 确实指向某一结点 remove(p): if p->next: p->next->prev = p->prev if p->prev: p->prev->next = p->next delete p
循环 (单) 链表 的结点结构与单链表相同;
与单链表的区别在于, 最后一个指针不是空指针,
而是指向第一个结点 (或头结点, 如果存在), 整个链表形成一个环.
与单链表不同, 循环链表常设尾指针, 而不是头指针, 这是因为表非空时,
tail->next
就是头指针.
空循环链表的 tail == NULL
; 只有单个元素的循环链表,
其惟一结点的后继就是自己.
struct Node: Type val Node *next Node *tail # 在表尾插入一个新结点 append(val): if !tail: tail = new Node(val, NULL) tail->next = tail else: tail = tail->next = new Node(val, tail->next) # 删除 p 所指结点的下一结点. 要求 p 确实指向一个结点. remove_after(p): if tail->next == tail: tail = NULL delete tail else: tmp = p->next p->next = tmp->next if tmp == tail: tail = p delete tmp # 遍历所有结点 traverse(visit): if tail: p = tail do: p = p->next visit(p) while p != tail
循环双链表 的结点结构与双链表相同;
与双链表的区别在于, 最后一个结点的 next
指针指向第一个结点, 而第一个结点的 prev
指针指向最后一个结点.
静态链表 借助数组实现链表结构.
因为不涉及指针,
所以静态链表在用于一些大小固定的数据结构时有一定的方便之处.
后面会看到, 二叉树, 图... 凡是用到指针的数据结构,
统统可以做出它们的静态版本来.
动态分配的算法 new
,
delete
等需要自己实现. 一般数组的 0 号单元空出不用
(亦即, 0 号单元用作头结点), 这样就可以用 0 表示空指针.
假设静态链表可用单元最多为 N
个, 则数组大小须是
N+1
.
struct StaticList: Type val[N+1] int next[N+1]
静态链表的空间分配算法
将未分配的空间链接成一单链表 (实为链栈),
next[0] == 0
表示可用空间为空.
每次分配 (回收) 空间时从表头进行操作即可.
init(): for i = 0 to N-1: next[i] = i+1 next[N] = 0 # 分配 next[0] 所指的单元; 如果空间用尽, 返回 0 alloc(&p): p = next[0] next[0] = next[p] # 空间用尽时, 这里其实是自赋值 # 释放 p 所指的单元 free(p): next[p] = next[0] next[0] = p