概览
1.二叉树
1.1 概念
一棵二叉树是结点的一个有限集合,该集合或者为空,或者是由一个根节点加上两棵别称为左子树和右子树的二叉树组成。
二叉树的特点:
- 每个结点最多有两棵子树,即二叉树不存在度大于 2 的结点。
- 二叉树的子树有左右之分,其子树的次序不能颠倒,因此二叉树是有序树。
1.2 二叉树的基本形态
图示:
可以从上图看到,从左到右,依次是空树,只有根节点的二叉树、只有左子树的二叉树、只有右子树的二叉树、节点的左右子树均存在
1.3 两种特殊的二叉树
- 满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。也就是说,如果一个二叉树的层数为K,且结点总数是 2^k - 1,则它就是满二叉树。
图示:
- 完全二叉树: 完全二叉树是效率很高的数据结构,完全二叉树是由满二叉树而引出来的。对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。 要注意的是满二叉树是一种特殊的完全二叉树。
图示:
1.4 二叉树的性质
- 若规定根节点的层数为1,则一棵非空二叉树的第i层上最多有 2^i -1 (i>0)个结点
- 若规定只有根节点的二叉树的深度为1,则深度为K的二叉树的最大结点数是 2^k - 1(k>=0)
- 对任何一棵二叉树, 如果其叶结点个数为 n0, 度为2的非叶结点个数为 n2,则有n0=n2+1
- 具有n个结点的完全二叉树的深度k为 log2(n+1)上取整
- 对于具有n个结点的完全二叉树,如果按照从上至下从左至右的顺序对所有节点从0开始编号,则对于序号为i的结点有:
- 若i>0,双亲序号:(i-1)/2;i=0,i为根节点编号,无双亲节点
- 若2i+1<n,左孩子序号:2i+1,否则无左孩子
- 若2i+2<n,右孩子序号:2i+2,否则无右孩子
推导:
n:代表一颗二叉树中的所有节点个数
n1:代表一颗二叉树中,度为1 的节点个数
e:代表一颗二叉树中的所有边的个数
从节点个数角度:
n = n0+n1+n2;
从边的角度:
从节点上面的边:
e = n-1
从节点下面的边:
e = 2 * n2 + 1 * n1+0 * n0 = 2 * n2+n1
推导出:n0 = n2+1
对于一颗二叉树中共n个节点:
- 节点个数是偶数,则1个节点只有左孩子,0个有右孩子;个数为奇数,则0个节点只有左孩子,0个只有右孩子。
- 叶子节点:先看对于满二叉树缺了多少节点,然后最后一层的满节点数减去缺的节点数加上缺的节点/2即为叶子节点
- 非叶子节点:节点总数减去叶子节点个数
1.5 二叉树的存储
二叉树的存储结构分为:顺序存储和类似于链表的链式存储。
二叉树的链式存储是通过一个一个的节点引用起来的,常见的表示方式有二叉和三叉表示方式,具体如下:
// 孩子表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
}
// 孩子双亲表示法
class Node {
int val; // 数据域
Node left; // 左孩子的引用,常常代表左孩子为根的整棵左子树
Node right; // 右孩子的引用,常常代表右孩子为根的整棵右子树
Node parent; // 当前节点的根节点
}
主要采用孩子表示法来构建二叉树
1.6 二叉树的基本操作
首先创建一个节点类:
//二叉树节点类
public class TreeNode {
public int val;
public TreeNode left;
public TreeNode right;
public TreeNode(int val) {
this.val = val;
}
}
1.6.1 二叉树的遍历
1.6.1.1 前序遍历
- 利用递归的思想:先根,再左子树,再右子树
代码示例:
/**
* 前序遍历
* @param root
*/
public static void preTraversal(TreeNode root){
if(root!=null){
//以字符形式打印
System.out.printf("%C",root.val);
preTraversal(root.left);
preTraversal(root.right);
}
}
- 非递归前序遍历:需要用到栈
代码示例:
public static void preOrderTraversal(TreeNode root){
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
while(!stack.isEmpty() || cur!=null){
while(cur!=null){
System.out.printf("%C ",cur.val);
stack.push(cur);
cur = cur.left;
}
//当cur 为空时,出栈让cur等于栈顶元素的right 右子树继续走
TreeNode top = stack.pop();
cur = top.right;
}
}
1.6.1.2 中序遍历
- 利用递归的思想:先左子树,再根,再右子树
代码示例:
/**
* 中序遍历
* @param root
*/
public static void inTraversal(TreeNode root){
if(root!=null){
inTraversal(root.left);
System.out.printf("%C ",root.val);
inTraversal(root.right);
}
}
- 非递归中序遍历:需要一个栈
代码示例:
// 非递归中序遍历
public static void inOrderTraversal(TreeNode root){
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
while(!stack.isEmpty() || cur!=null){
while(cur!=null){
//第一次
stack.push(cur);
cur = cur.left;
}
//当cur 为空时,出栈让cur等于栈顶元素的right 右子树继续走
TreeNode top = stack.pop();
//第二次
System.out.printf("%C ",top.val);
cur = top.right;
}
}
1.6.1.3 后序遍历
- 利用递归的思想:先左子树,再右子树,再根
代码示例:
/**
* 后序遍历
* @param root
*/
public static void postTraversal(TreeNode root){
if(root!=null){
postTraversal(root.left);
postTraversal(root.right);
System.out.printf("%C ",root.val);
}
}
- 非递归后序遍历:需要用到栈
代码示例:
/**
* 非递归后序遍历
* 需要记录上一次遍历出来的节点
*/
public static void postOrderTraversal(TreeNode root){
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
TreeNode last = null; //记录上一次遍历出来的节点
while(!stack.isEmpty() || cur!=null){
while(cur!=null){
//第一次经过
stack.push(cur);
cur = cur.left;
}
//当cur 为空时,查看栈顶元素 右子树继续走
TreeNode top = stack.peek();
if(top.right==null){
//第二次经过
//从左子树回来,但由于右子树为空,可以看作从右子树回来 可以视为第三次
stack.pop();
last = top;
System.out.println(top.val);
}else if(top.right==last){
//第三次经过
//说明从右子树回来
stack.pop();
last = top;
}else{
//说明从左子树回来 第二次经过
cur = top.right;
}
}
}
1.6.2 二叉树的基本操作
1.6.2.1 求所有节点个数
- 遍历思路:
代码示例:
/**
* 求树中的所有节点个数
* 遍历思路
*/
private static int n = 0;
public static void preOrder_1(TreeNode root){
if(root!=null){
n++;
preOrder_1(root.left);
preOrder_1(root.right);
}
}
public static int getSize_1(TreeNode root){
n = 0;
preOrder_1(root);
return n;
}
- 子问题思路:
代码示例:
/**
* 子问题思路
* 以root为根的节点个数 根的个数加上 以root.left 为根的节点个数加上 root.right 为根的节点个数
* root 为空 则为0
*/
public static int getSize_2(TreeNode root){
if(root!=null){
return 1+getSize_2(root.left)+getSize_2(root.right);
}
return 0;
}
1.6.2.2 求叶子节点个数
- 遍历思路:
代码示例:
/**
* 遍历思路
* 求叶子节点个数
*/
private static int n = 0;
//利用前序遍历
public static void preOrder_2(TreeNode root){
if(root!=null){
if(root.left==null && root.right==null){
n++;
}
preOrder_2(root.left);
preOrder_2(root.right);
}
}
public static int leafSize_1(TreeNode root){
n=0;
preOrder_2(root);
return n;
}
- 子问题思路:
代码示例:
/**
* 子问题思路
*
*/
public static int leafSize_2(TreeNode root){
if(root!=null){
//只有一个节点的树,求叶子节点个数
if(root.left==null && root.right==null){
return 1;
}
//左子树 加右子树叶子节点个数
return leafSize_2(root.left)+leafSize_2(root.right);
}
//空树
return 0;
}
1.6.2.3 求第k层节点个数
利用递归思想:
求第k层节点个数即为求左子树第k-1层节点个数+右子树第k-1层节点个数,当二叉树只有一层,此时节点个数为1
代码示例:
/**
* 遍历思路求第k层 节点个数
* 求左子树第k-1层的节点个数+右子树第k-1层的节点个数
* 当k==1时,返回 1
* @param root
* @return
*/
public static int getKLevelSize(TreeNode root,int k){
//root代表空树
//只有一个节点
//其他情况
if(root==null){
return 0;
}else if(k==1){
return 1;
}else{
return getKLevelSize(root.left,k-1)+getKLevelSize(root.right,k-1);
}
}
1.6.2.4 获取二叉树的高度
思路:返回一棵树的左子树的高度与右子树的高度比较,其中的大值加上根的高度即1
代码示例:
/**
* 获取二叉树的高度
*/
public static int getHeight(TreeNode root){
if(root==null){
return 0;
}
int leftHeight = getHeight(root.left);
int rightHeight = getHeight(root.right);
return leftHeight>rightHeight?leftHeight+1:rightHeight+1;
}
1.6.2.5 查找val所在值的节点
思路:二叉树为空返回false,二叉树只有一个节点比较值。在左子树找,找到返回,没找到在右子树找。
代码示例:
/**
* 查找值所在的节点
* @param root
* @param val
* @return
*/
public static boolean contains(TreeNode root,int val){
if(root==null){
return false;
}
if(root.val == val){
return true;
}
if(contains(root.left,val)){
return true;
}
return contains(root.right,val);
}
1.6.2.6 查找节点是否存在于二叉树
思路:与查找val所在值的节点思路一致,不过需要比较的是两个节点的引用是否相等,而不是值
代码示例:
/**
* 查找节点是否存在于二叉树
*/
public static boolean contains(TreeNode root,TreeNode node){
if(root==null){
return false;
}
if(root==node){
return true;
}
boolean leftContains = contains(root.left,node);
if(leftContains){
return true;
}
return contains(root.right,node);
}
1.6.2.7 利用带空节点的先序遍历 ,创建一颗二叉树
思路1: 使用两个list
- 情况1:如果前序序列为空,剩余序列也是空的
- 情况2:取出的根的值==’#’ 空树 去除第一个字符作为剩余
- 情况3:
- 1.利用根的值,构建根节点[根][左子树][右子树][剩余部分]
- 2.递归调用 将[左子树][右子树][剩余部分] 作为输入,构建输出 左子树/ [右子树][剩余部分] 作为构建左子树的剩余部分
- 3.递归调用 将[右子树][剩余部分] 构建二叉树
输出:右子树 / [剩余部分](构建完整树的剩余部分) 作为构建右子树时的剩余部分
代码示例:
public static TreeNode buildTree(List<Character> in,List<Character> out){
if(in.isEmpty()){
//没有序列
return null;
}
//in不为空
char rootVal = in.remove(0);
if(rootVal == '#'){
//剩下的就是in 去除第一个元素(#)
//遇到#一定是空树
out.addAll(in);
return null;
}
// 当rootVal 一定不为#,构建不同的节点和以该节点为根的树
TreeNode root = new TreeNode(rootVal);
//这里的in 由于调用过remove,不包括第一个元素
List<Character> rightOut = new ArrayList<>();
TreeNode left = buildTree(in,rightOut);
TreeNode right = buildTree(rightOut,out);
root.left = left;
root.right = right;
return root;
}
思路2:使用下标控制
代码示例:
private static int i=0;
public static TreeNode buildTree(List<Character> pre){
if(pre.isEmpty()){
//没有序列
return null;
}
if(pre.get(i)=='#'){
return null;
}else{
TreeNode root = new TreeNode(pre.get(i));
i++;
TreeNode left = buildTree(pre);
i++;
TreeNode right = buildTree(pre);
root.left = left;
root.right =right;
return root;
}
}
1.7 二叉树的层序遍历
设二叉树的根节点所在层数为1,层序遍历就是从所在二叉树的根节点出发,首先访问第一层的树根节点,然后从左到右访问第2层上的节点,接着是第三层的节点,以此类推,自上而下,自左至右逐层访问树的结点的过程就是层序遍历。层序遍历需要用到队列
- 不带层级的层序遍历:
图示:
代码示例:
/**
* 层序遍历 广度优先(采用队列)
*/
public static void levelOrderTraversal(TreeNode root){
if(root==null){
return;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while(!queue.isEmpty()){
TreeNode r = queue.remove();
System.out.printf("%C ",r.val);
if(r.left!=null){
queue.add(r.left);
}
if(r.right!=null){
queue.add(r.right);
}
}
}
- 带层级的层序遍历
思路:使用一个类封装节点类型和int类型的层级,每次入队将这个类添加进去,同时对应层级加1
代码示例:
//带层级的 节点类
public static class NL{
public TreeNode node;
public int level;
public NL(TreeNode node, int level) {
this.node = node;
this.level = level;
}
}
/**
* 带层级的遍历
* @param root
*/
public static void levelOrderTraversal_withLevel(TreeNode root){
if(root==null){
return;
}
Queue<NL> queue = new LinkedList<>();
queue.add(new NL(root,1));
while(!queue.isEmpty()){
NL l = queue.remove();
System.out.printf("层级: %d %C ",l.level,l.node.val);
if(l.node.left!=null){
queue.add(new NL(l.node.left,l.level+1));
}
if(l.node.right!=null){
queue.add(new NL(l.node.right,l.level+1));
}
}
}
1.8 小结
-
遍历:
前、中、后序的遍历(深度优先——栈)。递归、非递归
层序遍历(广度优先——队列) -
遍历思路:(求节点个数、求叶子节点个数、搜索树变有序链表)(前中后 OR 层序)
-
汇总的思路/子问题思路(前中后序)
- 把树看作三部分(根+左子树+右子树)
- 左右子树使用小规模的思路解决
- 层序遍历的变形
-
递归方法的理解:不做递归展开
递归:将相同子问题,较大规模问题转换为较小规模问题,直到规模小到一定程度(空树/一个节点的树)