这是我参与8月更文挑战的第29天,活动详情查看:8月更文挑战
复杂链表的复制
剑指Offer 35.复杂链表的复制
难度:中等
请实现copyRandomList
函数,复制一个复杂链表。在复杂链表中,每个节点除了有一个next
指针指向下一个节点,还有一个random
指针指向链表中的任意节点或者null
。
示例1:
输入:head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
输出:[[7,null],[13,0],[11,4],[10,2],[1,0]]
示例2:
输入:head = [[1,1],[2,1]]
输出:[[1,1],[2,1]]
示例3:
输入:head = [[3,null],[3,0],[3,null]]
输出:[[3,null],[3,0],[3,null]]
示例4:
输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。
提示:
- -10000 <= Node.val <= 10000
Node.random
为空(null)或指向链表中的节点- 节点数目不超过1000
题解
难点:本题主要是新增了random指针,在复制链表过程中需要新链表各节点的random引用指向,如下:(random无法确定指向)
var copyRandomList = function(head) {
let cur = head;
let dum = new Node(0), pre = dum;
while(!cur){
let node = new Node(cur.val); // 复制节点 cur
prev.next = node; // 新链表的 前驱节点->当前节点
// prev.random = ??? // 无法确定
cur = cur.next; // 遍历下一节点
pre = node; // 保存当前节点
}
return dum.next;
};
法一 哈希表
利用哈希表的查询特点,构建原链表节点和新链表对应节点的键值对映射关系,再遍历构建新链表各节点的next
和random
引用指向即可。
流程:
- 若头节点head为空,返回null。
- 初始化:新建哈希表,节点cur指向头节点。
- 复制链表:
- 建立新节点,并向map添加键值对(原cur节点,新cur节点)。
- cur遍历至原链表下一节点。
- 构建新链表的引用指向:
- 构建新节点next和random引用指向。
- cur 遍历至原链表下一节点。
- 返回值:新链表的头节点。
/**
* // Definition for a Node.
* function Node(val, next, random) {
* this.val = val;
* this.next = next;
* this.random = random;
* };
*/
/**
* @param {Node} head
* @return {Node}
*/
var copyRandomList = function(head) {
if(!head) return null;
const map = new Map(); // 新建哈希表
let cur = head;
while(cur){
map.set(cur,new Node(cur.val))
cur = cur.next;
}
cur = head;
while(cur){
map.get(cur).next = cur.next ? map.get(cur.next) : null;
map.get(cur).random = cur.random ? map.get(cur.random) : null;
cur = cur.next;
}
return map.get(head);
};
-
时间复杂度O(N):两轮遍历链表
-
空间复杂度O(N):使用哈希表的额外空间
法二 拼接 + 拆分
构建旧节点1 -> 新节点1 -> 旧节点2 -> 新节点2 -> ...
的拼接链表,如此可以方便旧节点的random指向节点的同时找到新对应新节点的random指向节点。
步骤:
- 复制各节点,并构建拼接链表
- 构建各新节点的random指向,当访问cur.random时,对应新节点cur.next的随机指向节点为cur.random.next。
- 拆分旧新链表,设置pre和cur分别指向旧链表和新链表的头节点,通过遍历,讲pre和cur两条链表拆分开。
- 返回新链表的头节点res。
var copyRandomList = function(head) {
if(head === null) return null;
// 1.复制各节点,并构建拼接链表
while(cur){
let tmp = new Node(cur.val);
tmp.next = cur.next;
cur.next = tmp;
cur = tmp.next;
}
// 2.构建各新节点的random指向
cur = head;
while(cur){
if(cur.random){
cur.next.random = cur.random.next;
}
cur = cur.next.next;
}
// 3.拆分两链表
cur = head.next;
let pre = head, res = head.next;
while(cur.next){
pre.next = pre.next.next;
cur.next = cur.next.next;
pre = pre.next;
cur = cur.next;
}
pre.next = null; // 处理旧链表表尾
return res; // 返回新链表头节点
}
- 时间复杂度O(N):三轮遍历链表
- 空间复杂度O(1):引用变量使用常数大小的空间。
二叉搜索树与双向链表
剑指Offer 36.二叉搜索树与双向链表
难度:中等
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
为了让您更好地理解问题,以下面的二叉搜索树为例:
我们希望将这个二叉搜索树转化成双向循环链表。链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
下面展示了上面的二叉搜索树转换化成的链表。“head”表示指向链表中有最小元素的节点。
特别地,我们希望可以就地完成转换操作。当转化完成以后,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。还需要返回链表中的第一个节点的指针。
题解
根据二叉搜索树的性质,可以推断出应该采用中序遍历。
法一 递归中序遍历
/**
* @param {Node} root
* @return {Node}
*/
var treeToDoublyList = function(root){
if(!root) return;
let head = null;
let pre = head;
inorder(root);
head.left = pre;
pre.right = head;
return head;
/**
* @param {Node} node
*/
function inorder(node){
if(!node) return;
// 遍历左子树
inorder(node.left,pre);
// 遍历当前根节点
if(!pre){
// 遍历到最小的节点(最左边的节点),此时节点就是双向链表的head
head = node;
}else{
pre.right = node;
}
node.left = pre;
pre = node;
// 遍历右子树
inorder(node.right,pre);
}
}
- 时间复杂度O(N)
- 空间复杂度O(N)
法二 非递归中序遍历
利用栈来模拟递归过程
var treeToDoublyList = function(root) {
if(!root) return;
const stack = [];
let node = root
let pre = null;
let head = null;
while(stack.length || node){
if(node){
stack.push(node);
node = node.left;
}else{
const top = stack.pop();
if(!pre){
head = top;
}else{
pre.right = top;
}
top.left = pre;
pre = top;
node = top.right;// 用于遍历右子树
}
}
head.left = pre;
pre.right = head;
return head;
}
坚持每日一练!前端小萌新一枚,希望能点个赞
哇~