题目1:二叉树的遍历
先序、中序、后序
先序:先打印当前节点,再打印整颗左子树,再打印整颗右子树
中序:先左子树,当前节点,右子树
后序:先做子树,右子树,当前节点
下面是通过递归的方式遍历二叉树,代码层面的不同在于打印时机不同
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
public static void preOrderRecur(Node head) { //先序
if (head == null) {
return;
}
System.out.print(head.value + " "); //唯一的不同就是打印过程的位置
preOrderRecur(head.left);
preOrderRecur(head.right);
}
public static void inOrderRecur(Node head) { //中序
if (head == null) {
return;
}
inOrderRecur(head.left);
System.out.print(head.value + " ");
inOrderRecur(head.right);
}
public static 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<Node>();
stack.add(head);
while (!stack.isEmpty()) {
head = stack.pop();
System.out.print(head.value + " ");
if (head.right != null) {
stack.push(head.right);
}
if (head.left != null) {
stack.push(head.left);
}
}
}
System.out.println();
}
非递归版中序遍历:
思路:
当前节点为空,从栈内取出一个打印,当前节点向右移动,即到弹出节点的右子树
当前节点不为空,当前节点压入栈,当前节点向左下方移动,即移到当前节点左子树
public static void inOrderUnRecur(Node head) {
System.out.print("in-order: ");
if (head != null) {
Stack<Node> stack = new Stack<Node>();
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();
}
非递归版后序遍历:
思路:
和先序遍历的唯一区别是将中左右的压栈出栈打印的压栈顺序换成了中右左,打印过程变成了压入另外的一个栈的过程;然后新的栈弹栈就会变成左右中的后序遍历顺序了
public static void posOrderUnRecur1(Node head) {
System.out.print("pos-order: ");
if (head != null) {
Stack<Node> s1 = new Stack<Node>();
Stack<Node> s2 = new Stack<Node>();
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();
}
题目2:如何直观的打印一颗二叉树
下面代码可以用来帮助分析二叉树的直观结构:
public static void printTree(Node head) {
System.out.println("Binary Tree:");
printInOrder(head, 0, "H", 17);
System.out.println();
}
public static 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 static String getSpace(int num) {
String space = " ";
StringBuffer buf = new StringBuffer("");
for (int i = 0; i < num; i++) {
buf.append(space);
}
return buf.toString();
}
生成的直观二叉树的样式如下所示,从左向右看,其中的上下箭头是表征其父节点的相对位置。

题目3:在二叉树中找到一个节点的后继节点
【题目】 现在有一种新的二叉树节点类型如下:
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的后继节点。 同理,前驱节点指的是中序遍历中的前一个
思路:主要是避免遍历整棵树,而是去求该节点距离后继节点的距离~
思考:1. 若一个节点有右子树,其后继节点一定是其右子树上的最左边的节点;2.若一个节点没有右子树, 通过x的父指针找到x的父节点。如果当前节点是其父节点的左孩子,则该父节点是后继节点,否则当前节点往上移,再找父节点,判断,循环。最终得到的父节点就是原始节点的后继节点。
同理,前驱节点,找左子树的最右节点,如果没有左子树,就往上找该节点是父节点的右孩子的父节点~~
代码如下:
//主函数
public static Node getSuccessorNode(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 static Node getLeftMost(Node node) {
if (node == null) {
return node;
}
while (node.left != null) {
node = node.left;
}
return node;
}
题目4:介绍二叉树的序列化和反序列化
序列化:记录化; 反序列化:重新生成
一、按一种顺序(如先序)生成字符串,但是注意遍历遇到空时,需要用特殊字符记录。
先序情况:当前节点+左树字符串+右树字符串
序列化:
public static String serialByPre(Node head) {
if (head == null) { //字符遇到空
return "#!";
}
String res = head.value + "!"; //当前节点+左树字符串+右树字符串
res += serialByPre(head.left);
res += serialByPre(head.right);
return res;
}
反序列化: 按序列化标准写
public static Node reconByPreString(String preStr) {
String[] values = preStr.split("!"); //按序列化中的间隔符!来分割成字符串数组
Queue<String> queue = new LinkedList<String>(); //把数组加入队列中去,方便依次弹出
for (int i = 0; i != values.length; i++) {
queue.offer(values[i]);
}
return reconPreOrder(queue);
}
//按字符串数组来反序列化
public static 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); //先把左子树确定,直到遇到null
head.right = reconPreOrder(queue); //然后确定右子树
return head;
}
二、按层序列化 同样不要忽略空
public static String serialByLevel(Node head) {
if (head == null) {
return "#!";
}
String res = head.value + "!";
Queue<Node> queue = new LinkedList<Node>();
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 static Node reconByLevelString(String levelStr) {
String[] values = levelStr.split("!");
int index = 0;
Node head = generateNodeByString(values[index++]);
Queue<Node> queue = new LinkedList<Node>();
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;
}
public static Node generateNodeByString(String val) {
if (val.equals("#")) {
return null;
}
return new Node(Integer.valueOf(val));
}
题目6:判断一棵二叉树是否是平衡二叉树
平衡二叉树定义:
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树
注意二叉树和递归函数的重要联系;递归意味着会到一个节点三次
思路:考察以每个节点为头的子树是不是平衡二叉树;而判断一个树是不是平衡二叉树需要下列信息:
- 左树平衡
- 右树平衡
- 左树的高度和右树的高度差是否超过1
- 接着设计递归返回结构,包含两个信息:是否平衡、高度;然后向上继续返回
树形DP
这里可以总结出的套路是,先思考所有可能性,然后确定返回结构,然后得到左右子树信息后再将自身信息上传,递归~
代码如下:
public static class returnData{ //返回的结构,这里直接把返回的内容新设置了一个类
public boolean isB;
public int h;
public returnData(boolean isB,int h){
this.isB = isB;
this.h = h;
}
}
public static boolean isBalance(Node head) { //判断主函数
return process(head).isB;
}
//处理过程 先左侧,后右侧,再判断高度差,最后返回
public static returnData process(Node head){
if (head == null){
return new returnData(true,0); //从null开始返回
}
returnData leftData = process(head.left);
if (!leftData.isB){
return new returnData(false,0);
}
returnData rightData = process(head.right);
if (!rightData.isB){
return new returnData(false,0);
}
if (Math.abs(leftData.h - rightData.h) > 1){
return new returnData(false,0);
}
//左树和右树的高度的较大值+1返回成本树的高度
else {
return new returnData(true,Math.max(leftData.h,rightData.h)+1);
}
}
下面这种做法是把直接返回信息,而不是返回一个类,都可。注意下面的代码包含单独求高度的函数
public static boolean isBalance(Node head){
boolean[] res = new boolean[1];
res[0] = true;
getHight(head,1,res);
return res[0];
}
public static int getHight(Node head,int level,boolean[] res){
if(head == null){
return level;
}
int lH = getHight(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);
}
题目7:判断一棵树是否是搜索二叉树、判断一棵树是否是完全二叉树
搜索二叉树:对于任何节点,左子树都比他小,右子树都比他大。
思路:二叉树的中序遍历是从小到大的就是搜索二叉树 ; 因为中序即是左→中→右
一般来说,搜索二叉树不会有重复节点,它的功能是为了提供搜索。
下面代码用到了morris遍历,也可以在前面中序遍历的非递归版本中加入判断条件
public static 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;
}
完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边,这就是完全二叉树。
思路: 节点按层遍历
- 先判断:一个节点有右孩子,而没有左孩子,一定不是完全二叉树;
- 再判断:只要不是左右孩子都有,那必然是有左没右,或者左右都没有; 出现这种情况,之后遍历的所有节点都是叶节点,那么就是完全二叉树; 否则一定不是完全二叉树;
代码如下:
public static boolean isCBT(Node head) {
if (head == null) {
return true;
}
Queue<Node> queue = new LinkedList<Node>(); //双向链表,用来实现队列结构
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;
题目8:已知一棵完全二叉树,求其节点的个数
要求:时间复杂度低于O(N),N为这棵树的节点个数
思路:
要求时间复杂度低于O(N),则不能用遍历的方式,考虑完全二叉树的特点,并联系满二叉树;PS:满二叉树的高度为h,则它的节点个数 = 2^h-1
- 考虑一个节点,先遍历左边界,得到完全二叉树的高度h,这个时间复杂度近似O(logN)
- 判断该节点的右子树的左边界是否到最后一层,
分情况:
情况1:若该节点的右子树的左边界到最后一层,则说明该节点的左子树是满二叉树;计算左子树的节点个数,再加上该节点,继续考虑它的右儿子,递归去求
情况2:若该节点的右子树的左边界没有到最后一层(只到了倒数第二层),则证明该节点的右子树是一个满二叉树;加上本来的该节点,继续考虑它的左儿子,递归去求
代码如下:
public static int nodeNum(Node head){
if(head == null){
return 0;
}
//最开始计算出最大的高度
return bs(head,1,mostLeftLevel(head,1));
}
public static int bs(Node node,int l,int h){
if(l == h){
return 1;
}
//判断右子树的左最大高度是否达到了h,也就是左边界是否到最后一层。
if(mostLeftLevel(node.right,l+1) == h){
return (1<<(h-l))+bs(node.right,l+1,h); //(1<<(h-1)) == 2^(h-1)
}else{
return(1<<(h-l-1))+bs(node.left,l+1,h);
}
}
public static int mostLeftLevel(Node node,int level){ //计算左边界高度的函数
while(node != null){
level++;
node = node.left;
}
return level-1;
}