本文主要是介绍链表和环链的一些思考,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
我们拿到一个单链表,遍历链表的时候当运行指针走到NULL的时候(这也是为什么我们每次都需要把链表的最后一个节点的next指向NULL),遍历就停止了,这样看来NULL是我们对链表进行一系列操作的唯一一个标志,因为链表中各个节点是malloc函数开辟出来的在栈上的空间,这样每个节点在内存中的地址可以说都是随机值,不像数组每个元素我们可以用++或者--的方式就能找到前面一个或者回到后面一个元素,当我们对链表节点的指针变量进行++或者--操作的时候,我们找到的可不是下一个或者上一个节点的地址,因为链表每个节点的地址都不是+1或者-1的关系,因为malloc开辟空间的时候是哪里有内存位置就去哪里开辟,不一定是在连续的空间开辟(因为内存中存在内存碎片)。好了,当我们拿到一个如下图所示的链表的时候,这个链表是一个环状的,4这个节点是入环点,这里我们思考如何让程序找到4这个入环点呢,因为链表中不存在最后一个节点,如果用cur=cur->next这样的表达式去遍历链表的话,这将会是一个无线循环,我们找不到停止标志位。(想象一下,你是因为看到了我画的图,才判定4为入环点的,如果给你一个环链表,你是无法看出入环点在哪里的,必须用程序去探测出来)
这里考虑用快慢指针的方法来解决这个问题,让快指针每次走过两个节点,让慢指针每次走过一个节点,这样快指针会比慢指针先入环然后不停的在环里面转圈圈,随着时间的推移,慢指针必定也会在某个时刻入环进行无休止的转圈圈,好了,我们有两个指针在同一个圈圈里面转,那么随着时间的推移快指针一定会同慢指针相遇(可以想象成他们在环内的相对速度就是一步,那么想象成慢指针不动,快指针一次走一步,快指针一定会遇上慢指针)。而且从慢指针入环的那一刻开始,在快指针遇上慢指针之前,慢指针最多只能走上一圈,证明如下:
废话不多说了,上代码:
#include <stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<assert.h>typedef int nodedatatype;typedef struct node {nodedatatype data;struct node* next;
} node;node* buynode() {node* newnode=(node*)malloc(sizeof(node));//这里的malloc函数会在栈里开辟空间,是以下所有开辟空间函数的源头,处理完数据之后,必须用destroy函数对开辟的空间进行销毁newnode->data=0;newnode->next=NULL;return newnode;
}void slpushfront(node**phead,nodedatatype x) {//这里不需要再定义一个新的指针变量来保存原来的头,因为是头插,我们的目的就是要改变头指针变量的值,原来的头地址被保存到了新的头地址的next里node* newnode=buynode();newnode->data=x;newnode->next=*phead;*phead=newnode;
}void slpushback(node**phead,nodedatatype x) {if(*phead==NULL)//如果头为空,不进行该语句判断的话,下面就会用到NULL->next操作而出现错误slpushfront(phead,x);else {node* ptail=*phead;//这里定义一个尾指针变量防止对头指针变量进行赋值操作而把原来最开始的头地址丢失while(ptail->next) {ptail=ptail->next;}node* newnode=buynode();newnode->data=x;newnode->next=NULL;ptail->next=newnode;}}void nodeinsert(node**phead,node* pos,nodedatatype x) { //在pos位置之前插入x节点assert(pos);if(pos==*phead)//如果要插入的位置就是头,那么调用头插函数,因为头插要改变原来头指针变量的值,该插入函数不具备该功能slpushfront(phead,x);node* prv=*phead;//定义一个prv指针变量让它一直走到pos位置的前一个,间接保留原来的头地址node* newhead=buynode();newhead->data=x;newhead->next=pos;while(prv->next!=pos)prv=prv->next;prv->next=newhead;}void nodeerase(node**phead,node* pos) {assert(*phead);//防止为空时以下->操作出现错误if(!pos)//如果pos为空,那么就不需要擦除了,不管三七二十一我直接return就行了return;node* prv=*phead;while(prv->next!=pos)prv=prv->next;prv->next=pos->next;free(pos);pos=NULL;
}node* slfind(node**phead,nodedatatype x) {node* cur=*phead;while(cur->data!=x) {cur=cur->next;if(!cur)return NULL;//防止cur走到最后变成空,而返回while循环里进行->操作时,出现错误}return cur;}void printupdate(node**phead) {if(!*phead)//如果头为空指针直接就不用打印了,返回就行了return;node* cur=*phead;while(cur) {printf("%d",cur->data);cur=cur->next;Sleep(500);//这里用到了清除屏幕和暂停函数,从而把节点存储的每个数据值一一动态显示出来,这里为了方便展示如果是一个环链的情况下会一直循环显示数据system("cls");Sleep(500);}printf("NULL");
}void print(node**phead) {node* cur=*phead;while(cur) {//如果头为空,这个循环压根就不会进去,所以不用assert判断空的情况printf("%d->",cur->data);cur=cur->next;}printf("NULL\n");//打印最后的空,如果头为空上面的循环不进去,直接正好打印这个空
}void printnodeaddress(node**phead) {if(!*phead)//如果头为空,直接返回了,不需要打印任何东西return;node* cur=*phead;while(cur) {//从头到尾遍历每个节点,打印每个节点的地址和对应的数据值,如果是一个环链,这里加上sleep函数可以让屏幕一一动态显示在循环的打印节点地址和对应的数据值printf("%d-",cur->data);printf("%p->",cur);Sleep(500);cur=cur->next;}printf("NULL\n");
}node* findnodepoint(node**phead) { //找入环点函数的实现if(!*phead)return NULL;node* fast=*phead;node* slow=*phead;node* head=*phead;while(fast&&fast->next) { //这里fast必须写在前面那个位置以保证当fast正好变为空的时候,逻辑与判断符前面为假,后面的表达式不执行,也就是NULL->next这个语句不会执行,以防止程序出错slow=slow->next;fast=fast->next->next;if(slow==fast)break;}if(fast==NULL||fast->next==NULL)//这里fast必须写在前面那个位置,当fast为空,空等于空,条件为真,逻辑或后面的语句不执行了。return NULL;node* meet=fast;//这时候fast就停留在快慢指针的相遇点while(head!=meet) { //meet从断点开始走,head从原头开始走,两者相遇,则找到了入环点meet=meet->next;head=head->next;}return meet;}void nodedestroy(node**phead) {node* judge=findnodepoint(phead);if(!judge) {//考虑如果是环链的情况下要进入else后面进行执行 int len=1;//用来保存有效节点个数node*cur=*phead;node*tail=*phead;while(tail->next) { tail=tail->next;len++;}node* arr[len];//这里创建一个指针数组,数组元素类型是node*型的,数组大小就是上面计算的lenint i=0;for(i=0; i<len; i++) {arr[i]=cur;//从头节点开始,把每个节点的地址都保存到对应的数组里面,因为上面创建的是指针数组,所以当然可以保存节点的地址拉
// printf("%p->",arr[i]);//数组的每个元素说白了就是一个指针,这里用%p的形式打印出来看看是否成功保存了每个节点的地址cur=cur->next;}printf("\n");for(i=0; i<len; i++) {free(arr[i]);//既然我们把每个节点的地址都保存到了对应的数组元素里,现在我们就可以任意的操作节点啦,也不怕free了前面的指针而找不到其next对应的下一个节点地址拉,这里是以空间换取时间,这个函数的时间复杂度是O(N),网上其他一些销毁函数必须每次从头走到尾巴,然后把尾巴free掉,形成一个新的尾巴,然后又从头走到尾,把新的尾巴free掉,这里的时间复杂度是O(n2)arr[i]=NULL;
// printf("%p->",arr[i]);}*phead=NULL;}else{//如果程序走到这里,说明有入环点,是个环链,judge当中为入环点地址node* run=judge->next;//从入环点judge开始,让run这个指针跑一圈回到judge点,算出环的周长,还需要考虑周长为1的特殊情况 int len=0; while(run!=judge) {//如果环的周长为1,run会等于judge,该循环不会进去,所以周长为1的情况也容纳进去了 run=run->next;len++;//len最后为环的周长,不包括入环点 }node* cur=*phead;//接下来计算出从头指针跑到入环点的长度,必须包括入环点while(cur!=judge){cur=cur->next;len++;} len++;//当从头走到入环点的时候上面的循环没进去,缺少入环点这一点的计数,所以这里必须补上去 cur=*phead;//让cur重新变成头 node* arr[len];//这里创建一个指针数组,数组元素类型是node*型的,数组大小就是上面计算的lenint i=0;for(i=0; i<len; i++) {arr[i]=cur;//从头节点开始,把每个节点的地址都保存到对应的数组里面,因为上面创建的是指针数组,所以当然可以保存节点的地址拉
// printf("%p->",arr[i]);//数组的每个元素说白了就是一个指针,这里用%p的形式打印出来看看是否成功保存了每个节点的地址cur=cur->next;}printf("\n");for(i=0; i<len; i++) {free(arr[i]);//既然我们把每个节点的地址都保存到了对应的数组元素里,现在我们就可以任意的操作节点啦,也不怕free了前面的指针而找不到其next对应的下一个节点地址拉,这里是以空间换取时间,这个函数的时间复杂度是O(N),网上其他一些销毁函数必须每次从头走到尾巴,然后把尾巴free掉,形成一个新的尾巴,然后又从头走到尾,把新的尾巴free掉,这里的时间复杂度是O(n2)arr[i]=NULL;
// printf("%p->",arr[i]);}*phead=NULL;}}int main() {node* p=NULL;int i=15;while(i)slpushback(&p,i--);node* add7=slfind(&p,7);//拿到数据值为15的节点地址node* add1=slfind(&p,1);//拿到数据值为1的节点地址add1->next=add1;//把1节点连接上15节点,也就是首尾连接了,形成了一个闭环node* point=findnodepoint(&p); //找到入环点地址printf("%d\n",point->data); //打印验证入环地址nodedestroy(&p);printnodeaddress(&p);//打印会无限循环的打印下去return 0;
}
这篇关于链表和环链的一些思考的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!