循环链表:优点和缺点

什么是循环链表?

循环链表是一种节点序列,其排列方式使得每个节点都可以追溯到自身。这里的“节点”是一个自引用的元素,其中包含指向其紧邻的一个或两个节点的指针。

下面是包含 3 个节点的循环链表的示意图。

Circular Linked List

在这里,您可以看到每个节点都可以追溯到自身。上面显示的例子是一个循环单链表。

注意:最简单的循环链表是仅指向自身的节点,如下所示

Circular Linked List

循环链表中的基本操作

循环链表上的基本操作是

  1. 插入
  2. 删除,以及
  3. 遍历
  • 插入是将节点放置在循环链表中指定位置的过程。
  • 删除是从链表中移除现有节点的过程。节点可以通过其值的出现或其位置来识别。
  • 循环链表的遍历是显示整个链表内容并追溯回源节点的过程。

在下一节中,您将了解节点插入以及循环单链表中可能出现的插入类型。

插入操作

最初,您需要创建一个指向自身的节点,如图所示。没有这个节点,插入会创建第一个节点。

Insertion Operation

接下来,有两种可能性

  • 在循环链表的当前位置插入。这对应于在常规单链表的开头或结尾插入。在循环链表中,开头和结尾是相同的。
  • 在索引节点之后插入。节点应由对应于其元素值的索引号标识。

要在循环链表的开头/结尾插入,也就是在添加第一个节点的位置,

  • 您需要打破与现有节点的现有自链接
  • 新节点的下一个指针将链接到现有节点。
  • 最后一个节点的下一个指针将指向插入的节点。

注意:作为令牌主控或圆的开头/结尾的指针可以更改。在遍历时,它仍然会返回到同一个节点,稍后将进行讨论。

下面显示了 (a) i-iii 中的步骤

Insertion Operation

(现有节点)

Insertion Operation

第一步) 断开现有链接

Insertion Operation

第二步) 创建前向链接(从新节点到现有节点)

Insertion Operation

第三步) 创建到第一个节点的循环链接

接下来,您将尝试在节点之后插入。

例如,让我们在值为“VALUE0”的节点之后插入“VALUE2”。假设起始点是值为“VALUE0”的节点。

  • 您需要断开第一个和第二个节点之间的链接,并将值为“VALUE2”的节点放在中间。
  • 第一个节点的下一个指针必须链接到此节点,并且此节点的下一个指针必须链接到之前的第二个节点。
  • 其余的排列保持不变。所有节点都可以追溯到自身。

注意:由于存在循环排列,插入节点涉及所有节点的相同过程。完成循环的指针就像任何其他节点一样完成循环。

下面展示了这一点

Insertion Operation

(假设只有两个节点。这是一个平凡的案例)

Insertion Operation

第一步) 移除连接节点之间的内部链接

Insertion Operation

第二步) 将左侧节点连接到新节点

Insertion Operation

第三步) 将新节点连接到右侧节点。

删除操作

假设有一个 3 节点循环链表。删除的案例如下

  • 删除当前元素
  • 删除元素之后。

删除开头/结尾

  1. 从最后一个节点遍历到第一个节点。
  2. 要从末尾删除,应该只有一个遍历步骤,从最后一个节点到第一个节点。
  3. 删除最后一个节点和下一个节点之间的链接。
  4. 将最后一个节点链接到第一个节点之后的一个元素。
  5. 释放第一个节点。

Deletion Operation

(现有设置)

Deletion Operation

第一步) 移除循环链接

Deletion Operation

第二步) 移除第一个和下一个之间的链接,将最后一个节点链接到第一个后面的节点

Deletion Operation

第三步) 释放/解除分配第一个节点

删除节点之后

  1. 遍历直到下一个节点是要删除的节点。
  2. 遍历到下一个节点,将指针放在前一个节点上。
  3. 使用下一个指针将前一个节点连接到当前节点之后的节点。
  4. 释放当前(已断开链接的)节点。

Deletion Operation

第一步) 假设我们需要删除值为“VALUE1”的节点。

Deletion Operation

第二步) 移除前一个节点和当前节点之间的链接。将其前一个节点与其当前节点(值为“VALUE1”)指向的下一个节点链接起来。

Deletion Operation

