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

一、单循环链表的奥秘

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

(一)结构与初始化

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

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

// 在单循环链表头部插入节点
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.尾插法:尾插法是将新节点插入到单循环链表的尾部。首先找到链表的尾节点,然后将新节点插入到尾节点之后,使其成为新的尾节点。例如:

// 在单循环链表尾部插入节点
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.删除操作:删除操作可以分为删除头节点和删除其他节点两种情况。删除头节点时,只需将头节点的指针指向头节点的下一个节点即可。删除其他节点时,需要找到要删除节点的前一个节点,然后将其指针指向要删除节点的下一个节点,完成删除操作。例如:

// 删除指定值的节点
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 语言中,初始化双向循环链表通常先创建一个头节点,头节点的数据域一般不存储实际数据,其前驱和后继指针都指向自身,形成一个循环。例如:

#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.头部插入:头部插入操作先创建新节点,然后调整新节点的前驱和后继指针,使其指向头节点和原来的头节点的下一个节点,最后调整头节点和原来头节点的下一个节点的指针,使其指向新节点。例如:

// 在双向循环链表头部插入节点
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.尾部插入:尾部插入操作先找到链表的尾节点,然后创建新节点,调整新节点的前驱和后继指针,使其指向尾节点和头节点,最后调整尾节点和头节点的指针,使其指向新节点。例如:

// 在双向循环链表尾部插入节点
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.指定位置插入:指定位置插入操作先找到要插入位置的前一个节点,然后创建新节点,调整新节点的前驱和后继指针,使其指向该节点和该节点的下一个节点,最后调整该节点和该节点的下一个节点的指针,使其指向新节点。例如:

// 在双向循环链表指定位置插入节点
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.删除头部节点:删除头部节点操作先保存头节点的下一个节点,然后调整头节点和头节点的下一个节点的下一个节点的指针,使其指向对方,最后释放头节点的下一个节点。例如:

// 删除双向循环链表的头部节点
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.删除尾部节点:删除尾部节点操作先找到链表的尾节点的前一个节点,然后调整该节点和头节点的指针,使其指向对方,最后释放尾节点。例如:

// 删除双向循环链表的尾部节点
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.删除指定位置节点:删除指定位置节点操作先找到要删除位置的前一个节点,然后保存要删除的节点,调整该节点和该节点的下一个节点的指针,使其指向对方,最后释放要删除的节点。例如:

// 删除双向循环链表指定位置的节点
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.遍历:遍历双向循环链表可以从任意一个节点开始,向前或向后遍历。例如从头节点开始向后遍历:

// 遍历双向循环链表
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.查找:查找指定元素可以从任意一个节点开始,向前或向后遍历,比较节点的数据域和要查找的元素是否相等。例如从头节点开始向后查找:

// 在双向循环链表中查找指定元素
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;
}

(五)总结与展望

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

