树形结构是很是重要的一种数据结构。咱们能够经过平衡二叉树来实现排序问题,用树结构来表示源程序的语法结构,树也能够表示数据库或文件系统。而且不少容器的底层都是树结构。算法
下面先来了解关于树结构的名词有哪些: 数据库
这里只解释了二叉树(一个节点只有左右两个孩子的状况),固然树不必定只有两个孩子,好比下文会出现的并查集和Trie,但咱们能够把大多数树转化为二叉树,这样更便于让咱们理解概念。数组
在开始二叉树以前再来看关于二叉树中的概念:bash
满二叉树:一个深度为k且有2^k - 1个结点的二叉树称为满二叉树网络
彻底二叉树:彻底二叉树从根结点到倒数第二层知足完美二叉树,最后一层能够不彻底填充,其叶子结点都靠左对齐。数据结构
平衡二叉树:每一个节点的左右子树深度差不超过1ui
绝对平衡树:只有最后一层是叶子节点,而且知足二分搜索树的特色spa
若是如今有一个数组,给出一个数(这个数在数组中),要求找到这个数在这个数组的下标位置,咱们只能一个一个去遍历查找,这个时候的时间复杂度是O(n),为了加快查找速度,若是咱们获得的数组是排好序的,咱们可使用折半查找法,这样时间复杂度就为O(logn)了,速度会很快。而折半查找的思路就是利用了二分搜索的思想。设计
先来看二分搜索树的节点构造:3d
class Node{
int data;//节点值
Node left;//左孩子
Node right;//右孩子
}
复制代码
节点的左子树的全部节点小于这个节点,节点的右子树的全部节点大于这个节点。
二分搜索树的插入操做十分简单:
插入的平均时间复杂度为O(logn)
删除的操做稍微麻烦一些,将要删除的节点分为三种状况
删除的平均时间复杂度为O(logn)
查找和插入的方法相似,这里就不赘述了
查找的平均时间复杂度为O(logn)
若是想遍历一棵二叉树有两种方法,深度优先遍历和广度优先遍历,而深度优先遍历又能够分为前序,中序,后序遍历。
下面来看个图来看具体的遍历时如何的:
堆是一种彻底二叉树,而且一个节点要大于(小于)它的左右孩子
来看堆的节点构造
class Node{
int data;//节点值
Node left;//左孩子
Node right;//右孩子
}
复制代码
对于堆来讲他更注重将最大的放在首位和堆性质的维护。这里由于堆是一个彻底二叉树,因此能够用数组来保存堆中的元素。
如图(如下使用最小堆,即每一个节点小于它的孩子节点):
那么对于一个节点,咱们能够很容易的找到这个节点的双亲结点和左右节点。
//得到双亲结点的下标
int getParent(int index){
return (index + 1) / 2 - 1;
}
//得到左孩子
int getLeftChild(int index){
return index * 2 + 1;
}
//得到右孩子
int getRightChild(int index){
return index * 2 + 2;
}
复制代码
堆可以实现优先队列,在现实生活中也有优先队列的应用,例如排队的时候会先让老人和小孩到队列前面。
彻底d叉树,根最小。能够想成原来咱们的堆是二叉堆,这个d能够为2,3,4
以上咱们讨论的堆是比较每一个节点的data,若是这个data是一种很庞大的数据结构,那么会很耗时。这时咱们能够用索引堆,在原来堆的基础上用一个索引数组来存储数据元素的位置,即索引堆里面包含两个数组
二项堆是二项树的集合。二项树也是一种树结构,二项树的第K棵树有2k个结点,高度是k;深度为d的结点共有个结点。二项堆就由一组二项树所构成。
还有斐波那契堆,Pairing堆等等。
线段树也是一种平衡二叉树,可是它节点和平衡二叉树的节点有些不一样,它的每一个节点能够看做是由一段数组组成,若是一个节点的数组是[l,r]而他的左孩子和它的右孩子值就是数组空间为[l,mid]和[mid+1,r]的值(这里值的意思是[left,right]按照本身的意愿得到的树,能够输left*right,也能够是left+right),mid通常取作l+(r-l)/2
看一下线段树的关键字段:
//tree表示线段树中的全部节点,和层次遍历的顺序同样,和堆的物理结构同样
//上图中tree[0]就是0-7的值,tree[3]就是0-1的值
private int[] tree;
复制代码
线段树可以解决一些算法问题,例如
若是用线段树这种结构就有一个方便的思路解决以上问题
这里假设咱们要求求解[1,7]的值咱们直接获取tree[8]+tree[4]+tree[2]+便可,即[1,1]+[2,3]+[4,7]就获得[1,7]的值。若是将0-7采用二分搜索树的方法,并从1加到7那个就是7*O(logn)的复杂度。
线段树主要针对于查询和修改,至于增长和删除咱们不讨论。
这里直接看若是须要查询一段区间该如何操做(懂了区间查询以后单个元素查询也会简单一些)
这里直接放一个例子来理解简单一些,例如如今我要查询[1,7]的值,让查找的值为x返回:
[2,3]的值就是tree[4],[4,7]的值就是tree[2],[1,1]的值就是tree[8]
大概流程就是上面所述,可能一开始看有点晦涩,但结合代码来看使用递归仍是挺清晰的
修改的操做和查询的思路同样的,不过要对遍历过的节点进行修改,这里就不赘述了
实际上,可以用线段树解决的问题都能
字典树不是二叉树,和它的名字同样,首先来看他的节点结构(根节点是不包含字符的,根节点的c是空)
class Node{
char c;//当前节点的字符
Node[] next;//当前节点的下一个节点
boolean end;//判断这个字母是否是一个结尾
}
复制代码
咱们能够发现字典树的每一个节点由若干个指向下一节点的指针,总体字典树的结构以下图:
能够看到上面的图用深度优先中序遍历的话他每遍历到叶子节点都是一个单词,如and,as,at,cn,com。
在之前的电话簿系统中,若是这时咱们用二分搜索树去保存每一个联系人的信息,假设咱们的通信录有一千万个联系人信息,那么咱们经过名字去查询这个联系人的信息是很是费时的O(logn)。而若是咱们用字典树,好比咱们要查询一个名叫and的联系人,咱们经过根节点在next中找a,在a节点中找n,再在n节点中找b(若是这里全是英文字母那么next也只有26种状况),这时咱们的时间复杂度只和这个单词的长度有关O(K),K为单词长度。那么可见这种状况下字典树的优点很是明显。固然,字典树的这种设计也就是典型的用空间换取时间的思路。
字典树的操做主要为增,删,查
在开始以前,解释一下end的做用,例如咱们有一个as和一个ass(坏笑.jpg),咱们如何判断ass这个单词是只有ass仍是还有as这个单词呢?咱们须要对每个节点进行一个标识,标识这个单词是否在这里是一个完整的单词。例如ass,最后一个s中end就要为true,由于ass最后的s是ass的结尾。而若是还有个as单词,那么第一个s就要为true来表明as这个单词以s结束。
增长的逻辑就很简单了,例如增长一个geek
例如上图中咱们要删除an这个单词
例如如今咱们要查找and
并查集是一种树形结构,又叫“不相交集合”,保持了一组不相交的动态集合,每一个集合经过一个表明来识别,表明即集合中的某个成员,一般选择根作这个表明。
能够解决链接问题,网络中节点链接状态,路径问题。
举个例子,例如咱们有若干个城镇,不一样城镇之间可能会有相连的道路,将这些城镇当作节点,而后判断哪些城镇之间是能够通路的。
先看左边的图,也就是合并前的图,咱们能够说d和c是连通的,g和e是连通的,c和f是不相通的。
若是这时咱们想在b城镇和f城镇之间连通,这时,咱们不能直接将b和f相连,应该让f的根节点加到d的根节点下,那么这里f的根节点是e,b的根节点是a,也就是直接让a成为e的双亲节点,如图。
咱们能够看到并查集的逻辑结构是一种树结构,但每一个节点有的只是本身的父亲节点。
由于并查集主要是来解决路径查找问题的,因此相应的对并查集就须要一个操做isConnect来判断两个节点是否相连。若是两个节点以后能够连通,那么就须要union操做将两个节点连通
boolean isConnect(Node p, Node q)
经过上面的分析咱们知道了判断节点是否相连主要比较的是根节点,因此只要获得p的根节点和q的根节点再判断两个节点是否相同便可
void unionElements(Node p, Node q)
和一开始分析的时候同样,咱们能够先求得p节点的根节点,再求得q节点的根节点,让q节点的根节点的双亲结点指向p节点的根节点便可。
有可能出现全部的节点都在一条链上,而咱们能够将整棵树弄成只有两层。如图
下一章将会总结AVL,红黑树,B+树