第十课 — 循环链表,约瑟夫环问题的解决
单链表的局限
有些线性关系是循环的,即没有队尾元素
一年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\n", pv->v);
}
printf("\n");
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\n", pv->v);
CircleList_DeleteNode(list, (CircleListNode*)pv);
}
CircleList_Destroy(list);
return 0;
}
先打印1到8,然后约瑟夫环问题输出,结果和上面的图片一致。

循环链表比单链表更加灵活。
用循环链表这样的数据结构解决了约瑟夫环问题,使用数学公式推导的解决办法肯定是最佳的,但是数学推导又有几个人能那么容易得到呢?循环链表可以很好的解决,但是实现一个循环链表,也是颇费时间的,但是这次我们写好了之后,以后就可以复用了。
数学是每个程序员的必修的,然而大部分人都在数学能力上逐步下滑,这样是成为不了优秀的程序员的。
posted on 2019-12-05 13:48 blogernice 阅读(272) 评论(0) 收藏 举报

浙公网安备 33010602011771号