链表其实也就是 线性表的链式存储结构,与之前讲到的顺序存储结构不同。

我们知道顺序存储结构中的元素地址都是连续的,那么这就有一个最大的缺点:当做插入跟删除操作的时候,大量的元素需要移动。

如图所示,元素在内存中的位置是挨着的,当中有元素被删除,就产生空隙,于是乎后面的元素需要向前挪动去弥补。



正是因为顺序存储有这这个缺点,所以链式存储结构就变得非常的有意义。

一、链表的存储形式

首先,链表是有序的列表,但是在内存中它是这样存储的:

  • 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要添加进来,那么来梳理一下实现的思路:

  1. 先要找到结点2应该添加到的位置,没错就是结点1与结点4之间
  2. 将结点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

提示了已经存在了,不可加入。

下面会继续单链表的修改和删除等。

【小白学算法】5.链表(linked list)、链表的添加的更多相关文章

  1. 深夜学算法之SkipList:让链表飞

    1. 前言 上次写Python操作LevelDB时提到过,有机会要实现下SkipList.摘录下wiki介绍: 跳跃列表是一种随机化数据结构,基于并联的链表,其效率可比拟二叉查找树. 我们知道对于有序 ...

  2. 数据结构与算法 —— 链表linked list(01)

    链表(维基百科) 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的指针(Pointer).由于不必须按顺序存储, ...

  3. 数据结构与算法 —— 链表linked list(03)

    继续关于linked list的算法题: 删除排序链表中的重复元素 给定一个排序链表,删除所有重复的元素使得每个元素只留下一个. 案例: 给定 1->1->2,返回 1->2 给定  ...

  4. 数据结构与算法 —— 链表linked list(06)

    回文链表 链接 请检查一个链表是否为回文链表. 进阶:你能在 O(n) 的时间和 O(1) 的额外空间中做到吗? 解题思路: 回文链表的特点就是对称. 把链表放到栈中去,利用栈的先进后出的规则,和原链 ...

  5. 算法与数据结构基础 - 链表(Linked List)

    链表基础 链表(Linked List)相比数组(Array),物理存储上非连续.不支持O(1)时间按索引存取:但链表也有其优点,灵活的内存管理.允许在链表任意位置上插入和删除节点.单向链表结构一般如 ...

  6. 数据结构与算法——链表 Linked List(单链表、双向链表、单向环形链表-Josephu 问题)

    链表是有序的列表,但是在内存中存储图下图所示 链表是以 节点 的方式来存储,是 链式存储 每个节点包含 data 域.next 域,指向下一个节点 链表的各个节点 不一定是连续存储,如上图所示 链表还 ...

  7. 数据结构与算法 —— 链表linked list(02)

    我们继续来看链表的第二道题,来自于leetcode: 两数相加 给定两个非空链表来代表两个非负整数,位数按照逆序方式存储,它们的每个节点只存储单个数字.将这两数相加会返回一个新的链表. 你可以假设除了 ...

  8. 数据结构与算法 —— 链表linked list(05)

    反转一个单链表. 进阶:链表可以迭代或递归地反转.你能否两个都实现一遍? 示例 : 给定这个链表:1->2->3->4->5 返回结果: 5->4->3->2 ...

  9. LeetCode 141. 环形链表(Linked List Cycle) 19

    141. 环形链表 141. Linked List Cycle 题目描述 给定一个链表,判断链表中是否有环. 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 ...

随机推荐

  1. Linux的进程权限控制

    Linux系统的安全性得益于其进程权限和文件权限的控制机制.今天抽空梳理下Linux下的进程权限控制相关的文件权限涉及一点.首先明确四个名词:真实用户ID(real ID).有效用户ID(effect ...

  2. Linux 如何查看一个文件夹下面有多少个文件

    Linux 如何查看一个文件夹下面有多少个文件 $ tree $ find ./ -type f | wc -l $ ls -l | grep "^-" | wc -l refs ...

  3. SVG tada 🎉 animation effects

    SVG tada animation effects symbol & use <svg viewBox="0 0 80 20" xmlns="http:/ ...

  4. macOS 升级后导致 dart bug

    macOS 升级后导致 dart bug macOS 10.15.5 $ demo_app git:(master) flutter doctor # /Users/xgqfrms-mbp/Docum ...

  5. SVG & convert polygon/polyline to path

    SVG & convert polygon/polyline to path SVG Polygon/Polyline to Path Converter https://codepen.io ...

  6. 记录PyQt5 学习中遇到的一些问题

    1   信号与槽的设置中,槽函数不用写括号: btn.clicked.connect(cao()) def cao(): ******** 会报错:argument 1 has unexpected ...

  7. java: 类 RegisterController 是公共的, 应在名为 RegisterController.java 的文

    public声明的类名需要和文件名一致,检查一下

  8. .NET测试--模拟框架NSubstitute

    .NET测试--模拟框架NSubstitute .NET测试 NSubstitute在GitHub的开源地址:https://github.com/nsubstitute/nsubstitute/do ...

  9. Mybatis初步认识

    分三层 第一章 1.三层架构 界面层:和用户打交道,接收用户的请求参数明显是处理结果的(jsp,html,servlet) 业务逻辑层:接收了界面层传递的数据,计算逻辑,调用数据库,获取数据 数据访问 ...

  10. Hi3559AV100 SDK的详细安装过程及问题解决方法

    下面给出Hi3559AV100 SDK的安装的详细步骤(一些注意事项可以参照我之前写的随笔-<Hi3519 SDK搭建.问题总结及yolov3 RFCN的运行结果与测试 >): 1.开发环 ...