中序 + 后序建树:后序末尾是根,先建右
后序最后访问根,所以从尾巴往回拿。拿完根,下一段属于右子树,必须先递归右区间,再递归左区间。先建左会把右子树的根错安到左边。
后序是左、右、根。当前子树的根是 postorder[postIndex],取完 postIndex 减一。中序里根把区间切成 [L,mid−1] 和 [mid+1,R]。因为从尾往前看,根的前一个区段是右子树,所以先 build 右再 build 左。时间 O(n),空间 O(n)。
给定一棵树的中序遍历和后序遍历,重建这棵二叉树。节点值互不相同。中序是左、根、右;后序是左、右、根。
主例 inorder = [9, 3, 15, 20, 7],postorder = [9, 15, 7, 20, 3]。后序最后一个数是 3,它就是整棵树的根。3 在中序里把数组切成左边 [9]、右边 [15, 20, 7]。重建结果层序是 [3, 9, 20, null, null, 15, 7]。
只知道后序能确定根,切不开左右;只知道中序能切开,找不到根。本文要回答:从尾巴往回消费后序时,为什么下一刀必须先建右子树。
从尾巴拿根,下一刀是右子树
后序最后访问根,所以整段 postorder 的最后一个元素就是当前这棵子树的根。根在中序里的位置 mid 一旦确定,左边那段只能是左子树,右边那段只能是右子树。这一步和前序+中序建树相同:中序负责划界。
差别出在消费方向。前序是根、左、右,从头往后拿,拿完根下一截是左子树,所以先建左。后序是左、右、根,从尾巴往前拿,拿完根下一截是右子树,所以先建右。缺的不是「根在哪」,而是「下一次 postIndex 指向的值属于哪一边」。把 LC105 的先左后右原样搬过来,主例会把 20 错安成 3 的左孩子。
用一个从尾巴往前走的下标 postIndex,每次 build 先取 postorder[postIndex] 做根,立刻减一。然后先递归中序右区间 [mid+1, R],再递归左区间 [L, mid−1]。区间空(L > R)返回空。中序下标用哈希表 O(1) 定位,避免每次扫描。
用手走主例。一开始 postIndex=4,L=0,R=4。根是 3,3 在中序下标 1。演示第一帧标的就是这一刀。右区间 [2,4] 对应 [15,20,7],先建它:postIndex 变成 3,根是 20,20 在中序下标 3。演示第二帧。20 的右区间 [4,4] 是 7,左区间 [2,2] 是 15,仍按先右后左拿走。回到 3 的左区间 [0,0],只剩 9。整棵是 3,左 9,右 20,20 下挂 15 和 7。
先建左会在 postIndex=3 时把 20 当成 3 的左孩子,中序左边却只有 9,立刻对不上。这不是实现细节,是后序「根之前是右子树」逼出来的调用顺序。节点值不重复,中序位置唯一;若有重复值,单靠这两个数组无法唯一确定树。
为什么必须先右从尾巴看后序:根、右子树、左子树。postIndex 减一之后指向的是右子树的根。先建左会把这个值错吃进左子树。
Go:postIndex 逆序
func buildTree(inorder, postorder []int) *TreeNode {pos := make(map[int]int, len(inorder))for i, v := range inorder { pos[v] = i }postIndex := len(postorder) - 1var build func(int, int) *TreeNodebuild = func(L, R int) *TreeNode {if L > R { return nil }rootVal := postorder[postIndex]postIndex--root := &TreeNode{Val: rootVal}mid := pos[rootVal]root.Right = build(mid+1, R)root.Left = build(L, mid-1)return root}return build(0, len(inorder)-1)}
1中序值到下标,划界时 O(1) 找到 mid。
2postIndex 从尾巴起步。每次取根后立刻减一,下一个值留给右子树。
3空区间 L>R 返回空,必须写在取根之前。
4先递归右区间再递归左区间。这两行对调就会在主例上把 20 接到 3 的左边。
总结
后序末尾取根,中序切开;从尾巴看下一截是右子树,所以先建右。主例根 3,先建 20。
- 后序最后访问根。主例第一个根是 3,下一个根是 20,不是 9。
- 先建右是消费方向逼出来的。先建左会把右子树的根错吃进左边。
- 中序哈希定位。空区间返回空。