LeetCode刷题 - 树小结

树结构是一种非线性存储结构,存储的是具有“一对多”关系的数据元素的集合

一.树的遍历

Java中binary tree的表示

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

iterative写法的模板

//iterative1
public List<Integer> inorderTraversal(TreeNode root) {
  List<Integer> res = new ArrayList<>();
  Stack<TreeNode> 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<Integer> inorderTraversal(TreeNode root) {
  List<Integer> res = new ArrayList<>();
  Stack<TreeNode> 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<String> q = new LinkedList<>(Arrays.asList(data.split(",")));
  return helper(q);
}
private TreeNode helper(Queue<String> 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;
}

从Array建树类似模板

public class Solution {
  int[] inorder;
  int[] postorder;
  int N;
  Map<Integer,Integer> 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

什么是二分查找树BST?

若是它的左子树不为空,左子树上所有节点的值都小于它的根节点;若它的右子树不为空,右子树上所有的节点的值都大于它的根节点;它的左右子树也都是二分查找树

如何构造BST?

1.search

//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;
  }
}

2.insert

//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;
}

3.delete

删除操作相比上边的操作要麻烦得多,首先需要定位一个节点,删除节点后,我们需要始终保持BST的性质

删除一个节点涉及三种情况:①节点是叶节点 ②节点有一个孩子 ③节点有两个孩子

叶子节点和单孩子节点都很好处理

因为BST inorder顺序遍历,回忆一下linkedlist如何删除当前节点,我们只需要把后继替换过来

四.Binary Index Tree

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;
  }
}

六.LCA:Lowest Common Ancestor

给定一棵二叉树,求最近公共祖先

最近公共祖先定义:对于有跟树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;
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
预处理 第一遍dfs求出每个结点的深度deep[x],其为根的子大小size[x] 以及祖先的信息fa[x][i]表示x往上距离为2^i的祖先 第二遍dfs ž根节点为起点,向下拓展构建重链 选择最大的一个子的根继承当前重链 其余节点,都以该节点为起点向下重新拉一条重链 ž给每个结点分配一个位置编号,每条重链就相当于一段区间,用数据结构去维护。 把所有的重链首尾相接,放到同一个数据结构上,然后维护这一个整体即可 修改操作 ž1、单独修改一个点的权值 根据其编号直接在数据结构中修改就行了。 2、修改点u和点v的路径上的权值 (1)若u和v在同一条重链上 直接用数据结构修改pos[u]至pos[v]间的值。 (2)若u和v不在同一条重链上 一边进行修改,一边将u和v往同一条重链上靠,然后就变成了情况(1)。 伪代码 CHANGE (x, y ,d) while top[x]≠top[y] do if dep[top[x]]<dep[top[y]] then SWAP(x,y), SWAP (gx,gy) CHANGE-IT(tid[top[x]],tid[x],d) fa[x]→x if dep[x]>dep[y] then SWAP (x,y) CHANGE-IT(tid[x],tid[y],d) //CHANGE-IT(l,r,d)为数据结构的修改操作:将区间[l,r]上的所有权值改为d 查询操作 ž查询操作的分析过程同修改操作 伪代码 QUERY (x, y) while top[x]≠top[y] do if dep[top[x]]<dep[top[y]] then SWAP (x,y), SWAP (gx,gy) QUERY-IT(tid[top[x]],tid[x]) fa[x]→x if dep[x]>dep[y] then SWAP (x,y) QUERY-IT(tid[x],tid[y]) //QUERY-IT(l,r)为数据结构的查询操作, 目不同,选用不同的数据结构来维护值,通常有线段和splay [2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值