点击关注上方“五分钟学算法”,

设为“置顶或星标”,第一时间送达干货。

图解:链式存储结构之循环链表(修订版)

转自景禹


循环,顾名思义就是:绕。

打个比方,就是从前山上有座庙,庙里有个老和尚和一个小和尚,有一天老和尚对小和尚说“从前山上有座庙,庙里有个老和尚和一个小和尚,有一天老和尚对小和尚说“从前~~

对于单链表,由于每个结点只存储了向后的指针,到了尾部标识就停止了向后链的操作。也就是说,按照这样的方式,只能索引后继结点不能索引前驱结点。这样一来,不从头结点出发,这样就无法访问到全部结点。

为了解决这个问题,我们只需要将单链表的尾结点的指针由空指针改为指向头结点的指针,问题就结了。

将单链表中尾结点的指针由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表成为单循环链表,简称循环链表

图解:链式存储结构之循环链表(修订版)

:这里并不是说循环链表一定有头结点

其实循环链表的单链表的主要差异就在于循环的判断空链表的条件上,原来判断head->next是否为空,现在则是head->next是否等于head;

终端结点用尾指针rear指示,则查找终端结点是O(1),而开始结点是rear->next->next,当然也是O(1)

01.
循环链表的初始化操作
图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)

左右滑动查看初始化代码执行过程


typedef struct CLinkList
{

    int data;
    struct CLinkList *next;
}node;

void ds_init(node **pNode)
{
    int item;
    node *temp;
    node *target;
    
    printf("请输入结点的值,输入0完成初始化n");
    
    while(1)
    {
        scanf("%d",&item);
        fflush(stdin);
        
        if(item == 0)
            return;
        
        if((*pNode) == NULL)
        {
            *pNode = (node*)malloc(sizeof(struct CLinkList));
            
            if(!(*pNode))
                exit(0);
            
            (*pNode)->data = item;
            (*pNode)->next = *pNode;
        }
        else
        {
            for(target = (*pNode); target->next != (*pNode); target = target->next)
                ;
            temp = (node *)malloc(sizeof(struct CLinkList));
            
            if(!temp)
                exit(0);
            
            temp->data = item;
            temp->next = *pNode;
            target->next = temp;
        }
    }
}


02.

循环链表的插入操作


图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)

左右滑动查看循环链表插入操作的代码执行过程


void ds_insert(node **pNode, int i)
{
    node *temp;
    node *target;
    node *p;
    int item;
    int j = 1;
    
    printf("请输入要插入结点的值:");
    scanf("%d", &item);
    
    if(i == 1)
    {
        temp = (node *)malloc(sizeof(struct CLinkList));
        
        if(!temp)
            exit(0);
        
        temp->data = item;
        
        for(target = (*pNode); target->next != (*pNode); target = target->next)
            ;
        temp->next = (*pNode);
        target->next = temp;
        *pNode = temp;
    }
    else
    {
        target = *pNode;
        
        for(; j < (i-1); ++j)
        {
            target = target->next;
        }
        
        temp = (node *)malloc(sizeof(struct CLinkList));
        
        if(!temp)
            exit(0);
        
        temp->data = item;
        
        p = target->next;
        target->next = temp;
        temp->next = p;
    }
}



03.

循环链表的删除操作

图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)

左右滑动查看循环链表删除操作的代码执行过程


void ds_delete(node **pNode, int i)
{
    node *target;
    node *temp;
    int j = 1;
    
    if(i == 1)
    {
        for(target = *pNode; target->next != *pNode; target = target->next)
            ;
        
        temp = *pNode;
        *pNode = (*pNode)->next;
        target->next = *pNode;
        free(temp);
    }
    else
    {
        target = *pNode;
        
        for(; j < i-1; ++j)
        {
            target = target->next;
        }
        
        temp = target->next;
        target->next = temp->next;
        free(temp);
    }
}


04.

循环链表的查找操作

图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)

左右滑动查看循环链表查找操作的代码执行过程


/*返回结点所在位置*/
int ds_search(node *pNode, int elem)
{
    node *target;
    int i = 1;

    for(target = pNode; target->data != elem && target->next != pNode; ++i)
  {
    target = target->next;
  }
  if(target->next == pNode) /*表中不存在该元素*/
  {
      if(target->data == elem){
            return i;
      }
      return 0;
  }
    else
        return i;
}


05.

约瑟夫环问题

