单链表的局限
有些线性关系是循环的,即没有队尾元素
一年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,然后约瑟夫环问题输出,结果和上面的图片一致。
循环链表比单链表更加灵活。
用循环链表这样的数据结构解决了约瑟夫环问题,之前我写过一篇随笔,使用数学公式推导的解决办法肯定是最佳的,但是这样的数学推导又有几个人能那么容易得到呢?循环链表可以很好的解决,但是实现一个循环链表,也是颇费时间的,但是这次我们写好了之后,以后就可以复用了。
数学是每个程序员的必修的,然而大部分人都在数学能力上逐步下滑,这样是成为不了优秀的程序员的。