二叉树结构
实现二叉树的先序、中序、后序遍历,包括递归方式和非递归方式
题目:
用递归和非递归方式,分别按照二叉树先序、中序和后序打印所有的节点。我们约定:先序遍历顺序为根、左、右;中序遍历顺序为左、根、右;后续遍历顺序为左、右、根。
思考:
用非递归方式实现二叉树的先序遍历,具体过程如下:
- 申请一个新的栈,记为stack。然后将头节点head压入stack中。
- 从stack中弹出栈顶节点,记为cur,然后打印cur节点的值,再将节点cur的右孩子(不为空的话)先压入stack中,最后将cur的左孩子(不为空的话)压入stack中.
- 不断重复步骤2,直到stack为空,全部过程结束。
用非递归方式实现二叉树的中序遍历,具体过程如下:
- 申请一个新的栈,记为stack。初始时,令变量cur=head。
- 先把cur节点压入栈中,对以cur节点为头的整棵子树来说,依次把左边界压入栈中,即不停地令cur=cur.left,然后重复步骤1.
- 不断重复步骤2,直到发现cur为空,此时从stack中弹出一个节点,记为node。打印node的值,并且让cur=node.right,然后继续重复步骤2.
- 当stack为空且cur为空时,整个过程停止。
用非递归方式实现二叉树的后序遍历稍显麻烦,有两种方式:
方法一:使用两个栈实现后序遍历的过程,具体如下:
- 申请一个栈,记为s1,然后将头节点head压入s1中。
- 从s1中弹出的节点记为cur,然后依次将cur的左孩子和右孩子压入s1中。
- 在整个过程中,每一个从s1中弹出的节点都放进s2中。
- 不断重复步骤2和步骤3,直到s1为空,过程停止。
- 从s2中依次弹出节点并打印,打印的顺序就是后序遍历的顺序。
方法二:使用一个栈实现后序遍历的过程,具体如下:
1.申请一个栈,记为stack,将头节点压入stack,同时设置两个变量head和cur。在这个流程中,head代表最近一次弹出并打印的节点,cur代表stack的栈顶节点,初始时head为头节点,cur为null。
2.每次令cur等于当前stack的栈顶节点,但是不从stack中弹出,此时分以下三种情况:
①如果cur的左孩子不为null,并且head不等于cur的左孩子,也不等于cur的右孩子,则把cur的左孩子压入stack中。head的意义是最近一次弹出并打印的节点,所以如果head等于cur的左孩子或右孩子,说明cur的左子树与右子树已经打印完毕,此时不应该再将cur的左孩子放入stack中,否则,说明左子树还没处理过,那么此时将cur的左孩子压入stack中。
②如果条件①不成立,并且cur的右孩子不为null,head不等于cur的右孩子,则把cur的右孩子压入stack中。含义是如果head等于cur的右孩子,说明cur的右子树已经打印完毕,此时不应该再将cur的右孩子放入stack中,否则,说明右子树还没处理过,此时将cur的右孩子压入stack中。
③如果条件①和条件②都不成立,说明cur的左子树和右子树已经打印完毕了,那么从stack中弹出cur并打印,然后令head=cur。
3.一直重复步骤2,直到stack为空,过程停止。
代码:
import java.util.Stack;
/**
* 实现二叉树先序、中序、后序遍历,递归或非递归方式
*/
public class OrderRecur {
public class Node {
int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
/**
* 递归方式先序遍历
*
* @param head
*/
public void preOrderRecur(Node head) {
if (head == null) {
return;
}
System.out.print(head.value + " ");//首先打印根节点
preOrderRecur(head.left);
preOrderRecur(head.right);
}
/**
* 递归方式中序遍历
*
* @param head
*/
public void inOrderRecur(Node head) {
if (head == null) {
return;
}
inOrderRecur(head.left);
System.out.print(head.value + " ");//左子树遍历完之后打印根节点
inOrderRecur(head.right);
}
/**
* 递归方式后序遍历
*
* @param head
*/
public void posOrderRecur(Node head) {
if (head == null) {
return;
}
posOrderRecur(head.left);
posOrderRecur(head.right);
System.out.print(head.value + " ");//最后打印根节点
}
/**
* 非递归方式实现先序遍历
*/
public static void preOrderUnRecur(Node head) {
System.out.print("pre-order: ");
if (head != null) {
Stack<Node> stack = new Stack<>();
stack.add(head);
while (!stack.isEmpty()) {
head = stack.pop();
System.out.println(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
System.out.println();
}
/**
* 使用非递归方式实现中序遍历
*
* @param head
*/
public static void inOrderUnRecur(Node head) {
System.out.print("in-order: ");
if (head != null) {
Stack<Node> stack = new Stack<>();
while (!stack.isEmpty() || head != null) {
if (head != null) {
stack.push(head);
head = head.left;
} else {
head = stack.pop();
System.out.print(head.value + " ");
head = head.right;
}
}
}
System.out.println();
}
/**
* 使用两个栈实现后序遍历
*
* @param head
*/
public static void posOrderUnRecur1(Node head) {
System.out.println("pos-order: ");
if (head != null) {
Stack<Node> s1 = new Stack<>();
Stack<Node> s2 = new Stack<>();
s1.push(head);
while (!s1.isEmpty()) {
head = s1.pop();
s2.push(head);
if (head.left != null) {
s1.push(head.left);
}
if (head.right != null) {
s1.push(head.right);
}
}
while (!s2.isEmpty()) {
System.out.print(s2.pop().value + " ");
}
}
System.out.println();
}
/**
* 只用一个栈实现后序遍历
*
* @param head
*/
public static void posOrderUnRecur2(Node head) {
System.out.print("pos-order: ");
if (head != null) {
Stack<Node> stack = new Stack<>();
stack.push(head);
Node curr = null;
while (!stack.isEmpty()) {
curr = stack.peek();
if (curr.left != null && head != curr.left && head != curr.right) {
stack.push(curr.left);
} else if (curr.right != null && head != curr.right) {
stack.push(curr.right);
} else {
System.out.print(stack.pop().value + " ");
head = curr;
}
}
}
System.out.println();
}
}
如何较为直观地打印二叉树?
题目:
二叉树可以用常规的三种遍历结果来描述其结构,但是不够直观,尤其是二叉树中有重复值的时候,仅通过三种遍历的结果来构造二叉树的真实结构更是难上加难,有时则根本不可能。给定一棵二叉树的头节点head,已知二叉树节点值的类型为32位整型,请实现一个打印二叉树的函数,可以直观地展示树的形状,也便于画出真实的结构。
思考:
- 设计打印的样式。首先,二叉树大概的样子是把打印结果顺时针旋转90°;接下来,怎么清晰地确定任何一个节点的父结点呢?如果一个节点打印结果的前缀与后缀都有‘H’,说明这个节点是头节点,当然就不存在父结点。如果一个节点打印结果的前缀与后缀都有‘v’,表示父结点在该节点所在列的前一列,在该节点所在行的下方,并且是离该节点最近的节点。如果一个节点打印结果的前缀与后缀都有‘^’,表示父节点在该节点所在列的前一列,在该节点所在行的上方,并且是离该节点最近的节点。
- 规定节点打印时占用的统一长度。否则,在打印一个长短值交替的二叉树时必然会出现格式不对齐的问题,进而产生歧义。在Java中,整型值占用长度最长的值为Integer.MIN_VALUE(即-2147483648),占用的长度为11,加上前缀和后缀之后占用的长度为13.为了在打印时更好地区分,再把前面加上两个空格,后面加上两个空格,共占用长度为17。也就是说,长度为17的空间必然可以放下任何一个32位整数,同时样式还不错。所以,打印每一个节点时,必须让每一个节点在打印时占用长度都为17,如果不足,前后都用空格补齐。
- 具体实现。打印的整体过程结合了二叉树先右子树、再根节点、最后左子树的递归遍历过程。如果递归到一个节点,首先遍历它的右子树。右子树遍历结束后,回到这个节点。如果这个节点所在层为l,那么先打印l×17个空格(不换行),然后开始制作该节点的打印内容,这个内容当然包括节点的值,以及确定的前后缀字符。如果该节点是其父结点的右孩子,前后缀为‘v’,如果是其父结点的左孩子,前后缀为'^',如果是头节点,前后缀为‘H’。最后在前后分别贴上数量几乎一致的空格,占用长度为17的打印内容就制作完成了。打印这个内容后换行,最后进行左子树的遍历过程。
代码:
/**
* 直观地打印二叉树
*/
public class PrintTree {
public class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
public void printTree(Node head) {
System.out.println("Binary Tree:");
printInOrder(head, 0, "H", 17);
System.out.println();
}
private void printInOrder(Node head, int height, String to, int len) {
if (head == null) {
return;
}
printInOrder(head.right, height + 1, "v", len);
String val = to + head.value + to;
int lenM = val.length();
int lenL = (len - lenM) / 2;
int lenR = len - lenM - lenL;
val = getSpace(lenL) + val + getSpace(lenR);
System.out.println(getSpace(height * len) + val);
printInOrder(head.left, height + 1, "^", len);
}
public String getSpace(int num) {
String space = " ";
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf.toString();
}
}
找到二叉树中的最大搜索二叉树
题目:
给定一棵二叉树的头节点head,已知其中所有节点的值都不一样,找到含有节点最多的搜索二叉子树,并返回这棵子树的头节点。
要求:
如果节点数为N,要求时间复杂度为O(N),额外空间复杂度为O(h),h为二叉树的高度。
思考:
以节点node为头的树中,最大的搜索二叉子树只可能来自以下两种情况:
情况一:如果来自node左子树上的最大搜索二叉子树是以node.left为头的;来自node右子树上的最大搜索二叉子树是以node.right为头的;node左子树上的最大搜索二叉子树的最大值小于node.value;node右子树上的最大搜索二叉子树的最小值小于node.value,那么以节点node为头的整棵树都是搜索二叉树。
情况二:如果不满足第一种情况,说明以节点node为头的树整体不能连成搜索二叉树。这种情况下,以node为头的树上的最大搜索二叉子树是来自node的左子树上的最大搜索二叉子树和来自node的右子树上的最大搜索二叉子树之间,节点数较多的那么。
通过以上分析,求解的过程如下:
- 整体过程是二叉树的后序遍历。
- 遍历到当前节点记为cur时,先遍历cur的左子树收集4个信息,分别是左子树上最大搜索二叉子树的头节点(IBST)、节点数(ISize)、最小值(IMin)和最大值(IMax)。再遍历cur的右子树收集4个信息,分别是右子树上最大搜索二叉子树的头节点(rBST)、节点数(rSize)、最小值(rMin)和最大值(rMax)。
- 根据步骤2所收集的信息,判断是否满足第一种情况,如果满足第一种情况,就返回cur节点,如果满足第二种情况,就返回IBST和rBST中较大的一个。
- 可以使用全局变量的方式实现步骤2中收集节点数、最小值和最大值的问题。
代码:
/**
* 找到二叉树中的最大搜索二叉子树
*/
public class BiggestSubBST {
public class Node {
int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
public Node biggestSubBST(Node head) {
int[] record = new int[3];//存放节点数、最小值、最大值
return posOrder(head, record);
}
public Node posOrder(Node head, int[] record) {
if (head == null) {
record[0] = 0;
record[1] = Integer.MAX_VALUE;
record[2] = Integer.MIN_VALUE;
return null;
}
int value = head.value;
Node left = head.left;
Node right = head.right;
Node lBST = posOrder(left, record);
int lSize = record[0];
int lMin = record[1];
int lMax = record[2];
Node rBST = posOrder(right, record);
int rSize = record[0];
int rMin = record[1];
int rMax = record[2];
record[1] = Math.min(lMin, value);
record[2] = Math.max(rMax, value);
//是否满足第一种情况,如果满足,则返回头节点
if (left == lBST && right == rBST && lMax < value && value < rMin) {
record[0] = lSize + rSize + 1;
return head;
}
//否则,比较左子树和右子树的节点数,返回节点数最多的最大搜索二叉子树
record[0] = Math.max(lSize, rSize);
return lSize > rSize ? lBST : rBST;
}
}
判断二叉树是否为平衡二叉树
题目:
平衡二叉树的性质为:要么是一棵空树,要么任何一个节点的左右子树高度差的绝对值不超过1.给定一棵二叉树的头节点head,判断这棵二叉树是否为平衡二叉树。
要求:
如果二叉树的节点数为N,要求时间复杂度为O(N).
思考:
解法的整体过程为二叉树的后序遍历,对任何一个节点node来说,先遍历node的左子树,遍历过程中收集两个信息,node的左子树是否为平衡二叉树,node的左子树最深到哪一层记为lH。如果发现node的左子树不是平衡二叉树,无须进行任何后序遍历,此过程返回什么已不重要,因为已经发现整棵树不是平衡二叉树,退出遍历过程;如果node的左子树是平衡二叉树,再遍历node的右子树,遍历过程中再收集两个信息,node的右子树是否为平衡二叉树,node的右子树最深到哪一层记为rH。如果发现node的右子树不是平衡二叉树,无须进行任何后续过程,返回什么也不重要,因为已经发现整棵树不是平衡二叉树,退出遍历过程;如果node的右子树也是平衡二叉树,就看lH和rH差的绝对值是否大于1,如果大于1,说明已经发现整棵树不是平衡二叉树,如果不大于1,则返回lH和rH较大的一个。
代码:
/**
* 判断二叉树是否为平衡二叉树
*/
public class IsBalance {
public class Node {
int value;
Node left;
Node right;
public Node(int data) {
this.value = data;
}
}
public boolean isBalance(Node head) {
boolean[] res = new boolean[1];
res[0] = true;
getHeight(head, 1, res);
return res[0];
}
/**
* 在递归函数中,一旦发现不符合平衡二叉树的性质,递归过程迅速推出,此时返回什么根本不重要。
* boolean[] res的长度为1,其功能相当于一个全局boolean变量
* 每个节点最多遍历一次
* @param head
* @param level
* @param res
* @return
*/
public static int getHeight(Node head, int level, boolean[] res) {
if (head == null) {
return level;
}
int lH = getHeight(head.left, level + 1, res);
if (!res[0]) {
return level;
}
int rH = getHeight(head.right, level + 1, res);
if (!res[0]) {
return level;
}
if (Math.abs(lH - rH) > 1) {
res[0] = false;
}
return Math.max(lH, rH);
}
}
根据后序数组重建搜索二叉树
题目:
给定一个整型数组arr,已知其中没有重复值,判断arr是否可能是节点值类型为整型的搜索二叉树后序遍历的结果。
进阶:
如果整型数组arr中没有重复值,且已知是一棵搜索二叉树的后序遍历结果,通过数组arr重构二叉树。
思考:
原问题的解法:二叉树的后序遍历为先左、再右、最后根的顺序,所以,如果一个数组是二叉树后序遍历的结果,那么头节点的值一定会是数组的最后一个元素。搜索二叉树的性质,所以比后续数组最后一个元素值小的数组会在数组的左边,比数组最后一个元素大的数组会在数组的右边。比如arr=[2,1,3,6,5,7,4],比4小的部分[2,1,3],比4大的部分为[6,5,7]。如果不满足这种情况,说明这个数组一定不可能是搜索二叉树后序遍历的结果。接下来数组划分成左边数组和右边数组,相等于二叉树分出了左子树和右子树,只要递归地进行如上判断即可。
进阶问题的解法原理与原问题同理,一棵树的后序数组中最后一个值为二叉树头节点的值,数组左部分都比头节点的值小,用来生成头节点的左子树,剩下的部分用来生成右子树。
代码:
/**
* 根据后序遍历重组搜索二叉树
*/
public class IsPost {
/**
* 判断arr是否是节点值类型为整型的搜索二叉树后序遍历的结果
*/
public boolean isPostArray(int[] arr) {
if (arr == null || arr.length == 0) {
return false;
}
return isPost(arr, 0, arr.length - 1);
}
public static boolean isPost(int[] arr, int start, int end) {
if (start == end) {
return true;
}
int less = -1;
int more = end;
for (int i = start; i < end; i++) {
if (arr[end] > arr[i]) {
less = i;
} else {
more = more == end ? i : more;
}
}
if (less == -1 || more == end) {
return isPost(arr, start, end - 1);
}
if (less != more - 1) {
return false;
}
return isPost(arr, start, less) && isPost(arr, more, end - 1);
}
/**
* 一棵树的后序数组中最后一个值为二叉树头节点的值,
* 数组左部分都比头节点的值小,用来生成头节点的左子树,剩下的部分用来生成右子树。
*/
public static class Node {
int value;
Node left;
Node right;
public Node(int data) {
this.value = data;
}
}
public static Node posArrayToBST(int[] posArr) {
if (posArr == null) {
return null;
}
return posToBST(posArr, 0, posArr.length - 1);
}
public static Node posToBST(int[] posArr, int start, int end) {
if (start > end) {
return null;
}
Node head = new Node(posArr[end]);
int less = -1;
int more = end;
for (int i = start; i < end; i++) {
if (posArr[end] > posArr[i]) {
less = i;
} else {
more = end == more ? i : more;
}
}
head.left = posToBST(posArr, start, less);
head.right = posToBST(posArr, more, end - 1);
return head;
}
}
判断一棵二叉树是否为搜索二叉树和完全二叉树
题目:
给定一个二叉树的头节点head,已知其中没有重复值的节点,实现两个函数分别判断这棵二叉树是否是搜索二叉树和完全二叉树。
思考:
判断一棵二叉树是否为搜索二叉树,只要改写一个二叉树的中序遍历,在遍历的过程中看节点值是否都是递增的即可。这里采用时间复杂度为O(N),额外空间复杂度为O(1)的Morris中序遍历算法。Morris遍历分调整二叉树结果和恢复二叉树结构两个节点,所以,当发现节点值降序时,不能直接返回false,这么做可能会跳过恢复阶段,从而破坏二叉树的结构。
判断一棵二叉树是否是完全二叉树,可以依据以下标准:
- 按层遍历二叉树,每层的左边向右依次遍历所有的节点
- 如果当前节点有右孩子,但没有左孩子,直接返回false。
- 如果当前节点并不是左右孩子全有,那么之后的节点必须都为叶结点,否则返回false
- 遍历过程中如果不反悔false,遍历结束后返回true。
代码:
import java.util.LinkedList;
import java.util.Queue;
/**
* 判断一棵二叉树是否为搜索二叉树和完全二叉树
*/
public class IsBSTandIsCBT {
public class Node {
int value;
Node left;
Node right;
public Node(int data) {
this.value = data;
}
}
/**
* 判断一棵二叉树是否为搜索二叉树
*
* @param head
* @return
*/
public boolean isBST(Node head) {
if (head == null) {
return true;
}
boolean res = true;
Node pre = null;
Node cur1 = head;
Node cur2 = null;
while (cur1 != null) {
cur2 = cur1.left;
if (cur2 != null) {
while (cur2.right != null && cur2.right != cur1) {
cur2 = cur2.right;
}
if (cur2.right == null) {
cur2.right = cur1;
cur1 = cur1.left;
continue;
} else {
cur2.right = null;
}
}
if (pre != null && pre.value > cur1.value) {
res = false;
}
pre = cur1;
cur1 = cur1.right;
}
return res;
}
/**
* 判断一棵二叉树是否是完全二叉树
* @param head
* @return
*/
public boolean isCBT(Node head) {
if (head == null) {
return true;
}
Queue<Node> queue = new LinkedList<>();
boolean leaf = false;
Node l = null;
Node r = null;
queue.offer(head);
while (!queue.isEmpty()) {
head = queue.poll();
l = head.left;
r = head.right;
if ((leaf && (l != null || r != null)) || (l == null && r != null)) {
return false;
}
if (l != null) {
queue.offer(l);
}
if (r != null) {
queue.offer(r);
} else {
leaf = true;
}
}
return true;
}
}
通过有序数组生成平衡搜索二叉树
题目:
给定一个有序数组sortArr,已知其中没有重复值,用这个有序数组生成一棵平衡搜索二叉树,并且该搜索二叉树中序遍历的结果与sortArr一致。
思考:
用有序数组中最中间的数生成搜索二叉树的头节点,然后用这个数左边的数生成左子树,用右边的数生成右子树即可。
代码:
/**
* 通过有序数组生成平衡搜索二叉树
*/
public class GenerateTree {
public class Node{
int value;
Node left;
Node right;
public Node(int data){
this.value=data;
}
}
public Node generateTree(int[] sortArr){
if (sortArr==null){
return null;
}
return generate(sortArr,0,sortArr.length-1);
}
public Node generate(int[] sortArr,int start,int end){
if (start>end){
return null;
}
int mid = (start+end)/2;
Node head = new Node(sortArr[mid]);
head.left = generate(sortArr,start,mid-1);
head.right = generate(sortArr,mid+1,end);
return head;
}
}
二叉树节点间的最大距离问题
题目:
从二叉树的节点A出发,可以向上或者向下走,但沿途的节点只能经过一次,当到达节点B时,路径上的节点数叫作A到B的距离。给定一棵二叉树的头结点head,求整棵树上节点间的最大距离。
要求:
如果二叉树的节点数为N,时间复杂度要求为O(N)。
思路:
一个以head为头的树上,最大距离只可能来自以下三种情况:
- head的左子树上的最大距离。
- head的右子树上的最大距离。
- head左子树上离head.left最远的距离+1(head)+head右子树上离head.right最远的距离。
三个值中最大的那个就是整棵树中最远的距离。
根据上面的分析,算法过程如下:
- 整个过程为后序遍历,在二叉树的每个子树上执行步骤2.
- 假设子树头为head,处理head左子树,得到两个信息:左子树上的最大距离记为lMax,左子树上距离head左孩子的最远距离记为maxfromLeft。同理,处理head右孩子得到右子树上的最大距离记为rMax和距离head右孩子的最远距离记为maxfromRight。那么maxfromLeft+1+maxfromRight就是跨head节点情况下的最大距离,再与lMax和rMax比较,把三者中的最值作为head树上的最大距离返回,maxfromLeft+1就是head左子树上离head最远的点到head的距离,maxfromRight+1就是head右子树上离head最远的点到head的距离,选两者中最大的一个作为head树上距离head最远的距离返回。如何返回两个值?一个正常返回,另一个用全局变量表示。
代码:
/**
* 二叉树节点间的最大距离问题
*/
public class MaxDistance {
public class Node {
int value;
Node left;
Node right;
public Node(int data) {
this.value = data;
}
}
public int maxDistance(Node head) {
int[] record = new int[1];
return posOrder(head, record);
}
public int posOrder(Node head, int[] record) {
if (head == null) {
record[0] = 0;
return 0;
}
int lMax = posOrder(head.left, record);
int maxfromLeft = record[0];
int rMax = posOrder(head.right, record);
int maxfromRight = record[0];
int curNodeMax = maxfromLeft + maxfromRight + 1;
record[0] = Math.max(maxfromLeft, maxfromRight) + 1;
return Math.max(Math.max(lMax, rMax), curNodeMax);
}
}
在二叉树中找到一个节点的后继节点
题目:现在有一种新的二叉树节点类型如下:
public class Node{
public int value;
public Node left;
public Node right;
public Node parent;
public Node(int data){
this.value=data;
}
}
该结果比普通二叉树节点结构多了一个指向父结点的parent指针。假设有一棵Node类型的节点组成的二叉树,树中每个节点的parent指针都正确地指向自己的父结点,头节点的parent指向null。只给一个在二叉树中的某个节点node,请实现返回node的后继节点的函数。在二叉树的中序遍历的序列中,node的下一个节点叫做node的后继节点。
思考:
先考虑一种简单的解法,时间复杂度和空间复杂度比较高。既然新类型的二叉树节点有指向父结点的指针,那么一直往上移动,自然可以找到头节点。找到头节点之后,在进行二叉树的中序遍历,生成中序遍历序列,然后在这个序列中找到node节点的下一个节点返回即可。如果二叉树的节点数为N,这种方法要把二叉树的所有节点至少遍历一遍,生成中序遍历的序列还需要大小为N的空间,所以这种方法的时间复杂度和空间复杂度都是O(N)。
最后解法不必遍历所有节点,如果node节点和node后继节点之间的实际距离为L,最优解法只用走过L个节点,时间复杂度为O(L),额外空间复杂度为O(1)。接下来详细说明最优解法是如何找到node的后继节点的:
- 如果node有右子树,那么后继节点就是右子树上最左边的节点。
- 如果node没有右子树,那么先看node是不是node父结点的左孩子,如果是左孩子,那么此时node的父结点就是node的后继节点;如果是右孩子,就向上寻找node的后继节点,假设向上移动到的节点记为s,s的父结点记为p,如果发现s是p的左孩子,那么结点p就是node节点的后继节点,否则就一直向上移动。
- 如果在情况2中一直向上寻找,都移动到空节点时还没有发现node的后继节点,说明node根本不存在后继节点。
情况1和情况2遍历的节点就是node到node后继节点这条路径上的点;情况3遍历的节点数也不会超过二叉树的高度。
代码:
/**
* 在二叉树中找到一个节点的后继节点
*/
public class GetNextNode {
public class Node {
public int value;
public Node left;
public Node right;
public Node parent;
public Node(int data) {
this.value = data;
}
}
public Node getNextNode(Node node) {
if (node == null) {
return node;
}
if (node.right != null) {
return getLeftMost(node.right);
} else {
Node parent = node.parent;
while (parent != null && parent.left != node) {
node = parent;
parent = node.parent;
}
return parent;
}
}
public Node getLeftMost(Node node) {
if (node == null) {
return node;
}
while (node.left != null) {
node = node.left;
}
return node;
}
}
二叉树的序列化和反序列化
题目:
二叉树被记录成文件的过程叫做二叉树的序列化,通过文件内容重建原来的二叉树的过程叫做二叉树的反序列化。给定一棵二叉树的头节点head,并已知二叉树节点值类型为32位整型,请设计一种二叉树的序列化和反序列化的方案,并用代码实现。
思考:
方法一:通过先序遍历实现序列化和反序列化。
- 先序遍历实现序列化
- 首先假设序列化的结果字符串为str,初始时str=""。
- 先序遍历二叉树,如果遇到null节点,就在str的末尾加上“#_”,“#”表示这个节点为空,节点值不存在,“_”表示一个值的结束;如果遇到不为空的节点,假设节点值为3,就在str的末尾加上“3_”。
- 通过先序遍历序列化的结果反序列化,重构二叉树。
- 把结果字符串str编程字符串类型的数组,其中“_”作为字符串的分割符,记为values,数组代表一棵二叉树先序遍历的节点顺序。
- 用values按照先序遍历的顺序建立整棵树。
方法二:通过层遍历实现序列化和反序列化
- 层遍历下的序列化过程
- 首先假设序列化的结果字符串为str,初始时str=“”.
- 然后实现二叉树的按层遍历,具体方式是利用队列结果,这也是图的宽度优先搜索的常见方式。
- 层遍历的反序列化过程就是遇到“#”就生成null节点,同时不把null节点放在队列里面即可。
代码:
import java.util.LinkedList;
import java.util.Queue;
/**
* 二叉树的序列化和反序列化
*/
public class SerialAndReserial {
public class Node {
int value;
Node left;
Node right;
public Node(int data) {
this.value = data;
}
}
/**
* 使用先序遍历进行序列化
*
* @param head
* @return
*/
public String serialByPre(Node head) {
if (head == null) {
return "#_";
}
String res = head.value + "_";
res += serialByPre(head.left);
res += serialByPre(head.right);
return res;
}
/**
* 基于先序遍历结果的反序列化
*
* @param preStr
* @return
*/
public Node reconByPreString(String preStr) {
String[] values = preStr.split("_");
Queue<String> queue = new LinkedList<>();
for (int i = 0; i != values.length; i++) {
queue.offer(values[i]);
}
return reconPreOrder(queue);
}
/**
* 重构二叉树
*
* @param queue
* @return
*/
private Node reconPreOrder(Queue<String> queue) {
String value = queue.poll();
if (value.equals("#")) {
return null;
}
Node head = new Node(Integer.valueOf(value));
head.left = reconPreOrder(queue);
head.right = reconPreOrder(queue);
return head;
}
/**
* 使用层次遍历进行序列化
*/
public String serialByLevel(Node head) {
if (head == null) {
return "#_";
}
String res = head.value + "_";
Queue<Node> queue = new LinkedList<>();
queue.offer(head);
while (!queue.isEmpty()) {
head = queue.poll();
if (head.left != null) {
res += head.left.value + "_";
queue.offer(head.left);
} else {
res += "#_";
}
if (head.right != null) {
res += head.right.value + "_";
queue.offer(head.right);
} else {
res += "#_";
}
}
return res;
}
/**
* 基于层次遍历的结果反序列化
*/
public Node reconByLevelString(String levelStr) {
String[] values = levelStr.split("_");
int index = 0;
Node head = generateNodeByString(values[index++]);
Queue<Node> queue = new LinkedList<>();
if (head != null) {
queue.offer(head);
}
Node node = null;
while (!queue.isEmpty()) {
node = queue.poll();
node.left = generateNodeByString(values[index++]);
node.right = generateNodeByString(values[index++]);
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return head;
}
private Node generateNodeByString(String val) {
if (val.equals("#")) {
return null;
}
return new Node(Integer.valueOf(val));
}
}
已知一棵完全二叉树,求其节点的个数
题目:
给定一棵完全二叉树的头节点head,返回这棵树的节点个数。
要求:
时间复杂度低于O(N),N为这棵树的节点个数
思考:
如果完全二叉树的层数为H,具体过程为:
- 如果head=null,说明为空树,直接返回0;
- 如果不是空树,就求树的高度,求法是找到树的最左节点看能到哪一层,层数记为H。
- 主要逻辑是一个递归过程记为bs(node,L,H),node表示当前节点,L表示node所在的层数,H表示整棵树的层数是始终不变的。bs(node,L,H)的返回值表示以node为头的完全二叉树的节点数是多少。初始时node为头节点head,L为1,因为head在第一层,一共有H层始终不变。
- 找到node右子树的最左节点,如果它能够达到最后一层,说明node的整棵左子树都是满二叉树,并且层数为H-1,一棵层数为H-1的满二叉树,其节点个数为个。如果加上node节点自己,那么节点数为个。此时如果再知道node右子树的节点数,就可以求出以node为头的完全二叉树上有多少个节点了。那么node右子树的节点数到底是多少呢?就是bs(node.right,L+1,H)的结果,递归去求即可。最后整体返回+bs(node.right,L+1,H)。
- 找到node右子树的最左节点,如果它没有达到最后一层,说明node的整棵右子树都是满二叉树,并且层数为H-L-1层。一棵层数为H-L-1层的满二叉树,其节点数为个。如果加上node节点子集,那么节点数为个。此时如果再知道node左子树的节点数,就可以求出以node为头的完全二叉树有多少个节点了。那么node左子树的节点数到底是所少呢?就是bs(node.left,L+1,H)的结果,递归去求即可。最后整体返回+bs(node.left,L+1,H)。
代码:
/**
* 统计完全二叉树的节点数
*/
public class NodeNum {
public class Node {
int value;
Node left;
Node right;
public Node(int data) {
this.value = data;
}
}
public int nodeNum(Node head) {
if (head == null) {
return 0;
}
return bs(head, 1, mostLeftLevel(head, 1));
}
public int bs(Node node, int L, int H) {
if (L == H) {
return 1;
}
if (mostLeftLevel(node.right, L + 1) == H) {
return (1 << (H - L) + bs(node.right, L + 1, H));
} else {
return (1 << (H - L - 1)) + bs(node.left, L + 1, H);
}
}
public int mostLeftLevel(Node node, int level) {
while (node != null) {
level++;
node = node.left;
}
return level - 1;
}
}
折纸问题
题目:
请把一段纸条竖着放在桌子上,然后从纸条的下边向上方对折1次,压出折痕后展开。此时折痕是凹下去的,即折痕突起的方向指向纸条的背面。如果从纸条的下边向上方连续对折2次,压出折痕后展开,此时有三条折痕,从上到下依次是下折痕、下折痕和上折痕。给定一个输入参数N,代表纸条都从下边向上方连续对折N次,请从上到下打印所有折痕的方向。
例如:
N=1时,打印:down
N=2时,打印:down down up
思考:
对折第一次的折痕: 下
对折第二次的折痕: 上 下
对折第三次的折痕: 上 下 上 下
对折第四次的折痕: 上 下 上 下 上 下 上 下
从图中关系可以总结出:
- 产生第i+1次折痕的过程,就是在对折i次产生的每一条折痕的左右两侧,依次插入上折痕和下折痕的过程。
- 所有的折痕的结构是一棵满二叉树,在这棵满二叉树中,头节点为下折痕,每一棵左子树的头节点为上折痕,每一棵右子树的头节点为下折痕。
- 从上到下打印所有折痕的方向,就是二叉树的先右、再中,最后左的中序遍历。
代码:
/**
* 折纸问题
*/
public class PrintAllFolds {
public static void printAllFolds(int N) {
printProcess(1, N, true);
}
/**
* 连续对折n次之后一定产生2^(n-1)条折痕
* 时间复杂度为O(n^2),额外空间复杂度为O(1)
*
* @param i
* @param N
* @param down
*/
private static void printProcess(int i, int N, boolean down) {
if (i > N) {
return;
}
printProcess(i + 1, N, true);
System.out.println(down ? "down" : "up");
printProcess(i + 1, N, false);
}
}