数据结构——双向链表

文章目录

双向链表

概念

对链表而言,双向均可遍历是最方便的,另外首尾相连循环遍历也可大大增加链表操作的便捷性。因此,双向循环链表,是在实际运用中是最常见的链表形态。

基本操作

与普通的链表完全一致,双向循环链表虽然指针较多,但逻辑是完全一样。基本的操作包括:

  1. 节点设计
  2. 初始化空链表
  3. 增删节点
  4. 链表遍历
  5. 销毁链表

节点设计

双向链表的节点只是比单向链表多了一个前向指针。示例代码如下所示:

c 复制代码
typedef int DATA;
typedef struct node
{
    // 以整型数据为例
    DATA data;
    // 指向相邻的节点的双向指针
    struct node *prev;
    struct node *next;
}NODE;

初始化

所谓初始化,就是构建一条不含有效节点的空链表。

以带头结点的双向循环链表为例,初始化后,其状态如下图所示:

在初始空链表的情况下,链表只有一个头结点,下面是初始化示例代码:

c 复制代码
int dlist_create(NODE** head,DATA data)
{
    // 创建新节点(申请内存空间)
    NODE *pNew = (NODE*)malloc(sizeof(NODE));
    if(!pNew)
    return -1;
    // 给节点赋初值
    pNew -> data = data;
    // 前后指针默认都指向NULL
    pNew -> prev = pNew -> next = NULL;
    // 将新节点作为头节点
    *head = pNew;
    return 0;
}

插入节点

与单链表类似,也可以对双链表中的任意节点进行增删操作,常见的有所谓的头插法、尾插法等,

即:将新节点插入到链表的首部或者尾部,示例代码是:

  • 头插法:新节点插入到链表的头部

    c 复制代码
    // 将新节点pNew,插入到链表的首部
    int dlist_addHead(NODE** head,DATA data)
    {
        // 创建新节点并申请内存
        NODE *pNew = (NODE*)malloc(sizeof(NODE));
        if(!pNew)
        return -1;
        // 给新节点赋值
        pNew -> data = data;
        pNew -> prev = NULL;
        // 后针指向头指针
        pNew -> next = *head;
        // 如果头指针存在
        if(*head)
        // 头指针的前指针指向新节点
        (*head) -> prev = pNew;
        // 新插入的节点作为新的头节点
        *head = pNew;
        return 0;
    }
  • 尾插法

    c 复制代码
    // 将新节点pNew,插入到链表的尾部
    int dlist_addTail(NODE **head, DATA data)
    {
        // 创建节点并申请内存
        NODE *pNew = (NODE *)malloc(sizeof(NODE));
        if (!pNew)
            return -1;
        // 初始化节点
        pNew->data = data;
        pNew->prev = NULL;
        pNew->next = NULL;
        // 用来记录尾节点,默认头节点就是尾节点
        NODE *p = *head;
        if (!p)
        {
            // 头节点不存在,新插入的节点作为头节点
            *head = pNew;
            return 0;
        }
        // 通过循环,查找尾节点
        while (p->next)
        {
            p = p->next;
        }
        // 尾节点的后指针指向新插入的节点
        p->next = pNew;
        // 新插入的节点的前指针指向尾节点
        pNew->prev = p;
        // 此时的新节点作为了新的尾节点
        return 0;
    }
  • 中间插法:将新节点插入到链表的指定位置

    c 复制代码
    // 将新节点pNew,插入到链表的指定位置
    int dlist_insert(NODE** head,DATA pos,DATA data)
    {
        NODE *pNew = (NODE*)malloc(sizeof(NODE));
        if(!pNew)
            return -1;
        pNew -> data = data;
        pNew -> prev = NULL;
        pNew -> next = NULL;
        NODE* p = *head, *q = NULL;
        if(!p)
        {
            *head = pNew;
            return 0;
        }
        if(memcmp(&(p -> data),&pos,sizeof(DATA)) == 0)
        {
            pNew -> next = p;
            p -> prev = pNew;
            *head = pNew;
            return 0;
        }
        while(p)
        {
            if(memcmp(&(p -> data),&pos,sizeof(DATA)) == 0)
            {
                pNew -> next = p;
                pNew -> prev = q;
                p -> prev = pNew;
                q -> next = pNew;
                return 0;
            }
            q = p;
            p = p -> next;
        }
        q -> next = pNew;
        pNew -> prev = q;
        return 0;
    }

剔除节点

注意,从链表中将一个节点剔除出去,并不意味着要释放节点的内容。当然,我们经常在剔除了一个节点之后,紧接着的动作往往是释放它,但是将"剔除"与"释放"两个动作分开,是最基本的函数封装的原则,因为它们虽然常常连在一起使用,但它们之间并无必然联系,例如:当我们要移动一个节点的时候,实质上就是将"剔除"和"插入"的动作连起来,此时就不能释放该节点了。

