3. 链表
3.1 应用场景和介绍
生活中,火车是一节连着一节,由火车头连接后面的车厢

火车可以存储物资,对应到程序中,就是链表(Linked List)这种数据结构,同样用来存储数据
链表:有序不连续的链式结构
有序指的是我们可以通过每一个节点,找到下一个节点,每个节点之间都是有顺序的
不连续指的是在内存中,不像是数组一样,通过一块连续的内存空间来存储数据,如下图所示

链表的特点:
-
链表是有序不连续的数据结构
-
每个节点(node)至少有两个域(属性),一个data 域用来保存数据,一个next 域用来指向下一个节点的位置
-
链表通常会有链表头节点,类似火车头一样,不存储数据,只用来记录位置
-
单链表带头节点的逻辑结构示意图如下

应用场景:
我们使用链表来完成水浒英雄的crud 操作
3.2 代码思路和示例
先定义一个HeroNode 类,用来保存数据和指向下一个节点的地址,然后定义一个LinkedList,拥有一个 Head 节点,不保存数据,只记录链表的首位置
// 节点对象
class HeroNode {
int id;
String name;
String nickName;
HeroNode next;
public HeroNode(int id, String name, String nickName) {
this.id = id;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "HeroNode{" +
"id=" + id +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
public HeroNode() {
}
}
class SingleLinkedList {
// 头节点,在链表中不能进行操作,只记录
private HeroNode head = new HeroNode();
public SingleLinkedList() {
}
}
-
当我们添加节点的时候,一般有两种方式,第一种是直接添加到链表的尾部,一种是按照Node内部的顺序来进行添加
第一种方式:直接添加到链表的尾部

示例代码:
// 默认每次添加节点到链表的末尾
public void add(HeroNode node) {
HeroNode temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = node;
}
第二种方式:按照 HeroNode 的id 来进行添加,也就是二号节点一定要添加到一号节点和三号节点之间,如果当前链表已经有了二号节点,则提示添加失败(通过找到待添加节点的上一个节点来进行添加)

示例代码:
// 按照顺序插入节点
public void addByOrder(HeroNode node) {
HeroNode temp = head;
boolean flag = false;
while (true) {
// 循环到链表最后一个节点,表示待插入的 node的id 大于当前链表的所有node 的id,所以待插入的node应该插入到该链表的末尾
if (temp.next == null) {
break;
}
// 找到了待插入的node 的位置(即找到待插入node的前一个node位置即可)
if (temp.next.id > node.id) {
break;
}
// 表示待插入的node 位置已经有node了,无法插入
if (temp.next.id == node.id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
System.out.println("该节点当前位置已经有节点了,无法插入");
} else {
// 执行插入节点操作
node.next = temp.next;
temp.next = node;
}
}
-
显示所有节点信息,通过遍历完成
示例代码
public void list() { HeroNode temp = head; while (temp.next !=null){ System.out.println(temp.next); temp = temp.next; } } -
根据id找到节点并修改节点数据
public void update(HeroNode newNode) { HeroNode temp = head; // 用来标记是否找到在链表中找到该节点 boolean flag = false; while (true) { if (temp.next == null) { break; } // 根据id找到该节点 if (temp.id == newNode.id) { flag = true; break; } temp = temp.next; } if (flag) { // 修改该节点数据 temp.name = newNode.name; temp.nickName = newNode.nickName; } else { System.out.println("没找到对应的节点,无法修改"); } } -
根据节点移除链表中的该节点

public void remove(HeroNode node) {
HeroNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
// 找到待移除节点的上一个节点的位置即可
if (temp.next.id == node.id) {
flag = true;
break;
}
temp = temp.next;
}
// 找到了对应id的node节点,将该node 的上一个node的next 指向该node的next即可
if (flag) {
temp.next = temp.next.next;
} else {
System.out.println("没有找到对应的节点,无法删除");
}
}
3.3 完整代码和测试
public class SingleLinkedList_03 {
public static void main(String[] args) {
SingleLinkedList singleLinkedList = new SingleLinkedList();
HeroNode node1 = new HeroNode(1, "宋江", "及时雨");
HeroNode node2 = new HeroNode(2, "卢俊义", "玉麒麟");
HeroNode node3 = new HeroNode(3, "吴用", "智多星");
HeroNode node4 = new HeroNode(4, "林冲", "豹子头");
singleLinkedList.add(node1);
singleLinkedList.add(node2);
singleLinkedList.add(node3);
singleLinkedList.add(node4);
singleLinkedList.list();
System.out.println("----------------------------------");
SingleLinkedList singleLinkedList2 = new SingleLinkedList();
singleLinkedList2.addByOrder(node1);
singleLinkedList2.addByOrder(node4);
singleLinkedList2.addByOrder(node3);
singleLinkedList2.addByOrder(node2);
singleLinkedList2.list();
singleLinkedList2.addByOrder(node3);
System.out.println("**********************************");
singleLinkedList2.update(new HeroNode(5, "鲁智深", "花和尚"));
singleLinkedList2.update(new HeroNode(3, "小吴", "智多星~~~"));
singleLinkedList2.list();
System.out.println("**********************************");
singleLinkedList2.remove(new HeroNode(1, "宋江", "及时雨"));
singleLinkedList2.remove(new HeroNode(4, "林冲", "豹子头"));
singleLinkedList2.remove(new HeroNode(5, "鲁智深", "花和尚"));
singleLinkedList2.list();
}
}
class SingleLinkedList {
// 头节点,在链表中不能进行操作
private HeroNode head = new HeroNode();
public SingleLinkedList() {
}
// 默认每次添加节点到链表的末尾
public void add(HeroNode node) {
HeroNode temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = node;
}
public void remove(HeroNode node) {
HeroNode temp = head;
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
if (temp.next.id == node.id) {
flag = true;
break;
}
temp = temp.next;
}
// 找到了对应id的node节点,将该node 的上一个node的next 指向该node的next即可
if (flag) {
temp.next = temp.next.next;
} else {
System.out.println("没有找到对应的节点,无法删除");
}
}
// 按照顺序插入节点
public void addByOrder(HeroNode node) {
HeroNode temp = head;
boolean flag = false;
while (true) {
// 循环到链表最后一个节点,表示待插入的 node的id 大于当前链表的所有node 的id,所以待插入的node应该插入到该链表的末尾
if (temp.next == null) {
break;
}
// 找到了待插入的node 的位置(即找到待插入node的前一个node位置即可)
if (temp.next.id > node.id) {
break;
}
// 表示待插入的node 位置已经有node了,无法插入
if (temp.next.id == node.id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
System.out.println("该节点当前位置已经有节点了,无法插入");
} else {
// 执行插入节点操作
node.next = temp.next;
temp.next = node;
}
}
public void update(HeroNode newNode) {
HeroNode temp = head;
// 用来标记是否找到在链表中找到该节点
boolean flag = false;
while (true) {
if (temp.next == null) {
break;
}
// 根据id找到该节点
if (temp.id == newNode.id) {
flag = true;
break;
}
temp = temp.next;
}
if (flag) {
// 修改该节点数据
temp.name = newNode.name;
temp.nickName = newNode.nickName;
} else {
System.out.println("没找到对应的节点,无法修改");
}
}
public void list() {
HeroNode temp = head;
while (temp.next !=null){
System.out.println(temp.next);
temp = temp.next;
}
}
}
// 节点对象
class HeroNode {
int id;
String name;
String nickName;
HeroNode next;
public HeroNode(int id, String name, String nickName) {
this.id = id;
this.name = name;
this.nickName = nickName;
}
@Override
public String toString() {
return "HeroNode{" +
"id=" + id +
", name='" + name + '\'' +
", nickName='" + nickName + '\'' +
'}';
}
public HeroNode() {
}
}
3.4 常见的单链表面试题
① 求单链表中有效元素的个数
// 显示链表中有效节点的个数(不包括head节点),可以判断链表是否为空,更好的做法是维护一个计数器
public int size() {
HeroNode temp = head;
int count = 0;
while (temp.next != null) {
count++;
temp = temp.next;
}
return count;
}
② 查找单链表倒数第k 个元素
// 获取倒数第 index 个节点
public HeroNode get(int index) {
if (index > size()) {
throw new RuntimeException("没有倒数第" + index + "个元素");
}
HeroNode temp = head;
// 查找倒数第一个其实就遍历到最后一个元素
for (int i = 0; i < size() - index + 1; i++) {
temp = temp.next;
}
return temp;
}
③ 单链表的反转
代码思路:
新建一个 HeadNode,用于记录反转后的新链表头位置信息,然后依次将原链表的每个元素(node)添加到 HeadNode 的后面(HeadNode.next = node),在此之前我们需要一个 Node 对象(newTemp)来保存每次HeadNode 原来next 的值,然后将 node.next = newTemp
示例代码:
// 链表反转
public void reverse() {
// 新链表的头节点
HeroNode newHead = new HeroNode();
// 用于暂时保存节点数据
HeroNode newTemp = new HeroNode();
while (head.next != null) {
// 首先将新链表头节点指向的节点保存起来
newTemp = newHead.next;
// 将新链表的头节点重新指向原链表头指向的节点
newHead.next = head.next;
// 原链表头节点指向节点的下个节点(相当于将原链表的元素向前进一位)
head.next = head.next.next;
// 新链表头节点指向的节点再指向原来指向的节点
newHead.next.next = newTemp;
}
head = newHead;
}
④ 从尾到头打印单链表
两种方法:

方法2的示例代码如下:
// 链表逆序打印(使用栈 stack 这种先进后出的数据结构来完成,并且不会影响原链表的数据结构)
public void reversePrint() {
Stack<HeroNode> heroNodeStack = new Stack<>();
HeroNode temp = head;
while (temp.next != null) {
heroNodeStack.add(temp.next);
temp = temp.next;
}
while (heroNodeStack.size() > 0) {
System.out.println(heroNodeStack.pop());
}
}
⑤ 合并两个有序的单链表,合并后的链表同样有序
代码思路:遍历一个链表的每个元素,将该元素按照顺序添加到另一个链表中,需要使用到 addByOrder 方法
示例代码如下:
// 合并两个有序的单链表,合并后的链表依然有序(在一个链表上按顺序添加另外一个链表的每个元素,会影响原来两个有序链表的结构)
public void mergeByOrder(SingleLinkedList singleLinkedList) {
HeroNode temp = singleLinkedList.head.next;
// 用来保存temp的next
HeroNode temp3 = new HeroNode();
// 不停的将 singleLinkedList 中的每个元素按顺序添加到当前的链表中
while (temp != null) {
temp3 = temp.next;
// 将 temp 按照顺序添加到当前链表结构中(temp.next 将会被改变,因此影响到singleLinkedList 的结构)
this.addByOrder(temp);
temp = temp3;
}
}