树
基本概念-二叉树
树是一种层次化的结构,相较前面的数据结构来说,可以表达非线性的关系。
根据树分叉的最多个数,有多叉树和二叉树。多叉树可以转换为二叉树而不损失信息,所以我们将二叉树作为一种特殊的数据结构。
二叉树的相关概念:
- 根节点:最上面的起始节点
- 叶子节点:没有子节点的节点
- 内部节点:叶子节点之外的节点
- 深度:看上面有多少条边;高度:看下面有多少条边
二叉树有如下分类:
- 完全二叉树:从左到右填满
- 满二叉树:全部填满
- 真二叉树:节点要么两个分叉,要么就是叶节点
- 平衡二叉树:某一个结点的左右子树高度差不超过1
二叉树有如下性质:
- 树中节点个数=总分分叉数+1
- 完全二叉树节点总数为n,叶子节点是0.5n(向上取整)
- 满二叉树可以根据层数算出节点个数
二叉树常用算法以及代码
基本实现
#define BinNodePosi BinNode<T> *
template <typename T> struct BinNode{
T data;
BinNodePosi parent;
BinNodePosi lr,lf;
int height;
} ;
template <typename T> class BinTree {
protected:
int _size;
BinNodePosi _root;
virtual int updateHeight ( BinNodePosi x );
void updateHeightAbove ( BinNodePosi x );
};
待补充二叉树构造代码
遍历
递归思想在其中的应用:要明确函数的定义是什么,相信这个定义,然后利用定义推导出最终的结果
在这里简单来说就是就是想象出一个最小单元,搞清楚最小单元中某节点该做什么和什么时候做:该做什么指做点什么可以给下面的提供信息,或者能从下面获取什么信息;什么时候做指先序中序还是后序
决不要陷入递归的细节(脑子堆不了几层栈的,会炸的)
遍历分先序中序和后序三种
- 先序: 自身 左子树 右子树
- 中序:左子树 自身 右子树
- 后序:左子树 右子树 自身
void preOrder(BinNode n)
{
if(n==nullptr) return ;
cout<<n.data;
preOrder(n.lf);
preOrder(n.lr);
}
迭代不好写,而且时空复杂度都没有变得更好(不值得
AVL Tree-搜索
遍历的基础上加上比较就成为了搜索,但是此时时间复杂度为
O
(
N
)
O(N)
O(N),似乎并不是很好。
但正常来说,二叉树左子树的下每个点的值小于根节点的值,右子树则是大于,因此搜索的话复杂度可以类比二分查找的时间复杂度,为
O
(
l
o
g
n
)
O(logn)
O(logn)。
BinNode* preOrder(BinNode n)
{
if(n==nullptr)
return ;
if(n.data=number) return &n;
if(n.lf->data>=number)
{
preOrder(n.lf);
}
else {
preOrder(n.lr);
}
}
但是如果左右子树高度过大(比如左子树全部为空,都集中在右子树上),此时时间复杂度则不可能有 L o g Log Log的量级,退化为 O ( N ) O(N) O(N),为了避免这种情况,我们引入了平衡二叉树,即所有结点的平衡因子最大为1
高度:向下过几条边,左右中取较大值
平衡因子:左右子树高差,左减右
由此引出了第一个问题,怎么求的节点的平衡因子
int findHeight(BinNode n)
{
if(n==nullptr)
return 0;
else{
return max(1+findHeight(n.lf),1+findHeight(n.lr));
}
}
int findBf(BinNode n)
{
if(n==nullptr)
return 0;
else{
return abs(findHeight(n,lf)-findHeight(n.lr));
}
}
平衡因子大于1时,就会触发AVL树的修正操作
基本的修正操作分为左旋和右旋两种,左旋是逆时针旋转,在右边更高的时候用,右旋时顺时针旋转,在左边更高的时候用。
左旋操作的具体过程是,失衡的节点n,失衡节点的右孩子m,m的左孩子变成n的右孩子,n变成m的左孩子,n的根节点去链接m。
右旋同理。
只进行左旋右旋操作的称作LL/RR情况:即失衡是一边失衡,n是因为右子树高,m也是右子树高。这种情况只用转一次就行了。
但是如果失衡不是一边失衡:n是因为右子树高,m是左子树高,如果只左旋一次n并不能达到平衡的目的,此种称作LR情况,需要左旋一次n,右旋一次m。
RL情况同理。
简而言之:根据失衡节点的平衡因子正负以及失衡节点的失衡侧的平衡因子正负,我们可以判定其是以上的哪一种情况。采取相关的操作就可以保证不改变平衡性。
待补充旋转代码以及修正代码
B-Tree
来由:
因为数据量比较大,所以我们把数据放在外存也就是磁盘,而不是内存中。内存一般是高速访问,存一些比较常访问的数据。因为数据量较大,从外存中I/O操作花费的时间比较长,但是读取一个字节和读取一千个字节的差别却并不是很大,因此我们将叶节点和其上一层的节点合并成一组,要读的话一次读所有,这样减少了I/O操作的次数,增加了读取数据的量,而且一次读的数据都靠的比较近。
由此就有了B树,B树不是二叉树,但是继承了平衡二叉树“平衡”的思想,所以也叫作平衡多路查找树。其所有的叶子节点都处在一个层级。
为什么不都把所有的关键码放在一个节点?因为硬件的限制,关键码下带的数据一个扇区内放不下。
M阶B树构建:
左小右大的原则
非叶节点子节点数在(1,M],代表有多少个查找路径。
关键字数比查找路径的个数少1,但是大于M等于/2向下取整。
找的还是找关键字,但是可以在一个节点读到很多关键字,根据与关键字的大小关系(此处要求查找路径比关键字个数多1)再向下找。或者说度数比关键码个数多1.
非根节点关键码的个数要大于等于
[
M
/
2
]
−
1
[M/2]-1
[M/2]−1(向上取整),小于等于
M
−
1
M-1
M−1。
且有
h
=
O
(
l
o
g
m
N
)
h =O (log_m N)
h=O(logmN)
基本操作
插入-上溢-向上提
删除-下溢-旋转-合并
kd-树
2d树:一些节点有坐标,要用二叉树的结构把他组织起来,而且为了保证向下搜索的效率,尽量使得树是平衡的
我们采用用水平线和竖直线划分的方式,进行树的构建,每次尽量都在划分范围内的中位点去划分
划分时,我们定义该矩形右上边都是闭的,左下边都是开的
根据如上规定划分出的树,越靠近左子树位置越接近左下
2d构建实现
利用迭代的思想,逐层向深进行构建。
具体代码见收藏blog
查询实现
范围查询:一直某个矩形范围,希望寻找在该矩形区域内的点。
从上向下搜,根节点如果可判断在该矩形整体外部,则可以只搜左右子树中的一个,否则将矩形区域分开,分别在左右子树中搜索;最后判断根节点是不是在范围内。
最小查询:
已知某个点,希望寻找到树中与其最相近的点。
大体思路是根据根节点与该点在根节点划分对应维度上进行比较,若大于则进入右子树,小于则进入左子树。
问题:是否可能存在找到的并不是最小距离的情况?是否可能存在正好在边上的情况怎么处理?最小距离的点并不是某个叶子节点?
kd
多维数据空间的划分和搜索
时间空间复杂度的分析
待补充2d构造与两种查询的实现
二叉堆
堆是二叉树 大顶堆与小顶堆
大顶堆:顶点大于左右子树 左边比右边大(与传统区别?)
本质上的存储结构是向量,但是逻辑结构是完全二叉树,保证了比较好的时间复杂度
核心保证堆的性质是靠上浮和下沉
插入
把要插入的放在最后,然后上浮
删除
把最后一个移到首位,然后下沉
待补充实现
相关应用
重构二叉树
如果已知是满二叉树,前序+后序可重构
如果没有这个已知,前序+中序,后序+中序才可重构
前序+中序实现如下:
int preorder[];
int inorder[];
void buildtree(int prel,int prer,int inl,int inr,BinNodePosi tree)
{
int rootidx=inorder.find(preorder[prel]);
if(rootidx=-1)
return ;
int b=preorder.find(inorder[rootidx+1]);
if(b=-1)
return ;
if(tree!=nullptr)
{
BinNodePosi root =tree;
}
else{
BinNodePosi root=new BinNode(preorder[prel]);
}
if(prel<prer)
{
BinNodePosi ltree=new BinNode(preorder[prel+1]);
root.lf=ltree;
buildtree(prel+1,b-1,inl,rootidx-1,ltree);
}
if(inl<inr)
{
BinNodePosi rtree=new BinNode(inorder[rootidx+1]);
root.lr=rtree;
buildtree(b,prer,rootidx+1,inr,rtree);
}
}
后序+中序实现同理
满二叉树前序+后序实现重构实现如下:
int preorder[];
int postorder[];
void buildtree(int prel,int prer,int postl,int postr,BinNodePosi)
{
if(prel<=prer)
return ;
if(postl<=postr)
return ;
int idx=preorder.find(postorder[postr-1]);
if(tree1!=nullptr)
{
BinNodePosi root =tree;
}
else{
BinNodePosi root=new BinNode(preorder[prel]);
}
BinNodePosi ltree=new BinNode(preorder[prel+1]);
root.lf=ltree;
buildtree(prel+1,idx-1,postl,postl+idx-prel-2,ltree);
BinNodePosi rtree=new BinNode(preorder[idx]);
root.lr=rtree;
buildtree(idx,prer,postr-1+idx-prer,postr-1,rtree);
}
卡特兰数
栈的应用:解决卡特兰数问题
要求一序列操作的任意前m位,入栈数大于等于出栈数
进出栈序列问题
每一个非法出栈的序列,必然有一个最前面使得其非法的前缀序列,这样的前缀使得其为非法(前n项和第一次出现-1)。
对这样的前缀序列,(注意这里只对前缀)取反,则可以得到与对应的非法序列的一一映射。
取反之后的序列,1比-1 多出去一个,所以取反之后一共有
C
2
n
n
−
1
C_{2n}^{n-1}
C2nn−1种结果,也就是说非法的序列有这么多,相减之后则可以知道合法的序列有多少
(括号序列问题同理)
n+1个叶子节点可以构成多少种形状不同的真二叉树
转换为多少种前序搜索的可能性
n+1 个叶子节点相当于有2n个数在栈里(?)
1、1、2、5
待补充例题
判定树 哈夫曼编码树
n个叶子节点 每个叶子带权 带权路径长度最小的二叉树为最优编码树
构造方法:
比较小的先去做叶子节点,父节点的权重更新为两者之和,之后再次进行这样的构造过程
(等的时间短的人多等几次)
k个叶子节点 一共要新产生k-1个节点
编码:加权和 比定长编码要短 同时没有歧义(优于普通的变长编码)
因为树的遍历 往左走为1 往右走为0 无歧义就是指没有一个字符的编码是另一个字符的编码的前缀,事实上也是这样的,因为所有的代表字符的节点都是叶子节点,不可能一个节点出现在一个节点的路径上
开始写的话可以有多个叶子节点,不一定只要有两个
待补充例题
优先级队列
队列是先进先出
离散事件模拟?
切割木条时间成本的核算问题