二叉树的遍历一般有如下几种,先序遍历(根->左->右),中序遍历(左->根->右),后序遍历(左->右->根),层序遍历。各种遍历的实现都有递归和非递归方式,下面分别描述。
假设二叉树定义如下
type TreeNode struct {
Val int
Left *TreeNode
Right *TreeNode
}
一、递归实现
先序遍历
先序遍历的递归实现很简单,各种教材上都有描述,直接给出代码如下
// 先序递归遍历 根左右
func (root *TreeNode) PreOrderRecur() []int {
res := make([]int, 0)
return root.preOrderRecurHelper(res)
}
func (root *TreeNode) preOrderRecurHelper(res []int) []int {
if root == nil {
return res
}
res = append(res, root.Val)
res = root.Left.preOrderRecurHelper(res)
res = root.Right.preOrderRecurHelper(res)
return res
}
中序遍历
中序遍历的递归实现很简单,各种教材上都有描述,直接给出代码如下
// 中序递归遍历 左根右
func (root *TreeNode) InOrderRecur() []int {
res := make([]int, 0)
return root.inOrderRecurHelper(res)
return res
}
func (root *TreeNode) inOrderRecurHelper(res []int) []int {
if root == nil {
return res
}
res = root.Left.inOrderRecurHelper(res)
res = append(res, root.Val)
res = root.Right.inOrderRecurHelper(res)
return res
}
后序遍历
后序遍历的递归实现很简单,各种教材上都有描述,直接给出代码如下
// 后序递归遍历 左右根
func (root *TreeNode) PosOrderRecur() []int {
res := make([]int, 0)
return root.posOrderRecurHelper(res)
}
func (root *TreeNode) posOrderRecurHelper(res []int) []int {
if root == nil {
return res
}
res = root.Left.posOrderRecurHelper(res)
res = root.Right.posOrderRecurHelper(res)
res = append(res, root.Val)
return res
}
层序遍历
一般的层序遍历的结果一般需要按层聚合,比如二叉树
层序遍历结果为:[[1] [2 5] [3 4 6]]
我们假设把二叉树的节点错一下位,变成如下所示的样子
按照深度优先的处理顺序,会先访问节点1,再访问节点2,接着是节点3。
之后是第二列的4和5,最后是第三列的6。
每次递归的时候都需要带一个index(表示当前的层数),也就对应那个田字格子中的第几行,如果当前行对应的list不存在,就需要加入一个空list进去。
代码如下
// 层序遍历 递归方式
func (root *TreeNode) LevelOrderRecur() [][]int {
res := make([][]int, 0)
root.levelOrderRecurHelper(0, &res)
return res
}
func (root *TreeNode) levelOrderRecurHelper(level int, res *[][]int) {
if root == nil {
return
}
fmt.Println("get val ", root.Val)
if len(*res) == level {
levRes := make([]int, 0)
fmt.Println("new lev val ", root.Val)
*res = append(*res, levRes)
}
(*res)[level] = append((*res)[level], root.Val)
root.Left.levelOrderRecurHelper(level+1, res)
root.Right.levelOrderRecurHelper(level+1, res)
}
二、非递归实现
先序遍历
用非递归的方式实现先序遍历的过程如下:
- 申请一个新的栈,记为stack,然后将跟节点root压入栈;
- 从stack中弹出栈顶元素,记为cur,打印cur节点的值,然后将cur的右孩子压入stack(不为空的话), 最后将cur的左孩子(不为空的话)压入stack;
- 重复步骤2直到stack为空,过程结束;
以下图所示的树为例,整个过程为
说明 | stack | cur | res |
---|---|---|---|
根节点1入栈 | [1] | ||
1出栈,并打印 | [] | 1 | 1 |
右子树3入栈 | [3] | ||
右子树2入栈 | [2,3] | ||
2出栈,并打印 | [3] | 2 | 1,2 |
右子树5入栈 | [5,3] | ||
左子树4入栈 | [4,5,3] | ||
4出栈,并打印 | [5,3] | 4 | 1,2,4 |
4没有子树,5出栈,并打印 | [3] | 5 | 1,2,4,5 |
5没有子树,3出栈,并打印 | [] | 3 | 1,2,4,5,3 |
右子树7入栈 | [7] | ||
左子树6入栈 | [6,7] | ||
6出栈,并打印 | [7] | 6 | 1,2,4,5,3,6 |
6没有子树,7出栈,并打印 | [] | 7 | 1,2,4,5,3,6,7 |
7没有子树,stack为空,结束 | 1,2,4,5,3,6,7 |
代码如下
// 先序非递归遍历 根左右
func (root *TreeNode) PreOrderUnRecur() []int {
res := make([]int, 0)
return root.preOrderUnRecurHelper(res)
return res
}
func (root *TreeNode) preOrderUnRecurHelper(res []int) []int {
stack := list.New()
if root == nil {
return res
}
// 根节点入栈
stack.PushBack(root)
for stack.Len() != 0 {
//fmt.Println("stack ", *stack)
// 弹出栈顶元素
cur := stack.Back().Value.(*TreeNode)
stack.Remove(stack.Back())
fmt.Println("cur ", cur.Val)
// 获取cur node的值
res = append(res, cur.Val)
// 右孩子入栈
if cur.Right != nil {
stack.PushBack(cur.Right)
}
// 左孩子入栈
if cur.Left != nil {
stack.PushBack(cur.Left)
}
}
// 栈为空 结束
return res
}
中序遍历
用非递归的方式实现中序遍历,具体过程如下
- 申请一个新的栈,记为stack,令cur=root;
- 先把cur节点压入stack,对以cur为头的整个子树来说,依次把左边界压入stack,即不停的令cur=curl.Left,然后重复步骤2;
- 重复步骤2,直到发现cur为null,然后从stack中弹出一个节点,记为node,打印该节点值,然后让cur=node.Right,然后继续重复步骤2;
- 当stack为空并且cur为空时 整个过程停止。
还是以1,2,3,4,5,6,7这个二叉树为例来说明整个过程
代码如下
// 中序非递归遍历
func (root *TreeNode) InOrderUnRecur() []int {
res := make([]int, 0)
return root.inOrderUnRecurHelper(res)
}
func (root *TreeNode) inOrderUnRecurHelper(res []int) []int {
stack := list.New()
// 初始时 cur为root
cur := root
for stack.Len() != 0 || cur != nil {
// 如果左子树非空 则入栈,然后继续往左子树走
for cur != nil {
stack.PushBack(cur)
cur = cur.Left
}
// 找到第一个左子树为空的节点 出栈并打印
cur = stack.Back().Value.(*TreeNode)
stack.Remove(stack.Back())
res = append(res, cur.Val)
// 处理右子树
cur = cur.Right
}
return res
}
后序遍历
后序遍历相对复杂一下,先介绍用两个栈的实现方式,然后再介绍用一个栈的实现方式。
两个栈实现
具体过程如下:
- 申请两个栈s1,s2,然后先将根节点压入s1中
- 从s1中弹出的节点记为cur,然后依次将cur的左孩子和右孩子压入s1中
- 整个过程中从s1弹出的每一个节点都压入s2
- 不断重复步骤2和步骤3直到s1为空
- 从s2依次弹出节点并打印,打印的顺序就是后序遍历的顺序
通过上述过程可以知道,每棵子树的根节点都先弹出,然后左孩子右孩子入栈,所以s1的弹出顺序是根,右,左。所以s2的入栈顺序也是根,右,左,所以s2的出栈顺序是左,右,根。刚好是后序遍历的顺序。
还是以1,2,3,4,5,6,7这个二叉树为例来说明整个过程
说明 | s1 | cur | s2 |
---|---|---|---|
开始1入栈 | [1] | nul | [] |
1从s1出栈,并压入s2 | [] | 1 | [1] |
1的左子树2入栈 | [2] | 1 | [1] |
1的右子树3入栈 | [3,2] | 1 | [1] |
3从s1出栈,并压入s2 | [2] | 3 | [3,1] |
3的左子树6入栈 | [6,2] | 3 | [3,1] |
3的右子树7入栈 | [7,6,2] | 3 | [3,1] |
7从s1出栈,并压入s2 | [6,2] | 7 | [7,3,1] |
7的子树为空所以6出栈 | [2] | 6 | [6,7,3,1] |
6的子树为空所以2出栈 | [] | 2 | [2,6,7,3,1] |
2的左子树4入栈 | [4] | 2 | [2,6,7,3,1] |
2的右子树5入栈 | [5,4] | 2 | [2,6,7,3,1] |
5从s1出栈,并压入s2 | [4] | 5 | [5,2,6,7,3,1] |
5的子树为空,所以4出栈 | [] | 4 | [4,5,2,6,7,3,1] |
4的子树为空且s1为空 步骤4 结束 | [] | 4 | [4,5,2,6,7,3,1] |
依次取s2的栈顶元素出栈 | [4,5,2,6,7,3,1] |
代码如下
// 后序非递归遍历 两个栈实现 左右根
func (root *TreeNode) PosOrderUnRecur2() []int {
res := make([]int, 0)
return root.posOrderUnRecurHelper2(res)
}
func (root *TreeNode) posOrderUnRecurHelper2(res []int) []int {
s1 := list.New()
s2 := list.New()
if root == nil {
return res
}
s1.PushBack(root)
for s1.Len() != 0 {
cur := s1.Back().Value.(*TreeNode)
s1.Remove(s1.Back())
s2.PushBack(cur)
if cur.Left != nil {
s1.PushBack(cur.Left)
}
if cur.Right != nil {
s1.PushBack(cur.Right)
}
}
for s2.Len() != 0 {
node := s2.Back().Value.(*TreeNode)
s2.Remove(s2.Back())
res = append(res, node.Val)
}
return res
}
一个栈实现
具体过程如下:
- 申请一个栈,记为stack,将根节点压入stack,同时设计两个变量h和c。整个流程中,h代表最近一次弹出并打印的节点,c代表stack的栈顶节点,初始时,h为根节点,c为null;
- 每次令c等于当前stack的栈顶节点,但是不从stack中弹出。此时有三种情况:
1.如果c的左孩子不为null,并且h不等于c的左孩子,也不等于c的右孩子,则把c的左孩子压入stack中。这是因为:首先,h的意义时最近一次弹出并打印的节点,所以如果h等于c的左孩子或者右孩子,说明c的左子树和右子树已经打印完成,此时不应该再将c的左孩子放入stack中,否则,说明左子树还没处理过,那么此时应该将c的左孩子压入stack中。
2.如果条件1不成立,并且c的右孩子不为null,h不等于c的右孩子,则把c的右孩子压入stack中。含义是如果h等于c的右孩子,说明c的右子树已经打印完毕,此时不应再将c的右孩子放入stack中。否则,说明右子树还没处理过,此时应该将c的右孩子压入stack中。
3.如果条件1和条件2都不成立,说明c的左子树和右子树都已经打印完毕,那么从stack中弹出c并打印。然后另h=c
3.一直重复步骤2,知道stack为空。过程停止。
还是用1,2,3,4,5,6,7这个树来举例,过程如下:
- 开始时,节点1 压入栈stack,h=节点1,c=null。s=[1]
- 令c=s.Top ,即c=节点1 。此时c.Left=2。不等于h,满足步骤2的条件1,将节点2压入stack。s=[2,1]
- 令c=s.Top ,即c=节点2。此时c.Left=4。不等于h,满足步骤2的条件1,将节点4压入stack。s=[4,2,1]
- 令c=s.Top ,即c=节点4。此时c.Left=null。c.Right=null,h=节点1。满足步骤2的条件3,将4从stack中弹出,并打印。h=4 ,s=[2,1]
- 令c=s.Top ,即c=节点2。此时c.Left=4。c.Right=5,h=4,满足步骤2的条件2,将节点5压入stack。s=[5,2,1]
- 令c=s.Top ,即c=节点5。此时c.Left=null。c.Right=null,h=节点4。满足步骤2的条件3,将5从stack中弹出,并打印。h=5 ,s=[2,1]
- 令c=s.Top ,即c=节点2。此时c.Left=4。c.Right=5.h=5,满足步骤2的条件3,将2从stack中弹出,并打印。h=2 ,s=[1]
- 令c=s.Top ,即c=节点1。此时c.Left=2。c.Right=3, h=2,满足步骤2的条件2,将节点3压入stack。s=[3,1]
- 令c=s.Top ,即c=节点3。此时c.Left=6。c.Right=7, h=2,满足步骤2的条件1,将节点6压入stack。s=[6,3,1]
- 令c=s.Top ,即c=节点6。此时c.Left=null。c.Right=null, h=2,满足步骤2的条件3,将节点6弹出并打印。h=6,s=[3,1]
- 令c=s.Top ,即c=节点3。此时c.Left=6。c.Right=7, h=6,满足步骤2的条件2,将节点7压入stack。s=[7,3,1]
- 令c=s.Top ,即c=节点7。此时c.Left=null。c.Right=null, h=6,满足步骤2的条件3,将节点7弹出并打印。h=7,s=[3,1]
- 令c=s.Top ,即c=节点7。此时c.Left=6。c.Right=7, h=7,满足步骤2的条件3,将节点3弹出并打印。h=3,s=[1]
- 令c=s.Top ,即c=节点1。此时c.Left=2。c.Right=3, h=3,满足步骤2的条件3,将节点1弹出并打印。h=1,s=[]
- s=空,结束。
- 打印的顺序为 4 5 2 6 7 3 1
代码如下
// 后续遍历 非递归 一个栈实现 左右根
func (root *TreeNode) PosOrderUnRecur1() []int {
res := make([]int, 0)
return root.posOrderUnRecurHelper1(res)
}
func (root *TreeNode) posOrderUnRecurHelper1(res []int) []int {
stack := list.New()
if root == nil {
return res
}
h := root
stack.PushBack(h) // 入栈 h指向最近打印的节点 初始化为root 如果h初始化为nil 对于只有一个没有右子树的情况会有问题
for stack.Len() != 0 {
c := stack.Back()
cv := c.Value.(*TreeNode)
if cv.Left != nil && cv.Left != h && cv.Right != h {
// 左子树入栈条件 左子树非空 左子树没有打印 右子树没有打印
stack.PushBack(cv.Left) // 入栈
} else if cv.Right != nil && cv.Right != h {
// 右子树入栈条件 右子树非空 右子树没有打印
stack.PushBack(cv.Right)
} else {
// 否则 打印节点 并出栈
h = cv
res = append(res, cv.Val)
stack.Remove(c)
}
}
return res
}
层序遍历
层序遍历的非递归实现,需要用到一个队列,过程如下
- 申请一个队列,记为list,将根节点加入队列中。
- 此时队列中元素的个数,即为该层的元素个数。从队列中取出队首元素,记为cur。将cur的左孩子(如果存在)和右孩子(如果存在)加入队列。
- 重复步骤2 知道队列为空,过程结束。
代码如下
func (root *TreeNode) LevelOrderUnRecur() [][]int {
res := make([][]int, 0)
if root == nil {
return res
}
l := list.New()
l.PushBack(root)
for l.Len() != 0 {
size := l.Len() //当前层个数等于队列长度
levelRes := make([]int, 0)
// 取当前层节点 放入levelRes
for i := 0; i < size; i++ {
node := l.Front().Value.(*TreeNode)
l.Remove(l.Front())
levelRes = append(levelRes, node.Val)
if i == 0 {
// 左视图
fmt.Println(node.Val)
}
if i == size-1 {
// 右视图
fmt.Println(node.Val)
}
if node.Left != nil {
l.PushBack(node.Left)
}
if node.Right != nil {
l.PushBack(node.Right)
}
}
res = append(res, levelRes)
}
return res
}
三、Morris遍历
如果要求遍历的时间复杂度是O(N),空间复杂度是O(1)来完成二叉树的先序,中序,后序遍历。能做到吗?这里的难点在于空间复杂度的限制,之前分析的递归和非递归算法都无法做到。如果完全不用栈结构能完成三种遍历吗?可以,答案是使用二叉树节点中大量指向null的指针,这个算法就是Morris遍历。
递归和非递归算法都使用了栈,在处理完二叉树某个节点后 可以回到上层取。为什么从下层到上层会这么困难。因为二叉树结构如此,每个节点都有指向孩子节点的指针,但是没有指向父节点的指针。所以从下层到上层需要用栈结构辅助完成。Morris遍历的实质就是避免使用栈结构,而是让下层到上层有指针,具体就是将底层空闲的null指针指向上层某个节点,从而完成从下层到上层的移动。
中序遍历
中序遍历的过程如下:
- 假设当前子树的根节点为c1,让c1的左子树中最右节点的right指针指向c1,然后c1的左子树继续执行步骤1的处理过程,直到遇到一个没有左子树的节点node,进入步骤2
- 从node开始通过每个节点的right指针进行移动,并依次打印,假设移动到的节点为cur。对每一个cur节点都判断 cur节点的左子树的最右节点是否指向cur。有两种情况:
1.如果是,让cur节点的左子树中最右节点的right指针指向空,也就是把步骤1调整过的指针再调整回来,然后打印cur,继续通过cur的right指针移动到下一个节点。重复步骤2。
2.如果不是,以cur为根的子树重回步骤1执行。
以上图所示二叉树来说明Morris中序遍历的具体过程:
- 开始时c1指向节点4,c1的左子树最右节点为节点3,让节点3的right指针指向c1
- 令c1=c1.Left,即c1指向节点2,c1的左子树最右节点为节点1,让节点1的right指针指向c1
- 令c1=c1.Left,即c1指向节点1,c1的左子树为空,所以令node=节点1,然后准备进入步骤2。经过这一步后,二叉树的结构如下图:
- 进入步骤2 节点1先打印,然后令c1=c1.Right 即c1=节点2。满足步骤2的条件1 。于是令节点1的右指针等于null。然后打印节点2,然后,c1=c1.Right,即c1指向节点3
- 节点3符合步骤2的条件2,所以以节点3为根节点的子树进入步骤1处理。因为节点3没有孩子节点,所以步骤1很快处理完又回到节点3并打印,然后通过节点3的right指针移动到节点4 。
- 发现节点4符合步骤2的条件2,所以令节点3的right指针指向null,然后打印节点4,再通过节点4的right指针移动到节点6。到目前为止,二叉树又回到了最初的样子。
- 发现节点6符合步骤2的条件2,所以以节点6为根节点的子树进入步骤1处理,处理之后,二叉树变成下图的样子
- 重新来到步骤的2的第一个节点是节点5,发现节点5符合步骤2的条件2,节点5为根的子树进入步骤1,因为这个子树只有节点5,所以步骤1很快完成,然后打印节点5,然后通过节点5的右指针移动到节点6.
- 发现节点6符合步骤2的条件1,所以令节点5的右指针指向null,然后打印节点6,再通过节点的right指针移动到节点7。到目前为止二叉树又回到了最初的样子
- 节点7符合步骤2的条件2,以节点7位根节点的子树,经过步骤1和步骤2并打印。然后通过7的right指针移动到null。
- 步骤2最终移动到null。整个过程结束。
代码如下:
func (root *TreeNode) InOrderMorris() []int {
ret := make([]int, 0)
c1 := root
var c2 *TreeNode
for c1 != nil {
c2 = c1.Left
if c2 != nil {
for c2.Right != nil && c2.Right != c1 {
c2 = c2.Right
}
// 找c1左子树到最右节点
if c2.Right == nil {
// 如果最右节点指向null 则将其指向c1
c2.Right = c1
// c1继续向下
c1 = c1.Left
continue
} else {
// 如果他指向c1 将其设置为nil,并打印c1
c2.Right = nil
}
}
// 没有左子树的节点 node 打印node 然后通过right指针移动
ret = append(ret, c1.Val)
c1 = c1.Right
}
return ret
}
先序遍历
先序遍历是中序遍历的简单改写,只需将打印时机放在进入这个子树的时候即可。代码如下
func (root *TreeNode) PreOrderMorris() []int {
ret := make([]int, 0)
c1 := root
var c2 *TreeNode
for c1 != nil {
c2 = c1.Left
if c2 != nil {
for c2.Right != nil && c2.Right != c1 {
c2 = c2.Right
}
if c2.Right == nil {
// 找c1左子树到最右节点
c2.Right = c1
// 进入这个子树的时候就打印该节点
ret = append(ret, c1.Val)
c1 = c1.Left
continue
} else {
// 找c1左子树到最右节点 他指向c1 将其设置为nil
c2.Right = nil
}
} else {
// 没有左子树的节点 node 也直接打印
ret = append(ret, c1.Val)
}
c1 = c1.Right
}
return ret
}
后序遍历
后序遍历也是中序遍历的改写,打印时机放在回调right指针为null的时候 ,并且打印需要逆序打印最子树的右边界节点。代码如下
func (root *TreeNode) PosOrderMorris() []int {
ret := make([]int, 0)
c1 := root
var c2 *TreeNode
for c1 != nil {
c2 = c1.Left
if c2 != nil {
for c2.Right != nil && c2.Right != c1 {
c2 = c2.Right
}
// 找c1左子树到最右节点
if c2.Right == nil {
// 如果最右节点指向null 则将其指向c1
c2.Right = c1
// c1继续向下
c1 = c1.Left
continue
} else {
// 如果他指向c1 将其设置为nil,并逆序打印c1的左子树的右边界
c2.Right = nil
ret = append(ret, printEdge(c1.Left)...)
}
}
c1 = c1.Right
}
// 逆序打印根节点的右边界
ret = append(ret, printEdge(root)...)
return ret
}
func printEdge(node *TreeNode) []int {
ret := make([]int, 0)
tail := reverseEdge(node)
cur := tail
for cur != nil {
ret = append(ret, cur.Val)
cur = cur.Right
}
reverseEdge(tail)
return ret
}
func reverseEdge(from *TreeNode) *TreeNode {
var pre *TreeNode
var next *TreeNode
for from != nil {
next = from.Right
from.Right = pre
pre = from
from = next
}
return pre
}
参考
左程云 《程序员代码面试指南》