据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到一个洞中,39个犹太人决定宁愿死也不要被敌人抓到,于是决定了一个自杀方式,41个人排成一个圆圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而Josephus 和他的朋友并不想遵从。首先从一个人开始,越过k-2个人(因为第一个人已经被越过),并杀掉第k个人。接着,再越过k-1个人,并杀掉第k个人。这个过程沿着圆圈一直进行,直到最终只剩下一个人留下,这个人就可以继续活着。问题是,给定了和,一开始要站在什么地方才能避免被处决?Josephus要他的朋友先假装遵从,他将朋友与自己安排在第16个与第31个位置,于是逃过了这场死亡游戏。

通过这个故事,有木有感觉学好数据结构的好处呢,可以在特殊的情况下保命用奥!

typedef struct node
{

    int data;
    struct node *next;
}node;

node *create(int n)
{
    node *p = NULL, *head;
    head = (node*)malloc(sizeof(node)); //创建一个头结点
    p = head; //指向当前结点的指针
    node *s;
    int i = 1;
    
    if( 0 != n)
    {
        while(i <= n)
        {
            s = (node*)malloc(sizeof(node));
            s->data = i++;
            p->next = s;
            p = s;
        }
        s->next = head->next;
    }
    
    free(head);
    
    return s->next;
}

int main()
{
    int n = 41;
    int m = 3;
    int i;
    node *p = create(n);
    node *temp;
    
    while (p != p->next) //不是空表执行while循环
    {
        for (i = 1; i < m-1; i++)
        {
            p = p->next;
        }
        printf("%d->", p->next->data);
        temp = p->next;
        p->next = temp->next;
        
        free(temp);
        
        p = p->next;
    }
    
    printf("%dn", p->data);
    
    return 0;
}


06.

约瑟夫环变体,每一次m的值发生变化

编号为1~N的N个人按顺时针方向围坐一圈,每人持有一个密码(正整数,可以自由输入),开始人选一个正整数作为报数上限值M,从第一个人按顺时针方向自1开始顺序报数,报道M时停止报数。报M的人出列,将他的密码作为新的M值,从他顺时针方向上的下一个人开始从1报数,如此下去,直至所有人全部出列为止。

图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)

左右滑动查看该程序的执行流程


static void StatGame(NodeType **ppHead, int iCipher)
{
    int iCounter, iFlag = 1;
    NodeType *pPrv, *pCur, *pDel;
    
    pPrv = pCur = *ppHead;
    /* 将pPrv初始为指向尾结点,为删除作好准备 */
    while (pPrv->next != *ppHead)
        pPrv = pPrv->next;
    
    while (iFlag)
    {
        for (iCounter = 1; iCounter < iCipher; iCounter++)
        {
            pPrv = pCur;
            pCur = pCur->next;
        }
        
        if (pPrv == pCur)
            iFlag = 0;
        
        pDel = pCur; /* 删除pCur指向的结点,即有人出列 */
        pPrv->next = pCur->next;
        pCur = pCur->next;
        iCipher = pDel->cipher;
        
        printf("第%d个人出列, 密码: %dn", pDel->id, pDel->cipher);
        free(pDel);
    }
    *ppHead = NULL;
    getchar();
}



07.

循环链表的特点

  • 在单链表中,我们有了头结点时,我们可以用O(1)的时间访问第一个结点,但对于要访问的最后一个结点,我们必须挨个向下索引,所以需要O(n)

  • 对于循环链表,用O(1)的时间就可以有链表指针访问到最后一个结点。

  • 我们定义一个用于指向终端结点的尾指针来表示循环链表,此时查找开始结点和终端结点就很方便了。

图解:链式存储结构之循环链表(修订版)

题目:实现将两个线性表连接成一个线性表的运算

分析:

  • 若在单链表或头指针表示的单链表上做这种链接操作,都需要遍历第一个链表,找到最后一个结点,然后将第二个链表链接到第一个的后面,其执行时间是O(n)

  • 若在尾指针表示的单循环链表上实现,则只需要修改指针,无需遍历,其执行时间是O(1)

图解:链式存储结构之循环链表(修订版)


LinkList Connect(LinkList A, LinkList B)
{
    LinkList p = A->next;
    A->next = B->next->next;
    
    free(B->next);
    
    B->next = p;
    
    return B;
}



08.

经典题目解析

题目一: 判断单链表中是否有环

有环的定义是,链表的尾结点指向了链表中的某个结点。

图解:链式存储结构之循环链表(修订版)

解析:

  • 方法一:使用p,q 两个指针,p 总是向前走,但 q 每次都从头开始走,对于每个结点,看p走的步数是否和 q 一样。如上面的图,当 p 从1走到3时用了5步,此时若q从head出发,则需要两步就到了3,因而步数不等,出现矛盾,存在环。

图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)

左右滑动查看第一种方法的执行过程


