文章目录
剑指Offer Day2 – Leetcode练习题
第二天 – 链表
剑指 Offer 06. 从尾到头打印链表
题目:
输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
题目来源力扣官网
链接: https://leetcode.cn/problems/cong-wei-dao-tou-da-yin-lian-biao-lcof
昨天Day01做了栈和队列有关的题目,一看到这个 逆序,首先就想到用辅助栈存储
但写到一半发现,返回的是int数组啊!!!
再一个个地放到新建的数组中?并没有觉得有多简便……
跟遍历到底部再处理成数组没啥区别,相反感觉浪费了空间。
解决:
遍历到链表底层,记录下链表长度
根据链表长度创建对应大小的数组
从头结点开始遍历,元素的存放顺序是从数组的 右边 到左边
代码:
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public int[] reversePrint(ListNode head) {
int length = 0;
ListNode node = head;
while (node != null) {
++ length;
node = node.next;
}
int[] nums = new int[length];
for (int i = nums.length-1; i >= 0; --i) {
nums[i] = head.val;
head = head.next;
}
return nums;
}
}
事实证明,处理逆序
元素,辅助栈并不是万能的,大道至简。
要结合场景使用,有时候遍历反而是最优的解法。
剑指 Offer 24. 反转链表
题目:
输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
题目来自力扣官网
链接:https://leetcode.cn/problems/fan-zhuan-lian-biao-lcof/
这是第一道自己很快就做出来的题目!
1、遍历链表,用辅助栈存储值得到逆序输出
2、递归改变各个节点值
提交成功
代码:
class Solution {
public ListNode reverseList(ListNode head) {
Deque<Integer> deque = new ArrayDeque<>();
ListNode node = head;
while (node != null) {
deque.push(node.val);
node = node.next;
}
change(deque,head);
return head;
}
public void change(Deque<Integer> deque,ListNode head) {
if (head != null && deque.size() > 0) {
head.val = deque.pop();
if (head.next != null) {
change(deque,head.next);
}
}
}
}
然而跟评论区大佬一比较发现自己写的太拉了,比如这位:
太优雅了!
剑指 Offer 35. 复杂链表的复制 (完全不会做)
题目:
请实现 copyRandomList 函数,复制一个复杂链表。
在复杂链表中,每个节点除了有一个 next 指针指向下一个节点,还有一个 random 指针指向链表中的任意节点或者 null。
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/fu-za-lian-biao-de-fu-zhi-lcof
看了半天才理解题意,题目就是要我们复制这个链表。谜底就在谜面儿上
因为链表的每个节点都存有一个 random
的Node,是随机的,我们要考虑指向的问题。
题目难度就在这儿。
链表的遍历是存在顺序的,由于random
是随机位置的。
节点被引用前不一定就是完整的。
官方给出两种解法:
- 哈希+回溯
- 迭代+节点拆分
(1)哈希+回溯 (算法技巧)
用 HashMap
记录 原节点(key) + 复制节点(value)
/**原节点,复制节点**/
Map<Node, Node> cachedNode = new HashMap<Node, Node>();
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
if (!cachedNode.containsKey(head)) {//当前节点未进行复制
Node headNew = new Node(head.val);//复制一个新的节点
cachedNode.put(head, headNew);//原节点和新节点放进map
headNew.next = copyRandomList(head.next);//主动去创建新节点的next
headNew.random = copyRandomList(head.random);//主动去创建新节点的ramdom
}
//如果已经存在复制好的节点则直接返回
return cachedNode.get(head);//返回当前节点的复制节点
}
利用map确实方便,只是牺牲了内存。
这里涉及到回溯,因为copyRandomList
过程中,虽然调用了多次本身
但在递归地创造新的复制节点,这个过程中,并不是无脑地去遍历,而是带有返回值的。
一旦map中存在当前节点的复制节点,那么直接拿了返回上一层,完全不用再往下递归一步。
这种在步骤中及时返回结果
的思想称为回溯
。
自己调自己,体现的是递归在代码结构上的特点。
回溯的思想,是可以基于递归实现的。
(2)迭代 + 节点拆分 (链表特性)
迭代 和 递归:
递归: 不断地往下层探索,直到获取到目标
。
迭代: 迭代是一个循环,在达到结束状态跳出之前,每次迭代,都是旧值代替新值
,然后继续迭代,直到达到结束的状态。
官方的解法是:
逐个地复制新的节点在原节点的后面
每次操作的,都是上一次复制后产生的新链表
相比较Map,原地修改 更考验
操作链表的步骤细节
- 原节点: 原链表中的每个Node
- 复制节点: 针对每个原节点,复制的一个Node。
- 值和原节点的值一样
复制节点.next
指向原节点.next的复制节点
复制节点.ramdom
指向原节点.ramdom的复制节点
1、复制新的节点 并放在 原节点的后面
因为复制节点的ramdom
一定指向另一个复制节点
,但那个复制节点
并不一定已经创建了。
所以,先把每个原节点的复制节点
都建起来,并插入到原节点后面,但先不管ramdom
。
比如:
原链表:1 – > 2 – > 3 – > 4
第一次:1 – > 1
– > 2 – > 3 – > 4
第二次:1 – > 1
– > 2 – > 2
– > 3 – > 4
……
复制完后:
1 – > 1
– > 2 – > 2
– > 3 – > 3
– > 4 – > 4
1
一定在 1 后面,2
一定在 2 的后面……
a作为原节点 ,a和b中间,插入了a 的复制节点,b不再是a的next了。
a.next
一定是a的复制节点
,那么要保持后面的链表元素不丢失。
a的
复制节点
的代替a 指向 b
然后a只要指向 它的复制节点就行了。
a.next.next -> b
a.next -> a.next.next
同理:
b.next – > b.next.next
b.next.next
– > c
c.next – > c.next.next
……
复制新节点next代码:
Node nowNode = head;
while (nowNode != null) {//当前不是最后的null节点
Node copyNode = new Node(nowNode.val);//复制原节点的值
copyNode.next = nowNode.next;//代替原节点指向下一个原节点
nowNode.next = copyNode; //原节点指向复制节点
nowNode = nowNode.next.next;//切换到下一个原节点继续进行复制
}
(注:迭代的循环,并不局限于使用
while
,for
也可以)
此时形成的
组合链表
:
- 当前原节点的
.next
必定是对应的复制节点- 当前原节点的下一个的下一个Node,才是下一个原节点
- 只要原节点不是null,这个原节点的
.next
必定是对应的复制节点
2、复制随机指针ramdom
我要想要指向某个复制节点
,只需要知道它的原节点
复制节点
必定在对应的 原节点 之后。
当前节点的ramdom的next,就是复制节点
需要指向的ramdom。
nowNode = head;
while (nowNode != null) { //是否为null节点
if (nowNode.random != null) { //随机指针是否指向某个复制节点
nowNode.next.random = nowNode.random.next;
}
nowNode = nowNode.next.next;//切换到下一个原节点
}
3、拆分链表
完成节点及节点指针的复制后,只需要
每个复制节点都摘出来,重新组成一个链表。
拆分链表需要考虑的步骤:
- 将下一个原节点
.next.next
重现放在当前原节点的后面.next
- 判断
当前复制节点
是否还存在下一个复制节点
如何判断?
此时当前复制节点
的.next
暂时还指向下一个原节点
下一个原节点不是null,下一个原节点的.next
自然是对应的复制节点
。 当前复制节点
的.next.next
自然就是它需要重新指向的.next
Node copyHead = head.next;
nowNode = head;
Node copyNode = head.next;//首个复制节点
while (nowNode != null) {
nowNode.next = nowNode.next.next;//下一个原节点重新回到当前原节点的后面
nowNode = nowNode.next;//切换到下一个原节点
if (copyNode.next != null) {//复制节点后面是否还存在原节点
copyNode.next = copyNode.next.next;//复制节点和它后面原节点的复制节点相连
copyNode = copyNode.next;//切换到下一个复制节点
}
}
return copyHead;
所有:
public Node copyRandomList(Node head) {
if (head == null) {
return head;
}
// 复制节点,先不管ramdom
Node nowNode = head;
while (nowNode != null) {
Node copyNode = new Node(nowNode.val);//建立复制节点
copyNode.next = nowNode.next;//下一个原节点放在复制节点后面
nowNode.next = copyNode;//复制节点放在原节点后面
nowNode = nowNode.next.next;//切换到下一个原节点
}
// 针对原节点,完成复制节点的ramdom复制
nowNode = head;
while (nowNode != null) {
if (nowNode.random != null) { // 判断当前节点随机指针是否指向某个复制节点
nowNode.next.random = nowNode.random.next;
}
nowNode = nowNode.next.next;//切换到下一个原节点
}
// 复制节点单独摘出来组成新链表
Node copyHead = head.next;
nowNode = head;
Node curCopy = head.next;
while (nowNode != null) {
nowNode.next = nowNode.next.next;
nowNode = nowNode.next;
if (curCopy.next != null) {
curCopy.next = curCopy.next.next;
curCopy = curCopy.next;
}
}
return copyHead;
}
总结:
三个问题主要还是体现的链表的数据结构的特点。
- 因为知道头结点就能遍历整个链表,所以在
操作对象涉及到整个链表的节点
,操作步骤可重复
,考虑使用迭代或递归. - 熟悉前后节点 建立链接和断链。