目录
问:为什么会存在树结构?
答:树具有高效的查找与搜索语义
如 OS中的文件系统就是基于树结构进行文件管理的
若当前OS中所有文件都放在一个”目录“下,若当前操作系统有1亿个文件,最坏情况遍历1亿次才能找到特定元素。而基于树结构的文件管理,只需遍历logN次
关于树的基础概念
线性数据结构——线性表,元素之间逻辑上一个挨着一个,呈直线排列,e.g. 数组 链表 栈 队列 字符串
树是一种非线性的数据结构,它是由n(n>=0)个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看 起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
-
有一个特殊的节点,称为根节点,根节点没有前驱节点
-
除根节点外,其余节点被分成M(M > 0)个互不相交的集合T1、T2、......、Tm,其中每一个集合 Ti (1 <= i <= m) 又是一棵与树类似的子树。每棵子树的根节点有且只有一个前驱,可以有0个或多个后继
-
树是递归定义的。
从根节点除法分成的M个子集,彼此之间不能相交,若相交就不是树
结论:
-
子树不相交
-
除了根节点没有父节点外,每个结点有且仅有一个父节点
-
树、边的个数x和树中节点的个数n,x=n-1(每个结点都只有一个父节点,只有根节点没有父节点)
-
节点的度:该节点中包含子树的个数
树的度:树中最大的节点的度
-
叶子节点:度为0的结点
非叶子节点:度不为0的节点
-
根节点:没有父节点的节点,树中有且仅有一个
-
节点的层次(高度):从根节点开始计算,到该节点的层数
树的高度:当前树中最大的节点层次
二叉树
树中节点最大的度为2的树,即在二叉树中,一个节点最多有两颗子树,二叉树节点的度<=2;子树有左右之分,左右的顺序不能颠倒。
二叉树常考的性质:
-
在深度为k的二叉树中,最多存在2^k-1个节点
-
在第k层,该层最多有2^(k-1)个节点
-
由于任意二叉树都满足节点个数n和边长x具备x=n-1 ==> 在二叉树中,度为2的节点和度为0的节点有以下关系: n0 = n2 + 1 (度为0的节点总比度为2的节点多一个)
常见的二叉树:
-
满二叉树: 每一个层的结点数都是最大值。也就是说,如果 一个二叉树的层数为K,且结点总数是2^k-1,则它就是满二叉树。
-
完全二叉树: 完全二叉树的节点编号和满二叉树完全一致。满二叉树是一种特殊的完全二叉树(从视觉上看,完全二叉树就是满二叉树缺了一个”右下角“)
(完全二叉树只可能在最深的一层节点没有拉满,且这一层的节点均靠左排列)
在完全二叉树中不存在只有右子树而没有左子树的结点
在完全二叉树中,若存在度为1的节点,这个节点必然只有左子树没有右子树,且这个节点有且仅有一个
堆:优先级队列,其实就是一个完全二叉树
完全二叉树是效率很高的数据结构。对于深度为K的,有n 个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全 二叉树。
完全二叉树的编号问题:
1). 当根节点从1开始编号,若任意一个节点x,既存在子树也有父节点,则该节点的父节点编号为x/2,左子树的编号为2x,右子树的编号为2x+1
2). 当根节点从0开始编号,”堆“就是基于数组实现的完全二叉树。此时,若任意一个节点x,既存在子树也有父节点,则该节点的父节点编号为(x-1)/2,左子树的编号为2x+1,右子树的编号为2x+2
3. 二分搜索树(BST):节点的值之间有一个大小关系,左子树节点值<根节点值<右子树节点值
若在BST中查找一个元素,其实讲就是二分查找
4. 平衡二叉树:该树中任意一个节点的左右子树高度差都<=1
AVL --> 严格平衡BST
RBTree --> “黑节点”严格平衡的树
二叉树的遍历
是所有二叉树的基础操作,其实所有二叉树问题的解决思路都是遍历问题的衍生
遍历:按照一定的顺序访问这个集合的所有元素,做到不重复,不遗漏
对二叉树来说有四种遍历方式
深度优先遍历DFS:
-
前序遍历(先序遍历):(根左右)先访问根节点,然后递归访问左子树,再递归访问右子树
第一次走到根节点就能”访问“
-
中序遍历:(左根右)先递归访问左子树,再访问根节点,最后递归访问右子树
第二次走到根节点才能”访问“
-
后序遍历:(左右根)先递归访问左子树,再递归访问右子树,最后访问根节点
第三次走到根节点才能”访问“
特点:
-
先序遍历的第一个节点一定是根节点
-
中序遍历根节点左侧为左子树遍历结果,右侧为右子树遍历结果
-
后续遍历结果镜像翻转后结果为根右左
注意:递归应掌握本质问题,就是这个方法能干的事,千万别纠结到底内部怎么运行的
/**
* 传入一颗树的根节点,递归打印先序遍历结果
*/
public void preOrder(TreeNode<E> root) {
// 当前树为空
if (root == null) {
return;
}
System.out.print(root.val + " ");
preOrder(root.left);
preOrder(root.right);
}
/**
* 传入一颗树的根节点,递归打印中序遍历结果
*/
public void midOrder(TreeNode<E> root) {
// 当前树为空
if (root == null) {
return;
}
midOrder(root.left);
System.out.print(root.val + " ");
midOrder(root.right);
}
/**
* 传入一颗树的根节点,递归打印后序遍历结果
*/
public void postOrder(TreeNode<E> root) {
// 当前树为空
if (root == null) {
return;
}
postOrder(root.left);
postOrder(root.right);
System.out.print(root.val + " ");
}
/**
* 迭代的先序遍历
* @param root
* @return
*/
public List<E> preIteration(TreeNode<E> root){
// 记住极端情况判断!!!!
// 存放先序遍历输出顺序
List<E> res = new ArrayList<>();
// 树为空
if(root==null){
return res;
}
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
TreeNode cur = null; // 可删
while (!stack.isEmpty()){
cur = stack.pop(); // 变为TreeNode cur = stack.pop();
res.add((E)cur.val);
if(cur.right!=null){
stack.push(cur.right);
}
if(cur.left!=null){
stack.push(cur.left);
}
}
return res;
}
/**
* 迭代的中序遍历
* @param root
* @return
*/
public List<E> midIteration(TreeNode<E> root){
// 记住极端情况判断!!!!
List<E> res = new ArrayList<>();
// 树为空
if(root==null){
return res;
}
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
// 循环开始时,栈中没有任何元素,因此需要用cur不空保证程序可以进循环
while(cur!=null || !stack.isEmpty()){
// 左
while (cur!=null){
stack.push(cur);
cur = cur.left;
}
// 中 或 没有左子树的最左节点
cur = stack.pop();
res.add((E)cur.val);
// 右:cur没有右子树时cur.right为null,下一轮循环不检查左边,直接取栈顶元素即cur的父节点
// 有右子树时cur.right不空,开始下一轮循环按左中右顺序中序遍历右子树
cur = cur.right;
}
return res;
}
/**
* 迭代的后序遍历
* @param root
* @return
*/
public List<E> postIteration(TreeNode<E> root){
// 记住极端情况判断!!!!
List<E> res = new ArrayList<>();
// 树为空
if(root==null){
return res;
}
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
TreeNode pre = null;
// 循环开始时,栈中没有任何元素,因此需要用cur不空保证程序可以进循环
while(cur!=null || !stack.isEmpty()){
while(cur!=null){
stack.push(cur);
cur = cur.left;
}
// 中
cur = stack.pop();
// 有无右子树
if(cur.right ==null || pre == cur.right){
// cur.right!=null: 左子树已经处理过,且无右子树,因此可以直接输出
// pre == cur.right: 左子树已经处理过,且右子树也已经处理过,因此可以直接输出
res.add((E)cur.val);
pre = cur;
cur = null; // 避免下一次循环再次处理cur
}else {
// 右子树不空
stack.push(cur); // cur放回去暂不处理
cur = cur.right; // 下一次循环处理cur的右子树,按左右根顺序后序遍历右子树
}
}
return res;
}
广度优先遍历BFS:
层序遍历:将二叉树一层层的访问(一层层从上到下,从左到右访问二叉树)
实现:借助队列实现,因为队列是先入先出,而层序遍历的结果也是从上到下从左到右搭建树的顺序,与队列类似。每当遍历一层节点结束,队列中恰好存储了下一层要遍历的节点。当整个队列为空,二叉树的层序遍历结束
/**
* 输入一颗树的根节点,返回存储层序遍历顺序的动态数组
* @param root
* @return
*/
public List<E> BFSOrder(TreeNode<E> root){
List<E> res = new ArrayList<>();
Deque<TreeNode> queue = new LinkedList<>();
queue.offer(root);
TreeNode cur = null;
while (!queue.isEmpty()){
cur = queue.poll();
res.add((E)cur.val); // Object下转型为E,E是包装类或自定义类对象
if(cur.left!=null){
queue.offer(cur.left);
}
if(cur.right!=null){
queue.offer(cur.right);
}
}
return res;
}
1. 写一个方法统计当前二叉树中节点个数
int getNode(TreeNode root):当前二叉树中总共有几个节点
递归:仍然是先序遍历,只是此时的”访问“不是打印,而是统计节点个数
/**
* 传入一颗树的根节点,求出树的节点个数
*
* @param root
* @return
*/
public int countTreeNode(TreeNode<E> root) {
if (root == null) {
return 0;
}
// 当前树的节点总个数 = 根节点个数 + 左子树中节点总数 + 右子树中节点总数
return 1 + countTreeNode(root.left) + countTreeNode(root.right);
}
2. 统计一颗二叉树中叶子节点的个数
传入一颗以root为根的树,就能求出叶子节点的个数
/**
* 传入一颗树的根节点,求出叶子的节点个数
*
* @param root
* @return
*/
public int countLeafNode(TreeNode<E> root) {
// 当前树为空
if (root == null) {
return 0;
}
// 找到叶子节点(即当前树只有一个节点)
if (root.left == null && root.right == null) {
return 1;
}
// 当前树的叶子节点总个数 = 左子树中节点总数 + 右子树中节点总数
return countLeafNode(root.left) + countLeafNode(root.right);
}
3. 求当前二叉树中第k层的节点个数
传入一颗以root为根的树,就能求出第k层的节点的个数,k<=树高
问题拆分:
以A为根节点求第三层的节点个数=以A的左树B为根节点求第二层节点个数+以A的右树C为根基的点求第二层节点个数
边界条件:
k<1 没有节点 return 0
k == 1 第一层,只有一个节点 return 1
/**
* 传入一颗树的根节点,求第k层节点的个数
*
* @param root
* @param k
* @return
*/
public int countKthNode(TreeNode<E> root, int k) {
// 当前树为空
if (root == null) {
return 0;
}
// 找到第k层
if (k == 1) {
return 1;
}
return countKthNode(root.left, k - 1) + countKthNode(root.right, k - 1);
}
4. 求一颗二叉树的高度
传入一颗以root为根的树,就能求出树的高度
以A为根节点的树高 = 1+ max( 以B为根节点的树高x,以C为节点的树高)
/**
* 传入一颗树的根节点,求该树的高度
*
* @param root
* @return
*/
public int countTreeDeep(TreeNode<E> root) {
// 当前树为空
if (root == null) {
return 0;
}
// 当前树不为空
// 当前树的高度 = 根节点高度 + 左子树高度和右子树高度中较大的
return 1 + Math.max(countTreeDeep(root.left), countTreeDeep(root.right));
}
/**
* 传入一颗树的根节点,判断一颗二叉树中是否包含指定的值val
*
* @param root
* @param val
* @return
*/
public boolean findVal(TreeNode<E> root, E val) {
// 当前树为空
if (root == null) {
return false;
}
// 当前树不空
// 当前树是否包含val = 判断根节点的值是否为val 或 左子树的值是否为val 或 右子树的值是否为val
return root.val == val || findVal(root.left, val) || findVal(root.right, val);
}