链表类总结
- 206. 反转链表
- 19. 删除链表的倒数第 N 个结点(快慢指针+虚拟头节点)
- 24. 两两交换链表中的节点(迭代+递归)
- 25. K 个一组翻转链表(递归)
- 82. 删除排序链表中的重复元素 II(虚拟头+三指针工作法)
- 83. 删除排序链表中的重复元素(链表的操作)
- 92. 反转链表 II(链表的基操)
- 141. 环形链表(快慢指针)
- 142. 环形链表 II(快慢指针)
- 203. 移除链表元素(链表基操)
- 237. 删除链表中的节点(链表基操)
- 328. 奇偶链表(链表的基操+分治)
- 725. 分隔链表(链表的基操)
- 876. 链表的中间结点(快慢指针)
- 2. 两数相加(加法模板)
- 21. 合并两个有序链表(虚拟头节点+递归法的理解)
- 23. 合并K个升序链表(小顶堆+顺序合并法)
- 86. 分隔链表(分治思想)
- 147. 对链表进行插入排序
- 148. 排序链表(自顶向下归并排序)
- 160. 相交链表(数学方法)
- 234. 回文链表(快慢指针找中间节点+反转链表)
- 445. 两数相加 II(反转链表 + 加法模板 + 头插法)
链表如何实现,如何遍历链表。链表可以保证头部尾部插入删除操作都是O(1),查找任意元素位置O(N),快慢指针和链表反转几乎是所有链表类问题的基础,尤其是反转链表,代码很短,建议直接背熟。
链表和二叉树是天然的递归数据结构,因此本专题一定要掌握递归的思想。
递归三部曲:
整个递归的终止条件。
一级递归需要做什么?
应该返回给上一级的返回值是什么?
参考博客:
https://lyl0724.github.io/2020/01/25/1/#vcomment
206. 反转链表
注:本题对递归又有了新的认知,前面学习递归的思想,更多的是不要用人脑去计算递归过程,而是用一种通用的办法直接得出答案,这种通用的过程称之为递推公式,后续部分已经完成了,只要关注第k个和第k+1个的逻辑即可!
那么本题新的认知是,递归是由递和归组成,递的过程反映的思想就是拆解成子问题的思想,归的过程就是在处理递推公式(完成指定功能),所谓的递归出口就是最小子问题的结果!。
/**
* 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) {
// 使用迭代法:充分定义好指针,这里需要三个指针,其中一个指针保存下一个节点信息,另外两个指针做好反转
// 这种链表的迭代法一般画好图,做好记录节点信息,一步一步走就可以!
ListNode prev = null;
ListNode cur = head;
while(cur != null){
// 若curr指针不空,记录下一个节点next
ListNode next = cur.next;
// cur指向前者
cur.next = prev;
// prev移动到cur
prev = cur;
// cur移动到下一个节点next
cur = next;
}
// 最后返回最后一个节点:prev
return prev;
}
}
/**
* 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 ||head.next == null){
return head;
}
// 递归的递过程,这里的p实际上是最后的一个节点
ListNode p = reverseList(head.next);
// 递归的归过程:递归函数,也就是递推公式的处理
// head下一个指针指向head
head.next.next = head;
// head的下一个指针指向null
head.next = null;
// 最后返回最后一个节点p
return p;
}
}
19. 删除链表的倒数第 N 个结点(快慢指针+虚拟头节点)
注意:在没有使用虚拟头节点的时候,需要考虑头节点的删除问题,容易漏掉因此一般链表一般都搭配虚拟头节点
/**
* 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 removeNthFromEnd(ListNode head, int n) {
/**
分析:
寻找倒数第k个节点使用 快慢指针法+虚拟头节点法
*/
ListNode fast = head,slow = head;
// 快指针先走 n 步
for(int i = 0; i < n; i++){
fast = fast.next;
}
// 解决删除头节点问题
if(fast == null){
// 表示删除第一个节点
return head.next;
}
// 遍历到链表末尾
while(fast.next !=null){
fast = fast.next;
slow = slow.next;
}
// 此时 slow指向倒数第n个指针的前驱节点
slow.next = slow.next.next;
// 返回节点
return head;
}
}
使用虚拟头结点,这样子就不需要考虑原始头结点的删除问题(因为原始头结点的前驱是dummy虚拟节点)
/**
* 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 removeNthFromEnd(ListNode head, int n) {
if(head == null){
return null;
}
// 创建一个虚拟头节点,其中 -1 代表虚拟头节点的值
ListNode dummy = new ListNode(-1);
dummy.next = head;
// 快慢指针
ListNode fast = dummy, slow = dummy;
// 快指针先走 n+1,此时要考虑虚拟头的存在
for(int i = 0; i <= n; i++){
fast = fast.next;
}
// 快指针 不为空的情况下,快慢指针一直走
while(fast != null){
fast = fast.next;
slow = slow.next;
}
// 快指针到链表末尾 且为空了
// slow指针指向倒数第k个节点的前驱
slow.next = slow.next.next;
// 返回节点
return dummy.next;
}
}
24. 两两交换链表中的节点(迭代+递归)
递归还是要好好理解,提取出递归终止条件后,要记得,我们只关系一个零部件的逻辑,剩下的,递归已经处理过了。
/**
* 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 swapPairs(ListNode head) {
/**
分析:
实际上是一个递归的问题,凡是递归的问题也可以使用迭代来解决
class Solution {
public ListNode swapPairs(ListNode head) {
if (head == null || head.next == null)
return head;
ListNode rest = head.next.next;
ListNode newHead = head.next;
newHead.next = head;
head.next = swapPairs(rest);
return newHead;
}
}
*/
// if(head == null || head.next == null){
// return head;
// }
// // 定义虚拟头结点
// ListNode dummy = new ListNode(-1);
// dummy.next = head;
// // 定义三个指针,用于交换节点
// ListNode pre = dummy;
// ListNode first = head;
// ListNode second = head.next;
// while(second != null){
// // 保存下一个节点信息
// ListNode next = second.next;
// // 开始交换节点
// pre.next = second;
// second.next = first;
// first.next = next;
// // 移动指针,进行后续的交换操作
// pre = first;
// first = next;
// // 如果first移动到了 null 则跳出循环
// if(first == null){
// break;
// }
// second = first.next;
// }
// // 返回节点
// return dummy.next;
//递归动画 https://leetcode-cn.com/problems/swap-nodes-in-pairs/solution/dong-hua-yan-shi-die-dai-yu-di-gui-liang-ha0u/
// 递归出口和递归函数是核心,那么我们只关注一个零部件的逻辑,其余的交给递归处理!!!
// 递归出口
if(head ==null || head.next == null){
return head;
}
// 递归函数逻辑
// subHead是后续swap处理过的链表
ListNode subHead = swapPairs(head.next.next);
// 交换链表节点:我只关心这个零部件的逻辑,morensubHead已经处理过了
ListNode headNext = head.next;
headNext.next = head;
// 指向的业务逻辑 subHead默认处理过了
head.next = subHead;
return headNext;
}
}
25. K 个一组翻转链表(递归)
本题为困难,但是分析出递归思路后,也还ok的
递归三部曲:
/**
* 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 reverseKGroup(ListNode head, int k) {
/**
分析:
这是一个递归的问题,递归出口?
*/
// 递归法,空间复杂度是O(n)
// 递归出口:当节点为空,只有一个节点,或者剩余节点数目不够k个
if(head == null || head.next == null){
return head;
}
// 探寻剩余节点数目不够k个的出口
ListNode end = head;
for(int i = 0; i < k; i++){
if(end == null){
// 小于k个 返回头节点,为什么返回head?
// 在递归的处理过程中只有:head head后面的逻辑 已完成处理的逻辑(返回值)
return head;
}
end = end.next;
}
// end节点是 要翻转链表末尾节点的下一个,因此设计翻转链表要设计设计成左闭右开形式
ListNode newHead = reverse(head,end);
// 下一轮循环的起始节点就是end节点
head.next = reverseKGroup(end,k);
// 返回 newHead
return newHead;
}
public ListNode reverse(ListNode start,ListNode end){
// 翻转的原则是一边保存下一个节点,一般改变指针
ListNode pre = null;
ListNode next = null;
while(start != end){
// 保存下一个节点
next = start.next;
// 改变指针
start.next = pre;
// 移动指针
pre = start;
start = next;
}
return pre;
}
}
迭代法:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null){
return head;
}
//定义一个假的节点。
ListNode dummy=new ListNode(0);
//假节点的next指向head。
// dummy->1->2->3->4->5
dummy.next=head;
//初始化pre和end都指向dummy。pre指每次要翻转的链表的头结点的上一个节点。end指每次要翻转的链表的尾节点
ListNode pre=dummy;
ListNode end=dummy;
while(end.next!=null){
//循环k次,找到需要翻转的链表的结尾,这里每次循环要判断end是否等于空,因为如果为空,end.next会报空指针异常。
//dummy->1->2->3->4->5 若k为2,循环2次,end指向2
for(int i=0;i<k&&end != null;i++){
end=end.next;
}
//如果end==null,即需要翻转的链表的节点数小于k,不执行翻转。
if(end==null){
break;
}
//先记录下end.next,方便后面链接链表
ListNode next=end.next;
//然后断开链表
end.next=null;
//记录下要翻转链表的头节点
ListNode start=pre.next;
//翻转链表,pre.next指向翻转后的链表。1->2 变成2->1。 dummy->2->1
pre.next=reverse(start);
//翻转后头节点变到最后。通过.next把断开的链表重新链接。
start.next=next;
//将pre换成下次要翻转的链表的头结点的上一个节点。即start
pre=start;
//翻转结束,将end置为下次要翻转的链表的头结点的上一个节点。即start
end=start;
}
return dummy.next;
}
//链表翻转
// 例子: head: 1->2->3->4
public ListNode reverse(ListNode head) {
//单链表为空或只有一个节点,直接返回原单链表
if (head == null || head.next == null){
return head;
}
//前一个节点指针
ListNode preNode = null;
//当前节点指针
ListNode curNode = head;
//下一个节点指针
ListNode nextNode = null;
while (curNode != null){
nextNode = curNode.next;//nextNode 指向下一个节点,保存当前节点后面的链表。
curNode.next=preNode;//将当前节点next域指向前一个节点 null<-1<-2<-3<-4
preNode = curNode;//preNode 指针向后移动。preNode指向当前节点。
curNode = nextNode;//curNode指针向后移动。下一个节点变成当前节点
}
return preNode;
}
}
82. 删除排序链表中的重复元素 II(虚拟头+三指针工作法)
/**
* 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 deleteDuplicates(ListNode head) {
/**
分析:
一次遍历,注意记录前驱节点,工作节点,next节点
*/
ListNode dummy = new ListNode(-1000);
dummy.next = head;
ListNode pre = dummy;
ListNode cur = head;
while(cur != null){
// 记录 next
ListNode next = cur.next;
// 判断
if(next != null && cur.val == next.val){
// 移动到非重复元素
while(next != null && cur.val == next.val){
next = next.next;
}
// 删除重复数字
pre.next = next;
// 摆正位置
cur = next;
// 跳过当前循环
continue;
}
// 正常的移动
pre = cur;
cur = next;
}
return dummy.next;
}
}
83. 删除排序链表中的重复元素(链表的操作)
/**
* 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 deleteDuplicates(ListNode head) {
/**
这不是上一题的思路~
*/
ListNode dummy = new ListNode(-1000);
dummy.next = head;
// ListNode pre = dummy;
ListNode cur = head;
while(cur != null){
// 记录下一个节点
ListNode next = cur.next;
while( next != null && cur.val == next.val){
next = next.next;
}
// pre = cur;
cur.next = next;
cur = next;
}
return dummy.next;
}
}
92. 反转链表 II(链表的基操)
/**
* 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 reverseBetween(ListNode head, int left, int right) {
/**
分析:
整体思想是分成三段来解决,先切开,然后翻转,最后拼接
*/
if(left == right){
return head;
}
ListNode dummy = new ListNode(-1000);
dummy.next = head;
ListNode pre = dummy;
ListNode cur = dummy;
for(int i = 1; i < left; i++){
cur = cur.next;
}
// pre节点是 反转链表的前驱
pre = cur;
ListNode first = pre.next;
for(int i = left; i <= right; i++){
cur = cur.next;
}
// next节点是反转链表的后继
ListNode next = cur.next;
// 切断链表
pre.next = null;
// 切断链表
cur.next = null;
// 翻转链表,并连接链表
pre.next = reverse(first);
first.next = next;
return dummy.next;
}
public ListNode reverse(ListNode left){
ListNode pre = null;
ListNode cur = left;
while( cur != null){
// 记录下一个节点信息
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
141. 环形链表(快慢指针)
/**
* 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||head.next == null){
// 只有一个节点或者没有节点
return false;
}
ListNode fast = head, slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
return true;
}
}
return false;
}
}
142. 环形链表 II(快慢指针)
/**
* 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) {
/**
分析:
首先判断有没有环,有环的话,找到环的入口即可。依旧是使用快慢指针法
*/
if(head == null || head.next == null){
return null;
}
ListNode fast = head, slow = head;
// 设置标记
boolean flag = false;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
// 存在环
flag = true;
break;
}
}
// 有环继续找 环入口
if(flag){
fast = head;
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
// 没有环就return null
return null;
}
}
203. 移除链表元素(链表基操)
/**
* 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) {
/**
分析:链表的基操
*/
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
ListNode cur = head;
while( cur != null){
// 时刻保证 cur不空
while( cur != null && cur.val != val){
pre = cur;
cur = cur.next;
}
// 时刻保证 cur 不空
if(cur != null){
ListNode next = cur.next;
pre.next = next;
cur = next;
}
}
return dummy.next;
}
}
237. 删除链表中的节点(链表基操)
从本题可以得知,删除链表元素,前驱节点还是必备得!
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public void deleteNode(ListNode node) {
/**
分析:
按道理说应该是得有前驱节点才可以删除的,但是本题规定了每个节点的值是不一样的,那么就可以使用覆盖操作
*/
// 前驱节点 用来删除最后一个节点
ListNode pre = null;
while(node != null){
ListNode next = node.next;
if(next != null){
node.val = next.val;
}else{
pre.next = null;
}
pre = node;
node = node.next;
}
}
}
328. 奇偶链表(链表的基操+分治)
基本思想是分治,尤其要注意边界条件
/**
* 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 oddEvenList(ListNode head) {
/**
分析:
一提到原地算法,立马就想到快慢指针法,但是本题是链表啊,快慢指针原地修改是不现实的。
切换思路,构建奇数链表和偶数链表,然后将奇数链表末尾串联偶数链表头~
*/
// 特判
if(head == null || head.next == null || head.next.next == null){
// 对应状态为空节点 一个节点 两个节点
return head;
}
// 记录奇数链表 和 偶数链表的工作指针
ListNode odd = head, even = head.next;
// 记录偶数链表的头节点,便于后续串联
ListNode evenHead = head.next;
// 当偶数节点不空时,构造两条链表
// 保证odd节点一定是非空的
while(even != null && even.next != null){
// 构造奇数链表
odd.next = even.next;
odd = even.next;
// 构造偶数链表
even.next = odd.next;
even = odd.next;
}
// 连接两条链表
odd.next = evenHead;
return head;
}
}
725. 分隔链表(链表的基操)
/**
* 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[] splitListToParts(ListNode head, int k) {
/**
分析:
考察链表的基操
*/
int len = 0;
ListNode p = head;
while(p != null){
len++;
p = p.next;
}
ListNode[] res = new ListNode[k];
if( len <= k){
// 小于k个,直接切割
for(int i = 0; i < len; i++){
ListNode next = head.next;
head.next = null;
res[i] = head;
head = next;
}
}else{
// 大于k个,就考虑切割分配个数问题
// 主要围绕着 基数+余数的问题
// 基数是 len / k ,余数是 len % k,若余数 = 2,表示前2个基数基础基础上+1
ListNode temp = head;
for(int i = 0; i < k; i++){
// 找到循环分配后的尾结点
ListNode tail = spilt(temp,len,k,i);
// 记录下一个节点
ListNode next = tail.next;
// 切割
tail.next = null;
// 保存
res[i] = temp;
temp = next;
}
}
return res;
}
public ListNode spilt(ListNode temp,int len,int k,int i){
// count 是基数
int count = len / k;
// 判断余数 分配的回数,可以画图
if(len % k >= (++i) ){
count++;
}
ListNode tail = temp;
// 找到尾结点
for(int j = 0; j < count - 1; j++){
tail = tail.next;
}
return tail;
}
}
上面的代码是基于链表的切割,也就是在原地修改,那么完全可以采用另一种思路(复制链表),不在原地切割修改,而是复制出我们需要的链表出来,这样的代码更加简洁(但是需要空间复杂度)
class Solution {
public ListNode[] splitListToParts(ListNode root, int k) {
int len = 0;
ListNode curr = root;
while (curr != null) {
len++;
curr = curr.next;
}
int width = len / k;
int remainder = len % k; // 余数
ListNode[] res = new ListNode[k];
curr = root;
for (int i = 0; i < k; i++) {
// 定义个虚拟节点
ListNode dummyNode = new ListNode(-1);
// 复制链表的节点
ListNode node = dummyNode;
// 定义真实长度
int realWidth = width + (i < remainder ? 1 : 0);
// 不断复制想要的链表
for (int j = 0; j < realWidth; j++) {
node.next = new ListNode(curr.val);
node = node.next;
if (curr != null) curr = curr.next;
}
// 将复制的链表储存起来
res[i] = dummyNode.next;
}
return res;
}
}
876. 链表的中间结点(快慢指针)
/**
* 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) {
// 想寻找倒数第K+1个节点,链表中间节点,这些都是使用快慢指针
// 快指针走2步,慢指针走1步,最后慢指针就是中点了
ListNode fast,slow;
fast = slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
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 addTwoNumbers(ListNode l1, ListNode l2) {
/**
分析:
一般思路是将两个链表转换为数字,数字相加,得到答案,然后将答案逆序放回链表中.
经过思考后,发现实现十分麻烦,不如直接将链表的对应数组相加,然后判断有无进位。
这是一道加法模板题,只不过场景换成了链表
*/
// 定义一个结果链表(有点像伪头部法)
ListNode res = new ListNode();
// 定义一个工作节点,指向首节点
ListNode cur = res;
// 默认进位为 0
int carry = 0;
while(l1 != null || l2 != null){
// 定义对应数字
int x = l1 == null ? 0 : l1.val;
int y = l2 == null ? 0 : l2.val;
int sum = x + y + carry;
// 构造节点
cur.next = new ListNode(sum % 10);
// 更新进位
carry = sum / 10;
// 移动工作节点
cur = cur.next;
// 移动l1和l2
// 一般而言为了避免空指针异常,都要进行异常判断
if(l1 != null){
l1 = l1.next;
}
if(l2 != null){
l2 = l2.next;
}
}
// 处理最后一个进位信息
if(carry != 0){
// 尾巴增加一个节点
cur.next = new ListNode(carry);
}
// 返回结果
return res.next;
}
}
21. 合并两个有序链表(虚拟头节点+递归法的理解)
/**
* 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 l1, ListNode l2) {
/**
分析:
通过不断比对,然后将节点添加到新链表中。
*/
// 虚拟头结点的妙处
ListNode dummmy = new ListNode(-1);
ListNode cur = dummmy;
// 两条链表都不为空 那么循环判断
while(l1 != null && l2 != null){
if(l1.val <= l2.val){
cur.next = l1;
l1 = l1.next;
cur = cur.next;
}else{
cur.next = l2;
l2 = l2.next;
cur = cur.next;
}
}
// 有一条链表为空了,直接连接下一条链表
if(l1 == null){
cur.next = l2;
}else if(l2 == null){
cur.next = l1;
}
return dummmy.next;
//
}
}
使用递归解法:无非就是关注递归出口,本级节点逻辑,本级节点返回值
/**
* 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 l1, ListNode l2) {
// 使用递归解法
// 递归出口:当 l1为空,那么直接返回 l2,当l2为空,那么直接返回l1
if( l1 == null) return l2;
if( l2 == null) return l1;
// 只关注本级节点做了什么,以及本级节点的返回值是什么
if( l1.val <= l2.val){
// l1 作为一个头节点,向下递,mergeTwoLists(l1.next,l2)是已经处理好的后续递归链表
l1.next = mergeTwoLists(l1.next,l2);
// 本级节点最后返回最终的链表头节点
return l1;
}else{
l2.next = mergeTwoLists(l1,l2.next);
return l2;
}
}
}
23. 合并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 ListNode mergeKLists(ListNode[] lists) {
/**
分析:
是合并两个有序链表的升级版,既然是多个链表合并,那么完全可以将这k个链表两两合并,然后调用两两合并的代码。
或者使用优先队列,按节点值构造小顶堆,然后依次取堆顶元素,构造一个新的队列
*/
// 解法一:使用两两合并链表
// 特判
// 对于数组,不但要判断它是否为空指针,也需要判断它是否有内容,同时要先判断空指针再判断长度是否为0,顺序不能颠倒,因为空指针没有length属性
// 常见的就是申请了一个对象空间为0的数组, int[] res = new int[0]; 此时长度为0
if(lists == null || lists.length == 0) return null;
// 取第一条链表
ListNode res = lists[0];
// 遍历合并
for(int i = 1; i < lists.length; i++){
res = merge_two_lists(res,lists[i]);
}
return res;
}
public ListNode merge_two_lists(ListNode l1,ListNode l2){
if(l1 == null) return l2;
if(l2 == null) return l1;
if( l1.val <= l2.val){
l1.next = merge_two_lists(l1.next,l2);
return l1;
}else{
l2.next = merge_two_lists(l1,l2.next);
return l2;
}
}
}
使用小顶堆,自定义数据结构要使用lambda表达式
/**
* 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 mergeKLists(ListNode[] lists) {
// 使用优先队列,但是要注意,这里优先队列使用的是小顶堆,里面存放的是k条链表
// 定义优先队列 默认小顶堆,但是listnode是自定义的,所以要使用lambada自定小顶堆
if(lists == null || lists.length == 0) return null;
PriorityQueue<ListNode> pq = new PriorityQueue<>((a,b) -> a.val - b.val);
// 储存
for(ListNode node:lists){
// 储存的是k条链表,按照链表头升序
// 链表数组中有可能出现空链表的情况
if(node == null) continue;
pq.add(node);
}
// 定义虚拟头节点
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
while( !pq.isEmpty()){
// 取出链表头较小的点
ListNode min = pq.remove();
// 构造新链表
cur.next = min;
cur = cur.next;
// 若取出的链表下面还有节点,那么重新加入队列中
if(min.next != null){
pq.add(min.next);
}
}
return dummy.next;
}
}
86. 分隔链表(分治思想)
/**
* 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 partition(ListNode head, int x) {
/**
分析:
考察链表的基操:构建两条链表,一条小链表一条大链表
*/
if(head == null || head.next == null) return head;
ListNode smallHead = new ListNode(-1);
ListNode small = smallHead;
ListNode largeHead = new ListNode(-1);
ListNode large = largeHead;
while( head != null){
if( head.val < x){
// 构建小链表
small.next = head;
small = small.next;
}else{
// 构建大链表
large.next = head;
large = large.next;
}
// head移动
head = head.next;
}
// 大链表 末尾为空
large.next = null;
// 小链表连接大链表
small.next = largeHead.next;
return smallHead.next;
}
}
147. 对链表进行插入排序
/**
* 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 insertionSortList(ListNode head) {
/**
分析:
大的方向是考察链表的基操,小的方向是考察链表的插入操作
*/
if(head == null || head.next == null){
return head;
}
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = head;
ListNode cur = head.next;
while( cur != null){
if(cur.val >= pre.val){
// 向后移动
pre = cur;
cur = cur.next;
}else{
// 找到小于 cur的最大节点p
ListNode p = dummy;
while( p.next != null && p.next.val < cur.val){
p = p.next;
}
// 删除cur节点
pre.next = cur.next;
// 将cur插入到p和p.next之间
cur.next = p.next;
p.next = cur;
// 回正cur
cur = pre.next;
}
}
return dummy.next;
}
}
148. 排序链表(自顶向下归并排序)
/**
* 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 sortList(ListNode head) {
/**
分析:
本题的时间复杂度要求是O(nlogn),自然会想到归并排序,快速排序和堆排序,链表的操作一般是使用归并排序。
自顶向下的归并排序的核心是划分两个子链表(使用快慢指针法),然后将两个排好序的子链表合并(这里可以使用递归合并)
*/
// 递归出口
if(head == null || head.next == null) return head;
// 找到中间节点
ListNode fast = head.next, slow = head;
while(fast != null && fast.next != null){
// 快指针走两步
fast = fast.next.next;
// 慢指针走一步
slow = slow.next;
}
// 记录右边子链表
ListNode rightHead = slow.next;
// 切割
slow.next = null;
// 递归寻找左子链表
ListNode left = sortList(head);
// 递归寻找右子链表
ListNode right = sortList(rightHead);
// 本级节点合并两个链表
return merge(left,right);
}
// 定义合并两个链表的方法 21题合并两个链表
// 这里使用两种办法,就当做是复习
public ListNode merge(ListNode left,ListNode right){
// 迭代法
// ListNode dummy = new ListNode(-1);
// ListNode cur = dummy;
// while(left != null && right != null){
// if(left.val <= right.val){
// cur.next = left;
// cur =cur.next;
// left = left.next;
// }else{
// cur.next = right;
// cur = cur.next;
// right = right.next;
// }
// }
// if( left == null){
// cur.next = right;
// }
// if( right == null){
// cur.next = left;
// }
// return dummy.next;
// 递归法
// 定义递归出口
if( left == null) return right;
if( right == null) return left;
if( left.val <= right.val){
left.next = merge(left.next,right);
return left;
}else{
right.next = merge(left,right.next);
return right;
}
}
}
160. 相交链表(数学方法)
/**
* 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) {
/**
分析:
解法一:哈希表走天下
解法二:指针法,A指针走完就去走B,B指针走完就去走A,核心思想是 a+c+b = b+c+a,两个指针相遇的点就是焦点
*/
// Set<ListNode> set = new HashSet<>();
// while(headA != null){
// set.add(headA);
// headA = headA.next;
// }
// while(headB != null){
// if(set.contains(headB)){
// return headB;
// }
// headB = headB.next;
// }
// return null;
if(headA == null || headB == null) return null;
ListNode a = headA;
ListNode b = headB;
while( a != b){
a = a != null ? a.next:headB;
b = b != null ? b.next:headA;
}
return a;
}
}
234. 回文链表(快慢指针找中间节点+反转链表)
/**
* 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 boolean isPalindrome(ListNode head) {
/**
分析:
双指针法:单链表的缺点就是无法往后走,那么从中间拆分两个链表然后一一比对,若值不同,那么必然不是回文链表
*/
if(head == null || head.next == null) return true;
// 通过快慢指针法找到中间节点
ListNode fast = head.next, slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
// 找到右边链表
ListNode left = head;
ListNode newHead = slow.next;
// 切断链表
slow.next = null;
// 反转链表
ListNode right = reverse(newHead);
// 从右边的链表开始,因为左边的链表长度是大于等于右边的(考虑到奇数的存在)
while(right != null){
if(left.val != right.val) return false;
left = left.next;
right = right.next;
}
return true;
}
public ListNode reverse(ListNode head){
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
445. 两数相加 II(反转链表 + 加法模板 + 头插法)
/**
* 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 addTwoNumbers(ListNode l1, ListNode l2) {
/**
分析:
反转链表 + 加法模板 + 头插法
*/
l1 = reverse(l1);
l2 = reverse(l2);
ListNode cur1 = l1;
ListNode cur2 = l2;
int pos = 0;
ListNode dummy = new ListNode(-1);
// ListNode cur = dummy;
while( cur1 != null || cur2 != null){
// 定义节点值
int x = cur1 == null ? 0 : cur1.val;
int y = cur2 == null ? 0 : cur2.val;
int sum = x + y + pos;
// 生成节点
ListNode temp = new ListNode(sum % 10);
// 头插法
temp.next = dummy.next;
dummy.next = temp;
// 更新进位
pos = sum / 10;
// 移动
if(cur1 != null){
cur1 = cur1.next;
}
if( cur2 != null){
cur2 = cur2.next;
}
}
// 判断最后进位
if(pos != 0){
ListNode temp = new ListNode(1);
// 头插法
temp.next = dummy.next;
dummy.next = temp;
}
return dummy.next;
}
public ListNode reverse(ListNode head){
ListNode pre = null;
ListNode cur = head;
while(cur != null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}