• 第十课 循环链表,约瑟夫环问题的解决


    单链表的局限
    有些线性关系是循环的,即没有队尾元素 

    一年12个月,是重复的,12月过了又要回到1月,对于这样的线性元素规律,前人给我指明了一条更好的道路:循环链表。

    循环链表拥有单链表的所有操作

     创建链表
     销毁链表
     获取链表长度
     清空链表
     获取第pos个元素操作
     插入元素到位置pos
     删除位置pos处的元素

    循环链表的新操作
     获取当前游标指向的数据元素
     将游标重置指向链表中的第一个数据元素
     将游标移动指向到链表中的下一个数据元素
     直接指定删除链表中的某个数据元素

    CircleListNode* CircleList_DeleteNode(CircleList* list, CircleListNode* node);
    CircleListNode* CircleList_Reset(CircleList* list);
    CircleListNode* CircleList_Current(CircleList* list);
    CircleListNode* CircleList_Next(CircleList* list);

    删除操作和单链表不同,直接删除元素,而不是像单链表那样删除函数指定的是pos位置,但是还是借助于了之前单链表删除位置的操作,具体看下面的代码分析。

    循环链表的实现由很多地方和单链表思想一致,只是需要我们根据情况做一些更改。

    循环链表头结点:

    typedef struct _tag_CircleList
    {
        CircleListNode header;
        CircleListNode* slider;//游标
        int length;
    } TCircleList;

    头文件类型声明:

    typedef void CircleList;
    typedef struct _tag_CircleListNode
    {
        struct _tag_CircleListNode* next;
    }CircleListNode;

    循环链表的创建:

    CircleList* CircleList_Create()
    {
        TCircleList * clist=(TCircleList *)malloc(sizeof(TCircleList));
        if(clist != NULL)
        {
            clist->header.next=NULL;
            clist->slider=NULL;
            clist->length=0;
        }
        return clist;
    }

    上面的代码创建一个循环链表的表头,包含结构,这个结构包含一个指向自身结构类型的指针,和一个游标指针,还有一个长度。

    循环链表的销毁,清除,获取长度和单链表的时候一致:

    void CircleList_Destroy(CircleList* list)
    {
        free(list);
    }
    
    void CircleList_Clear(CircleList* list)
    {
        TCircleList* sList = (TCircleList*)list;
        
        if( sList != NULL )
        {
            sList->length = 0;
            sList->header.next = NULL;
            sList->slider = NULL;
        }
    }
    
    int CircleList_Length(CircleList* list) 
    {
        TCircleList* sList = (TCircleList*)list;
        int ret = -1;
        
        if( sList != NULL )
        {
            ret = sList->length;
        }
        
        return ret;
    }

    插入节点,这和单链表的思路一样,但是实现需要做一定更改:

    int CircleList_Insert(CircleList* list, CircleListNode* node, int pos)
    {
        TCircleList *clist=(TCircleList *)list;
        int ret=(list != NULL && node != NULL && pos>=0);// !=优先级大于&&
        int i=0;
        if(ret)
        {
            CircleListNode* current=(CircleListNode*)clist;
            for (i = 0; i < pos && current->next != NULL; ++i)//思考为什么要加current->next != NULL?
            {
                current=current->next;//满足条件进入了说明不是第一次插入,即pos等于0处已经插入过了。
            }
            node->next=current->next;
            current->next=node;
            if(clist->length==0)//第一次插入
            {
                node->next=node;//循环链表,需要把尾部指向头部
                clist->slider=node;//游标要指向第一个节点
            }
            clist->length++;
        }
      return ret; }

    current->next != NULL是保证current指针的移位是正确的,如果current->next  == NULL,那么第一次插入就不会进入这个for循环,而当链表只有头结点的时候,是不应该执行current指针的移位的。

    获取pos位置的地址:

    CircleListNode* CircleList_Get(CircleList* list, int pos)
    {
        TCircleList *clist=(TCircleList *)list;
        int i ;
        if( (clist != NULL) && (pos >= 0) )
        {
            CircleListNode* current = (CircleListNode*)clist;
            
            for(i=0; i<=pos; i++)//移动pos+1次,刚好取得pos位置
            {
                current = current->next;
            }
            return current;
        }
        return NULL;
    }

    循环链表,获取位置不受链表长度的影响,这和单链表不同,循环链表只用限定pos大于等于0,而单链表应该限定获取的pos要小于长度。如同我们非要说第13个月,那么我们也知道那是第二年的1月。

    删除指定位置:

    CircleListNode* CircleList_Delete(CircleList* list, int pos) 
    {
        TCircleList * clist=(TCircleList *)list;
        CircleListNode* ret = NULL;
        int i = 0;
        if(clist !=NULL && pos>=0)
        {
            CircleListNode* current = (CircleListNode*)clist;
            //CircleListNode* first = sList->header.next;
            CircleListNode* last = (CircleListNode*)CircleList_Get(clist, clist->length - 1);
            for (i = 0; i < pos; ++i)//正常删除,不是特殊点
            {
                current=current->next;
            }
            ret=current->next;
            current->next=ret->next;
            clist->length--;
    
            //特殊点,如果删除的是第一个节点
            if (current==(CircleListNode*)clist)
            {
                //clist->header.next = ret->next;
                last->next = ret->next;
            }
            //如果删除的是游标指向的位置,需要把游标后移
            if( clist->slider == ret )
            {
                clist->slider = ret->next;
            }
            //如果上面的删除导致没有节点了,需要把头结点还原
            if( clist->length == 0 )
            {
                clist->header.next = NULL;
                clist->slider = NULL;
            }
            
        }
        return ret;
    }

    删除指定节点:

    CircleListNode* CircleList_DeleteNode(CircleList* list, CircleListNode* node) 
    {
        TCircleList * clist=(TCircleList *)list;
        CircleListNode* ret = NULL;
        int i = 0;
        
        if( clist != NULL )
        {
            CircleListNode* current = (CircleListNode*)clist;
            
            for(i=0; i<clist->length; i++)
            {
                if( current->next == node )//如果找到要删除的节点,就把这个节点返回
                {
                    ret = node;
                    //ret = current->next;和上面的等价
                    break;
                }
                
                current = current->next;
            }
            
            if( ret != NULL )//不等于NULL证明上面的for循环找到了对应需要删除的节点
            {
                CircleList_Delete(clist, i);
            }
        }
        
        return ret;
    }

    上面红色部分,是借用了之前实现的删除节点函数。

    游标的复位:

    CircleListNode* CircleList_Reset(CircleList* list)
    {
        TCircleList* slist = (TCircleList*)list;
        CircleListNode* ret = NULL;
        if (slist != NULL)
        {
            slist->slider=slist->header.next;
            ret=slist->slider;
        }
        return ret;
    }

    复位比较简单,就是回到第一个节点。

    获取当前游标的信息:

    CircleListNode* CircleList_Current(CircleList* list)
    {
        TCircleList *clist=(TCircleList *)list;
        CircleListNode* ret=NULL;
        if (clist !=NULL)
        {
            ret=clist->slider;
        }
        return ret;
    }

    移动游标至下一个位置:

    CircleListNode* CircleList_Next(CircleList* list)
    {
        TCircleList *clist=(TCircleList *)list;
        CircleListNode* ret=NULL;
        if (clist != NULL && clist->slider !=NULL)
        {
            //clist->slider=clist->header.next;
            ret = clist->slider;
            clist->slider = ret->next;
        }
        return ret;
    }

     定义辅助指针变量ret,先保存游标的值,然后ret后移一位,赋值给游标,这样就实现了游标的移动,返回移动前的游标。为什么ret->next 就可以相当于移动游标?ret = clist->slider;游标赋值给ret,此时ret是指向第一个节点的,因为游标在有元素插入之后是指向第一个节点的。ret->next就是下一个位置的地址,这样再把clist->slider = ret->next;就相当于把此时位置的后面一个位置的地址给了游标了,就达到了游标的移位,同时返回游标移位之前的位置,这样我们就可以通过这个游标的返回值访问用户定义的数据。

    代码练兵场:

    约瑟夫环问题
     n 个人围成一个圆圈,首先第 1 个人从 1 开始
    一个人一个人顺时针报数,报到第 m 个人,令
    其出列。然后再从下一 个人开始从 1 顺时针报
    数,报到第 m 个人,再令其出列,,如此下去
    ,求出列顺序 。

    这一类题目在面试中经常遇见,今天我们就使用循环链表来将其解决。

    main.c:

    #include <stdio.h>
    #include <stdlib.h>
    #include "CircleList.h"
    
    struct Value
    {
        CircleListNode header;
        int v;
    };
    
    int main(int argc, char *argv[])
    {
        int i = 0;
        CircleList* list = CircleList_Create();
        
        struct Value v1;
        struct Value v2;
        struct Value v3;
        struct Value v4;
        struct Value v5;
        struct Value v6;
        struct Value v7;
        struct Value v8;
        
        v1.v = 1;
        v2.v = 2;
        v3.v = 3;
        v4.v = 4;
        v5.v = 5;
        v6.v = 6;
        v7.v = 7;
        v8.v = 8;
        
        CircleList_Insert(list, (CircleListNode*)&v1, CircleList_Length(list));
        CircleList_Insert(list, (CircleListNode*)&v2, CircleList_Length(list));
        CircleList_Insert(list, (CircleListNode*)&v3, CircleList_Length(list));
        CircleList_Insert(list, (CircleListNode*)&v4, CircleList_Length(list));
        CircleList_Insert(list, (CircleListNode*)&v5, CircleList_Length(list));
        CircleList_Insert(list, (CircleListNode*)&v6, CircleList_Length(list));
        CircleList_Insert(list, (CircleListNode*)&v7, CircleList_Length(list));
        CircleList_Insert(list, (CircleListNode*)&v8, CircleList_Length(list));
        
        for(i=0; i<CircleList_Length(list); i++)
        {
            struct Value* pv = (struct Value*)CircleList_Next(list);
            
            printf("%d
    ", pv->v);
        }
        
        printf("
    ");
        
        CircleList_Reset(list);
        
        while( CircleList_Length(list) > 0 )
        {
            struct Value* pv = NULL;
            
            for(i=1; i<3; i++)//我们游标只用移动两次
            {
                CircleList_Next(list);
            }
            
            pv = (struct Value*)CircleList_Current(list);
            
            printf("%d
    ", pv->v);
            
            CircleList_DeleteNode(list, (CircleListNode*)pv);
        }
        
        CircleList_Destroy(list);
        
        return 0;
    }

    先打印1到8,然后约瑟夫环问题输出,结果和上面的图片一致。

    循环链表比单链表更加灵活。

    用循环链表这样的数据结构解决了约瑟夫环问题,之前我写过一篇随笔,使用数学公式推导的解决办法肯定是最佳的,但是这样的数学推导又有几个人能那么容易得到呢?循环链表可以很好的解决,但是实现一个循环链表,也是颇费时间的,但是这次我们写好了之后,以后就可以复用了。

    数学是每个程序员的必修的,然而大部分人都在数学能力上逐步下滑,这样是成为不了优秀的程序员的。

  • 相关阅读:
    Problem C: 爬楼梯
    Problem E: 倒水(Water)
    Problem H: tmk买礼物
    HDU 1078 FatMouse and Cheese
    POJ 3186 Treats for the Cows
    POJ 1661 Help Jimmy
    POJ 1458 Common Subsequence
    2018-软工机试-D-定西
    2018-软工机试-F-庙会
    2018-软工机试-C-和你在一起
  • 原文地址:https://www.cnblogs.com/yangguang-it/p/7243728.html
Copyright © 2020-2023  润新知