【小白学算法】5.单链表,插入、读取
链表其实也就是 线性表的链式存储结构,与之前讲到的顺序存储结构不同。
我们知道顺序存储结构中的元素地址都是连续的,那么这就有一个最大的缺点:当做插入跟删除操作的时候,大量的元素需要移动。
如图所示,元素在内存中的位置是挨着的,当中有元素被删除,就产生空隙,于是乎后面的元素需要向前挪动去弥补。
正是因为顺序存储有这这个缺点,所以链式存储结构就变得非常的有意义。
一、链表的存储形式
首先,链表是有序的列表,但是在内存中它是这样存储的:
- head:这是头指针,是链表指向第一个结点的指针。无论链表是否为空,头指针均不为空。
- 结点:由data域和next域共同组成,前者存储数据元素本身,后者存储后继位置。
上图所示中,各个结点不一定是连续存放的,最终会有N个节点链接成一个链表,所以就成了链式存储结构。
另外,因为此链表的每个结点中只包含一个next域,所以叫单链表。
二、头指针和头结点
1.头指针
上面提到了头指针,它是链表的必要元素。
因为链表既然也是线性表,所以还是要有头有尾,头指针就是链表中第一个结点的存储位置。
而最后一个结点,指针指向空,通常用NULL表示或者'^'来表示。
2.头结点
与头指针不同,头结点是不一定要有的,得更具实际需求来定。
有时候为了更加方便的操作链表,会在单链表的第一个结点前设一个结点,称为头结点。
加了头结点后,对于第一结点来说,在其之前插入结点或者删除第一结点,操作方式就与其它的结点相同了,不需要进行额外的判断处理。
头结点跟其他结点不同,它的数据域可以不存储任何信息,有必要的话,可以存储一些其他的附加信息,比如线性表的长度等。
现在我们已经知道了单向链表的储存形式以及其构成有哪些,那么现在可以用更直观的图来展示单向链表中数据元素之间的关系了。
三、代码实现一个单链表
1.直接在链表尾部依次添加
比如,现在要用单链表来存储LOL里英雄的信息。如果不带英雄排名顺序的话,那么可以直接依次在链表的末尾增加新的结点即可。
package linkedlist;
public class SingleLinkedListDemo {
public static void main(String[] args) {
// 测试
HeroNode hero1 = new HeroNode(1, "易大师","无极剑圣");
HeroNode hero2 = new HeroNode(2, "李青","盲僧");
HeroNode hero3 = new HeroNode(3, "艾希","寒冰射手");
HeroNode hero4 = new HeroNode(4, "菲奥娜","无双剑姬");
// 创建链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
// 加入对象结点
singleLinkedList.addHero(hero1);
singleLinkedList.addHero(hero2);
singleLinkedList.addHero(hero3);
singleLinkedList.addHero(hero4);
// 显示链表内容
singleLinkedList.linkList();
}
}
// 定义SingleLinkedList 管理英雄
class SingleLinkedList {
// 初始化一个头结点,不要动这个结点。
private HeroNode headNode = new HeroNode(0, "","");
// 添加结点 到 单向链表
// 当不考虑英雄顺序时,找到当前链表的最后一个结点,再讲此结点的next指向新的结点即可
public void addHero(HeroNode heroNode) {
// 因为head结点不能动,所以新建一个临时变量,帮助遍历
HeroNode temp = headNode;
// 开始遍历链表,到最后,找最后的结点
while (true) {
// 等于null时就是最后了
if (temp.next == null) {
break;
}
// 否则就不是最后,将temp继续向后移动
temp = temp.next;
}
// 直到退出循环,此时temp就指向了链表的最后
// 将最后的结点指向这个新的结点
temp.next = heroNode;
}
// 显示链表内容的方法
public void linkList() {
// 判断链表是否为空,空的话就不用继续了
if (headNode.next == null) {
System.out.println("链表为空");
return;
}
HeroNode temp = headNode.next;
while (true) {
// 判断是否已经到了链表最后
if (temp == null) {
break;
}
// 输出结点信息
System.out.println(temp);
// 然后后移temp继续输出下一个结点
temp = temp.next;
}
}
}
// 定义HeroNode,每个HeroNode对象就是一个结点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; // 指向下一个结点
// 构造器
public HeroNode(int heroNo, String heroName, String heroNickname) {
this.no = heroNo;
this.name = heroName;
this.nickname = heroNickname;
}
// 为了方便显示,重写toString方法
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
运行一下
HeroNode{no=1, name='易大师', nickname='无极剑圣'}
HeroNode{no=2, name='李青', nickname='盲僧'}
HeroNode{no=3, name='艾希', nickname='寒冰射手'}
HeroNode{no=4, name='菲奥娜', nickname='无双剑姬'}
Process finished with exit code 0
可以看到,链表中的结点是按照添加的顺序依次储存的。
2.考虑顺序的情况下添加链表
上面每个英雄有自己的排名,那么如果我想不关心添加的顺序,在链表中最终都可以按照英雄的排名进行存储,如何实现呢?
这里的话就没有上面直接在末尾添加那么直接了,但是也不算难理解,看个示意图。
如图所示,现在有一个结点2要添加进来,那么来梳理一下实现的思路:
- 先要找到结点2应该添加到的位置,没错就是结点1与结点4之间
- 将结点1的next指向结点2,再将结点2的next指向结点4即可
是不是很简单,不过为了实现第2点,我们还是需要借助一个辅助变量temp,可以把它看作一个指针。
temp会从头开始遍历链表,来找到结点2应该添加到的位置,此时会停在结点1,那么:
- 结点2.next = temp.next,这样可以将结点2指向结点4
- temp.next = 结点2,这样可以将结点1指向结点2
这样我们的目的就达成了,代码也就知道怎么去改了。
决定在SingleLinkedList类中,增加一个新方法,可以跟据英雄的排名进行添加。
package linkedlist;
public class SingleLinkedListDemo {
public static void main(String[] args) {
// 测试
HeroNode hero1 = new HeroNode(1, "易大师","无极剑圣");
HeroNode hero2 = new HeroNode(2, "李青","盲僧");
HeroNode hero3 = new HeroNode(3, "艾希","寒冰射手");
HeroNode hero4 = new HeroNode(4, "菲奥娜","无双剑姬");
// 创建链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
// 加入对象结点
singleLinkedList.addByNo(hero1);
singleLinkedList.addByNo(hero4);
singleLinkedList.addByNo(hero2);
singleLinkedList.addByNo(hero3);
// 显示链表内容
singleLinkedList.linkList();
}
}
// 定义SingleLinkedList 管理英雄
class SingleLinkedList {
// 初始化一个头结点,不要动这个结点。
private HeroNode headNode = new HeroNode(0, "","");
// 添加结点 到 单向链表
// 当不考虑英雄顺序时,找到当前链表的最后一个结点,再讲此结点的next指向新的结点即可
public void addHero(HeroNode heroNode) {
// 因为head结点不能动,所以新建一个临时变量,帮助遍历
HeroNode temp = headNode;
// 开始遍历链表,到最后,找最后的结点
while (true) {
// 等于null时就是最后了
if (temp.next == null) {
break;
}
// 否则就不是最后,将temp继续向后移动
temp = temp.next;
}
// 直到退出循环,此时temp就指向了链表的最后
// 将最后的结点指向这个新的结点
temp.next = heroNode;
}
// 添加方法2:根据排名将英雄按照排名顺序依次放到对应位置
public void addByNo(HeroNode heroNode) {
// 借助temp遍历链表,找到添加位置的前一个结点
HeroNode temp = headNode;
// 考虑一种情况:当添加的位置已经存在对应排名的英雄,则不能添加
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.no > heroNode.no) { // 位置找到,在temp的后面添加
break;
} else if (temp.next.no == heroNode.no) { // 目标添加位置,已经存在对应编号,不能添加
flag = true;
break;
}
temp = temp.next; // 继续后移
}
// 跳出循环,进行添加操作
if (flag) {
System.out.printf("准备插入的英雄编号%d已存在,不可加入\n", heroNode.no);
} else {
// 可以正常插入到链表
heroNode.next = temp.next;
temp.next = heroNode;
}
}
// 显示链表内容的方法
public void linkList() {
// 判断链表是否为空,空的话就不用继续了
if (headNode.next == null) {
System.out.println("链表为空");
return;
}
HeroNode temp = headNode.next;
while (true) {
// 判断是否已经到了链表最后
if (temp == null) {
break;
}
// 输出结点信息
System.out.println(temp);
// 然后后移temp继续输出下一个结点
temp = temp.next;
}
}
}
// 定义HeroNode,每个HeroNode对象就是一个结点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; // 指向下一个结点
// 构造器
public HeroNode(int heroNo, String heroName, String heroNickname) {
this.no = heroNo;
this.name = heroName;
this.nickname = heroNickname;
}
// 为了方便显示,重写toString方法
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickname='" + nickname + '\'' +
'}';
}
}
在main方法中,我们打乱结点添加的顺序,运行一下,看看最终链表里是不是按照影响的排名顺序存储的
HeroNode{no=1, name='易大师', nickname='无极剑圣'}
HeroNode{no=2, name='李青', nickname='盲僧'}
HeroNode{no=3, name='艾希', nickname='寒冰射手'}
HeroNode{no=4, name='菲奥娜', nickname='无双剑姬'}
Process finished with exit code 0
结果正确,符合预期,不管先添加谁,最终在链表里都是按照英雄的排名来存放。
继续测试,我重复添加结点3,看下会如何。
// 加入对象结点
singleLinkedList.addByNo(hero1);
singleLinkedList.addByNo(hero4);
singleLinkedList.addByNo(hero2);
singleLinkedList.addByNo(hero3);
singleLinkedList.addByNo(hero3);
运行一下:
准备插入的英雄编号3已存在,不可加入
HeroNode{no=1, name='易大师', nickname='无极剑圣'}
HeroNode{no=2, name='李青', nickname='盲僧'}
HeroNode{no=3, name='艾希', nickname='寒冰射手'}
HeroNode{no=4, name='菲奥娜', nickname='无双剑姬'}
Process finished with exit code 0
提示了已经存在了,不可加入。
四、总结
本文内容介绍了单链表的构成,另外代码中也涉及到了单链表的读取、插入。
读取,说白了还是遍历。
由于单链表的结构并没有定义好表的长度,所以不方便用for循环来操作了,因为你不知道要循环多少次。
读取的重点还是在于“指针后移”,从头开始,一个个的找,直到找到你要取的元素。
所以说,单链表在读取方便并没有啥优势。
插入的操作,就明显好很多了,因为不需要惊动其他结点,只要将目标位置的前后2个结点的指针做下调整即可。
下面会继续单链表的修改和删除等。