漫画算法-小灰的算法之旅(09)
二叉树遍历-非递归方式
二叉树的深度优先遍历还可以采用非递归的方式来实现,不过要稍微复杂一点。
绝大多数可以用递归解决的问题,其实都可以采用另一种数据结构来解决—栈。因为递归和栈都具备回溯的特性。
如何借助栈来实现二叉树的非递归遍历呢?
前序遍历
思路:在迭代算法中,思路演变成,每到一个节点A,就应该立即访问它。因为每棵树都是先访问其根节点。对节点的左右子树来说,也一定是先访问其根节点。在A的两颗子树中,遍历完左子树后,再遍历右子树。因此在访问完根节点后,遍历左子树前,要将右子树压入栈中。
// 伪码
栈S;
p=root;
while(p || S 不为空){
while(p){
访问p节点;
p的右子树入栈S;
p=p的左子树;
}
p=S栈顶弹出;
}
二叉树的前序遍历具体过程:
- 首先遍历二叉树的根节点1,放入栈中。
- 遍历根节点1的左孩子节点2,放入栈中。
- 遍历节点2的左孩子节点4,放入栈中。
- 节点4既没有左孩子,也没有右孩子,我们需要回溯到上一个节点2.可是现在并不是做递归操作,怎么回溯呢?别担心,栈已经存储了刚才遍历的路径。让旧的栈顶元素4出栈,就可以重新访问节点2,得到节点2的右孩子节点5。此时节点2已经没有利用价值(已经访问过左孩子和右孩子),节点2出栈,节点5入栈。
- 节点5既没有左孩子,也没有右孩子,我们需要再次回溯,一直回溯到节点1,所以让节点5出栈。根节点1的右孩子是节点3,节点1出栈,节点3入栈。
- 节点3的右孩子是节点6,节点3出栈,节点6入栈。
- 节点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,因此将节点2入栈
- 因为节点2存在左孩子节点,因此继续访问节点2的左孩子节点4,并将节点4入栈。
- 因为节点4不再存在左孩子节点,也不存在右孩子节点,此时我们需要回溯到上一个节点2,并将节点4出栈,
- 访问节点2,因为节点2还存在右孩子,因此将节点2出栈,并将节点5入栈。
- 因为节点5不存在左孩子节点、也不存在右孩子节点,因此我们需要再次回溯,一直回溯到节点1,所以将节点5出栈。根节点1的右孩子节点是节点3,将节点1出栈,节点3入栈。
- 节点3的左孩子是节点6,将节点6入栈。
- 因为节点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
}
}
}