关于本系列
如果你能够在阅读本文之后对该知识有一个初步的了解,知道基本的概念,那就足够了。
这就是本系列的创作初衷。
希望能帮助到正在学习这个知识点的你。
同时,为了不让编程语言成为阅读的障碍,本系列的文章都会给出C,Java,Python的代码(Python的代码在有空之后会补上)。
但是本人是菜鸡,基本上是一边查语法一边进行写作,肯定会有诸多不足,请大家多多担待。
欢迎大家的任何意见,由衷感激。
前言
假设你是一个火车头,你需要搭载乘客了,那么就需要1节车厢,这节车厢拼接在你后面,这样你就拥有了1节车厢用来搭载乘客。
当你拥有车厢之后,你就可以搭载乘客出发了。你的Boss说,因为你的乘客中有身份尊贵的人,必须安排每1个人都有1个车厢。你不知道为什么,但还是遵守了。
第一站,外面有4个乘客,你根据Boss的要求,先拼接一个车厢在你后面,然后再拼接第二个车厢在第一个车厢的后面,再拼接第三个车厢在第第二个车厢的后面,依次类推,这样,你就带着4个车厢继续出发了。
PS:他们车厢的编号分别是001,002,003,004哦
第二站,外面有一个乘客,他的要求很奇怪,他说:“我的车厢必须在最前面。”。你遵循以顾客为上帝的原则,还是答应了他的请求,你分配了005号车厢给他。但是你后面拖着4个车厢呢,那该怎么办呢?你灵机一动,先断开和后面车厢相连的部分,那4节车厢就只有他们自己是互相连接在一起的,然后你把这个奇怪要求的乘客的车厢拼接到你后面,然后把那4节车厢的 最前面的一节车厢 拼接 到了奇怪要求的乘客的车厢后面,这样,就完成了这个奇怪乘客的要求,带着5节车厢继续前往下一站了。
第三站,你遇到了警察,警察说怀疑你的车厢里面有他们抓捕的犯人,要求搜查你的车厢。为了不让犯人逃跑,他们会分成两批人,一批从最开始的车厢一节节往后搜查,一批从你连接的这批车厢的最后一个车厢开始一节节的往前搜查。因为除了最开始的车厢因为前面只有火车头的缘故,所以间隔很大,可以从最开始的车厢进入;最后一个车厢也是,因为最后一个车厢的最后没有车厢了,所以可以从最后车厢的后门进入,其余车厢由于互相连接在一起,只有少许的缝隙,肯定不能从那些地方出入。所以是一个不错的瓮中捉鳖的方法。
![]()
警察发现002号车厢的人是犯人,把他逮捕走了,但是你就开始发愁,002号车厢都没人了,那是不是应该把他撤走呢?如果空着的话,下一站要上车的人,就得经过005或者004,往002车厢的方向走,这样又会打扰到别的车厢里面的人。你犹豫许久,还是决定把002号车厢给撤走,免得下一站的客人来了需要经过别人的车厢,打扰到别人的休息。
于是你先把001跟002的连接断开,变成:![]()
再把003和002车厢连接的部分断开:![]()
再让001车厢和003车厢互相连接,就成功把002车厢移走了。
![]()
完成连接之后,你也开始继续出发,前往下一站啦~
你的旅程还在继续……
接下来我们通过上面这个小故事,来展开看一下链表,到底是什么。
链表
这里需要引入一个概念:线性表。
将具有一对一关系的数据线性地存储到物理空间中,这种存储结构就称为线性存储结构(简称线性表)。是一种最基本,最简单,最常用的数据结构。
一对一,可以简单理解为一个身份证对应了一个人。
线性,可以想象把一些数据可以集中起来,成为一条线。
既然是一条线,那么也可以有两种组成线的方法,这就是线性表的两种存储结构:一种是顺序表(顺序存储结构),一种是链表(链式存储结构)。他们之间的区别就是,他们存放数据的地址是否连续,顺序表是连续的,链表不一定是连续的。
节点
在上面的故事中,火车搭载人的最基本的东西,就是车厢,在链表中,最基本的单位,叫做节点。也就是说上面故事中的车厢,就跟我们链表的节点一样。
什么是节点呢?从火车的单个车厢来看,上面故事中的车厢,它能搭载人,有前门和后门这三个重要的属性。
- 搭载的人,就是节点中的属性,也就是该节点能够保存的数据。
- 前门,它能够和上一节车厢的后门互相连接,使这两个车厢连通,也就是该节点和上一个节点连接的方式,可以说是一个指针,指向了上一个节点,我们通过该指针,就可以访问到上一个节点。
- 后门,它能够与下一节车厢的前门相互连接,使这两个车厢连通,也就是该节点和下一个节点的连接方式,可以说是一个指针,指向了下一个节点,我们通过该指针,可以访问到下一个节点。
补充个小的知识点,如果了解内存地址的概念也可以直接跳过
假设你去看演出,需要将东西寄存。寄存处有一个柜子,柜子有很多抽屉。![]()
每个抽屉可以放一样东西,你有两样东西要寄存,因此要了两个抽屉。
![]()
你将两样东西存放在这里。![]()
现在你可以去看演出了!这大致就是计算机内存的工作原理。计算机就像是很多抽屉的集合体,每个抽屉都有地址。
![]()
fe0ffeeb是一个内存单元的地址。 需要将数据存储到内存时,你请求计算机提供存储空间,计算机给你一个存储地址。[1]
车厢中的前门,转换过来说就算是计算机内存中的一个地址,该地址存储了上一个车厢的地址,通过该地址,我们就可以访问到该车厢,如同探宝游戏一样。
小明在万圣节得到了一张藏宝图,他高兴坏了,迫不及待就根据藏宝图出发寻找宝藏去了。藏宝图上写的是家里鞋柜第二行第三列。
小明过去打开了鞋柜,发现里面也有一张藏宝图,上面写的是门口的垫子下面。
小明又在门口的垫子下面发现了一张藏宝图,上面写的是小明卧室的床底下。
这次,小明终于在床底下找到了他的圣诞礼物,是一本作业~!
像藏宝图一样,记录着下一个宝藏的地址,这就是指针。指针记录着一个地址,这个地址上有什么,只有访问的时候才知道啦~
到现在,我们的节点应该拥有的属性都出现了,分别是节点能保存的数据,上一个节点的地址,下一个节点的地址。
接下来我们来写一下 节点类 ,他是链表中最基本的单位,所以一定要理解。
C
typedef struct Node
{
struct Node *prev; //存放上一个节点的地址
int data; //该节点需要保存的元素
struct Node *next; //存放下一个节点的地址
} Node;
//创建节点的函数
Node* createNode(Node *prev,int data,Node *next)
{
Node* node = (Node*)malloc(sizeof(Node));//申请内存
node->data = data;
node->prev = prev;
node->next = next;
return node;
}
Java
class Node{
Node prev; //存放上一个节点的地址
int data; //该节点需要保存的元素
Node next; //存放下一个节点的地址
//必须提供三个属性用作构造该节点
//即,必须说明 和这个节点相连接的上一个节点是谁,这个节点包含的元素是什么,下一个节点是谁
public Node(Node prev, int data, Node next) {
this.prev = prev;
this.data = data;
this.next = next;
}
}
以Java的代码来介绍,首先我们创建了一个节点类,该类中有三个属性,分别是prev,data,next。
- prev,一个指针,它存放的是上一个节点的地址,通过该地址,这样我们就可以通过访问prev来找到上一个节点。
- data,是我们这个节点内保存的数据,也叫做元素。
- next,一个指针,它存放的是下一个节点的地址,通过该地址,我们就可以访问到该节点的西下一个节点是什么。
实现一个链表
首先了解一下链表中存在的几个成员属性~
C
typedef struct {
struct Node *head; //链表中的头节点
int size; //记录链表的长度
struct Node *last; //链表中的尾节点
} LinkedList;
//初始化链表的函数
void initLinkedList(LinkedList* list)
{
list->head = NULL;
list->last = NULL;
list->size = 0;
}
Java
public class LinkedList{
private Node head; //链表中的头节点
private int size; //记录链表的长度
private Node last; //链表中的尾节点
//初始化链表的方法
public LinkedList() {
size = 0;
head = null;
last = null;
}
}
- size,代表链表的长度,我们需要知道链表的长度,才能方便我们进行索引操作(如:根据索引插入指定的元素到指定的位置;删除指定位置的节点)。
- head,用作记录链表的头节点,也就如同火车中的最前面的车厢。
- last,用作记录链表的尾节点,如同火车中的最后面的车厢。
重点就是理解head和last这两个属性。就像故事中,我们只是一个火车头,我们需要时刻记得第一个车厢和最后一个车厢,这样才能通过他们走进去车厢中 检查乘客的情况,要是我们忘记了第一个车厢,那么我们就只能从最后一个车厢往前走,其余情况也是同理。
如果我们把第二个车厢记成了第一个车厢,那么我们就会从第二个车厢开始往下搜索,只有从最后一个车厢往前搜索的时候,才能重新找到第一个车厢,所以我们的head和last是非常重要的,每次如果修改了第一节车厢和最后一节车厢,都要更新我们的head和last。
该链表的实现中还有一个无参的构造方法 LinkedList() (C是initLinkedList()) ,用来初始化size,head,last这三个属性。 (不初始化的后果大家知道的哦)
现在我们通过实现一下链表中的方法,来继续了解链表。
增加元素
故事中,我们作为一个火车头,Boss给我们提的要求是每个车厢搭载一个乘客,也就是我们每个节点,只存放一个元素,因此,我将节点中的data属性设置为了整形,该属性也是只用来存放一个元素,对应一个乘客。
在第一站的时候,我们有四个客人,也就是我们需要四个车厢,让他们连接起来,才好方便带着他们继续出发前往下一站。换到链表这边,是不是我们就有四个元素,然后需要将他们拼接到一起呢?
我们可以按照客人的编号,将他们连接起来。让客人1号的后门连接到客人2号的前门,然后客人2号的后门,连接到客人3号的前门,客人3号的后门,连接到客人4号的前门~
像图里的车厢互相连接一样,客人1号的后门,和客人2号的前门互相连接了,也就是客人1号可以走到客人2号的车厢;同样,客人2号也可以走到客人1号的车厢,这样我们就实现了车厢的互通。
在车厢连通之后,我们就要将最前面的车厢和最后面的车厢记录下来,这样我们就可以通过地址,直接去到客人1号的车厢和客人4号的车厢。
我们使用head和last这两个元素,分别记录下了客人1号车厢的地址0x001、客人4号车厢的地址0x004,这样我们随时可以通过0x001,0x004这个两地址找到客人1号,客人4号的车厢。
在第二站,我们遇到了一个有着奇怪要求的乘客(客人5号),它要求它的车厢必须在最前面。
我们可以直接让客人5号的后门跟客人1号的前门互相连接,然后更新head这个元素,这样最前面的车厢就变成了客人5号的车厢,同时head这个元素记录的地址也变成了0x005。
这样我们就满足了这个奇怪乘客的要求。
现在我们经历了这两站的客人,学会了把车厢放在最前面和最后面。那如果还有更奇怪的乘客,他的要求是车厢必须在第三个位置,那我们该怎么做呢?
这肯定难不倒你。在故事中,我们002号车厢的客人是警察要逮捕的犯人,警察把他逮捕之后,002号车厢就空出来了,我们为了便携性考虑,就把002车厢给扔掉了呢,现在我们回想一下具体的操作。
我们首先让001号车厢和002号车厢的连接断开。
再让002和003车厢直接的连接断开,这样002号车厢就和整体的车厢块 分隔开来了。
之后再让001号车厢和003号车厢互相连接,就相当于把002号车厢给丢掉了。
那么,我们现在,要在第三个位置插入一个一个车厢的话,首先我们就需要让客人1号和客人2号的车厢互相断开。
然后让客人1号车厢的后门与客人6号的前门互相连接,然后让客人6号的后门和客人2号的前门相连,这样,客人6号就成功插入到了客人1号和客人2号之间。
因为本次插入车厢的操作并没有影响最开始的车厢和最后面的车厢,所以head和last这两个属性就不需要发生变化了。
到这里位置,关于增加元素的操作,我们都讲的差不多了,现在补一下代码的实现和分析~
C
void add(LinkedList* list,int value){
addLast(list,value);
}
void addAtIndex(LinkedList* list,int index,int value){
//判断索引是否合法
int size = list->size;
if( index>=0 && index<=size ){
if(index == size)
addLast(list,value);
else if(index == 0)
addHead(list,value);
else
addMiddle(list,index,value);
}else{
printf("索引不合法");
}
}
void addHead(LinkedList* list,int value){
Node *f = list->head;
//创建一个新节点
Node *newNode = createNode(NULL,value,f);
//更新链表中的头节点
list->head = newNode;
if(f == NULL) //初始化处理
list->last = newNode;
else
f->prev = newNode;
list->size++;
}
void addMiddle(LinkedList* list,int index,int value){
//拿到index索引处的节点
Node *target = getNode(list,index);
Node *pred = target->prev;
//创建一个新节点
Node *newNode = createNode(pred,value,target);
//原来index索引处的节点往后移动了一位,并且要让他的上一个节点成为新创建的节点
//这样我们创建的新节点就插入到了原来的index处,原来index处的索引往后移动了一个位置
target->prev = newNode;
if(pred == NULL)
list->head = newNode;
else
pred->next = newNode;
list->size++;
}
void addLast(LinkedList* list,int value){
Node *l = list->last;
//创建一个新节点
Node *newNode = createNode(l,value,NULL);
//更新链表中的头节点
list->last = newNode;
if(l == NULL) //初始化处理
list->head = newNode;
else
l->next = newNode;
list->size++;
}
Java
public void add(int value){
addLast(value);
}
public void add(int index,int value){
//判断索引是否合法
if( index>=0 && index<=size ){
if(index == size)
addLast(value);
else if(index == 0)
addHead(value);
else
addMiddle(index,value);
}else{
System.out.println("索引不合法");
}
}
private void addHead(int value){
Node f = head;
//创建一个新的节点
Node newNode = new Node(null,value,f);
//更新链表中的头节点
head = newNode;
if(f == null) //初始化处理
last = newNode;
else
f.prev = newNode;
//更新链表长度
size++;
}
private void addMiddle(int index,int value){
//拿到index索引处的节点
Node target = getNode(index);
Node pred = target.prev;
//创建一个新节点
Node newNode = new Node(pred,value,target);
//原来index索引处的节点往后移动了一位,并且要让他的上一个节点成为新创建的节点
//这样我们创建的新节点就插入到了原来的index处,原来index处的索引往后移动了一个位置
target.prev = newNode;
if(pred == null)
head = newNode;
else
pred.next = newNode;
size++;
}
private void addLast(int value){
Node l = last;
Node newNode = new Node(l,value,null);
last = newNode;
if(l == null) //初始化处理
head = newNode;
else
l.next = newNode;
size++;
}
以Java代码做例子,提供给外部使用的方法只有两个,分别是add(int value)方法和add(int index,int value)方法。
- add(int value) 方法,该方法默认将元素插入到链表的末尾。在内部,我封装了一个
addLast(int value)方法,该方法是将元素插入到链表的末尾。 - add(int index,int value) 方法,该方法是能够指定元素的插入位置,在判断index这个索引合法的情况下,会根据需要插入的位置,会调用不同的方法来实现。
- addHead(int value) 方法,该方法用作将元素插入到链表的头部。
- addMiddle(int index,int value) 方法,该方法会根据index将对应的元素插入到指定的位置。
- addLast(int value)方法,该方法用作将元素插入到链表的末尾。
现在主要分析add(int index,int value)及其调用的方法方法。
先来看一下addHead(int value)方法
private void addHead(int value){
Node f = head;
//创建一个新的节点
Node newNode = new Node(null,value,f);
//更新链表中的头节点
head = newNode;
if(f == null) //初始化处理
last = newNode;
else
f.prev = newNode;
//更新链表长度
size++;
}
- 首先是
Node f = head;,这代表我们创建了一个临时的指针,该指针目前存放的就是我们链表中head属性的地址,我们拿到这个地址,会在后续进行头部插入的操作,所以我们要拿到head属性的地址来使用。 - 紧接着是
Node newNode = new Node(null,value,f);,这句代表我们使用了Node的构造方法,创建了一个 头节点为null,节点的元素为通过形参传递进来的value,尾节点为f的节点。为什么尾节点是f呢?f就是我们上一个语句拿到的,当前链表中的头节点,也就是说,我们创建的这个节点,它的下一个节点就是我们当前链表中的头节点。也就是我们新创建的这个节点,这样新创建的节点就可以根据next指针中保存的地址,访问到我们当前链表中的头节点f。 - 然后是
head = newNode;,代表更新我们链表中的头节点 为 我们新创建的节点,也就是说,现在链表中的head属性,他保存的地址就是我们新创建的这个节点的地址,该节点就成为了链表中的头节点
if(f == null) //初始化处理
last = newNode;
else
f.prev = newNode;
-
这里会先判断,我们之前拿到的头节点f是不是为空,如果拿到的头节点是空的,代表链表中还没有元素,头节点和尾节点都是空的。
- 既然头节点和尾节点是空的,那么链表中就是没有元素,链表中没有元素,当前我们新创建的节点,我们已经让他成为了头节点。
- 但是链表中只有一个元素,所以我们的头节点记录的地址应该是和尾节点一样的,也就是头节点和尾节点同时指向同一个地址。所以我们让last 也保存 newNode这个节点的地址。
- 同样,既然我们插入的这个节点是链表中的唯一一个节点,那么相对应的,f是空(因为我们插入的时候链表中没有元素),所以他的prev指针,也就是指向上一个节点的指针是为空的,也就是该节点的prev指针没有保存有地址,next指针也是同理。
- 既然头节点和尾节点是空的,那么链表中就是没有元素,链表中没有元素,当前我们新创建的节点,我们已经让他成为了头节点。
-
如果f不是空的,代表f保存了一个节点的地址,也就是了链表中存在起码一个元素。
- 我们直接让f这个节点的地址的prev指针,指向我们新创建的节点newNode。
- 让newNode的next指针指向了f,f的prev指针指向了newNode,这两个节点就相互连接起来了。
注意哦,创建了新的节点之后,因为该方法是将我们新创建的节点插入到头部的方法,所以我们也要同时更新head这个属性。
- 我们直接让f这个节点的地址的prev指针,指向我们新创建的节点newNode。
-
最后,还剩一句
size++;,size这个属性我们之前说过,是用来记录链表的长度。这里在特殊声明一下:
size是链表的长度,不是索引,索引是从0开始,索引0代表链表中的第一个元素,但是size=1,那么就代表链表中只有一个元素,该元素的索引是0。
再看一下addMiddle(int index,int value)方法
private void addMiddle(int index,int value){
//拿到index索引处的节点
Node target = getNode(index);
Node pred = target.prev;
//创建一个新节点
Node newNode = new Node(pred,value,target);
//原来index索引处的节点往后移动了一位,并且要让他的上一个节点成为新创建的节点
//这样我们创建的新节点就插入到了原来的index处,原来index处的索引往后移动了一个位置
target.prev = newNode;
if(pred == null)
head = newNode;
else
pred.next = newNode;
size++;
}
getNode(int index);,这个方法是得到指定索引(index)处的节点,该方法在查询元素那块讲解,这里我们只需要记住该方法能够拿到指定索引处的节点即可,比如说链表中有三个节点,那么链表的长度就是3,要拿到中间的那个节点,中间元素的索引就是1,我们使用getNode(1)就能拿到链表中 中间的那个节点了。所以Node target = getNode(index)就拿到了index索引处的节点的地址,然后把这个地址给了target,这样target就能访问到index索引处的节点了。Node pred = target.prev,这里拿到 我们通过getNode(int index)方法拿到的节点的prev地址,也就是我们index索引处节点地址 的 上一个节点的地址信息。Node newNode = new Node(pred,value,target);,这里我们继续创建一个新的节点,这个新创建的节点的上一个节点的地址是pred,也就是我们index索引处的上一个节点的地址,然后该节点的元素为value,该节点的下一个节点的地址是target,也就是我们index索引处的节点。target.prev = newNode;,让原本index索引处的节点的上一个节点,变成我们新创建的节点。也就是让原本index索引处的节点与我们新创建的节点相连。- 接下来是经典的判空操作,简单,但重要。
if(pred == null)
head = newNode;
else
pred.next = newNode;
-
- 这里判断pred是否为空,是为了处理我们根据索引拿到的节点是头节点的情况。如果pred为空了,代表我们拿到的就是头节点了。
- 为什么是头节点呢?我们知道,链表中的节点总是有pred和next,pred如果为空,代表他前面没有元素,所以他就是链表中的头节点。next为空,代表该节点后面没有节点了,所以他就是链表中的尾节点。
- 既然pred这个节点是头节点了,代表我们通过index索引拿到的节点就是头节点,那么我们这个方法,是要将新创建的元素添加到index索引处的节点的前面的,也就是说,我们新创建的节点,替代了原先的头节点成为了新的头节点,所以我们就需要更新head这个属性。
- PS:只要操作涉及到head和last, 就必须进行更新,这是链表有序的保障。
-
如果pred不为空,则代表链表中存在至少一个节点,也代表我们拿到的这个节点不是头节点。
- 既然我们通过index索引拿到的节点不是头节点,那么我们直接让index索引处的上一个节点pred 的next指针,指向我们新创建的newNode节点,这样我们新创建的节点,就和原先index索引处节点的上一个节点互相连接起来了。
- 我们上面也操作pred指针处的节点(0x0010)和我们新创建的节点newNode(0x2345)相连了,至此,完成了链表中的节点插入到指定索引操作。
- 最后
size++;,操作完成不要忘记更新链表的长度了,这也是个很重要要的属性,他是我们链表中所有操作前提的保障。
- 既然我们通过index索引拿到的节点不是头节点,那么我们直接让index索引处的上一个节点pred 的next指针,指向我们新创建的newNode节点,这样我们新创建的节点,就和原先index索引处节点的上一个节点互相连接起来了。
至此,具体的操作方法我们已经说完了,addLast(int value)这个方法就留给大家自己思考~
接下来就是我们的add(int index,int value)这个方法了
public void add(int index,int value){
//判断索引是否合法
if( index>=0 && index<=size ){
if(index == size)
addLast(value);
else if(index == 0)
addHead(value);
else
addMiddle(index,value);
}else{
System.out.println("索引不合法");
}
}
- 该方法首先判断我们的索引是否合法,即索引的范围
index>=0 && index<=size,index>=0大家都理解的,索引是从0开始的,所以index必须大于等于0,那为什么index可以小于等于size呢? 我们通常通过索引和长度做判断不都是index<size吗? - 这里因为允许尾部插入,所以当index == size的时候,我们就让他执行
addLast(int value)方法 ,让该方法将元素插入到链表的末尾。 - 如果index != size,那么我们就在进行一次判断,判断index是不是0,如果是0,那么代表需要将包含value这个元素的节点插入到0索引的位置,也就是将该节点插入到链表的头部。所以这里就直接调用
addHead(int value)方法了。 - 如果及不满足index == size,也不满足index == 0,就代表着我们需要将包含value的元素插入到指定的索引处,我们上面也写了一个方法来帮我们完成操作,所以我们直接调用了
addMiddle(int index,int value)方法即可。 - 最后,
add(int index,int value)是必须判断index索引是否合法,合法的时候才会往下判断需要调用哪个方法来进行操作,如果index索引不合法,就在控制台输出相应信息了~索引不合法的处理大家也可以根据需求来更改。
最后这个add(int value)方法调用的addLast(int value)方法就交给大家进行测验,看看自己掌握的怎么样~
public void add(int value){
addLast(value);
}
private void addLast(int value){
Node l = last;
Node newNode = new Node(l,value,null);
last = newNode;
if(l == null) //初始化处理
head = newNode;
else
l.next = newNode;
size++;
}
删除元素
故事中,我们的002号车厢的客人被警察逮捕了,于是002号车厢就空出来了,我们就把002号车厢扔掉了。
我们在回顾一下我们扔掉002号车厢的操作,首先断开002号车厢前门和001号车厢后门连接的部分,在断开002号车厢后门与003号车厢前面连接的部分,然后再让001号车厢后门和003号前面相连,这样001号车厢就和003号车厢互相连接了,002号车厢就被我们抛弃啦。
删除其实跟增加差不多,也可以分成三种情况讨论,分别是删除头节点,删除尾节点,删除其他节点,写起来也蛮简单的。
C
void removeNode(LinkedList* list,int index){
//检查索引是否合法
int size = list->size;
if( index>=0 && index<size ){
//拿到需要删除的元素,让他的上一个元素直接连接到他的下一个元素即可
Node *killNode = getNode(list,index);
Node *prev = killNode->prev;
Node *next = killNode->next;
//需要做判空处理
if(prev == NULL)
list->head = next;
else
prev->next = next;
if(next == NULL)
list->last = prev;
else
next->prev = prev;
list->size--;
}else{
printf("索引不合法");
}
}
Java
public void remove(int index){
//检查索引是否合法
if( index>=0 && index<size ){
//拿到需要删除的元素,让他的上一个元素直接连接到他的下一个元素即可
Node killNode = getNode(index);
Node prev = killNode.prev;
Node next = killNode.next;
//需要做判空处理
if(prev == null)
head = next;
else
prev.next = next;
//需要做判空处理
if(next == null)
last = prev;
else
next.prev = prev;
size--;
}else{
System.out.println("索引不合法");
}
}
- 同样需要检查索引是否合法,index必须小于size,因为index作为索引,是肯定比size少一个,如果index==size,需要删除的元素就不在链表中了(没有该节点)。
- 首先是通过
Node killNode = getNode(int index)拿到需要删除的那个节点。 - 然后
Node prev = killNode.prev;,
Node next = killNode.next;记录需要删除节点的上一个节点和下一个节点,方便他们进行互相连接 - 之后就是经典的判空处理了,简单,但重要。这是需要删除的节点(killNode)的上一个节点的判空操作
//需要做判空处理
if(prev == null)
head = next;
else
prev.next = next;
-
- 首先要判断prev,也就是我们需要删除的那个节点的上一个节点。如果prev为空,那么代表我们需要删除的节点killNode是头节点,我们就直接让killNode的下一个节点成为头节点。这样就相当于删除掉了头节点,让原先的头节点的下一个节点成为了头节点。
- 如果prev不为空,直接让prev的下一个元素指向killNode的下一个节点,也就是next
- 接下来就是需要删除的节点(killNode)的下一个节点的判空操作了。
//需要做判空处理
if(next == null)
last = prev;
else
next.prev = prev;
-
- 同上面一样,同样判断next是否为空,如果为空,就代表killNode节点是尾节点,我们将尾节点删除,让尾节点的前一个元素prev成为新的尾节点,这样就更新了尾节点
- 如果next不为空,则正常进行连接操作,让killNode的下一个节点的prev属性,指向killNode的上一个节点,这样killNode的上下两个节点就互相连接在一起了。
如图所示,这样元素为value的节点就和元素为5的节点互相连接起来了,通过元素为value的next指针,就能访问到元素为5的节点了,虽然元素为7的节点还存在,但是我们已经没有办法访问到元素为7的节点了。所以我们说元素为7的节点已经被删除(扔掉)了。
修改元素
这个是链表操作中最容易理解的一个操作了。
假设故事中警察并没有找到犯人,同时那位尊贵的乘客来了,他指名道姓需要002号车厢,那么我们就只能把002号车厢的客人请出去,让我们那位尊贵的客人坐进去。也就是说,我们需要从最开始的车厢往后走,走到002号车厢,或者从最后面往前走,走到002号车厢,把尊贵的客人带到002号车厢,同时把002号车厢的客人请走轰走。
C
void setNode(LinkedList* list,int index,int value){
//检查索引是否合法
int size = list->size;
if( index>=0 && index<size){
Node *target = getNode(list,index);
target->data = value;
}else{
printf("索引不合法");
}
}
Java
public void set(int index,int value){
//检查索引是否合法
if( index>=0 && index<size){
Node target = getNode(index);
target.data = value;
}else{
System.out.println("索引不合法");
}
}
- 这个大家应该也看得懂了,我们通过
getNode(int index)拿到需要修改值的节点,直接修改节点的data属性即可
查询元素
在故事中,第三站,警察要搜查我们车厢,把犯人抓回去。
那么我们为了配合警察的工作,告诉了他们最前面的车厢和最后面的车厢的地址,这样他们就可以从最前面和最后面一起往中间搜查,这样排查能够防止遗漏,同时又全面~
但是我们只是一个人,没有办法同时从最后一节车厢和最前面一节车厢搜索,我们只能从一个方向出发,如果我们能够知道要搜查的是哪个客人的车厢,就能够判断是从最后一节车厢进去快,还是从最前面一节车厢进去快了。
从链表中看,也就是我们需要一个方法,能够根据索引,拿到对应的节点
C
//获取指定索引处的节点
Node* getNode(LinkedList* list,int index){
Node *target;
int size = list->size;
if(index< (size>>1) ){
target = list->head;
int i;
for(i = 0;i<index;i++){
target = target->next;
}
return target;
}else{
target = list->last;
int i;
for(i = size-1;i>index;i--){
target = target->prev;
}
return target;
}
}
int get(LinkedList* list,int index){
int size = list->size;
if(index>=size || index < 0) return -1;
Node *res = getNode(list,index);
return res->data;
}
Java
public int get(int index){
if(index>=size || index < 0) return -1;
return getNode(index).data;
}
private Node getNode(int index){
Node target;
if(size<(index>>1)){ //如果需要查找的索引在链表的前半段,则从头节点往后查找
target = head;
for(int i = 0;i<index;i++){
target = target.next;
}
return target;
}else{ //否则从尾节点往前找
target = last;
for(int i = size-1;i>index;i--){
target = target.prev;
}
return target;
}
}
get(int index)这个方法判断索引是否合法,只有合法的索引才能通过索引拿到链表中的节点的元素。也就是当索引合法之后,才会调用getNode(int index),这里的getNode(int index)是封装起来的,私有的,外部无法访问,所以不用担心我们会调用到这个未经检查索引的getNode(int index)方法。getNode(int index)该方法将返回我们通过index索引找到的节点的地址,通过该地址,我们就可以访问到这个节点,对该节点进行操作。Node target,算是个指针,记录我们之后找到的节点的地址信息。index>>1相当于index/2,右移比直接/快一点。- 也就是我们直接判断,index是处于链表中的哪个位置,如果在前半段,就从头节点开始往后找。否则就从后面往前找。
- 循环遍历就是需要经过多少个位置能找到对应索引处的节点,这里就不展开说了。
target = head或者target = last,代表我们的target拿到的是头节点的地址,还是尾节点的地址,如果是头节点的地址,那么我们需要往下找的话,直接让target = target.next这个语句每指向一次,target就会往下找一个节点,target = target.prev同理,只不过这是从后往前开始找。
- PS:除了更新操作,其余时候千万不要操作head和last这两个属性哦,这两个属性只用作记录和更新,如果我们对其进行了操作,比如
head = head.next,那么链表中记录的头节点就往后移动了一位,相当于我们执行了一次删除头节点的操作。 - 因此,head和last我们只有在头尾节点发生变化的时候才进行更新。
完整代码
因为C不太熟悉,所以只在main里面创建了链表,相应方法都已经测试 (跑了lc的测试) 。
C
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <stdbool.h>
typedef struct Node
{
struct Node *prev; //指向上一个节点
int data; //该节点需要保存的元素
struct Node *next; //指向下一个节点
} Node;
typedef struct {
struct Node *head; //链表中的头节点
int size; //链表中的长度
struct Node *last; //链表中的尾节点
} LinkedList;
void addHead(LinkedList* list,int value);
void addMiddle(LinkedList* list,int index,int value);
void addLast(LinkedList* list,int value);
//初始化链表的函数
void initLinkedList(LinkedList* list)
{
list->head = NULL;
list->last = NULL;
list->size = 0;
}
//创建节点的函数
Node* createNode(Node *prev,int data,Node *next)
{
Node* node = (Node*)malloc(sizeof(Node));//申请内存
node->data = data;
node->prev = prev;
node->next = next;
return node;
}
//获取指定索引处的节点
Node* getNode(LinkedList* list,int index){
Node *target;
int size = list->size;
if(index< (size>>1) ){
target = list->head;
int i;
for(i = 0;i<index;i++){
target = target->next;
}
return target;
}else{
target = list->last;
int i;
for(i = size-1;i>index;i--){
target = target->prev;
}
return target;
}
}
int get(LinkedList* list,int index){
int size = list->size;
if(index>=size || index < 0) return -1;
Node *res = getNode(list,index);
return res->data;
}
void add(LinkedList* list,int value){
addLast(list,value);
}
void addAtIndex(LinkedList* list,int index,int value){
//判断索引是否合法
int size = list->size;
if( index>=0 && index<=size ){
if(index == size)
addLast(list,value);
else if(index == 0)
addHead(list,value);
else
addMiddle(list,index,value);
}else{
printf("索引不合法");
}
}
void addHead(LinkedList* list,int value){
Node *f = list->head;
//创建一个新节点
Node *newNode = createNode(NULL,value,f);
//更新链表中的头节点
list->head = newNode;
if(f == NULL) //初始化处理
list->last = newNode;
else
f->prev = newNode;
list->size++;
}
void addMiddle(LinkedList* list,int index,int value){
//拿到index索引处的节点
Node *target = getNode(list,index);
Node *pred = target->prev;
//创建一个新节点
Node *newNode = createNode(pred,value,target);
//原来index索引处的节点往后移动了一位,并且要让他的上一个节点成为新创建的节点
//这样我们创建的新节点就插入到了原来的index处,原来index处的索引往后移动了一个位置
target->prev = newNode;
if(pred == NULL)
list->head = newNode;
else
pred->next = newNode;
list->size++;
}
void addLast(LinkedList* list,int value){
Node *l = list->last;
//创建一个新节点
Node *newNode = createNode(l,value,NULL);
//更新链表中的头节点
list->last = newNode;
if(l == NULL) //初始化处理
list->head = newNode;
else
l->next = newNode;
list->size++;
}
void removeNode(LinkedList* list,int index){
//检查索引是否合法
int size = list->size;
if( index>=0 && index<size ){
//拿到需要删除的元素,让他的上一个元素直接连接到他的下一个元素即可
Node *killNode = getNode(list,index);
Node *prev = killNode->prev;
Node *next = killNode->next;
//需要做判空处理
if(prev == NULL)
list->head = next;
else
prev->next = next;
if(next == NULL)
list->last = prev;
else
next->prev = prev;
list->size--;
}else{
printf("索引不合法");
}
}
void setNode(LinkedList* list,int index,int value){
//检查索引是否合法
int size = list->size;
if( index>=0 && index<size){
Node *target = getNode(list,index);
target->data = value;
}else{
printf("索引不合法");
}
}
//创建链表
LinkedList* list;
LinkedList* createLinkedList() {
//申请内存
list = (LinkedList*)malloc(sizeof(LinkedList));
initLinkedList(list);
return list;
}
int main(){
//创建链表
LinkedList *list = createLinkedList();
//之后就自己测试啦~
return 0;
}
Java
public class LinkedList{
private int size; //记录链表的长度
private Node head; //链表中的头节点
private Node last; //链表中的尾节点
public LinkedList() {
size = 0;
head = null;
last = null;
}
//在链表类中声明一个内部类,该类只在LinkedList中使用
class Node{
Node prev; //上一个节点的地址
int data; //该节点保存的元素
Node next; //下一个节点的地址
//必须提供三个属性用作构造该节点
//即,必须说明 和这个节点相连接的上一个节点是谁,这个节点包含的元素是什么,下一个节点是谁
public Node(Node prev, int data, Node next) {
this.prev = prev;
this.data = data;
this.next = next;
}
}
public int get(int index){
if(index>=size || index < 0) return -1;
return getNode(index).data;
}
private Node getNode(int index){
Node target;
if(size<(index>>1)){ //如果需要查找的索引在链表的前半段,则从头节点往后查找
target = head;
for(int i = 0;i<index;i++){
target = target.next;
}
return target;
}else{ //否则从尾节点往前找
target = last;
for(int i = size-1;i>index;i--){
target = target.prev;
}
return target;
}
}
public void add(int value){
addLast(value);
}
public void add(int index,int value){
//判断索引是否合法
if( index>=0 && index<=size ){
if(index == size)
addLast(value);
else if(index == 0)
addHead(value);
else
addMiddle(index,value);
}else{
System.out.println("索引不合法");
}
}
private void addHead(int value){
Node f = head;
//创建一个新的节点
Node newNode = new Node(null,value,f);
//更新链表中的头节点
head = newNode;
if(f == null) //初始化处理
last = newNode;
else
f.prev = newNode;
//更新链表长度
size++;
}
private void addMiddle(int index,int value){
//拿到index索引处的节点
Node target = getNode(index);
Node pred = target.prev;
//创建一个新节点
Node newNode = new Node(pred,value,target);
//原来index索引处的节点往后移动了一位,并且要让他的上一个节点成为新创建的节点
//这样我们创建的新节点就插入到了原来的index处,原来index处的索引往后移动了一个位置
target.prev = newNode;
if(pred == null)
head = newNode;
else
pred.next = newNode;
size++;
}
private void addLast(int value){
Node l = last;
Node newNode = new Node(l,value,null);
last = newNode;
if(l == null) //初始化处理
head = newNode;
else
l.next = newNode;
size++;
}
public void remove(int index){
//检查索引是否合法
if( index>=0 && index<size ){
//拿到需要删除的元素,让他的上一个元素直接连接到他的下一个元素即可
Node killNode = getNode(index);
Node prev = killNode.prev;
Node next = killNode.next;
//需要做判空处理
if(prev == null)
head = next;
else
prev.next = next;
if(next == null)
last = prev;
else
next.prev = prev;
size--;
}else{
System.out.println("索引不合法");
}
}
public void set(int index,int value){
//检查索引是否合法
if( index>=0 && index<size){
Node target = getNode(index);
target.data = value;
}else{
System.out.println("索引不合法");
}
}
}
参考文献
[1] [美] Aditya Bhargava.算法图解[M].袁国忠译注.北京:人民邮电出版社,2017: 16-17.