至此,关于单链表的基本概念、实现思路、结点的具体实现我们都已经了解了,但这些还都停留我们的脑子里。下面要做的就是把我们脑子里的东西,以内存喜闻乐见的形式搬到内存中去。
因为链表是由结点组成的,所以我们先来创造结点。
/** * 创造结点,返回指向该结点的指针 * elem : 结点的数据域的值 * return : 指向该结点的指针(该结点的地址) */ Node *create_node(int elem) { Node *node = (Node *) malloc(sizeof(Node)); node->data = elem; node->next = NULL; return node; }注意:我们要使用 malloc 函数给结点申请一块内存,然后才能对该结点的数据域进行赋值,而由于该结点此时是一个独立的结点,没有直接后继结点,所以其指针域为 NULL。
初始化链表初始化链表即初始化一个空链表,详见本文【空链表】一节中的两幅图。
不带头结点要初始化一个不带头节点的链表,我们直接创建一个可以指向结点的空指针即可,该空指针即为头指针:
Node *head = NULL; 带头结点带头结点的单链表的特点是多了一个不存储数据的头结点,所以我们初始化链表时,要将其创建出来。
但是在创建之前,我们先来搞清楚三个问题,分别是:
链表的头指针
指向【指向结点的指针】的指针
函数参数的值传递和地址传递
简单解释:
头指针是链表一定要有的,找到头指针才能找到整个链表,否则整个链表就消失在“茫茫内存”之中了。所以无论进行何种操作,头指针一定要像我们攥紧绳子头一样“被攥在我们手中”。
指针中保存了别人的地址,它也有自己地址。如果一个指针中保存了别的指针的地址,该指针就是“指向指针的指针”。因为头指针是指向链表第一个结点的指针,所以我们找到头指针也就找到了整个链表(这句话啰嗦太多遍了)。而为了能找到头指针,我们就需要知道头指针的地址,也即将头指针的地址保存下来,换句话说,用一个指针来指向头指针。
函数的值传递改变的是形参(实参的一份拷贝),影响不了实参。所以在一些情况下,我们需要传给函数的是地址,函数使用指针来直接操作该指针指向的内存。
如果以上内容还不清楚,说明对指针的掌握还不够熟练,请移步至文章【如何掌握 C 语言的一大利器——指针?】。
下面画一张比较形象的图:
上图中头指针和链表像不像一根带手柄的鞭子?
比如下面这个我小时候经常玩的游戏
/** * 初始化链表 * p_head: 指向头指针的指针 */ void init(Node **p_head) { //创建头结点 Node *node = (Node *) malloc(sizeof(Node)); node->next = NULL; //头指针指向头结点 *p_head = node; } 遍历操作所谓遍历,就是从链表头开始,向链表尾一个一个结点进行遍历,我们通常借助一个辅助指针来进行遍历。
不带头结点不带头结点的链表从头指针开始遍历,所以辅助指针的初始位置就是头指针,这里我们以获取链表的长度为例:
int get_length(Node *head) { int length = 0; Node *p = head; while (p != NULL) { length++; p = p->next; } return length; }使用 for 循环能使代码看起来更精简些。
带头结点带头结点的链表需要从头结点开始遍历,所以辅助指针的初始位置是头结点的后继结点:
int get_length(Node *head) { int length = 0; Node *p = head->next; while (p != NULL) { length++; p = p->next; } return length; } 插入操作 基本思想我们在前面举了“小孩子手拉手”这个例子来描述单链表。
孩子 A 和 B 手拉手链接在一起,现在有个孩子 C 想要插到他们之间,怎么做?
C 拉上 B 的手 => A 松开 B 的手(虚线表示松开) => A 拉上 C 的手
A 松开 B 的手 => A 拉上 C 的手 => C 拉上 B 的手
同样地,在链表中,我们也是类似的操作:
写成代码就是:
new->next = current; previous->next = new;或这换一下顺序:
previous->next = new; new->next = current;这两句就是插入操作的核心代码,也是各种情况下插入操作的不变之处,搞明白这两句,就可以以不变应万变了。