在双向链表中剔除指定节点的示例代码如下:

c 复制代码
// 将data对应的节点从链表中剔除
int dlist_delete(NODE** head,DATA data)
{
    NODE* p = *head;
    if(!p)
        return -1;
    if(memcmp(&(p -> data),&data,sizeof(DATA)) == 0)
    {
        if(p -> next == NULL)
        {
            *head = NULL;
            free(p);
            return 0;
        }
        *head = p -> next;
        p -> next -> prev = NULL;
        free(p);
        return 0;
    }
    while(p)
    {
        if(memcmp(&(p -> data),&data,sizeof(DATA)) == 0)
        {
            p -> prev -> next = p -> next;
            if(p -> next == NULL)
                p -> prev -> next = NULL;
            else
                p -> next -> prev = p -> prev;
            free(p) ;
            return 0;
        }
        p = p -> next;
    }
    return -1;
}

链表的遍历

对于双向循环链表,路径可以是向后遍历,也可以向前遍历。

下面是根据指定数据查找节点,向前、向后遍历的示例代码,假设遍历每个节点并将其整数数据输出:

c 复制代码
// 根据指定数据查找节点
NODE* dlist_find(const NODE* head,DATA data)
{
    const NODE* p = head;
    while(p)
    {
        if(memcmp(&(p -> data),&data,sizeof(DATA)) == 0)
        return (NODE*)p;
        p = p -> next;
    }
    return NULL;
    }
    // 向前|向后遍历
void dlist_showAll(const NODE* head)
    {
    const NODE* p = head;
    while(p)
    {
        printf("%d ",p -> data);
        p = p -> next;// 向后遍历
        // p = p -> prev;// 向前遍历
    }
    printf("\n");
}

修改链表

我们也可以针对链表中的数据进行修改,只需要提供一个修改的源数据和目标数据即可。

示例代码如下:

c 复制代码
int dlist_update(const NODE* head,DATA old,DATA newdata)
{
    NODE* pFind = NULL;
    if(pFind = dlist_find(head,old))
    {
        pFind -> data = newdata;
        return 0;
    }
    return -1;
}

销毁链表

由于链表中的各个节点被离散地分布在各个随机的内存空间,因此销毁链表必须遍历每一个节点,释放每一个节点。

注意:

销毁链表时,遍历节点要注意不能弄丢相邻节点的指针

示例代码如下:

c 复制代码
void dlist_destroy(NODE** head)
{
    NODE *p = *head, *q = NULL;
    while(p)
    {
        q = p;
        p = p -> next;
        free(q);
    }
    *head = NULL;
}

