链表LinkedList
链表LinkedList
1. 链表介绍
- 链表是有序的列表,它是数据结构中树结构和图结构的基础。
- 链表是以节点的方式存储的,是链式存储。每个节点包含data域和next域,data域用于存储数据,next是指针域用于指向下一个节点。
- 链表在内存中不一定是连续存储的。
- 链表分为带头节点的链表和不带头节点的链表,可根据实际开发需求确定是否需要头节点。
- 头节点的data域可以不存储任何信息(null),也可以存储链表长度等附加信息;指针域存储指向首元节点的指针。
- 头节点的引入可以方便链表的操作,如对首元节点进行插入、删除元素等操作时,方便其与其他节点的逻辑是统一的。
- 头指针指的是指向物理存储的第一个节点地址的指针(也即链表名),其意义在于访问链表时总要知道链表存储在什么位置。
- 若链表中有头节点,则头指针时指向头节点的指针。
- 链表中可以没有头节点,但一定不能没有头指针,若没有头节点,则头指针指向首元节点。
- 链表是一个真正的动态数据结构,而数组不是,即使可以通过后天补充使得数组具备动态扩容的能力,但数组这个数据结构是与生俱来的,链表则不同,向链表中添加元素不需要判断链表是否已满。理想状态下链表是不会满的,只有当内存被占用完时链表才会由于添加不了元素而出现堆溢出的情况,但此时链表其实还是没有满的,只是内存空间不够大了。
2. 单向链表
2.1 带头节点的单链表在内存中的实际结构示意图

2.2 带头节点的单链表的逻辑结构示意图

注:逻辑上看是有序的,实际内存是无序的。
2.3 单链表的创建与插入
2.3.1 直接插入
以梁山108将为例,直接插入就是将数据按插入顺序插入到链表的尾部,不考虑英雄排行顺序(即编号顺序)。

思路如下:
- 添加(创建):先创建head头节点,其后每创建一个节点就直接加入到链表的最后。
- 遍历:通过一个辅助变量temp来帮助遍历整个链表(因为head节点是不能动的,因此需要一个辅助变量),直至next域指向null终止。
代码实现如下:
package com.datastructure;
/**
* @author SnkrGao
* @create 2023-03-05 18:38
*/
public class SingleLinkedListDemo {
public static void main(String args[]) {
// 创建节点
HeroNode hero1 = new HeroNode(1, "宋江", "及时雨");
HeroNode hero4 = new HeroNode(4, "林冲", "豹子头");
HeroNode hero2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode hero3 = new HeroNode(3, "吴用", "智多星");
HeroNode hero5 = new HeroNode(3, "吴用", "智多星");
// 创建链表
SingleLinkedList singleLinkedList = new SingleLinkedList();
// 添加
singleLinkedList.addNode(hero1);
singleLinkedList.addNode(hero4);
singleLinkedList.addNode(hero2);
singleLinkedList.addNode(hero3);
singleLinkedList.addNode(hero5);
// 遍历链表
singleLinkedList.ergodic();
}
}
// 节点类,每个HeroNode对象都是一个节点
class HeroNode {
public int No; // 好汉编号
public String HeroName; // 好汉名
public String NickName; // 好汉绰号
public HeroNode next; // next域,指向下一个好汉节点
// 构造方法,创建节点
public HeroNode(int hNo, String heroName, String nickName) {
this.No = hNo;
this.HeroName = heroName;
this.NickName = nickName;
}
// 为了显示输出方便,(alt+insert快捷键)重写toString方法
// 注意不能写next,否则会递归输出
@Override
public String toString() {
return "HeroNode{" +
"No=" + No +
", HeroName='" + HeroName + '\'' +
", NickName='" + NickName + '\'' +
'}';
}
}
// 单向链表类,用于管理好汉节点HeroNode
class SingleLinkedList {
// 创建头节点,头节点不能动
private HeroNode headNode = new HeroNode(0, "", ""); // 头节点不存放数据
// 向链表中添加节点(不考虑编号No顺序)
public void addNode(HeroNode heroNode) {
// 头节点不能动,需要一个辅助变量
HeroNode temp = headNode;
// 遍历链表找到链表的尾部节点,即next->null的节点
while (temp.next != null) {
temp = temp.next;
}
// 将最后一个节点的next指向新的节点
temp.next = heroNode;
}
// 遍历整个链表
public void ergodic() {
// 判断链表是否为空
if (headNode.next == null) {
System.out.println("链表为空!");
return;
}
// 头节点不能动,需要一个辅助变量
/* 但此处与上不同,遍历时不输出headNode的信息,直接从headNode的下一个节点开始输出
因此temp表示的是headNode的next域指向的下一个节点,因此判断时直接判断temp是否为null
**/
HeroNode temp = headNode.next;
// 判断是否遍历到链表的尾部
while (temp != null) {
// 输出节点信息
System.out.println(temp);
// 注意一定要后移!!!
temp = temp.next;
}
}
}
示例结果:

