zoukankan      html  css  js  c++  java
  • 树:遍历算法

    树的遍历也一直都是重点,主要是在建造了一棵树之后,如何将这棵树输出来确定创建的树是否正确就成了问题。网上现在也有很多的方法来输出树,python也有专门的包来可视化,不过今天主要总结最基础的遍历算法。

    树的遍历主要根据访问根节点的时机来分为先序、中序、后序和层次遍历。其中要掌握了十种算法,分别是先序递归和先序非递归(深度优先搜索)、中序递归和中序非递归、后序递归和俩种后序非递归(单栈和双栈)、层次遍历(广度优先搜索),Morris遍历以及改善版。下面八种对图1的树进行遍历。


    图1. 实例树

    先序

    首先访问根节点、然后访问左子树,最后访问右子树。如果用递归实现这个思想非常简单,代码如下

        // 前序遍历,递归
        private void printTree(TreeNode node) {
        	if(node != null) {
        		System.out.print(node.node_value + ", ");
        		// 访问左子树
        		printTree(node.left_node);
        		// 访问右子树
            	printTree(node.right_node);
        	}
        }
    

    对于非递归的思想,先来看一下对于图1中的先序遍历的过程,就是先访问根节点6,之后访问6的左子树,再访问左子树的根2,之后访问根2的左子树1,之后再访问根2的右子树的根4,再访问根4的左子树3,这时已经访问完根6的左子树,可以访问根6的右子树的根8。所以最后得到的顺序是6,2,1,4,3,8。

    这时的过程有点类似DFS(深度优先搜索),都是一条路走到底,因此使用DFS来实现。其中使用到了栈来存储中间过程,主要注意的就是压入栈的顺序,是先将右节点压进去,再将左节点压进去,这时因为栈是先进后出。

        // 深度遍历,使用的是栈,前序遍历得非递归实现
        private void printTree(TreeNode node) {
        	Stack<TreeNode> tree_stack = new Stack<TreeNode>();
        	tree_stack.push(node);
        	while(!tree_stack.empty()) {
        		TreeNode stack_element = tree_stack.pop();
    			if(stack_element.right_node != null) tree_stack.push(stack_element.right_node);
    			if(stack_element.left_node != null) tree_stack.push(stack_element.left_node);
    			System.out.print(stack_element.node_value + ", ");
        	}
        	System.out.println();
        }
    

    中序

    首先访问左子树,然后访问根节点,最后访问右子树。下面先用递归来实现中序算法。

        // 中序遍历,递归
        private void printTree(TreeNode node) {
        	if(node != null) {
        		printTree(node.left_node);
        		System.out.print(node.node_value + ", ");
            	printTree(node.right_node);
        	}
        }
    

    先来看看中序在具体树上是怎么遍历的,先访问根6的左子树,再访问左子树2的左子树1,访问完1,就是访问左子树根2,再访问左子树2的右子树,直接到2的右子树的左子树3,之后访问2的右子树的根4,这时根6的左子树已经全部访问完,可以访问根6,最后访问右子树8,最后遍历的顺序是1,2,3,4,6,8。

    还是用栈这个结构,先将根压入栈中,然后不断地将左子树压进去,直到叶节点,之后可以对栈中的每个节点访问,访问完一个节点之后,再看这个节点是否有右子树,如果有,先压入栈中,再判断这个右子树是否有左子树,如果还有左子树,那么就一直压入栈中,如果没有左子树,那么继续取栈中的元素,之后再判断是否有右子树。如此过程,一直进行下去。整个过程就是判断这个节点是不是null,是的话,就拿栈中下一个节点,不是的话,就压入栈中,之后判断节点左子树是不是null。

        // 中序遍历,非递归
        // 思想就是用栈,先把当前节点的左子树压进去,之后拿出节点,再把右子树压进去
        private void printTree(TreeNode node) {
        	Stack<TreeNode> tree_stack = new Stack<TreeNode>();
        	
        	while(node != null || !tree_stack.empty()) {
        	    // 判断节点是否存在,主要是为了判断左子树是不是存在
        		if(node != null) {
        			tree_stack.push(node);
        			node = node.left_node;
        		}else {
        		    // 如果节点不存在,那么拿出栈中元素
        			TreeNode current_node = tree_stack.pop();
        			System.out.print(current_node.node_value + ", ");
        			node = current_node.right_node;
        		}
        	}	
        }
    

    后序

    首先访问左子树,之后访问右子树,最后访问根节点。用递归思想依旧很容易。

        // 后序遍历,递归
        private void printTree(TreeNode node) {
        	if(node != null) {
        		printTree(node.left_node);
            	printTree(node.right_node);
            	System.out.print(node.node_value + ", ");
        	}
        }
    

    先来看看后序具体的过程,先访问根6的左子树,再访问根6左子树的左子树1,访问完1之后,再看左子树2的右子树4,然后访问左子树2的右子树4的左子树3,访问完3之后,可以访问根4,然后就可以访问左子树的根2,这时根6的左子树全部访问完成,再访问根6的右子树8,最后访问根6,最后的访问顺序就是1,3,4,2,6,8。

    后序非递归思想是当中最难的,这时因为根节点最后才能访问,如果我们根据中序非递归的写法来看后序非递归,那么最起码还得有一个标识,标记节点的右子树是否被访问了,左子树就不用标记,因为栈中拿出来的就是左子树。如果访问过了,那么可以拿出来,如果没有,那么将右子树压进入。这时右子树的访问在根节点之前,那么只有看前一个访问过的节点是不是当前根节点的右节点就行。

        // 后序遍历,非递归
        // 使用一个栈,外加一个标志,这个标志就是说明右子树是否被访问了。
        private void printTree(TreeNode node) {
        	Stack<TreeNode> tree_stack = new Stack<TreeNode>();
        	// 标记右子树是否被访问
        	TreeNode previsited_node = null;
        	while(node != null || !tree_stack.empty()) {
        		if(node != null) {
        			tree_stack.push(node);
        			node = node.left_node;
        		}else {
        			TreeNode right_node = tree_stack.peek().right_node;
        			// 查看是否有右子树或者右子树是否被访问
        			if(right_node == null || right_node == previsited_node) {
        				TreeNode current_node = tree_stack.pop();
        				System.out.print(current_node.node_value + ", ");
        				previsited_node = current_node;
        				node = null;
        			}else {
        				// 处理右子树
        				node = right_node;
        			}
        		}
        	}
        }
    

    但是如果使用俩个栈的话,这个非递归算法的思想就可以很简单了,如果我们先将右子树压进去,再将左子树压进去,那么这个栈中就是后序遍历顺序。不过这需要俩个栈,一个栈保存结果,一个栈拿出压进去的节点,看这个节点是不是有左子树,有的话,压入俩个栈中。

        // 后序遍历,非递归
        // 使用双栈遍历,思想其实和中序递归差不多。
        // 一个栈是保存全部数据的,一个栈是把数据拿出来,找到它的左子树的,找到之后全部放入输出栈中
        private void printTree(TreeNode node) {
        	Stack<TreeNode> tree_stack = new Stack<TreeNode>();
        	Stack<TreeNode> output_stack = new Stack<TreeNode>();
        	
        	while(node != null || !tree_stack.empty()) {
        		if(node != null) {
        		    // 如果节点存在,那么压入俩个栈中
        			tree_stack.push(node);
        			output_stack.push(node);
        			node = node.right_node;
        		}else {
        			TreeNode current_node = tree_stack.pop();
        			node = current_node.left_node;
        		}
        	}
        	// 输出栈中的元素就是后序
        	while(!output_stack.empty()) {
        		TreeNode temp_node = output_stack.pop();
        		System.out.print(temp_node.node_value + ", ");
        	}
        }
    

    BFS(广度优先搜索)层次遍历

    层次遍历就是一层一层的输出节点,按照从左往右的顺序进行输出,图1树中输出的顺序就是6,2,81,4,3,这种类似于广度优先搜索算法,那么需要的是队列结构,这里就是先进先出,就是先将根节点进队,之后从队中拿出节点,看这个节点是否有左节点和右节点,有的话,就进队,然后不断地对队列中的树进行判断进队就行,直到队列中没有节点。

        // 广度层次遍历,使用的是队列
        private void printTree(TreeNode node) {
        	Queue<TreeNode> tree_queue = new LinkedList<TreeNode>(); 
    		tree_queue.offer(node);
    		// 查看队列中是否还有节点
    		while(!tree_queue.isEmpty()) {
    		    // 拿出队列中的节点
    			TreeNode queue_element = tree_queue.poll();
    			if(queue_element.left_node != null) tree_queue.offer(queue_element.left_node);
    			if(queue_element.right_node != null) tree_queue.offer(queue_element.right_node);
    			System.out.print(queue_element.node_value + ", ");
    		}
    		System.out.println();
    	}
    

    Morris遍历(线索二叉树)

    上面的遍历一般都是使用递归、栈或者队列实现的,空间复杂度和时间复杂度都是(O(n)),因为遍历的话,你肯定需要访问每个元素依次,那么时间复杂度最少就是(O(n)),能优化的也只是空间复杂度。之前都是使用数据结构存储还未访问的节点,如果我们不存储呢,每次访问一个节点时候,如果有左子树,直接找出直接前驱,这个前驱肯定是个叶子节点,没有右子树,将这个前驱节点的右节点与当前节点链接,形成线索二叉树那样的链接,之后去掉当前节点的左链接,那么是不是直接将树结构变成单链表结构,并且还是中序遍历,如果遍历的是BST树,那么遍历出来的是从小到大的顺序的。


    图2. 树结构变单链表

    为了能够形象的展示遍历过程,将上面图的过程进行详细的表述:

    1. 节点6设置为当前节点;
    2. 查看节点6是否有左子树,发现有,那么找出左子树中的最右侧节点4,也就是当前节点的直接前驱,将最右侧节点4的右节点指向当前节点6;
    3. 将当前节点设置为节点6的左节点2,然后删除节点2和节点6的链接;
    4. 查看节点2是否有左子树,发现有,那么找出左子树中的最右侧节点1,将节点1的右节点指向当前节点2;
    5. 将当前节点设置为节点2的左节点1,然后删除节点2和节点1的链接;
    6. 查看节点1是否有左子树,发现当前节点1没有左子树,那么访问当前节点1,之后进入节点1的右链接中,到了节点2,节点2设置成当前节点;
    7. 查看当前节点2是否有左子树,发现没有,访问当前节点2,进入节点2的右子树中,到了节点4,将之设置成当前节点;
    8. 查看当前节点4是否有左子树,发现有,那么找出左子树中的最右侧节点3,将之右节点指向当前节点4;
    9. 将当前节点设置成节点4的左节点3,然后删除节点4和节点3的链接;
    10. 查看当前节点3是否有左子树,发现没有,访问当前节点3,之后进入节点3的右链接中,到了节点4,节点4设置成当前节点;
    11. 查看当前节点4是否有左子树,发现没有,访问当前节点4,之后进入节点4的右链接中,到了节点6,节点6设置成当前节点;
    12. 查看当前节点6是否有左子树,发现没有,访问当前节点6,之后进入节点6的右链接中,到了节点8,节点8设置成当前节点;
    13. 查看当前节点8是否有左子树,发现没有,访问当前节点8,之后进入节点6的右链接中,发现是null,那么退出循环;
        // 使用线索二叉树的Morris遍历,但是会改变树的结构,直接变成单项升序链表形式
        private void printTree(TreeNode node) {
        	TreeNode current_node = node;
        	while(current_node != null) {
        		// 左孩子不为空的话,就找到左子树的最右节点指向自己
        		if(current_node.left_node != null) {
        			TreeNode leftNode = current_node.left_node;
        			while(leftNode.right_node != null) {
        				leftNode = leftNode.right_node;
        			}
        			leftNode.right_node = current_node;
        			leftNode = current_node.left_node;
        			current_node.left_node = null;
        			current_node = leftNode;
        		}else {
        			System.out.print(current_node.node_value + ", ");
        			current_node = current_node.right_node;
        		}
        	}
        }
    
    

    上面最大的问题就是强行改变了树的结构,将之变成了单链表结构,这在实际应用中,往往是不可取的,但是在上面的程序中,是必须的,因为删除左孩子,是为了防止无限循环,因为判定条件都是节点是否有左孩子。我们加上一个条件来查看是否是第二次访问该节点。当查看当前节点是有左子树的时候,还要看直接前驱右链接是不是当前节点,如果是当前节点,那么删除直接前驱的右链接,并且将当前节点强制移动到右子树中。

        // 上面那个会使得二叉树强行改变,不可取,因此有了这个。
        private void printTree(TreeNode node) {
        	TreeNode current_node = node;
        	while(current_node != null) {
        		// 左孩子不为空的话,就找到左子树的最右节点指向自己
        		if(current_node.left_node == null) {
        			System.out.print(current_node.node_value + ", ");
        			current_node = current_node.right_node;	
        		}else {
        			TreeNode leftNode = current_node.left_node;
        			while(leftNode.right_node != null && leftNode.right_node != current_node) {
        				leftNode = leftNode.right_node;
        			}
        			
        			if(leftNode.right_node == null) {   				
        				leftNode.right_node = current_node;
            			current_node = current_node.left_node;
        			}else {
        				System.out.print(current_node.node_value + ", ");
        				leftNode.right_node = null;
        				current_node = current_node.right_node;
        			}
        		}
        	}
    	}
    

    总结

    其实这章我想等等再写,奈何在写BST树要输出,只好先把遍历这章全部写完,代码也和我得github中BST树的实现放在一起了。树遍历的算法很基础,但是也很重要,以前在本科时,都用C写过,奈何那时没有写博客的习惯,已经全部丢失,只好重新再写一边,就当复习。其中的非递归遍历算法都用到了栈这个数据结构,主要是因为遍历的特性,不过完全自己写的话,还真的很难写。

  • 相关阅读:
    数据中的悖论
    Exchange server2007自动发现服务(Auto discover service)原理及调试
    Exchange 2007 安装完后需要注意的几件事情
    有感
    Exchange Server 2007 LCR小试
    【转】Vmware ESX 3.0出现“error connecting: can not connect to host x.x.x.x: a connection attempt failed because the connected party did not properly respond after a period of time, or established connection failed because connected host has failed……的解决方法
    学习Exchange的几个站点
    在Exchange server 2007中启用pop3和IMAP4协议访问
    Exchange server 2007环境下,outlook2007同步脱机地址簿时出现“0x80190194”错误的分析与解决方法
    安装isa2006后加入域提示“RPC服务器不可用”
  • 原文地址:https://www.cnblogs.com/mary1/p/13355457.html
Copyright © 2011-2022 走看看