数据结构之树(基本概念与二叉树的遍历)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zengxiantao1994/article/details/80689139

一、树的基本概念

(一)基本概念

        1、定义:树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。

        2、树节点的分类:结点拥有的子树数称为结点的度(Degree)。度为0的结点称为叶结点(Leaf)或终端结点;度不为0的结点称为非终端结点或分支结点。除根结点之外,分支结点也称为内部结点。树的度是树内各结点的度的最大值。如图6-2-4所示,因为这棵树结点的度的最大值是结点D的度,为3,所以树的度也为3。



        3、节点间关系:结点的子树的根称为该结点的孩子(Child),相应地,该结点称为孩子的双亲(Parent)。同一个双亲的孩子之间互称兄弟(Sibling)。结点的祖先是从根到该结点所经分支上的所有结点。所以对于H来说,D、B、A都是它的祖先。反之,以某结点为根的子树中的任一结点都称为该结点的子孙。B的子孙有D、G、H、I,如图6-2-5所示。


        4、树的深度:结点的层次(Level)从根开始定义起,根为第一层,根的孩子为第二层。若某结点在第l层,则其子树就在第l+1层。其双亲在同一层的结点互为堂兄弟。显然图6-2-6中的D、E、F是堂兄弟,而G、H、I与J也是堂兄弟。树中结点的最大层次称为树的深度(Depth)或高度,当前树的深度为4。


        如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。

        5、森林:是m(m≥0)棵互不相交的树的集合。


(二)定义总结

        树的结点:包含一个数据元素及若干指向子树的分支;

        孩子结点:结点的子树的根称为该结点的孩子;

        双亲结点:B 结点是A 结点的孩子,则A结点是B 结点的双亲;

        兄弟结点:同一双亲的孩子结点;堂兄结点:同一层上结点;

        祖先结点:从根到该结点的所经分支上的所有结点;子孙结点:以某结点为根的子树中任一结点都称为该结点的子孙;

        结点层:根结点的层定义为1;根的孩子为第二层结点,依此类推;

        树的深度:树中最大的结点层;

        结点的度:结点子树的个数;

        树的度:树中最大的结点度;

        叶子结点:也叫终端结点,是度为0的结点;

        分枝结点:度不为0的结点;

        有序树:子树有序的树,如:家族树;

        无序树:不考虑子树的顺序。


二、二叉树的基本概念

(一)二叉树的定义

        在计算机科学中,二叉树是每个节点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)。二叉树常被用于实现二叉查找树和二叉堆。

        二叉树是递归定义的,其结点有左右子树之分,逻辑上二叉树有五种基本形态:

        (1)、空二叉树;

        (2)、只有一个根结点的二叉树;

        (3)、只有左子树;

        (4)、只有右子树;

        (5)、完全二叉树。

        注意:尽管二叉树与树有许多相似之处,但二叉树不是树的特殊情形。

 

(二)二叉树的性质

        1、二叉树的每个结点至多只有二棵子树(不存在度大于2的结点),二叉树的子树有左右之分,次序不能颠倒;

        2、二叉树的第i层至多个结点;

        3、深度为k的二叉树至多有个结点,至少k个节点;

        4、对任何一棵二叉树T,如果其叶结点数为n_0,度为2的结点数为n_2,则n_0 = n_2 + 1证明:假设二叉树中度为012的节点数分别为:n_0n_1n_2,则总节点数为N = n_0 + n_1 + n_2。同时再来考虑树的子树个数即度的和,二叉树中除了根节点,每个节点都有一个父节点,所以二叉树的度为N-1,而所有的度又等于n_1*1 + n_2*2,所以有:n_0 + n_1 + n_2 – 1 = n_1*1 + n_2*2n_0 = n_2 + 1得证。

        5、给定N个节点,能构成h(N)种不同的二叉树。h(N)为卡特兰数的第N项。


(三)二叉树的类型

        1、完全二叉树:若设二叉树的高度为h,除第h层外,其它各层(1~h-1)的结点数都达到最大个数,第h层有叶子结点,并且叶子结点都是从左到右依次排布,这就是完全二叉树。具有n个结点的完全二叉树的深度为

        2、满二叉树:除了叶结点外每一个结点都有左右子树且叶结点都处在最底层的二叉树。具有n个节点的满二叉树的深度为

        同时完全二叉树和满二叉树还可以定义如下:一棵深度为k,且有个节点的二叉树称之为满二叉树;深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中,序号为1至n的节点对应时(序号按层编),称之为完全二叉树。

        满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

        3、平衡二叉树:平衡二叉树又被称为AVL树(区别于AVL算法),它是一棵二叉排序树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。


(四)存储结构

        二叉树的存储结构一般有两种:一种是顺序存储,即用数组按一定的顺序存储二叉树的节点。另一种是链式存储。链式存储的节点通常包含数据字段、左子树指针、右子树指针。

        有N个结点的完全二叉树各结点如果用顺序方式存储,则结点之间有如下关系:

        若i为结点编号则:如果i > 1,则其父结点的编号为i/2

        如果2*i <= N,则其左子树根结点编号为2*i;若2*i > N,则无左子树;

        如果2*i+1 <= N,则其右子树根结点编号为2*i+1;若2*i+1 > N,则无右子树。


