文章目录
一、了解链表
1.单链表与双链表
1).链表可以抽象成一列运输火车,火车的每节车厢通过一定的方式进行连接,每节车厢里面都可以装载不同的物品。查找数据就是遍历链表,在火车里面从车头走到车尾的过程
2).链表分为单向链表和双向链表,顾名思义:
单链表 | 双链表 |
---|---|
一个节点只有一个指向后继节点的指针 | 一个节点有一个指向前驱节点的指针 和一个指向后继节点的指针 |
只有一个头结点,若丢失则整个链表丢失 | 可以从任意节点开始 从前往后/从后往前 遍历 |
尾结点指针指向 Null | 头结点和尾结点都有个指向 Null 的指针 |
单链表:
双链表:
2.链表学习流程分享
大概总结了一下链表的学习思路,若有更好的建议欢迎提出~
二、链表基本知识
1. 单链表的简单实现
这里写的简单实现是基于 单个[int] 类型,比较好进行测试,可以尝试使用其他类型或者多个其他类型的元素。
定义一个单链表节点类
节点类内部就是需要储存的元素(可以是其他类型的元素,或多个元素),一个指向下一个节点的指针引用
class Node {
// 每个节点存储的值
int val;
// 指向下一个节点的引用
Node next;
Node() {}
Node(int val) {
this.val = val;
}
Node(int val, Node next) {
this.val = val;
this.next = next;
}
}
Node节点抽象:
定义单链表类
1.统计链表节点个数 (int size;
, 作为索引) 和定义一个链表头节点 (Node head;
)
2.拥有一个头结点就等于拥有了整个单链表,所以必须保证头结点使用一个引用来定义,这样在遍历的时候才不会丢失链表。
3.所有增删改查的方法都写在这个类中
public class myLinkedList {
// 链表有效节点个数,需要添加或删除的时候自己统计
private int size;
// 链表的头结点,不能丢失
private Node head;
}
节点的插入(增)
有两种方式,头插法和尾插法;
头插法
示例:从头部插入节点
public void addFirstHead(int val) {
// 创建节点就是创建一个 Node 对象并传入 输入的 val 值
Node node = new Node(val);
// 首先判断头结点是否为空,若为空则此节点就是第一节点
if (head == null) {
head = node;
} else {
// 此时存在前驱节点,使用头插法从头部进行插入
// 将新节点的引用指向原链表的头部
node.next = head;
// 再让头节点变成新节点
head = node;
}
// 统计节点个数
size ++;
}
尾插法
示例:从尾部插入节点
public void addLastNode(int val) {
Node node = new Node(val);
// 一样首先判空
if (head == null) {
head = node;
} else if (head.next == null){
// 若头结点存在,且头结点指向空,此时只有一个节点,从尾部插入新节点
head.next = node;
} else {
// 若头结点后边跟着很多节点,则需要遍历到尾部后再插入
Node pre = head.next;
while (pre.next != null) {
pre = pre.next;
}
// 此时走到了链表最后一个节点处,添加新节点即可
pre.next = node;
}
size ++;
}
一般来说没有顺序要求优先使用头插法,可以节省遍历的时间复杂度,需要考虑的条件不多,写起来也比尾插法简单很多。
插入中间节点
插入中间节点是头插法和尾插法相结合,其实这个方法完全可以直接替代尾插法
示例:在 index 位置插入 val 值的节点
思路:先遍历到 index 位置的前一位再进行插入
public void addIndex(int index, int val) {
// 首先判断索引是否越界
if (index < 0 || index > size) {
// 若越界则报错
System.err.println("添加失败,该索引非法!");
}
// 分两种情况,索引为0,和索引不为0
if (index == 0) {
// 在头部插入,直接调用头插法
addFirstNode(val);
} else {
// 和尾插法思路类似,先遍历到需要插入的位置
// 需要插入的节点是在 index 位置,这个插入操作必须在 index 的前一位开始,
// 所以 index - 1
for (int i = 0; i < index - 1; i++) {
pre = pre.next;
}
// 找到前驱节点后就可以进行下一步插入操作
// 在新建节点的时候传入 val 和 prev.next (值 和 prev 指向的下一个节点)
// prev指向的下一个节点后面的链表就不会丢失
Node node = new Node(val,pre.next);
// 拼接插入新节点后的链表
pre.next = node;
}
}
pre 停留在 2 的位置再做后继插入操作
简化前的插入方式
// 新建一个值为 val 的节点
Node node = new Node(val);
// 新节点的 next 引用指向 prev 的下一个节点拼接上原链表
node.next = prev.next;
// prev的 next 引用指向新节点 node,完成整个拼接过程
prev.next = node;
中间节点插入分四步:
- 新建节点
- 新节点引用先指向需要插入位置的后继节点
- 断开原先链表中该位置前驱节点与后继节点的连接
- 前驱节点引用指向新节点
尾插可以调用中间插入节点的方法直接改写成
public void addLast(int val) {
addIndex(size, val);
}
修改节点 (改)
修改 index 节点的值,返回修改前的值
和插入中间节点的思路差不多,先遍历,不同的是 index 是直达该节点
public int set(int index, int newVal) {
// 先判段是否越界
if (index < 0 || index >= size) {
System.err.println("该引用对应位置不存在,修改错误");
return -1;
}
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
// 暂存一下需要返回的原链表值
int oldVal = node.val;
// 修改原链表值为新值
x.val = newVal;
return oldVal;
}
查找节点 (查)
输入的值全部为正整数的情况
1.输入一个值查找该节点的位置,存在返回该节点索引,若不存在返回 -1
public index getVal(int val) {
int index = 0;
// 使用 foreach 循环遍历链表
for (Node x : head) {
if (x.val == val) {
return index;
}
index ++;
}
// 否则返回 -1
return -1;
}
2.输入一个索引查找对应的节点
public int get(int index) {
// 有索引值必须判断是否越界
if (index < 0 || index >= size) {
System.err.println("该索引对应位置不存在,获取值失败");
return -1;
}
Node node = head;
for (int i = 0; i < index; i++) {
node = node.next;
}
// 找到后返回该节点的值
return node.val;
}
3.输入一个值查找该节点是否存在
直接调用第一个查找方法即可,若结果不为 -1 即找到
public boolean contains(int val) {
return getVal(val) != -1;
}
删除节点 (删)
1.删除索引值为 index 的节点,思路与在插入中间节点类似
分情况讨论:头结点是待删除的节点 与 中间节点是待删除的节点
public int remove(int index) {
// 判断 index 是否越界
if (index < 0 || index >= size) {
System.err.println("输入的索引不存在,删除失败");
return -1;
}
// 头结点是待删除的节点
if (index == 0) {
// 为避免待删除节点还挂在链表上的情况,需要将该节点的引用置为空
Node tmp = head;
head = head.next;
tmp.next = null;
} else {
// 头结点不是待删除的节点,使一个指针遍历到待删除节点位置标记
Node pre = head;
// pre 在待删除节点的前一个节点停下
for (int i = 0; i < index - 1; i++) {
pre = pre.next;
}
Node node = prev.next; // 临时储存待删除节点
prev.next = node.next; // 连接待删除节点的下一个节点
node.next = null; // node 就是待删除节点,引用置空
size --; // 索引 - 1
return node.val; // 返回已删除节点的值
}
}
删除节点分三步
- 暂存待删除节点 node
- 前驱节点 pre 引用指向待删除节点 node 的后继节点 node.next
- 待删除节点引用指向置空
直接调用该方法删除头结点或尾结点
// 删除头结点
public int removeHead() {
return remove(0);
}
// 删除尾结点
public int removeTail() {
return remove(size - 1);
}
2.单链表典型题目
1). 删除链表中所有值为 val 的元素
思路:使用虚拟头结点以及双指针(一个在前一个在后),遍历链表,遇到与 val 相同的节点就删除。
public Node removeAllValue(int val) {
// 创建一个虚拟头结点
Node dummyHead = new Node();
dummyHead.next = head; // 这个节点连接 head
// 使用双指针
Node node = dummyHead;
Node cur = head;
if (head == null) {
return;
}
while (cur != null) {
if (cur.val == val) {
node.next = cur.next;
break;
}
node = cur;
cur = cur.next;
}
return dummyHead.next;
}
2). 反转链表
将给定的链表从尾到头重新反转排列,并返回反转后的链表
public Node reverseList(Node head) {
Node dummy = new Node();
dummy = null;
Node cur = head;
while (cur != null) {
Node pre = cur.next;
cur.next = dummy;
dummy = cur;
cur = pre;
}
return dummy;
}
从头节点开始到第一次完整循环结束示意图
3).环形链表
给定一个链表,判断链表中是否有环
思路:使用快慢指针,若链表中存在环,则快指针一定会与慢指针相遇
public boolean hasCycle(Node head) {
Node fast = head, slow = head;
while (fast != null && fast.next != null) {
// 快指针一次走两步
fast = fast.next.next;
// 慢指针一次走一步
slow = slow.next;
// 若相等,则证明快指针追上了慢指针,是环形链表
if (fast == slow) {
return true;
}
}
// 若走出循环,则一定不是环形链表
return false;
}