C语言超详细讲解数据结构中双向带头循环链表

C语言超详细讲解数据结构中双向带头循环链表

什么是双向带头循环链表

双向带头循环链表是一种非常常用的数据结构,它由多个节点组成,每个节点都有一个前驱指针和一个后继指针,形成一个双向链表;同时,它也是循环链表,即链表的头指针和尾指针是相连的形成一个环的结构;而带头链表则是在链表的开头添加一个头节点来方便书写,方便读者理解,常见于书籍和教程中。

因此,双向带头循环链表的结构可以表示为:

------         ------         ------   
|head | <----> |node1| <----> |node2| <----> ... <----> |nodeN| <----> |head |
------         ------         ------                       ------ 

双向带头循环链表的实现

定义节点结构体

首先,我们需要定义一个节点的结构体,如下所示:

typedef struct DNode {
    int data;                      // 节点数值 data
    struct DNode *prior, *next;    // 指向直接前驱和直接后继的指针
} DNode, *DLinkList;

节点结构体中,除了记录节点的数值外,还有指向前驱和后继节点的指针,此处用prior和next变量表示,方便我们理解。

初始化双向带头循环链表

接下来我们需要初始化双向带头循环链表,我们定义一个InitList()函数来实现链表的初始化。

int InitList(DLinkList *L) {
    DNode *head = (DNode*)malloc(sizeof(DNode));    // 为头节点分配内存空间
    if (!head)        // 内存分配失败
        return 0;   // 返回0表示初始化失败

    head->data = 0;                      // 头节点的数据域为0
    head->next = head;                   // 头节点的后继指针指向头节点本身
    head->prior= head;                   // 头节点的前驱指针指向头节点本身
    *L = head;                           // 将头节点赋给L指向的链表指针

    return 1;        // 返回1表示初始化成功
}

双向带头循环链表的插入

接下来,我们需要实现双向带头循环链表的插入,即在链表指定位置插入一个新节点。此处我们定义一个InsertList()函数来实现节点的插入。

int InsertList(DLinkList L, int pos, int e) {
    int i = 0;                // i为计数器
    DNode *p = L, *s;         // p为指向当前遍历到的节点, s为插入的新节点
    while (p && i < pos - 1) {   // 定位到 pos 的前一位,如果 pos 为 1,则 p 为头节点
        i++;
        p = p->next;
    }

    if (!p || i > pos - 1)    // 跳出循环有两种情况, i > pos - 1 表示 pos 已经越界
        return 0;            // 返回0表示插入失败 

    s = (DNode*)malloc(sizeof(DNode));   // 为新节点分配内存空间
    s->data = e;           // 新节点的数据域为e
    s->prior= p;           // 新节点的前驱指向p
    s->next = p->next;     // 新节点的后继指向p的后继
    p->next->prior= s;     // p的后继的前驱指向s
    p->next = s;           // p的后继指向s

    return 1;              // 返回1表示插入成功
}

双向带头循环链表的删除

除了插入节点,我们还需要实现双向带头循环链表的删除操作。这里我们定义一个DeleteList()函数来实现删除操作。

int DeleteList(DLinkList L, int pos) {
    int i = 0;                   // i为计数器
    DNode *p = L, *q;            // p为指向当前遍历到的节点,q为被删除的节点

    while (p->next != L && i < pos - 1) {  // 定位到 pos 的前一位,如果 pos 为 1,则 p 为头节点
        i++;
        p = p->next;
    }

    if (p->next == L || i > pos - 1)    // 跳出循环有两种情况, i > pos - 1 表示 pos 已经越界
        return 0;               // 返回0表示删除失败

    q = p->next;          // q指向要删除的节点 
    q->next->prior= p;   // q的后继的前驱指向p
    p->next = q->next;    // p的后继指向q的后继 
    free(q);             // 释放q节点的内存空间

    return 1;             // 返回1表示删除成功
}

双向带头循环链表的遍历

