关于二叉树的递归遍历就不写了,这里介绍一下非递归方式
二叉树的非递归遍历
二叉树结构如下:
public static class Node {
public intvalue;
public Node left;
public Node right;
public Node(int value) {
this.value = value;
}
先序
需要按照根-左-右的方式遍历。
所以将根节点压入栈后,先输出它的值。接着看看根节点有无右子树,如果有则把右节点压入,然后再压左子树,因为栈是后进先出的所以后压入左子树可以保证其先输出。
public void nonRecursionPreOrder(Node head) {
if (head != null) {
Stack<Node> stack = new Stack<>();
stack.push(head);
while (!stack.empty()) {
head = stack.pop();
System.out.println(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
}
中序
需要按照左-根-右的方式遍历。
public void nonRecursionPreOrder(Node head) {
if (head != null) {
Stack<Node> stack = new Stack<>();
while (!stack.empty() || head != null) {
if (head.left != null) {
stack.push(head.left);
head = head.left;
}else {
head = stack.pop();
System.out.println(head.value + " ");
head = head.right;
}
}
}
}
后序
需要按照左-右-根的方式遍历
在先序遍历的时候,是先打印根节点,然后将其右、左孩子分别压栈。我们可以把其改成根-右-左的遍历方式,这很容易做到。但在这个过程中,我们把打印的位置换成一个栈,当程序执行结束后再依次弹出栈,于是便得到了左-右-根的遍历顺序。
public void nonRecursionPostOrder(Node head) {
if (head !=null) {
Stack<Node> stack = new Stack<>();
Stack<Node> stack2 = new Stack<>();
stack.push(head);
while (!stack.empty()) {
head = stack.pop();
stack2.push(head);
if (head.left != null) {
stack.push(head.left);
}
if (head.right != null) {
stack.push(head.right);
}
}
while (!stack2.empty()) {
System.out.println(stack2.pop().value + " ");
}
}
}
树的序列化和反序列化
序列化:将树的结构从内存保存到硬盘中。
下图是先序序列化的代码:
反序列化:读取树的文件加载到内存中,下图的preStr就是从文件中读取的由上一步存储到文件中的字符串。下图是先序反序列化的代码,序列化是怎么做的,反序列换同样怎么做。
按层序列化
练习题
分析:
- 若一个节点X有右子树,则它的后继节点是右子树最左的节点。
- 若X没有右子树,则它的后继节点有两种情况:
- 情况1如下图,节点8没有右子树,它是其父节点4的左孩子,因此节点8的后继节点就是4
- 情况2如下图,节点11没有右子树,它是其父节点的右孩子,这意味着父节点在11节点之前就已经访问过了。继续向上移动,判断节点11的父节点5与节点5的父节点2的关系,如果5是2的右孩子,那么继续上述过程;如果5是2的左孩子,说明2就是11的后继节点。但在下图的例子中,显然5是2的右孩子,因此要看2和其父节点1的关系,2是1的左孩子,因此1就是节点11的后继节点。
- 需要注意的是,最后一个节点,在本例中为7,是没有后继节点的。
判断一棵二叉树是否为平衡二叉树
判断一棵二叉树是否为搜索二叉树/完全二叉树
**搜索二叉树:**对于每个节点,它的左子树都比它小,右子树都比它大。
二叉树中序遍历后如果是升序的就是搜索二叉树。具体来说,对于前面给出的二叉树中序非递归代码,只要在输出语句那里加上一个判断:这次打印的值是否比上一次的大即可。
完全二叉树:
若用层序遍历:
- 发现某个节点只有右孩子但没有左孩子,则一定不是完全二叉树
- 上一条执行后,当第一次发现某节点的孩子不是双全的状态,即两种情况:1. 叶子节点 2. 有左孩子无右孩子。这时要求后面的节点全都要为叶子结点,否则不是完全二叉树。
- 若程序执行到这里,说明此树为完全二叉树。
代码如下,leaf表示当前节点是否为非双全状态的节点,若为true则意味着之后的节点必须全都是叶子节点。
求完全二叉树节点的个数,要求时间复杂度低于O(N),N为其节点个数
我们知道,对于高度为h的二叉树,其节点数为2^h-1。
所以执行以下过程:
- 先向左走到头,得出二叉树的高度,记为h。
- 然后看看根节点的右孩子的最左子树高度是多少,如果等于h,说明整棵树的左部分肯定是完全二叉树(由完全二叉树的概念可知),那么左部分的节点数就是2^(h-level) - 1,加上根节点就是2^(h-level):
现在要求的就是下图所示的树的节点个数,这是原问题的子问题,可以通过递归求解。
- 如果不等于h,说明右子树至少在level-h-1的高度是满二叉树,同样通过公式求解,别忘了把根节点加上:
而左侧又是原问题的子问题,同样可以通过递归求解。
代码如下,h代表树的总高度。1 << (h-level)的意思是 2的(h-level)次方,它对应着上面说的根节点的右子树的最左子树高度等于树的高度h的情况,此时整个左子树是一个满二叉树,节点个数为2的(h-level)次方-1,再加上根节点,正好是 2的(h-level)次方,即1 << (h-level)。
说明:
mostLeftLevel方法的参数是一个节点和当前的层高度(也就是层数),返回的是这个节点的最左子树的高度,可以借助下图理解。
现在有这么一棵树,其高度为5,左子树是满二叉树。mostLeftLevel方法会计算出树的高度h为5,然后进入程序的主逻辑:
- 第一个if是看当前节点的右孩子的最左子树高度是多少,由于是从当前节点的右孩子开始向左遍历,因此使用mostLeftLevel计算树的高度时应该将当前节点的高度1也算上,所以level+1。如果算出来的结果与h相等,说明当前节点的左子树一定为满二叉树,使用公式计算即可,别忘了把当前节点也加上。
- 接着开始下一轮递归,现在的树变成了这样:
注意这时root的level由1变成了2,所以在用其右孩子计算最左子树的高度时,mostLeftLevel中level参数应该传2 + 1,算出来仍然等于h,即5。说明当前root的左子树为满二叉树,再次使用公式计算。
- 再一次递归,root的level变成了3,所以mostLeftLevel的参数应该传4,这次root节点的右子树的最左子树和h不相等,这说明root的左侧可能不是满二叉树,当然也可能是。但至少能说明root的右子树是h-level-1的满二叉树,因此用公式求出root右子树的节点个数+root节点。
- 再一次递归,此时root的level为4,所以mostLeftLevel的参数应该传4,求出结果为5,说明root的左子树是满二叉树,仍然使用上面的公式计算节点数。
- 最后一次递归,只剩下了最后一个节点,其level为5,和h相等。说明是叶子节点,因此返回1。
时间复杂度:对于每一层,每次只选其中一个节点进行操作,一共有logN层,所以要选logN个节点。对于每个节点,每次都需要看看其右孩子的最左子树高度,这需要logN的时间,因此总的时间复杂度是O(logN * logN)。