程序设计课学到了链表,这东西很玄学,所以自己再从头梳理一下。
如果大佬发现哪里有问题的话,希望能帮我指出,谢谢!
目录
-->1.PTA 7-1 程序设计综合实践-1.1(简单的插入)
-->2.PTA 7-4 程序设计综合实践 1.4(简单的分离)
-->3.PTA 7-5 程序设计综合实践 1.5(简单的约瑟夫环)
--> 4.PTA 7-8 特殊约瑟夫问题(双向链表约瑟夫环问题)
1.什么是链表?
把数组拿出来和链表做对比,可能更容易理解链表。
1.数组的内存是连续的。但是链表一般不连续。
2.数组的元素个数是确定的(加长数组在现在的c语言标准里是没有意义的),链表可以改变。
3.数组不可以删除元素,不可以添加元素(当然,可以用算法实现类似的操作,但是不是真正意义上的删除),链表可以。
4.数组可以下标访问,链表只能遍历访问,所以说数组访问是o(1),链表访问是o(n)。
说了这么多不还是没有说什么是链表吗?
别急,下面就是链表的基本样子。
struct node {
//数据区
int data;
//指针区
struct node* next;
};
一个链表的每一个节点由这样一个结构体组成,这个结构体内部分为数据和域指针域。数据域可以是这样一个简单的data,也可以是多个数据,甚至一个结构体。
通常的单向链表是有一个指向下一个节点的指针,还有双向链表(用两个指针记录上家和下家)。
链表的结尾通常指向NULL这也是链表结尾的标志。(循环链表除外)
2.怎么构建一个链表?
1).申请节点
和数组不同,链表的每一个节点都是通过动态分配得到的,每次创建节点的时候需要调用头文件<stdlib.h>中的函数malloc的声明。malloc函数的参数是所需要申请的内存空间大小,一般使用sizeof()函数来当其参数的,增强代码的移植性。malloc函数的返回值是一个(void*)类型的指针,我们需要转换成我们需要的指针,注意如果申请失败返回NULL,在申请的时候应该判断是否申请成功。下面是申请一个新节点的函数newnode()。
struct node* newnode()
{
struct node* p = (struct node*)malloc(sizeof(struct node));
//如果返回空指针,说明内存里面找不到满足要求的地方了,及内存不足。
if (p == NULL)
printf("Error!\a\n"), exit(-1);
//返回的p指向我们申请的节点
return (p);
}
2).连接链表
根据链表的定义,每一个节点的指针指向下一个节点。我们需要使用到一种运算符 :
->
它由一个减号和一个大于符号组成由一个指针使用比如
p->next等价于(*p).next
ok有了基本知识就可以开始连接节点了。这里用最简单的单数据单向链表为例。构建函数add()
void add(struct node* la, int x)
{
struct node* q = newnode();
q->data = x; //把数据输入申请的节点中
struct node* p = la;//为了不改变la
while (p->next)//只要p->next 不指向空
{
p = p->next;//往后移一个位置
}
q->next = NULL;//链表结尾是NULL
p->next = q;
}
3).创建单向链表
单向链表如果有一个没有参数的头节点会让操作方便很多。所以一般创建单向链表的时候会申请一个头节点,然后再加入带节点的数据。那么这时判断链表是否为空的办法就是判断head->next是否等于NULL。
int n;
struct node* la=newnode();
la->next = NULL;
while (scanf("%d", &n) != -1)
{
add(la,n);
}
这样插入数据之后的la就是链表的头节点,之后的操作只需要传入头节点就行。
3.单向链表的基本操作
1).插入函数insert() (这里以升序为例)
void insert(struct node* la, int x)
{
struct node* q = newnode();
q->data = x;
//查找合适的位置插入
struct node* p = la;
while (p->next && x > p->next->data)
{
p = p->next;//往后移一个位置
}
//开始插入
q->next = p->next;
p->next = q;
}
2).打印函数print()
void print(struct node* la)
{
la = la->next;//跳过头节点
if (la)
{
printf("%d", la->data);
la = la->next;
}
while (la)
{
printf("->%d", la->data);
la = la->next;
}
printf("\n");
}
3).销毁函数destory()
注意不是删除,这是销毁整个链表,使用函数free()实现,特别提醒!!链表使用完之后一定要销毁,这是一个好习惯,因为在一个工程中如果不停的malloc节点但是不释放的话,会造成内存泄漏,后果就是程序会运行到内存中没有可用内存为止。
void destory(struct node* la)
{
while (la)
{
struct node* q = la->next;
free(la);
la = q;
}
}
4).倒置函数reverse()
把一个链表的所有数据倒序重排,但是头节点是不动的。注意
void reverse(struct node* la)
{
//跳过头节点
struct node* p = la->next;
la->next=NULL;//挂上最后一个节点也就是空
while (p)//每次把 p 挂到 la 后面,所以是判断 p 是否为NULL而不是p->next
{
struct node* q = p;//用 q 记下现在 p 的位置
p = p->next;// p 向后移
q->next = la->next;//把 q 插到 la 后面
la->next = q;
}
}
5).连接函数link()
连接两个带头节点的链表,前面的参数为改过的链表.
void link(struct node* start, struct node* end)
{
while (start->next != NULL)
{
start = start->next;
}
start->next = end->next;
}
6). 删除函数delete()
删除传入的地址的下一个元素,并重新连接链表,释放删除节点。
void deleta(struct node* la)
{
struct node* p = la->next;
la->next = p->next;
free(p);
}
4.单向链表之循环链表
1. 什么是循环链表?(个人理解)
单向的,非循环的链表是以NULL为结尾,而循环链表是首尾相连的,它也没有头节点,最后一个节点指向的也不是NULL而是第一个节点,那么在读取链表数据的时候就会一直循环,所以叫循环链表。
这就是一个简单的循环链表,la指向第 4 个元素,然后依次链接,最后回到 4 。
2.循环链表的创建
上面说过了,循环链表是没有头节点的,只是一个指针去指向环的某一个元素就行了。我一般循环让这个指针指向尾节点,这样方便添加元素。因为添加节点需要首节点的地址,还需要尾节点的地址,但是我们现在还是用的单向链表,只能向下寻找节点,所以只有用尾节点同时找到这两个地址。如果是双向链表的话,可以直接指向第一个节点。
7).循环链表创建函数append()
struct node* la = newnode();
la = NULL;
for (int i = 1; i <= n; i++)
{
la = append(la, i);//la指向最后一个人
}
struct node* append(struct node* la, int n)
{
if (!la)//空的判断第一个人需要特判,让它指向自己
{
struct node* p = newnode();
p->data = n;
p->next = p;
return p;
}
else
{
struct node* p = newnode();//新添加的节点
p->data = n;
struct node* q = la->next;//记录第一个节点的地址
la->next = p;//最后一个节点指向新加的节点,使得新的节点成为最后一个节点
p->next = q;//新的节点指向第一个节点,形成环
return p;//返回最后一个节点的地址
}
}
5.双向链表
前面有提到过双向链表,顾名思义双向链表就是每一个节点都保存了上一个节点的地址和下一个节点的地址,使用起来会比单向链表简单,特别是在处理约瑟夫环的问题的时候。双向链表的构造和单向链表很像,这里我直接放代码了。
struct node {
int data;
struct node* last;
struct node* next;
};
typedef struct node node;//这种写法可以少敲 亿 个struct!!
//环形双向链表构造
node* append(node* la, int m)
{
node* p = newnode();
p->data = m;
if (la == NULL)
{
p->next = p;
p->last = p;
}
else
{
node* q;
q = la->next;
la->next = p;
p->last = la;
p->next = q;
q->last = p;
}
return p;
}
6.实战!
1.PTA 7-1 程序设计综合实践-1.1(简单的插入)
1.1编写程序,建立2个带头结点单链表,输入若干整数将正整数插入第1个单链表,将负整数插入第2个单链表,插入前和插入后单链表保持递增或相等次序,显示2个单链表,最后销毁。程序不可存在内存泄漏。
输入格式:
若干整数。
输出格式:
每行显示一个链表,元素间用分隔符->分隔;共两行
输入样例:
100 2 3 -2 -8 -6 -9 -10 50 2 -1
输出样例:
2->2->3->50->100 -10->-9->-8->-6->-2->-1
1.AC代码
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
struct node* next;
};
void insert(struct node* la, int x);
struct node* newnode();
void print(struct node* la);
void destory(struct node* la);
int main()
{
int n;
struct node* zhen=newnode(), * fu = newnode();
zhen->next = NULL;
fu->next = NULL;
while (scanf("%d", &n) != -1)
{
if (n > 0)
{
insert(zhen, n);
}
else if (n < 0)
{
insert(fu, n);
}
}
print(zhen);
print(fu);
destory(zhen);
destory(fu);
return 0;
}
void insert(struct node* la, int x)
{
struct node* q = newnode();
q->data = x;
//查找合适的位置插入
struct node* p = la;
while (p->next && x > p->next->data)
{
p = p->next;//往后移一个位置
}
//插入之后
q->next = p->next;
p->next = q;
}
struct node* newnode()
{
struct node* p = (struct node*)malloc(sizeof(struct node));
if (p == NULL)
printf("Error!\a\n"), exit(-1);
return (p);
}
void print(struct node* la)
{
la = la->next;//跳过头节点
if (la)
{
printf("%d", la->data);
la = la->next;
}
while (la)
{
printf("->%d", la->data);
la = la->next;
}
printf("\n");
}
void destory(struct node* la)
{
while (la)
{
struct node* q = la->next;
free(la);
la = q;
}
}
2.PTA 7-4 程序设计综合实践 1.4(简单的分离)
1.4 编写程序,输入若干正整数,按从小到大次序建立1个带头结点单链表,设计一个实现单链表分离算法的Split函数,将原单链表中值为偶数的结点分离出来形成一个新单链表,新单链表中头结点重新申请,其余结点来自原链表,分离后,原链表中只剩非偶数值所在结点,最后显示2个单链表,在程序退出前销毁单链表。要求Split算法时间复杂性达到O(n),程序不可存在内存泄漏。
输入格式:
若干正整数。
输出格式:
每个单链表输出占一行,元素间用分隔符->分隔;初始单链表、剩余元素单链表、偶数元素单链表,共3行。
输入样例:
100 2 3 50 2 1 5 8
输出样例:
1->2->2->3->5->8->50->100 1->3->5 2->2->8->50->100
2.AC代码
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
struct node* next;
};
void insert(struct node* la, int x);
struct node* newnode();
void print(struct node* la);
void destory(struct node* la);
void link(struct node* start, struct node* end);
//返回奇数的头
void split(struct node* la,struct node*ji);
//删除下一个元素,记得释放哦!!
void deleta(struct node* la);
int main()
{
int n;
struct node *ou=newnode(),*ji=newnode();
ou->next = NULL;
ji->next = NULL;
while (scanf("%d", &n) != -1&&n!=0)
{
insert(ou, n);
}
print(ou);
split(ou,ji);
print(ji);
print(ou);
destory(ou);
destory(ji);
return 0;
}
void insert(struct node* la, int x)
{
struct node* q = newnode();
q->data = x;
//查找合适的位置插入
struct node* p = la;
while (p->next && x > p->next->data)
{
p = p->next;//往后移一个位置
}
//插入之后
q->next = p->next;
p->next = q;
}
struct node* newnode()
{
struct node* p = (struct node*)malloc(sizeof(struct node));
if (p == NULL)
printf("Error!\a\n"), exit(-1);
return (p);
}
void print(struct node* la)
{
la = la->next;//跳过头节点
if (la)
{
printf("%d", la->data);
la = la->next;
}
while (la)
{
printf("->%d", la->data);
la = la->next;
}
printf("\n");
}
void destory(struct node* la)
{
while (la)
{
struct node* q = la->next;
free(la);
la = q;
}
}
void link(struct node* start, struct node* end)
{
while (start->next != NULL)
{
start = start->next;
}
start->next = end->next;
}
//分离奇数和偶数
void split(struct node* la,struct node*ji)
{
struct node* q = la;//q酒店la的上一个
la = la->next;
while (la->next)
{
if (la->data % 2 == 1)
{
insert(ji, la->data);
la = la->next;
deleta(q);
continue;
}
q = la;
la = la->next;
}
}
void deleta(struct node* la)
{
struct node* p = la->next;
la->next = p->next;
free(p);
}
3.PTA 7-5 程序设计综合实践 1.5(简单的约瑟夫环)
1.5 约瑟夫环是个经典的问题。有M个人围坐成一圈,编号依次从1开始递增,现从编号为1的人开始报数,报到N的人出列,然后再从下一人开始重新报数,报到N的人出列;重复这一过程,直至所有人出列。求出列次序。本题要求用循环单链表实现。提示:开始时将循环单链表的指针变量设为空,设计实现尾部添加一人函数Append,添加第1人时,将结点的指针域指向自己,后面新添加人员时,在循环单链表指针变量所指尾部后添加新结点,并始终将循环单链表指针变量指向新添加结点,对应M个人的循环单链表中有M个结点;报数时,报到指定数后输出对应结点里的人员编号,并将该结点从链表中删除。题目输入包括M、N两个正整数,题目要求按出队列顺序输出他们的编号。
输入格式:
正整数M、N
输出格式:
出队列次序,1行,每个数据占4位。
输入样例:
10 3
输出样例:4
3 6 9 2 7 1 8 5 10 4
3.AC代码
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
struct node* next;
};
void add(struct node* la, int x);
struct node* newnode();
void print2(struct node* la);
void destory(struct node* la);
void reverse(struct node* la);
void deleta(struct node* la);
//尾部添加元素
struct node* append(struct node* la,int n);
//开始游戏
struct node* runysf(struct node* la, int m);
int main()
{
int n, m;
scanf("%d%d", &n, &m);
struct node* la = newnode();
la = NULL;
for (int i = 1; i <= n; i++)
{
la=append(la, i);//la指向最后一个人
}
la=runysf(la, m);
destory(la);
return 0;
}
void add(struct node* la, int x)
{
struct node* q = newnode();
q->data = x;
//插在后面
struct node* p = la->next;
la->next = q;
q->next = p;
}
//申请新节点
struct node* newnode()
{
struct node* p = (struct node*)malloc(sizeof(struct node));
if (p == NULL)
printf("Error!\a\n"), exit(-1);
return (p);
}
//最小四段打印链表
void print2(struct node* la)
{
la = la->next;//跳过头节点
if (la)
{
printf("%4d", la->data);
la = la->next;
}
while (la)
{
printf("%4d", la->data);
la = la->next;
}
printf("\n");
}
//释放一个链表
void destory(struct node* la)
{
while (la)
{
struct node* q = la->next;
free(la);
la = q;
}
}
//倒序输出
void reverse(struct node* la)
{
struct node* p = la->next;
la->next = NULL;//考虑没有元素的情况
while (p)
{
struct node* q = p;
p = p->next;
q->next = la->next;
la->next = q;
}
}
//删除后一个元素
void deleta(struct node* la)
{
struct node* p = la->next;
la->next = p->next;
free(p);
}
//返回的指针指向最后一个人
struct node* append(struct node* la, int n)
{
if (!la)//空的判断
{
struct node* p = newnode();
p->data = n;
p->next = p;
return p;
}
else
{
struct node* p = newnode();
p->data = n;
struct node* q = la->next;
la->next = p;
p->next = q;
return p;
}
}
//运行游戏
struct node* runysf(struct node* la, int m)
{
struct node* p = newnode(),*q,*k=p;
p->next = NULL;
while (la)
{
for (int i = 0; i < m; i++)
{
if(i==m-1) q = la;
la = la->next;
}
add(p,la->data);
if (la->next != la)
deleta(q);//la被删除了!!q接上了la的下一个元素!!
else
{
la = NULL;
break;
}
la = q;
}
reverse(k);//因为add函数实现是倒序加入的,所以最后加一个reverse倒置链表
print2(k);
return k;
}
4.PTA 7-8 特殊约瑟夫问题(双向链表约瑟夫环问题)
编号为1…N的N个小朋友玩游戏,他们按编号顺时针围成一圈,从第一个人开始按逆时针次序报数,报到第M个人出列;然后再从下个人开始按顺时针次序报数,报到第K个人出列;再从下一个人开始按逆时针次序报数,报到第M个人出列;再从下个人开始按顺时针次序报数,报到第K个人出列……以此类推不断循环,直至最后一人出列。请编写程序按顺序输出出列人的编号。
输入格式:
输入为3个正整数,分别表示N、M、K,均不超过1000。
输出格式:
输出为一行整数,为出列人的编号。每个整数后一个空格。
输入样例:
6 3 5
输出样例:
5 3 1 2 4 6
4.AC代码
#include <stdio.h>
#include <stdlib.h>
struct node {
int data;
struct node* last;
struct node* next;
};
typedef struct node node;
node* newnode();
node* append(node* la, int m);
void runysf(node* la, int m, int k,int n);
int main()
{
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
node* la = newnode();
la = NULL;
for (int i = 1; i <= n; i++)
{
la=append(la, i);
}
la = la->next;//现在la是第一个数,我想让指到的那个节点为第一节点。
runysf(la, m, k,n);
}
node* newnode()
{
node* p = (node*)malloc(sizeof(node));
if (!p)
{
printf("Error!\a\n");
exit(-1);
}
return p;
}
node* append(node* la, int m)
{
node* p = newnode();
p->data = m;
if (la == NULL)
{
p->next = p;
p->last = p;
}
else
{
node* q;
q = la->next;
la->next = p;
p->last = la;
p->next = q;
q->last = p;
}
return p;
}
// m 为逆时针, k 为顺时针
void runysf(node* la, int m, int k,int n)
{
int a = 1;
while (n--)
{
if (a)
{
for (int i = 0; i < m-1; i++)
{
la = la->last;
}
printf("%d ", la->data);
node* p = la->next;
node* q = la->last;
free(la);
p->last = q;
q->next = p;
la = q;
a ^= 1;
}
else
{
for (int i = 0; i < k - 1; i++)
{
la = la->next;
}
printf("%d ", la->data);
node* p = la->last;
node* q = la->next;
free(la);
p->next = q;
q->last = p;
la = q;
a ^= 1;
}
}
}
好了,总结到此结束。感觉自己已经算是一个链表小白了呢,好耶!觉得有一点用的话,可以点一个赞哦,嘿嘿。