数据结构(五) -- C语言版 -- 线性表的链式存储 - 双向链表、双向循环链表

零、读前说明

  • 本文中没有涉及到很多的相关理论知识,也没有做深入的了解,所以,您如果是想要系统的学习、想要多学习关于理论的知识等,那么本文可能并不合适您。
  • 本文中所有设计的代码均通过测试,并且在功能性方面均实现应有的功能。
  • 设计的代码并非全部公开,部分无关紧要代码并没有贴出来。
  • 如果你也对此感兴趣、也想测试源码的话,可以私聊我,非常欢迎一起探讨学习。
  • 由于时间、水平、精力有限,文中难免会出现不准确、甚至错误的地方,也很欢迎大佬看见的话批评指正。
  • 嘻嘻。。。。 。。。。。。。。收!

一、双向链表的概述

  双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。(来自百度百科)

二、双向链表的模型

  双向链表是在单链表的节点中增加一个指向前驱节点的prev指针。所以双向链表在单链表的每个节点中,再设置一个指向前驱节点的指针域,所以在双向链表中的节点都有两个指针域,一个指向前驱节点,一个指向后继节点。所以,双向链表的节点一般存储结构为:

typedef struct __dulinklist
{
    data_t data;
    struct node *next;
    struct node *prev;
} dulinklist_t;

  所以,双向链表的示意图可以为:
在这里插入图片描述

三、工程结构及简单测试案例

  双向链表拥有单链表中的所有操作。
  首先使用一个简单的双向链表的demo程序来直观感受一下起基本的简单的操作。

3.1、测试工程的目录结构

  为了兼容unixwindows系统以及方便进行工程管理,特意使用Cmake工具进行编译等,目前测试工程的目录结构如下所示。

dulinklist/
├── CMakeLists.txt
├── README.md
├── build
├── main
│   └── main.c
├── runtime
└── src
    ├── dulinklist.c
    └── dulinklist.h

4 directories, 5 files

  是的,还是和以前的是一样的结构,而且也说了好多次了,但是我还是忍不住要贴一下,只要你知道了我的工程的结构,那就对于我后续的操作以及文件就不会陌生了。

3.2、双向链表示例源码

3.2.1 、dulinklist.h文件

  dulinklist.h文件内容如下:

#ifndef __LINKLIST_H__
#define __LINKLIST_H__

#define null NULL
typedef int data_t;

typedef struct node
{
    data_t data;
    struct node *next;
    struct node *prev;
} dulinklist_t;

dulinklist_t *dulinklist_create();
int dulinklist_insert_head(dulinklist_t *list, int data);
int dulinklist_insert_tail(dulinklist_t *list, int data);
int dulinklist_insert(dulinklist_t *list, int data, int pos);
dulinklist_t* dulinklist_delete_pos(dulinklist_t *list, int pos);
int dulinklist_length(dulinklist_t *list);
int dulinklist_display_next(dulinklist_t *list);
int dulinklist_display_prev(dulinklist_t *list);

#endif

3.2.2 、dulinklist.c文件

  dulinklist.c文件内容如下:

#include "dulinklist.h"
#include "stdio.h"
#include "stdlib.h"
#include "string.h"

/**
 * 功 能:
 *      创建一个链表
 * 参 数:
 *      无
 * 返回值:
 *      成功:操作句柄
 *      失败:NULL
 **/
dulinklist_t *dulinklist_create()
{
    dulinklist_t *list = (dulinklist_t *)malloc(sizeof(dulinklist_t));
    if (list == null) return NULL;
    
    list->next = NULL;
    list->prev = NULL;
    list->data = 0;
    
    return list;
}

/**
 * 功 能:
 *      链表头部插法节点
 * 参 数:
 *      list: 要操作的链表
 *      data: 插入的数据
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int dulinklist_insert_head(dulinklist_t *list, int data)
{
    if(list == NULL) return -1;
    
    dulinklist_t *node = (dulinklist_t *)malloc(sizeof(dulinklist_t));

    node->data = data;

    node->next = list->next;
    node->prev = list;

    /* 如果是第一次插入节点,则需要进行处理,因为 list->next = NULL */
    if (node->next != NULL)
        node->next->prev = node;
    list->next = node;

    return 0;
}

