数据结构初步了解
链表是数据结构的一部分,所以我想先理清数据结构的要点:
它们说:程序=算法+数据结构,在计算机中,把所有可以输入到计算机中能被计算机进行处理的符号的总称叫做数据。
数据结构包括两部分,有数据的集合和数据之间关系的集合。
Data_structure={D,S}; 在这个式子中,D是数据元素的有限集,S是D之间的关系(数据之间关系的有限集);在数据结构中,描述数据之间的逻辑关系,称为逻辑结构。数据结构在计算机的表示称为数据的物理结构。
线性表
假如要保存26个英文字母{A,B,C,D,E,F,G....Z},既要保存26个英文字母,也要保存字母之间的先后顺序,这个时候就用到了线性表;
线性表物理结构的实现:
(1)顺序结构:在这里我们用数组实现。线性表的顺序表示的是用一组连续的存储单元依次存储线性表中的数据元素。只要用一组连续的空间去存放变量,这样的话,数据被保存了,数据之间的关系也被保存了。即在这里的物理地址的关系就是逻辑关系;
(2)链式结构:如果我们不想按照连续的地址去存放,就比如我和小伙伴们去看电影,售货员不一定每次分配到连着的位子,这个时候我们我们小伙伴之间互相记住座位也可以找到彼此,这种情况就像我们的链表。 保存每个数据时,不需要用连续的地址空间,数据元素的存储单元是不连续的,但是在保存每个数据元素的同时,额外开辟一块空间去保存下一个或者上一个元素的地址;
为什么要学习链表?
有这样一个问题:我想去保存一组数据,以及它们之间的前后的关系位置,用C语言中的数组即可实现,但是假设,数据有100个,1000个,或者更多呢?用数组去存储的话,需要正好有这么一块这么大的空间去存储,这样一来,平白又加了些难度.......
你看,同样一个功能,我用C语言也能实现,但是受限制较多。假如有这样一个东西,既可以保存各数据,但是又不需要是连续的地址,在一块空间中,我保存数据时,在空间位置,把它们表示出来,但是它们是同一组数据。这样一来,我极大的节省了空间。这就是链表,更有效率的解决问题。当然,我的认知现在仅限于我学完链表之后,后面会随着学习,我将会了解更多更优的算法。
当我学完C语言去学链表的时候,我总是在想,C语言和链表有什么关系呢?而之前用C语言通过指针,通过for循环,通过结构体做出来的程序,实现的功能等等,好是好,但是不够好,主要体现在时间效率以及占用程序空间的程度上,简单来说,新内容的引入主要是为了更好的解决问题。
简单来说,我要保存一组数据,和一组数据之间的关系,用C语言我可以实现,看下面代码:
//首先用C定义一组结构体
typedef struct node
{
Mytype data;
struct node* next;
}Node;
//用c语言去实现
int main()
{
Node a,b,c,d,e;
//这里的abcde都是定义的结点,用点取其中的数据。不是指针
a.data=1;
a.next=&b;
b.data=2;
b.next=&c;
c.data=3;
c.next=&d;
d.data=4;
d.next=&e;
e.data=5;
e.next=NULL;
//打印方法1
printf("a:%d b:%d c:%d d:%d e:%d\n",a.data,b.data,c.data,d.data,e.data);
//打印方法2:利用指针打印
Node* p=&a;
while(p)
{
printf("%d ",p->data);
p=p->next;
}
printf("\n");
return 0;
}
我的编译器用的是DevC++,这里是输出结果:
这个意思我理解是:如果我要从湖南去新疆,用C语言好像是走路出发,而用链表就好像是坐马车去。当然,我们以后会学到更好的方法,争取坐汽车、坐火车、坐高铁、坐飞机去想去的地方,持续进步。
一、不带头结点的单向链表
通过小伙伴的讲解,我理解到链表像是一节一节火车,通过中间的链子相连。那么假设我们从无到有要构建这样一列火车,该怎么去构建呢?
像小朋友搭积木一样,一点一点去完成我们的作品。
从无到有:新进来一节车厢,也就是第一节车厢
从少到多:再新进来一节车厢,要么持续接在上一节车厢的后面,环环相扣,构成一列火车;要么持续接在上一节车厢的前面,环环相扣,构成一列火车。加在上一节车厢后面的叫尾插法;加在上一节车厢前面,我们叫头插法。
在实际创建链表中,每一个车厢我们叫数据结点(存放每个数据和该数据下一个位置的地址的结点),单链表是指由一个或多个含有指针成员变量的结构体,通过其指针成员的地址形成的一种逻辑上的链式结构。每一个结构体叫做这个链表的结点。
这里注意链表的两个要点:
- 只要知道首结点的地址,就可以访问其他结点
- 保存新的结点,需要把新的结点位置加到上一个结点的指针指向中
现在开始创建一个单链表:
第一步
用typedef定义结点类型,其中要有一个保存数据的变量,要有一个保存地址的指针变量(指针类型是和我们定义本结点一样的结构体类型);
在这里,我们先将int类型的数据连接成链表,但是后面,我们可能会需要将单精度型、浮点型各种类型的数据也连成链表,而这些创建链表的过程是一致的。为了使我们创建的链表具有通用性,这里我们还要多定义一步,将int定义成Mytype,后面如果需要创建其他类型数据的结点时,只需要将int替换成想要创建的数据类型就可以啦。
这里解释一下typedef的定义:typedef是一个关键字,作用是为一个已经存在的类型(基本数据类型或自定义数据类型和数组指针定义的类型)起一个别名。这里要和#define进行区分,typedef在编译的时候有类型的检查功能,#define只是简单的文本替换。
在这里还需要额外创建两个结点指针,一个指向链表中第一个数据结点为首结点,另一个指向链表中最后一个数据结点,为尾结点。创建首结点和尾结点的作用是能够通过它们的指向去选择对整个链表进行尾插还是头插。想一下,如果没有这两个结点的话,每次新结点进来只能接在上一个结点的后面,每次插入一个结点都是新结点,不能进行头插,也不能知道第一个结点的位置。有了这两个结点之后,我就可以记录下来我整个链表是从从哪里开始的,打印的时候会方便很多,也可以知道最后一个结点的位置,下一个数据结点插入的时候,就可以进行指向。
typedef int Mytype;
typedef struct node
{
Mytype data;
struct node* next;
}Node;
Node * head = NULL; //指向链表首结点的指针
Node * tail = NULL; //指向链表尾结点的指针
第二步
每获取一个数据,创建一个新的结点
Mytype ddd; //定义结点中的数据存放在变量ddd中
scanf("%d",&ddd);
Node *pnew = (Node *)malloc(sizeof(Node));
//通过动态开辟空间创建一个结构体所需要的空间,并且将该空间的首地址存放在结点指针p中
第三步
把数据写入到创建的数据结点中,并对数据结点的其他变量进行初始化
pnew->data = ddd; //这个新开辟的空间中data保存数据
pnew->next = NULL; //存放地址的指针先指向NULL,后面再根据要求进行指向
第四步
把新的结点加入到链表中,一般分为两个阶段:从无到有+从少到多
//从无到有(最开始创建:没有一个数据结点时)
if(head == NULL)
{
//pnew是一个指向新结点的指针
head = pnew; //记录头的结点head指向新结点
tail = pnew; //记录尾的结点tail指向新结点
}
//从少到多 (创建了一个结点之后)开始排链表:有尾插法和头插法两种
//尾插法
else
{
tail->next = pnew; //尾结点和新数据结点相连
tail = pnew; //尾结点变成新的数据结点
}
//头插法
else
{
pnew->next=head; //新数据结点和首结点相连
head=pnew; //首结点变成新的数据结点
}
//注意:在实际代码运行中,头插和尾插选择一种方法连成链子即可
单链表练习题一
问题
假如需要你在创建的链表中查找值为x的结点,并在x的前面添加a;如果没有找到的话,就把a添加在链表的最后面,现在怎么样去做呢?
第一步
查看题目意思,无论有没有找到a,都要把a加到链表中去。所以这里必须要开辟空间创建一个存放数据为a的数据结点
Node * pnew = (Node *)malloc(sizeof(Node));
pnew->data = a;
pnew->next = NULL;
第二步
从链表中一个一个去查找x,确定a要插入的位置;在这里,有两点需要去考虑:
- 首先定义一个遍历查找数据的结点指针p
- 我们的目的是在找到的x位置的前面插入a,所以这里必须知道x的前一个位置是谁,所以在这里我们还需要定义一个结点pre指针去保存x的前一个位置的地址。因为是要遍历寻找x,所以事先不知道x的位置,所以要使用确定性去找不确定性,我们的pre指针只能随着p去查找(pre随着p变化,每次记录p的前一个位置),当p找到x的时候,pre正好是p的前一个位置,这样一来,在pre的后面位置,p的前面位置插入数值a就方便多了
Node *p = head;
Node *pre = NULL;
//在这里我们需要写一个循环,让p一直去寻找
while(p) //每次循环后,p会一直等于它指向的位置, 因为链表的最后一个位置的指向为空,
{ //所以当while里面的条件为空时,正好跳出循环,预定遍历的次数正好是链表中结点个数
if(p->data == x)
{
break;//找到了
}
//注意赋值符号左右^_^
pre = p; //把p的位置给pre指针
p = p->next; //把p指向的位置给它自己
}
第三步
遍历完成后添加结点,我发现,有两种情况:
- 找到x(说明退出循环的时候,遍历指针p都不为空):根据x的位置又可以分为两种情况,当x是第一个结点时,整个链表的首结点需要调整;当x在链表的中间位置或者咋链表最后一个位置时,x都需要接在pre的后面,p的前面;
- 没找到x(要么这是一个空链表,要么找完整个链表,直到找完最后一个值也没找到,遍历指针p一定为空):只能接在整个链表的最后面;
if(p != NULL) //在链表的里面找到了这个值为x的结点
{
//链表的第一个值就找到了x
if(p == head)
{
//头插法
pnew->next = head; //数据为a的数据结点和首结点相连
head = pnew; //同时首结点变成新的数据结点
}
//在链表的中间或者链表的最后一个位置找到x
else
{
pre->next = pnew; //记录x前一个位置的pre和新的存放a的数据结点相连
pnew->next = p; //同时存放a的数据结点pnew和找到x的结点p的位置相连
}
}
//找了整个链表都没找到x时
else
{
if(head == NULL) //要么这是一个空链表
{
head = pnew; //要插入的a变成链表中唯一的值,首结点也变成插入a的数据结点
}
else //要么这个链表中没有x
{
//尾插法 就在链表的最后一个结点(尾结点)的后面添加数据为a的数据结点pnew
pre->next = pnew;
//不用给pnew的指向置空,因为刚开始初始化的时候pnew的指向就为空
}
}
整体代码如下:
/*
函数名:add_a_node,在链表中查找值为x的结点,在x的前面添加一个值为a的结点
如果没有找到的话,就把a添加在链表的最后面
参数列表:
@head:要查找的链表
@x:要查找结点的值
@a:要添加的结点的值
*/
Node * add_a_node(Node *head,ElemType x,ElemType a)
{
//1.创建一个新的结点pnew,把数据a写入到pnew中
Node * pnew = (Node *)malloc(sizeof(Node));
pnew->data = a;
pnew->next = NULL;
//2.遍历查找结点数据为x的结点
Node *p = head;//遍历的那个指针
Node *pre = NULL;//遍历的时候每次保存p的前一个结点
while(p)//如果最后p==NULL的时候还没找到
{
if(p->data == x)
{
break;//找到之后退出break
}
pre = p;
p = p->next;
}
//3.循环退出后,考虑a的插入位置
if(p != NULL)//在链表的里面找到了这个值为x的结点
{
if(p == head)
{
//头插法
pnew->next = head;
head = pnew;
}
else
{
//中间或最后位置
pre->next = pnew;
pnew->next = p;
}
}
else //没有找到,尾插法还是有两种情况,要么就是没找到,要么就是一个空链表
{
if(head == NULL)//空链表
{
head = pnew;
}
else
{
//尾插法 当p指向了NULL的时候,pre刚好就在链表的最后一个位置上(尾结点)
pre->next = pnew;
}
}
//返回首结点
return head;
}
单链表练习题二
问题
在链表中找值为x的结点,将其删除,如果有多个值为x的结点的话,则只删除第一个结点x,其余不动,没找到就不删除,返回新链表即可
注意:删除结点不仅仅删除数值或者将结点中的指向为NULL这么简单,这是你动态开辟的空间,如果你不释放,它将永远存在于那里,而且你不指向,它永远不能被访问。这种情况在专业术语叫做内存泄漏,会一直占用内存资源,导致程序运行速度变慢。虽然我暂时不太懂它的重要性,但是及时free调malloc出来的空间是很重要的
第一步
创建一个结点指针p去遍历链表中每个数据结点,并且还需要创建一个结点指针pre去保存删除数据那个结点的前一个结点位置;
如果不定义这个指针pre的话,当p遍历时发现某个结点符合我们删除结点的条件,立马删除,就像一条链子找到中间要删除的点时,从那个点那一剪刀减掉,但是为了保持整个链子的完整性(题目要求只让你删掉链子中特定的一个数据结点),还需要将删除的数据结点前面和后面连接起来,后面可以通过p->next找到,但是前面的话(这是一个单向链表),只能通过定义一个结点指针指向遍历指针p的前一个位置去完成;
Node *p = head; //从链表第一个数据结点首结点开始
Node *pre = NULL;
第二步
指针p从一个结点去遍历,直到最后一个结点--->所以,我们这里用到while语句;
当遍历结点p指向位置中数据为x时,去进行删除;
删除时分为:头删、尾删和中间删(中间删和尾删是一种情况,都是将pre的指向连到p的指向,再把p的指向断掉)
while(p)
{
if(p->data == x) //找到要删的结点位置
{
//分情况讨论:尾删和中间尾巴删
if(p == head)//要删除的是首结点
{
head = head->next; //先把首结点移到下一个
p->next = NULL; //工程规范,一个指针释放之前先指向NULL 断开这个结点
free(p);
}
else//删除中间和尾巴的结点
{
pre->next = p->next;//把要删除结点的前一个结点位置的指向要删除结点位置指向
p->next = NULL;//断开这个结点
free(p); //释放p所占用的空间
}
break; //只要一找到就跳出循环
}
else //如果没找到就继续往下找
{
pre = p;
p = p->next;
}
}
单链表练习题三
问题
在链表中查找值为x的结点,把所有值为x的结点修改成a
参考代码如下
/* 函数名: replace_data
参数列表:
@head:要修改值的链表的首结点地址
@x:修改之前的值
@a:修改之后的值
返回值:
修改过后的链表
*/
Node *replace_data(Node *head,Mytype x,Mytype a)
{
Node *p = head;
while(p)
{
if(p->data == x) //找到了
{
p->data = a;
}
p = p->next;
}
return head;
}
单链表练习题四
问题
写一个函数,销毁整个链表
参考代码如下
/* 函数名: destroy_list 用来销毁整个链表
函数参数:传进来一个单向链表
返回值:无 */
void destroy_list(Node *head)
{
//创建一个遍历销毁结点p
Node *p = head;
while(p)
{
//从头开始删除 每次删完第一个之后将首结点变成之前第一个结点的下一个
head = head->next;
p->next = NULL; //p还有指向它的下一个,规定要在释放某个空间销毁某个结点之前将它的指向置为空
free(p); //释放p指向的空间
p = head; //p变成新的首结点
}
}
我在学习的时候我们老师讲过,在链表中,经常会出现段错误,这个时候,很多时候是因为访问越界,也就是说,你的代码一直访问到NULL的时候还在访问,所以很多时候要注意这种情况^_^
单链表练习题五
问题
在链表中找值为x的数据结点,将其进行删除,如果有多个值为x的数据结点的话,就删除多个数据结点,没找到就不删除,最后返回新链表
思路一
跟练习题二差不多,上面是删除一个,只要找到一个符合条件的值就退出循环,这里是删除多个,所以if的执行语句中不用break,分析找到的情况和没找到的执行语句,直到整个链表的最后一个值为止;
Node* delete_all_node(Node* head, Mytype x)
{
//1.找值为x的结点
Node* p = head;//遍历指针
Node* pre = NULL;//保存p的前一个结点
while (p)
{
if (p->data == x) //找到了
{
//2.删除值为x的结点,分情况讨论
if (p == head)//要删除的是首结点
{
head = head->next;//先把首结点移到下一个
p->next = NULL;//工程规范,一个指针释放之前先指向NULL 断开这个结点
free(p);
p = head;
}
else//删除中间和尾巴的结点
{
pre->next = p->next;
p->next = NULL;//断开这个结点
free(p);
p = pre->next;
}
}
else//没找到就继续往下找
{
pre=p;
p = p->next;
}
}
//返回头结点就OK
return head;
}
主函数
int main()
{
Node* t1 = create_lsit();
Node* t2= delete_all_node(t1, 5);
print_list(t2);
return 0;
}
代码执行结果
思路二
创建三个数据结点指针,一个指针p用来遍历寻找;一个指针pre用来保存找到的数据结点的上一个位置,这两个结点指针的作用相信已经不用说了;还有一个结点指针ps,用来保存找到的数据结点的下一个位置,当链表中有多个重复数据的话,这样一来,每次在删除完一个数据之后,从删除数据的下一个继续寻找,继续删除。在我看来,这个方法更灵活一点,而这种思维正是一个程序员该做的,要让你的代码像人一样去思考,在以后得学习中,我希望我的代码能努力朝着这个方向走!
/*
函数名:delete_all_node
参数列表:
@head:传一个链表的首结点地址
@x:要删除的结点数据
返回值:
删除完之后的链表的首结点地址
*/
Node * delete_all_node(Node *head,ElemType x)
{
//定义一个遍历的指针
Node *p = head;
//定义一个指针保存p的前一个
Node *pre = NULL;
//定义一个指针,保存要删除结点的下一个位置,然后每删完一个结点
//下次从ps这个位置开始往后继续遍历 ,同是遍历结点p变成ps的位置
Node *ps = head;
while(1)//一直循环,只有全部删完才能退出
{
while(p)//当p不等于NULL,说明还在找
{
if(p->data == x)
{
break;
}
pre = p; //保存前面位置
p = p->next; //p一直往后寻找
}
//这里注意先后顺序:
//1、退出寻找循环时,先判断是否为空;如果为空,先退出while
if(p == NULL) //对应while(1) 如果没有要删除的结点时退出循环
{
break;
}
//2、保存删除结点的后面位置;如果先删除就找不到它后面的位置了
ps = p->next;//保存下一次遍历的开始位置
//删除值为x的结点
if(p == head)
{
head = head->next;
p->next = NULL;
free(p);
}
else
{
pre->next = p->next;
p->next = NULL;
free(p);
}
//3、每次p变成新的起始位置(上次删除结点的后面),再次循环时也从新位置往下寻找
p = ps;
}
return head; //返回首结点
}
代码执行结果
单链表练习题六
问题
创建一个升序的链表,也就是在链表的插入过程中排序
示例:输入1 9 8 6 7 5 4 输出1 4 5 6 7 8 9
思路
先找数据要插入的位置,再执行插入数据的操作
代码实现
Node* sheng_pai()
{
//开始准备工作
Mytype d;
//这是一个单链表,所以需要两个结点,一个指向链表的头,一个指向链表的尾,以便于去创建这个链表
Node* head = NULL;//记得初始化
Node* tail = NULL;
//需要定义一个结束的标志,每次规定直到输入0才结束升序链表创建
while (1)
{
scanf_s("%d", &d);
if (d == 0)
{
break;
}
//每输入一个数据的时候,开辟一块空间创建一个新的数据结点
Node* pnew = (Node*)malloc(sizeof(Node));
pnew->data = d;
pnew->next = NULL;
//注意看:一个普通链表和升序链表的创建这一步有区别
//把新的结点加入到链表中
if (head == NULL)//从无到有
{
head = pnew;
tail = pnew;
}
else//从少到多
{
//定义一个遍历的结点,用以和新添加进的数据比较
Node* p = head;
Node* pre = NULL;
//链表中有多少数据,这里p需要遍历多少遍,所以这里的while循环的条件是p,而且p是要从头往后要移动的
while (p)
{
//这个while的功能只是帮忙找插入位置
//直到遍历的值大于新插入的值就退出,否则就一直往后找
if (p->data > pnew->data)
{
break;
}
//记录p和pre的位置
pre = p;
p = p->next;
}
//找到插入位置之后执行插入操作
//分情况:
//1、如果第一个值比新插入的值大,就把新值插入到最前面
if (p == head)
{
pnew->next = head;
head = pnew;
}
else//尾插和中间插一个情况
{
pre->next = pnew;
pnew->next = p;
}
}
}
return head;
}
代码运行结果
单链表练习题七
问题
逆置一个单链表(不能申请额外的链表空间保存原来的结点数值),要求最后返回一个新链表;
要求:不能开辟新的空间
示例:输入1 2 3 4 5 6 输出6 5 4 3 2 1
思路
因为单向链表只能是从一个数值指向下一个数值,它的方向是单一的(从头指向尾),所以这里一定考虑到不能从尾摘下结点去创造链表(因为摘下位尾结点之后就没办法记录前面结点的位置);所以这里的思路一定是把旧链表的首结点从前往后摘下来,再利用头插法从后往前一个一个连成新链表
代码实现
/*
函数名:Zhuan_list
参数列表:传进来一个需要转置的链表首结点地址 head1
返回值:返回转置操作完成之后的链表首结点地址 head2
*/
Node* Zhuan_list(Node* head1)
{
//这里需要定义一个结点指针去遍历,从原链表的第一个结点找到最后一个结点(俗称一个一个摘下来)
Node* p=head1;
//这里还需要定义一个新链表的首结点,去帮助连成新的链表
Node* head2=NULL;
while(p)
{
//从头到尾一个一个摘下旧链表的结点
//用head记住每次摘结点摘到哪里了
head1=head1->next;
p->next=NULL;
//把摘下来的结点从尾到头一个个连成新链表 借助新的首结点去记住每次连成新链表连到哪了
if(head2==NULL)
{
head2=p;
}
else
{
p->next=head2;
head2=p;
}
//体会一下这里p的作用
//我感觉在这里p像一个辛勤的劳作者,每次从原链表那里背一个值过来,到新链表中去,等一切安排好之后,再去原链表那里去,如此往复
//最后他尘埃落定,不求一丝功名,自己默默指向NULL
//被感动到了呜呜0o0
p=head1;
}
//最后要返回逆转后新链表的首结点地址
return head2;
}
代码运行结果
单链表练习题八
问题
输出一个单链表的倒数第k个结点,k由用户输入
思路
定义两个指针,假设为p和q,让p先走k次,然后两个指针再同时走,然后直到先走的指针p为空的时候,后面走的指针q所指的结点即为倒数第k个结点
代码实现
/*
函数名:Di_k
参数列表:传进去一个单链表的首结点的地址 @head
返回值:返回倒数第k个结点的数据 (数据类型为我指定的类型Mytype)
*/
Mytype Di_k(Node* head)
{
int k;
printf("请问你要求倒数第几个值?/n");
scanf_s("%d",&k);
Node* p=head;
Node* q=head;
//这里需要考虑到输入的链表可能是一个空链表 如果是空链表,那么它无法指向
while(k--&&p)
{
p=p->next;
}
//这里还需要考虑到:如果输入的k值大于链表的节点个数值,p和q指向NULL,也无法访问数据,正确的k值跳出循环之后值应该是-1
// 假如要求倒数第三个结点的数值,即k=3
// k-- k
//第一次 3 2
//第二次 2 1
//第三次 1 0 注意看:循环里面的条件是k-- 第三次这里k--还不为0,继续
//第四次 0 -1 此时此刻,k--为0,跳出循环,k的值为-1
if(k!=-1)
{
printf("您输入的k值不符合链表长度规则!\n");
return -1; //不正确的时候返回值为-1
}
while(p)
{
p=p->next;
q=q->next;
}
return q->data;
}
代码执行结果
单链表练习题九
问题
将一个链表中所有的负数放到最前面,要求不能开辟额外的链表空间,不能通过排序完成
思路
定义两个指针,假设为p1、p2,一个指针p1从头到尾找非负数,另一个指针p2从头到尾找非数,在寻找过程中如果两个都找到就交换两个结点中的数值;
而且一定是先寻找非负数,这样一来,每当找到一个非负数,就与后面找到的负数进行交换数据;如果先找负数,每找到一个负数,就与后面的非负数交换,就会变成所有的非负数在链表前面了;
直到两个指针中有一个从头到尾都找完了,它们的任务也就完成了,只需要返回链表的首结点地址就可以了;
但这个方法有个缺陷,就是只能寻找两种类型的数据,这道题的数据可以分为负数和非负数两种;
代码实现
/*
函数名:Fu_qian
参数列表:传进来一个创建好的链表 @head
返回值:返回将所有负数放在最前面的链表的首结点的地址
*/
Node* Fu_qian(Node* head)
{
Node* p1=head;
Node* p2=head;
while(1) //大循环:一直寻找,持续交换
{
//p1找非负数,有它自己寻找的位置,找到就停下来,跳出它自己的循环
while(p1)
{
if(p1->data>=0)
{
break;
}
p1=p1->next;
}
//所以这个方法只适用于两种情况的选择:一种是负数,一种是非负数
//判断一下p1是不是已经找完了,找完的话
if(p1==NULL)
{
return head;
}
while(p2->data<0)
{
break;
}
if(p2==NULL)
{
return head;
}
Mytype temp;
temp=p1->data;
p1->data=p2->data;
p2->data=temp;
}
return head;
}
二、带头结点的链表
1、头结点
(1)什么是头结点?
我的理解是相当于一个部门的管理层,组员中的小组长。在一组数据里面,头结点的作用是记下数据个数,第一个数据的位置和最后一个数据的位置
ps:这里需要注意一下头结点和首节点的区别,头结点属于管理层了,是另一种类型的结构体;而首结点(这里我们可以叫其他名字,第一个数据结点等等)是数据元素中第一个结点,还是属于数据层,属于员工层。我的管理层头结点可以任命你员工层中第一个数据元素为首结点,或者任命新插入的元素为首节点(头插法),这里是由头结点去记录,去指向;
(2)头结点的定义
//数据结点类型不变
typedef struct node
{
Mytype data;
struct node* next;
}Node;
//这里多定义一个头结点类型
typedef struct tou
{
int num;
Node* first;
Node* last;
}Tou;
(3)带头结点单链表的创建
利用尾插法,根据用户的输入顺序创建各个数据结点;
在这里多了一个头结点的指向,需要将first指向整个链表的第一个结点,last指向整个链表的最后一个结点,num记录整个链表中有多少个结点个数
/*
函数名:create_tou_list
参数列表:无
返回值:链表的头结点的地址
*/
Tou* create_tou_list()
{
//创建一个头结点,并初始化
Tou* tou=(Tou*)malloc(sizeof(Tou));
tou->first=NULL;
tou->last=NULL;
tou->num=0;
//创建数据结点
Mytype d;
//持续获取数据---用while
while(1)
{
scanf_s("%d",&d);
//约定直到输入0才输入结束
if(d==0)
{
break;
}
//以创建一个数据结点为例
Node* pnew=(Node*)malloc(sizeof(Node));
pnew->next = NULL;
pnew->data = d;
//把创建好的数据结点添加到链表中
if (head->first == NULL)
{
head->first = pnew;
head->last = pnew;
}
else
{
head->last->next = pnew;
head->last = pnew;
}
head->num++;
}
return head;
}
(4)带头结点的单链表的打印
/*
函数名:print_toulist
参数列表:传进来一个带头结点的单链表 @head
返回值:无
*/
void print_toulist(Tou* head)
{
if (head == NULL)
{
printf("不是吧,这是个空链表哇!\n");
}
//遍历打印结点
Node* p = head->first;
while (p)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
注意它俩的区别:定义的时候,typedef后面起完别名之后要加分号,define语句写完之后不需要加分号
带头结点的单向链表练习题一
问题
删除整个链表中所有的值为x的数据结点
思路
前面在不带头结点的单向链表练习题中有删除链表中某一个特定数值,在这个问题中,有很多相似之处;
代码实现
算法一
利用两个指针,消除break,每次找到要删除的数据之后要对p重新进行指向;如果没有多加黑色加粗的那两行代码,每次找到删除的结点之后,先进入if(p->data==x)这个语句,再进入相应的删除的位置的操作语句,就不会进入后面如果没找到结点else的那部分语句,然后再进入循环,但是这个时候的p没有被重新指向,已经被释放掉了,所以相应的,while循环中p一直出不来,也无法打印了;
所以这里一定要记住,看清if和else的执行条件,这是分支语句;一定要对p及时赋值,否则已经被释放的p就无法再进入循环,程序也无法跑成功
Node* delete_all_node(Node* head, Mytype x)
{
//1.找值为x的结点
Node* p = head;//遍历指针
Node* pre = NULL;//保存p的前一个结点while (p)
{
if (p->data == x) //找到了
{
//2.删除值为x的结点,分情况讨论
if (p == head)//要删除的是首结点
{
head = head->next;//先把首结点移到下一个
p->next = NULL;//工程规范,一个指针释放之前先指向NULL 断开这个结点
free(p);
p = head;
}
else//删除中间和尾巴的结点
{
pre->next = p->next;
p->next = NULL;//断开这个结点
free(p);
p = pre->next;
}
}
else//没找到就继续往下找
{
pre=p;
p = p->next;
}
}
//返回头结点就OK
return head;
}
主函数
int main()
{
Tou* t1 = create_tou_list();
Tou* t2 = delete_all_node(t1, 5);
print_toulist(t2);
return 0;
}
代码执行结果
算法二
定义三个结点指针,一个结点指针p进行遍历查找,另一个pr进行保存每次遍历查找指针的前一个结点地址,还有一个ps保存每次要删除结点的后面一个结点的位置,等删除结点之后再从保存的ps的位置继续进行遍历
Tou* delete_all_node(Tou* head, Mytype x)
{
//1.找值为x的结点
Node* p = head->first;//遍历指针
Node* pr = NULL;//保存p的前一个结点
Node* ps = head->first;//保存删除结点的下一个位置,以便继续遍历while (1)
{
p = ps;
while (p)//找删除位置
{
if (p->data == x)
{
break;
}
pr = p;
p = p->next;
}
//没找到
if (p == NULL)
{
return head;
}
else//找到了
{
//每次找到首先一定要记录删除结点的下面一个结点的位置,为了后面继续遍历
ps = p->next;
//同时头结点里面的数目减减
head->num--;
//判断每次要删除位置:分情况
if (p == head->first)
{
//假如要删除的是首结点
head->first = head->first->next;
p->next = NULL;
free(p);//如果删除了一个 链表中就没有数据结点了
if (head->num == 0)
{
head->last = NULL;
}
}
else if (p == head->last)
{
head->last = pr;
pr->next = NULL;
free(p);
//如果删除了最后一个结点 整个链表中就没有结点的话
if (head->num == 0)
{
head->last = NULL;
}
}
else
{
pr->next = p->next;
p->next = NULL;
free(p);
}
}
}
return head;
}
代码执行结果
图形辅助理解
带头结点的单向链表练习题二
问题
找到链表最中间那个结点的数值
思路
1、利用头结点中保存的结点个数,打印数据结点中的值
2、定义两个指针,一个一次走一步,一个一次走两步;用数学计算,会发现每次快指针每次走的结点个数是慢指针走的结点个数的2倍;
代码实现
//算法一:知道头结点的个数num,遍历指针遍历一半就知道了
/*
函数名:middle_node
参数列表:传进去一个带头结点的单链表@head
返回值:返回中间结点的值
*/
Mytype middle_node(Tou* head)
{
Node* p = head->first;
int num2 = (head->num) / 2;
while (num2)
{
p = p->next;
num2--;
}
return p->data;
}//算法二:快慢指针
//一个指针快,一次走两步;一个指针慢,一次走一步;
// 这样的话,因为每次快指针走的步数是慢指针的二倍;
// 所以当快指针走完的时候,慢指针走的步数正好是快指针步数的一半
//而快指针走完整个链表的时候正好走完,后面没有结点,无法再继续走了
/*
函数名:Kuai_man
参数列表:传进来一个带头结点的单链表
返回值:返回中间结点的值
*/
Mytype Kuai_man(Tou* head)
{
if (head == NULL)
{
printf("不是吧亲,这是个空链表!\n");
}
Node* p1 = head->first;//慢指针
Node* p2 = head->first;//快指针
while (p2)
{
p2 = p2->next;
//每走一步要判断一下,它是不是已经为空
if (p2 == NULL)
{
break;//没有这一步一定会造成越界访问,出现段错误
}
p2 = p2->next;
p1 = p1->next;
}
return p1->data;
}
带头结点的单向链表练习题三
问题
写一个函数,销毁这个带头结点的链表
思路
一个一个释放这个结点,最后再释放头结点
代码实现
/*
函数名:destroy_list
参数列表:传进去一个带头结点的单链表
返回值:无
*/
void destroy_list(Tou* head)
{
//遍历结点
Node* p = head->first;
while (p)
{
head->first = head->first->next;
p->next = NULL;
free(p);
//在这里又缺了一步很重要的一部:一定不能不管它的何去何从
p = head->first;
}
head->last = NULL;
//还要记得把头结点里面的数值个数置0,否则里面的数值一直是不变的
head->num = 0;
free(head);
}
带头结点的单向链表练习题四
问题
输入两个链表a和b,求这两个链表的交集(假设为c)
思路
创建一个链表c存放交集数据,在链表a和链表b中对每个数据一个个进行遍历,所以这里需要两个遍历指针,假设定为p1和p2;如果其中两个数据相等,就将结点的数据放进新链表中,
代码实现
Tou* Jiao_lian(Tou* a, Tou* b)
{
//1、创建新链表存放交集数据
Tou* head = (Tou*)malloc(sizeof(Tou));
head->first = NULL;
head->last = NULL;
head->num = 0;
//2、新链表中的新的数据结点定义
Node* pnew = NULL;
//3、定义两个链表中的遍历结点
Node* p1 = a->first;
Node* p2 = b->first;
if (p1 == NULL || p2 == NULL)
{
printf("有空集,它们没有交集哦!\n");
return NULL;//什么都不返回
}
//思路是:一个链表中一个数据定着,一个个去遍历另一个链表中的每一个数据
// 如果数据相等,就把第一个链表中的数据保存在新的数据结点中
while (p1)
{
p2 = b->first;
while (p2)
{
if (p1->data == p2->data)
{
pnew = (Node*)malloc(sizeof(Node));
pnew->data = p1->data;
pnew->next = NULL;if (head->first == NULL)
{
head->first = pnew;
head->last = pnew;
}
else
{
head->last->next = pnew;
head->last = pnew;
}
head->num++;
}
p2 = p2->next;
}
p1 = p1->next;
}
return head;
}
代码执行结果
带头结点的单向链表练习题五
问题
将两个链表合成一个链表
思路
将链表一不变,将链表二的从尾部开始一个一个结点断掉,再通过尾插法一个一个连到链表一当中
代码实现
/*
函数名:Combine_list
参数列表:@h1 一个链表的头结点的地址
@h2 另一个链表的头结点的地址
返回值:返回合并之后的链表的头结点的地址
*/
Tou* Combine_list(Tou* h1, Tou* h2)
{
if (h1==NULL)
{
return h2;
}
else if (h2 == NULL)
{
return h1;
}
//将h2合并到h1当中去
Node* p = h2->first;
int ci = h2->num;
while (ci--)
{//不用开辟新的空间,只需要把h2链表的每一个数据结点从第一个开始一个一个摘下来接到h1链表的后面,并使新接的结点h1新的末尾结点
h1->last->next = p;
h1->last = p;
p = p->next;
}
return h1;
}
主函数
int main()
{
Tou* t1 = create_tou_list();
Tou* t2 = create_tou_list();
Combine_list(t1, t2);
print_toulist(t1);
return 0;
}
代码执行结果
带头结点的单向链表练习题六
问题
创建一个升序的带头结点的单向链表
思路
跟创建一个不带头结点的单向链表一样,这里只是多了一个头结点;其实就是前面的创建一个链表和插入一个值使整个链表仍然有序这两个题结合起来;
因为要创建升序链表,所以比创建普通链表多了一步在每次输入数据之后创建数据结点的时候放的位置多了一步判断,要找到第一个比输入数据大的值,它在链表中的位置就是第一个比他大的值的前面;
总体也是分为两步:第一找插入位置,第二进行插入操作;
图形辅助理解
代码实现
Tou* Up_tou_list()
{
//创建一个头结点
Tou* h = (Tou*)malloc(sizeof(Tou));
h->first = NULL;
h->last = NULL;
h->num = 0;
//设置变量保存输入数据
Mytype d;
//设置结点指针保存每次新创建的数据结点
Node* pnew = NULL;
//记录插入位置的前一个位置
Node* pr = NULL;
//定义一个遍历指针
Node* p = NULL;
while (1)
{
//一直检测输入数值,直到输入0结束
scanf_s("%d", &d);
if (d == 0)
{
break;
}
//如果不等于0,每接受到输入一个数据,开辟一块空间
pnew = (Node*)malloc(sizeof(Node));
pnew->data = d;
pnew->next = NULL;
//把数据结点串进链表中
if (h->first == NULL) //从无到有
{
h->first = pnew;
h->last = pnew;
}
else //从少到多
{
p = h->first;
//查找插入位置
while(p)
{
if (p->data > pnew->data)
{
break;
}
pr = p;
p = p->next;
}
if (p == NULL)
{
//尾插
h->last->next = pnew;
h->last = pnew;
}
else if(p==h->first)
{
//头插(错误点)
pnew->next = h->first;
h->first = pnew;
}
else
{
//中间插
pr->next = pnew;
pnew->next = p;
}
h->num++;
}
}
return h;
}
主函数
int main()
{
Tou* t1 = Up_tou_list();print_toulist(t1);
return 0;
}
代码执行结果
带头结点的单向链表练习题七
问题
判断一个链表是否有环
思路
发现一个很神奇的东西,不知道哪位前者想出来的,太牛了;刚刚去实验了一下,各种情况尝试了一下,竟然都满足了,太神奇了;
定义两个指针,一个一次走一步,一个一次走两步,如果链表有环,那么它们一定会相遇,也就是里面的data一定有相等的一次;
代码实现
为了显示实验效果,我这里写了带头结点的带环链表的创建和打印;
//创建一个有环链表
/*
函数名:Create_huan_list
参数列表:无
返回值:环型链表的第一个结点地址
*/
Tou* Create_huan_list()
{
//创建一个头结点
Tou* head = (Tou*)malloc(sizeof(Tou));
head->first = NULL;
head->last = NULL;
head->num = 0;Node* pnew = NULL;
Mytype d;
while (1)
{
//约定输入0结束
scanf_s("%d", &d);
if (d == 0)
{
break;
}//创建数据结点
pnew = (Node*)malloc(sizeof(Node));
pnew->data = d;
pnew->next = NULL;//把数据结点串到链表中去
if (head->first == NULL)
{
head->first = pnew;
head->last = pnew;
}
else
{
head->last->next = pnew;
head->last = pnew;
}
head->num++;
}
head->last->next = head->first->next->next;
return head;
}
//带环链表的打印
/*
函数名:print_huan_list
参数列表:传进来一个环形链表的头结点@head
返回值:无
*/
void print_huan_list(Tou* head)
{
if (head == NULL || head->num == 0)
{
printf("空的就别打印了吧!\n");
return;
}
//遍历结点
Node* p = head->first;
//打印次数
int ci = 2*head->num;
printf("环形链表打印(2次):");
while (ci)
{
printf("%d ", p->data);
p = p->next;
ci--;
}
printf("\n");
}
//判断一个链表是否有环
/*
函数名:Huan_list
参数列表:传进来一个带头结点的单向链表 @head
返回值:无,直接判断->有环打印,没有直接打印
*/
void Huan_list(Tou* head)
{
Node* p1 = head->first;
Node* p2 = head->first;
//设置标志位:默认是不带环的flag=0 如果有环设置flag=1
int flag = 0;
while (p2)
{
p1 = p1->next;
p2 = p2->next;
p2 = p2->next;
if (p1 = p2)
{
flag = 1;
printf("您输入的链表是带环的!\n");
return;
}
}
printf("链表不带环!\n");
return;
}
代码执行结果
带头结点的单向链表练习题八
问题
判断一个链表是不是另一个链表的子序列
思路
将链表2的第一个数据和链表1的第一个数据比较,如果相等,两个链表都往下走,继续比较第二个值,直到第二个链表比较完(整个为空);
如果相等,标志位为1,打印链表2是链表1的子集;在这当中有一个不相等,就不再继续比较,重来,重来的时候,p1要从刚刚比较之后的地方继续进行,而p2一定要从头再进行比较;
将p1的下一个位置开始与整个p2再进行比较,在比较过程中,如果存在p1的一个位置开始p2往后比,p2为空,说明匹配成功,是子集,得退出,不需要再比了;
如果没有匹配成功,下一次比较一定是p1现在比较的下一个位置;
这里有个绝妙的点:这是我做上面求两个链表的交集那道题受到的启发,有两个需要遍历的时候,我想到了for循环嵌套;但是在这里写for的条件不太好写,所以用到了while;
假如要打印3行3列的每一个值,用for循环实现:
int a[3][3];
for(i=0;i<3;i++)
{
for(j=0;j<3;j++)
{
printf("%d",a[i][j]);
这里注意看:for循环外面的i=0时,进入里面的for循环
//当i=0,执行里面的for语句时,外面的for循环里面的i都是等于0的,直到i=0的for执行完毕,
//又会执行i=1、i=2的内循环语句
}
}
当用while的时候,以下面的代码为例,while里面的条件p1,只要在里面用到了p1,让p1移动,在后面是移动的,并不是固定不变的
代码实现
//判断链表2是不是链表1的子集
/*
函数名:list1_ziji_list2
参数列表:传进来两个链表的头结点 @head1 @head2
返回值:无,用标志位去判断(默认不是子集0),是不是子集打印出来就行
*/
void list1_ziji_list2(Tou* head1,Tou* head2)
{
if (head1 == NULL || head2 == NULL)
{
printf("不是吧姐妹,这里面有个空集!\n");
printf("这还能比较吗???\n");
return;
}
//设置两个遍历结点
Node* p1 = head1->first;
Node* p2 = head2->first;
int flag = 0;while (p1)
{
p2 = head2->first;
while (p2)
{
if (p1->data == p2->data)
{
p1 = p1->next;
p2 = p2->next;
}
else
{
break;
}
}
if (p2 == NULL)
{
flag = 1;
printf("链表2是链表1的子集!\n");
return;
}
p1 = p1->next;
}
printf("链表2不是链表1的子集!\n");
return;
}
主函数
#include <stdio.h>
#include <stdlib.h>
#include "Toulian.h"
int main()
{
Tou* t1 = create_tou_list();
Tou* t2 = create_tou_list();
list1_ziji_list2(t1, t2);
return 0;
}或者
int main()
{
Tou* t1 = create_tou_list();
//Tou* t2 = create_tou_list();
list1_ziji_list2(t1, NULL);
return 0;
}
代码执行结果
三、 双向链表
相比单向链表,这里的每个数据结点多了一个向前的指向,所以数据结点类型有所改变;因为头结点的first和last指向数据结点,所以这里的头结点也需要重新定义
因为学到这里的时候我们是跟着老师上课讲的一起动手敲的代码,整体架构基本相同;所以我把这段按照上课写的记录下来,相应的,数据结点和头结点的定义也有不同;在后面双向循环那里依旧按照我自己的定义形式;
数据形式定义:
typedef int ElemType;
数据结点的定义:
typedef struct binode
{
ElemType data;
struct binode* prev;
struct binode* next;
}biNode;
头结点的定义:
typedef struct bilist
{
biNode* first;
biNode* last;
int num;
}biList;
双向升序链表的创建
这里的方法是先创建一个空的头结点去记录这个双向升序链表,然后在主函数中根据每输入一个数据创建这个对应的数据结点,再写一个Insert函数,把每一个新创建的数据结点插入到链表中合适的位置,使成为一个双向升序链表;
头结点(空的双向链表)的创建:
typedef struct bilist
{
biNode* first;
biNode* last;
int num;
}biList;
插入函数Insert的创建
//将数据结点插入到双向链表当中去
/*
函数名:Insert
参数列表:传进去一个双向链表头结点的地址 @l
还有需要插入的数据结点地址 @p
返回值:无
*/
void Insert(biList* l,biNode* p)
{
if(l==NULL)
{
return ;
}
//要么是第一个要么不是第一个结点,是第一个结点的话直插入一次,返回,不需要执行其他操作
if(l->first==NULL)
{
l->first=p;
l->last=p;
l->num++;
return ;
}
//找插入位置
biNode* pk=l->first;
while(pk)
{
if(pk->data>p->data)
{
break;
}
pk=pk->next;
}
//插入
if(pk==l->first)
{
//头插
p->next=l->first;
l->first->prev=p;
l->first=p;
}
else if(pk==NULL)
{
//尾插
l->last->next=p;
p->prev=l->last;
l->last=p;
}
else
{
//中间插
pk->prev->next=p;
p->prev=pk->prev;
p->next=pk;
pk->prev=p;
}
l->num++;
}
图形辅助理解
双向链表的打印
//打印双向链表
/*
函数名:print_bilist
参数列表:传进来一个带头结点的双向链表的头结点的地址
返回值:无
*/
void print_bilist(biList* l)
{
if(l==NULL)
{
//空的话不返回
return ;
}
printf("--------------------------\n");
biNode* p1=l->first;
biNode* p2=l->last;
printf("正向:");
while(p1)
{
printf("%d ",p1->data);
p1=p1->next;
}
printf("\n");
printf("逆向:");
while(p2)
{
printf("%d ",p2->data);
p2=p2->prev;
}
printf("\n");
printf("链表中有%d个数!\n",l->num);
return ;
}
双向链表中一个结点的删除
//双向链表中一个结点的删除
/*
函数名:delete_a_node
参数列表:传进来一个要删除结点的带头结点的双向链表@l
还有要删除的数值 @d
返回值:返回删除一个数值完之后的链表的头结点地址
*/
biList* delete_a_node(biList* l,ElemType d)
{
if(l==NULL||l->first==NULL)
{
printf("这个链表有问题啊亲!\n");
return l;
}
//遍历结点
biNode* px=l->first;
while(px)
{
if(px->data==d)
{
break;
}
px=px->next;
}
//说明没找到
if(px==NULL)
{
printf("sorry,没有找到你要删除的数值!\n");
return l;
}
else
{
//找到了
if(px==l->first)
{
//头删
if(l->first==l->last)
{
//要考虑到这种情况!!!
//说明链表中只有一个结点
l->first=NULL;
l->last=NULL;
free(px);
}
else
{
l->first=l->first->next;
l->first->prev=NULL;
px->next=NULL;
free(px);
}
}
else if(px==l->last)
{
//尾删
l->last=l->last->prev;
l->last->next=NULL;
px->prev=NULL;
free(px);
}
else
{
//中间删
px->next->prev=px->prev;
px->prev->next=px->next;
px->prev=NULL;
px->next=NULL;
}
l->num--;
}
return l;
}
图形辅助理解
双向链表中删除所有值为x的结点
biList* delete_all_node(biList* l,ElemType d)
{
if(l==NULL||l->first==NULL)
{
printf("这个链表有问题啊亲!\n");
return l;
}
//遍历结点
biNode* px=l->first;
biNode* ps=NULL;
while(px)
{
while(px)
{
if(px->data==d)
{
break;
}
px=px->next;
}
//说明没找到
if(px==NULL)
{
return l;
}
else
{
ps=px->next;
//找到了
if(px==l->first)
{
//头删
if(l->first==l->last)
{
//要考虑到这种情况!!!
//说明链表中只有一个结点
l->first=NULL;
l->last=NULL;
free(px);
}
else
{
l->first=l->first->next;
l->first->prev=NULL;
px->next=NULL;
free(px);
}
}
else if(px==l->last)
{
//尾删
l->last=l->last->prev;
l->last->next=NULL;
px->prev=NULL;
free(px);
}
else
{
//中间删
px->next->prev=px->prev;
px->prev->next=px->next;
px->prev=NULL;
px->next=NULL;
}
l->num--;
}
px=ps;
}
return l;
}
销毁一个双向链表
//销毁一个链表
/*
函数名:Xiaohui_list
参数列表:传进来一个带头结点的双向链表的头结点的地址@l
返回值:无
*/
void Xiaohui_list(biList* l)
{
//防空操作
if(l == NULL)
{
return;
}
//这里只需要从头开始把链表中每一个值删除释放空间即可,或者从最后一个往前删也行
//定义遍历删除结点
biNode* p=l->first;
while(p)
{
l->first=l->first->next;
p->next=NULL;
if(l->first)
{
//这里需要考虑到每次删除结点的后面一个结点不存在的情况
//当不存在的时候,l->first->prev会造成越界访问
//如果是最后一个结点,不需要置空,它本身没有被指向
l->first->prev=NULL;
}
free(p);
p=l->first;
}
l->last=NULL;
l->num=0;
free(l);
}
主函数记录
#include <stdio.h>
#include <stdlib.h>
#include "Toushuang.h"
int main()
{
biList* l=create_bilist();
int d;
biNode* p=NULL;
while(1)
{
scanf("%d",&d);
if(d==0)
{
break;
}
p=(biNode*)malloc(sizeof(biNode));
p->data=d;
p->prev=NULL;
p->next=NULL;
Insert(l,p);
}
biList* l2=delete_all_node(l,5);
print_bilist(l2);
return 0;
}
代码运行结果
这个是我用gcc编译器运行结果示意图
这里可以看到,随意输入数值,功能是删除所有目标数值之后,再按照升序排列之后正向打印和逆向打印;
四、循环链表
1、单向循环
第一步
定义数据结点(通过结构体和指针),数据结点中除了存储数据,还要存储上一个结点或下一个结点的位置,也就是指针,所以这里指针的类型和结点类型一样
//这里定义int 为Mytype类型,后面数据类型统一用Mytype去表示
//这要做的好处是:后续如果想输入浮点型等其他类型数据时,不需要在代码中一个个去修改数据类型,只需要将int改为需要的数据类型即可
typedef int Mytype;
typedef struct node
{
Mytype data;
struct node* next;
}Node;
//结点定义:通过结构体和指针进行数据存储,以及数据下一个位置存储
//在这里,结构体中data存储每一个结点中的数据
//next指针存储指向的前一个结点或后一个结点的位置(所以这里指针的类型和定义的结点相同)
第二步
定义(管理层)头结点,在这里需要定义一个int变量去帮我们数一组中数据个数,还要记录一组数据中第一个数据结点的位置和最后一个结点的位置;既然是位置,这里必然是指针,既然是指向一组的数据结点,那么指针类型必然是和数据结点类型相同。代码可表示如下:
头结点定义:
typedef struct tou
{
int num;
Node* first;
Node* last; //在这里你可以多加一些功能,让你的管理层管的更多一些
}Tou;
//我们定义了一个管理层类型叫Tou,它的功能是记录首尾位置和帮我们数数
第三步
创建一个函数去根据输入数据元素而组成一个单向循环链表
/* 函数名:XHdan() 创建一个单向循环链表
函数参数:无传入参数
返回值:返回一个创建好的单向循环链表的管理层头结点(所以这里的头结点类型Tou)*/
Tou* XHdan()
{
//创建一个头结点并初始化
Tou* head=(Tou*)malloc(sizeof(Tou));
//创建一个头结点指针(Tou类型)head;
//这里动态开辟了一块大小和管理层Tou一样大的空间,这一块空间的首地址用head接收
//这里注意:head是一个指针,指针去访问成员变量时,用->去指向
// 假如head是Tou定义的一个实体,用.去指向
head->first=NULL;
head->last=NULL;
head->num=0;
Mytype ddd; //ddd是我定义的每次输入保存数据的变量
while(1)
{
//循环:持续检测去接收输入数据
scanf("%d",&ddd);
//每一次输入数据之后都要完成while里面完整的一次执行
//设定循环结束条件:直到输入为0才整个执行结束
if(ddd==0)
{
break; //跳出while
}
// pnew是我们每一次输入数据之后创造的数据结点
//创建一个结点,保存数据,并初始化指针
Node* pnew=(Node*)malloc(sizeof(Node));
pnew->data=ddd;
pnew->next=NULL;
head->num++; //每次输入一个数据,管理层里面的num每次帮你加加
//把数据节点添加到链表中
//当输入第一个数据时进行的操作
if(head->first==NULL) //创建链表:开始第一个结点 从无到有
{
head->first=pnew;
//管理层的first帮你记录下你的第一个数据的位置;
head->last=pnew;
//当只有一个数据时,这个数据结点的位置既是第一个也是最后一个
//管理层的last帮你记录下你的最后一个数据的位置
}
else
{
//从少到多:尾插法
//这里需要理一下:head是管理部门(部门类型是Tou*)
//last是管理部门里的成员(属于管理层) 类型是Node*
//last成员的存的指向位置是新成员的地址
//这里可以把->next看做一个关系;
//好比head->last和pnew是两节车厢,->next就好像是中间的铁链
head->last->next=pnew; //将最后一个数据结点和新的数据结点连接起来
head->last=pnew; //并且把新的数据结点变成(在管理层中)最后一个数据结点
}
//下面是头插法
{
//新数据结点指向链表中第一个数据结点
pnew->next=head->first;
//同时新数据节点变成新的头结点
head->first=pnew;
}
}
//这里考虑假如输入的第一个数值就是0,就退出循环,也就是说这是一个空链表
//如果是空链表,也就不存在首尾相连作循环的过程
//全部添完,加循环
if(head->num!=0)//不等于0时才首尾相连
{
//最后一个数据结点连着第一个数据结点
head->last->next=head->first;
}
return head;
}
将上面的代码整理如下
typedef int Mytype;
typedef struct node
{
Mytype data;
struct node* next;
}Node;
typedef struct tou
{
int num;
Node* first;
Node* last;
}Tou;
Tou* XHdan()
{
//创建一个头结点并初始化
Tou* head=(Tou*)malloc(sizeof(Tou));
head->first=NULL;
head->last=NULL;
head->num=0;
Mytype ddd;
while(1)
{
scanf("%d",&ddd);
if(ddd==0)
{
break;
}
//创建一个变量和结点,保存数据,并初始化指针
Node* pnew=(Node*)malloc(sizeof(Node));
pnew->data=ddd;
pnew->next=NULL;
head->num++;
//把数据节点添加到链表中
if(head->first==NULL)
{
head->first=pnew;
head->last=pnew;
}
else
{
head->last->next=pnew;
head->last=pnew;
}
}
//全部添完,加循环
if(head->num!=0)//不等于0时才首尾相连
{
head->last->next=head->first;
}
return head;
}
链表打印
/*
函数名:printxun 打印单向循环链表
函数参数:传进来一个单向循环链表的头结点
返回值:无
*/
void printxun(Tou* head)
{
//通过头结点判断这个链表是否为空
if(head==NULL)
{
printf("不是吧朋友,空链表你也输入?\n");
return ;//什么也不返回<-->对应返回值为void
}
Node* p=head->first;
//为了证明这是一个循环单向链表:我们输出两轮看看效果
int n=head->num*2;
while(n--)
{
printf("%d ",p->data);
p=p->next;
}
printf("\n");
}
(功能函数要记得在头函数中声明)
主函数
int main()
{
Tou* p=XHdan();
printxun(p);
return 0;
}
代码执行结果
2、双向循环 (方式一)
创建一个函数去根据输入数据元素而组成一个双向循环链表
数据结点定义
typedef struct dnode
{
Mytype data;
struct dnode* prev;
struct dnode* next;
}Dnode;
头结点定义
typedef struct toou
{
Mytype num;
Dnode* first;
Dnode* last;
}Toou;
代码实现
/* 函数名:创建一个循环双向链表
函数参数:无
返回值:返回创建后循环双向链表的头结点 */
Toou* xunshuang()
{
//建头结点并初始化
Toou* head=(Toou*)malloc(sizeof(Toou));
head->first=NULL;
head->last=NULL;
head->num=0;
//定义输入数据类型
Mytype ddd;
while(1)
{
//创建数据结点
scanf("%d",&ddd);
if(ddd==0)
{
break;
}
Dnode* pnew=(Dnode*)malloc(sizeof(Dnode));
pnew->data=a;
pnew->next=NULL;
pnew->prev==NULL;
head->num++;
//将数据结点加到链表中:从无到有
if(head->first==NULL)
{
head->first=pnew;
head->last=pnew;
}
//尾插法和头插法选其一即可
//尾插法
else//从少到多
{
head->last->next=pnew;//最后一个结点位置指向新数据结点
pnew->prev=head->last;//新数据结点前驱指向最后一个
head->last=pnew; //最后一个结点位置变成新的数据结点
}
//实际代码中,头插尾插二者选其一即可
//头插法
else
{
head->first->prev=pnew; //管理层的记录第一个位置的数据成员与新数据成员相接
pnew->next=head->first; //新数据成员与管理层记录第一个位置的数据成员相接
//切记:这是一个双向奔赴
head->first=pnew; //同时,管理层中第一个位置的数据成员变成新数据成员
}
}
head->last->next=head->first;
//整个数据排队完成后,要记住,将最后一个位置的数据成员和第一个位置的数据成员相接
head->first->prev=head->last;
//将第一个位置的数据成员和最后一个位置的数据成员相接
//在这里,将->next和->prev看做一个链子,连接两个数据成员
return head; //最后,返回管理层
}
链表打印
/* 函数名:print_tou_shuang()
函数参数: 传进去一个带头结点的双链表a
返回值:无 */
void print_tou_shuang(Toou* a)
{
Dnode* p=(Dnode*)malloc(sizeof(Dnode));
p=a->first;
int number=a->num*2;
printf("顺序打印!\n");
while(number--)
{
printf("%d ",p->data);
p=p->next;
}
printf("\n");
printf("逆序打印!\n");
number=a->num*2;
p=a->last;
while(number--)
{
printf("%d ",p->data);
p=p->prev;
}
printf("\n");
}
主函数
int main()
{
Toou* t=xunshuang();
print_tou_shuang(t);
return 0;
}
代码执行结果
2、双向循环 (方式二)
因为这个循环是双向的,所以这里头结点可以只定义一个first指针只记录整个链表第一个结点的位置,不过这样显得更难理解且不方便操作;
创建一个双向循环升序链表
1、定义数据结点中数据的类型:
typedef int ElemType;
2、定义数据结点:
typedef struct binode
{
ElemType data;
struct binode* prev;
struct binode* next;
}biNode;
3、头结点定义:
typedef struct bilist
{
biNode* first;
}biList;
创建一个空的带头结点的双向循环链表
biList* create_XHlist()
{
biList* l = (biList*)malloc(sizeof(biList));
l->first = NULL;
return l;
}
将创建的数据结点添加到空的双向循环链表中
/*
函数名:Insert
参数列表:传进来一个带头结点的双向链表的头结点的地址 @l
还有要添加进去的数据结点 @p
返回值:无
*/
void Insert(biList* l, biNode* pnew)
{
if (l == NULL)
{
printf("链表不存在\n");
return;
}
//创建第一个数据结点:从无到有
if (l->first == NULL)
{
//体现循环的思想
pnew->prev = pnew;
pnew->next = pnew;
l->first = pnew;
}
else
{
biNode* pk = l->first;
//插入数据:
//1、找插入位置
while (1)
{
//退出循环的条件:要么是找到比它大的值,要么是遍历完一次循环,遍历第二遍链表时候的第一个结点的时候退出
if (pk->data > pnew->data)
{
break;
}
pk = pk->next;
//第二次遍历
if (pk == l->first)
{
break;
}
}
//2、实行插入操作
if (pk == l->first)
{
//头插和尾插区别:尾数据比头大
if (l->first->data > pnew->data)
{
//头插
pnew->next = l->first;
pnew->prev = l->first->prev;
l->first->prev = pnew;
l->first->prev->next = pnew;
l->first = pnew;
}
else
{
//尾插
pnew->prev = pk->prev;
pnew->next = pk;
pk->prev->next = pnew;
pk->prev = pnew;
}
}
else
{
pnew->prev = pk->prev;
pnew->next = pk;
pk->prev->next = pnew;
pk->prev = pnew;
}
}
}
图形辅助理解
打印一个带头结点的双向循环链表
/*
函数名:print_xunhuan
参数列表:传进来一个带头结点的双向链表的头结点的地址
返回值:无
*/
void print_xunhuan(biList* l)
{
if (l == NULL)
{
printf("这个链表不存在\n");
return;
}
if (l->first == NULL)
{
printf("这个链表里什么都没有\n");
return;
}
biNode* p = l->first;
printf("循环链表中的值有:");
while (p)
{
printf("%d ", p->data);
p = p->next;
if (p == l->first)
{
break;
}
}
printf("\n");
}
在带头结点的双向循环链表中删除值为x的一个数据结点
/*
函数名:delete_a_node
参数列表:传进来一个带头结点双链表的头结点的地址@l
还有要删除结点的数据@d
返回值:返回删除完之后的循环双链表的头结点地址
*/
biList* delete_a_node(biList* l, ElemType d)
{
biNode* px = l->first;
while (px)
{
if (px->data == d)
{
break;
}
px = px->next;
if (px == l->first)
{
//第二次遍历
break;
}
}
if (px == l->first)
{
//头删:考虑只有一个结点和其他结点的情况
//只有一个结点的情况
if (px == px->next)
{
px->prev = NULL;
px->next = NULL;
l->first = NULL;
free(px);
printf("链表里面已经全部删除了\n");
return l;
}
else
{
//其他正常头删的情况
l->first->next->prev = l->first->prev;
l->first->prev->next = l->first->next;
l->first = l->first->next;
px->prev = NULL;
px->next = NULL;
free(px);
}
}
else if(px==l->first->prev)
{
px->prev->next=px->next;
px->next->prev=px->prev;
px->prev=NULL;
px->next=NULL;
free(px);
}
else
{
px->prev->next = px->next;
px->next->prev = px->prev;
px->prev = NULL;
px->next = NULL;
free(px);
}
return l;
}
图形辅助理解
删除所有值为x的数据结点
biList* delete_all_node(biList* l, ElemType d)
{
biNode* px = l->first;
biNode* ps = NULL;
int flag;//不能放在这里初始化
while (px)
{
flag=0;
while (px)
{
if (px->data == d)
{
flag = 1;
break;
}
px = px->next;
if (px == l->first)
{
//第二次遍历
break;
}
}
if (flag == 0)
{
//没找到要删除的值
break;
}
if (px != px->next)
{
ps = px->next;
}
else
{
ps = NULL;
}if (px == l->first)
{
//头删:考虑只有一个结点和其他结点的情况
//只有一个结点的情况
if (px == px->next)
{
px->prev = NULL;
px->next = NULL;
l->first = NULL;
free(px);
// printf("链表里面已经全部删除了\n");
}
else
{
//其他正常头删的情况
l->first = px->next;
px->next->prev = px->prev;
px->prev->next = px->next;
px->prev = NULL;
px->next = NULL;
free(px);
}
}
else if(px==l->first->prev)
{
px->prev->next=px->next;
px->next->prev=px->prev;
px->prev=NULL;
px->next=NULL;
free(px);
}
else
{
px->prev->next = px->next;
px->next->prev = px->prev;
px->prev = NULL;
px->next = NULL;
free(px);
}
px = ps;
}
return l;
}
注意看:这里的flag是需要每次被重新置0的,要不然上次被删完之后flag=1,会一直往下走,持续头删尾删、中间删,会把整个链表中的值删完。
这些内容和图是我根据自己的理解一点一点写出来,算作是对我学习过程的梳理,很多题是我根据思路做出来,其中不乏有一些bug,以及思维漏洞;
其中内容希望能帮到像我一样的初学者,给它们的学习过程提供一些参考!
希望我能坚持独立写代码,多思考多记录,坚持!也希望有越来越多的学习者一起同行,一起慢慢变得厉害\^ …^/
如果有朋友们感兴趣,可以在评论区提出本篇文章的一些bug,评论区欢迎大家讨论!