面试题34.二叉树中和为某一值的路径
/**
public class TreeNode {
int val = 0;
TreeNode left = null;
TreeNode right = null;
public TreeNode(int val) {
this.val = val;
}
}
*/
public class Solution {
List<List<Integer>> res;
Deque<Integer> path;
public List<List<Integer>> FindPath(TreeNode root,int target) {
res = new ArrayList<>();
if(root == null) return res;
path = new ArrayDeque<>();
dfs(root, target);
return res;
}
public void dfs(TreeNode root, int target) {
if(root == null) return;
//将当前节点值 root.val 加入路径 path
path.addLast(root.val);
//目标值 target 从 sum 减至 0
target -= root.val;
// 当root为叶节点 且 路径和等于目标值(由于在这里用的是减法,所以只需判断target是否为0)
//则将此路径 path 加入 res
if(target == 0 && root.left == null && root.right == null) {
res.addLast(new ArrayList<>(path));
}
//递归左右子节点
dfs(root.left, target);
dfs(root.right, target);
//向上回溯前,需要将当前节点从路径 path 中删除
path.removeLast():
}
}
- 时间复杂度 O(N) : N 为二叉树的节点数,先序遍历需要遍历所有节点。
- 空间复杂度 O(N) : 最差情况下,即树退化为链表时,path 存储所有树节点,使用 O(N) 额外空间。
————————————————————————————————————————
面试题35.复杂链表的复制
方法一:拼接+拆分
用三步来实现:
-
第一步,根据遍历到的原节点创建对应的新节点,每个新创建的节点是在原节点后面,比如下图中原节点1不再指向原节点2,而是指向新节点1。
-
第二步,用来设置新链表的随机指针。原节点 i 的随机指针(如果有的话),指向的是原节点 j 那么新节点 i 的随机指针,指向的是原节点 j 的 next。
如:原节点1的随机指针指向原节点3,新节点1的随机指针指向的是原节点3的next -
第三步,将两个链表分离开,再返回新链表。
class Solution {
public Node copyRandomList(Node head) {
if(head==null) return null;
Node node = head;
//第一步,在每个原节点后面创建一个新节点
//1->1'->2->2'->3->3'
while(node !=null) {
Node tmp = new Node(node.val);
tmp.next = node.next;
node.next = tmp;
node = tmp.next;
}
node = head;
//第二步,设置新节点的随机节点
while(node !=null) {
if(node.random!=null) {
node.next.random = node.random.next;
}
node = node.next.next;
}
Node newHead = new Node(-1);
node = head;
Node cur = newHead;
//第三步,将两个链表分离
while(node !=null) {
cur.next = node.next;
cur = cur.next;
node.next = cur.next;
node = node.next;
}
return newHead.next;
}
}
- 时间复杂度 O(N) : 三轮遍历链表,使用 O(N) 时间。
- 空间复杂度 O(1) : 节点引用变量使用常数大小的额外空间。
哈希表
首先创建一个哈希表,再遍历原链表,遍历的同时再不断创建新节点
我们将原节点作为key,新节点作为value放入哈希表中
第二步我们再遍历原链表,这次我们要将新链表的next和random指针给设置上
从上图中我们可以发现,原节点和新节点是一一对应的关系,所以
- map.get(原节点),得到的就是对应的新节点
- map.get(原节点.next),得到的就是对应的 新节点.next
- map.get(原节点.random),得到的就是对应的 新节点.random
所以,我们只需要再次遍历原链表,然后设置:
- 新节点.next -> map.get(原节点.next)
- 新节点.random -> map.get(原节点.random)
这样新链表的next和random都被串联起来了
最后,我们然后map.get(head),也就是对应的新链表的头节点,就可以解决此问题了。
class Solution {
public Node copyRandomList(Node head) {
if(head == null) return null;
//创建一个哈希表,key是原节点,value是新节点
Map<Node, Node> map = new HashMap<>();
Node node = head;
//将原节点和新节点放入哈希表中
while(node != null) {
Node tmp = new Node(node.val);
map.put(node, tmp);
node = node.next;
}
node = head;
//遍历原链表,设置新节点的next和random
while(node != null) {
Node tmp = map.get(node);
//p是原节点,map.get(p)是对应的新节点,p.next是原节点的下一个
//map.get(p.next)是原节点下一个对应的新节点
if(node.next != null) {
tmp.next = map.get(node.next);
}
//p.random是原节点随机指向
//map.get(p.random)是原节点随机指向 对应的新节点
if(node.random != null) {
tmp.random = map.get(node.random);
}
node = node.next;
}
//返回头结点,即原节点对应的value(新节点)
return map.get(head);
}
}
- 时间复杂度 O(N) : 两轮遍历链表,使用O(N) 时间。
- 空间复杂度 O(N) : 哈希表使用线性大小的额外空间。
————————————————————————————————————————
面试题36.二叉搜索树与双向链表
将二叉搜索树转换成一个“排序的循环双向链表”,其中包含三个要素:
- 1.排序链表:节点应从小到大排序,因此应使用中序遍历“从小到大”访问树的节点
- 2.双向链表:在构建相邻节点的引用关系时,设前驱结点 pre 和当前节点 cur,不仅应构建 pre.right = cur,也应构建 cur.left = pre;
- 3.循环链表:设链表头节点 head 和尾节点 tail,则应构建 head.left = tail 和 tail.right = head
根据分析,考虑使用中序遍历访问树的各节点 cur,并在访问每个节点时构建 cur 和前驱节点 pre 的引用指向,中序遍历完成后,最后构建头节点和尾节点的引用指向即可
class Solution {
Node pre, head;
public Node treeToDoublyList(Node root) {
if(root == null) return null;
dfs(root);
head.left = pre;
pre.right = head;
return head;
}
public void dfs(Node cur) { //cur表示当前节点
if(cur == null) return;
//递归左子树
dfs(cur.left);
//当 pre 不为空时,cur 存在前一个节点,需要进行前一节点的右指针指向当前节点
if(pre != null) pre.right = cur;
//当 pre 为空时,表示此事正在访问链表头节点,记为head;
else head = cur;
//当前节点的左指针应指向前一节点
cur.left = pre;
//更新节点,进入下一轮操作
pre = cur;
dfs(cur.right);
}
}
- 时间复杂度 O(N) : N 为二叉树的节点数,中序遍历需要访问所有节点。
- 空间复杂度 O(N) : 最差情况下,即树退化为链表时,递归深度达到 N,系统使用 O(N) 栈空间。
————————————————————————————————————————
面试题37.序列化二叉树
BFS
本题要求:把二叉树转化为一个字符串,并且还能把这个字符串还原成原来的二叉树
序列化——很典型的BFS
-
维护一个队列,初始让根节点入列,考察出列节点
- 如果出列的节点是 null,将 ‘X’ 推入 res 数组
- 如果出列的节点不是 null,将节点值推入数组,并将它的左右子节点入列
-
入列、出列…直到队列为空,所有节点遍历完,res 数组也好了,转成字符串,即序列化结果。
public class Codec {
//把树转化为字符串(使用BFS遍历)
public String serialize(TreeNode root) {
//边界判断,如果为空就返回一个字符串“X”
if(root == null) return "X";
Queue<TreeNode> queue = new LinkedList<>();
StringBuilder res = new StringBuilder();
queue.add(root);
while(!queue.isEmpty()) {
//节点出队
TreeNode node = queue.poll();
//如果节点为空,添加一个字符“X”作为空节点
if(node == null) {
res.append("X,");
continue;
}
//如果节点不为空,把当前节点的值加入到字符串中,
//注意节点之间都是以逗号","分隔的,在下面把字符
//串还原二叉树的时候也是以逗号","把字符串进行拆分
res.append(node.val).append(",");
//左子节点加入到队列中(左子节点有可能为空)
queue.add(node.left);
//右子节点加入到队列中(右子节点有可能为空)
queue.add(node.right);
}
res.deleteCharAt(res.length() - 1);
return res.toString();
}
}
反序列化
-
先将序列化字符串转成数组 values,用一个指针从第二项开始扫描,每次考察两个节点值。
-
起初,用values[0]值构建根节点,并让根节点入列。
-
节点出列,考察,此时指针 i 指向它的左子节点值,i+1 指向它的右子节点值。
- 如果子节点值是数值,则创建节点,并认了出列的父亲,并且自己也是父亲,入列。
- 如果子节点值为 ‘X’,什么都不用做,因为出列的父节点的 left 和 right 本来就是 null
-
可见,所有的真实节点都会在队列里走一遍,出列就带出儿子入列
//把字符串还原为二叉树
public TreeNode deserialize(String data) {
//如果是“X”,就表示一个空节点
if(data == "X") return null;
Queue<TreeNode> queue = new LinkedList<>();
//因为上面每个节点之间是以逗号","分隔的,所以这里
//也要以逗号","来进行拆分
String[] values = data.split(",");
//上面使用的是BFS,所以第一个值就是根节点的值,这里创建根节点
TreeNode root = new TreeNode(Integer.valueOf(values[0]));
queue.add(root);
for(int i = 1; i < values.length; i++) {
//队列中节点出栈
TreeNode parent = queue.poll();
//因为在BFS中左右子节点是成对出现的,所以这里挨着的两个值
//一个是左子节点的值一个是右子节点的值,
//当前值如果是"X",就表示这个子节点是空的,如果不是"X"就表示不是空的
if(!"X".equals(values[i])) {
TreeNode left = new TreeNode(Integer.valueOf(values[i]));
parent.left = left;
queue.add(left);
}
//上面如果不为空就是左子节点的值,这里是右子节点的值,注意这里有个i++(因为左右子节点成对出现)
if(!"X".equals(values[++i])) {
TreeNode right = new TreeNode(Integer.valueOf(values[i]));
parent.right = right;
queue.add(right);
}
}
return root;
}
DFS
DFS采用前序遍历,是因为 [根∣左∣右] 的打印顺序,在反序列化时更容易定位出根节点的值。
序列化
class Codec {
//把树转化为字符串(使用DFS遍历,也是前序遍历,顺序是:根节点→左子树→右子树)
public String serialize(TreeNode root) {
if(root == null) return "X";
//序列化的结果为:根节点值 + "," + 左子节点值(进入递归) + "," + 右子节点值(进入递归)
return root.val + "," + serialize(root.left) + "," + serialize(root.right);
}
反序列化
1、先将字符串转换成队列
2、递归
- 弹出左侧元素,即队首元素出列
- 如果元素为“X”,返回null
- 否则,新建一个值为弹出元素的新节点
- 其左子节点为队列的下一个元素,进入递归;右子节点为队列的下下个元素,也进入递归
- 递归就是不断将子树的根节点连接到父节点的过程
//把字符串还原为二叉树
public TreeNode deserialize(String data) {
//把字符串data以逗号","拆分,拆分之后存储到队列中
Queue<String> queue = new LinkedList<>(Arrays.asList(data.split(",")));
return dfs(queue);
}
public TreeNode dfs(Queue<String> queue) {
String val = queue.poll();
if("X".equals(val)) return null;
TreeNode root = new TreeNode(Integer.valueOf(val));
root.left = dfs(queue);
root.right = dfs(queue);
return root;
}