数据结构和算法 - 二叉树 - 相关概念
在了解树之前的相关知识
1、树是在什么样子的条件下面产生的?
在计算机中,电脑里面的文件中是存在索引的,在图书馆中,书也是存在索引的,这都是为了实现快速查找的目的。想一下,所谓的数据结构,就是为了实现数据的快速增删改查,提高响应效率,使得系统看起来运行时流畅的,不然用户在使用的过程中,点击一次响应一天,那这个系统(软件)就没有人使用了。
在进行查找的时候,使用遍历的方式,是非常的消耗电脑的内存的,可以,但是有更好的查询方式。这个时候查找出来了二分查找,使得查询的效率得到了提升,从二分查找得到启示,使用树的结构进行元素的查找就算是查询10亿条数据,最坏情况下查询30次(因为查询的元素个数是2^30 ,30 也代表查询的次数 ,2^30次方表示可以放进去二叉树的元素数目,就是每一个节点都可以有两个子节点,所以是2的n 次方(n 为层数) )
2、树的存储方式
存储方式一般是使用数组或者链表的方式进行树的存储;
数组形式适合完全二叉树;
链表形式适合普通的树的表达;
3、树的遍历方式(Source:MOOC)
1、前序遍历
2、中序遍历
3、后序遍历
4、层序遍历
二叉树遍历的基本思想:
将二维结构的树状转换成为一维结构的线性结构
创建一个队列,把根结点 A 放进去,然后A 从队列里面抛出来,B C 放进去;
B 出来,把它的左右儿子放到队列里面去,就这样一直的进行循环即可
得到了最终的访问结果如下所示:
层序遍历主要做三件事情:
1、把队列里面最前面的元素抛出来;
2、把抛出来的元素进行打印;
3、把它的两个左儿子,右儿子结点放到队列里面去;
4、二叉树的结构的应用
(1)输出二叉树中的所有的叶子结点
在前序遍历中简单的加一行代码即可,当一个结点没有左儿子,没有右儿子的时候,我们才进行打印,其他的时候不打印,进行控制
(2)求解二叉树的高度
思想:求解二叉树中左右子树的最大高度 + 1;
也就是 max(HeighLeft,HeightRight)
上面使用了后序遍历求解了二叉树的高度
(3) 二元运算表达式树及其遍历
(4)判断使用了两种遍历序列能否唯一确定一颗二叉树?
告诉前序,后序两个,不能唯一确定,因为左右的边界是不清楚的;
使用前序,中序可以唯一确定二叉树
(5)树的同构问题
解决同构问题的关键点:
如何求解出来树的根结点呢?
关键:
1、二叉树是用什么进行表示的呢?
2、建立起来二叉树
3、进行二叉树的结构的判断,判断其是不是同构的二叉树
上面图片中的两棵树,左右经过了有限次数的盖子的相互互换,可以转变成为相同的树,所以认定两个树是同构的;
5、二叉树的一些典型应用
5.1 二叉搜索树(动态的查找)(BST)(二叉搜索树)(二叉查找树)
静态查找:查找的时候元素是不变的;(有二分查找)
动态查找:查找的时候伴随着增删改查等操作;
需要满足的条件:左子树小于根节点的数值,右子树大于根结点的数值
二叉搜索树的常用函数:
一、树
一个树只有一个根节点 父节点 兄弟节点 子节点 兄弟节点
1、空树
没有任何节点,就是空树; 一个树可以只有一个节点:就是根节点; 子树,左子树,右子树
2、节点的度
节点的度就是子树的个数,下面树中节点 1 的度就是 5;
节点 61 的度就是 0
3、叶子节点
叶子节点就是度为 0 的节点,就是没有子树的节点;
上图中的 4 就是叶子节点;
非叶子节点:度不为 0 的节点;
4、层数
根节点是第一层,后面的一次递增
从 1 开始,不是从 0 开始的;
5、节点的深度
从根节点到当前节点的唯一路径 比如 2 的深度是 2 ; 223 的深度是 4 ;是从1 开始的, 根节点是 1 ;
6、高度
从当前节点到最远叶子节点的路径上的节点数目 比如:2 的高度是 3;
5 的高度是 2 ;
从自己本身开始,自己就是 1 ;
7、树的深度等于树的高度
8、有序树
上图中的 2 3 4 4 5 6 都是顺序排列的
9、无序树
2 3 4 5 6 之间的顺序是没有规则的; 无序树也叫做自由树;
二、二叉树
1、二叉树的特点
(1)每个节点的度最大为 2 ; 最多拥有两个子树; 度的值为:0 1 2
(2)左子树和右子树之间是存在差异的;
(3)即使某个节点只有一颗子树,也是需要区分左子树和右子树
2、二叉树是有序树吗?
是的,因为左右子树是有严格的要求的
3、二叉树的性质
(1)非空二叉树的第 i 层,最多有 2 * i - 1 个节点 (i >= 1); 树从第一层开始计算的,不是 0 层开始计算的
(2)在高度为 h 二叉树上面,里面最多存在着 2^h - 1 个节点(h >= 1); 就是所有的元素都是满的;
(3)对于任何的非空二叉树,如果叶子节点的个数是 n0,度为 2 的节点个数为 n2 ,则有 n0 = n2 + 1
(4)二叉树的边数目
4、真二叉树
所有节点的度要么是 0 要么是 2; 对于满二叉树,相关的定义需要严格一些;
下面的不是真二叉树:
5、满二叉树极其相关的性质
- 满二叉树的节点的度要么是 0 ,要么是 2 ,并且所有的叶子节点都是在最后一层; 满二叉树是真二叉树的加强版本;
- 在同样高度的二叉树中,满二叉树的叶子节点的数目是最多的,总节点的数目是最多的;
- 满二叉树一定是真二叉树,真二叉树不一定是满二叉树;
满二叉树的叶子节点的数量:2^(h - 1)
总节点的数目为:n = 2^h - 1;
6、完全二叉树
叶子节点只会出现在最后两层,并且最后一层的叶子节点都是靠着左边进行对齐的; 最后一层的叶子节点是需要靠着左边进行对齐的; 如果在最后一层是在右边对齐的,就不是完全二叉树,需要加以区别;
7、完全二叉树的性质
- 度为 1 的节点只有左子树;
- 度为 1 的节点,要么是 0 个要么是 1 个;
- 完全二叉树的倒数第二层一定是一个满二叉树;
- 完全二叉树放满了就是满二叉树;
- 同样节点数量的二叉树,完全二叉树的高度是最小的;
拥有 n 个节点的二叉树,从 1 开始编号,第一个节点就是根节点,当 i > 1 的时候,它的父节点 的编号为:floor(i / 2) ;
floor 是向下取整的函数;
如果 2i <= n ,左子节点的编号为: 2i;
2i > n 没有左子节点;
面试题目(计算叶子节点的数目)
一个完全二叉树有 768 个节点,求叶子节点的个数:
总节点数量:n n 为奇数 叶子节点的数量: n0 = (n + 1) / 2;
n 为偶数 叶子节点数量: n0 = n / 2;
结算叶子节点的数量: n0 = n / 2 + 1 / 2;
使用取整函数进行公式的统一: n0 = floor(n / 2 + 1 / 2); 完美解决
上面提到的向上,向下取整都是可以互相替换的 ceiling();
关于上面的公式总结:
叶子节点的个数:
n0 = floor((n + 1) / 2) = ceiling(n / 2)
非叶子节点的数目:
n1 + n2 = floor(n / 2) = ceiling((n - 1) / 2)
8、真二叉树,满二叉树,完全二叉树的区别
真二叉树:
所有的非叶子节点的度都是 2
满二叉树:
所有的非叶子节点的度都是 2 ,并且所有的叶子节点都在最后一层
完全二叉树:
二叉树,从左到右,从上到下面,按照顺序依次的,进行放置数据;
三、二叉搜索树(binary search tree)
// 二叉搜索树也叫做二叉查找树或者二叉排序树
public class BinarySearchTree {
}
1、二叉搜索树的相关性质
1、任意一个节点的值都是大于其左子树的所有节点的值
2、任意一个节点的值都小于其右子树的所有节点的值
3、左右子树也是一颗二叉搜索树
二叉搜索树可以大大提高搜索数据的效率;
二叉搜索树存储的元素必须是可以进行比较的,比如 int double 等;
2、二叉搜索树的接口设计
二叉树的插入,删除元素的操作,和插入的顺序是没有关系的,所以说二叉树是没有索引的相关概念的,也就是说,用不到;
四、搜索二叉树实现源代码
重要代码实现
add()
实现思路
* if null == root
* 在第一个位置进行元素的创建以及元素的连接即可
* if null != root
* 1、找到需要插入元素的父节点;
* 2、创建新的节点
* 3、parent.left = node || parent.right = node
public void add(E element) {
elementNotNullCheck(element);
if (null == root) {
root = new Node<>(element, null);
System.out.println(root.element);
size++;
return;
} else {
// 定义一个父节点,目的是与下面的联合使用,找到插入元素的饿父节点,方便元素的插入操作;
Node<E> parent = root;
// 找到父节点
Node<E> node = root;
int cmp = 0;
while (node != null) {
cmp = compare(node.element, element);
// parent 变量是为了找出父节点,经过循环 node 在改变,parent 的位置也是在改变的
parent = node;
if (cmp > 0) {
node = node.right; // node 是中间变量,可以进行遍历操作,因为是树,所以进行左右的遍历
} else if (cmp < 0) {
node = node.left;
} else { // 剩下了返回值是 == 0 的;
return;
}
}
// 循环出来找到了父节点的位置;
// 找到了父节点所在的位置,进行父节点下面的额元素的插入,到底是在左边还是右边
Node<E> newNode = new Node<>(element, parent);
if (cmp > 0) {
parent.left = newNode;
System.out.println("添加到了父节点的左节点" + newNode.element);
} else {
parent.right = newNode;
System.out.println("添加到了父节点的右节点" + newNode.element);
}
size++;
}
}
全部代码实现
public class BinarySearchTree<E> {
// 创建在下面进行判断操作时候使用的比较器;
private final Comparator<E> comparator;
// 节点的根节点属性
Node<E> root;
// 节点的数目属性
private int size;
// 对于二叉树里面的比较器的赋值
public BinarySearchTree(Comparator<E> comparator) {
this.comparator = comparator;
}
// 可能是不会进行比较器的传递的
public BinarySearchTree() {
this(null);
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public void clear() {
}
/**
* if null == root
* 在第一个位置进行元素的创建以及元素的连接即可
* <p>
* if null != root
* 1、找到需要插入元素的父节点;
* 2、创建新的节点
* 3、parent.left = node || parent.right = node
* <p>
* 遇到的插入的值已经在树中存在了,应该怎么处理?
* 1、直接return
* 2、使用了覆盖
*
* @param element 需要放进去的元素的值
*/
public void add(E element) {
elementNotNullCheck(element);
if (null == root) {
root = new Node<>(element, null);
System.out.println(root.element);
size++;
return;
} else {
// 定义一个父节点,目的是与下面的联合使用,找到插入元素的饿父节点,方便元素的插入操作;
Node<E> parent = root;
// 找到父节点
Node<E> node = root;
int cmp = 0;
while (node != null) {
cmp = compare(node.element, element);
// parent 变量是为了找出父节点,经过循环 node 在改变,parent 的位置也是在改变的
parent = node;
if (cmp > 0) {
node = node.right; // node 是中间变量,可以进行遍历操作,因为是树,所以进行左右的遍历
} else if (cmp < 0) {
node = node.left;
} else { // 剩下了返回值是 == 0 的;
return;
}
}
// 循环出来找到了父节点的位置;
// 找到了父节点所在的位置,进行父节点下面的额元素的插入,到底是在左边还是右边
Node<E> newNode = new Node<>(element, parent);
if (cmp > 0) {
parent.left = newNode;
System.out.println("添加到了父节点的左节点" + newNode.element);
} else {
parent.right = newNode;
System.out.println("添加到了父节点的右节点" + newNode.element);
}
size++;
}
}
public void remove(E element) {
}
public boolean contains(E element) {
return false;
}
private void elementNotNullCheck(E element) {
if (element == null) {
try {
throw new IllegalAccessException("element not be null!!!");
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
/**
* @param e1
* @param e2
* @return 返回值等于 0;代表,两个元素 e1 和 e2 是相等的;
* > 0, e1 > e2
* < 0, e1 < e2
*/
private int compare(E e1, E e2) {
// 比较器不是空的,那就是有了比较器,直接使用比较器进行比较即可
if (comparator != null) {
return comparator.compare(e1, e2);
}
// 传进来的参数。没有对其进行比较器的传入,默认两个数值一定是可以进行比较的,把 e1 进行强制的类型转换,进行比较的方法的调用;
// 强制进行了类型的转换之后,e1 具有了 Comparable 的功能;
return ((java.lang.Comparable<E>) e1).compareTo(e2);
}
// 在二叉树里面,需要对于一个节点进行维护,保证各个节点之间的连线的正确性
private static class Node<E> {
E element;
Node<E> left; // 表示开辟了内存空间,至于是否存放相关的 Node 数据类型,看后面的程序的需要
Node<E> right;
Node<E> parent;
public Node(E element, Node<E> parent) { // element parent 才是必要的,其他的不是必要的,这两个元素是比较常用的
this.element = element;
this.parent = parent;
}
}
}
五、遍历
5.1 前序遍历
先找到根节点,然后找到其左子树,然后遍历右子树
5.2 中序遍历
先访问左子树,在访问根节点,再访问右子树
访问出来的数字是从小到大的呈现;
先访问右子树,在访问中间节点,再访问左节点;
5.3 后序遍历
根节点放在了后面进行遍历
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!