/**
 * 功 能:
 *      链表尾部插法节点
 * 参 数:
 *      list: 要操作的链表
 *      data: 插入的数据
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int dulinklist_insert_tail(dulinklist_t *list, int data)
{
    if (list == NULL) return -1;
    dulinklist_t *current = list;
    dulinklist_t *node = (dulinklist_t *)malloc(sizeof(dulinklist_t));
    
    node->data = data;
    /* 将指针向后移动 */
    while (NULL != current->next)
        current = current->next;
    
    current->next = node;
    node->prev = current;
    node->next = NULL;

    return 0;
}

/**
 * 功 能:
 *      链表的指定位置pos插入节点
 * 参 数:
 *      list: 要操作的链表
 *      data: 插入的数据
 *      pos : 要插入节点的位置
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int dulinklist_insert(dulinklist_t *list, int data, int pos)
{
    dulinklist_t *current = list;

    if(list == NULL || pos < 0) return -1;

    dulinklist_t *node = (dulinklist_t *)malloc(sizeof(dulinklist_t));

    while (current->next != NULL && pos--) 
        current = current->next;

    node->next = current->next;
    current->next = node;
    node->prev = current;

    /* 第一次插入节点,需要额外的处理 */
    if(node->next != NULL) 
        node->next->prev = node;

    node->data = data;

    return 0;
}

/**
 * 功 能:
 *      删除链表的指定位置pos节点
 * 参 数:
 *      list: 要操作的链表
 *      pos : 要删除节点的位置
 * 返回值:
 *      成功:删除的节点
 *      失败:NULL
 **/
dulinklist_t* dulinklist_delete_pos(dulinklist_t *list, int pos)
{
    if(list == NULL || pos < 0) return NULL;

    dulinklist_t *ret = NULL;
    dulinklist_t *current = list;
    dulinklist_t *next = NULL;

    while(current->next->next != NULL && pos --)
        current = current->next;

    ret = current->next;
    next = ret->next;
    current->next = ret->next;
    
    if(next != NULL)
        next->prev = current;

    return ret;
}

/**
 * 功 能:
 *      获取链表的长度
 * 参 数:
 *      list: 要操作的链表
 * 返回值:
 *      成功:链表的长度
 *      失败:-1
 **/
int dulinklist_length(dulinklist_t *list)
{
    int len = 0;

    if(list == NULL) return -1;
    while (list->next)
    {
        list = list->next;
        len ++;
    }
    return len;
}

