二叉树的各种遍历

二叉树的遍历一般有如下几种,先序遍历(根->左->右),中序遍历(左->根->右),后序遍历(左->右->根),层序遍历。各种遍历的实现都有递归和非递归方式,下面分别描述。

假设二叉树定义如下

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

 二、非递归实现

先序遍历

用非递归的方式实现先序遍历的过程如下:

  1. 申请一个新的栈,记为stack,然后将跟节点root压入栈;
  2. 从stack中弹出栈顶元素,记为cur,打印cur节点的值,然后将cur的右孩子压入stack(不为空的话), 最后将cur的左孩子(不为空的话)压入stack;
  3. 重复步骤2直到stack为空,过程结束;

 以下图所示的树为例,整个过程为

说明stackcurres
根节点1入栈[1]  
1出栈,并打印[]11
右子树3入栈[3]  
右子树2入栈[2,3]  
2出栈,并打印[3]21,2
右子树5入栈[5,3]  
左子树4入栈[4,5,3]  
4出栈,并打印[5,3]41,2,4
4没有子树,5出栈,并打印[3]51,2,4,5
5没有子树,3出栈,并打印[]31,2,4,5,3
右子树7入栈[7]  
左子树6入栈[6,7]  
6出栈,并打印[7]61,2,4,5,3,6
6没有子树,7出栈,并打印[]71,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
}

中序遍历

用非递归的方式实现中序遍历,具体过程如下

  1. 申请一个新的栈,记为stack,令cur=root;
  2. 先把cur节点压入stack,对以cur为头的整个子树来说,依次把左边界压入stack,即不停的令cur=curl.Left,然后重复步骤2;
  3. 重复步骤2,直到发现cur为null,然后从stack中弹出一个节点,记为node,打印该节点值,然后让cur=node.Right,然后继续重复步骤2;
  4. 当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
}

后序遍历

后序遍历相对复杂一下,先介绍用两个栈的实现方式,然后再介绍用一个栈的实现方式。

两个栈实现

具体过程如下:

  1. 申请两个栈s1,s2,然后先将根节点压入s1中
  2. 从s1中弹出的节点记为cur,然后依次将cur的左孩子和右孩子压入s1中
  3. 整个过程中从s1弹出的每一个节点都压入s2
  4. 不断重复步骤2和步骤3直到s1为空
  5. 从s2依次弹出节点并打印,打印的顺序就是后序遍历的顺序

 通过上述过程可以知道,每棵子树的根节点都先弹出,然后左孩子右孩子入栈,所以s1的弹出顺序是根,右,左。所以s2的入栈顺序也是根,右,左,所以s2的出栈顺序是左,右,根。刚好是后序遍历的顺序。

还是以1,2,3,4,5,6,7这个二叉树为例来说明整个过程

说明s1curs2
开始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
}

一个栈实现

具体过程如下:

  1. 申请一个栈,记为stack,将根节点压入stack,同时设计两个变量h和c。整个流程中,h代表最近一次弹出并打印的节点,c代表stack的栈顶节点,初始时,h为根节点,c为null;
  2. 每次令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
}

层序遍历

层序遍历的非递归实现,需要用到一个队列,过程如下

  1. 申请一个队列,记为list,将根节点加入队列中。
  2. 此时队列中元素的个数,即为该层的元素个数。从队列中取出队首元素,记为cur。将cur的左孩子(如果存在)和右孩子(如果存在)加入队列。
  3. 重复步骤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指针指向上层某个节点,从而完成从下层到上层的移动。

中序遍历

中序遍历的过程如下:

  1. 假设当前子树的根节点为c1,让c1的左子树中最右节点的right指针指向c1,然后c1的左子树继续执行步骤1的处理过程,直到遇到一个没有左子树的节点node,进入步骤2
  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
}

参考

左程云 《程序员代码面试指南》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值