链表
翻转链表
206.反转链表
92. 反转链表 II
25. K 个一组翻转链表
Node定义
public class Node {
// 链表
Node head;
int val;
Node next;
public Node() {
}
public Node(int val) {
this.val = val;
}
public Node(int val, Node next) {
this.val = val;
this.next = next;
}
public Node(int ... nodes) {
if (nodes.length > 0) {
head = new Node(nodes[0]);
Node cur = head;
for (int i = 1; i < nodes.length; i++) {
cur = cur.next = new Node(nodes[i]);
}
}
}
public Node getHead() {
return head;
}
public void setHead(Node head) {
this.head = head;
}
public int getVal() {
return val;
}
public void setVal(int val) {
this.val = val;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
}
双指针
反转链表
:::info
- 假如链表是a,b,c,d
- 翻转后的链表a是不指向任何节点,所以注意把a–>b断开
- cur的存在是表示当前操作的结点,也就是如果cur是b,就表示把b的指针改变方向
- pre的存在是为了保存前一个节点,因为要改变b的指针方向,需要知道b将来应该指向谁,这里就是要指向pre
- next的存在是为了提前保存c节点,因为一旦b的节点改变了方向指向了a,不提前保存c,就永远无法找到c了,从而无法完成后面的翻转
- 操作完一个节点后,就操作下一个节点,但是前提是下一个节点不为空,所以while的终止条件就是next==null,因为此时next赋值给cur就没有操作的意义了
@Test
public void test0() throws Exception {
Node head = new Node(1, 2, 3, 4, 5).getHead();
if (head == null) {
return;
}
Node pre = null;
Node cur = head;
Node next = cur.getNext();
cur.setNext(null);
while (next != null) {
pre = cur;
cur = next;
next = next.getNext();
cur.setNext(pre);
}
while (cur != null) {
System.out.println(cur.getVal());
cur = cur.getNext();
}
}
反转链表 II
:::info
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
- 在反转链表的基础上完成
- 添加一个i计数,标识cur的位置,cur到left索引指定的位置才会开始反转,到right索引为止停止反转
- 需要 preLast、midFirst,分别保存原链表 left-1 位置、left位置 的节点
- 注意边界问题,可以将边界问题转化成:执行多少次循环==修改多少次指向(也就是i < right ,而不是i <= right)
- 如果 left==1 表示从头开始反转,head就动了,就不能直接返回原链表的head
:::
@Test
public void test1() throws Exception {
Node head = new Node(1, 2, 3, 4, 5, 6).getHead();
int left = 2, right = 4;
if (head == null) {
return;
}
Node pre = null;
int i = 1;
Node cur = head;
Node next = cur.getNext();
while (next != null && i < left) {
pre = cur;
cur = next;
next = next.getNext();
i++;
}
// 保存前一个链表的最后一个节点
Node preLast = pre;
// 保存待反转链表的第一个节点
Node midFirst = cur;
while (next != null && i < right) {
pre = cur;
cur = next;
next = next.getNext();
cur.setNext(pre);
i++;
}
if (null != preLast) {
preLast.setNext(cur);
}
midFirst.setNext(next);
// 注意这里,因为left==1 表示从头开始反转,head就动了
if (left == 1){
head = cur;
}
while (head != null) {
System.out.println(head.getVal());
head = head.getNext();
}
}
K 个一组翻转链表
:::info
给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。
k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
- 下面这是一个循环
- int i;计数cur走过的索引下标
- 达到k个就执行反转
- 不达到k个,指的是前面几组反转完成后,剩下的节点不足k个
- k个节点一组
- k个节点,断开这k个节点子链表的前后链表(其实只需要断开后面,因为前面的已经被上一组断开了)
- 反转这个子链表
- 声明resLast保存上一组子链表的最后一个节点,因为当前子链表反转完成后需要拼接到上一组后面
- 在链表断开前,声明nextFirst保存下一组子链表的第一个节点,因为还需要继续反转下一组
- i<k的时候,剩下的子链表不需要翻转,可以 把 nextFirst直接拼接到 resLast 后面即可
- int i;计数cur走过的索引下标
- 注意边界问题
- 外层循环 终止条件是 cur != null ,需要保证所有节点拼接到resLast后,cur赋值为null,否则无法结束循环(因为在最后一组k个节点反转循环中 设置了 cur = headTmp)
- 但是如果外层循环 终止条件是 cur.next != null ,就会导致 在 最后不足k个的节点是一个的时候,出现这个节点没有拼接到resLast的问题,因为 cur = headTmp; 会导致 cur.next==null,直接进入不了外层循环,就把最后一个节点落下了
:::
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
if (head == null) {
return head;
}
/*
head
如果k>链表长度,head不变
否则 第一组的 结果链表的 头结点 是最终结果链表的head
下面这是一个循环
k个一组,断开翻转,再接上,
断开前,需要保存 下一个节点,nextFirst 保存
等第二组反转完成在接到第一组后面
resLast 保存上一组的最后一个节点,用来拼接
headTmp 每一组要反转的节点的head
int i 索引位置计数,达到k就断开,断开后重置
需要用 flag 标识是否是第一次循环,因为只有第一组的结果链表的 尾结点 才是最终的head
*/
int i = 1;
ListNode nextFirst = head;
ListNode headTmp = nextFirst;
ListNode cur = headTmp;
ListNode resLast = headTmp;
// 是第一次循环吗,是第一组吗
boolean flag = true;
while (cur != null) {
while (cur.next != null && i < k) {
cur = cur.next;
i++;
}
// k个才能成为一组,小于k个不反转
if (i == k) {
i = 1;
nextFirst = cur.next;
// 断开
cur.next = null;
// 反转后 headTmp 成为了结果链表的尾结点
ListNode reverse = reverse(headTmp);
if (flag) {
head = reverse;
flag = false;
} else {
// 接上
resLast.next = reverse;
}
resLast = headTmp;
headTmp = nextFirst;
cur = headTmp;
} else {
// 拼接最后一组不足k个的链表
resLast.next = headTmp;
// 为了终止外层循环
cur = null;
}
}
return head;
}
public ListNode reverse(ListNode head) {
if (head == null) {
return head;
}
ListNode pre = null;
ListNode cur = head;
ListNode next = cur.next;
cur.next = null;
while (next != null) {
pre = cur;
cur = next;
next = next.next;
cur.next = pre;
}
return cur;
}
}
递归方法
:::info
递归方法这里其实只需要看下第一个就可以,因为其他的进阶的链表反转的题目都跟指针法一样,都是在基础的基础上加上计数
- 递归就是大问题化成小问题
- 因此想象只有一个 head 节点的情况,反转的结果就是 返回head
- 两个节点:head、head.next
- 递归调用 反转 head.next,返回的就是当前的head.next
- 返回结果赋值为 last
- head.next.next=head; // 指向自己,实现反转
- head.next=null; // 断开链表,避免循环
- return last; // 其实每一层递归都是返回last,因为last是结果链表的head,也就是最终返回结果
- 除了last,last之前的所有节点都在自己的那一层递归中通过head和head.next引用,从而实现反转
:::
/**
* 反转链表 递归
*/
public class a92 {
/**
* 递归反转,想象head是有两个节点组成的链表
*
* @param head
* @return
*/
ListNode reverse(ListNode head) {
if (head.next == null) return head;
ListNode last = reverse(head.next);
head.next.next = head;
head.next = null;
return last;
}
//后继结点
ListNode successor = null;
/**
* 反转前n个节点
* 化整为零:
* 要反转的最后一个节点,下一个节点是后继结点
* 反转两个 1 个节点
*/
ListNode reverseN(ListNode head, int n) {
if (n == 1) {
successor = head.next;
return head;
}
//被反转部分的最后一个节点,将来是反转后链表的head,所以最后return head
ListNode last = reverseN(head.next, n - 1);
//想象反转两个节点
head.next.next = head;
head.next = successor;
return last;
}
/**
* 反转 m到n个节点
* 化整为零:
* head 一直往后走,把这个题目转变成反转前 n 个节点
* m==1,说明是反转前n个节点
* m!=1,head.next 是要被操作的链表,此时head还是没动,因此返回head
*/
ListNode reverseMtoN(ListNode head, int m, int n) {
if (m == 1) {
return reverseN(head, n);
}
head.next = reverseMtoN(head.next, m - 1, n - 1);
return head;
}
}
单链表是否有环
- set
- 双指针
- 快慢指针
- 快指针一次走两步
- 慢指针一次走一步
- 快慢指针相遇就表示有环
找到环入口
快慢指针,按照上面的步骤得知:
快慢指针相遇的时候:
- 快指针走了 y 步,长度就是 2y
- 慢指针 在环里 走了 x,(慢指针共走了 l+x)
- 起点到环的距离是 l
- 环的周长是 s
- 所以
- y=l+x
- 2y=l+x+ns
- 所以
- y=ns=l+x
- l=ns-x
- l=(n-1)s+(s-x)
- (s-x) 相当于从快慢指针相遇点到环的起点的距离
- 所以 快指针变成慢指针从相遇点开始走,慢指针从起始点开始走,再次相遇的位置就是环的起点
删除链表的倒数第n个节点
:::info
right先走n步,left开始走,左右指针的间隔为n,同时走到right.next==null,就把left.next位置的节点删除
:::
public ListNode removeNthFromEnd(ListNode head, int n) {
// 由于可能会删除链表头部,用哨兵节点简化代码
ListNode dummy = new ListNode(0, head);
ListNode left = dummy, right = dummy;
while (n-- > 0) {
right = right.next; // 右指针先向右走 n 步
}
while (right.next != null) {
left = left.next;
right = right.next; // 左右指针一起走
}
left.next = left.next.next; // 左指针的下一个节点就是倒数第 n 个节点
return dummy.next;
}