/**
 * 功 能:
 *      从头节点开始往后遍历链表
 * 参 数:
 *      list: 要操作的链表
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int dulinklist_display_next(dulinklist_t *list)
{
    if(list == NULL) return -1;
    while (list->next != NULL)
    {
        list = list->next;
        printf("%-3d ", list->data);
    }
    putchar(10);
    return 0;
}

/**
 * 功 能:
 *      从最后一个节点开始往前遍历链表
 * 参 数:
 *      list: 要操作的链表
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int dulinklist_display_prev(dulinklist_t *list)
{
    if(list == NULL) return -1;
    while (list->next != NULL)
    {
        list = list->next;
    }
    while (list->prev != NULL)
    {
        printf("%-3d ", list->data);
        list = list->prev;
    }
    putchar(10);
    return 0;
}

3.3、简单测试案例

  main.c文件内容如下:

#include "../src/dulinklist.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char *argv[])
{
    dulinklist_t *h = dulinklist_create();

    dulinklist_insert_tail(h, 10);
    dulinklist_insert_tail(h, 20);
    dulinklist_insert_tail(h, 30);
    
    printf("尾插后next遍历:");
    dulinklist_display_next(h);
    printf("prev遍历的显示:");
    dulinklist_display_prev(h);
    putchar(10);

    dulinklist_insert_head(h, 80);
    dulinklist_insert_head(h, 90);
    
    printf("头插后next遍历:");
    dulinklist_display_next(h);
    printf("prev遍历的显示:");
    dulinklist_display_prev(h);
    putchar(10);

    dulinklist_insert(h, 50, 0);
    dulinklist_insert(h, 60, 100);

    printf("插入后next遍历:");
    dulinklist_display_next(h);
    printf("prev遍历的显示:");
    dulinklist_display_prev(h);
    putchar(10);

    printf("链表长度length = %d\n\n", dulinklist_length(h));

    dulinklist_delete_pos(h, 0);
    printf("0删除后next遍历显示:");
    dulinklist_display_next(h);
    printf("0删除后prev遍历显示:");
    dulinklist_display_prev(h);
    putchar(10);


    dulinklist_delete_pos(h, 80);
    printf("80删除后next遍历显示:");
    dulinklist_display_next(h);
    printf("80删除后prev遍历显示:");
    dulinklist_display_prev(h);

    return 0;
}

3.4、测试功能构建编译

  构建过程使用cmake工具进行编译,使用如下指令即可。本次测试是在windows环境下进行,其他系统等详细说明在README.md中查看。

cd build
cmake -G"MinGW Makefiles" ..
make

  使用的效果如下图所示。
在这里插入图片描述

3.4、测试结果

  经过cmake编译之后,配置cmake可执行文件放在固定目录runtime下,可以使用在当前目录下使用指令 ./../runtime/dulinklist.exe来运行可执行程序,也可以进入到目录runtime中,然后使用指令 ./dulinklist.exe ,即可运行测试程序。实际测试的结果如下图所示。
在这里插入图片描述

四、通用双向链表的操作

4.1、通用双向链表的操作概述

  是得哈,双向链表拥有单链表中的所有操作。
  对于通用链表,本文设计的操作函数定义如下

DLinkList *DLinkList_Create();
int DLinkList_Destroy(DLinkList **list);
int DLinkList_Clear(DLinkList *list);
int DLinkList_Length(DLinkList *list);
int DLinkList_Insert(DLinkList *list, DLinkListNode *node, int pos);
DLinkListNode *DLinkList_Get(DLinkList *list, int pos);
DLinkListNode *DLinkList_Delete(DLinkList *list, int pos);
DLinkListNode *DLinkList_DeleteNode(DLinkList *list, DLinkListNode *node);
DLinkListNode *DLinkList_Reset(DLinkList *list);
DLinkListNode *DLinkList_Current(DLinkList *list);
DLinkListNode *DLinkList_Next(DLinkList *list);
DLinkListNode *DLinkList_Prev(DLinkList *list);

  函数名字起得这么好,就不需要再进行说明了吧^_^

4.2、通用双向链表的结构定义

  在单链表的基础上,双向链表内部结构定义中,在其内部定义一个prev指针,另外也增加了一个游标的指针slider
  在本例程中使用的结构体定义如下。

#define null NULL
typedef void DLinkList;

typedef struct _tag_DLinkListNode
{
    struct _tag_DLinkListNode *next;
    struct _tag_DLinkListNode *prev;
} DLinkListNode;

typedef struct _tag_DLinkList
{
    DLinkListNode header;
    DLinkListNode*slider;
    int length;
}TDLinkList;

4.3、通用双向链表的操作函数申明

  在本例中,使用的双向链表的操作函数如下所示。

#ifndef __LINKLIST_H__
#define __LINKLIST_H__

#define null NULL
typedef void DLinkList;

typedef struct _tag_DLinkListNode
{
    struct _tag_DLinkListNode *next;
    struct _tag_DLinkListNode *prev;
} DLinkListNode;

typedef struct _tag_DLinkList
{
    DLinkListNode header;
    DLinkListNode*slider;
    int length;
}TDLinkList;

typedef struct _func_DLinkList
{
    DLinkList *(*create)();
    int (*destroy)(DLinkList **list);
    int (*clear)(DLinkList *list);
    int (*length)(DLinkList *list);
    int (*insert)(DLinkList *list, DLinkListNode *node, int pos);
    DLinkListNode *(*get)(DLinkList *list, int pos);
    DLinkListNode *(*delete)(DLinkList *list, int pos);
    DLinkListNode *(*deleteNode)(DLinkList *list, DLinkListNode *node);
    DLinkListNode *(*reset)(DLinkList *list);
    DLinkListNode *(*current)(DLinkList *list);
    DLinkListNode *(*next)(DLinkList *list);
    DLinkListNode *(*prev)(DLinkList *list);
} func_DLinkList;

#endif

4.4、通用双向链表的插入操作

  在双向链表注入的过程中,有两个地方需要额外注意的。
  1、插入第一个节点的时候的异常情况处理
  2、头插入节点的时候的特殊处理
  现在简单说明一下插入的过程,比如在3号位置插入新节点,也就是让原先的3号位置变成4号位置,4号位置变成5号位置。定义辅助指针变量 currentnext指针变量,新插入的节点为 node,所以插入的结构示意图可以为:
在这里插入图片描述
  所以简单来说,插入的直接代码可以为:

    current->next = node;
    node->next = next;

    next->prev = node;
    node->prev = current;

  是的,上面的代码是有坑存在的,想一下刚才说的第一个节点和0号位置的插入的异常处理,就像下面所示的插入状态。
在这里插入图片描述
  当辅助指针 next为空(next == NULL) 的时候,next->prev = node 明显示不合法的,所以,在进行插入的需要特殊处理。

 if(next != NULL) 
 	next->prev = node;

  综上所述,那么插入的代码可以为:

/**
 * 功 能:
 *      在指定的位置插入一个元素
 * 参 数:
 *      list:要操作的链表
 *      node:要插入的节点
 *      pos :要插入的位置
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int DLinkList_Insert(DLinkList *list, DLinkListNode *node, int pos)
{
    int i = 0;
    TDLinkList *slist = NULL;
    /* 参数判断 */
    if (list == NULL || node == NULL || pos < 0)
        return -1;
    
    /* 参数判断并矫正 */
    slist = (TDLinkList *)list;
    
    if (pos > slist->length)
        pos = slist->length;

    /* 下面两个个语句实现的效果一致 */
    // DLinkListNode *current = &(slist->header);
    DLinkListNode *current = (DLinkListNode *)slist;
    DLinkListNode *next = NULL;
    
    i = 0;
    while (i < pos && current->next != NULL)
    {
        i++;
        current = current->next;
    }

    // 记录当前节点的后续节点的信息
    next = current->next;   

    // 让前面的链表链接新的node节点
    current->next = node;
    // 让node链接后续的节点
    node->next = next;
    // 当链表插入第一个节点的时候,需要将特殊处理
    if(next != NULL) next->prev = node;
    
    node->prev = current;

    // 如果是第一次插入元素
    if (slist->length == 0)
        slist->slider = node;

    // 如果是头插法,则current还是指向头部
    // 如果链表是初始的状态,即只有头节点,没有其他的任何业务节点,那么
    // 最后一个节点也是第一个节点,还需要下面的操作吗,你品,你细品。
    if (current == (DLinkListNode *)slist)
        node->prev = NULL;

    // 让长度自加一
    slist->length++;
    
    return 0;
}

