漫画算法-学习笔记(09)

漫画算法-小灰的算法之旅(09)

二叉树遍历-非递归方式

二叉树的深度优先遍历还可以采用非递归的方式来实现,不过要稍微复杂一点。

绝大多数可以用递归解决的问题,其实都可以采用另一种数据结构来解决—。因为递归都具备回溯的特性。

如何借助栈来实现二叉树的非递归遍历呢?
前序遍历

思路:在迭代算法中,思路演变成,每到一个节点A,就应该立即访问它。因为每棵树都是先访问其根节点。对节点的左右子树来说,也一定是先访问其根节点。在A的两颗子树中,遍历完左子树后,再遍历右子树。因此在访问完根节点后,遍历左子树前,要将右子树压入栈中。

// 伪码
栈S;
p=root;
while(p || S 不为空){
		while(p){
				访问p节点;
				p的右子树入栈S;
				p=p的左子树;
		}
		p=S栈顶弹出;
}

二叉树的前序遍历具体过程:

  1. 首先遍历二叉树的根节点1,放入栈中。
  2. 遍历根节点1的左孩子节点2,放入栈中。
  3. 遍历节点2的左孩子节点4,放入栈中。
  4. 节点4既没有左孩子,也没有右孩子,我们需要回溯到上一个节点2.可是现在并不是做递归操作,怎么回溯呢?别担心,栈已经存储了刚才遍历的路径。让旧的栈顶元素4出栈,就可以重新访问节点2,得到节点2的右孩子节点5。此时节点2已经没有利用价值(已经访问过左孩子和右孩子),节点2出栈,节点5入栈。
  5. 节点5既没有左孩子,也没有右孩子,我们需要再次回溯,一直回溯到节点1,所以让节点5出栈。根节点1的右孩子是节点3,节点1出栈,节点3入栈。
  6. 节点3的右孩子是节点6,节点3出栈,节点6入栈。
  7. 节点6既没有左孩子,也没有右孩子,所以节点6出栈。此时栈为空,遍历结束。

时间复杂度

  • 时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。

  • 空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。

代码实现

二叉树非递归前序遍历的代码实现


/** 
* 二叉树的非递归前序遍历
* param root 二叉树根节点
*/
public static void preOrderTraveralWithStack(TreeNode root){
  Stack<TreeNode> stack=new Stack<TreeNode>();
  TreeNode treeNode=root;
  while(treeNode!=null || !stack.isEmpty()){
    //		迭代访问节点的左孩子,并入栈
    while(treeNode !=null){
      System.out.println(treeNode.data);
      stack.push(treeNode);
      treeNode=treeNode.leftChild;
    }
    //	如果节点没有左孩子,则弹出栈顶节点,访问节点右孩子
    if (!stack.isEmpty){
      treeNode =stack.pop();
      treeNode=treeNode.rightChild;
    }
  }
}

golang实现

//前序遍历 每到一个节点A,就应该立即访问它。因为每棵树都是先访问其根节点。
//对节点的左右子树来说,也一定是先访问其根节点。在A的两颗子树中,遍历完左子树后,再遍历右子树。
//因此在访问完根节点后,遍历左子树前,要将右子树压入栈中。
func preOrderTraveral(root *TreeNode) {
	stack := []*TreeNode{}
	node := root
	for node != nil || len(stack) > 0 {
		for node != nil {
			log.Println(node.Value)
			stack = append(stack, node)
			node = node.LeftChild
		}
		node = stack[len(stack)-1].RightChild
		stack = stack[:len(stack)-1]
	}
}
中序遍历

思路每到一个节点A,因为根的访问在中间,将A入栈,然后遍历左子树,接着访问A,最后遍历右子树。在访问完A后,A就可以出栈了。因为A和其左子树都已经访问完成。

// 伪码
栈S;
p=root;
while(p || S不为空){
		while(p){
				p入S;
				p=p的左子树;
		}
		p =S.top 出栈;
		访问p;
		p=p的右子树;
}

