树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {val = x;}
}
树的基础知识知道一定程度就可以了,很多avl红黑树不要求
遍历作为最为重要的一部分,以后会分为:①backtracking,data自上而下的传输
②pure recursion,data自下而上的传输
熟练使用inorder遍历的recursive和iterative模板
添加删除需要注意
时间复杂度一般和O(logn)挂钩
iterator就是按顺序用iterative写法去遍历,记得default method
//iterative1
public List inorderTraversal(TreeNode root) {
List res = new ArrayList<>();
Stack stack = new Stack<>();
while (root != null || !stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
res.add(root.val);
root = root.right;
}
return res;
}
//iterative2
public List inorderTraversal(TreeNode root) {
List res = new ArrayList<>();
Stack stack = new Stack<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
if (cur != null) {
stack.push(cur);
cur = cur.left;
} else {
cur = stack.pop();
res.add(cur.val);
cur = cur.right;
}
}
return res;
}
树 -> 线性
线性 -> 树
结构转换主要围绕:Tree → linear(直接前序遍历),linear(array)→ tree(前序遍历traverse过程中构造)
本身含有PreOrder的可以不去maintain两个边界,用一个global variable就可以只要你的遍历顺序也是PreOrder
BST可以根据自身性质把low,high传下去
public String serialize(TreeNode root) {
if (root == null) return "#";
StringBuilder sb = new StringBuilder();
sb.append(root.val);
sb.append(",").append(serialize(root.left));
sb.append(",").append(serialize(root.right));
return ab.toString();
}
public TreeNode deserialize(String data) {
Queue q = new LinkedList<>(Arrays.asList(data.split(",")));
return helper(q);
}
private TreeNode helper(Queue q) {
String s = q.poll();
if (s.equals("#")) return null;
TreeNode root = new TreeNode(Integer.valueOf(s));
root.left = helper(q);
root.right = helper(q);
return root;
}
public class Solution {
int[] inorder;
int[] postorder;
int N;
Map map = new HashMap<>();
public TreeNode builder(int[] inorder,int[] postorder) {
this.inorder = inorder;
this.postorder = postorder;
N = inorder.length;
for (int i = 0;i < N;i++) map.put(inorder[i],i);
return helper(0,N-1,0,N-1);
}
public TreeNode helper(int inStart,int inEnd,int postStart,int postEnd) {
if (inStart > inEnd || postStart > postEnd) return null;
TreeNode root = new TreeNode(postorder[postEnd]);
int inIndex = map.get(root.val);
int rightTreeSize = inEnd - inIndex;
root.left = helper(inStart,inIndex - 1,postStart,postEnd - rightTreeSize - 1);
root.right = helper(inIndex + 1,inEnd,postEnd - rightTreeSize,postEnd - 1);
return root;
}
}
什么是二分查找树BST?
若是它的左子树不为空,左子树上所有节点的值都小于它的根节点;若它的右子树不为空,右子树上所有的节点的值都大于它的根节点;它的左右子树也都是二分查找树
//recursive
public TreeNode search(TreeNode root,int target) {
//Base Cases: root is null or key is present at root
if (root == null || root.val == target) return root;
//Key is greater than root's key
if (root.val < target) return search(root.right,target);
//Key is smaller than root's key
return search(root.left,target);
}
//iterative
public TreeNode search2(TreeNode root,int target) {
TreeNode cur = root;
while (true) {
if (cur == null) return null;
if (cur.val == target) return cur;
if (cur.val < target) cur = cur.right;
else cur = cur.left;
}
}
//recursive
TreeNode insert(TreeNode root,int target) {
if (root == null) {
root = new TreeNode(target);
return root;
}
if (target < root.val) root.left = insert(root.left,target);
else if (target > root.val) root.right = insert(root.right,target);
return root;
}
//iterative
public TreeNode addNode(TreeNode root,int target) {
TreeNode cur = root;
TreeNode newNode = new TreeNode(target);
if (cur == null) {
cur = newNode;
return cur;
}
TreeNode prev = null;
while (cur != null) {
prev = cur;
if (cur.val < target) cur = cur.right;
else cur = cur.left;
}
if (prev.val < target) prev.right = newNode;
else prev.left = newNode;
return root;
}
删除操作相比上边的操作要麻烦得多,首先需要定位一个节点,删除节点后,我们需要始终保持BST的性质
删除一个节点涉及三种情况:①节点是叶节点 ②节点有一个孩子 ③节点有两个孩子
叶子节点和单孩子节点都很好处理
因为BST inorder顺序遍历,回忆一下linkedlist如何删除当前节点,我们只需要把后继替换过来
BIT一般两个默认method:sum(i)取的[0,1]所有数的和,update(i,val)index为i的数字增加val的值
时间复杂度sum和update都是O(logN),建立BIT可以有O(n)优化或者普通nlogn类似heap
rangeSum = sum(j) - sum(i - 1)
not for range max/min,求区间最大最小值用segment tree
class BIT {
int[] parent;
public BIT(int N) {
parent = new int[N];
}
public int sum(int x) {
int sum = 0;
for (x++; x > 0; x -= (x & -x)) sum += parent[x];
return sum;
}
public void add(int x, int val) {
for (x++; x < parent.length; x += (x & -x)) parent[x] += val;
}
}
1.想解决什么问题?
给定一个长度为 n 的序列,需要频繁地求其中某个区间的最值,以及更新某个区间的所有值
最朴素的算法是遍历地查询与插入,则查询的时间复杂度为 O(q*n),q为查询的个数。在数据量大,查询次数多的时候,效率是很低的
另一种思路是使用一个 O(n^2) 的数组,a[i][j] 表示区间 [i,j] 的最小值。这样查询操作的复杂度为 O(1),相当于用空间换时间。但是修改某个区间的值较麻烦,空间复杂度较大
线段树可以解决这类需要维护区间信息的问题。线段树可以在 O(logn) 的时间复杂度内实现
单点修改 (logn)
区间修改 (这里需要用到 lazy propogation 来优化到 logn,完全不在面试范围)
区间查询 (logn区间求和,求区间最大值,求区间最小值,区间最小公倍数,区间最大公因数)
2.什么是线段树?
parent 的 value 等于两个 child 的和
①叶子节点存储输入的数组元素
②每一个内部节点表示某些叶子节点的合并。合并的方法可能会因问题而异。对于这个问题,合并指的是某个节点之下的所有叶子节点的和(求区间最大最小的时候就不是求和了,而是找最大或者最小)
此处使用树的数组形式来表示线段树。对于下标 i 的节点,其左孩子为 2*i+1,右孩子为 2*i+2,父节点为 floor((i-1)/2)
默认的method为:update(),rangeSum(),rangeMax(),rangeMin(),rangeUpdate()
3.总结
prefixSum,BinaryIndexTree,SegmentTree很类似处理rangeQuery系列的问题
一般来讲,凡是可以使用树状数组解决的问题,使用线段树也可以解决,但是线段树能够解决的问题树状数组未必能够解决(例如求区间最大/小值)
4.什么情况下无法使用线段树?
如果我们删除或者增加区间中的元素,那么区间的大小将发生变化,此时是无法使用线段树解决这种问题的。(如果只求count可以转化为bucket sort再做rangeSum就没有元素的增加减少了)
public NumArray(int[] nums) {
n = nums.length;
st = new int[2*n];
for (int i = n; i < n*2; i++)
st[i] = nums[i - n]; //叶子是原始元素
for (int i = n - 1; i > 0; i--) //parent等于两个child和
st[i] = st[2*i] + st[2*i + 1];
}
//update()
//直接更新对应元素,重新balance tree,和heap的heapify非常的类似
public void update(int i, int val) {
int diff = val - st[i + n];
for (i += n; i > 0; i /= 2) st[i] += diff;
}
//rangeSum()
public int sumRange(int i, int j) {
int res = 0;
for (i += n, j += n; i <= j; i /= 2, j /= 2) {
if (i % 2 == 1) res += st[i++]; //st[i]是右子节点
if (j % 2 == 0) res += st[j--]; //st[j]是左子节点
}
return res;
}
普通segment tree:①array based ②tree based
我们这里介绍一种tree based,容易给面试官解释清楚,只需要考虑node之间的关系,无须担心index
classSegmentTree {
int[] nums;
Node root;
public SegmentTree(int[] nums) {
this.nums = nums;
this.root = buildTree(nums,0,nums.length - 1);
}
private Node buildTree(int[] nums,int start,int end) {
if (start > end) return null;
Node node = new Node(start,end);
if (start == end) node.sum = nums[start];
else {
int mid = start + (end - start) / 2;
node.left = buildTree(nums,start,mid);
node.right = buildTree(nums,mid + 1,end);
node.sum = node.left.sum + node.right.sum;
}
return node;
}
public void update(Node node,int i,int val) {
if (node.start == node.end) {
node.sum = val;
return;
}
int mid = node.start + (node.end - node.start) / 2;
if (i <= mid) update(node.right,i,val);
node.sum = node.left.sum + node.right.sum;
}
public int sumRange(Node node,int start,int end) {
if (start > end) return 0;
if (node.start == start && node.end) return node.sum;
int mid = node.start + (node.end - node.start) / 2;
if (end <= mid) return sumRange(node.left,start,end);
else if (start > end) return sumRange(node.left,start,end);
else return sumRange(node.left,start,mid) + sumRange(node.right,mid + 1,end);
}
}
class Node {
int start,end,sum;
Node left,right;
Node(int start, int end) {
this.start = start;
this.end = end;
}
}
给定一棵二叉树,求最近公共祖先
最近公共祖先定义:对于有跟树T的两个结点p,q,最近公共祖先表示为一个结点x,满足x是p,q的祖先且x的深度尽可能大(一个结点也可以是它自己的祖先)
总结
LCA基础模板的基础上其实是使用了pure recursion的知识,也就是我们从childNode里面拿到处理后的结果,然后在当前节点汇总来自左右孩子的数据,data自下而上
与之对比的是backtracking,我们拿到来自parent的信息,data自上而下
模板
public TreeNode lowestCommonAncestor(TreeNode root,TreeNode p,TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) return root;
if (left != null) return left;
if (right != null) return right;
return null;
}
优化?
1.树上倍增考虑了二进制的思想,预处理每个结点 2^i 层父亲节点的下标
2.Tarjan离线LCA (O(n*m))
Tarjan(u) //merge和find为并查集合并函数和查找函数
{
for each(u,v) //访问所有u子节点v
{
Tarjan(v); //继续往下遍历
merge(u,v); //合并v到u上
标记v被访问过
}
for each(u,e) //访问所有和u有询问关系的e
{
如果e被访问过;
u,e的最近公共祖先为find(e);
}
}
由于面试几乎考察不到,leetcode如果加这个题目的话其实有点偏毕竟tarjan很少有人会复习到,所以没有完全的代码提供,上面是核心思路:第一个for loop构建遍历顺序和LCA,第二个for loop对于每个node如果被查询就放入result的一部分
3.树链剖分求LCA (O(logn*m))
int LCA(int a,int b)
{
while(1)
{
if (top[a] == top[b])
return dep[a] <= dep[b] ? a : b;
else if (dep[top[a] >= dep[top[b])
a = fa[top[a]];
else b = fa[top[b]];
}
}
4.用欧拉序列转化为RMQ问题(range max/min query)O(nlogn)时间内进行预处理,然后在
O(1)时间内回答每个查询
我们在拿到当前node的信息后,可以自上而下传递给left,right node,也可以自下而上传递给parent node
我们把自上而下的信息传递叫做backtracking:可以理解为我们想去做一个搜索的操作,类似dfs,我们在走一条路不通后我们undo我们的操作,去下一条路再次尝试,在进行的过程中其实我们使用了很多次的recursion。注意这个过程中每条我们尝试的道路是互相不影响的,这也是我们为什么说信息是自上而下传递的
自下而上的信息传递叫pure recursion:又要解决的问题的定义出发,尝试划分成相同的问题的子问题,并且利用子问题的结果来解决原来的问题,而最关键的点有两处:base case和induction rule。base case代表的是最小号的不可分的问题这里指的是叶子节点,induction rule表示的是如何利用子问题的结果,表示在当前node处理leftchild,rightchild返回来的结果。比如我们在找subtree all nodes sum的时候,我们通过left,right node return back它们的sum,这样我们在每一层的currentnode节点就可以拿到汇总起来的当前subtree的数据
总结
1.backtracking考察较少,一般特征为method void,各自的尝试道路互相不影响
2.pure recursion 单value return考察较多,需要着重练习
3.pure recursion 双value return以及不同type value return算是进阶扩展,不同type建议建立新的class来wrap
4.树的题目多用recursion去思考
5.熟练掌握扩展的返回两个不同的值给current node去做处理
模板
int max;
public int longestZigZag(TreeNode root) {
dfs(root);
return max == 0 ? 0 : max - 1;
}
private int[] dfs(TreeNode root) {
int[] res = new int[2];
if (root == null) return res;
int[] left = dfs(root.left);
int[] right = dfs(root.right);
res[0] = left[1] + 1; //这里我们拿了左孩子的rightSum
res[1] = right[0] + 1; //这里我们拿了右孩子的leftSum
max = Math.max(max,Math.max(res[0],res[1]));
return res;
}