4.5、删除节点

  假设要删除3号位置的节点,也就是让原先的4号位置变成3号位置,5号位置变成4号位置。要删除3号节点,那么需要先将3号位置节点的信息缓存,返回之后让调用者进行节点的后续操作,且3号节点记录了4号位置节点的信息。所以,定义三个辅助指针 currentretnext。辅助指针ret用户缓存要删除的节点的信息,设置当前指针current2号位置节点,要删除的3号位置的节点为ret,所以简单的示意图可以为:
在这里插入图片描述
  所以简单来说,删除的直接代码可以为:

 ret = current->next;
 next = ret->next;
 current->next = next;
 next->prev = current;

  是的,对于删除最后一个节点next指针为NULL next == NULL,那么next->prev 为非法存在,简单示意图可以为:
在这里插入图片描述
  所以还是需要特殊处理一下。

if(next != NULL)
{
     next->prev = current;
     if( current == (DLinkListNode *)slist)
         next->prev = NULL;
}

  至此,删除节点完成。删除节点的示例代码代码可以为:

/**
 * 功 能:
 *      从指定的位置删除一个元素
 * 参 数:
 *      list:要操作的链表
 *      pos :要删除元素的位置
 * 返回值:
 *      成功:删除节点的首地址
 *      失败:NULL
 **/
DLinkListNode *DLinkList_Delete(DLinkList *list, int pos)
{
    int i = 0;
    TDLinkList *slist = (TDLinkList *)list;
    DLinkListNode *current = NULL, *ret = NULL, *next = NULL;
    
    if (list == NULL || pos < 0 || slist->length < 1)
        return NULL;

    if (pos > slist->length)
        pos = slist->length;

    // current = &(slist->header);
    current = (DLinkListNode *)slist;

    while (i < pos && current->next != NULL)
    {
        i++;
        current = current->next;
    }

    // 缓存被删除的节点位置
    ret = current->next;
    next = ret->next;

    // 连线,跳过要删除的节点
    current->next = next;
    
    if(next != NULL)
    {
        next->prev = current;
        if( current == (DLinkListNode *)slist)
            next->prev = NULL;
    }

    // 若删除元素为游标所指的元素
    if (slist->slider == ret)
        slist->slider = next;

    // 长度自减一
    slist->length--;

    return ret; //将删除的节点的地址返回,让调用者析构这个内存
}