int HasLoop_StepCount(LinkList L)
{
    LinkList p = L; // 定义结点 p
    int step_p = 0; // 指针 p 走过的步数

    while(p)
    { // p 结点存在
        LinkList q = L; // 定义结点 q
        int step_q = 0; // 指针 q 的步数
        while(q)
        { // q 指向的结点不为空
            if(q == p)
            { // 当p与q到达相同结点时
                if(step_p == step_q) // 走过的步数一样
                    break; // 说明没有环
                else                // 否则
                {
                    printf("环的位置在第%d个结点处。nn", step_q);
                    return 1; // 有环并返回1
                }
            }
            q = q->next; // 如果没发现环,继续下一个结点
            step_q++; // step_q 步数自增
        }
        p = p->next; // p继续向后一个结点
        step_p++; // step_p 步数自增
    }
    return 0;
}


  • 方法二:使用p,q两个指针,p每次向前走一步,q每次向前走两步,若在某个时候p == q,则存在环。(快慢指针法)

图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)

左右滑动查看方法二的执行过程


int hasLoop_SpeedPointer(LinkList L)
{
    int step1 = 1;
    int step2 = 2;
    LinkList p = L;
    LinkList q = L;
    
    while(p != NULL && q != NULL && q->next != NULL)
    {
        p = p->next;
        if(q->next != NULL)
        {
            q = q->next->next;
        }
        
        printf("p:%d, q:%dn", p->data, q->data);
        
        if(p == q)
            return 1;
    }
    return 0;
}


题目二:魔术师发牌问题

题目描述:魔术师利用一副牌中的13张黑牌,预先将他们排好后叠放在一起,牌面朝下。对观众说:“我不看牌,只数数就可以猜到每张牌是什么,我大声数数,你们听,不信?现场演示。”魔术师将最上面的那张牌数为1,把他翻过来正好是黑桃A,将黑桃A放在桌子上,第二次数1,2,将第一张牌放在这些牌的下面,将第二张牌翻过来,正好是黑桃2,也将它放在桌子上这样依次进行将13张牌全部翻出,准确无误。

问题:牌的开始顺序是如何安排的?

图解:链式存储结构之循环链表(修订版)图解:链式存储结构之循环链表(修订版)

左右滑动查看扑克牌的顺序安排过程


void Magician(LinkList head)
{
    LinkList p;
    int j;
    int Countnumber = 2;
    
    p = head;
    p->data = 1;
    
    while(1)
    {
        for( j = 0; j < Countnumber; j++)
        {
            p = p->next;
            if(p->data != 0)
            {
                p = p->next;
                j--; //j--就相当于跳过已经有值的存储单元
            }
        }
        
        if(p->data == 0)
        {
            p->data == CountNumber;
            CountNumber++;
            if(CountNumber == 14)
                break;
        }
    }
}


题目三:拉丁方阵问题

问题描述:拉丁方阵是一种n×n的方阵,方阵中恰有n种不同的元素,每种元素恰有n个,并且每种元素在一行和一列中 恰好出现一次。著名数学家和物理学家欧拉使用拉丁字母来作为拉丁方阵里元素的符号,拉丁方阵因此而得名。

拉丁方阵小故事:拉丁方阵追溯到18世纪的欧洲,据说普鲁士的腓特列大帝曾组成一支仪仗队。仪仗队共有36名军官,来自6支部队,每支部队中,上校、中校、少校、上尉、中尉、少尉各一名。他希望这36名军官排成6×6的方阵,方阵的每一行,每一列的6名军官来自不同的部队并且军衔各不相同。后来,他去求教瑞士著名的大数学家欧拉。欧拉发现这是一个不可能完成的任务。

void Latin_square(LinkList head)
{
    LinkList p;
    int n; //方阵的大小
     int i,j; //计数器
    p = head;
    p_cur = head;
    for(i = 0; i < n; i++){
        for(j = 0; i < n; j++)
        {
            printf("%d,", p_cur->data);
            p_cur = p_cur->next;
        }
        printf("n");
        
        p = p->next;
        p_cur = p;
    }
}



感谢大家的阅读,记得帮景禹点一点右下角的在看奥!,ありがとう。


推荐阅读

•   60 个相见恨晚的神器工具•   一线互联网公司技术面试的流程以及注意事项•   自学编程的八大误区!克服它!•   新手如何有效的刷算法题(LeetCode)•   10款VS Code插件神器,第7款超级实用!•   在拼多多上班,是一种什么样的体验?我tm心态崩了呀!•   写给小白,从零开始拥有一个酷炫上线的网站!


欢迎关注我的公众号“五分钟学算法”,如果喜欢,麻烦点一下“在看”~

图解:链式存储结构之循环链表(修订版)

原文始发于微信公众号(五分钟学算法):图解:链式存储结构之循环链表(修订版)