目录
3.3 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点
3.5 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
3.6 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前
3.10 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
1.ArrayList的缺陷
通过篇章四,我们已经熟悉了ArrayList的使用,并且进行了简单模拟实现。通过源码知道,ArrayList底层使用数组来存储元素。
那这样会出现什么问题呢?
由于其底层是一段连续空间,当在ArrayList任意位置插入或者删除元素时,就需要将后序元素整体往前或者往后搬移,时间复杂度为O(n),效率比较低。因此ArrayList不适合做任意位置插入和删除比较多的场景。因此:java集合中又引入了LinkedList,即链表结构。
2. 链表
2.1 链表的概念及结构
链表是一种 物理存储结构上非连续 存储结构,数据元素的 逻辑顺序 是通过链表中的 引用链接 次序实现的。
注意:
1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
2.现实中的结点一般都是从堆上申请出来的
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
2.2 链表结构
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向
什么是双向?
2.带头或者不带头
什么是带头?
什么是不带头?
3.循环或者非循环
什么是循环?
组合成的 8种链表结构:
虽然有这么多的链表的结构,但是我们重点掌握两种:
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
2. 无头双向非循环链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表。
2.3 链表的实现
1.完整代码
/**
* Created with IntelliJ IDEA
* Description 无头单向非循环链表实现
* User: 王杰
* Date: 2025-05-26
* Time: 20:33
*/
public class MySingleList implements IList{
static class ListNode {
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
// 创建链表
public void createList() {
ListNode node1 = new ListNode(12);
ListNode node2 = new ListNode(23);
ListNode node3 = new ListNode(34);
ListNode node4 = new ListNode(45);
ListNode node5 = new ListNode(56);
node1.next = node2;
node2.next = node3;
node3.next = node4;
node4.next = node5;
this.head = node1;
}
// 显示方法
@Override
public void display() {
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
}
// 链表大小
@Override
public int size() {
int len = 0;
ListNode cur = head;
while (cur != null) {
len++;
cur = cur.next;
}
return len;
}
// 链表是否存在 key 值
@Override
public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
// 头插法
@Override
public void addFirst(int data) {
ListNode node = new ListNode(data);
node.next = head;
head = node;
}
// 尾插法
@Override
public void addLast(int data) {
ListNode node = new ListNode(data);
// 一个节点都没有
if (head == null) {
head = node;
return;
}
// 找尾巴
ListNode cur = head;
while (cur != null) {
if (cur.next == null) {
cur.next = node;
return;
}
cur = cur.next;
}
}
//中间插入
@Override
public void addIndex(int index, int data) {
int len = size();
if (index < 0 || index > len) {
System.out.println("index位置不存在");
return;
}
if (index == 0) {
addFirst(data);
return;
}
if (index == len) {
addLast(data);
return;
}
// 中间插入
ListNode cur = head;
if (index - 1 != 0) {
cur = cur.next;
index--;
}
ListNode node = new ListNode(data);
// 所有的插入 优先 绑定后边
node.next = cur.next;
cur.next = node;
}
// 删除 key值 节点
@Override
public void remove(int key) {
if (head == null) {
return;
}
// 删除头节点
if (head.val == key) {
head = head.next;
return;
}
ListNode cur = findNodeOfKey(key);
if (cur == null) {
return;
}
ListNode del = cur.next;
cur.next = del.next;
}
private ListNode findNodeOfKey(int key) {
ListNode cur = head;
while (cur.next != null) {
if (cur.next.val == key) {
return cur;
}
cur = cur.next;
}
return null;
}
// 删除 所有key值 节点
@Override
public void removeAllKey(int key) {
if (head == null) {
return;
}
ListNode prev = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == key) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
if (head.val == key) {
head = head.next;
}
}
@Override
public void clear() {
ListNode cur = head;
while (cur != null) {
ListNode curNext = cur.next;
cur.next = null;
cur = curNext;
}
head = null;
}
}
2.图解
单向不带头非循环链表:
3.显示方法
// 显示方法
@Override
public void display() {
ListNode cur = head;
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
}
4.链表大小
// 链表大小
@Override
public int size() {
int len = 0;
ListNode cur = head;
while (cur != null) {
len++;
cur = cur.next;
}
return len;
}
实现到这里,我们需要掌握的是:
1.ListNode cur = head;
此处申请了临时变量,因为数据存储在堆空间,而且 cur 也指向链表,所以改变有效。同时 此处也是为了不改变 head 的位置,因为后续要用head找到该链表,如果head位置变动了,就找不到该链表了。
2.cur != null;
此处最后一个节点,是会运算的。最后,cur指向的是最后一个节点的下一个节点,也就是 null;
3.cur.next != null;
此处最后一个节点,是不会运算的。最后,cur指向的是最后一个节点。
5. 链表是否存在 key 值
// 链表是否存在 key 值
@Override
public boolean contains(int key) {
ListNode cur = head;
while (cur != null) {
if (cur.val == key) {
return true;
}
cur = cur.next;
}
return false;
}
6.头插法
// 头插法
@Override
public void addFirst(int data) {
ListNode node = new ListNode(data);
node.next = head;
head = node;
}
7.尾插法
// 尾插法
@Override
public void addLast(int data) {
ListNode node = new ListNode(data);
// 一个节点都没有
if (head == null) {
head = node;
return;
}
// 找尾巴
ListNode cur = head;
while (cur != null) {
if (cur.next == null) {
cur.next = node;
return;
}
cur = cur.next;
}
}
8.中间插入
//中间插入
@Override
public void addIndex(int index, int data) {
int len = size();
if (index < 0 || index > len) {
System.out.println("index位置不存在");
return;
}
if (index == 0) {
addFirst(data);
return;
}
if (index == len) {
addLast(data);
}
// 中间插入
ListNode cur = head;
if (index - 1 != 0) {
cur = cur.next;
index--;
}
ListNode node = new ListNode(data);
// 所有的插入 优先 绑定后边
node.next = cur.next;
cur.next = node;
}
注意:
1.此处中间插入,关键是找到 要 插入的位置 的 前一个位置。
2.要牢记 所有的插入 优先 绑定后边
9.删除key值节点
// 删除 key值 节点
@Override
public void remove(int key) {
if (head == null) {
return;
}
// 删除头节点
if (head.val == key) {
head = head.next;
return;
}
ListNode cur = findNodeOfKey(key);
if (cur == null) {
return;
}
ListNode del = cur.next;
cur.next = del.next;
}
private ListNode findNodeOfKey(int key) {
ListNode cur = head;
while (cur.next != null) {
if (cur.next.val == key) {
return cur;
}
cur = cur.next;
}
return null;
}
注意:
找到 要删除的节点 的前一个节点
这一思路在单链表中很关键
10.删除所有key值节点
// 删除 所有key值 节点
@Override
public void removeAllKey(int key) {
if (head == null) {
return;
}
ListNode prev = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == key) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
if (head.val == key) {
head = head.next;
}
}
11.clear
@Override
public void clear() {
ListNode cur = head;
while (cur != null) {
ListNode curNext = cur.next;
cur.next = null;
cur = curNext;
}
head = null;
}
注意:
此处最后 head 也要置空
3.练习
3.1 删除链表中等于给定值 val 的所有节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
ListNode prev = head;
ListNode cur = head.next;
while (cur != null) {
if (cur.val == val) {
prev.next = cur.next;
cur = cur.next;
}else {
prev = cur;
cur = cur.next;
}
}
if (head.val == val) {
head = head.next;
}
return head;
}
}
203. 移除链表元素 - 力扣(LeetCode) 此处和 2.3 中的 10.删除所有key节点 一样
3.2 反转一个单链表
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null) {
return head;
}
ListNode cur = head.next;
head.next = null;
while(cur != null) {
ListNode curNext = cur.next;
cur.next = head;
head = cur;
cur = curNext;
}
return head;
}
}
3.3 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode middleNode(ListNode head) {
if(head == null) {
return null;
}
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}
注意:
做这些练习的时候,要考虑 空链表 的情况,也就是 head == null
3.4 输入一个链表,输出该链表中倒数第k个结点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public int kthToLast(ListNode head, int k) {
if(head == null) {
return -1;
}
ListNode fast = head;
ListNode slow = head;
// fast 走 k - 1 步
int count = 0;
while(count != k - 1) {
fast = fast.next;
count++;
}
while(fast.next != null) {
slow = slow.next;
fast = fast.next;
}
return slow.val;
}
}
注意:
1. fast.next != null;
此处 fast 走到最后一个节点即可,不必走到 null
2.此处 k 值,不确定是否合法,一般题目中会设置范围,但是没设置的话就需要补充上k值合法性的校验
补充:验证 k 的合法性
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public int kthToLast(ListNode head, int k) {
if(head == null) {
return -1;
}
if(k <= 0) {
return -1;
}
ListNode fast = head;
ListNode slow = head;
// fast 走 k - 1 步
int count = 0;
while(count != k - 1) {
fast = fast.next;
if(fast == null) {
return -1;
}
count++;
}
while(fast.next != null) {
slow = slow.next;
fast = fast.next;
}
return slow.val;
}
}
3.5 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newHead = new ListNode();
ListNode cur = newHead;
while(list1 != null && list2 != null) {
if(list1.val > list2.val) {
cur.next = list2;
list2 = list2.next;
cur = cur.next;
}else {
cur.next = list1;
list1 = list1.next;
cur = cur.next;
}
}
if(list1 != null) {
cur.next = list1;
}
if(list2 != null) {
cur.next = list2;
}
return newHead.next;
}
}
3.6 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前
import java.util.*;
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class Partition {
public ListNode partition(ListNode pHead, int x) {
ListNode cur = pHead;
ListNode beforStart = null;
ListNode beforEnd = null;
ListNode afterStart = null;
ListNode afterEnd = null;
while(cur != null) {
if(cur.val < x) {
if(beforStart == null) {
beforStart = cur;
beforEnd = cur;
}else {
beforEnd.next = cur;
beforEnd = beforEnd.next;
}
cur = cur.next;
}else {
if(afterStart == null) {
afterStart = cur;
afterEnd = cur;
}else {
afterEnd.next = cur;
afterEnd = afterEnd.next;
}
cur = cur.next;
}
}
if(beforStart == null) {
return afterStart;
}
// 置空 afterEnd.next
if(afterStart != null) {
afterEnd.next = null;
}
beforEnd.next = afterStart;
return beforStart;
}
}
注意:
此处思路:是把 大于x 和 小于x 的值分为两个链表。
但是:注意特殊情况,比如大于x的链表为空 和 小于x的链表为空。
而且:要注意,在 小于x的链表不为空时, 置空 afterEnd.next
3.7 链表的回文结构
import java.util.*;
/*
public class ListNode {
int val;
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}*/
public class PalindromeList {
public boolean chkPalindrome(ListNode A) {
if(A == null) {
return true;
}
ListNode fast = A;
ListNode slow = A;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
ListNode cur = slow.next;
while(cur != null) {
ListNode curNext = cur.next;
cur.next = slow;
slow = cur;
cur =curNext;
}
while(A != slow) {
if(A.val != slow.val) {
return false;
}
// 偶数情况
if(A.next == slow) {
return true;
}
A = A.next;
slow = slow.next;
}
return true;
}
}
注意:
1. 三步走:找中间节点,反转中间节点后的链表,比较前半部分和后半部分链表的val值
2. 偶数情况:
if(A.next == slow) {
return true;
}
3.8 输入两个链表,找出它们的第一个公共结点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pLong = headA;
ListNode pShort = headB;
// 先求两个链表的长度
int lenA = 0;
int lenB = 0;
while(pLong != null) {
lenA++;
pLong = pLong.next;
}
while(pShort != null) {
lenB++;
pShort = pShort.next;
}
pLong = headA
pShort = headB
// 求差值
int len = lenA - lenB;
if(len < 0) {
pLong = lenB;
pShort = lenA;
len = lenB - lenA;
}
// 走完上述两步 pLong 一定指向最长的链表 pShort 一定指向最短的链表
// 接下来的操作 只需要操作 pLong 和 pShort 就行了
// 让最长的链表走 len 步
while(len != 0) {
pLong = pLong.next;
len--;
}
// 两个引用同时走 直到他们相遇
while(pLong != pShort) {
pLong = pLong.next;
pShort = pShort.next;
}
if(pLong == null) {
return null; // 不相交
}
return pLong;
}
}
注意:
1.主要思路就是:走两个链表长度的差值步
2. 此处注意还原 pLong 和 pShort ,因为算长度,他们的位置发生了改变,要还原,否则影响下面代码
pLong = headA
pShort = headB3.注意不相交的情况:
if(pLong == null) {
return null; // 不相交
}
3.9 给定一个链表,判断链表中是否有环
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null) {
return false;
}
if(head.next == null) {
return false;
}
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) {
return true;
}
}
return false;
}
}
【思路】
快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。比如:陪女朋友到操作跑步减肥。
【扩展问题】
为什么快指针每次走两步,慢指针走一步可以?
假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快
指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在慢指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。
快指针一次走3步,走4步,...n步行吗?
注意:
1.链表为空,和只有一个节点的情况是不存在环的
if(head == null) {
return false;
}if(head.next == null) {
return false;
}2.快慢指针的步数问题:
快 2 慢 2:会相遇
快3 慢 1 :永不相遇
可见:并不是快指针比慢指针快就行,还得注意步数问题。
3.10 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
if(head == null) {
return null;
}
if(head.next == null) {
return null;
}
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) {
break;
}
}
if(fast == null || fast.next == null) {
return null; // 没有环
}
slow = head;
while(slow != fast) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
【结论】
让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。
【证明】
注意:
1.链表为空,和只有一个节点的情况是不存在环的
if(head == null) {
return false;
}if(head.next == null) {
return false;
}2.相遇时出循环:
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) {
break;
}
}3.去除没有环的情况:
if(fast == null || fast.next == null) {
return null; // 没有环
}4.关键:让慢指针回到链表开头,然后慢指针与快指针以相同的速度走
slow = head;
while(slow != fast) {
fast = fast.next;
slow = slow.next;
}5.证明:此处最关键的是,fast的路程是slow路程的二倍,建立等式,计算,取极端情况 得出关系,写代码。