最后,我们需要遍历整个双向带头循环链表,即输出链表中的每一个节点。此处我们定义一个TraverseList()函数来实现链表的遍历。

int TraverseList(DLinkList L) {
    DNode *p = L->next;    // 从第一个节点开始遍历

    while (p != L) {       // 遍历到尾节点为止
        printf("%d ", p->data);  // 输出节点的数据域
        p = p->next;      // 指向下一个节点
    }
    printf("\n");

    return 1;    // 返回1表示遍历成功
}

示例说明

示例1:向链表中插入3个节点,并输出链表

#include <stdio.h>
#include <stdlib.h>

typedef struct DNode {
    int data;                      // 节点数值 data
    struct DNode *prior, *next;    // 指向直接前驱和直接后继的指针
} DNode, *DLinkList;

int InitList(DLinkList *L) {
    DNode *head = (DNode*)malloc(sizeof(DNode));    // 为头节点分配内存空间
    if (!head)        // 内存分配失败
        return 0;   // 返回0表示初始化失败

    head->data = 0;                      // 头节点的数据域为0
    head->next = head;                   // 头节点的后继指针指向头节点本身
    head->prior= head;                   // 头节点的前驱指针指向头节点本身
    *L = head;                           // 将头节点赋给L指向的链表指针

    return 1;        // 返回1表示初始化成功
}

int InsertList(DLinkList L, int pos, int e) {
    int i = 0;                // i为计数器
    DNode *p = L, *s;         // p为指向当前遍历到的节点, s为插入的新节点
    while (p && i < pos - 1) {   // 定位到 pos 的前一位,如果 pos 为 1,则 p 为头节点
        i++;
        p = p->next;
    }

    if (!p || i > pos - 1)    // 跳出循环有两种情况, i > pos - 1 表示 pos 已经越界
        return 0;            // 返回0表示插入失败 

    s = (DNode*)malloc(sizeof(DNode));   // 为新节点分配内存空间
    s->data = e;           // 新节点的数据域为e
    s->prior= p;           // 新节点的前驱指向p
    s->next = p->next;     // 新节点的后继指向p的后继
    p->next->prior= s;     // p的后继的前驱指向s
    p->next = s;           // p的后继指向s

    return 1;              // 返回1表示插入成功
}

int DeleteList(DLinkList L, int pos) {
    int i = 0;                   // i为计数器
    DNode *p = L, *q;            // p为指向当前遍历到的节点,q为被删除的节点

    while (p->next != L && i < pos - 1) {  // 定位到 pos 的前一位,如果 pos 为 1,则 p 为头节点
        i++;
        p = p->next;
    }

    if (p->next == L || i > pos - 1)    // 跳出循环有两种情况, i > pos - 1 表示 pos 已经越界
        return 0;               // 返回0表示删除失败

    q = p->next;          // q指向要删除的节点 
    q->next->prior= p;   // q的后继的前驱指向p
    p->next = q->next;    // p的后继指向q的后继 
    free(q);             // 释放q节点的内存空间

    return 1;             // 返回1表示删除成功
}

int TraverseList(DLinkList L) {
    DNode *p = L->next;    // 从第一个节点开始遍历

    while (p != L) {       // 遍历到尾节点为止
        printf("%d ", p->data);  // 输出节点的数据域
        p = p->next;      // 指向下一个节点
    }
    printf("\n");

    return 1;    // 返回1表示遍历成功
}

int main() {
    DLinkList L;     // 声明双向带头循环链表

    if (InitList(&L)) {     // 初始化链表
        InsertList(L, 1, 1);   // 向链表的第一个位置插入节点1
        InsertList(L, 2, 2);   // 向链表的第二个位置插入节点2
        InsertList(L, 3, 3);   // 向链表的第三个位置插入节点3
        TraverseList(L);       // 输出链表

        return 0;
    }
}

输出结果为:

1 2 3 

示例2:删除链表中第二个节点,并输出链表

#include <stdio.h>
#include <stdlib.h>

