数据结构和算法 —— 链表(一)单链表的简介及实例应用
简介
概念
- 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
- 链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
特点
- 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
- 使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
- 链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。
- 链表有很多种不同的类型:单向链表,双向链表以及循环链表。链表可以在多种编程语言中实现。像 Lisp 和 Scheme 这样的语言的内建数据类型中就包含了链表的存取和操作。程序语言或面向对象语言,如 C , C++ 和 Java 依靠易变工具来生成链表。
小结
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域:指向下一个节点.
- 如下图:发现链表的各个节点不一定是连续存储.
- 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定
内存布局
链表是有序的列表,但是它在内存中是存储如下:
单链表(带头结点) 逻辑结构 示意图如下:
应用实例
使用带head头的单向链表实现 –水浒英雄排行榜管理,完成对英雄人物的增删改查操作
- 第一种方法在添加英雄时,直接添加到链表的尾部
- 第二种方式在添加英雄时,根据排名将英雄插入到指定位置(如果有这个排名,则添加失败,并给出提示)
一、单链表插入结点
第一种实现方式(直接添加到链表的尾部)
创建结点类
//英雄结点
public class HeroNode {
public int no;
public String name;
public String nickName;
public HeroNode next;
//构造方法
public HeroNode(int hNo, String hName, String hNickName) {
this.no = hNo;
this.name = hName;
this.nickName = hNickName;
}
@Override
public String toString() {
return "HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
}
创建单链表类
//英雄结点集合
public class SingleLinkedList {
//创建头结点
private HeroNode head = new HeroNode(0, "", "");
//第一种添加方式:不考虑添加时的编号顺序,直接添加结点到单链表的末尾
public void addToEnd(HeroNode heroNode) {
//分析
//要把给定的结点 heroNode 添加到单链表的末尾,需要先找到单链表末尾的结点是哪个
HeroNode temp = head;
while (true) {
//当结点中的 next 属性是空的时候,说明它就是单链表中最后的结点
if (temp.next == null) {
//将该结点的指向下一个结点
temp.next = heroNode;
break;
}
//如果不是最后的结点
temp = temp.next;
}
}
//显示链表(遍历)
public void list(){
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空!");
return;
}
//分析
//怎么打印出链表中所有的结点到控制台
//1、根据什么来遍历单链表?
//我们想到的就是根据头结点来遍历,因为头结点是不动的,而且头结点中存放着下一个结点的信息
//只要结点中的 next 属性不是空,就直接打印,知道 next 为空为止
//2、需要一直找到链表的末尾,只要没有找到链表的末尾,就一直找,找到就打印
HeroNode temp = head;
while (temp != null){//说明不知最后的结点
System.out.println(temp);
//后移
temp = temp.next;
}
}
}
测试类
public class TestSingleLinkedList {
public static void main(String[] args) {
//先创建几个结点
HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星");
HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头");
//创建单链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
//向单链表中添加结点
singleLinkedList.addToEnd(heroNode1);
singleLinkedList.addToEnd(heroNode4);
singleLinkedList.addToEnd(heroNode2);
singleLinkedList.addToEnd(heroNode3);
//查看单链表
singleLinkedList.list();
}
}
结果打印
- 从程序和打印结果的对比分析,添加过后单链表中结点的顺序是 “1,4,2,3” ,上边这种实现方式,并没有按照 “1,2,3,4” 的大小顺序来排列。
- 下面我们就来介绍另一种添加的方法 ↓ ,可以根据排名将英雄插入到指定位置,如果有这个排名,则添加失败,并给出提示。
第二种实现方式(根据排名插入到指定位置)
- 根据排名将英雄插入到指定位置
- 如果有这个排名,则添加失败,并给出提示
代码
在单链表类 SingleLinkedList 中添加下面的方法:
//第二种添加方式,在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode heroNode) {
//我们需要先确定下来新增的这个结点会添加到单链表的哪个位置
//需要根据 temp 来遍历,然后实现
//头结点是不能动的,需要通过一个辅助指针(变量)temp
//因此我们的 temp 是位于添加位置之前的一个结点,否则插入不了
HeroNode temp = head;
Boolean flag = false;//flag表示添加的节点是否存在
//开始遍历
while (true) {
if (temp.next == null) {//当前的结点就是最后一个结点
break;
}
if (heroNode.no < temp.next.no) {
break;
} else if (heroNode.no == temp.next.no) {//说明要添加的结点的编号已经存在
flag = true;
break;
}
temp = temp.next; //后移,遍历当前链表
}
//判断flag的值
if (flag) {
System.out.printf("准备插入的结点的编号 %d 已经存在了, 不能重复加入! \n", heroNode.no);
} else {
//插入到temp后面
//然后 heroNode.next = temp.next
//temp.next = heroNode
heroNode.next = temp.next; //注意这两行代码不能替换位置,如果替换了位置就是一个死循环
temp.next = heroNode;
}
}
测试类
public class TestSingleLinkedList {
public static void main(String[] args) {
//先创建几个结点
HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星");
HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头");
//创建单链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
//向单链表中添加结点
// singleLinkedList.addToEnd(heroNode1);
// singleLinkedList.addToEnd(heroNode4);
// singleLinkedList.addToEnd(heroNode2);
// singleLinkedList.addToEnd(heroNode3);
//addByOrder
singleLinkedList.addByOrder(heroNode1);
singleLinkedList.addByOrder(heroNode4);
singleLinkedList.addByOrder(heroNode2);
singleLinkedList.addByOrder(heroNode3);
//查看单链表
singleLinkedList.list();
}
}
结果打印
二、单链表结点的修改
代码
在单链表类 SingleLinkedList 中添加下面的方法:
//修改结点的信息,注意:修改结点的时候,需要根据 no 来修改,否则,将会是添加操作
public void update(HeroNode heroNode) {
//先判断新添加的结点是否为空
HeroNode temp = head;
if (temp.next == null) {
System.out.println("您要修改的链表为空!");
}
Boolean flag = false;
//遍历单链表, 找到要修改的结点
while (true) {
if (temp.next == null) {//链表已经遍历完
break;
} else {
if (temp.next.no == heroNode.no) {//在单链表中找到了和目标元素 heroNode 中的 no 属性值一样的结点
//说明要修改的就是这个结点
flag = true;
break;
}
}
temp = temp.next;//指向下一个结点,继续遍历单链表
}
if (flag) {
//开始修改
temp.next.name = heroNode.name;
temp.next.nickName = heroNode.nickName;
} else {
System.out.printf("没有找到编号为 %d 的结点,修改失败! \n", heroNode.no);
}
}
测试类
public class TestSingleLinkedList {
public static void main(String[] args) {
//先创建几个结点
HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星");
HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头");
//创建单链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
//向单链表中添加结点
// singleLinkedList.addToEnd(heroNode1);
// singleLinkedList.addToEnd(heroNode4);
// singleLinkedList.addToEnd(heroNode2);
// singleLinkedList.addToEnd(heroNode3);
//addByOrder
singleLinkedList.addByOrder(heroNode1);
// singleLinkedList.addByOrder(heroNode4);
singleLinkedList.addByOrder(heroNode4);
singleLinkedList.addByOrder(heroNode2);
singleLinkedList.addByOrder(heroNode3);
//查看单链表
singleLinkedList.list();
//修改时创建新的结点
HeroNode heroNodeForUpdate = new HeroNode(2, "小卢", "玉麒麟~~~");
singleLinkedList.update(heroNodeForUpdate);
System.out.println("修改后链表的情况: ");
singleLinkedList.list();
}
}
结果打印
三、单链表结点的删除
代码
在单链表类 SingleLinkedList 中添加下面的方法:
//删除单链表中指定的结点
public void delete(int no) {
//指定辅助结点
HeroNode temp = head;
Boolean flag = false;//表示要删除的结点是否存在
//遍历单链表
while (true) {
if (temp.next == null) {//链表已经遍历完
break;
}
if (no == temp.next.no) {//找到了要删除的结点的位置
flag = true;
break;
}
temp = temp.next;
}
if (flag) {//找到
//找到要删除的结点的位置,然后让该结点位置的上一个位置的结点指向该结点位置的下一个结点
//开始删除
temp.next = temp.next.next;
} else {
System.out.printf("没有找到编号为 %d 的结点,删除失败! \n", no);
}
}
测试类
public class TestSingleLinkedList {
public static void main(String[] args) {
//先创建几个结点
HeroNode heroNode1 = new HeroNode(1, "宋江", "及时雨");
HeroNode heroNode2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode heroNode3 = new HeroNode(3, "吴用", "智多星");
HeroNode heroNode4 = new HeroNode(4, "林冲", "豹子头");
//创建单链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
//向单链表中添加结点
// singleLinkedList.addToEnd(heroNode1);
// singleLinkedList.addToEnd(heroNode4);
// singleLinkedList.addToEnd(heroNode2);
// singleLinkedList.addToEnd(heroNode3);
//addByOrder
singleLinkedList.addByOrder(heroNode1);
// singleLinkedList.addByOrder(heroNode4);
singleLinkedList.addByOrder(heroNode4);
singleLinkedList.addByOrder(heroNode2);
singleLinkedList.addByOrder(heroNode3);
//查看单链表
System.out.println("修改前链表的情况: ");
singleLinkedList.list();
//修改时创建新的结点
HeroNode heroNodeForUpdate = new HeroNode(2, "小卢", "玉麒麟~~~");
singleLinkedList.update(heroNodeForUpdate);
System.out.println("修改后链表的情况: ");
singleLinkedList.list();
//删除一个结点
singleLinkedList.delete(1);
singleLinkedList.delete(2);
singleLinkedList.delete(3);
singleLinkedList.delete(4);
singleLinkedList.delete(4);
System.out.println("删除后链表的情况: ");
singleLinkedList.list();
}
}