二叉树的节点结构
今天我们来看一下二叉树。二叉树的节点结构是:
public static class Node{
public int value;
public Node left;
public Node right;
public Node(int data){
this.value = data;
}
}
前中后序遍历(递归实现)
用递归的方式实现前中后序遍历,这里讲解一下前序遍历,中序后序可以类推: 在调用前序遍历的时候,如果现在我们不打印第一层次,而是每一层次都打印的话,应该是这样的
全层次打印的话:1 2 4 4 4 2 5 5 5 2 1 3 6 6 6 3 7 7 7 3 1
注意到每一个会打印三次,而前序遍历只需要打印出来第一次出现的数就可以也就是:1 2 4 5 3 6 7
注意:打印的是节点的值而不是节点本身
相关的代码是:
public static void main(String[] args) {
Node head = new Node(5);
head.left = new Node(3);
head.right = new Node(8);
head.left.left = new Node(2);
head.left.right = new Node(4);
head.left.left.left = new Node(1);
head.right.left = new Node(7);
head.right.left.left = new Node(6);
head.right.right = new Node(10);
head.right.right.left = new Node(9);
head.right.right.right = new Node(11);
System.out.println("==============用递归做==============");
System.out.print("pre-order:");
preOrderRecur(head);
System.out.println();
System.out.print("in-order:");
inOrderRecur(head);
System.out.println();
System.out.print("pos-order:");
posOrderRecur(head);
}
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 + " ");
}
那么不用递归怎么实现呢?
这里可以用栈,可以记住,比较难想。
前序遍历(深度优先遍历)(dfs)
- 前提条件:先将根结点压入栈中
-
- 从栈中弹出一个节点cur
-
- 打印cur
-
- 这个节点cur如果有对应的,先压进栈它的右节点后压入它的左节点
-
- 回到1.直到栈为空
- 回到1.直到栈为空
相关的代码是:
public static void preOrderUnRecur(Node head){
if (head != null){
Stack<Node> stack = new Stack<>();
//先将头结点压入栈,作为前提条件
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);
}
}
}
}
后序遍历
我们发现前序遍历是先压右节点再压左节点,最后得到的是前序遍历也就是前左右,那么如果我们先压左节点后压右节点,就会得到前右左,我们会发现将前右左反转之后就是左右前,也即后序遍历,所以我们准备一个收集栈来收集原来栈弹出的节点。
- 前提条件:先将根结点压入原始栈中
-
- 从原始栈中弹出一个节点cur,并放到收集栈中
-
- 这个节点cur如果有对应的,先压进原始栈它的左节点后压入它的右节点
-
- 回到步骤1.直到原始栈为空
-
- 单独打印收集栈中的东西 相关的代码是:
public static void posOrderUnRecur(Node head){
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 + " ");
}
}
}
中序遍历
-
- 每棵子树的整棵树左边界进栈
-
- 从栈中弹出一个节点cur,并打印
-
- 这个节点cur如果有对应的右树,将它的右树返回步骤1.
相关的代码是:
public static void inOrderUnRecur(Node head){
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;
}
}
}
}
代码集锦,包括用递归和不用递归的:
public static void main(String[] args) {
Node head = new Node(5);
head.left = new Node(3);
head.right = new Node(8);
head.left.left = new Node(2);
head.left.right = new Node(4);
head.left.left.left = new Node(1);
head.right.left = new Node(7);
head.right.left.left = new Node(6);
head.right.right = new Node(10);
head.right.right.left = new Node(9);
head.right.right.right = new Node(11);
System.out.println("==============用递归做==============");
System.out.print("pre-order:");
preOrderRecur(head);
System.out.println();
System.out.print("in-order:");
inOrderRecur(head);
System.out.println();
System.out.print("pos-order:");
posOrderRecur(head);
System.out.println();
System.out.println("==============不用递归做==============");
System.out.print("pre-order:");
preOrderUnRecur(head);
System.out.println();
System.out.print("in-order:");
inOrderUnRecur(head);
System.out.println();
System.out.print("pos-order:");
posOrderUnRecur(head);
}
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){
if (head != null){
Stack<Node> stack = new Stack<>();
//先将头结点压入,作为前提条件
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);
}
}
}
}
public static void posOrderUnRecur(Node head){
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 + " ");
}
}
}
public static void inOrderUnRecur(Node head){
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;
}
}
}
}
宽度优先遍历(BFS)
- 前提条件:先将根结点压入队列中
-
- 从队列中弹出一个节点cur, 并打印
-
- 这个节点cur如果有对应的,先压进队列它的左节点后压入它的右节点
-
- 返回步骤1.直到队列为空
相关的代码是:
public static void widthOrder(Node head){
if (head == null){
return;
}
//注意:java中的双向链表可以实现队列
Queue<Node> queue = new LinkedList<>();
//先将头结点压入,作为前提条件
queue.add(head);
while (!queue.isEmpty()){
//弹出队列的头
head = queue.poll();
System.out.print(head.value + " ");
if (head.left != null){
//加入左节点
queue.add(head.left);
}
if (head.right != null){
//加入右节点
queue.add(head.right);
}
}
}
二叉搜索树
二叉搜索树的定义:Binary Search Tree
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。 二叉搜素树的中序遍历是有顺序的
//是否是搜索二叉树可以依据中序遍历改,如果中序遍历是升序就是搜索二叉树,这里给出三种方法
//方法1:从递归上面改
public static int preValue = Integer.MIN_VALUE;
public static boolean isBST(Node head){
if (head == null){
return true;
}
boolean isLeftBst = isBST(head.left);
if (!isLeftBst ){
return false;
}
if (head.value <= preValue){
return false;
}else {
preValue = head.value;
}
return isBST(head.right);
}
//方法2:用非递归改
public static boolean isBST2(Node head){
if (head != null){
int preValue = Integer.MIN_VALUE;
Stack<Node> stack = new Stack<>();
while (!stack.isEmpty() || head != null){
if (head != null){
stack.push(head);
head = head.left;
}else {
head = stack.pop();
if (head.value <= preValue){
return false;
}else {
preValue = head.value;
}
head = head.right;
}
}
}
return true;
}
//方法3:判断最后输出的中序遍历是不是升序的
public static boolean isBST3(Node head){
if (head == null){
return true;
}
LinkedList<Node> inOrdeList = new LinkedList<>();
process(head, inOrdeList);
int pre = Integer.MIN_VALUE;
for (Node cur : inOrdeList){
if (pre >= cur.value){
return false;
}
pre = cur.value;
}
return true;
}
public static void process(Node node, LinkedList<Node> inOrdeList) {
if (node == null){
return;
}
process(node.left, inOrdeList);
inOrdeList.add(node);
process(node.right, inOrdeList);
}
完全二叉树
完全二叉树的定义:Complete Binary Tree
- 除了最底层节点可能没填满外,其余每层节点数都达到最大值
- 最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。 可以简记为:
- 任一节点不会存在有右节点没有左节点
- 如果遇到了第一个左右节点不全的节点,那么后续的节点都是叶节点
//是否是完全二叉树
public static boolean isCBT(Node head) {
if (head == null) {
return true;
}
LinkedList<Node> queue = new LinkedList<>();
//用于界定是否第一次遇到了有左节点没有右节点的节点
boolean leaf = false;
Node l = null;
Node r = null;
queue.add(head);
while (!queue.isEmpty()) {
head = queue.pop();
l = head.left;
r = head.right;
//如果该节点是左节点为空,右节点不为空返回false,如果已经遇到了有左节点没有右节点的节点,即leaf为true
//那么之后的节点必须都为叶节点,也即之后的节点没有左节点和右节点
if ((l == null && r != null) || (leaf && (l != null || r != null))) {
return false;
}
if (l != null) {
queue.add(l);
}
if (r != null) {
queue.add(r);
} else {
leaf = true;
}
}
return true;
}
平衡二叉树
平衡二叉树的定义:
- 任意节点的子树的高度差都小于等于1
- 常见的符合平衡树的有,B树(多路平衡搜索树)、AVL树(二叉平衡搜索树)等。
接下来讲一个适合二叉树的套路:
- 整理题目的条件信息
- 左树和右树需要返回的信息,左右树返回的信息要一样
- 可以用递归做 那么这个题就可以整理为:
- 整个题目的条件信息为:左子树是平衡二叉树、右子树是平衡二叉树、|左高 - 右高| <= 1
- 左树返回左树是否是平衡二叉树、高度是多少
- 右树返回右树是否是平衡二叉树、高度是多少
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
//判断是不是平衡树的方法
public static boolean isBalanced(Node head) {
return process(head).isBalanced;
}
//自定义的返回结构,是否是平衡二叉树以及相应的高度
public static class ReturnType {
public boolean isBalanced;
public int height;
public ReturnType(boolean isB, int hei) {
isBalanced = isB;
height = hei;
}
}
//递归操作
public static ReturnType process(Node x) {
if (x == null) {
return new ReturnType(true, 0);
}
//返回对应的高度、是否是平衡二叉树
ReturnType leftData = process(x.left);
ReturnType rightData = process(x.right);
//以整体来看,返回左右树是否是平衡二叉树以及对应的高度后,判断整体的高度也就可以知道
//,整体是不是平衡二叉树也就知道了
//加1是因为包括自己的节点
int height = Math.max(leftData.height, rightData.height) + 1;
boolean isBalanced = leftData.isBalanced && rightData.isBalanced && Math.abs(leftData.height - rightData.height) < 2;
return new ReturnType(isBalanced, height);
}
这个套路也可以解答是否是搜索二叉树:
-
整个题目的条件信息为:左树是搜索二叉树、右树是搜索二叉树、左树的max < x < 右树的min
-
左树返回是否是搜索二叉树以及max值
-
右树返回是否是搜索二叉树以及min值 但是左树和右树返回的信息就不一样了,所以改成
-
左树返回是否是搜索二叉树以及max值、min值
-
右树返回是否是搜索二叉树以及max值、min值
public static boolean isValidBST(Node root) {
if(root == null){
return true;
}
return(process(root).isBST);
}
public static class ReturnType{
public boolean isBST;
public int min;
public int max;
public ReturnType(boolean is, int mi, int ma){
isBST = is;
min = mi;
max = ma;
}
}
public static ReturnType process(Node x){
//因为是空的话,min和max没法填写,所以写成了null,
//但是写成null之后,底下就要多做判断,以防空指针
if (x == null){
return null;
}
ReturnType left = process(x.left);
ReturnType right = process(x.right);
//先给min 和 max设置初始值,作为比较
int min = x.value;
int max = x.value;
//每一项都要排除null
if (left != null){
min = Math.min(min, left.min);
max = Math.max(max, left.max);
}
//每一项都要排除null,以这棵树为整体求出整体min和max
if (right != null){
min = Math.min(min, right.min);
max = Math.max(max, right.max);
}
//每一项都要排除null
boolean isBST = true;
//每一项都要排除null,如果左侧不是搜索二叉树以及左侧的max大于了当前值,都要返回false
if (left != null && (!left.isBST || left.max >= x.value)){
isBST = false;
}
//每一项都要排除null,如果右侧不是搜索二叉树以及右侧的min小于了当前值,都要返回false
if (right != null && (!right.isBST || right.min <= x.value)){
isBST = false;
}
return new ReturnType(isBST, min, max);
}
满二叉树
这个套路解答是否是满二叉树:Full Binary Tree
- 整个题目的条件信息为:整体的个数 = 2 ^ (整体的高度) - 1;
- 左树返回左树的高度和节点数
- 右树返回右树的高度和节点数
public static boolean isValidFBT(Node root) {
if(root == null){
return true;
}
ReturnType data = process(head);
return data.nodes == (1 << data.height - 1);
}
public static class ReturnType{
public int height;
public int nodes;
public ReturnType(int height, int nodes){
this.height = height;
this.nodes = nodes;
}
}
public static ReturnType process(Node x){
if (x == null){
return new ReturnType(0, 0);
}
ReturnType left = process(x.left);
ReturnType right = process(x.right);
int height = Math.max(left.height, right.height) + 1;
int nodes = left.nodes + right.nodes+ 1;
return new ReturnType(height, nodes);
}
补充题目(寻找公共祖先)
寻找公共祖先,各个节点的值是唯一的
相关的代码:法一
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
//整体思路是:
//将所有节点和其对应的父节点填入Map中,从map中依次找到o1的父节点的父节点...放到新的Map中,
//最后看o2的父节点第一次在o1的父节点Map中找到时,对应的就是答案
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null || p == root || q == root){
return root;
}
HashMap<TreeNode, TreeNode> fatherMap = new HashMap<>();
fatherMap.put(root, root);
process(root, fatherMap);
TreeNode cur = p;
HashSet<TreeNode> fatherSet = new HashSet<>();
//注意:cur != fatherMap.get(cur) 和 !(fatherMap.get(cur) == root) 不同,因为右边少加了顶点下面的点
while(cur != fatherMap.get(cur)){
//也需要压入自己的值,因为可能它们的公共祖先就是p
fatherSet.add(cur);
cur = fatherMap.get(cur);
}
//最顶点没有压进去,所以压入
fatherSet.add(root);
cur = q;
while(cur != fatherMap.get(cur)){
if(fatherSet.contains(cur)){
break;
}
cur = fatherMap.get(cur);
}
return cur;
}
public void process(TreeNode root, HashMap<TreeNode, TreeNode> fatherMap){
if(root == null){
return;
}
//下面这两行代码因为返回的是void所以放在哪里都可以,但是要区别前中后序遍历代码的不同
fatherMap.put(root.left, root);
fatherMap.put(root.right, root);
process(root.left, fatherMap);
process(root.right, fatherMap);
}
}
上面有两行代码因为返回的是void所以放在哪里都可以,但是要区别前中后序遍历代码的不同,注意这个细节
相关的代码:法二
public static Node lowestAncestor(Node head, Node o1, Node o2){
if (head == null || head == o1 || head == o2){
return head;
}
Node left = lowestAncestor(head.left, o1, o2);
Node right = lowestAncestor(head.right, o1, o2);
if (left != null && right != null){
return head;
}
return left != null ? left : right;
}
这个方法很难想,下面解释一下,我们知道o1, 和 o2只有两种可能
- 其中一个是另一个的祖先
- 两个节点共有另外一个祖先
- 我们先看情况1,示意图如下:
根据代码,root节点左边返回的和右边返回的示意图是:
- 再看情况2:示意图是:
根据代码,root节点左边返回的和右边返回的示意图是:
补充题目(序列化和反序列化)
- 序列化指的是将二叉树以字符串的形式表示出来
- 反序列化指的是将表达出来的字符串反转成原来的二叉树 方法:
- 序列化:可以通过前序、中序、后序、宽度优先转化成相应的字符串,如null变成#!,其他节点变成节点的值再加上!
- 反序列化:经过相同的前序、中序、后序、宽度优先方法转成对应的二叉树 相关的代码:
public static class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
//运用先序遍历序列化二叉树
public static String serialByPre(Node head) {
if (head == null) {
return "#!";
}
String res = head.value + "!";
res += serialByPre(head.left);
res += serialByPre(head.right);
return res;
}
//以!为分隔点转化成数组,并加入队列,方便取出
//为什么反序列化分成两个函数,因为其中一个函数需要递归的操作
//其实队列不一定必须,可以用在递归函数中设置index,用values[index++]即可
public static Node reconByPreString(String preStr) {
String[] values = preStr.split("!");
Queue<String> queue = new LinkedList<String>();
for (int i = 0; i != values.length; i++) {
//队列的add()和offer()都是加入的方法,区别是:
//两者都是往队列尾部插入元素,不同的是,当超出队列界限的时候,
//add()方法是抛出异常让你处理,而offer()方法是直接返回false
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);
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 generateNodeByString(String val) {
////注意用equals()方法,因为是比较的值
if (val.equals("#")) {
return null;
}
return new Node(Integer.valueOf(val));
}
//反序列化
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;
}