紧接上一章
3.2 链表的反转
3.2.1 LeetCode #206 反转链表
题目描述
反转一个单链表。
进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
解题思路:
- 思路1:迭代反转
- 思路2:递归反转,一次拆掉一个节点并递归处理剩余的子链表
解法一:迭代反转
定义三个指针:
pre指针指向的是已翻转链表的头节点
cur指针指向的是未翻转部分的头节点
next指针指向的是未翻转部分的头节点的下一个节点
- 翻转节点
- 将cur指针所指向的节点指向pre指针所指向的节点
- 然后移动pre指针到cur所在的位置,移动cur指针到next所在位置,next指针移动到cur指针所指向节点的下一个节点,此时已经翻转了第一个节点
- 循环翻转节点的操作,直到当cur指针指向null时,说明已经完成整个链表的翻转
/**
* 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 null;
}
ListNode pre = null;//已经翻转链表的头节点
ListNode cur = head;//待翻转链表的头节点
ListNode next = head.next;//待翻转链表头节点下一个节点
while (cur != null) {
cur.next = pre;
pre = cur;
cur = next;
next = cur == null ? null : cur.next;
}
return pre;
}
}
解法二:递归反转
递归思想:先递归遍历整个链表的最后一个节点,然后往回返的过程中在处理,往回返的过程其实就是逆序的遍历每个节点,递归的回溯过程其实就相当于在逆序遍历链表
/**
* 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){
//如果链表为null或者只有一个节点,直接返回该头节点,不需要翻转
return head;
}
//否则翻转,但是只处理head一个节点,将head.next委托递归给reverseList
ListNode tail = head.next;//记录将来已翻转区域的尾节点
ListNode newHead = reverseList(head.next);//将当前节点next后面的链表翻转,返回翻转后的头节点
tail.next = head;//将已翻转区域的尾节点接上当前节点
head.next = null;
return newHead;
}
}
就性能来说递归方式没有迭代方式好,但是我们主要是为了锻炼编码能力
扩展:添加参数控制反转链表要翻转的节点个数
实例,比如:
输入: 1->2->3->4->5->NULL,我要翻转前3个节点
输出: 3->2->1->4->5->NULL
public class LeetCode206_Ext {
public static void main(String[] args) {
ListNode head = new ListNode(1);
head.next = new ListNode(2);
head.next.next = new ListNode(3);
head.next.next.next = new ListNode(4);
head.next.next.next.next = new ListNode(5);
ListNode pre = head;//专门定义一个指针遍历
while (pre != null) {
System.out.print(pre.val);
pre = pre.next;
}
System.out.println("\n---");
ListNode newHead = reverseListNum(head, 3);
while (newHead != null) {
System.out.print(newHead.val);
newHead = newHead.next;
}
}
public static ListNode reverseListNum(ListNode head, int num) {
if (head == null || head.next == null || num == 1) {
//如果链表为null或者只有一个节点,直接返回该头节点,不需要翻转
//或者超过指定数量
return head;
}
//否则翻转,但是只处理head一个节点,将head.next委托递归给reverseList
ListNode tail = head.next;//记录将来已翻转区域的尾节点,注意是 "已翻转区域的尾节点"
ListNode newHead = reverseListNum(head.next, num - 1);//将当前节点next后面的链表翻转,返回翻转后的头节点
ListNode pre = tail.next;//此时需要定义一个指针维护已翻转区域尾节点后面没有翻转的节点
//因为head.next后面的链表区域并不是翻转所有节点
tail.next = head;//将已翻转区域的尾节点接上当前节点
head.next = pre;//将当前节点接上后面没有翻转的节点
return newHead;
}
}
3.2.2 LeetCode #92 反转链表II
题目描述
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明:1 ≤ m ≤ n ≤ 链表长度。
解题思路:
- 技巧:使用虚拟头结点(dummy head)
- 因为如果从头节点开始翻转,那么链表的首地址会发生改变,为了简化临界条件判断,添加一个虚拟节点
- 思路1:迭代反转
- LeetCode206我们知道翻转一个链表最少要3个指针,现在我们要做的就是在翻转前先确定要翻转的链表区域
- 思路2:递归反转
- LeetCode92扩展中我们已经实现了递归翻转指定个数的前几个节点,已经确定了要翻转区域的尾节点,在这个的基础上只要再确定从哪开始翻转就可以了
解法一:迭代反转
LeetCode206我们知道翻转一个链表最少要3个指针:
pre指针指向的是已翻转链表的头节点
cur指针指向的是未翻转部分的头节点
next指针指向的是未翻转部分的头节点的下一个节点
现在我们要做的就是在翻转前先确定要翻转的链表区域,我们需要额外定义2个指针:
start为反转区域翻转后的前驱连接节点
end为反转区域翻转后的后驱连接节点
首先创建虚拟头节点,然后确定翻转区域
- 定义start指针指向虚拟头节点,向后移动m-1个节点
- 定义end指针指向虚拟头节点,向后移动n+1个节点
翻转待翻转区域
- 定义pre指针,指向end指针指向的节点,为待反转区域翻转后的头节点
- 定义cur指针为start.next指向的节点,为未翻转部分的头节点
- 定义next指针为cur.next指向的节点,为未翻转部分的头节点的下一个节点
按照206翻转逻辑循环翻转,直到cur指针到达了end指针结束循环,完成翻转。
翻转完成以后将start指针指向翻转区域的头节点即可。
/**
* 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) {
ListNode hair = new ListNode(0, head);//定义虚拟节点
//确定翻转范围
ListNode start = hair;
ListNode end = hair;
int pace = left - 1;//定义start指针的步伐
while (pace > 0) {
start = start.next;
pace--;
}
pace = right - left + 2;//end指针步伐
end = start;//end从start开始走,少走几步
while (pace > 0) {
end = end.next;
pace--;
}
//确定范围以后开始翻转
ListNode pre = end;
ListNode cur = start.next;
ListNode next = cur.next;
while (cur != end) {
cur.next = pre;
pre = cur;
cur = next;
next = cur == null ? null : cur.next;
}
//翻转完毕后
start.next = pre;
return hair.next;
}
}
解法二:递归反转
递归思路:之前我们已经实现了一个函数可以指定反转链表的头n位,现在我们只需要找到待反转区域的前一位,然后再反转待反转区域的头n-m+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 reverseBetween(ListNode head, int left, int right) {
//先找到待反转区域的前一个节点,有可能反转第一个节点,所以创建一个虚拟头节点
ListNode hair = new ListNode(0, head);
ListNode pre = hair;//pre为待反转区域前一个节点
int num = right - left + 1;
while (--left > 0) {
pre = pre.next;
}
pre.next = reverseListNum(pre.next, num);
return hair.next;
}
public ListNode reverseListNum(ListNode head, int num) {
if (head == null || head.next == null || num == 1) {
//如果链表为null或者只有一个节点,直接返回该头节点,不需要翻转
//或者超过指定数量
return head;
}
//否则翻转,但是只处理head一个节点,将head.next委托递归给reverseList
ListNode tail = head.next;//记录将来已翻转区域的尾节点,注意是 "已翻转区域的尾节点"
ListNode newHead = reverseListNum(head.next, num - 1);//将当前节点next后面的链表翻转,返回翻转后的头节点
head.next = tail.next;//将当前节点接上后面没有翻转的节点
//因为head.next后面的链表区域并不是翻转所有节点
tail.next = head;//将已翻转区域的尾节点接上当前节点
return newHead;
}
}
3.2.3 LeetCode #25 K个一组翻转链表
题目描述
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗?
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
解题思路:
- 思路:先判断是否有K个元素,有的话对这K个节点进行反转 然后拼接一下首尾部分,然后再循环判断剩余未翻转区域是否还有K个元素,有的话继续重复翻转操作,拼接首尾节点,直到不满足K个元素
迭代解法:
和上一题类似,首先翻转一个链表最少要3个指针:
pre指针指向的是已翻转链表的头节点
cur指针指向的是未翻转部分的头节点
next指针指向的是未翻转部分的头节点的下一个节点
其次需要额外定义2个指针,在翻转前先确定待翻转的链表区域:
start为反转区域翻转后的前驱连接节点
tail为反转区域翻转前的反转区域内的最后一节点
(需要tail判空决定是否有K个节点)end为反转区域翻转后的后驱连接节点
这里判断区域,只需要满足从头开始每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 ListNode reverseKGroup(ListNode head, int k) {
if (head == null) {
return head;
}
//创建虚拟头节点
ListNode hair = new ListNode(0, head);
//先判断是否有K个节点
ListNode start = hair;
ListNode tail = getEndByK(start, k);
while (tail != null) {
//tail 不为null说明满足K个节点
ListNode pre = tail.next;
ListNode cur = start.next;
ListNode newTail = cur;
ListNode next = cur.next;
//开始反转
ListNode end = tail.next;//记录停止反转的边界条件
while (cur != end) {
cur.next = pre;
pre = cur;
cur = next;
next = cur == null ? null : cur.next;
}
//反转完成后连接头部
start.next = pre;
//继续找下一个K个节点的反转区域
//start = getEndByK(start, k);
start = newTail;
tail = getEndByK(start, k);
}
return hair.next;
}
/**
* @param start 待反转区域的前驱连接节点
* @param k 向后找第几个节点
* @return
*/
private ListNode getEndByK(ListNode start, int k) {
while (k-- > 0 && start != null) {
start = start.next;
}
return start;
}
}
递归解法:
利用LeetCode #206 中已经实现的可以指定反转链表的头n位的函数,只需要循环判断是否有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 reverseKGroup(ListNode head, int k) {
ListNode hair = new ListNode(0, head);
ListNode pre = hair;
//循环寻找k个节点
ListNode end = getTail(pre, k);
while (end != null) {
ListNode tail = pre.next;
ListNode newHead = reverseListNum(pre.next, k);
pre.next = newHead;
pre = tail;
end = getTail(pre, k);
}
return hair.next;
}
private ListNode getTail(ListNode pre, int k) {
while (k-- > 0 && pre != null) {
pre = pre.next;
}
return pre;
}
public static ListNode reverseListNum(ListNode head, int num) {
if (head == null || head.next == null || num == 1) {
//如果链表为null或者只有一个节点,直接返回该头节点,不需要翻转
//或者超过指定数量
return head;
}
//否则翻转,但是只处理head一个节点,将head.next委托递归给reverseList
ListNode tail = head.next;//记录将来已翻转区域的尾节点,注意是 "已翻转区域的尾节点"
ListNode newHead = reverseListNum(head.next, num - 1);//将当前节点next后面的链表翻转,返回翻转后的头节点
head.next = tail.next;//此时需要定义一个指针维护已翻转区域尾节点后面没有翻转的节点
//因为head.next后面的链表区域并不是翻转所有节点
tail.next = head;//将已翻转区域的尾节点接上当前节点
return newHead;
}
}
3.2.4 LeetCode #61 旋转链表
题目描述:
给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。
解题思路
思路:把整个链表首尾相接,然后从头节点向后走 length-(K%length)-1 位(或者从尾节点向后走length-(k%length)),找到新链表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 rotateRight(ListNode head, int k) {
if (head == null) {
return null;
}
int length = 1;//链表长度
ListNode tail = head;
while (tail.next != null) {
tail = tail.next;
length++;
}
//连接成环
tail.next = head;
k = length - (k % length);
while (k-- > 0) {
tail = tail.next;
}
head = tail.next;
tail.next = null;
return head;
}
}
3.2.5 LeetCode #24 两两交换链表中的节点
题目描述
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
解题思路
- 思路1:与LeetCode #25完全一致,是K = 2的简单情形。(略)
- 思路2:可以简化不需要那么多指针了,只需要四个指针即可。
思路2解法:
定义4个指针
start为反转区域翻转后的前驱连接节点
end为反转区域翻转后的后驱连接节点
one指针指向的是要翻转的第一个节点
two指针指向的是要翻转的第二个节点
- 每次按顺序让one指向end,two指向one,start指向two,完成一次翻转
- 然后移动指针start到one,one到start.next,two到one.next,end到two.next
- 只要one或者two为null说明不够翻转,结束
/**
* 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) {
if (head == null) {
return null;
}
ListNode hair = new ListNode(0, head);
ListNode start = hair;
ListNode one = null;
ListNode two = null;
//ListNode end = null;
while (start.next != null && start.next.next != null) {
one = start.next;
two = start.next.next;
one.next = two.next;//省去了end指针
two.next = one;
start.next = two;
//完成翻转
start = one;
}
return hair.next;
}
}
3.3 链表的节点删除
3.3.1 LeetCode #19 删除链表的倒数第N个节点
题目描述
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?
解题思路:
- 思路一:第一次遍历计算链表length,第二次遍历指针移动 length-n-1步找到要删除节点前一个节点进行删除(略)
- 思路二:一趟扫描,
找到待删除节点待前一个节点(需要虚拟头),需要两个指针一开始相距n个节点,然后一起向后走,当后面指针走到null时,说明前个指针指向的就是要删除节点的前一个节点
,然后进行删除操作调整指针
解法二:
/**
* 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) {
ListNode hair = new ListNode(0, head);
ListNode q = hair;
ListNode p = head;
while (n-- > 0 && p != null) {
p = p.next;
}
while (p != null) {
p = p.next;
q = q.next;
}
q.next = q.next.next;
return hair.next;
}
}
3.3.2 LeetCode #83 删除排序链表中的重复节点
题目描述
给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
解题思路:
定义快慢两个指针指向头节点,判断两个节点值是否相等
- 如果相等,快指针往后走一步,慢指针不动
- 如果不相等,将慢指针指向快指针所指向的节点,并将慢指针移到快指针的位置
一直循环操作,直到快指针的下一个指针为null结束。
一个指针解法:
/**
* 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) {
if (head == null) {
return head;
}
ListNode p = head;
while (p.next != null) {
if (p.val == p.next.val) {
//相等删除下个节点
p.next = p.next.next;
} else {
p = p.next;
}
}
return head;
}
}
3.3.3 LeetCode #82 删除排序链表中的重复节点II
题目描述:
给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。
解题思路:
- 需要定义一个虚拟头节点,因为如果头节点重复删除的话头节点地址会发生变化
- 定义两个指针
- 定义指针pre,一开始指向虚拟头节点,作为待删除区的前置节点指针,因为现在是重复的全部删除,需要维护删除节点的前一个节点才能进行删除,比较的时候比较pre.next和pre.next.next
- 定义指针notEqual,当发现
遍历的时候比较cur.val 和 cur.next.val
- 如果不相等,那么pre移动到cur,cur移动到cur.next
- 如果相等,pre不动,cur移动到cur.next,继续重复比较
重复上述操作,直到cur.next为null结束。
解法:
/**
* 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) {
if (head == null) {
return null;
}
ListNode hair = new ListNode(0, head);
ListNode notEqual = null;//定义一个寻找不相等节点的指针
ListNode pre = hair;
while (pre.next != null) {
if (pre.next.next != null && pre.next.val == pre.next.next.val) {
//相等,寻找下个不相等的节点
notEqual = pre.next.next;
while (notEqual != null && notEqual.val == pre.next.val) {
notEqual = notEqual.next;
}
//此时unlikeness.next为不相等的节点
pre.next = notEqual;
} else {
//如果pre后面两个节点值不相等,可以往前走一步
pre = pre.next;
}
}
return hair.next;
}
}
3.4 总结
什么情况下才需要使用虚拟头节点?
链表的头地址有可能改变的情况
,才考虑用虚头简化操作(比如插入、删除操作,都有可能改变头地址)- 虚头的作用简化了链表操作边界条件的处理