题目一 实现二叉树的先序、 中序、 后序遍历, 包括递归方式和非递归方式
1 递归思路
假设有一棵二叉树如左所示,将二叉树进行遍历,忽略打印行为,递归函数依次访问节点的顺序如右所示。打印时机放在第一次来到这个节点的时候就是先序遍历,第二次就是中序遍历,第三次就是后序遍历。(代码都很像)
public static void preOrder(Node head){
if(head == null){
return;
}
System.out.print(head.value + " ");
preOrder(head.left);
preOrder(head.right);
}
public static void inOrder(Node head){
if(head == null){
return;
}
inOrder(head.left);
System.out.print(head.value + " ");
inOrder(head.right);
}
public static void posOrder(Node head){
if(head == null){
posOrder(head.left);
posOrder(head.right);
System.out.print(head.value + " ");
}
}
2 非递归思路(压栈)
先序遍历: 把头结点压进去,如果栈不为空的话把栈顶弹出来,然后如果这个节点右孩子不为空,压右孩子,左孩子不为空压左孩子,只要有往里压,压头弹栈,先右后左。
public static void preOrderUnRecur(Node head){
if(head != null){
Stack<Node> stack = new Stack<Node>();
stack.push(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();
}
中序遍历: 如果栈不为空或者头节点不为空,只要是当前节点就把左边界都压到栈里,来到空时,从栈中弹出一个节点,head向右移动。当前节点为空,从栈拿一个打印 ,当前节点往右跑;当前节点不为空,当前节点压入栈,当前节点往左。栈或头节点不空,压左,为空打印压右。
public static void inOrderUnRecur(Node head){
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 posOrderUnRecur(Node head){
if(head != null){
Stack<Node> stack1 = new Stack<Node>();
Stack<Node> stack2 = new Stack<Node>();
stack1.push(head);
while (!stack1.isEmpty()){
head = stack1.pop();
stack2.push(head);
if(head.left != null){
stack1.push(head.left);
}
if(head.right != null){
stack1.push(head.right);
}
}
while(!stack2.isEmpty()){
System.out.print(stack2.pop().value + " ");
}
}
System.out.println();
}
后续遍历(一个栈): 书里,先不学了吧,整不明白
题目二 如何直观的打印一棵二叉树
检验自己的二叉树有没有调对
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();
}
题目三 在二叉树中找到一个节点的后继节点
现在有一种新的二叉树节点类型如下:
public static 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的后继节点。
思路
避免遍历整棵树,只需要该节点到后继节点的距离就可以找到。
两种情况:第一种,该结点有右子树时,下一个打印的一定是右子树的最左节点。 第二种,该结点没有右子树,找哪个左子树的最后一个节点被打印。往上找,直到当前节点是它父亲节点的左孩子,则父节点是后继。
public static Node getNode(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 != null){
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;
}
题目四 介绍二叉树的序列化和反序列化
序列化是计算机内存方便记录二叉树,反序列化就是计算机重建二叉树。
//先序
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.add(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);
head.right = reconPreOrder(queue);
return head;
}
题目五 折纸问题
请把一段纸条竖着放在桌子上, 然后从纸条的下边向上方对折1次, 压出折痕后展开。 此时折痕是凹下去的, 即折痕突起的方向指向纸条的背面。 如果从纸条的下边向上方连续对折2 次, 压出折痕后展开, 此时有三条折痕, 从上到下依次是下折痕、 下折痕和上折痕。给定一 个输入参数N, 代表纸条都从下边向上方连续对折N次,请从上到下打印所有折痕的方向。 例如: N=1时, 打印: down;N=2时, 打印: down down up
N=1 down
N=2 down down up
N=3 down down up down down up up
相当于每对折一次,就在每个子节点加两个左右节点,分别是down和up。树形结构,中序遍历即可。
public static void printAllFolds(int N){
printProcess(1, N, true);
}
public 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);
}
题目六 判断一棵二叉树是否是平衡二叉树(套路)
平衡二叉树:在这棵树中任何一个节点,它左子树和右子树的高度差不超过1。不一定是满二叉树,满二叉树一定是平衡二叉树。
高度套路化的处理:递归函数很好用!
判断每个节点的信息,列出可能性:1)它的左子树是不是平衡的 2)它的右子树是不是平衡的 3)左树平衡右树平衡的条件下,左树右树的高度都需要知道,则高度差知道
则递归函数返回值应该包含两个信息:1)这棵树是否是平衡的 2)这棵树的高度是什么
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 isB(Node head){
return process(head).isB;
}
public static ReturnData process(Node head){
if(head == null){
return new ReturnData(true, 0);
}
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);
}
return new ReturnData(true, Math.max(leftData.h, rightData.h) + 1);
}
题目七 判断一棵树是否是搜索二叉树(BST)、 判断一棵树是否是完全二叉树 (CBT)
搜索二叉树:对于这棵树上任何一个节点为头的子树,左子树都比他小,右子树都比他大。二叉树中序遍历的节点都是依次升序的。 通常来讲搜索二叉树是不出现重复节点的。 中序遍历改写:
List<Integer> list = new ArrayList<>();
public boolean isValidBST(TreeNode root) {
if(root == null){
return true;
}
inOrder(root);
for(int i = 1; i < list.size(); i++){
if(list.get(i) <= list.get(i - 1)){
return false;
}
}
return true;
}
public void inOrder(TreeNode head){
if(head != null){
inOrder(head.left);
list.add(head.val);
inOrder(head.right);
}
}
完全二叉树:判断逻辑是二叉树按层遍历:1)如果一个节点有右无左孩子,不是完全二叉树 2)如果一个节点,不是左右两个孩子都全 (有左没右或左右都没有),后面遍历到的所有的节点都必须是叶节点 3)不违反1也不违反2,是完全二叉树。设置了一个leaf变量,表示是否开启后续节点都是叶结点的阶段。
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;
}
题目八 已知一棵完全二叉树, 求其节点的个数
要求时间复杂度低于O(N), N为这棵树的节点个数。
一般情况下使用数组实现堆,二叉树实现堆的好处:1)减少空间浪费 2)没有扩容代价
时间复杂度低于O(N)证明不可以用遍历的方式,我们已知满二叉树节点个数的计算方法:若二叉树共l层,则个数为2l - 1。
思路
首先遍历左边界求树的总高度h总,接下来我们求右子树的左边界,共有两种情况:
1、右子树的左边界到最后一层:则左子树是满二叉树,将左子树的总节点数计算出来之后加上右子树的节点,右子树的节点使用递归计算。
2、右子树的左边界没有到最后一层:则右子树是满二叉树,计算右子树的总节点数,加上左子树的节点数,左子树的节点使用递归计算。
另外,主函数的变量代表的含义:node为当前节点,l为当前节点的层数,h为整个树的总深度。 bs返回的是以node为头节点的整个树的节点个数。
时间复杂度计算: 共有n个节点,一共有logN层,每层只遍历一个节点,则遍历节点的时间复杂度为O(logN),每个节点都要遍历子树的边界,时间复杂度为O(logN),则总体时间复杂度为O((logN)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;
}
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 static int mostLeftLevel(Node node, int level) {
while (node != null) {
level++;
node = node.left;
}
return level - 1;
}