<think>好的,我现在需要帮用户解决如何用代码实现单循环链表双向链表的问题。首先,我得回忆一下单循环链表双向链表的基本概念结构。 对于单循环链表,它普通单链表的区别在于尾节点的指针不再指向空,而是指向头节点,形成一个环。所以在初始化的时候,需要将头节点的next指向自己。插入删除操作时,要特别注意处理尾节点指向头节点的情况,避免出现断链。比如在插入节点时,如果是在尾部插入,新节点的next应该指向头节点,而原来的尾节点的next指向新节点。遍历的时候也需要判断是否回到头节点,否则会进入死循环。 然后考虑双向链表,每个节点有两个指针,分别指向前驱后继。初始化时,头节点的前驱后继可能都指向自己,或者根据具体实现有所不同。插入操作需要调整前后节点的指针,比如在节点后插入新节点,需要设置新节点的前驱后继,然后修改原节点后继节点的指针指向。删除操作同样需要调整前后节点的指针,确保链表不断开。 接下来需要结合用户提供的引用内容。用户给出的引用中有提到循环链表的初始化,双链表的结构更复杂,解决了单链表的缺陷,以及双链表的一些操作链表类似。比如引用[1]提到循环链表的初始化,引用[2][4]说明双链表的优势,引用[3]提到双链表的部分操作与单链表相同。这些信息可以帮助我确认代码实现中的关键点,比如循环链表的初始化步骤,双链表节点的结构设计。 然后,我需要用Python代码来具体实现这两种链表。对于单循环链表,首先定义节点类,包含数据next指针。初始化时,创建头节点,并将next指向自己。插入遍历的方法需要处理循环的情况。比如在遍历时,从头节点出发,循环直到再次遇到头节点为止。 对于双向链表,节点类需要有prevnext两个指针。初始化时,头节点的prevnext可能都指向自己,或者初始为空,根据实现方式不同。插入方法需要处理前驱后继节点的指针调整,比如在头部插入时,新节点的next指向原来的第一个节点,原来的第一个节点的prev指向新节点,头节点的next指向新节点,新节点的prev指向头节点。 需要确保代码符合用户的要求,比如使用中文注释,结构清晰。同时要检查代码是否正确,比如循环链表的遍历是否不会死循环,双向链表的插入删除是否正确处理了前后节点的指针。 可能遇到的问题包括循环链表的遍历终止条件是否正确,双向链表在边界情况下的处理,比如在头部或尾部插入节点时是否正确。需要测试这些情况,确保代码的正确性。 最后,根据用户的要求生成相关问题,比如比较两种链表的优缺点,循环链表应用场景,双链表删除操作的时间复杂度等。这些问题需要与实现内容相关,帮助用户进一步理解。</think>### 单循环链表双向链表的实现方法 #### 一、单循环链表实现 单循环链表的尾节点指针指向头节点,形成闭环。以下是Python实现的关键步骤: **1. 节点定义** ```python class Node: def __init__(self, data): self.data = data self.next = None ``` **2. 初始化循环链表** ```python class CircularLinkedList: def __init__(self): self.head = Node(None) self.head.next = self.head # 构成循环[^1] ``` **3. 插入操作示例(尾部插入)** ```python def append(self, data): new_node = Node(data) current = self.head while current.next != self.head: # 遍历到最后一个节点 current = current.next current.next = new_node new_node.next = self.head # 新节点指向头节点形成环 ``` **4. 遍历操作** ```python def display(self): nodes = [] current = self.head.next while current != self.head: # 终止条件避免死循环 nodes.append(str(current.data)) current = current.next print("->".join(nodes)) ``` --- #### 二、双向链表实现 双向链表每个节点包含前驱(`prev`)后继(`next`)指针,支持双向遍历[^4]。 **1. 节点定义** ```python class DNode: def __init__(self, data): self.data = data self.prev = None self.next = None ``` **2. 初始化双向链表** ```python class DoublyLinkedList: def __init__(self): self.head = DNode(None) self.head.prev = self.head # 初始前驱指向自身 self.head.next = self.head # 初始后继指向自身[^3] ``` **3. 插入操作示例(头部插入)** ```python def insert_at_head(self, data): new_node = DNode(data) new_node.next = self.head.next new_node.prev = self.head self.head.next.prev = new_node # 原首节点prev更新 self.head.next = new_node # 头节点next更新 ``` **4. 删除操作示例** ```python def delete_node(self, target_data): current = self.head.next while current != self.head: # 遍历终止条件 if current.data == target_data: current.prev.next = current.next current.next.prev = current.prev return current = current.next ``` --- #### 三、关键区别对比 | 特性 | 单循环链表 | 双向链表 | |---------------------|------------------------------|-----------------------------------| | 指针方向 | 仅`next`指针,尾节点指向头节点 | 含`prev``next`双向指针 | | 插入/删除操作复杂度 | $O(n)$ | $O(1)$(已知节点位置时)[^2] | | 内存占用 | 每个节点1个指针 | 每个节点2个指针 | | 典型应用 | 轮询任务调度 | 浏览器历史记录/撤销操作栈[^4] | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值