无头结点的双向链表的一般操作。
声明
本文针对的是无头结点的双链表,而下文所出现的头结点所表示的指的是双链表的第一个结点。文中可能出现语句不当的情况,但绝不会影响读者对本文的理解,请以宽容、乐观的心态阅读本文。文中可能会出现一些错误,在此提前向读者致以歉意,并欢迎在评论区留言指正!
说在前面
和线性链表一样,我们无论创建怎样的双向链表都不可能装得下整个世界。所以我们考虑某一元素,用整个世界去包含该元素,虽然不能包含整个世界,但我们可用该元素去窥探世界。当然,我们可以为该元素添加其他的细枝末节,以使尽可能地包含我们所需要的信息。
马克思说世物质是世界的本源,本源,这词实在是太抽象了,不过,还是得感谢这位英明神武的伟大无产阶级主义者的创造,因为链表创造了一部分世界,那么结点是物质的代名词。
首先,来个结点来瞧瞧:
typedef struct node {
int data;
struct node * next;
}node, list_node;
没错,它就是一个结构体,里面存放了两样东西:本结点的数据data和指向下一结点的指针next。很简陋,但无比强大,如同普通的数据类型,赋予变量预设置的属性,往下看:
node node_1;
node node_2;
node node_3;
由结构体node类型创建的结构体对象就是我们所谓的结点,它们毋庸置疑地包含了int型的数据类型和指向下一结点的node型指针类型。这和我们使用过的int a;具有相同的形式,只不过内部存储的数据不同而已。
铁索连环:
node_1->next = node_2;
node_2->next = node_3;
node_3->next = NULL;
没有存放数据信息,但是结点连起来了,很神奇。
虽然这样纯粹的结构体类型的确已经可以包含很多东西了,但是要想适应千变万化的世界,还需要有千变万化的结点类型;如果存在某类结点,其变化足以应付世界的变化,那么这种结点的生命力相当强大。休得多言,来个例子看看:
typedef struct lively_node {
int data;
string str;
char ch;
my_type my_data;
node my_node;
}lively_node, smart_node;
很明显,这样的结点具有更多的信息,如此一来,我们可以创建各种存储类型的链表,虽然通常情况下只会用到其中的几个。当然,除了C语言外,还可以用C++创建链表,创建方式大致相同,但C++可以使用模板创建链表,可以根据需求自动选择存储的数据类型。
在线性链表中,我们已经见识到了链表的风采。我们可以在链表上插入我们想要插入的数据,从头到尾去遍历,貌似轻松愉悦,但反过来遍历呢?复杂度似乎很大。不难想到,访问尾结点,需要遍历n个结点;访问倒数第二个结点,需要遍历n-1个结点;……;直到访问头结点。这样造下去,发际线又得后移了。为什么要这个样子,太蹩脚了,终于有些聪明人在已有链表的基础上又创造出一种新的链表,双向链表,铁轨两道,中国的高铁听说又提速了。
还记得我们在用于定义结点的结构体中放了哪些东西?整形数据和node指针,完成了单向,好的,现在我们要完成双向的链表,我们需要往这个结构体里边加点东西了,对的,需要加一个指针,方向得向前了,对吧。就是这这副德行:
typedef struct node {
int data;
struct node * forward;
struct node * next;
}node, list_node;
这个结构体就比前面的多长了一条往回走的腿。如此一来,我们现在用该结构体造个人出来向前向后都比较灵活了。试试看:
node node_1;
node node_2;
node node_3;
对,就是这样的,每个结构体对象都具有一个int型的数据属性、一个node型向前的指针属性和一个node型向后的指针属性。走起来吧:
往前走:
node_1->next = node_2;
node_2->next = node_3;
node_3->next = NULL;
往回走:
node_3->forward = node_2;
node_2->forward = node_1;
都说到这了,就开始创建链表和增删改查吧!
如果对线性链表的增删改查api相当熟悉的话,那双向链表的api的实现将变得相当快速,同一个妈生的。
链表操作
接口是否够完善,和对链表的操作是否周全有很大关系,增删改查自是不能少了,但还有一些其他的操作也是经常需要用到的。
0、链表之前
1、创建结点
2、头插法
3、尾插法
4、按索引插入
5、按索引获取结点
6、打印链表
7、删除结点
8、清空链表
9、销毁链表
感觉要干的事还很多啊!
0、链表之前
typedef struct list {
int data;
struct list * forward;
struct list * next;
}list, link_list;
1、创建结点
list * create(int value) {
list * node = NULL;
node = (list *)malloc(sizeof(list));
if (node == NULL) {
return NULL;
}
node->data = value;
node->forward = NULL;
node->next = NULL;
return node;
}
我们在这个函数中干了些什么事情?首先对于函数的输入和输出,形参value就是要放进data的数据,返回的是list的结构体指针,用于和其他结点连接起来形成链表;然后就申明了一个list结构体指针对象,并为该结点分配动态空间,如果空间分配失败,则返回空指针;空间分配成功,则value的值赋给data,往前、往后的指针初始化指向空,结点创建好之后根据实际需求重新调整指向;最后将创建好的结点返回。创建结点的函数就此实现。
2、头插法
顾名思义,从头开始插入,插在第一个结点之前。
int head_insert_list(list * link, int value) {
if (link == NULL) return -1;
list * node = NULL;
node = (list *)malloc(sizeof(list));
if (node == NULL) return -2;
node->data = value;
node->next = link;
link->forward = node;
return 0;
}
从函数的名称就知道我们要干什么,给函数起一个通俗易懂的名称可以提高代码的可读性,利国利民。其中输入输出参数中,形参link表示待操作的双链表,形参value表示利用头插法插入的结点的数值,返回值表示函数的错误返回码,-1表示双链表为空,-2表示创建待插入双链表的新结点时分配空间出错,0表示插入成功;使用头插法,新结点node的往后(next)指向待插入双链表的开头(link), 待插入双链表的开头(link)往前(forward)指向待插入的新结点(node)。函数完成头插法。
说这么久都没上图,来张图吧:
瞧见了吧,首先创建了三个结点a,b,c,结点的值分别为1,2,3,然后通过调整指针的指向创建双链表,并链表值进行打印;接着使用头插法将值4插入双链表中,则a的前一个即为双链表的头结点,插入后的链表的打印结果如图1所示。完美!明天接着写尾插法。
3、尾插法
我又回来啦!现在我们来看看尾插法。和头插法相反,尾插法在链表的尾巴上添加结点。多说无益,上代码:
int tail_insert_list(list * link, int value) {
if (link == NULL) return -1;
list * node = NULL;
node = (list *)malloc(sizeof(list));
if (node == NULL) return -2;
node->data = value;
while (link->next) {
link = link->next;
}
link->next = node;
node->next = NULL;
node->forward = link;
return 0;
}
和头插法长贼像,和尾插法的差异将暴露在光天化日之下,就多了几行代码,主要用于遍历链表,使指针指向链表尾节点,然后在尾节点加入新结点,新结点成为新的尾节点,尾节点往后(next)指向NULL,往前(forward)指向原来的尾结点。函数的输入输出和头插法类似,此处不再赘述。
给出尾插法的效果图:
OK,一起正常,接下来实现按索引插入结点。
4、按索引插入
集头插法和尾插法于一身的插入方式,可以在链表长度以内的任何位置插入新的结点。上代码:
int index_insert_list(list * link, int index, int value) {
if (link == NULL) return -1;
list * node = NULL;
node = (list *)malloc(sizeof(list));
if (node == NULL) return -2;
if (index == 0) {
head_insert_list(link, value);
return 0;
}
node->data = value;
while (index - 1 && link->next) {
link = link->next;
index--;
}
if (index - 1 != 0) return -3;
else if (link->next == NULL) {
tail_insert_list(link, value);
return 0;
}
else {
node->next = link->next;
link->next = node;
node->forward = link;
node->next->forward = node;
}
return 0;
}
很明显,按索引插入的函数返回值又多出一个错误码-3,其表示的是待插入结点的位置(index)超过了双链表的长度;当插入位置为0时,即在链表的头部插入结点,直接调用头插法插入结点即可;当插入位置为链表长度时,即在链表的尾部插入结点,直接调用尾插法插入结点即可;当插入位置在头结点和尾节点之间时,先遍历链表,移动指针至插入位置,然后插入结点即可。OK,按索引插入实现完毕。但里面涉及到头插法和尾插法函数的调用,所以这两个函数需要在该函数的作用域中。
截图来见:
5、按索引获取结点
比起结点的插入,结点的获取要简单的多。下面就是按索引获取结点的实现,请看代码:
int index_print_list(list * link, int index) {
if (link == NULL) return -1;
while (index && link->next) {
index--;
link = link->next;
}
if (index != 0) return -3;
printf("%d", link->data);
return 0;
}
与按索引插入的返回值类似,错误码-3表示的索引值(index)超过了双链表的长度,函数将不打印任何数值,一旦索引值在双链表长度的范围内,则会打印索引对应结点的数值,当然,索引是从0开始的。
看看效果:
6、打印链表
从头结点到尾结点,打印整个双链表的数值,当然首先要指定双链表的头结点,show time!
int print_list(list * link) {
if (link == NULL) return -1;
while (link) {
printf("%d\t", link->data);
link = link->next;
}
printf("\n");
return 0;
}
不用多说,上个效果图:
7、删除结点
删除结点,就是和插入结点对着干,什么样的插入方式,反其道而行之,上代码:
int index_delete_list(list * link, int index) {
if (link == NULL) return -1;
list * node = NULL;
if (index == 0) {
node = link;
link = link->next;
link->next->forward = link;
free(node);
node->forward = NULL;
node->next = NULL;
node = NULL;
return 0;
}
while (index && link->next) {
index--;
link = link->next;
}
if (index != 0) return -3;
else if (link->next == NULL) {
node = link;
node->forward->next = NULL;
free(node);
node->forward = NULL;
node->next = NULL;
node = NULL;
return 0;
}
else {
node = link;
node->forward->next = node->next;
node->next->forward = node->forward;
free(node);
node->forward = NULL;
node->next = NULL;
node = NULL;
return 0;
}
}
代码量瞬间长了好多,有没有?只有三个主要部分:删头、删尾、删中间,OK,删除结点函数大功告成。看看效果:
从图中可以看出,干掉了头结点后, 头结点与之后的结点断开,即原来的双链表变成了一个结点和一个双链表,而这个双链表的头结点即为原双链表的头结点的下一个,故详情请参阅上图。
8、清空链表
首先需要说明,清空不带表销毁链表,而是把里边的数据值置为0,还是挺简单的!
int clean_list(list * link) {
if (link == NULL) return -1;
while (link) {
link->data = 0;
link = link->next;
}
return 0;
}
虽然很简单,但还是上个图比较踏实:
全变成0了,欣喜若狂,好吧,过于简单,没有必要!
9、销毁链表
与清空不同,销毁链表会彻底焚毁链表包括创建链表时临时创建的动态空间,来,上代码:
int destroy_list(list * link) {
if (link == NULL) return -1;
list * node = NULL;
while (link) {
node = link->next;
free(link);
link->forward = NULL;
link->next = NULL;
link = node;
}
return 0;
}
链表被毁成了这个样子,上图:
OK,一切大功告成,请读者批评指正!