疫情期间想要刷一刷算法题,看见有大佬的文章中推荐,首先可以从树这种结构先下手,因为算法题其实是需要有一个框架的思维的,虽然树这种问题一般来讲是比较难得,但是掌握了树之后,就可以很好的刷算法的思维方式。笔者虽然是计算机科班出身,但是感觉大学四年没有学到啥,打算在上研究生之前的这几个月,磨练磨练自己的技术,这几天先把树总结一下。然后在后续的博客当中会有相关树的算法练习题。
第一点要明确的是什么是树:
树是由结点或顶点和边组成的(可能是非线性的)且不存在着任何环的一种数据结构。没有结点的树称为空(null或empty)树。一棵非空的树包括一个根结点,还(很可能)有多个附加结点,所有结点构成一个多级分层结构。
树的算法,归根结底就是增删改查的操作。
一. 二叉树的相关内容介绍
1.二叉树的定义
2.二叉树的性质
3.二叉树的几种特殊形式
4.二叉树的遍历
5.二叉树的相关操作
二.二叉查找树
三.多路查找树
一.二叉树的相关内容介绍
1.二叉树的定义
树的最基本的结构就是二叉树。每个结点至多拥有两棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分,其次序不能任意颠倒。
树节点的定义:
class TreeNode{
int value;//树节点中的值
TreeNode left;
TreeNode right;
TreeNode(int x){
val = x;
}
}
2.二叉树的性质
1)若二叉树的层次从0开始,则在二叉树的第i层至多有2^i个结点(i>=0)
2)高度为k的二叉树最多有2^(k+1) - 1个结点(k>=-1)(空树的高度为-1)
3)对任何一棵二叉树,如果其叶子结点(度为0)数为m, 度为2的结点数为n, 则m = n + 1
3.二叉树的几种特殊结构
二叉树有三种的特殊结构,分别为完美二叉树,完全二叉树,完满二叉树。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200519103856804.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxODY3OTAw,size_16,color_FFFFFF,t_70)4.二叉树的遍历
前面三小节介绍了有关于二叉树的基本的概念,但是遍历和查找才是算法里面考察的主要的内容。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20200519165010845.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxODY3OTAw,size_16,color_FFFFFF,t_70) 先是最基础的三种遍历的方式中序、先序和后序。1)先序遍历:即根-左-右遍历 1 2 4 6 7 8 3 5
public static void pretravel(TreeNode tree){
if(tree != null){
System.out.println(tree.value);//先输出当前节点的值
pretravel(tree.left);//遍历节点的左子树
pretravel(tree.right);//遍历节点的右子树
}
}
2)中序遍历:即左-根-右遍历 4 7 6 8 2 1 3 5
public static void midtravel(TreeNode tree){
if(tree != null){
midtravel(tree.left);//遍历节点的左子树
System.out.println(tree.value);//输出当前节点的值
midtravel(tree.right);//遍历节点的右子树
}
}
3)后序遍历:即左-右-根遍历 7 8 6 4 2 5 3 1
public static void aftertravel(TreeNode tree){
if(tree != null){
aftertravel(tree.left);//遍历节点的左子树
aftertravel(tree.right);//遍历节点的右子树
System.out.println(tree.value);//输出当前节点的值
}
}
5.二叉树的相关操作
本节中介绍二叉树的增删改查操作
public class SearchTree
{
private class Node{
int data; // 节点的值
Node right; // 右子树
Node left; // 左子树
}
private Node root; // 树根节点
}
二叉树的插入操作(默认插入的值没有相同的)
public void insert(int data){
Node p=new Node(); //待插入的节点
p.data=data;
//当没有根节点的时候,当前插入的值就为根节点
if(root==null)
{
root=p;
}
else
{
Node parent=new Node();
Node current=root;
while(true)
{
parent=current;
//判断插入的值应该插入到左子树还是右子树
if(data>current.data)
{
current=current.right; // 右子树
if(current==null)
{
parent.right=p;
return;
}
}
else
{
current=current.left; // 左子树
if(current==null)
{
parent.left=p;
return;
}
}
}
}
}
查找节点
public Node find(int data)
{
//从根节点开始查找
Node current = root;
while(current.data! = data)
{
if(data > current.data) current = current.right;
else current = current.left;
if(current.data == null) return null;
}
return current;
}
当查找的节点的值等于要查找的值时,返回当前节点。
根据二叉查找树的特性,可以看出查找和插入都很方便,但是删除就很不方便。
删除要分三个情况:
- 删除的节点有零个子节点,可以直接删除。
- 删除的节点有一个孩子,将当前节点的父节点的left或者right改为当前节点的right或者left。
- 删除的节点有两个孩子,这种情况最麻烦,可以将要删除的电的right的最小结点替代要删除结点的数据,然后递归删除right中最小结点,本质上是将第三种情况转化为第二种情况进行处理。
关于二叉树增删改查的代码操作可以参考博客
https://www.cnblogs.com/yahuian/p/10813614.html
二.二叉查找树
**二叉查找树**的优势在于查找和插入的时间复杂度较低。什么样的二叉树是二叉查找树呢? 二叉查找树有以下四种性质: 1. 任意左子树不为空的节点的左子树的所有节点都小于该节点的值; 2. 任意右子树不为空的节点的右子树的所有节点都大于该节点的值; 3. 任意节点的左右子树都为二叉查找树; 4. 任意两个节点的值都不相等。二叉查找树的查找、插入的时间复杂度为 O ( log n ) 。
但是二叉查找树有一个弊端,可能会退化成一个有n个节点的线性链。
为了防止这种情况的产生,平衡二叉树(AVL树)出现了。
平衡二叉树是指当且仅当任何节点的两棵子树的高度差不大于1的二叉树。不管执行什么操作,只要不满足这个条件,就要通过旋转保持树的平衡。在代码实现过程中,添加了高度这一变量,用于平衡因子的计算。有关于AVL树的相关操作可以参考博客https://blog.csdn.net/qq_25343557/article/details/89110319
虽然AVL树可以解决出现退化为线性链的情况,但是每次的插入删除时候的旋转操作很浪费时间,适用于查找比较多的情况,红黑树的出现可以解决需按照浪费时间的情况。
性质:
1.节点是红色或黑色。
2.根节点是黑色。
3.每个叶子节点都是黑色的空节点(NIL节点)。
4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
5.从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
红黑树是一个相对比较难理解的概念,可以参考相关的博客https://www.jianshu.com/p/e136ec79235c。
查找操作还是很容易,但是增加和删除操作很是麻烦,需要分为多种情况进行处理,但是是考察的重点。红黑树多用于搜索,插入,删除操作多的情况下。
红黑树应用比较广泛:
1.广泛用在C++的STL中。map和set都是用红黑树实现的。
2.著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块。
3.epoll在内核中的实现,用红黑树管理事件块
4.nginx中,用红黑树管理timer等
5.在Java集合类中TreeSet和TreeMap的底层
红黑树的查询性能略微逊色于AVL树,因为比AVL树会稍微不平衡最多一层,也就是说红黑树的查询性能只比相同内容的AVL树最多多一次比较,但是,红黑树在插入和删除上完爆AVL树,AVL树每次插入删除会进行大量的平衡度计算,而红黑树为了维持红黑性质所做的红黑变换和旋转的开销,相较于AVL树为了维持平衡的开销要小得多。
最优二叉树(哈夫曼树)也是需要知道的,这个就简单很多。
哈夫曼树是一种带权路径长度最短的二叉树,也称为最优二叉树。
一般可以按下面步骤构建:
1,将所有左,右子树都为空的作为根节点。
2,在森林中选出两棵根节点的权值最小的树作为一棵新树的左,右子树,且置新树的附加根节点的权值为其左,右子树上根节点的权值之和。注意,左子树的权值应小于右子树的权值。
3,从森林中删除这两棵树,同时把新树加入到森林中。
4,重复2,3步骤,直到森林中只有一棵树为止,此树便是哈夫曼树。
哈夫曼编码,其实就是哈夫曼树的应用。即如何让电文中出现较多的字符采用尽可能短的编码且保证在译码时不出现歧义。
三. 多路查找树
在上一章节中介绍了二叉树,但是如果数据量过大的话,会导致二叉树的深度过大使磁盘I/O读写过于频繁,从而使查询效率降低。B树(B-tree)就这样出现了。是一种自平衡的树,能够保持数据有序。同时它还保证了在查找、插入、删除等操作时性能都能保持在O(logn),为大块数据的读写操作做了优化,同时它也可以用来描述外部存储(支持对保存在磁盘或者网络上的符号表进行外部查找)。
1.根结点至少有两个子女。
2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m
3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
4.所有的叶子结点都位于同一层。
5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。
由于相比于磁盘IO的速度,内存中的耗时几乎可以省略,所以只要树的高度足够低,IO次数足够小,就可以提升查询性能。
B树的增加删除同样遵循自平衡的性质,有旋转和换位。
B树的应用是文件系统及部分非关系型数据库索引。
B+树
1.所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2.不可能在非叶子结点命中;
3.非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4.更适合文件索引系统;
如果想要深入了解的可以参考这个博客B+的特性:https://www.cnblogs.com/George1994/p/7008732.html