数据结构之——单循环链表和双向循环链表

一、单循环链表的奥秘

单循环链表是一种特殊的链表结构,它在数据结构领域中具有重要的地位。其独特的循环特性使得它在某些特定的应用场景中表现出强大的优势。

(一)结构与初始化

单循环链表的结构由节点组成,每个节点包含数据域和指向下一个节点的指针域。与普通单链表不同的是,单循环链表的最后一个节点的指针指向头节点,从而形成一个循环。在初始化单循环链表时,通常先创建一个头节点,头节点的数据域可以存储一些特定信息,比如链表长度等。头节点的指针域指向自身,代表此时链表为空。例如在 C 语言中,可以这样初始化单循环链表:

cpp 复制代码
typedef int ElemType;

typedef struct Node 
{
    ElemType data;
    struct Node *next;
} Node;

typedef struct Node *LinkList;

// 初始化单循环链表
void InitList(LinkList &L) 
{
    // 分配内存空间用于创建头节点
    L = (LinkList)malloc(sizeof(Node));
    if (!L) 
    {
        // 如果内存分配失败,退出程序并返回错误码 OVERFLOW
        exit(OVERFLOW);
    }
    // 将头节点的 next 指针指向自身,形成单循环链表
    L->next = L;
    // 头节点的数据域可以用于存储链表的长度等信息,这里初始化为 0
    L->data = 0;
}

(二)插入与删除操作

1.头插法:头插法是将新节点插入到单循环链表的头部。首先创建新节点,将新节点的指针指向头节点的下一个节点,然后将头节点的指针指向新节点,完成插入操作。例如:

cpp 复制代码
// 在单循环链表头部插入节点
void headInsert(LinkList &L, int data) 
{
    // 分配新节点内存空间
    Node *newNode = (Node *)malloc(sizeof(Node));
    // 设置新节点的数据域为传入的数据
    newNode->data = data;
    // 让新节点的 next 指针指向当前链表头部的下一个节点
    newNode->next = L->next;
    // 更新链表头部的 next 指针,使其指向新节点
    L->next = newNode;
}

2.尾插法:尾插法是将新节点插入到单循环链表的尾部。首先找到链表的尾节点,然后将新节点插入到尾节点之后,使其成为新的尾节点。例如:

cpp 复制代码
// 在单循环链表尾部插入节点
void tailInsert(LinkList &L, int data) 
{
    // 分配新节点内存空间
    Node *newNode = (Node *)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = L;
    // 创建一个临时指针用于遍历链表找到尾节点
    Node *p = L;
    // 遍历链表直到找到尾节点(即当前节点的下一个节点是头节点)
    while (p->next!= L) 
    {
        p = p->next;
    }
    // 将尾节点的 next 指针指向新节点
    p->next = newNode;
}

3.删除操作:删除操作可以分为删除头节点和删除其他节点两种情况。删除头节点时,只需将头节点的指针指向头节点的下一个节点即可。删除其他节点时,需要找到要删除节点的前一个节点,然后将其指针指向要删除节点的下一个节点,完成删除操作。例如:

cpp 复制代码
// 删除指定值的节点
void deleteNode(LinkList &L, int data) 
{
    // 创建两个指针 p 和 q,用于遍历链表
    Node *p = L;
    Node *q = L->next;
    // 遍历链表,直到找到要删除的节点或遍历到链表尾部(q == L)
    while (q!= L && q->data!= data) 
    {
        p = q;
        q = q->next;
    }
    // 如果遍历到链表尾部仍未找到要删除的节点
    if (q == L) 
    {
        printf("节点不存在!\n");
    } else 
    {
        // 调整指针,将待删除节点从链表中移除
        p->next = q->next;
        // 释放待删除节点的内存空间
        free(q);
    }
}

(三)总结与应用

单循环链表的优势在于可以方便地进行循环遍历,无需担心链表的末尾。在一些需要循环处理数据的场景中,如约瑟夫问题、魔术师发牌问题等,单循环链表表现出了强大的应用价值。然而,单循环链表也存在一些问题,比如在插入和删除操作时,需要特别注意链表的循环特性,否则容易出现错误。解决这些问题的方法是在编写代码时,仔细考虑各种情况,确保代码的正确性。总之,单循环链表是一种非常有用的数据结构,掌握它的特点和操作方法,对于提高编程能力和解决实际问题具有重要意义。

二、双向循环链表的魅力

双向循环链表作为一种复杂而强大的数据结构,具有独特的魅力和价值。

(一)结构与初始化

双向循环链表的节点结构包含数据域、指向前驱节点的指针域和指向后继节点的指针域。这使得在链表中可以方便地双向遍历,提高了操作的灵活性。在 C 语言中,初始化双向循环链表通常先创建一个头节点,头节点的数据域一般不存储实际数据,其前驱和后继指针都指向自身,形成一个循环。例如:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef int ElemType;

// 定义双向链表节点结构体
typedef struct DNode 
{
    ElemType data;
    struct DNode *prev; // 指向前驱节点的指针
    struct DNode *next; // 指向后继节点的指针
} DNode;