第三步) 释放或解除分配当前节点。

循环链表的遍历

要遍历循环链表,从最后一个指针开始,检查最后一个指针本身是否为 NULL。如果此条件不成立,请检查是否只有一个元素。否则,使用临时指针进行遍历,直到再次到达最后一个指针,或者根据需要进行多次遍历,如下面的 GIF 所示。

Traversal of a Circular Linked List

循环链表的优点

循环链表的一些优点是

  1. 代码中无需分配 NULL。循环列表除非完全解除分配,否则永远不会指向 NULL 指针。
  2. 循环链表在末尾操作方面具有优势,因为开头和结尾是重合的。诸如轮循调度之类的算法可以整洁地消除以循环方式排队的进程,而不会遇到悬空或 NULL 引用指针。
  3. 循环链表也执行单链表的所有常规功能。事实上,下面讨论的循环双向链表甚至可以消除完全遍历以查找元素的需要。该元素最多只会与起点完全相对,只完成链表的一半。

循环链表的缺点

使用循环链表的缺点如下

  1. 单链表相比,循环链表更复杂。
  2. 与单链表或双向链表相比,反转循环链表更复杂。
  3. 如果处理不当,代码可能会陷入无限循环。
  4. 难以找到列表的末尾和控制循环。
  5. 插入到开头,我们必须遍历整个列表才能找到最后一个节点。(从实现角度)

单链表作为循环链表

鼓励您尝试阅读并实现下面的代码。它展示了与循环链表相关的指针算术。

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

struct node
{
    int item;
    struct node *next;
};

struct node* addToEmpty(struct node*,int);
struct node *insertCurrent(struct node *, int);
struct node *insertAfter(struct node *, int, int);
struct node *removeAfter(struct node *, int);
struct node *removeCurrent(struct node *);

void peek(struct node *);