4.6、其他新增操作函数

  实际上,很多的操作函数其实和单链表的操作函数相差不多,此处不再进行叙述相关重复的函数,主要介绍一下下面的两个函数。

4.6.1、获取节点

  获取指定位置的元素,只需要将辅助指针移动到相对应的位置,之后直接将节点返回即可。

/**
 * 功 能:
 *      获取指定位置的元素
 * 参 数:
 *      list:要操作的链表
 *      pos :要获取元素的位置
 * 返回值:
 *      成功:节点的首地址
 *      失败:NULL
 **/
DLinkListNode *DLinkList_Get(DLinkList *list, int pos)
{
    int i = 0;
    TDLinkList *slist = NULL;
    DLinkListNode *current = NULL;
    
    if (pos < 0 || list == NULL) return NULL;
    
    slist = (TDLinkList *)list;
    /* 参数判断并矫正 */ 
    if (pos > slist->length) pos = slist->length;
    // 让辅助指针变量指向链表的头部
    // current = &(slist->header);
    current = (DLinkListNode *)slist;

    while (i < pos && current->next != NULL)
    {
        i++;
        current = current->next;
    }

    return current->next;
}
4.6.2、游标前移

  相对于前面的单链表,双连表还额外增加了操作函数游标前移,主要实现可以为:

/**
 * 功 能:
 *      将游标前移,指向链表中上一个节点
 *      返回当前游标的值
 * 参 数:
 *      list:要操作的链表
 * 返回值:
 *      成功:当前游标的值
 *      失败:NULL
 **/
DLinkListNode *DLinkList_Prev(DLinkList *list)
{
    TDLinkList *slist = (TDLinkList *)list;
    DLinkListNode *ret = NULL;

    if (slist == NULL || slist->slider == NULL)
        return NULL;

    ret = slist->slider;
    slist->slider = ret->prev;

    return ret;
}

4.7、简单的测试案例

  前面已经说明整体工程的结构,此处使用的结构与上面一模一样,也是使用Cmake进行管理编译等处理。

4.7.1、业务节点定义

  由上面的已经说明,通用链表并不关心业务节点是什么样式的,为了测试底层功能函数的功能、及明显的测试效果,本历程中也是定义简单的结构为:

typedef struct __value
{
    DLinkListNode node;
    int data;
} value_t;
4.7.2、调用测试案例

  本测试案例,只是进行一些操作函数的测试说明,测试案例的代码为:

#include "../src/dulinklist.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 声明底层链表的函数库 */
extern func_DLinkList fun_DLinkList;

typedef struct __value
{
    DLinkListNode node;
    int data;
} value_t;

int main(int argc, const char *argv[])
{
    int i; 

    value_t* pv = NULL;
    value_t v1, v2, v3, v4, v5;

    v1.data = 1;
    v2.data = 2;
    v3.data = 3;
    v4.data = 4;
    v5.data = 5;

    DLinkList *dlist = fun_DLinkList.create();

    fun_DLinkList.insert(dlist, (DLinkListNode *)&v1, fun_DLinkList.length(dlist));
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v2, fun_DLinkList.length(dlist));
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v3, fun_DLinkList.length(dlist));
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v4, fun_DLinkList.length(dlist));
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v5, fun_DLinkList.length(dlist));

    printf("\ndata : ");
    for (i = 0; i < fun_DLinkList.length(dlist); i++)
    {
        pv = (value_t *)fun_DLinkList.get(dlist, i);
        printf("%d\t", pv->data);
    }
    printf("\n\n");

    pv = ( value_t *)fun_DLinkList.delete(dlist, fun_DLinkList.length(dlist) - 1);
    printf("delete : %d\n", pv->data);
    pv = ( value_t *)fun_DLinkList.delete(dlist, 0);
    printf("delete : %d\n\n", pv->data);
    
    printf("data : ");
    for (i = 0; i < fun_DLinkList.length(dlist); i++)
    {
        pv = ( value_t *)fun_DLinkList.next(dlist);
        printf("%d\t", pv->data);
    }
    printf("\n\n");

    fun_DLinkList.reset(dlist);
    fun_DLinkList.next(dlist);
    fun_DLinkList.next(dlist);
    pv = ( value_t *)fun_DLinkList.current(dlist);
    printf("next current : %d\n", pv->data);
    fun_DLinkList.prev(dlist);
    pv = ( value_t *)fun_DLinkList.current(dlist);
    printf("prev current : %d\n", pv->data);

    fun_DLinkList.reset(dlist);
    fun_DLinkList.next(dlist);
    pv = ( value_t *)fun_DLinkList.current(dlist);
    printf("current : %d\n", pv->data);

    printf("length : %d\n", fun_DLinkList.length(dlist));

    fun_DLinkList.clear(dlist);
    fun_DLinkList.destroy(&dlist);

    if(dlist == NULL) 
    {
        printf("free success\n");
    }    
    else
    {
        free(dlist);
        printf("free failed!\n");
    }
    
    return 0;
}

