链表和环链的一些思考

2023-11-10 10:10
文章标签 思考 链表 环链

本文主要是介绍链表和环链的一些思考,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

我们拿到一个单链表,遍历链表的时候当运行指针走到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;
}

这篇关于链表和环链的一些思考的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/382004

相关文章

csu1329(双向链表)

题意:给n个盒子,编号为1到n,四个操作:1、将x盒子移到y的左边;2、将x盒子移到y的右边;3、交换x和y盒子的位置;4、将所有的盒子反过来放。 思路分析:用双向链表解决。每个操作的时间复杂度为O(1),用数组来模拟链表,下面的代码是参考刘老师的标程写的。 代码如下: #include<iostream>#include<algorithm>#include<stdio.h>#

深入手撕链表

链表 分类概念单链表增尾插头插插入 删尾删头删删除 查完整实现带头不带头 双向链表初始化增尾插头插插入 删查完整代码 数组 分类 #mermaid-svg-qKD178fTiiaYeKjl {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-

【编程底层思考】垃圾收集机制,GC算法,垃圾收集器类型概述

Java的垃圾收集(Garbage Collection,GC)机制是Java语言的一大特色,它负责自动管理内存的回收,释放不再使用的对象所占用的内存。以下是对Java垃圾收集机制的详细介绍: 一、垃圾收集机制概述: 对象存活判断:垃圾收集器定期检查堆内存中的对象,判断哪些对象是“垃圾”,即不再被任何引用链直接或间接引用的对象。内存回收:将判断为垃圾的对象占用的内存进行回收,以便重新使用。

建立升序链表

题目1181:遍历链表 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:2744 解决:1186 题目描述: 建立一个升序链表并遍历输出。 输入: 输入的每个案例中第一行包括1个整数:n(1<=n<=1000),接下来的一行包括n个整数。 输出: 可能有多组测试数据,对于每组数据, 将n个整数建立升序链表,之后遍历链表并输出。 样例输

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟)

【每日一题】LeetCode 2181.合并零之间的节点(链表、模拟) 题目描述 给定一个链表,链表中的每个节点代表一个整数。链表中的整数由 0 分隔开,表示不同的区间。链表的开始和结束节点的值都为 0。任务是将每两个相邻的 0 之间的所有节点合并成一个节点,新节点的值为原区间内所有节点值的和。合并后,需要移除所有的 0,并返回修改后的链表头节点。 思路分析 初始化:创建一个虚拟头节点

学习记录:js算法(二十八):删除排序链表中的重复元素、删除排序链表中的重复元素II

文章目录 删除排序链表中的重复元素我的思路解法一:循环解法二:递归 网上思路 删除排序链表中的重复元素 II我的思路网上思路 总结 删除排序链表中的重复元素 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。 图一 图二 示例 1:(图一)输入:head = [1,1,2]输出:[1,2]示例 2:(图

【数据结构与算法 | 灵神题单 | 删除链表篇】力扣3217, 82, 237

总结,删除链表节点问题使用到列表,哈希表,递归比较容易超时,我觉得使用计数排序比较稳,处理起来也不是很难。 1. 力扣3217:从链表中移除在数组中的节点 1.1 题目: 给你一个整数数组 nums 和一个链表的头节点 head。从链表中移除所有存在于 nums 中的节点后,返回修改后的链表的头节点。 示例 1: 输入: nums = [1,2,3], head = [1,2,3,

c++ 链表详细介绍

链表是数据结构的一种,由节点组成,每个节点包含数据和指向下一个节点的指针。链表在C++中的实现可以是单链表、双链表或循环链表。以下是链表的详细介绍: 1. 单链表 结构: 节点(Node):每个节点包含数据和一个指针(next),指向链表中的下一个节点。 示例结构: struct Node {int data;Node* next;Node(int d) : data(d), next(

带头结点的线性链表的基本操作

持续了好久,终于有了这篇博客,链表的操作需要借助图像模型进行反复学习,这里尽可能的整理并记录下自己的思考,以备后面复习,和大家分享。需要说明的是,我们从实际应用角度出发重新定义了线性表。 一. 定义 从上一篇文章可以看到,由于链表在空间的合理利用上和插入、删除时不需要移动等优点,因此在很多场合下,它是线性表的首选存储结构。然而,它也存在某些实现的缺点,如求线性表的长度时不如顺序存储结构的

数据结构基础(栈,队列,数组,链表,树)

栈:后进先出,先进后出 队列:先进先出,后进后出 数组:查询速度快,通过地址值和索引定位,查询任意数据消耗时长相同,在内存中是连续存储的,删除效率低,要将原始数据删除,然后后面的数据前移,添加效率低,添加索引位置的元素,剩下的都需要向前后移动 链表:节点的存储位置(地址)里面存储本身的数据值,和下一个节点的地址值,链表中的节点是独立对象,在内存中是不连续的。查询速度慢,无论查询哪个数据都要从