二叉树的中序遍历,具体过程:

  1. 首先遍历二叉树的根节点1,将节点1入栈,然后遍历根节点的左孩子,如果这个左孩子还存在左孩子,则继续深入访问下去并将该左孩子入栈。
  2. 根节点1的左孩子节点为节点2,因此将节点2入栈
  3. 因为节点2存在左孩子节点,因此继续访问节点2的左孩子节点4,并将节点4入栈。
  4. 因为节点4不再存在左孩子节点,也不存在右孩子节点,此时我们需要回溯到上一个节点2,并将节点4出栈,
  5. 访问节点2,因为节点2还存在右孩子,因此将节点2出栈,并将节点5入栈。
  6. 因为节点5不存在左孩子节点、也不存在右孩子节点,因此我们需要再次回溯,一直回溯到节点1,所以将节点5出栈。根节点1的右孩子节点是节点3,将节点1出栈,节点3入栈。
  7. 节点3的左孩子是节点6,将节点6入栈。
  8. 因为节点6既没有左孩子,也没有右孩子,所以节点6出栈,回溯到节点3,因为节点3没有右孩子节点,因此节点3出栈。至此栈为空,遍历结束。

时间复杂度

  • 时间复杂度 : O(n); 其中n表二叉树节点的数量。二叉树的遍历中每个节点都会被访问一次。
  • 空间复杂度:O(n); 空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到O(n)的级别。

代码实现

/** 
* 二叉树的非递归中序遍历
* param root 二叉树根节点
*/
public static void inOrderTraveral(TreeNode root){
  Stack<TreeNode> stack=new Stack<TreeNode>();
  TreeNode treeNode =root;
  while(treeNode!=null || !stack.isEmpty()){
    
    while(treeNode !=null){
      stack.push(treeNode);
      treeNode=treeNode.leftChild;
    }
    treeNode =stack.pop();
    System.out.println(treeNode.data);
    treeNode=treeNode.rightChild;
  }
}

go实现

/ 中序遍历,先左子树-->再根-->最后右子树
func inOrderTraveral(root *TreeNode) {
	stack :=[]*TreeNode{}
	for root!=nil ||len(stack)>0 {
		for root!=nil {
			stack=append(stack,root)
			root=root.LeftChild
		}
		root=stack[len(stack)-1]
		stack=stack[:len(stack)-1]//出栈
		log.Println(root.Value)
		root=root.RightChild
	}

}
后序遍历

按照左子树-根-右子树的方式,将其转化成迭代方式。

思路: 每到一个节点A,因为根要最后访问,将其入栈,然后遍历左子树,遍历右子树,最后返回到A.

但这样会有一个问题,无法区分是左子树返回,还是有子树返回。

因此,给A节点附加一个标记T。在访问其右子树前,T设置为true。之后子树返回时,当T为true表示从右子树返回,否则从左子树返回。当T为false时,表示A的左子树遍历完,还要访问右子树。

同时,当T为true时,表示A的两颗子树都遍历过了,要访问A了.并且正在A访问完后,A这颗子树都访问完成了。

// 伪码
栈S;
p=root;
T<节点,TRUE/False> :节点标记;
while(p || S不空){
		while(p){
				p入S;
				p =p的左子树;
		}
		while(S不为空,且T[S.top]=true){
				访问S.top;
				S.top出S;
		}
		if(S不空){
				p =S.top的右子树
				T[S.top]=True;
		}
}

时间复杂度

  • 时间复杂度:O(n); 其中n是二叉搜索树的节点数量。
  • 空间复杂度:O(n); 为迭代过程中显示栈道开销,平均情况下为O(logn);最坏情况下树呈链状,为O(n)。

代码实现

/** 
* 二叉树的非递归后序遍历
* param root 二叉树根节点
*/
public static void postOrderTraveral(TreeNode root){
  Stack<TreeNode> stack=new Stack<TreeNode>();
  TreeNode prev =null;
  while(root!=null || !stack.isEmpty()){
    
    while(root !=null){
      stack.push(root);
      root=root.leftChild;
    }
    
    root =stack.pop();
    if(root.rightChild==null || root.rightChild==prev){
      System.out.println(treeNode.data);
      prev=root;
      root=null;
    }else{
      stack.push(root);
      root=root.rightChild;
    }
  }
}

go实现

// 后序遍历,先左子树-->再右子树-->最后根
func postOrderTraveral(root *TreeNode) {
	stack :=[]*TreeNode{}
	var prev *TreeNode
	for root!=nil ||len(stack)>0 {
		for root!=nil {
			stack=append(stack,root)
			root=root.LeftChild
		}
		root=stack[len(stack)-1]
		stack=stack[:len(stack)-1]
		if root.RightChild==nil ||root.RightChild==prev {
			log.Println(root.Value)
			prev=root
			root=nil
		}else {
			stack=append(stack,root)
			root=root.RightChild
		}
	}
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值