typedef struct DNode {
    int data;                      // 节点数值 data
    struct DNode *prior, *next;    // 指向直接前驱和直接后继的指针
} DNode, *DLinkList;

int InitList(DLinkList *L) {
    DNode *head = (DNode*)malloc(sizeof(DNode));    // 为头节点分配内存空间
    if (!head)        // 内存分配失败
        return 0;   // 返回0表示初始化失败

    head->data = 0;                      // 头节点的数据域为0
    head->next = head;                   // 头节点的后继指针指向头节点本身
    head->prior= head;                   // 头节点的前驱指针指向头节点本身
    *L = head;                           // 将头节点赋给L指向的链表指针

    return 1;        // 返回1表示初始化成功
}

int InsertList(DLinkList L, int pos, int e) {
    int i = 0;                // i为计数器
    DNode *p = L, *s;         // p为指向当前遍历到的节点, s为插入的新节点
    while (p && i < pos - 1) {   // 定位到 pos 的前一位,如果 pos 为 1,则 p 为头节点
        i++;
        p = p->next;
    }

    if (!p || i > pos - 1)    // 跳出循环有两种情况, i > pos - 1 表示 pos 已经越界
        return 0;            // 返回0表示插入失败 

    s = (DNode*)malloc(sizeof(DNode));   // 为新节点分配内存空间
    s->data = e;           // 新节点的数据域为e
    s->prior= p;           // 新节点的前驱指向p
    s->next = p->next;     // 新节点的后继指向p的后继
    p->next->prior= s;     // p的后继的前驱指向s
    p->next = s;           // p的后继指向s

    return 1;              // 返回1表示插入成功
}

int DeleteList(DLinkList L, int pos) {
    int i = 0;                   // i为计数器
    DNode *p = L, *q;            // p为指向当前遍历到的节点,q为被删除的节点

    while (p->next != L && i < pos - 1) {  // 定位到 pos 的前一位,如果 pos 为 1,则 p 为头节点
        i++;
        p = p->next;
    }

    if (p->next == L || i > pos - 1)    // 跳出循环有两种情况, i > pos - 1 表示 pos 已经越界
        return 0;               // 返回0表示删除失败

    q = p->next;          // q指向要删除的节点 
    q->next->prior= p;   // q的后继的前驱指向p
    p->next = q->next;    // p的后继指向q的后继 
    free(q);             // 释放q节点的内存空间

    return 1;             // 返回1表示删除成功
}

int TraverseList(DLinkList L) {
    DNode *p = L->next;    // 从第一个节点开始遍历

    while (p != L) {       // 遍历到尾节点为止
        printf("%d ", p->data);  // 输出节点的数据域
        p = p->next;      // 指向下一个节点
    }
    printf("\n");

    return 1;    // 返回1表示遍历成功
}

int main() {
    DLinkList L;     // 声明双向带头循环链表

    if (InitList(&L)) {     // 初始化链表
        InsertList(L, 1, 1);   // 向链表的第一个位置插入节点1
        InsertList(L, 2, 2);   // 向链表的第二个位置插入节点2
        InsertList(L, 3, 3);   // 向链表的第三个位置插入节点3

        DeleteList(L, 2);      // 删除节点2

        TraverseList(L);       // 输出链表

        return 0;
    }
}

输出结果为:

1 3 
阅读剩余 84%

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:C语言超详细讲解数据结构中双向带头循环链表 - Python技术站

(0)
上一篇 2023年6月27日
下一篇 2023年6月27日