三、二叉树的遍历

        二叉树有前、中、后三种遍历方式,因为树的本身就是用递归定义的,因此采用递归的方法实现三种遍历,不仅代码简洁且容易理解,但其开销也比较大,而若采用非递归方法实现三种遍历,则要用栈来模拟实现(递归也是用栈实现的)。

        已知前序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。已知后序遍历序列和中序遍历序列,可以唯一确定一棵二叉树。已知前序和后序遍历,是不能确定一棵二叉树的。


(一)前序遍历

        前序遍历:根→左→右。若二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,再前序遍历右子树。如下图,前序遍历:ABDGHCEIF。


/**
 * 前序遍历的访问顺序:根左右
 * 前序遍历递归解法: 
 * (1)如果二叉树为空,空操作 
 * (2)如果二叉树不为空,访问根节点,前序遍历左子树,前序遍历右子树
 * 
 * @param root
 */
public static ArrayList<Integer> preOrder(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}

	// 访问根节点
	list.add(root.getValue());
	// 前序遍历左子树
	preOrder(root.getLeft(), list);
	// 前序遍历右子树
	preOrder(root.getRight(), list);
	
	return list;
}

/**
 * 非递归前序遍历
 * 遍历思想:先让根进栈,只要栈不为空,就可以做弹出操作,每次弹出一个结点,
 * 记得把它的左右结点都进栈,记得右子树先进栈,这样可以保证右子树在栈中总处于左子树的下面。(先进后出)
 * 
 * @param root
 */
public static ArrayList<Integer> preOrder1(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}
	
	Stack<TreeNode> stack = new Stack<TreeNode>();
	stack.push(root);
	TreeNode p = null;
	
	while(!stack.isEmpty()) {
		p = stack.pop();
		list.add(p.getValue());
		
		// 右子树先进栈
		if(p.getRight() != null) {
			stack.push(p.getRight());
		}
		if(p.getLeft() != null) {
			stack.push(p.getLeft());
		}
	}
	
	return list;
}

/**
 * 非递归前序遍历
 * 思路是:从根节点开始访问,然后访问左子树,一直到最下方。
 * 然后访问最近访问的左子树的右子树,同样地访问到最下方。
 * 
 * @param root
 */
public static ArrayList<Integer> preOrder2(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}
	
	Stack<TreeNode> stack = new Stack<TreeNode>();
	TreeNode p = root;
	
	// 从根节点开始首先访问左子树的根节点,并入栈
	while(p != null) {
		// 访问
		list.add(p.getValue());
		stack.push(p);
		p = p.getLeft();
	}
	
	while(!stack.isEmpty()) {
		p = stack.pop();
		
		// 从栈中取出的节点,其左子树已经访问过,所以访问其右子树
		if(p.getRight() != null) {
			// 同样地先访问左子树到最下方
			p = p.getRight();
			while(p != null) {
				// 访问
				list.add(p.getValue());
				stack.push(p);
				p = p.getLeft();
			}
		}
	}
	
	return list;
}

public static ArrayList<Integer> preOrder3(TreeNode root, ArrayList<Integer> list) {
	Stack<TreeNode> stack = new Stack<TreeNode>();
	TreeNode p = root;
	
	while(p != null || !stack.isEmpty()) {
		if(p != null) {
			// 先访问再入栈
			list.add(p.getValue());
			stack.push(p);
			p = p.getLeft();
			
		} else {
			p = stack.pop();
			p = p.getRight();
		}
	}
	
	return list;
}


(二)中序遍历

        中序遍历:左→根→右。若树为空,则空操作返回,否则从根节点开始(注意并不是先访问根节点),中序遍历根节点的左子树,然后是访问根节点,最后中序遍历右子树。如下图,中序遍历:GDHBAEICF。


/**
 * 递归中序遍历二叉树:左根右
 * (1)如果二叉树为空,空操作。
 * (2)如果二叉树不为空,中序遍历左子树,访问根节点,中序遍历右子树
 * 
 * @param root
 */
public static ArrayList<Integer> inOrder(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}

	// 中序遍历左子树
	inOrder(root.getLeft(), list);
	// 访问根节点
	list.add(root.getValue());
	// 中序遍历右子树
	inOrder(root.getRight(), list);
	
	return list;
}

/**
 * 
 * @Description:非递归中序遍历
 * 从根节点开始将左子树入栈,直到最下方。然后依次弹出访问,同样将左子树入栈,直到最下方。
 * 
 * @param root
 * @param list
 * @return
 */
public static ArrayList<Integer> inOrder1(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}
	
	Stack<TreeNode> stack = new Stack<TreeNode>();
	TreeNode p = root;
	
	while(p != null) {
		stack.push(p);
		p = p.getLeft();
	}
	
	while(!stack.isEmpty()) {
		p = stack.pop();
		// 访问
		list.add(p.getValue());
		
		// 从栈中取出的节点,其左子树与根都访问过,考虑右节点
		if(p.getRight() != null) {
			p = p.getRight();
			while(p != null) {
				stack.push(p);
				p = p.getLeft();
			}
		}
	}
	
	return list;
}

