一.树
1.为什么需要树这种数据结构
1.数组存储方式的分析
优点:通过下标方式访问元素,速度快。对于有序数组,还可使用二分查找提高检索速度。
缺点:如果要检索具体某个值,或者插入值(按一定顺序)会整体移动,效率较低
2.链式存储方式的分析
优点:在一定程度上对数组存储方式有优化(如:插入一个数值节点,只需要将插入节点,链接到链表中即可, 删除效率也很好)
缺点:在进行检索时,效率仍然较低,比如(检索某个值,需要从头节点开始遍历)
3.树存储方式的分析
能提高数据存储,读取的效率, 比如二叉排序树,既可以保证数据的检索速度,同时也可以保证数据的插入,删除,修改的速度。
2.树的常用术语
节点
根节点
父节点
子节点
叶子节点 (没有子节点的节点)
节点的权(节点值)
路径(从 root 节点找到该节点的路线)
层
子树
树的高度(最大层数)
森林 :多颗子树构成森林
3.二叉树
树有很多种,每个节点最多只能有两个子节点的一种形式称为二叉树
二叉树的子节点分为左节点和右节点
如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二 层的叶子节点在右边连续,我们称为完全二叉树
4.二叉树的遍历
使用前序,中序和后序对下面的二叉树进行遍历
1.前序遍历
先输出父节点,再遍历左子树和右子树
2.中序遍历
先遍历左子树,再输出父节点,再遍历右子树
3.后序遍历
后序遍历: 先遍历左子树,再遍历右子树,最后输出父节点
5.顺序储存二叉树
从数据存储来看,数组存储方式和树的存储方式可以相互转换,即数组可以转换成树,树也可以转换成数组
对数组表示的二叉树进行遍历(前序,中序,后序)
思路同普通二叉树
第 n 个元素的左子节点为 2 * n + 1
第 n 个元素的右子节点为 2 * n + 2
顺序存储二叉树的特点:
顺序二叉树通常只考虑完全二叉树
第 n 个元素的左子节点为 2 * n + 1
第 n 个元素的右子节点为 2 * n + 2
第 n 个元素的父节点为 (n-1) / 2
n : 表示二叉树中的第几个元素(按 0 开始编号如图所示
6.线索化二叉树
- n 个结点的二叉链表中含有 n+1 【公式 2n-(n-1)=n+1】 个空指针域。利用二叉链表中的空指针域,存放指向 该结点在某种遍历次序下的前驱和后继结点的指针(这种附加的指针称为"线索")
- 这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。根据线索性质 的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种
- 一个结点的前一个结点,称为前驱结点
- 一个结点的后一个结点,称为后继结点
说明
中序遍历的结果:{8, 3, 10, 1, 14, 6}
当线索化二叉树后,Node 节点的 属性 left 和 right ,有如下情况:
left 指向左子树,也可能是指向前驱节点,比如 ① 节点 left 指向的左子树, 而 ⑩节点的 left 指向的就是前驱节点(leftType)
right 指向右子树,也可能是指向后继节点,比如①节点 right 指向的是右子树,而⑩节点的 right 指向的是后继节点(rightType)
public void threadedNodes(node) {
if(node == null) {
return;
}
threadedNodes(node.getLeft()); //先线索化左子树
//线索化当前结点
if(node.getLeft() == null) { //处理前驱节点
node.setLeft(pre); //让当前结点的左指针指向前驱结点
node.setLeftType(1); //修改当前结点的左指针的类型,指向前驱结点
}
if (pre != null && pre.getRight() == null) { //处理后继结点
pre.setRight(node); //让前驱结点的右指针指向当前结点
pre.setRightType(1); //修改前驱结点的右指针类型
}
pre = node; //移动到下一节点
threadedNodes(node.getRight()); //线索化右子树
}
线索二叉树的遍历
因为线索化后,各个结点指向有变化,因此原来的遍历方式不能使用,这时需要使用新的方式遍历线索化二叉树,各个节点可以通过线型方式遍历,因此无需使用递归方式,这样也提高了遍历的效率。 遍历的次序应当和中序遍历保持一致
二.堆(特殊的二叉树)
堆基本介绍
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆,注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系
- 每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
- 大顶堆举例说明
- 小顶堆举例说明
- 一般升序采用大顶堆,降序采用小顶堆
三.霍夫曼树(特殊的二叉树)
1.基本介绍
- 给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(WPL)达到最小,称这样的二叉树为最优二叉树,也称为霍夫曼树
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
2.重点概念
1.路径和路径长度
- 在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。路径中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1
2.结点的权
- 若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
3.带权路径长度
结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL
3.构成霍夫曼树的步骤
- 从小到大进行排序,将每一个数据,每个数据都是一个节点 , 每个节点可以看成是一颗最简单的二叉树
- 取出根节点权值最小的两颗二叉树
- 组成一颗新的二叉树, 该新的二叉树的根节点的权值是前面两颗二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点的权值大小再次排序, 不断重复 1-2-3-4 的步骤,直到数列中,所有的数据都被处理,就得到一颗霍夫曼树
给定数组构建霍夫曼树
4.霍夫曼编码
1.基本介绍
- 霍夫曼编码,是一种编码方式,属于一种程序算法
- 霍夫曼编码广泛地用于数据文件压缩。其压缩率通常在 20%~90%之间
2.实现
根据不同字符出现的频率给定其权值,构建霍夫曼树
根据得到的霍夫曼树,给各个字符确定霍夫曼编码,想做的路径为0,向右的路径为1
根据霍夫曼编码将原字符串转换为只包含0和1的序列
3.注意事项
- 这个霍夫曼树根据排序方法不同,也可能不太一样,这样对应的霍夫曼编码也不完全一样,但是 wpl 是 一样的,都是最小的, 最后生成的霍夫曼编码的长度是一样
10.二叉排序树(特殊的二叉树)
1.二叉排序树的介绍
对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当 前节点的值小,右子节点的值比当前节点的值大
2.二叉排序树的创建和遍历
中序遍历二叉排序树的创建
删除
分三种情况
- 叶子节点
- 只有一个子树
- 有两个子树,找到右子树最小值,清除最小值位置,并将最小值放到目标位置
遍历
按对应方法遍历可直接得到排序后的数组
11.平衡二叉树(特殊的二叉树)
1.基本介绍
平衡二叉树也叫平衡二叉搜索树,又被称为 AVL 树, 可以保证查询效率较高
具有以下特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过 1,并且左右两个子树都是一棵平衡二叉树
2.创建
1.左旋转
private void leftRotate() {
//创建新的结点,以当前根结点的值
Node newNode = new Node(value);
//把新的结点的左子树设置成当前结点的左子树
newNode.left = left;
//把新的结点的右子树设置成带你过去结点的右子树的左子树
newNode.right = right.left;
//把当前结点的值替换成右子结点的值
value = right.value;
//把当前结点的右子树设置成当前结点右子树的右子树
right = right.right;
//把当前结点的左子树(左子结点)设置成新的结点
left = newNode;
}
2.右旋转
private void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
3.双旋转
在某些情况下,单旋转不能完成平衡二叉树的转换
解决思路
- 当满足右旋转条件时
- 如果该节点左子树的右子树高度大于左子树的左子树高度,先对该节点的左子节点进行左旋转
- 再对当前节点进行右旋转
//当添加完一个结点后,如果: (右子树的高度-左子树的高度) > 1 , 左旋转
if(rightHeight() - leftHeight() > 1) {
//如果它的右子树的左子树的高度大于它的右子树的右子树的高度
if(right != null && right.leftHeight() > right.rightHeight()) {
right.rightRotate(); //先对右子结点进行右旋转
leftRotate(); //然后在对当前结点进行左旋转
} else {
leftRotate(); //直接进行左旋转即可
}
return ; //添加完毕,退出
}
//当添加完一个结点后,如果 (左子树的高度 - 右子树的高度) > 1, 右旋转
if(leftHeight() - rightHeight() > 1) {
//如果它的左子树的右子树高度大于它的左子树的高度
if(left != null && left.rightHeight() > left.leftHeight()) {
left.leftRotate(); //先对当前结点的左结点(左子树)->左旋转
rightRotate(); //再对当前结点进行右旋转
} else {
rightRotate(); //直接进行右旋转即可
}
} //代码结束
12.多路查找树(特殊的二叉树)
1.多叉树
如果允许每个节点可以有更多的数据项和更多的子节点(>2), 就是多叉树(multiway tree)
2-3 树,2-3-4 树就是多叉树,多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
2. B 树的应用
B 树通过重新组织节点,降低树的高度,并且减少 i/o 读写次数来提升效率
- 如图 B 树通过重新组织节点, 降低了树的高度
- 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页得大小通常为 4k), 这样每个节点只需要一次 I/O 就可以完全载入
- 将树的度 M 设置为 1024,在 600 亿个元素中最多只需要 4 次 I/O 操作就可以读取到想要的元素, B 树(B+)广泛 应用于文件存储系统以及数据库系统
3.2-3树
2-3树是最简单的B树结构
1.特点
- 2-3 树的所有叶子节点都在同一层(只要是 B 树都满足这个条件)
- 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点
- 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点
- 2-3 树是由二节点和三节点构成的树
2.应用
满足二叉排序树条件,所有节点必须是二节点或三节点,所有叶子节点都在同一层
当按照规则插入一个数到某个节点时,不能满足上面三个要求,就需要拆,先向上拆,如果上层满,则拆本层, 拆后仍然需要满足上面 3 个条件
3. B 树
B树,B即 balance 平衡
利用B树作为索引
- B 树的阶:节点的最多子节点个数(比如 2-3 树的阶是 3,2-3-4 树的阶是4)
- B-树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
- 关键字集合分布在整颗树中, 即叶子节点和非叶子节点都存放数据
- 搜索有可能在非叶子结点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
4. B+ 树
B+树是 B 树的变体,也是一种多路搜索树
利用B+树作为索引
- B+树的搜索与 B 树也基本相同,区别是 B+树只有达到叶子结点才命中(B 树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找
- 所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据) 恰好是有序的
- 不可能在非叶子结点命中
- 非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
- 更适合文件索引系统
6. B* 树
B*树是 B+树的变体,在 B+树的非根和非叶子结点再增加指向兄弟的指针
利用B+树作为索引
B_ 树定义了非叶子结点__关键字个数至少_为 **(2/3) ** M,即块的最低使用率为 2/3(而 B+树的块的最低使用率为的 1/2)
从第 1 个特点我们可以看出,**B_树分配新结点的概率比 B+树要低,空间使用率更高_
红黑树
13.图
1.介绍
图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。 结点也可以称为顶点
2.常用概念
- 顶点(vertex)
- 边(edge)
- 路径
- 无向图
- 有向图
- 带权图
3.图的表示方法
二维数组表示(邻接矩阵);链表表示(邻接表)
1.邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于 n 个顶点的图而言,矩阵是的 row 和 col 表示的是 1…n 个点
2.邻接表
邻接矩阵需要为每个顶点都分配 n 个边的空间,其实有很多边都是不存在,会造成空间的一定损失
邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成
4.简单实现思路
存储顶点String,使用ArrayList(2)保存矩阵(边)
5.深度优先搜索
1.基本思想
- 深度优先遍历,从初始访问结点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解: 每次都在访问完当前结点后首先访问当前结点的第一个邻接结点
- 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问
- 显然,深度优先搜索是一个递归的过程
2.算法实现
//深度优先遍历算法
//i 第一次就是 0
private void dfs(boolean[] isVisited, int i) {
//首先我们访问该结点,输出
System.out.print(getValueByIndex(i) + "->");
//将结点设置为已经访问
isVisited[i] = true;
//查找结点 i 的第一个邻接结点 w
int w = getFirstNeighbor(i);
while(w != -1) {//说明结点w 存在
if(!isVisited[w]) {
dfs(isVisited, w);
}
//如果 w 结点已经被访问过
w = getNextNeighbor(i, w);
}
}
//对 dfs 进行一个重载, 遍历我们所有的结点,并进行 dfs
public void dfs() {
isVisited = new boolean[vertexList.size()];
//遍历所有的结点,进行 dfs[回溯]
for(int i = 0; i < getNumOfVertex(); i++) {
if(!isVisited[i]) {
dfs(isVisited, i);
}
}
}
6.广度优先算法
1.基本思想
类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点
2.算法实现
//广度优先遍历算法
private void bfs(boolean[] isVisited, int i) {
int w ; // 邻接结点 w
//记录结点访问的顺序的队列
LinkedList queue = new LinkedList();
//访问结点,输出结点信息
System.out.print(getValueByIndex(i) + "=>")
//标记为已访问
isVisited[i] = true;
//生成队列
w=getFirstNeighbor(i);
while(w!=-1){
//如果该结点被访问过,则跳过该结点
if(!isVisited[w]) queue.addLast(w);
//将w指向下一个邻接结点
w == getNextNeighbor(i,w);
}
//按队列中顺序进行遍历
while(!queue.isEmpty()){
int u=(Integer)queue.removeFirst();
//递归
bfs(isVisited,u);
}
}
//对 bfs 进行一个重载, 遍历我们所有的结点,并进行 bfs
public void bfs() {
isVisited = new boolean[vertexList.size()];
//遍历所有的结点,进行 dfs[回溯]
for(int i = 0; i < getNumOfVertex(); i++) {
if(!isVisited[i]) {
bfs(isVisited,temp, i);
}
}
}