完整案例

  • dlist.h

    c 复制代码
    #ifndef __DLIST_H
    #define __DLIST_H
    typedef int DATA;
    typedef struct node
    {
        DATA data;
        struct node *prev;// 前驱指针
        struct node *next;// 后继指针
    }NODE;
    // 创建链表(初始化)
    int dlist_create(NODE**,DATA);
    // 向链表插入数据(头插法)
    int dlist_addHead(NODE** head,DATA data);
    // 向链表插入数据(尾插法)
    int dlist_addTail(NODE** head,DATA data);
    // 向链表插入数据(中间插法)
    int dlist_insert(NODE** head,DATA pos,DATA data);
    // 链表数据查询
    NODE* dlist_find(const NODE* head,DATA data);
    // 链表数据更新
    int dlist_update(const NODE* head,DATA old,DATA newdata);
    // 链表数据遍历
    void dlist_showAll(const NODE* head);
    // 链表数据删除
    int dlist_delete(NODE** head,DATA data);
    // 链表回收
    void dlist_destroy(NODE** head);
    #endif
  • dlist.c

    c 复制代码
    #include "dlist.h"
    
    /**
     * @function:   int dlist_create(NODE**,DATA);
     * @berif:      创建双向链表
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              data: 存储在节点中的数据
     * @return :    成功返回 0
     *              失败返回 -1
     */
    int dlist_create(NODE **head, DATA data)
    {
        // 创建一个节点(申请内存)
        NODE *pNew = (NODE *)malloc(sizeof(NODE));
        if (!pNew)
        {
            perror("内存申请失败!");
            return -1;
        }
    
        // 节点的初始化
        pNew->data = data;
        pNew->prev = pNew->next = NULL; // 给前驱指针和后继指针都赋值为NULL
    
        // 将pNew作为头结点
        *head = pNew;
        return 0;
    }
    
    /**
     * @function:   int dlist_addHead(NODE**,DATA);
     * @berif:      向链表头部插入数据
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              data: 存储在节点中的数据
     * @return :    成功返回 0
     *              失败返回 -1
     */
    int dlist_addHead(NODE **head, DATA data)
    {
        // 创建一个节点,申请内存
        NODE *pNew = (NODE *)malloc(sizeof(NODE));
        // 校验
        if (!pNew)
        {
            perror("内存申请失败!");
            return -1;
        }
        // 给节点赋值
        pNew->data = data;
        pNew->prev = NULL;
        pNew->next = *head;
    
        // 如果头结点存在,需要设置head.prev指向pNew
        if (*head)
        {
            (*head)->prev = pNew;
        }
        // 将pNew作为新的头结点
        *head = pNew;
    
        return 0;
    }
    
    /**
     * @function:   int dlist_addTail(NODE**,DATA);
     * @berif:      向链表尾部插入数据
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              data: 存储在节点中的数据
     * @return :    成功返回 0
     *              失败返回 -1
     */
    int dlist_addTail(NODE **head, DATA data)
    {
        // 创建一个节点,申请内存
        NODE *pNew = (NODE *)malloc(sizeof(NODE));
        if (!pNew)
        {
            perror("内存申请失败!");
            return -1;
        }
    
        // 给节点赋初值
        pNew->data = data;
        pNew->prev = pNew->next = NULL;
    
        // 定义一个变量,用来存储尾结点
        NODE *p = *head;
    
        // 第一种情况:没有节点,pNew作为头结点
        if (!p)
        {
            *head = pNew;
            return 0;
        }
    
        // 第二种情况:有节点,向末尾添加pNew
        while (p->next)
        {
            // 移动p的位置
            p = p->next;
        }
    
        // 末尾的p节点跟pNew建立联系,此时pNew称为尾结点
        p->next = pNew;
        pNew->prev = p;
    
        return 0;
    }
    
    /**
     * @function:   int dlist_insert(NODE**,DATA,DATA);
     * @berif:      向链表任意位置插入数据(中间插法)
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              pos:目标位置数据
     *              data: 存储在节点中的数据
     * @return :    成功返回 0
     *              失败返回 -1
     */
    int dlist_insert(NODE **head, DATA pos, DATA data)
    {
        // 创建节点,申请内存
        NODE *pNew = (NODE *)malloc(sizeof(NODE));
        if (!pNew)
        {
            perror("内存申请失败!");
            return -1;
        }
    
        // 给节点赋初值
        pNew->data = data;
        pNew->prev = pNew->next = NULL;
    
        NODE *p = *head, *q = NULL;
    
        // 第一种情况:没有节点,pNew作为头结点
        if (!p)
        {
            *head = pNew;
            return 0;
        }
    
        // 第二种情况:pos对应的节点刚好是头结点(有效节点是头结点,只有一个节点的时候)
        if (memcmp(&(p->data), &pos, sizeof(DATA)) == 0)
        {
            pNew->next = p;
            p->prev = pNew;
            *head = pNew;
            return 0;
        }
        // 第三种情况:pos对应的节点不是头结点(有效节点超过两个)
        while (p) // p 判断当前节点是否为NULL,p->next 判断写一个节点是否为NULL
        {
            if (memcmp(&(p->data), &pos, sizeof(DATA)) == 0)
            {
                pNew->next = p;
                pNew->prev = q;
                p->prev = pNew;
                q->next = pNew;
                return 0;
            }
            // 记录当前位置(上一个节点的位置)
            q = p;
            // 记录下一个节点的位置,也就是更新后的p
            p = p->next;
        }
        // 如果在链表中找不到pos对应的节点,就尾插
        q->next = pNew;
        pNew->prev = q;
    
        return 0;
    }
    
    /**
     * @function:   void dlist_showAll(const NODE*);
     * @berif:      向链表任意位置插入数据(中间插法)
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              pos:目标位置数据
     *              data: 存储在节点中的数据
     * @return :    成功返回 0
     *              失败返回 -1
     */
    void dlist_showAll(const NODE *head)
    {
        const NODE *p = head;
        while (p)
        {
            printf("%d ", p->data);
            // 向后遍历
            p = p->next;
            // 向前遍历
            // p = p->prev;
        }
        printf("\n");
    }
    
    /**
     * @function:   NODE* dlist_find(const NODE*,DATA);
     * @berif:      链表数据查询(根据指定的数据查找节点)
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              data: 要检索的数据
     * @return :    成功返回 NODE*
     *              失败返回 NULL
     */
    NODE *dlist_find(const NODE *head, DATA data)
    {
        // 创建一个变量去接收链表
        const NODE *p = head;
    
        while (p)
        {
            if (memcmp(&(p->data), &data, sizeof(DATA)) == 0)
            {
                return (NODE *)p;
            }
    
            p = p->next;
        }
        return NULL;
    }
    
    //
    /**
     * @function:   dlist_update(const NODE*,DATA,DATA);
     * @berif:      链表数据更新
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              old_data:源数据
     *              new_data: 目标数据
     * @return :    成功返回 0
     *              失败返回 -1
     */
    int dlist_update(const NODE *head, DATA old_data, DATA new_data)
    {
        // 用来接收查询到的节点
        NODE *pFind = NULL;
    
        // 利用刚刚写的查询函数,查询
        if (!(pFind = dlist_find(head, old_data)))
        {
            return -1;
        }
        // 更新数据
        pFind->data = new_data;
    
        return 0;
    }
    
    /**
     * @function:   dlist_delete(NODE**,DATA);
     * @berif:      链表数据删除
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              data: 需要删除的数据
     * @return :    成功返回 0
     *              失败返回 -1
     */
    int dlist_delete(NODE **head, DATA data)
    {
        // 记录需要删除的节点的位置
        NODE *p = *head;
    
        // 第一种情况:链表不存在
        if (!p)
        {
            return -1;
        }
        // 第二种情况:删除的数据对应的节点正好是头结点
        if (memcmp(&(p->data), &data, sizeof(DATA)) == 0)
        {
            // 1. 链表中只有一个节点
            if (p->next == NULL)
            {
                free(p);
                *head = NULL;
                return 0;
            }
            // 2. 链表中有两个以上节点
            // 执行下列代码之前,head和p都指向头结点,下面代码的意思是:将p节点的下一个节点作为新的头结点
            *head = p->next;
            // 解除p的引用关系(下一个节点指向它的关系)
            p->next->prev = NULL;
            // 回收p
            free(p);
    
            return 0;
        }
    
        // 正常删除:链表中要删除的节点不是头结点
        while (p)
        {
            if (memcmp(&(p->data), &data, sizeof(DATA)) == 0)
            {
                // p的上一个节点的next指向p的下一个节点
                p->prev->next = p->next;
                
                // p是尾结点
                if(p->next==NULL)
                {
                    // 解除p的上一个节点跟p的引用
                    p->prev->next = NULL;
                }
                else // p不是尾结点
                {
                    // p的下一个节点的prev指向p的上一个节点
                    p->next->prev = p->prev;
                }
                // 回收解除引用的节点
                free(p);
                return 0;
            }
    
            // 改变循环条件
            p = p->next;
        }
    
        return -1;
    }
    
    /**
     * @function:   void dlist_destroy(NODE**);
     * @berif:      链表回收
     * @argument:   head: 指向头指针变量的地址,用来接收首节点地址
     *              old_data:源数据
     *              new_data: 目标数据
     * @return :    成功返回 0
     *              失败返回 -1
     */
    void dlist_destroy(NODE **head)
    {
        // p记录移动的节点,q记录需要回收的节点
        NODE *p = *head, *q = NULL;
    
        while (p)
        {
            // 实现指针尾随
            q = p;       // 前一个节点
            p = p->next; // 后一个节点
            // 回收q
            free(q);
        }
        *head = NULL;
    }

