目录
神马是链表
链表的分类
单向链表
链表的常用操作
查找操作
插入操作
删除操作
链表与数组
数组的插入
数组的删除
链表的应用
尾声
神马是链表
记得很久很久以前…我们学习过数组, 数组是在内存中一段连续的存储空间, 可以在常数时间内访问任意位置的元素, 但是数组也有缺点, 无法做到快速的插入和删除, 因为空间是连续且固定的, 想要在p位置插入/删除一个元素, 则 p之后的位置的元素都需要移动。
为了能够在常数时间内实现元素的插入和删除, 我们引入链表这种数据结构。
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
那么非连续、非线性有什么含义呢?这表明链表的内存是不连续的,前一个元素存储地址的下一个地址中存储的不一定是下一个元素。链表通过一个指向下一个元素地址的引用将链表中的元素串起来。(嗯,这东西真的6啊)
接着,我们来做一道小小的练习题
线性表若采用链表存储结构,要求内存中可存储单元地址()
A.必须连续
B.部分地址必须连续
C.一定不连续
D.连续不连续均可
答案:D
解析:数组的存储空间要求连续的内存;线性表不要求连续的内存,当然连续的内存也可以。
链表有哪些呢,请看
链表的分类
分类 | 描述 |
---|---|
单向链表 | 每一个节点包含了数据块和指向下一个节点的指针 |
双向链表 | 每一个节点包含了数据块和指向下一个节点的指针以及指向前一个节点的指针 |
咋们先来看单向链表:
单向链表
单向链表是最简单的链表形式。我们将链表中最基本的数据称为节点 (node) ,每一个节点包含了数据块(data)和指向下一个节点的指针(next),链表有一个头节点,图中以 head 表示。可以看出, ℎhead 指向第一个元素,第一个元素的 next 又指向第二个元素……直到最后一个元素,该元素不再指向其他元素,它称为表尾,它的 next 为空(NULL),链表到此结束。
可以看到,要找链表中的某一元素,必须先找到上一个元素,根据它提供的下一个元素的地址才能找到下一个元素。如果不提供头指针,则整个链表都无法访问。链表如同一条铁链一样,一环扣一环,中间是不能断开的。
我们使用数组模拟的方法来学习链表。我们创建一个结构体, 这个结构体有两个 𝑖𝑛𝑡int 型的变量, 分别是data 和next ,data 就是链表当前节点存储的数据,next 指向下一个节点的节点编号(在数组中的编号,这样可以避免使用指针)(讲的真好!!~~)。
die码:
struct node{ //next即后面节点的编号,data就是需要维护的数据
int next,data;
}a[10000];
int head; //head即头指针
//head节点是头结点, 不存储数据, 作用只是用来找到链表的第一个节点。
链表的常用操作
操作 | 描述 |
---|---|
查找 | 找到符合条件的节点 |
插入 | 添加一个新节点 |
删除 | 删除一个存在的节点 |
查找操作
如何根据给出的数据,找到链表中符合条件的节点呢?需要从头节点开始,逐个向后遍历节点,比较 p 的 data 同待查找的数据是否相同,相同则返回当前节点。
struct node{ //next即后面节点的编号,data就是需要维护的数据
int next,data;
}a[10000];
int head; //head即头节点的编号
node find(int value){
int p=head; //从
while(a[p]!=NULL){
if(value==a[p].data)
return a[p];
p=a[p].next;
}
return NULL;
}
插入操作
我们先通过一个选择题了解插入操作
在一个单链表中,若要在p节点后,插入一个q节点,则执行( )
A. p -> next = q -> next; q -> next = p;
B.q -> next = p -> next; p -> next = q;
C.p -> next = q -> next; p -> next = q;
D.q -> next = p -> next; p = q;
答案:B
分析思路:
若要在p节点后,插入一个q节点,则需要:q -> next = p -> next; p -> next = q;
链表如何实现插入操作呢? 如果我们想在第 p 个节点之后插入一个新节点, 因为我们不能像使用数组一样直接访问下标, 链表只能头遍历, 直到走到第 p 个节点。
到达第 p 个节点之后, 我们需要先将新节点的next 指向当前 p 节点next 指向的节点, 再将 p 节点的next 指向这个新的节点
struct node{ //next即后面节点的编号,data就是需要维护的数据
int next,data;
}a[10000];
int head; //head即头节点的编号
//注意要使用引用传递
void insert(int newIndex, node &pre){
a[newIndex].next=pre.next;
pre.next=newIndex;
}
删除操作
删除与插入操作类似, 如果我们想要删除第 p 个节点, 那么我们先要从头遍历到第p−1 的节点, 将p−1 节点的next 指向 p 的next ,同时,那么第 p 个节点就从当前的链表中移除了。
struct node{ //next即后面节点的编号,data就是需要维护的数据
int next,data;
}a[10000];
int head; //head即头节点的编号
void delete(node &delNode, node &pre){
pre.next=delNode.next
}
链表与数组
操作 | 链表 | 数组 |
---|---|---|
查找 | 从头节点开始查找 | 从下标 00 开始查找 |
插入 | 很快,因为只需要修改 𝑝𝑟𝑒pre 以及新节点的 𝑛𝑒𝑥𝑡next 编号 | 所有后面的节点都要向后移 11 位 |
删除 | 很快,因为只需要修改 𝑝𝑟𝑒pre 的 𝑛𝑒𝑥𝑡next 编号 | 所有后面的节点都要向前移 11 位 |
数组的插入
数组的删除
链表的应用
由于链表存储不连续,一般来讲,访问效率低于数组。但链表的删除插入效率高。
课堂练习:队列复原
小瓜现在让1到n这n个整数排成一列,但是他只告诉你每个整数的后面那个数是什么(最后一个整数的后面那个数是0),请你帮忙复原这个队列。
输入格式
第一行一个整数n(n<=100000),表示有n个整数。 接下来n行,每行两个数i,j,表示排在整数i后面的那个数是j。
输出格式
n行,每行一个整数,表示完整的队列。
输入样例
4
1 3
2 4
3 2
4 0
输出样例
1
3
2
4
思路:
用链表记录下每个节点的next ,然后从头节点开始,遍历整个链表,并输出值。
如何找到头节点呢?因为是头节点,所以没有任何节点的next 指向头节点。可以开一个 bool 数组来记录,或者用原来的 data 来存储每个节点的 pre (前一个节点的编号),这样便形成了一个双向链表,如果某个节点的 pre 为0,则是头节点。
尾声
咱们今天的链表就讲到这里,下篇博文再见👋