相关文章

  • linux 断网 扫描基本命令

    当Linux系统出现网络问题时,可以使用一些基本命令来扫描和诊断问题。本文将为您提供Linux断网扫描基本命令的完整攻略,包括其原理、实现方法和示例。 原理 当Linux系统出现网络问题时,可以使用一些基本命令来扫描和诊断问题。这些命令可以帮助您确定网络连接是否正常,以及确定网络问题的根本原因。以下是一些常用的Linux网络扫描命令: ping:用于测试网络…

    other 2023年5月7日
    00
  • ae怎么制作小球页面加载动效?

    对于怎么制作小球页面加载动效,实际上可以使用 ae 动画软件制作,具体步骤如下: 步骤一:新建一个 ae 项目,并导入素材 首先,我们新建一个 ae 项目,选择一个合适的分辨率(如 1920 * 1080),然后需要导入素材,可以使用 ae 自带的素材库,也可以选择自己准备的素材,或者通过网络下载一些素材。 步骤二:制作小球动画 接下来,我们需要制作小球动画…

    other 2023年6月25日
    00
  • latex笔记

    LaTeX笔记 LaTeX 是一种基于TeX的排版系统,广泛用于学术界、出版社、科研机构等场合。它通过与代码的高度耦合使得用户能够快速排版,并且最终输出的文档具有清晰的结构和优秀的排版效果,非常适合于写作论文、期刊、书籍等需要严谨排版的场合。 本篇笔记主要介绍LaTeX的一些基本语法和常用技巧,以帮助使用者能够更愉快地享受排版的乐趣。 基本语法 注释 在La…

    其他 2023年3月28日
    00
  • 30个开发人员有用的CSS代码片段整理值得借鉴

    下面我就为大家详细讲解“30个开发人员有用的CSS代码片段整理值得借鉴”的攻略。 1. 确认需要的代码片段 在网站中添加CSS代码片段前,需要先确定需要什么样的代码片段。通常来说,我们可以从以下几个方面进行考虑: 网站风格:选择与网站整体风格相符的代码片段,并且可以通过调整代码来实现与网站风格的协调。 网站功能需求:选择能够帮助实现网站功能的代码片段,例如交…

    other 2023年6月28日
    00
  • 递归之斐波那契数列java的3种方法

    递归之斐波那契数列Java的3种方法 什么是斐波那契数列 在数学中,斐波那契数列是以递归的方式定义的:前两个数字是0和1,随后每个数字都是前两个数字的和。 斐波那契数列的前几个数字是0、1、1、2、3、5、8、13、21、34……以此类推。 三种递归方法实现斐波那契数列 方法1:最基本的递归方法 这是最基本的递归方法,但是由于重复计算太多,不适合大规模的计算…

    other 2023年6月27日
    00
  • java IP地址网段计算的示例代码

    Java IP地址网段计算的示例代码攻略 1. 简介 IP地址网段计算是指根据给定的IP地址和子网掩码,计算出该IP地址所在的网段范围。在Java中,可以使用位运算和逻辑运算来实现这个功能。 2. 示例代码 下面是一个示例代码,展示了如何计算IP地址网段的范围: import java.net.InetAddress; import java.net.Unk…

    other 2023年7月31日
    00
  • Android虚拟机与类加载机制详情

    Android虚拟机与类加载机制 什么是Android虚拟机 Android虚拟机是为了在计算机上模拟Android系统环境,方便开发者开发和测试安卓应用程序的工具。目前Android系统所用的虚拟机主要是Dalvik和ART两种。 Dalvik虚拟机 Dalvik虚拟机是Google在Android系统中使用的Java虚拟机,它使用了一种叫做DEX的字节码…

    other 2023年6月25日
    00
  • 腾讯手游助手一直在加载中怎么办?腾讯手游助手无法加载解决方法

    下面是腾讯手游助手一直在加载中的解决方法。 问题描述 有时候我们在使用腾讯手游助手下载游戏时会出现加载中的情况,但始终无法加载完成,无法正常使用。这个问题可能是由于网络问题、软件版本过低或者其他原因引起的。 解决方法 方法一:检查网络连接状态 首先检查一下您的网络连接是否正常,确保您的电脑或者移动设备以及腾讯手游助手能够正常访问互联网。如果您的网络连接不稳定…

    other 2023年6月25日
    00
合作推广
合作推广
分享本页
返回顶部