4.7.2、测试编译运行结果

  构建过程使用cmake工具进行编译,使用如下指令即可。本次测试是在windows环境下进行,其他系统等详细说明在README.md中查看。
  使用的效果如下图所示。
在这里插入图片描述
  编译完成之后,使用在当前目录下使用指令 ./../runtime/linkList启动程序运行,效果如下图所示。
在这里插入图片描述

五、通用双向循环链表

  双向循环链表在很多方面与双向链表是一致的,在前面的循环链表中已经说明了,循环链表的插入操作需要注意是头部插入还是尾部插入的操作,那么在双向链表中也需要这样的问题,毕竟想要组成一个环状的结构,头尾的工作还是要稍微多做一点的,下面就简单的说明一下在插入和删除的操作中如何解决相关的问题。

5.1、循环链表的模型

  本文中使用的双向循环链表的示意图可以为下图这样。使用的链表是不带头部的。
在这里插入图片描述
  对于链表的头部节点来说,其实就是一个链表头头的作用。

  • 头节点的后继指针next指针域的值为第一个节点的位置,如果目前链表为,则其next域为NULL。大概示意图可以为下图这样。
      在这里插入图片描述
  • 头节点的前驱指针prev永远为NULL
  • 头节点在链表的插入、删除、获取节点等等操作中,均不参与操作。

5.2、插入操作

  在链表的中间位置插入节点的操作与双向链表的操作是一模一样的,不再赘述了。详情可查看章节 4.4、通用双向链表的插入操作

5.2.1、头插入

  对于循环链表来说,头插入法其实和普通位置插入法一样,只是对于双向循环链表来说需要注意两个地方:

  • 新插入节点的前驱指针 prev 需要连接尾节点的位置
  • 尾节点的后继指针 next 需要连接新插入节点

  细想想,其实对于双向循环链表来说,尾节点其实就是第一个节点的的前驱指针所指的节点。即可以表示为:

// tip:此种表示方式只符合本文中的带有头节点的链表
tail = head->next->prev;

  所以,头部插入的简单的示意图可以为:
在这里插入图片描述

		current->next = node;
		node->next = next;
		next->prev = node;
		
		node->prev = tail;
		tail->next = node;

  可以看出来,这样写代码,一定是会有问题出现的,毕竟在现在这个大千世界,每个事物都有自己的特点,都需要有对自己特殊的处理。
  其实,在链表的初始状态的时候,像上面提到的辅助指针 nexttail等都是不存在的(严格来说应该是NULL),那就不能按照上面的代码这样进行连接,需要进行一些特殊处理了。

  所以,在链表初始的状态下进行节点的插入,此时,其实没有严格意义上的头插入和尾插入了,因为是第一个节点的插入,所以,他既是头插入也是尾插入,其简单的示意图可以为:
在这里插入图片描述
  是的,第一个节点就是前面是自己,后面还是自己,在别人至少成双成对的时候,只能自己抱紧自己了。那么,用代码可以简单的概括为这样:

		node->next = node;
        node->prev = node;
        current->next = node;
        current->prev = NULL;    
        slist->slider = node;
5.3.2、尾插入

  对于循环链表来说,尾插入法其实也和普通位置插入法一样的,只是因为是双向循环链表,所以我们不需要遍历整个链表去找尾部节点,其实尾节点的位置就是第一个位置节点的prev所指的节点,那么插入的是示意图可以为下图所示的这样。
在这里插入图片描述
  猛地一看却是是尾部插入,咋一看还真是像模像样,但是仔细看就会发现,这NM的不就是普通的插入嘛。。。。。。。。。。。

  对!我们只是轻描淡写的将尾部节点找到即可。

// tip:此种表示方式只符合本文中的带有头节点的链表
tail = head->next->prev;

  所以,综上所述呢,整个的插入代码那就可以为这样咯:

