链表反转的拓展问题
一、指定区间反转
LeetCode92:给你单链表的头指针head和两个整数left和right,其中left<=right。请你反转从位置left到位置right的链表节点,返回反转后的链表。
1.1 头插法
反转的整体思想是,在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。下面的图展示了整个流程。
class Solution {
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy, last= dummy;
int num = 0;
for(num = 0; num < left - 1; num++){
pre = pre.next;
}
ListNode cur = pre.next;
for(int i = 1; i <= right - left; i++){
ListNode next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy.next;
}
}
1.2 穿针引线法
算法步骤:
第1步:先将待反转的区域反转;
第2步:把pre的next指针指向反转以后的链表头节点,把反转以后的链表的尾节点的next指针指向Succ。
public ListNode reverseBetween(ListNode head,int left,int right){
//因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode pre = dummyNode;
//第1步:从虚拟头节点走1eft-1步,来到Left节点的前一个节点
//建议写在fo「循环里,语义清晰
for (int i = 0;i < left -1; i++)
pre = pre.next;
//第2步:从pre再走right-left+1步,来到right节点
ListNode rightNode = pre;
for (int i = 0;i < right - left +1; i++)
rightNode = rightNode.next;
//第3步:切出一个子链表
ListNode leftNode = pre.next;
ListNode succ = rightNode.next;
//思考一下,如果这里不设置next为null会怎么样
rightNode.next null;
//第4步:同第206题,反转链表的子区间
reverseLinkedList(leftNode);
//第5步:接回到原来的链表中
//想一下,这里为什么可以用rightNode
pre.next = rightNode;
leftNode.next = succ;
return dummyNode.next;
}
private void reverseLinkedList(ListNode head){
//也可以使用递归反转一个链表
ListNode pre = null;
ListNode cur = head;
while(cur!=null){
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
}
二、两两交换链表中的节点
LeetCode24.给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)
指针调整如下
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy, cur = head;
while(cur != null && cur.next != null){
ListNode next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
//两两交换,要把pre移到本次交换后的第二个元素,cur移到下一次交换的第一个元素
pre = cur;
cur = cur.next;
}
return dummy.next;
}
}
三、单链表加1
LeetCode369.用一个非空单链表来表示一个非负整数,然后将这个整数加一。你可以假设这个整数除了0本身,没有任何前导的0。这个整数的各个数位按照高位在链表头部、低位在链表尾部的顺序排列。
我们先看一下加法的计算过程:
计算是从低位开始的,而链表是从高位开始的,所以要处理就必须反转过来,此时可以使用栈,也可以使用链表反转来实现。
基于栈实现的思路不算复杂,先把题目给出的链表遍历放到栈中,然后从栈中弹出栈顶数字digit,加的时候再考虑一下进位的情况就ok了,加完之后根据是否大于0决定视为下一次要进位。
public ListNode plusone(ListNode head){
Stack<Integer>st = new Stack();
while(head != null){
st.push(head.val);
head head.next;
}
int carry = 0; //表示是否进位
ListNode dummy = new ListNode(0);
int adder = 1; //负责给个位数加1
while (!st.empty() || carry != 0){
int digit = st.empty() ? 0 : st.pop();
int sum = digit + adder + carry;
carry = sum >10 ?1 :0;
sum = sum > 10 ? sum -10 : sum;
ListNode cur = new ListNode(sum);
cur.next = dummy.next;
dummy.next = cur;
adder = 0;
}
return dummy.next;
}
四、链表加法
相加相链表是基于链表构造的一种特殊题,反转只是其中的一部分。这个题还存在进位等的问题,因此看似简单,但是手写成功并不容易。
LeetCode445题,给你两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。你可以假设除了数字0之外,这两个数字都不会
以零开头。示例:
(1)使用栈实现
思路是先将两个链表的元素分别压栈,然后再一起出栈,将两个结果分别计算。之后对计算结果取模,模数保存到新的链表中,进位保存到下一轮。完成之后再进行一次反转就行了。
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode headsum = new ListNode(0);
//创建两个栈分别存储l1和l2的每一位
Deque<ListNode> stack1 = new LinkedList();
Deque<ListNode> stack2 = new LinkedList();
while(l1 != null){
stack1.push(l1);
l1 = l1.next;
}
while(l2 != null){
stack2.push(l2);
l2 = l2.next;
}
int judge = 0;//判断是否有进位
while(!stack1.isEmpty() || !stack2.isEmpty() || judge == 1){
//初始化结点为0,巧妙处理一个栈为空另外一个栈不为空的情形
ListNode node1 = new ListNode(0);
ListNode node2 = new ListNode(0);
if(!stack1.isEmpty()) node1 = stack1.pop();
if(!stack2.isEmpty()) node2 = stack2.pop();
int number = (node1.val + node2.val) + judge;
ListNode num = new ListNode(number % 10);
//顺势解决链表反转问题
num.next = headsum.next;
headsum.next = num;
judge = number > 9 ? 1 : 0;
}
return headsum.next;
}
}
(2)使用链表实现
如果使用链表反转,先将两个链表分别反转,最后计算完之后再将结果反转,一共有三次反转操作,所以必然将反转抽取出一个方法比较好,代码如下:
public class Solution{
public ListNode addInList(ListNode head1,ListNode head2){
head1 = reverse(head1);
head2 = reverse(head2);
ListNode head = new ListNode(-1);
ListNode cur = head;
int carry = 0;
while(head1 != null || head2 != null){
int val = carry;
if (head1 != null){
val += head1.val;
head1 head1.next;
}
if (head2 != null){
val += head2.val;
head2 = head2.next;
}
cur.next = new ListNode(val % 10);
carry = val / 10;
cur = cur.next;
}
if (carry > 0){
cur.next = new ListNode(carry);
}
return reverse(head.next);
}
}
private ListNode reverse(ListNode head){
ListNode cur = head;
ListNode pre = null;
while(cur != null){
ListNode temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
}
return pre;
}
}
五、再论链表的回文序列问题
在上一关介绍链表回文串的时候,我们介绍的是基于栈的,相对来说比较好理解,但是除此之外还有可以使用链表反转来进行,而且还可以只反转一半链表,这种方式节省空间。
我们姑且称之为“快慢指针+一半反转”法。
这个实现略有难度,主要是在while循环中pre.next=prepre和orepre=pre两行实现了一边遍历一边将访问过的链表给反转了,所以理解起来有些难度,如果不理解可以在学完链表反转之后再看这个问题。
public boolean isPalindrome(ListNode head){
if(head =null || head.next =null){
return true;
}
ListNode slow = head,fast = head;
ListNode pre = head,prepre = null;
while(fast != null && fast.next != null){
pre = slow;
slow = slow.next;
fast = fast.next.next;
//将前半部分链表反转
pre.next prepre;
prepre pre;
}
if(fast != null){
slow = slow.next;
}
while(pre != null && slow != null){
if(pre.val != slow.val){
return false;
}
pre = pre.next;
slow = slow.next;
}
return true;
}