// 定义指向双向链表节点的指针类型别名
typedef struct DNode *DLinkList;

// 初始化双向循环链表
void initDList(DLinkList &DL) 
{
    // 分配内存空间用于创建头节点
    DL = (DLinkList)malloc(sizeof(DNode));
    if (!DL) 
    {
        // 如果内存分配失败,退出程序并返回错误码 OVERFLOW
        exit(OVERFLOW);
    }
    // 将头节点的 prev 和 next 指针都指向自身,形成双向循环链表
    DL->prev = DL;
    DL->next = DL;
    // 头节点的数据域可以用于存储链表的长度等信息,这里初始化为 0
    DL->data = 0;
}

(二)插入操作

1.头部插入:头部插入操作先创建新节点,然后调整新节点的前驱和后继指针,使其指向头节点和原来的头节点的下一个节点,最后调整头节点和原来头节点的下一个节点的指针,使其指向新节点。例如:

cpp 复制代码
// 在双向循环链表头部插入节点
void headInsertDList(DLinkList &DL, int data) 
{
    // 分配新节点内存空间
    DNode *newNode = (DNode *)malloc(sizeof(DNode));
    newNode->data = data;
    // 设置新节点的前驱指针指向头节点
    newNode->prev = DL;
    // 设置新节点的后继指针指向原来头节点的下一个节点
    newNode->next = DL->next;
    // 原来头节点下一个节点的前驱指针更新为新节点
    DL->next->prev = newNode;
    // 头节点的后继指针更新为新节点
    DL->next = newNode;
}

2.尾部插入:尾部插入操作先找到链表的尾节点,然后创建新节点,调整新节点的前驱和后继指针,使其指向尾节点和头节点,最后调整尾节点和头节点的指针,使其指向新节点。例如:

cpp 复制代码
// 在双向循环链表尾部插入节点
void tailInsertDList(DLinkList &DL, int data) 
{
    // 分配新节点内存空间
    DNode *newNode = (DNode *)malloc(sizeof(DNode));
    newNode->data = data;
    // 设置新节点的前驱指针指向原尾节点(即头节点的前驱)
    newNode->prev = DL->prev;
    // 设置新节点的后继指针指向头节点
    newNode->next = DL;
    // 更新原尾节点的后继指针为新节点
    DL->prev->next = newNode;
    // 更新头节点的前驱指针为新节点
    DL->prev = newNode;
}

3.指定位置插入:指定位置插入操作先找到要插入位置的前一个节点,然后创建新节点,调整新节点的前驱和后继指针,使其指向该节点和该节点的下一个节点,最后调整该节点和该节点的下一个节点的指针,使其指向新节点。例如:

cpp 复制代码
// 在双向循环链表指定位置插入节点
void insertAtPositionDList(DLinkList &DL, int pos, int data) 
{
    // 创建一个指针 p,初始指向头节点
    DNode *p = DL;
    // 用于计数当前位置的变量
    int i = 0;
    // 遍历链表找到要插入的位置
    while (p->next!= DL && i < pos - 1) 
    {
        p = p->next;
        i++;
    }
    // 如果遍历完没有找到指定位置,输出插入位置无效的提示信息并返回
    if (i!= pos - 1) 
    {
        printf("插入位置无效!\n");
        return;
    }
    // 分配新节点的内存空间
    DNode *newNode = (DNode *)malloc(sizeof(DNode));
    newNode->data = data;
    // 设置新节点的前驱指针指向当前位置的节点 p
    newNode->prev = p;
    // 设置新节点的后继指针指向当前位置节点 p 的下一个节点
    newNode->next = p->next;
    // 将当前位置节点 p 的下一个节点的前驱指针更新为新节点
    p->next->prev = newNode;
    // 将当前位置节点 p 的后继指针更新为新节点
    p->next = newNode;
}

(三)删除操作

1.删除头部节点:删除头部节点操作先保存头节点的下一个节点,然后调整头节点和头节点的下一个节点的下一个节点的指针,使其指向对方,最后释放头节点的下一个节点。例如:

cpp 复制代码
// 删除双向循环链表的头部节点
void deleteHeadDList(DLinkList &DL) 
{
    // 如果链表为空(头节点的下一个节点还是头节点),输出提示信息并返回
    if (DL->next == DL) 
    {
        printf("链表为空,无法删除头部节点!\n");
        return;
    }
    // 保存要删除的头节点
    DNode *temp = DL->next;
    // 更新头节点的 next 指针指向原来头节点的下一个节点
    DL->next = temp->next;
    // 更新新的头节点的前驱指针为原来的头节点的前驱(现在还是尾节点)
    temp->next->prev = DL;
    // 释放被删除的头节点的内存空间
    free(temp);
}

2.删除尾部节点:删除尾部节点操作先找到链表的尾节点的前一个节点,然后调整该节点和头节点的指针,使其指向对方,最后释放尾节点。例如:

cpp 复制代码
// 删除双向循环链表的尾部节点
void deleteTailDList(DLinkList &DL) 
{
    // 如果链表为空(头节点的下一个节点还是头节点),输出提示信息并返回
    if (DL->next == DL) 
    {
        printf("链表为空,无法删除尾部节点!\n");
        return;
    }
    // 找到尾节点的前驱节点
    DNode *p = DL->prev->prev;
    // 保存要删除的尾节点
    DNode *temp = DL->prev;
    // 更新尾节点前驱节点的 next 指针指向头节点
    p->next = DL;
    // 更新头节点的 prev 指针指向新的尾节点(原来尾节点的前驱)
    DL->prev = p;
    // 释放被删除的尾节点的内存空间
    free(temp);
}

3.删除指定位置节点:删除指定位置节点操作先找到要删除位置的前一个节点,然后保存要删除的节点,调整该节点和该节点的下一个节点的指针,使其指向对方,最后释放要删除的节点。例如:

cpp 复制代码
// 删除双向循环链表指定位置的节点
void deleteAtPositionDList(DLinkList &DL, int pos) 
{
    // 创建一个指针 p,初始指向头节点
    DNode *p = DL;
    // 用于计数当前位置的变量
    int i = 0;
    // 遍历链表找到要删除的位置的前一个节点
    while (p->next!= DL && i < pos - 1) 
    {
        p = p->next;
        i++;
    }
    // 如果遍历完没有找到指定位置或者链表为空,输出删除位置无效的提示信息并返回
    if (i!= pos - 1 || p->next == DL) 
    {
        printf("删除位置无效!\n");
        return;
    }
    // 保存要删除的节点
    DNode *temp = p->next;
    // 更新当前节点的 next 指针指向要删除节点的下一个节点
    p->next = temp->next;
    // 更新要删除节点的下一个节点的 prev 指针指向当前节点
    temp->next->prev = p;
    // 释放被删除的节点的内存空间
    free(temp);
}

(四)遍历与查找

1.遍历:遍历双向循环链表可以从任意一个节点开始,向前或向后遍历。例如从头节点开始向后遍历:

cpp 复制代码
// 遍历双向循环链表
void traverseDList(DLinkList DL) 
{
    // 如果链表为空(头节点的下一个节点还是头节点),输出提示信息并返回
    if (DL->next == DL) 
    {
        printf("链表为空!\n");
        return;
    }
    // 创建一个指针 p,初始指向头节点的下一个节点
    DNode *p = DL->next;
    // 遍历链表并输出每个节点的数据
    while (p!= DL) 
    {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

2.查找:查找指定元素可以从任意一个节点开始,向前或向后遍历,比较节点的数据域和要查找的元素是否相等。例如从头节点开始向后查找:

cpp 复制代码
// 在双向循环链表中查找指定元素
DNode *findInDList(DLinkList DL, int data) 
{
    // 如果链表为空(头节点的下一个节点还是头节点),输出提示信息并返回 NULL
    if (DL->next == DL) 
    {
        printf("链表为空!\n");
        return NULL;
    }
    // 创建一个指针 p,初始指向头节点的下一个节点
    DNode *p = DL->next;
    // 遍历链表查找指定元素
    while (p!= DL && p->data!= data) 
    {
        p = p->next;
    }
    // 如果遍历完没有找到指定元素,输出未找到的提示信息并返回 NULL
    if (p == DL) 
    {
        printf("未找到指定元素!\n");
        return NULL;
    }
    // 返回找到的节点指针
    return p;
}

(五)总结与展望

双向循环链表具有双向遍历、高效插入和删除等特点,在很多应用场景中都有重要的价值。例如在操作系统的内存管理中,可以使用双向循环链表来管理空闲内存块;在数据库系统中,可以用双向循环链表来实现事务的回滚和重做。未来,随着计算机技术的不断发展,双向循环链表可能会在更多的领域得到应用,并且可能会与其他数据结构结合,产生更强大的数据管理和处理能力。同时,对于双向循环链表的优化和改进也将是一个持续的研究方向,比如提高插入和删除操作的效率、减少内存占用等。总之,双向循环链表作为一种重要的数据结构,将在计算机科学的发展中继续发挥重要作用。

相关推荐
wrx繁星点点1 分钟前
行为型模式-策略模式详解
java·开发语言·数据结构·数据库·tomcat·hibernate·策略模式
音符犹如代码32 分钟前
第十三届蓝桥杯真题Java c组D.求和(持续更新)
java·蓝桥杯
KuaiKKyo35 分钟前
QT九月28日
java·数据库·qt
码农小苏241 小时前
排序--希尔排序
java·算法·排序算法
颜淡慕潇1 小时前
【数据库】在 Java 中使用 MongoDB 进行数据聚合
java·数据库·sql·mongodb
Passion不晚1 小时前
Java NIO 全面详解:掌握 `Path` 和 `Files` 的一切
java·后端·nio
林小果11 小时前
NIO基础
java·网络·nio
DavidSoCool1 小时前
java socket bio 改造为 netty nio
java·nio
J老熊2 小时前
Spring Boot 实现动态配置导出,同时支持公式和动态下拉框渲染和性能优化案例示范
java·spring boot·后端·面试·性能优化·系统架构
胡耀超2 小时前
序列化与反序列化深入分析:UUID案例的实践与JSON转换对比
java·json·uuid·序列化·反序列化