/**
 * 功 能:
 *      在指定的位置插入一个元素
 * 参 数:
 *      list:要操作的链表
 *      node:要插入的节点
 *      pos :要插入的位置
 * 返回值:
 *      成功:0
 *      失败:-1
 **/
int DLinkList_Insert(DLinkList *list, DLinkListNode *node, int pos)
{
    TDLinkList *slist = NULL;
    /* 参数判断 */
    if (list == NULL || node == NULL || pos < 0) return -1;
    
    slist = (TDLinkList *)list;
    
    /* 参数判断并矫正 */
    if (pos > slist->length) pos = slist->length;

    /* 下面两个个语句实现的效果一致 */
    // DLinkListNode *current = &(slist->header);
    DLinkListNode *current = (DLinkListNode *)slist;
    DLinkListNode *next = NULL;
    DLinkListNode *tail = NULL;
    
    // 向后移动指针
    while (pos --) current = current->next;

    if (current == (DLinkListNode *)slist && slist->length > 0)
        tail = current->next->prev;

    // 记录当前节点的后续节点的信息
    next = current->next;   

    // 让前面的链表链接新的node节点
    current->next = node;
    // 让node链接后续的节点
    node->next = next;
    // 当链表插入第一个节点的时候,需要将特殊处理
    if(next != NULL) next->prev = node;
    
    node->prev = current;

    // 如果是第一次插入元素
    if (slist->length == 0)
    {
        node->next = node;
        node->prev = node;
        // current->next = node;
        current->prev = NULL;    
        slist->slider = node;
    }

    if (current == (DLinkListNode *)slist && slist->length > 0)
    {
        node->prev = tail;
        tail->next = node;
    }
    // 让长度自加一
    slist->length++;
    
    return 0;
}

5.2、删除操作

  根据上面的插入的操作的简单的描述,应该感觉到了删除节点的操作应该也差不多一样的简单,但是应该也有点需要注意的地方,是的哈,先来一张删除节点的示意图康康。。。
在这里插入图片描述
  嗯嗯嗯,还是头删除的示意图,因为啥呢?你品,你细品。。。
  其实不难看出来,尾删除和普通位置删除也是一模一样的。而且,对于头节点删除的方法,也只是需要额外增加将第一个节点和最后一个节点相连接形成环的操作,主要代码可以为下面这样。

	next = ret->next;
    current->next = next;
	next->prev = current;
    ret->prev->next = next;

  对于头节点删除来说,只需要增加这个即可。

	next->prev = ret->prev;

  综上所述,删除节点的代码可以为这样咯。

/**
 * 功 能:
 *      从指定的位置删除一个元素
 * 参 数:
 *      list:要操作的链表
 *      pos :要删除元素的位置
 * 返回值:
 *      成功:删除节点的首地址
 *      失败:NULL
 **/
DLinkListNode *DLinkList_Delete(DLinkList *list, int pos)
{
    TDLinkList *slist = (TDLinkList *)list;
    DLinkListNode *current = NULL, *ret = NULL, *next = NULL;
    
    if (list == NULL || pos < 0 || slist->length < 1)
        return NULL;

    if (pos > slist->length) pos = slist->length;

    // current = &(slist->header);
    current = (DLinkListNode *)slist;

    while (pos--) current = current->next;
    // 缓存被删除的节点位置
    ret = current->next;
    next = ret->next;
    // 连线,跳过要删除的节点
    current->next = next;
    
    if(next != NULL)
    {
        next->prev = current;
        ret->prev->next = next;
        if( current == (DLinkListNode *)slist)
            next->prev = ret->prev;
    }
    // 若删除元素为游标所指的元素
    if (slist->slider == ret) slist->slider = next;

    // 长度自减一
    slist->length--;

    return ret; //将删除的节点的地址返回,让调用者析构这个内存
}

5.3、简单的测试案例

  测试案例就是来简单的测试一下底层实现的函数的功能啥的,那么,很久上面的描述呢,测试案例也就这样了。

#include "../src/dulinklist.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 声明底层链表的函数库 */
extern func_DLinkList fun_DLinkList;

typedef struct __value
{
    DLinkListNode node;
    int data;
} value_t;