适用场合

经过单链表、双链表的学习,可以总结链表的适用场合:

  • 适合用于节点数目不固定,动态变化较大的场合
  • 适合用于节点需要频繁插入、删除的场合
  • 适合用于对节点查找效率不十分敏感的场合
相关推荐
菜鸡中的奋斗鸡→挣扎鸡1 小时前
滑动窗口 + 算法复习
数据结构·算法
axxy20003 小时前
leetcode之hot100---240搜索二维矩阵II(C++)
数据结构·算法
Uu_05kkq4 小时前
【C语言1】C语言常见概念(总结复习篇)——库函数、ASCII码、转义字符
c语言·数据结构·算法
1nullptr6 小时前
三次翻转实现数组元素的旋转
数据结构
TT哇6 小时前
【数据结构练习题】链表与LinkedList
java·数据结构·链表
A懿轩A6 小时前
C/C++ 数据结构与算法【栈和队列】 栈+队列详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·栈和队列
1 9 J7 小时前
数据结构 C/C++(实验五:图)
c语言·数据结构·c++·学习·算法
汝即来归8 小时前
选择排序和冒泡排序;MySQL架构
数据结构·算法·排序算法
aaasssdddd9611 小时前
C++的封装(十四):《设计模式》这本书
数据结构·c++·设计模式
芳菲菲其弥章11 小时前
数据结构经典算法总复习(下卷)
数据结构·算法