原文章:
数据结构学习记录:第二章下半:链表 - 知乎 (zhihu.com)
此文章为作者本人搬运至该网站。
说在前头:
各位在学习过程中,如果有任何不懂的地方,可以随时评论或者私信我!!!我每天都在高强度网上冲浪(这一点真的属实),从各位进行评论,到我发现评论,我认为应该最多不超过半个小时,所以啊,各位,尽情用你们的评论和私信淹没我吧!(=・ω・=)
由于上一篇文章内容挺多,而链表我认为又是一个重量级,所以我决定再开一章专门讲这个。
首先先推荐一下令我茅厕顿开(雾)的文章:
一口气搞懂「链表」,就靠这20+张图了 - 知乎 (zhihu.com)
里面比较详细地介绍了链表从定义到使用的全过程,如果有耐心可以看一下这个文章。下面我再结合一下自己的认识尽量用大白话讲出来:
链表和普通线性表(顺序表)的区别在于什么呢?这里我画一个图应该比较好理解:
这是顺序表与链表在内存空间中的样子,显然,顺序表的元素是有序排列、贴在一起的,也就和数组差不多,而链表在内存空间中不是连续排列的,但是每个元素(除了最后一个)都有指向下一个元素的地址指针,而最后一位由于后面没有元素,所以指针也为空指针(NULL)。
打比方说,有几个人的信息按顺序组成了一个链表,虽然他们地理位置上不在一起,但是前一个人知道后面的人的地理上的地址的,这样通过一步一步传,就可以知道所有人的位置和信息。这样子是不是一目了然?
至于双链表,循环链表什么的,等先把链表讲清楚了再来讲。
1.链表的创建
了解到这些,我们就可以开始创建链表了,还记得载体是什么吗?
还是万恶(划掉)
万能的结构体!!!
第28页,请无视左边的痕迹(
//-----线性表的单链表存储结构-----
typedef struct LNode{
ElemType data;
struct LNode * next;
}LNode, * LinkList;
这里还是用typedef给结构体改名字,不过我也不知道为啥上下都要搞成LNode,虽然还是能用LNode *L创建指向结构体的指针。。。
而里面的
struct LNode *next;
是链表结构体的重点所在,这个定义出的指针指向名为LNode的结构体。
但是!
它并不是指向自身,即使这个外面的结构体确实叫LNode。而这个指针是需要去指向下一个结构体的,因为刚才也讲过链表的原理,每一处区域都需要有一个指针,来指向下一个区域,这个区域就可以理解为这个大的结构体。这样就可以实现链表的基本模样了。
还有就是最下面的那两个:
LNode, * LinList;
这两个也是用typedef定义后的结构体指针的名字,不过不同的是,LNode定义指针变量是用
LNode * L;
而LinList则是直接用:
LinkList L;
除此之外,LNode就和我们第二章上半所遇到的:
typedef struct {
ElemType * elem; //存储空间基址(就是该结构体储存的数据)
int length; //当前长度
int listsize; //当前分配的存储容量(以sizeof(ElemType)为单位
}SqList;
这里面的SqList差不多,不过LNode和LinList其他不同的是前者可以定义普通结构体变量也可以定义指针,而后者只能定义指针。
不知道我能不能讲清楚。。。。
创建完之后,接下来就是开辟空间了。
2.初始化与开辟空间
一开始我们创造的链表只有一节长,而其实这一节也只有这个指针有用处,它是用来指向第一个有数据的节点的。其尾部的指针当然就是空(NULL),于是就有了:
这课本还是不给写这些内容!我只能把上面那个教程的内容搬过来了(
void listinit(LinkedList &L){
L=(Node*)malloc(sizeof(Node)); //开辟空间
if(L==NULL){ //判断是否开辟空间失败,这一步很有必要
printf("申请空间失败");
//exit(0); //开辟空间失败可以考虑直接结束程序
}
L->next=NULL; //指针指向空
}
这里next赋成NULL也说明了它是指向下一个节点的指针。
3.头插入法建立链表
在这里我先贴上代码,如果能看懂就行,看不懂就看我讲:
//头插法建立单链表
LinkedList LinkedListCreatH() {
Node *L;
L = (Node *)malloc(sizeof(Node)); //申请头结点空间
L->next = NULL; //初始化一个空链表
int x; //x为链表数据域中的数据
while(1) {
scanf("%d",&x);
if (x < 0){
break;
}
Node *p;
p = (Node *)malloc(sizeof(Node)); //申请新的结点
p->data = x; //结点数据域赋值
p->next = L->next; //将结点插入到表头L-->|2|-->|1|-->NULL
L->next = p;
}
return L;
}
在这里它是把前面建立的一个节点在这里合并起来了,如果两个函数分开的话就是这样,
下面的这个函数和我课本上的形式差不多:
//头插法建立单链表,以输入负数为结束
Status LinkedListCreatH(LinkedList &L) {
int x; //x为链表数据域中的数据
while(1) {
scanf("%d",&x);
if (x < 0){
break;
}
Node *p;
p = (Node *)malloc(sizeof(Node)); //申请新的结点
p->data = x; //结点数据域赋值
p->next = L->next; //将结点插入到表头L-->|2|-->|1|-->NULL
L->next = p;
}
return OK;
}
但其实意思都差不多。看不懂?且听我娓娓道来~
一开始我们不是创建了一个只有空指针的节点嘛:
在进入第一次循环后,我们又创建了个p指向的结构体:
然后我们再用:
p->next = L->next;
L->next = p;
将p->next变成NULL,也就是把p变成最后一个节点
L的指针指向p,来连接起来:
这样就实现了第一个节点的接入,那么我们继续来看第二节点,就能明白为什么是头插入了。
第二个循环依然是:
while(scanf("%d",&x) != EOF) {
Node *p;
p = (Node *)malloc(sizeof(Node)); //申请新的结点
p->data = x; //结点数据域赋值
p->next = L->next; //将结点插入到表头L-->|2|-->|1|-->NULL
L->next = p;
}
scanf输入一个x2,然后用p2->data储存,(这里我都带上了个2,是为了区别开)
而p->next = L->next; 使p2的指针实现L的指针的功能,就是指向p,之后L->next = p; 使L的指针指向p2,实现结果如下:
其实就可以看出,p2插在了p的前面,读取数据时也会先读取p2,但是写入数据是按照p,p2来的,于是就实现了我们的头插入法。
以此不断循环,就实现了链表的创建。
4.尾插入法建立链表:
//尾插法建立单链表,以输入负数为结束
Status LinkedListCreatT(LinkedList &L) {
Node *r;
r = L; //r始终指向终端结点,开始时指向头结点
int x; //x为链表数据域中的数据
while(1) {
scanf("%d",&x);
if (x < 0){
break;
}
Node *p;
p = (Node *)malloc(sizeof(Node)); //申请新的结点
p->data = x; //结点数据域赋值
r->next = p; //将结点插入到表头L-->|1|-->|2|-->NULL
r = p;
}
r->next = NULL;
return OK;
这段代码之前困扰了我一整个晚上!!!我现在必须要把它讲清楚!!!
这里一开始先定义了两个空链表,不必多讲
之后进入第一个循环,有了一个新节点:
最后的两句是重量级!!!
r->next = p; //将结点插入到表头L-->|1|-->|2|-->NULL
r = p;
第一句先让r指向p:
之后r=p这一句我之前一直搞不懂,现在终于明白了,
因为r一开始不是个指针嘛,之后分配空间让r指向一个结构体,但是这里的话,就是让r所指向的内容变成p所指向的内容,结果就是,r现在要指向右上角那个结构体,也就是说,现在的p可以改名字叫r了:
(其实r这个字母和下边的两个框之间应该有一个箭头的,但是之前我直接省略掉了,这样也间接导致了我的误解:我一直以为是把r指向下面的框的内容换成p指向下面框的内容,但是现在看来只是指针的指向进行了改变,并不改变指针所指向的内容)
(我也不知道L存在的意义是什么,好像循环里面就没用到L)
之后就是下一次循环:
while(1) {
scanf("%d",&x);
if (x < 0){
break;
}
Node *p;
p = (Node *)malloc(sizeof(Node)); //申请新的结点
p->data = x; //结点数据域赋值
r->next = p; //将结点插入到表头L-->|1|-->|2|-->NULL
r = p;
}
再定义一个节点p2,也就是创建新的p2指向的结构体,输入一个x2,
r->next = p:之后让r指向的结构体的指针指向p2:
r = p:再让r指向p2的结构体:
这样一看,就是顺序输入的链表,r一路转换。
这样子清楚了没?反正我是应该清楚了。。。。
5.链表的输出:
链表的输出,就是将链表从头至尾依次输出,但是由于通向下一个节点的指针的存在,使链表这一块儿可以只依靠一个指针,指向原先结构体完毕之后,就可以通过内部的指针再指向下一个结构体。
//遍历输出单链表
void printList(LinkedList L){
Node *p=L->next;
int i=0;
while(p){
printf("第%d个元素的值为:%d\n",++i,p->data);
p=p->next;
}
}
不好理解的话我还可以用图表示:
由于每个链表都有一个头指针,这个指针指向的结构体除了指针,其余没有元素,而这个就是为了指向第一个元素的,一开始p的值为L->next,意思就是充当了L->next的作用,指向L2。
当输出完毕之后,p=p->next,那么p又开始指向下一个指针指向的结构体了,这里就不赘述了。
6.链表的修改元素:
其实这个内容和输出链表内容差不多,我就直接略过了。
//链表内容的修改,在链表中修改值为x的元素变为为k。
Status LinkedListReplace(LinkedList &L,int x,int k) {
Node *p=L->next;
int i=0;
while(p){
if(p->data==x){
p->data=k;
}
p=p->next;
}
return OK;
}
但是我们还要注意到的是,从这个循环就可以看出,单链表的元素操作是必须要从头开始一个节点一个节点进行的,因此可能会比较繁琐。
7.把上面这些合并起来:
#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef int Status;
#define OK 1
//-----线性表的单链表存储结构-----
typedef struct LNode{
ElemType data;
struct LNode * next;
}LNode, * LinkedList;
void listinit(LinkedList &L){
L=(LNode*)malloc(sizeof(LNode)); //开辟空间
if(L==NULL){ //判断是否开辟空间失败,这一步很有必要
printf("申请空间失败");
//exit(0); //开辟空间失败可以考虑直接结束程序
}
L->next=NULL; //指针指向空
}
//头插法建立单链表,以输入负数为结束
Status LinkedListCreatH(LinkedList &L) {
int x; //x为链表数据域中的数据
while(1) {
scanf("%d",&x);
if (x < 0){
break;
}
LNode *p;
p = (LNode *)malloc(sizeof(LNode)); //申请新的结点
p->data = x; //结点数据域赋值
p->next = L->next; //将结点插入到表头L-->|2|-->|1|-->NULL
L->next = p;
}
return OK;
}
//尾插法建立单链表,以输入负数为结束
Status LinkedListCreatT(LinkedList &L) {
LNode *r;
r = L; //r始终指向终端结点,开始时指向头结点
int x; //x为链表数据域中的数据
while(1) {
scanf("%d",&x);
if (x < 0){
break;
}
LNode *p;
p = (LNode *)malloc(sizeof(LNode)); //申请新的结点
p->data = x; //结点数据域赋值
r->next = p; //将结点插入到表头L-->|1|-->|2|-->NULL
r = p;
}
r->next = NULL;
return OK;
}
//遍历输出单链表
void printList(LinkedList L){
LNode *p=L->next;
int i=0;
while(p){
printf("第%d个元素的值为:%d\n",++i,p->data);
p=p->next;
}
}
//链表内容的修改,在链表中修改值为x的元素变为为k。
Status LinkedListReplace(LinkedList &L,int x,int k) {
LNode *p=L->next;
int i=0;
while(p){
if(p->data==x){
p->data=k;
}
p=p->next;
}
return OK;
}
int main(){
LNode *L;
listinit(L);
//LinkedListCreatH(L);
LinkedListCreatT(L);
printList(L);
//int x, k;
//scanf("%d %d", &x, &k);
//LinkedListReplace(L, x, k);
//printList(L);
return 0;
}
这里就实现了链表的基本操作了。
8..链表的节点插入
在这里我们终于可以看课本里面的内容了(也是不容易):
(解压后)代码:
Status ListInsert_L(LinkList &L, int i, ElemType e) {
LNode *p;
LNode *s;
//在带头结点单链线性表L中第 i个位置之前插入元素 e
p = L;
int j = 0;
while (p && j < i - 1) { //寻找第i-1个节点
p = p->next;
++j;
}
if (!p || j > i - 1) {
return ERROR;
}
s = (LinkList)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}//ListInsert_L
我先用图示讲一下链表插入元素的方法:
首先我们定义了有两个节点的链表,然后输入一个s指向的结构体,我们怎么样才能把这个a插入到L2和L3之间呢?
还记得链表和普通线性表的区别所在吗?
无论这个结构体在哪里,只要有指针指向该结构体,或者该结构体的指针指向其他结构体,那么它就可以存在于该链表中!
也就是说,插入这个节点,只需要改变它前一位结构体的指针,让它指向要插入的节点,再将要插入的节点的结构体的指针指向后一位结构体,最后删除原先在前一位和后一位之间的指针即可!
是不是一目了然?不用再像普通线性表一样一个一个往后挪元素了,
而上面的代码中前面那部分很明显不是多重要,我们看的是后面这三行:
s->data = e;
s->next = p->next;
p->next = s;
在这里,我们很容易得知p指向的结构体就是L2指向的结构体,之后s指向的结构体中的数据更新为e(图里面是y),s的指针指向更新为p的指针指向,就是指向L3,p的指针指向变成s,也就是让p的结构体的指针指向s,
s->next = p->next; 在这里我们不用知道下一位结构体的指针就可以指向它,因为有前一位的指针。
其实就只有两步,
把新结构体的指针指向后一位的结构体(用上一位的结构体的指针覆盖),
把上一位的结构体中的指针指向新结构体。
应该能看懂吧~~
9.链表的节点删除
Status ListDelete_L(LinkList &L, int i, ElemType &e) {
//在带头节点的单链线性表L中,删除第i个元素,并由e返回其值
LNode *p;
p = L;
int j = 0;
while (p->next && j < i - 1) { //寻找第i个结点,并令p指向其前趋
p = p->next;
++j;
}
if (!(p->next) || j > i - 1) { //删除位置不合理
return ERROR; //删除并释放结点
}
LNode *q;
q = p->next;
p->next = q->next;
e = q->data;
free(q);
return OK;
}
在这里同样,重要的只是后面几句。前面先用p找到要删除的元素结构体的前一个结构体,然后再定义一个指针q,用指针q指向要删除的结构体(q = p->next;),
然后用 p指向的结构体的指针 指向 q指向结构体的指针 所指向的结构体(p->next = q->next;),
不会绕晕吧(
简单来说就是p的结构体的后继原先是q指向的结构体的,在图中也就是a指向的结构体,然后p,也就是a指向的结构体的后继就是L3指向的结构体,那么经过上面的步骤后p的结构体就直接指向L3的结构体了:
之后第三句,把q指向的结构体的data用e表示出来(e = q->data;),这一步应该挺简单的。
之后用free释放掉q,其指向的结构体的内存也被释放掉了:
这样就实现了链表的节点删除操作。主要就是要获得所要删除的节点的指针指向,以及这个节点的上一个节点的指针指向。
10.有一个点需要注意:
学习完这些东西,我又感觉自己会了,然后就又兴冲冲地去看PTA(虽然我们线性表的题目已经结束四五天了qaq),结果我又遇到一个坑!
我先给各位看看这个题吧,还是函数题(之前讲过的类型):
单链表逆转:
本题要求实现一个函数,将给定的单链表逆转。(就是把节点的顺序倒过来)
函数接口定义:
List Reverse( List L );
其中List结构定义如下:
typedef struct Node *PtrToNode;
struct Node {
ElementType Data; /* 存储结点数据 */
PtrToNode Next; /* 指向下一个结点的指针 */
};
typedef PtrToNode List; /* 定义单链表类型 */
L是给定单链表,函数Reverse要返回被逆转后的链表。
裁判测试程序样例:
#include <stdio.h>
#include <stdlib.h>
typedef int ElementType;
typedef struct Node *PtrToNode;
struct Node {
ElementType Data;
PtrToNode Next;
};
typedef PtrToNode List;
List Read(); /* 细节在此不表 */
void Print( List L ); /* 细节在此不表 */
List Reverse( List L );
int main()
{
List L1, L2;
L1 = Read();
L2 = Reverse(L1);
Print(L1);
Print(L2);
return 0;
}
/* 你的代码将被嵌在这里 */
输入样例:
5
1 3 4 5 2
输出样例:
1
2 5 4 3 1
限制:
代码长度限制
16 KB
时间限制
400 ms
内存限制
64 MB
首先需要注意到的:
肯定就是结构体的定义,明显和我们讲的不一样。
typedef struct Node *PtrToNode;
struct Node {
ElementType Data; /* 存储结点数据 */
PtrToNode Next; /* 指向下一个结点的指针 */
};
typedef PtrToNode List; /* 定义单链表类型 */
但其实细看的话,也差不多。
在这里是把PtrToNode 当作对结构体指针的定义,而下面的typedef则是把对结构体的定义又变成了用List定义,这一点我们在已经给出的函数里也能看出。
其次:
有两个函数 Read() 和 Print( List L ) ,一个是输入链表数据,一个是输出链表数据,后面注释写上了/*细节在此不表*/,也就是说,这个是PTA帮我们写好的,我们不需要再写了。我们需要写的就是List Reverse( List L );这个函数。
而关于这个题目的讲解,各位可以去看这个:
单链表逆转(数据结构)_本题要求实现一个函数,将给定的单链表逆转-优快云博客blog.youkuaiyun.com/Mas1461261388/article/details/80097158
我觉得这个非常详细,还配有图片讲解,所以我就不讲了摸个鱼_(:з」∠)_
而如果你看完了的话,你就会意识到一个问题:
这个链表的头节点是有数据的!
以往我们定义并创建链表的时候,头节点都是只有一个指向下一结构体的指针,而且没有元素数据:
但是这个题不一样!
很明显链表的头节点是有A这个数据的,这就存在了差异。
而差异就会导致出错!
希望大家可以意识到这一点,在做题的时候先观察或者测试一下别人创建好的链表的头节点到底是不是只有一个指向下一节点的指针。
11.还有高手?
上面不是说有的题的链表的头节点是有数据的吗,但是我遇到的下一个题立马就没有了!!
篇幅受限,在这里我就不细贴原题了,如果感兴趣的话可以搜一下 两个有序链表序列的合并,
简单来说就是把两个非递减的链表合并成一个链表,仍保持非递减。
在这里放出答案的部分代码:
有没有看到一开始的List p1 = L1->Next; 还有 List p2 = L2->Next; ?
这就说明L1和L2的头节点是没有元素的!
又一个大坑。。。。。
在这里我们只好走一步看一步,先当作头节点没有元素来写,如果提交不过再尝试有节点的。
To be continued:
由于后面的内容,循环链表,双向链表什么的重要性不是太高,所以我就暂且搁置,先去开下一章——栈
的学习,(学校老师都讲到二叉树了啊再不赶进度就来不及了啊淦)
有空再继续更~-