int main(int argc, const char *argv[])
{
    int i; 

    value_t* pv = NULL;
    value_t v1, v2, v3, v4, v5;

    v1.data = 1;
    v2.data = 2;
    v3.data = 3;
    v4.data = 4;
    v5.data = 5;

    DLinkList *dlist = fun_DLinkList.create();

    // 分别为头插入和尾插入,先简单的测试其中的一种,免得看数据乱
#if  0
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v1, fun_DLinkList.length(dlist));
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v2, fun_DLinkList.length(dlist));
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v3, fun_DLinkList.length(dlist));
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v4, fun_DLinkList.length(dlist));
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v5, fun_DLinkList.length(dlist));
#else
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v1, 0);
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v2, 0);
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v3, 0);
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v4, 0);
    fun_DLinkList.insert(dlist, (DLinkListNode *)&v5, 0);
#endif

    // 复位游标开始后面的测试工作
    fun_DLinkList.reset(dlist);
    printf("\n链表中的数据 :");
    for (i = 0; i < fun_DLinkList.length(dlist); i++)
    {
        pv = (value_t *)fun_DLinkList.get(dlist, i);
        printf("%d\t", pv->data);
    }
    printf("\n");

    printf("循环链表证明 :");
    
    for (i = 0; i < 2 * fun_DLinkList.length(dlist); i++)
    {
        // 区别于上面的使用 get 的方法,此处测试使用 next + current 的方式遍历
        pv = ( value_t *)fun_DLinkList.current(dlist);
        fun_DLinkList.next(dlist);
        printf("%d\t", pv->data);
    }
    printf("\n");


    pv = ( value_t *)fun_DLinkList.delete(dlist, 1);
    printf("删除位置 1为 : %d\n", pv->data);
    
    printf("删除后的数据 :");
    for (i = 0; i < fun_DLinkList.length(dlist); i++)
    {
        // 区别于上面的使用 next + current 的方法,此处测试使用 next 的方式遍历
        pv = ( value_t *)fun_DLinkList.next(dlist);
        printf("%d\t", pv->data);
    }
    printf("\n");


    pv = ( value_t *)fun_DLinkList.delete(dlist, 0);
    printf("头删除的数据 :%d\n", pv->data);
    fun_DLinkList.reset(dlist);
    printf("头删后的数据 : ");
    for (i = 0; i < 2 * fun_DLinkList.length(dlist); i++)
    {
        pv = ( value_t *)fun_DLinkList.next(dlist);
        printf("%d\t", pv->data);
    }
    printf("\n");

    // 复位游标指针,使用 prev 进行遍历
    fun_DLinkList.reset(dlist);
    fun_DLinkList.prev(dlist);
    printf("\n链表中的数据 :");
    for (i = 0; i < fun_DLinkList.length(dlist); i++)
    {
        pv = (value_t *)fun_DLinkList.prev(dlist);
        printf("%d\t", pv->data);
    }
    printf("\n");

    // 复位游标指针,使用 prev 进行遍历
    fun_DLinkList.reset(dlist);
    fun_DLinkList.prev(dlist);
    printf("循环证明数据 :");
    for (i = 0; i < 2 * fun_DLinkList.length(dlist); i++)
    {
        pv = (value_t *)fun_DLinkList.prev(dlist);
        printf("%d\t", pv->data);
    }
    printf("\n");
    
    // 清空 + 销毁链表
    fun_DLinkList.clear(dlist);
    fun_DLinkList.destroy(&dlist);

    if(dlist == NULL) 
    {
        printf("\nfree success\n\n");
    }    
    else
    {
        free(dlist);
        printf("\nfree failed!\n\n");
    }
    
    return 0;
}

5.4、工程编译及测试结果

  没什么可以说的,就是直接干,就是这么简单!一切用效果说话!!!
  编译一波。
在这里插入图片描述
  执行一波。
在这里插入图片描述

六、优点和缺点

6.1、优点

  • 双向链表因为有前驱指针的存在,所以可以同时访问前驱节点和后继节点
  • 当的得到一个节点的信息,可以往前逆向查找其他节点

6.2、缺点

  • 相对于单向链表占用内存来说,双向链表占用内存相对多一些
  • 在增加和删除节点操作相对比较复杂,对于一些特殊情况需要特殊处理

  暂时想到的这样,欢迎补充。。。。

七、说明

  如果你也对此感兴趣、也想测试源码的话,可以私聊我,非常欢迎一起探讨学习。嘻嘻。。。。

上一篇:数据结构(四) – C语言版 – 线性表的链式存储 - 循环链表
下一篇:数据结构(六) – C语言版 – 栈和队列 - 栈的设计与实现

猜你喜欢

转载自blog.csdn.net/zhemingbuhao/article/details/104849489