public static ArrayList<Integer> inOrder2(TreeNode root, ArrayList<Integer> list) {
	Stack<TreeNode> stack = new Stack<TreeNode>();
	TreeNode p = root;
	
	while(p != null || !stack.isEmpty()) {
		if(p != null) {
			// 先入栈
			stack.push(p);
			p = p.getLeft();
			
		} else {
			p = stack.pop();
			// 访问
			list.add(p.getValue());
			p = p.getRight();
		}
	}
	
	return list;
}


(三)后序遍历

        后序遍历:左→右→根。若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后访问根节点。如图,后序遍历:GHDBIEFCA。

/**
 * 递归后序遍历二叉树:左右根
 * (1)如果二叉树为空,空操作。
 * (2)如果二叉树不为空,后序遍历左子树,后序遍历右子树,访问根节点
 * 
 * @param root
 */
public static ArrayList<Integer> postOrder(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}

	// 后序遍历左子树
	postOrder(root.getLeft(), list);
	// 后序遍历右子树
	postOrder(root.getRight(), list);
	// 访问根节点
	list.add(root.getValue());
	
	return list;
}


public static ArrayList<Integer> postOrder1(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}
	
	Stack<TreeNode> stack = new Stack<TreeNode>();
	TreeNode p = root;
	// 一个用来记录上一次访问的节点(只有当一个节点的右子树为空,或者右子树上一次已经访问,才访问自己)
	TreeNode pre = null;
	
	
	while(p != null) {
		
		// 左子树入栈
		while(p.getLeft() != null) {
			stack.push(p);
			p = p.getLeft();
		}
		
		// 当前节点无右子树,或者已经输出
		while(p != null && ( p.getRight() == null || p.getRight() == pre ) ) {
			// 访问
			list.add(p.getValue());
			// 记录上一次访问的节点
			pre = p;
			
			if(stack.isEmpty()) {
				return list;
			}
			p = stack.pop();
		}
		
		// 处理右子树
		stack.push(p);
		p = p.getRight();
	}
	
	return list;
}

public static ArrayList<Integer> postOrder2(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}
	
	Stack<TreeNode> stack = new Stack<TreeNode>();
	TreeNode p = root;
	// 一个用来记录上一次访问的节点(只有当一个节点的右子树为空,或者右子树上一次已经访问,才访问自己)
	TreeNode pre = null;
	
	while(p != null || !stack.isEmpty()) {
		// 左子树入栈
		while(p != null) {
			stack.push(p);
			p = p.getLeft();
		}
		
		if(!stack.isEmpty()) {
			TreeNode temp = stack.peek().getRight();
			if(temp == null || temp == pre) {
				p = stack.pop();
				// 访问
				list.add(p.getValue());
				// 记录上一次访问的节点
				pre = p;
				
				p = null;
				
			} else {
				p = temp;
			}
		}
	}
	
	return list;
}

/**
 * 
 * @Description:双栈法后序遍历二叉树(构造一个中间栈来存储逆后序遍历的结果)
 * 
 * @param root
 * @param list
 * @return
 */
public static ArrayList<Integer> postOrder3(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}
	
	// 遍历辅助栈
	Stack<TreeNode> stack = new Stack<TreeNode>();
	// 中间栈
	Stack<TreeNode> temp = new Stack<TreeNode>();
	TreeNode p = root;
	
	while(p != null || !stack.isEmpty()) {
		while(p != null) {
			stack.push(p);
			temp.push(p);
			// 根  右  左  则为后序遍历的逆序
			p = p.getRight();
		}
		
		if(!stack.isEmpty()) {
			p = stack.pop();
			p = p.getLeft();
		}
	}
	
	while(!temp.isEmpty()) {
		p = temp.pop();
		list.add(p.getValue());
	}
	
	return list;
}


(四)层序遍历

        层序遍历:从上而下逐层遍历。若树为空,则空操作返回。否则从树的第一层,也就是根节点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对接点逐个访问。如图:层序遍历:ABCDEFGHI。

/**
 * 
 * @Description:层序遍历二叉树,用队列实现,先将根节点入队列,只要队列不为空,然后出队列,并访问,接着将访问节点的左右子树依次入队列
 * 
 * @param root
 * @param list
 * @return
 */
public static ArrayList<Integer> layerOrder(TreeNode root, ArrayList<Integer> list) {
	if (root == null) {
		return null;
	}
	
	Queue<TreeNode> queue = new LinkedList<TreeNode>();
	queue.add(root);
	
	while(!queue.isEmpty()) {
		TreeNode node = queue.poll();
		list.add(node.getValue());
		
		if(node.getLeft() != null) {
			queue.add(node.getLeft());
		}
		if(node.getRight() != null) {
			queue.add(node.getRight());
		}
	}
	
	return list;
}

猜你喜欢

转载自blog.csdn.net/zengxiantao1994/article/details/80689139