1.指定区间反转 | Leetcode92题 (1.头插法/2.穿针引线法) |
---|---|
2.两两交换链表中的结点 | Leetcode24题 |
3.单链表加1 | Leetcode369题 |
4.链表加法 | Leetcode445题 (1.栈实现/2.链表反转实现) |
前言:基本反转代码
public static ListNode reverseList(ListNode head) {
// 初始化前一个节点为null
ListNode prev = null;
// 当前节点为头结点
ListNode curr = head;
// 遍历链表
while (curr != null) {
// 保存下一个节点的引用
ListNode next = curr.next;
// 将当前节点指向前一个节点,实现反转
curr.next = prev;
// 前一个节点移到当前节点
prev = curr;
// 当前节点移到下一个节点
curr = next;
}
// 返回反转后的链表的头结点
return prev;
}
1.指定区间反转
这里的处理方式有很多种,甚至给个名字都有点困难,干脆就分别叫穿针引线法和头插法。穿针引线法本质上就是不带有结点的方式进行反转,而头插法本质上就是带头结点的反转。
1.1头插法
方法 一的缺点是:如果left和right的区域很大,恰好是链表的头结点和尾结点时,找到left和right需要遍历一次,反转他们直接的链表还需遍历一次,虽然总的时间复杂度为O(N),但是遍历了链表2次,可不可以只遍历一次呢?
答案是可以的,我们依然画图进行说明,我们仍然以方法一的序列为例子进行说明
反转的整体思想是:在需要反转的区间里,遍历每一个结点,让这个新的结点来到反转部分的起始位置。上面的图展示了整个流程图。
这个过程就是前面的带虚拟结点的插入操作,每走一步都要考虑各种指针怎么指,既要将结点摘下来对应到的位置上,还要保证后续结点能够找到,代码如下:
public static ListNode reverseBetween2(ListNode head, int left, int right) {
// 创建一个虚拟节点dummyNode,并将其指向头结点head,这是一种常见的处理方式
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode pre = dummyNode; // pre指针用于记录需要反转的区间的前一个节点
for (int i = 0; i < left - 1; i++) {
pre = pre.next; // 将pre指针移动到需要反转区间的起始位置的前一个节点
}
ListNode cur = pre.next; // cur指针指向需要开始反转的节点
ListNode next;
for (int i = 0; i < right - left; i++) {
next = cur.next; // next指针用于记录cur指针下一个需要处理的节点
cur.next = next.next; // 将cur节点指向next的下一个节点,即删除next节点
next.next = pre.next; // 将next节点插入到需要反转区间的起始位置之前
pre.next = next; // 更新pre的next指针,使其指向插入的next节点
}
return dummyNode.next; // 返回反转后的链表头节点
}
1.2穿针引线法
我们以反转下图中蓝色区域的链表反转为例:
我们可以这么做:先确定好需要反转的部分,也就是下图的left到right之间,然后再将三段链表拼接起来。这种方式类似于裁缝一样,找准位置减下来,再缝回去。这样问题就变成了如何标记下图的四个位置,以及如何反转left到right之间的链表。
算法步骤:
- 第一步:先将反转的区域反转
- 第二步:把pre的next指针向反转以后的链表头结点,把反转以后的链表的尾结点的next指针指向succ。
代码如下:
public static ListNode reverseBetween(ListNode head, int left, int right) {
// 因为头节点有可能发生变化,使用虚拟头节点可以避免复杂的分类讨论
ListNode dummyNode = new ListNode(-1);
dummyNode.next = head;
ListNode pre = dummyNode;
// 第 1 步:从虚拟头节点走 left - 1 步,来到 left 节点的前一个节点
// 建议写在 for 循环里,语义清晰
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;
// 注意:切断链接
// pre.next = null;
rightNode.next = null;
// 第 4 步:同第 206 题,反转链表的子区间
reverseList(leftNode);
// 第 5 步:接回到原来的链表中
pre.next = rightNode;
leftNode.next = succ;
return dummyNode.next;
}
public static ListNode reverseList(ListNode head) {
// 初始化前一个节点为null
ListNode prev = null;
// 当前节点为头结点
ListNode curr = head;
// 遍历链表
while (curr != null) {
// 保存下一个节点的引用
ListNode next = curr.next;
// 将当前节点的指针指向前一个节点,实现反转
curr.next = prev;
// 更新prev指针为当前节点
prev = curr;
// 更新curr指针为下一个节点
curr = next;
}
// 返回反转后的链表的头结点
return prev;
}
2.两两交换链表中的结点
LeetCode24.题目给你一个链表,两两交换其中相邻的结点,并返回交换后链表的头结点,你必须在不修改结点内部的值的情况下完成本题(即只能进行结点交换)。
这道题因为要成对反转,所以我们可以先画图来看一下调转的时候如何调整每一个结点的指向。
如果原始的顺序是dummy->node1->node2,交换后面两个结点关系要变成dummy->node2->node1,事实上我们只要多执行一次next就可以拿到后面的元素,也就是类似node2=cur.next.next这样的操作。
两两交换链表中的结点,新的链表的头结点是dummyHead.next,返回新的链表的头结点即可,指针的题哦啊真可以参考如下图所示:
完整代码
//直接根据两个指针的情况来设置
public static ListNode swapPairs(ListNode head) {
ListNode dummyHead = new ListNode(0);
dummyHead.next = head; // 创建一个虚拟头节点dummyHead,并将其指向链表的头节点head
ListNode cur = dummyHead; // 创建一个cur指针,初始指向虚拟头节点
while (cur.next != null && cur.next.next != null) {
ListNode node1 = cur.next; // node1指针指向需要交换的第一个节点
ListNode node2 = cur.next.next; // node2指针指向需要交换的第二个节点
cur.next = node2; // 将cur的next指针指向node2节点,实现交换
node1.next = node2.next; // 将node1的next指针指向node2的下一个节点
node2.next = node1; // 将node2的next指针指向node1节点,实现交换
cur = node1; // 将cur指针移动到下一组需要交换的节点的前一个节点
}
return dummyHead.next; // 返回交换后的链表头节点
}
3.单链表加1
LeetCode369 用一个非空链表来表示一个非负整数,然后将这个整数加一,你可以假设这个整数除了0本身,没有任何前导的0,这个整数的各个数位按照高位在链表头部、低位在链表尾部的顺序排列。
示例:
输入:[1,2,3]
输出:[1,2,4]
我们先看一下加法的计算过程:
计算是从低位开始的,而链表是从高位开始的,所以要处理就必须反转过来,此时可以使用栈,也可以使用链表反转来实现。
基于栈实现的思路不算复杂,先把题目给出的链表遍历放到栈中,然后从栈中弹出栈顶数字digit,加的时候再考虑一下进位的情况就ok了。加完之后根据是否大于0决定视为下一次要进位。
public static ListNode plusOne(ListNode head) {
Stack<Integer> st = new Stack(); // 创建一个栈,用于存储链表节点的值
while (head != null) {
st.push(head.val); // 将链表节点的值压入栈中
head = head.next; // 移动到下一个节点
}
int carry = 0; // 进位标志,初始为0
ListNode dummy = new ListNode(0); // 创建一个虚拟头节点dummy,并初始化为0
int adder = 1; // 加数,初始为1
while (!st.empty() || carry > 0) {
int digit = st.empty() ? 0 : st.pop(); // 如果栈为空,则将digit设置为0;否则从栈中弹出一个值作为digit
int sum = digit + adder + carry; // 将digit、adder和进位carry相加得到sum
carry = sum >= 10 ? 1 : 0; // 如果sum大于等于10,则设置进位carry为1;否则为0
sum = sum >= 10 ? sum - 10 : sum; // 如果sum大于等于10,则sum减去10
ListNode cur = new ListNode(sum); // 创建一个新的节点cur,值为sum
cur.next = dummy.next; // 将cur的next指针指向dummy的下一个节点
dummy.next = cur; // 更新dummy的next指针,使其指向cur
adder = 0; // 将adder重置为0,以便下一次循环中只加上进位carry
}
return dummy.next; // 返回dummy的下一个节点,即加一后的链表头节点
}
简单解释一下,carry的功能是记录进位的。adder这个主要表示就是需要加的 数1,也就是为了满足单链表加1的功能,那这里是否直接能使用1,而不再定义变量呢?
也就是将上面的while循环改成下面的样子:
while (!st.empty() || carry > 0) {
int digit = st.empty() ? 0 : st.pop();
int sum = digit + 1 + carry;
carry = sum >= 10 ? 1 : 0;
sum = sum >= 10 ? sum - 10 : sum;
ListNode cur = new ListNode(sum);
cur.next = dummy.next;
dummy.next = cur;
}
很遗憾不可以,否则的话,会将我们每个位置都要加1.例如,如果原始链表是{7,9}这里就会将其变成 {8,9}而不是我们要的{7,9},导致这样的原因是循环处理链表的每个结点元素的时候sum=digit+1+carry这一行会将每一个位置都多加了一个1,所以我们要使用变量adder,只有第一次是加了1,之后该变量变成0,就不会影响我们后续计算。
4 .链表加法LeetCode445题链接
这个题目的难点在于存放是从最高位向最低位开始的,但是因为低位会产生进位的问题,计算的时候必须从最低位开始,所以我们必须想办法将链表结点的元素返回过来。
怎么反转呢?
栈和链表反转都可以,这两种方式我们都看一下。
(1)使用栈实现
思路是:先将两个链表的元素分别压栈,然后再一起出栈,将两个结果分别计算,之后对比计算结果取模,模数保存到新的链表中,进位保存到下一轮,完成之后再进行一次反转就可以。
我们知道在链表 插入有头插法和尾插法两种。头插法就是每次都将新的结点插入到head之前。而尾插法就是将新结点都插入到链表的表尾。
二者区别就是尾插法的顺序与原始链表是一致的,而头插法与原始链表是逆序的,所以上面的最后一步如果不想进行反转,可以将新结点以头插法。
public static ListNode addInListByStack(ListNode head1, ListNode head2) {
Stack<ListNode> st1 = new Stack<ListNode>(); // 创建两个栈,用于存储链表节点
Stack<ListNode> st2 = new Stack<ListNode>();
while (head1 != null) {
st1.push(head1); // 将链表节点压入栈中
head1 = head1.next; // 移动到下一个节点
}
while (head2 != null) {
st2.push(head2);
head2 = head2.next;
}
ListNode newHead = new ListNode(-1); // 创建新链表头节点,初始值为-1
int carry = 0; // 进位标志,初始为0
// 当两个栈都不为空或者进位标志不为0时,循环迭代
while (!st1.empty() || !st2.empty() || carry != 0) {
ListNode a = new ListNode(0); // 创建两个新节点a和b,初始值为0
ListNode b = new ListNode(0);
if (!st1.empty()) { // 如果栈st1不为空,则弹出栈顶元素并赋给a
a = st1.pop();
}
if (!st2.empty()) { // 如果栈st2不为空,则弹出栈顶元素并赋给b
b = st2.pop();
}
int get_sum = a.val + b.val + carry; // 将a、b和进位carry相加得到get_sum
int ans = get_sum % 10; // 对get_sum取余得到本位的值
carry = get_sum / 10; // 将get_sum除以10得到进位carry
ListNode cur = new ListNode(ans); // 创建一个新节点cur,值为ans
cur.next = newHead.next; // 将cur的next指针指向newHead的下一个节点
newHead.next = cur; // 更新newHead的next指针,使其指向cur
}
return newHead.next; // 返回newHead的下一个节点,即相加后的链表头节点
}
(2)使用链表反转实现
如果使用链表反转,先将两个链表分别反转,最后计算完之后再将结果反转,一共有三次反转操作,所以必然将反转抽取一个比较好,代码如下
public static ListNode addInListByReverse(ListNode head1, ListNode head2) {
head1 = reverse(head1); // 翻转链表head1
head2 = reverse(head2); // 翻转链表head2
ListNode head = new ListNode(-1); // 创建新链表头节点,初始值为-1
ListNode cur = head; // 创建当前节点指针cur,初始指向新链表头节点
int carry = 0; // 进位标志,初始为0
while (head1 != null || head2 != null) { // 当两个链表任意一个不为空时,循环迭代
int val = carry; // 本位的值初始为进位carry
if (head1 != null) { // 如果链表head1不为空,则将head1的值累加到val中,并移动head1指针到下一个节点
val += head1.val;
head1 = head1.next;
}
if (head2 != null) { // 如果链表head2不为空,则将head2的值累加到val中,并移动head2指针到下一个节点
val += head2.val;
head2 = head2.next;
}
cur.next = new ListNode(val % 10); // 创建一个新节点,值为val对10取余
carry = val / 10; // 获取进位carry,即val除以10的商
cur = cur.next; // 移动当前节点指针cur到下一个节点
}
if (carry > 0) { // 如果最后还有进位carry,创建一个新节点,值为carry,并将其链接到新链表的末尾
cur.next = new ListNode(carry);
}
return reverse(head.next); // 返回翻转后的新链表头节点
}
private static ListNode reverse(ListNode head) {
ListNode cur = head; // 创建当前节点指针cur,初始指向head
ListNode pre = null; // 创建前一个节点指针pre,初始为null
while (cur != null) { // 当当前节点cur不为空时,循环迭代
ListNode temp = cur.next; // 临时保存当前节点的下一个节点
cur.next = pre; // 将当前节点的next指针指向前一个节点,实现翻转
pre = cur; // 移动前一个节点指针pre到当前节点
cur = temp; // 移动当前节点指针cur到下一个节点
}
return pre; // 返回翻转后的头节点
}