链表和环链的一些思考

我们拿到一个单链表,遍历链表的时候当运行指针走到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*型的,数组大小就是上面计算的len
		int 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*型的,数组大小就是上面计算的len
		int 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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mathhater

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值