外观
2.8 链表:关系存哪里,用C怎么表示
2.8.1 放弃"相邻"约定,关系存哪里
顺序存储和链式存储回答的是同一个问题:线性表的元素和它们之间的一对一关系,怎么放进一维的内存。顺序表的答案是"相邻格子放相邻元素",关系隐含在位置里,不占空间,按位置取元素一步到位;但2.6节看到了它的代价:中间插入必须挪动后面的所有元素。那么,放弃这条约定会怎样?
放弃之后,元素可以散落在内存的任何地方,插入一个新元素时随便找个空格子放进去就行,不用挪动任何人。但马上出现新问题:关系没了。顺序表里"a在c后面"这条关系是靠"a就放在c的下一格"表示的,现在两个字符各在一处,谁也不挨着谁,怎么知道a在c后面?
第1章已经给过答案:关系不能隐含在位置里,就明确地存下来——在每个元素旁边记下"下一个元素在哪",也就是下一个元素的地址。第1章用整数看过这个做法,这里换成字符再看一遍,因为后面要用它实现编辑器。
假设一行只有三个字符cat,我们不再把它们挨着放,而是分开存在内存的一段格子里:c在100号,t在104号,a在108号。
text
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ c │ │ │ │ t │ │ │ │ a │ │ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115c确实在第一格,可是接下来按位置往后找,找到的是t,不是a。a跑到了t的后面。也就是说,位置已经不能告诉我们谁在谁后面了。要读出cat,得有别的办法知道:c的下一个是a,a的下一个是t。这些关系存在哪?
办法是把它们明确地记下来:在每个字符右边的格子里,写上下一个字符的地址。
text
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ c │108│ │ │ t │ 0 │ │ │ a │104│ │ │ │ │ │ │
└───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115现在从c出发能走通了:101号格子记着108,去108号找到a;109号记着104,去104号找到t;105号记着0,表示走到头了。(地址0是特殊值,任何变量都不会住在0号格子,所以用它表示"没有下一个"。)走的路线是100→108→104,在内存里先往后跳、再往回跳,但读出来正好是cat。逻辑顺序不靠位置表示,靠的是这些右格里的数字。
要特别提醒一句:右格里的108、104不是数据,它们是另一个格子的编号。同样是格子里的内容,左格存的是字符,右格存的是地址。
现在每个元素占的不再是一个格子,而是两个:一个存数据,一个存下一个元素的地址。这两个格子合起来叫一个结点。一个个结点用地址串起来,就是链表。
按内存位置画图看得清"分开存",但看不清"顺序"。把三个结点按逻辑顺序重新摆一遍,右格里的地址用箭头画出来,方框下面标出它在内存里的位置:
text
head = 100
│
▼
┌───┬─────┐ ┌───┬─────┐ ┌───┬─────┐
│ c │ 108 │────►│ a │ 104 │────►│ t │ 0 │
└───┴─────┘ └───┴─────┘ └───┴─────┘
100 108 104这张图和上一张是同一份数据的两种画法:上一张按内存位置排,这一张按逻辑顺序排。方框下面的数字是它在上一张图里的位置,右格里的数字就是箭头指向的那个方框的地址。head是我们另外记下的第一个结点的地址,有了它才知道从哪里开始走。
后面讲操作时,图一律用这种箭头画法,并且大多数时候不再标地址数字——代码里我们永远看不到100、108这些数,地址存在指针变量里,指针变量替我们记着,箭头就是指针。只有在讲插入和删除、需要看清"右格里的数字怎么变"的时候,才把数字标回来。
2.8.2 用C表示结点和链表
结点
一个结点有两部分:一个字符,一个指向下一个结点的地址。用结构体表示:
c
typedef struct Node {
char data; /* 数据 */
struct Node *next; /* 下一个结点的地址 */
} Node;这里有一处第一次见到的写法:结构体里面有一个指向自己这种结构体的指针。next存的是"下一个结点的地址",下一个结点也是一个Node,所以next的类型是"指向Node的指针"。
注意结构体的名字写了两遍:开头的struct Node和结尾typedef出来的Node。这是因为在定义next的那一行,typedef还没有完成,Node这个简写还不存在,只能用完整的struct Node *。定义结束之后,其他地方就都可以用Node了。
结点从哪来
顺序表的格子是Init时一次申请一整段。链表的结点是零散的,谁需要谁申请:每插入一个元素,就向系统要一个结点大小的空间,把字符放进去。2.7节用过的malloc正好干这个事。写成一个小函数:
c
Node *NewNode(char x) {
Node *p = (Node *)malloc(sizeof(Node));
if (p == NULL) return NULL;
p->data = x;
p->next = NULL;
return p;
}malloc(sizeof(Node))申请一个结点的空间,返回它的地址;把字符填进data,next先置为NULL——这就是上一节说的地址0,C语言里写作NULL,表示"暂时不指向任何结点"。函数返回这个新结点的地址。
对应到上一节的图:NewNode('c')就是造出了c那个方框,它的右格暂时是0。
整个表用什么代表
有了结点,还要有一个东西代表"整张表"。只要记住第一个结点的地址,就能顺着next走完全表,所以最少只需要一个指针head。但2.2节定义的Length要返回长度,如果每次都从头数一遍,Length就成了O(n);顺序表的Length是O(1),链表没理由做得更差。所以再加一个计数器,和顺序表的结构对称:
c
typedef struct {
Node *head; /* 第一个结点的地址 */
int length; /* 结点个数 */
} LinkList;空表就是head为NULL、length为0。上一节cat那张图,就是一个head指向c结点、length为3的LinkList。
从现在起,六个操作的参数是LinkList *L,和顺序表的SeqList *L签名完全一样,只是类型名不同。2.5节的编辑器main里,把SeqList改成LinkList,其余能不能一行不改地跑起来,是2.10节要验证的事。
先手工搭一个
在写六个操作之前,先不借助任何操作,手工把cat搭出来、再走一遍,把结点、指针、箭头和代码对上:
c
LinkList L;
L.head = NewNode('c'); /* 造 c,head 指向它 */
L.head->next = NewNode('a'); /* 造 a,c 的 next 指向它 */
L.head->next->next = NewNode('t'); /* 造 t,a 的 next 指向它 */
L.length = 3;
for (Node *p = L.head; p != NULL; p = p->next)
putchar(p->data); /* 输出 cat */L.head->next是"head指向的结点的next",也就是a的地址;L.head->next->next是a的next,也就是t的地址。一连串箭头就是顺着链一站一站往下走。
最后那个for循环是链表最基本的动作,后面反复出现:p从第一个结点开始,每次p = p->next跳到下一个,直到p变成NULL说明走到头了。上一节按地址100→108→104读一遍,就是这个循环。
当然,L.head->next->next这种写法只能演示,不能用来干活——插第100个字符要写100个箭头。怎么在任意位置插入、删除、取元素,就是下一节六个操作要解决的问题。