面试题34、35、36、37

面试题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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值