双向链表

无头结点的双向链表的一般操作。

声明

本文针对的是无头结点的双链表,而下文所出现的头结点所表示的指的是双链表的第一个结点。文中可能出现语句不当的情况,但绝不会影响读者对本文的理解,请以宽容、乐观的心态阅读本文。文中可能会出现一些错误,在此提前向读者致以歉意,并欢迎在评论区留言指正!

说在前面

和线性链表一样,我们无论创建怎样的双向链表都不可能装得下整个世界。所以我们考虑某一元素,用整个世界去包含该元素,虽然不能包含整个世界,但我们可用该元素去窥探世界。当然,我们可以为该元素添加其他的细枝末节,以使尽可能地包含我们所需要的信息。

马克思说世物质是世界的本源,本源,这词实在是太抽象了,不过,还是得感谢这位英明神武的伟大无产阶级主义者的创造,因为链表创造了一部分世界,那么结点是物质的代名词。
首先,来个结点来瞧瞧:

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,一切大功告成,请读者批评指正!

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页