缺点:
- 无法按照英雄编号排序,只能按照插入的顺序;
- 可能会重复出现相同排行编号的好汉,也可以随意地进行添加,这样的规则在实际的示例(梁山108好汉规则)中是不被允许的。
2.3.2 按编号顺序插入,且不允许出现重复编号
仍然以梁山108将为例,但插入时只能以好汉的排名编号顺序插入,需要根据排名将HeroNode插入到指定的位置,若已有该排名,则添加失败并给出提示。

按序插入的思路:
- 首先通过遍历利用一个辅助变量temp找到要添加的新节点newNode的位置;
- newNode.next = temp.next;
- temp.next = newNode。
方法代码:
// 向链表中按编号顺序添加节点(考虑编号No顺序且不允许重复编号)
public void addByOrder(HeroNode heroNode) {
HeroNode temp = headNode;
while (temp.next != null && temp.next.No < heroNode.No) {
temp = temp.next;
}
if (temp.next != null && temp.next.No == heroNode.No) {
System.out.println("当前插入的好汉编号"+heroNode.No+"已存在,插入失败!");
}else{
// 若链表为空||已遍历至链表尾部||已遍历至符合条件的位置,则插入新节点
heroNode.next = temp.next;
temp.next = heroNode;
}
}
示例结果:

单链表更新节点的思路:
编号不能变,HeroName和NickName可以更改,若编号改变则相当于添加。
方法代码:
// 更新节点信息,更新时需传入一个新节点,
// 根据新节点的编号遍历查找链表中对应编号的节点,并更改对应属性的值
public void update(HeroNode heroNode) {
HeroNode temp = headNode;
// 判断链表是否为空
if (headNode.next == null) {
System.out.println("链表为空,无法更新!");
return;
}
boolean flag = false; // 用于标识是否找到对应编号的节点
while (temp.next != null) { // 循环遍历直至链表尾部
if (temp.next.No == heroNode.No) {
flag = true; // 找到链表中对应编号的节点,flag赋值并退出循环
break;
}
temp = temp.next; // 否则继续遍历
}
if (flag) { // 判断是否找到,若找到直接修改对应属性值(赋值,不能替换掉节点)
temp.next.HeroName = heroNode.HeroName;
temp.next.NickName = heroNode.NickName;
}else{
System.out.println("没有找到排行为"+heroNode.No+"的好汉,无法更新!");
}
}
示例结果:

单链表删除节点的思路:
- 首先遍历找到待删除节点的前一个节点previousNode;
- previousNode.next = previousNode.next.next;
- 被删除的节点将不会将不会有其他引用指向,会被垃圾回收机制回收。
方法代码:
// 删除节点,传入要删除的No编号,根据编号匹配遍历查找待删除节点的前一个节点
public void delete(int heroNo) {
HeroNode temp = headNode;
if (headNode.next == null) {
System.out.println("链表为空,无法删除!");
return;
}
boolean flag = false; // 标识是否找到对应编号的待删除节点的previous节点
while (temp.next != null) { // 循环遍历直至链表尾部
if (temp.next.No == heroNo) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) { // 判断是否找到,若找到则将previous节点指向后一个引用
temp.next = temp.next.next;
}else{
System.out.println("没有找到排行为"+heroNo+"的好汉,无法删除");
}
}
示例结果:


浙公网安备 33010602011771号