int main()
{
...

Singly Linked List

代码解释

  1. 代码的前两行是必需的包含头文件。
  2. 下一部分描述了每个自引用节点的结构。它包含一个值和一个与结构类型相同的指针。
  3. 每个结构都反复链接到相同类型的结构对象。
  4. 有不同的函数原型用于
    1. 向空链表添加元素
    2. 插入到循环链表的当前指向的位置。
    3. 插入到链表中特定索引值的后面。
    4. 从链表中删除/移除特定索引值之后的节点。
    5. 从循环链表的当前指向位置移除
  5. 最后一个函数通过在任何状态下的循环遍历打印每个元素。
int main()
{
    struct node *last = NULL;
    last = insertCurrent(last,4);
    last = removeAfter(last, 4);
    peek(last);
    return 0;
}

struct node* addToEmpty(struct node*last, int data)
{
    struct node *temp = (struct node *)malloc(sizeof( struct node));
    temp->item = data;
    last = temp;
    last->next = last;
    return last;
}
    
struct node *insertCurrent(struct node *last, int data)

Singly Linked List

代码解释

  1. 对于 addEmpty 代码,使用 malloc 函数分配一个空节点。
  2. 对于此节点,将数据放入临时变量。
  3. 将唯一的变量分配并链接到临时变量
  4. 将最后一个元素返回到 main() / 应用程序上下文。
struct node *insertCurrent(struct node *last, int data)
{
    if(last == NULL)
    {
       return    addToEmpty(last, data);
    }
    struct node *temp = (struct node *)malloc(sizeof( struct node));
    temp -> item = data;
    temp->next = last->next;
    last->next = temp;
    return last;
}
struct node *insertAfter(struct node *last, int data, int item)
{
    struct node *temp = last->next, *prev = temp, *newnode =NULL;
…

Singly Linked List

代码解释

  1. 如果没有要插入的元素,则应确保添加到空列表并返回控制。
  2. 创建一个临时元素放在当前元素之后。
  3. 按所示链接指针。
  4. 与上一个函数一样,返回最后一个指针。
...
struct node *insertAfter(struct node *last, int data, int item)
{
    struct node *temp = last->next, *prev = temp, *newnode =NULL;
    if (last == NULL)
    {
       return addToEmpty(last, item);
    }
    do
    {
        prev = temp;
        temp = temp->next;
    } while (temp->next != last && temp->item != data );


    if(temp->item != data)
    {
       printf("Element not found. Please try again");
...

Singly Linked List

代码解释

  1. 如果列表中没有元素,则忽略数据,将当前项添加为列表中的最后一项并返回控制。
  2. 对于 do-while 循环中的每次迭代,确保有一个前一个指针保存上一次遍历的结果。
  3. 只有这样才能进行下一次遍历。
  4. 如果找到数据,或者 temp 回到最后一个指针,则 do-while 终止。下一段代码决定了要对该项做什么。
...
    if(temp->item != data)
    {
       printf("Element not found. Please try again");
       return last;
    }
    else
    {
   	 newnode = (struct node *)malloc(sizeof(struct node));
             newnode->item = item;
             prev->next = newnode;
             newnode->next = temp;
    }
    return last;
}

struct node *removeCurrent(struct node *last)
...

Singly Linked List

代码解释

  1. 如果遍历了整个列表但未找到该项,则显示“未找到项”消息,然后将控制权返回给调用者。
  2. 如果找到一个节点,或者该节点还不是最后一个节点,则创建一个新节点。
  3. 将前一个节点与新节点链接。将当前节点与 temp(遍历变量)链接。
  4. 这确保了元素被放置在循环链表中的特定节点之后。返回到调用者。
struct node *removeCurrent(struct node *last)
{
    if(last == NULL)
    {
        printf("Element Not Found");
        return NULL;
    }
    struct node *temp = last->next;
    last->next = temp->next;
    free(temp);
    return last;
}

struct node *removeAfter(struct node *last, int data)

Singly Linked List

代码解释

  1. 要仅删除最后一个(当前)节点,请检查列表是否为空。如果为空,则无法删除任何元素。
  2. 临时变量只需向前移动一个链接。
  3. 将最后一个指针链接到第一个指针之后的指针。
  4. 释放临时指针。它会解除分配未链接的最后一个指针。
struct node *removeAfter(struct node *last,int data)
{
    struct node *temp = NULL,*prev = NULL;
    if (last == NULL)
    {
   	 printf("Linked list empty. Cannot remove any element\n");
   	 return NULL;
    }
    temp = last->next;
    prev = temp;
    do
    {
        prev = temp;
        temp = temp->next;
    } while (temp->next != last && temp->item != data );

    if(temp->item != data)
    {
      printf("Element not found");
...

Singly Linked List

代码解释

  1. 与上一个删除函数一样,检查是否没有元素。如果为真,则无法删除任何元素。
  2. 分配特定的位置给指针以定位要删除的元素。
  3. 指针一个接一个地前进。(prev 在 temp 后面)
  4. 过程继续,直到找到元素,或者下一个元素追溯到最后一个指针。
    if(temp->item != data)
    {
        printf("Element not found");
        return last;
    }
    else
    {
        prev->next = temp->next;
        free(temp);
    }
    return last;
}

void peek(struct node * last)
{
    struct node *temp = last;
    if (last == NULL)
    {
   return;	

Singly Linked List

程序说明

  1. 如果在遍历完整个链表后找到元素,则会显示错误消息“未找到项”。
  2. 否则,在步骤 3 和 4 中将该元素断开链接并释放。
  3. 前一个指针链接到要删除的元素(temp)指向的“下一个”地址。
  4. 因此,temp 指针被解除分配和释放。
...
void peek(struct node * last)
{
    struct node *temp = last;
    if (last == NULL)
    {
         return;    
    }
    if(last -> next == last)
    {
        printf("%d-", temp->item);
    }
    while (temp != last)
    {
       printf("%d-", temp->item);
       temp = temp->next;
    }
}

Singly Linked List

代码解释

  1. 如果需要零个元素,则无法进行查看或遍历。用户需要分配或插入节点。
  2. 如果只有一个节点,则无需遍历,可以打印节点的内容,while 循环不执行。
  3. 如果有多于一个节点,则 temp 打印所有项直到最后一个元素。
  4. 一旦到达最后一个元素,循环就会终止,并且函数将调用返回到 main 函数。

循环链表的应用

  • 在系统进程中实现轮循调度和在高速图形中实现循环调度。
  • 计算机网络中的令牌环调度。
  • 它用于显示单元,如需要数据连续遍历的店面广告牌。