图示:
反转的整体思想是,在需要反转的区间里,每遍历到一个节点,让这个新节点来到反转部分的起始位置。
下图展示了整个流程:
/**
* 方法1:头插法
*
* @param head
* @param left
* @param right
* @return
*/
public ListNode reverseBetween(ListNode head, int left, int right) {
ListNode dummyNode=new ListNode(-1);
dummyNode.next=head;
ListNode pre=dummyNode;
//遍历pre结点到left位置结点的前一个结点
for(int i=0;i<left-1;i++){
pre=pre.next;
}
//当前结点记录left位置结点
ListNode cur=pre.next;
ListNode next;
//遍历次数就是right-left次
for(int i=0;i<right-left;i++){
//1.先记录下一个位置的结点
next=cur.next;
//2.让当前结点指向下一个结点的下一个结点
//随着下一个结点被移到left位置,会让当前结点不断指向再下一个结点
cur.next=next.next;
//3.让下一个结点指向left位置的结点
next.next=pre.next;
//4.让pre指向next结点
//3.4让下一个结点移到left位置
pre.next=next;
}
return dummyNode.next;
}
这是每次循环的具体步骤:
先确定好需要反转的部分,也就是下图的 left 到 right 之间,然后再将三段链表拼接起来。这种方式类似裁缝一样,找准位置减下来,再缝回去。这样问题就变成了如何标记下图四个位置,以及如何反转left到right之间的链表。
算法步骤:
第 1 步: 先将待反转的区域反转
第 2 步:把 pre 的 next 指针指向反转以后的链表头节点,把反转以后的链表的尾节点的
next 指针指向 succ。
/**
* 方法2:穿针引线法
*
* @param head
* @param left
* @param right
* @return
*/
public ListNode reverseBetween(ListNode head, int left, int right) {
// 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
ListNode dummyNode=new ListNode(-1);
dummyNode.next=head;
ListNode pre=dummyNode;
//1.确认第一段的结束位置,从虚拟头节点走left-1步,来到left结点前一个结点
for(int i=0;i<left-1;i++){
pre=pre.next;
}
ListNode rightNode=pre;
//2.确认反转链表区间的最右边的结点,从pre再走right-left+1步,来到right结点
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.反转链表的子区间
reverseLinkedList(leftNode);
//5.将链表穿插接,因为区间链表已经反转了,所以让pre.next指向rightNode
//leftNode.next指向最后一段,就将三段链表接在一块了
pre.next=rightNode;
leftNode.next=succ;
return dummyNode.next;
}
public ListNode reverseLinkedList(ListNode head){
//不使用虚拟结点进行反转
ListNode cur=head;
ListNode pre=null;
while(cur!=null){
ListNode next=cur.next;
cur.next=pre;
pre=cur;
cur=next;
}
return head;
}
方法二的缺点是: 如果left 和 right 的区域很大,恰好是链表的头节点和尾节点时,找到left 和 right需要遍历一次,反转它们之间的链表还需要遍历一次,虽然总的时间复杂度为 O(N),历了链表 2次,而方法一只需要遍历链表一次
如果原始顺序是 dummy -> node1 -> node2,交换后面两个节点关系要变成 dummy -> node2->node1,事实上我们只要多执行一次next就可以拿到后面的元素,也就是类似node2 = temp.next.next这样的操作。 两两交换链表中的节点之后,新的链表的头节点是 dummyHead.next,返回新的链表的头节点即可。指针的调整可以参考如下图示:
/**
*
* @param head
* @return
*/
public ListNode swapPairs(ListNode head) {
ListNode dummyNode=new ListNode(-1);
dummyNode.next=head;
ListNode temp=dummyNode;
while(temp.next!=null&&temp.next.next!=null){
ListNode node1=temp.next;
ListNode node2=temp.next.next;
//temp指向下下一个结点
temp.next=node2;
//然后让node1指向node2的下一个结点
node1.next=node2.next;
//然后再让node2指向node1,达成两两互换
node2.next=node1;
//同时让temp排在下一批要互换的结点前,就是此时node1
temp=node1;
}
return dummyNode.next;
}
用一个非空单链表来表示一个非负整数,然后将这个整数加一。你可以假设这个整数除了0本身,没有任何前导的 0。这个整数的各人数位按照 高位在链表头部、低位在链表尾部的顺序排列。
示例:输入: [1,2,3]输出: [1,2,4]
加法的计算过程:
计算是从低位开始的,而链表是从高位开始的,所以要处理就必须反转过来,此时可以使用栈,也可以使用链表反转来实现。
基于栈实现的思路不算复杂,先把题目给出的链表遍历放到栈中,然后从栈中弹出栈顶数字 digit,加的时候再考虑一下进位的情况就ok了,加完之后根据是否大于0决定视为下一次要进位。
/** 方法一:使用栈
* @param head
* @return {@link ListNode}
*/
public static ListNode plusOne(ListNode head) {
Stack<Integer> stack = new Stack<>();
//链表元素压栈
while (head != null) {
stack.push(head.val);
head = head.next;
}
//是否要进1
int carry = 0;
//建立一个新链表,用于拼接
ListNode dummy = new ListNode(0);
//添加的数,需要以变量方式,当低位加完数后就不再加
int addr = 1;
//栈空时结束或当前元素是最高位,并满10需要进1,此时栈空,carry就会>1
while (!stack.empty() || carry > 0) {
int digit = stack.empty() ? 0 : stack.pop();
//值+需要加的数+进1数
int sum = digit + addr + carry;
//判断是否要进1
carry = sum >= 10 ? 1 : 0;
//判断数是否大于10,大于10需减去10进一
sum = sum >= 10 ? sum - 10 : sum;
//添加结点到开头,使得链表反转回来
ListNode newNode = new ListNode(sum);
newNode.next = dummy.next;
dummy.next = newNode;
//加完数归0
addr = 0;
}
return dummy.next;
}
/** 方法二:通过链表反转,再反转回去
* @param head
* @return {@link ListNode}
*/
public static ListNode plusOne1(ListNode head) {
ListNode reverse = reverseLinkedList(head);
ListNode temp=reverse;
int carry=0;
int addr=1;
while (reverse!=null){
int val = reverse.val;
int sum=val+addr+carry;
carry=sum>=10?1:0;
sum=sum>=10?sum-10:sum;
//当下一个结点不存在并且需要进1时
if (reverse.next==null&&carry>0){
ListNode newNode = new ListNode(sum);
reverse.next=newNode;
}
reverse.val=sum;
reverse=reverse.next;
addr=0;
}
return reverseLinkedList(temp);
}
/**不利用头结点链表反转
* @param head
* @return {@link ListNode}
*/
public static ListNode reverseLinkedList(ListNode head){
if (head==null){
return null;
}
ListNode pre=null;
ListNode cur=head;
while (cur!=null){
ListNode next=cur.next;
cur.next=pre;
pre=cur;
cur=next;
}
return pre;
}
思路:先将两个链表的元素分别压栈,然后再一起出栈,将两个结果分别计算。之后对计算结果取模,模数保存到新的链表中,进位保存到下一轮。完成之后再进行一次反转就行了。
在链表插入有头插法和尾插法两种。头插法就是每次都将新的结点插入到head之前。而尾插法就是将新结点都插入到链表的表尾。两者的区别是尾插法的顺序与原始链表是一致的,而头插法与原始链表是逆序的,所以上面最后一步如果不想进行反转,可以将新结点以头插法
/**
* 通过栈来实现
* @param head1
* @param head2
* @return
*/
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
Stack<Integer> stack1=new Stack();
Stack<Integer> stack2=new Stack();
//两个链表压栈
while(l1!=null){
stack1.push(l1.val);
l1=l1.next;
}
while(l2!=null){
stack2.push(l2.val);
l2=l2.next;
}
int carry=0;
ListNode dummy=new ListNode(0);
//判断是否栈空和是否要进1
while(!stack1.empty()||!stack2.empty()||carry>0){
int sum=carry;
//栈不为空时取出元素求和
if(!stack1.empty()){
sum+=stack1.pop();
}
if(!stack2.empty()){
sum+=stack2.pop();
}
carry=sum>=10?1:0;
sum=sum>=10?sum-10:sum;
ListNode newNode=new ListNode(sum);
newNode.next=dummy.next;
dummy.next=newNode;
}
return dummy.next;
}
如果使用链表反转,先将两个链表分别反转,最后计算完之后再将结果反转,一共有三次反转操作,所以必然将反转抽取出一个方法比较好
/** 反转链表
* @param l1
* @param l2
* @return {@link ListNode}
*/
public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
l1=reverse(l1);
l2=reverse(l2);
ListNode head=new ListNode(0);
ListNode cur=head;
int carry=0;
while(l1!=null||l2!=null||carry>0){
int sum=carry;
if(l1!=null){
sum+=l1.val;
l1=l1.next;
}
if(l2!=null){
sum+=l2.val;
l2=l2.next;
}
carry=sum/10;
sum=sum%10;
ListNode newNode=new ListNode(sum);
cur.next=newNode;
}
return reverse(head);
}
/** 反转链表
* @param head
* @return {@link ListNode}
*/
public static ListNode reverse(ListNode head){
ListNode cur=head;
ListNode pre=null;
while(cur!=null){
ListNode next=cur.next;
cur.next=pre;
pre=cur;
cur=next;
}
return pre;
}
链表回文串的时候,介绍的是基于栈的,相对来说比较好理解,但是除此之外还有可以使用链表反转来进行,而且还可以只反转一半链表,这种方式节省空间。姑且称之为“快慢指针+一半反转”法。
这个实现方式的难度,主要是在while循环中pre.next = prepre和prepre = pre两行实现了一边遍历一边将访问过的链表给反转了
/**
* 通过快慢指针+链表反转的方式来判断
*
* @param head
* @return
*/
public static boolean isPalindromeByTwoPoints(ListNode head) {
if (head == null || head.next == null) {
return true;
}
ListNode slow = head, fast = head;
ListNode pre = head, prepre = null;
//pre是反转存储了前半段倒置的链表
while (fast != null && fast.next != null) {
//存储slow遍历的每个结点,直到中间
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;
}