树
向量允许通过下标或秩,在常数的时间内找到目标对象;然而,一旦需要对这类结构进行修改,那么无论是插入还是删除,都需要耗费线性的时间。
列表允许借助引用或位置对象,在常数的时间内插入或删除元素;但是为了找出居于特定次序的元素,却不得不花费线性的时间,对整个结构进行遍历查找。
树将二者优势结合起来,可以视为 List<List>
或者 List^2
。
从图论的角度看,树等价于连通无环图。因此与一般的图相同,树也由一组顶点以及联接与其间的若干条边组成。在计算机科学中,往往还会在此基础上,再指定某一特定顶点,并称之为根 。在指定根节点之后,也称之为有根树 。此时,从程序实现的角度,更多地将顶点称作节点。
节点之间均有路径,称作连通团
不含环路,称作无环图
任一节点与根之间存在唯一路径。
沿每个节点 v
到根 r
的唯一通路所经过边的数目,称作 v
的深度 。约定根节点的深度depth(r) = 0
,故属于第 0
层。
任一节点v
在通往树根沿途所经过的每个节点都是其祖先 ,v
是它们的后代 。特别地,v
的祖先/后代包括其本身,而 v
本身以外的祖先/后代称作真祖先/真后代。
节点v
历代祖先的层次,自下而上以1
为单位逐层递减;在每一层次上,v
的祖先至多一个。特别地,若节点u
是v
的祖先且恰好比v
高出一层,则称u
是v
的父亲,v
是u
的孩子。
v
的孩子总数,称作其度数 或度 ,记作deg(v)
。无孩子的节点称作叶节点 ,包括根在内的其余节点皆为内部节点。
v
所有的后代及其之间的联边称作子树 ,记作subtree(v)
。在不致歧义时,往往不再严格区分节点(v
)及以之为根的子树(subtree(v)
)。
树T
中所有节点深度的最大值称作该树的高度,记作height(T)
。树的高度总是由其中某一叶节点的深度确定的。
任一节点v
所对应子树subtree(v)
的高度,亦称作该节点的高度,记作height(v)
。特别地,全树的高度亦即其根节点r
的高度,height(T) = height(r)
。
术语:
根节点(Root): 树的顶部节点,没有父节点,是树的起点。
子节点(Child): 根节点下面的节点,每个子节点都有一个父节点。
父节点(Parent): 每个节点都有一个父节点,除了根节点。
叶子节点(Leaf): 没有子节点的节点称为叶子节点。
内部节点(Internal Node): 有子节点的节点称为内部节点。
边(Edge): 连接节点的线段。
路径(Path): 从一个节点到另一个节点的连续边构成的序列。
层级(Level): 根节点位于第一层,每个子节点的层级比其父节点高一层。
高度(Height): 树的高度是树中任何节点到其最远叶子节点的最长路径的长度。
子树(Subtree): 子树是树中的一部分,它本身也是一棵树,包括一个节点以及其所有后代节点。
表示
节点 | 功能 |
---|---|
root() |
根节点 |
parent() |
父节点 |
firstChild() |
长子 |
nextSibling() |
兄弟 |
insert(i,e) |
将 e 作为第 i 个孩子插入 |
remove(i) |
删除第 i 个孩子(及其后代) |
traverse() |
遍历 |
树的长子兄弟表示法(Child-Sibling Representation)是一种用于表示树结构的方法,也称为左子右兄弟表示法(Left Child Right Sibling Representation)。这种表示法的主要思想是通过两个指针来表示每个节点的孩子和兄弟关系,从而构建树结构。
在长子兄弟表示法中,每个节点有两个指针:
长子指针(Child Pointer): 指向该节点的最左边的孩子节点。
兄弟指针(Sibling Pointer): 指向该节点的右兄弟节点。
使用长子兄弟表示法,可以有效地表示一棵多叉树,其中每个节点可以有任意数量的孩子节点。这种表示法的一个优点是它可以节省内存空间,因为每个节点只需要两个指针。
二叉树
二叉树(Binary Tree): 每个节点最多有两个子节点的树。
深度为 k k k 的节点,至多 2 k 2^k 2k 个。
含 n n n 个节点、高度为 h h h 的二叉树中 h < n < 2 n + 1 h<n<2^{n+1} h<n<2n+1:
1) n = h + 1 n = h+1 n=h+1时,退化成一条单链;
2) n = 2 h + 1 − 1 n = 2^{h+1} -1 n=2h+1−1时,即所谓满二叉树。
描述多叉树 :二叉树是多叉树的特例,但在有根且有序时,其描述能力却足以覆盖后者。
实现
BinNode 模板类
cpp
template <typename T> struct BinNode;
template <typename T> using BinNodePosi = BinNode<T>*; //节点位置
template <typename T> struct BinNode { //二叉树节点模板类
// 成员(为简化描述起见统一开放,读者可根据需要进一步封装)
T data; //数值
BinNodePosi<T> parent, lc, rc; //父节点及左、右孩子
Rank height; //高度(通用)
Rank npl; //Null Path Length(左式堆,也可直接用height代替)
RBColor color; //颜色(红黑树)
// 构造函数
BinNode() : parent(NULL), lc(NULL), rc(NULL), height(0), npl(1), color(RB_RED) {}
BinNode(T e, BinNodePosi<T> p = NULL, BinNodePosi<T> lc = NULL,
BinNodePosi<T> rc = NULL, int h = 0, int l = 1, RBColor c = RB_RED)
: data(e), parent(p), lc(lc), rc(rc), height(h), npl(l), color(c) {}
// 操作接口
Rank size(); //统计当前节点后代总数,亦即以其为根的子树的规模
BinNodePosi<T> insertAsLC(T const&); //作为当前节点的左孩子插入新节点
BinNodePosi<T> insertAsRC(T const&); //作为当前节点的右孩子插入新节点
BinNodePosi<T> succ(); //取当前节点的直接后继
template <typename VST> void travLevel(VST&); //子树层次遍历
template <typename VST> void travPre(VST&); //子树先序遍历
template <typename VST> void travIn(VST&); //子树中序遍历
template <typename VST> void travPost(VST&); //子树后序遍历
// 比较器、判等器(各列其一,其余自行补充)
bool operator< (BinNode const& bn) { return data < bn.data; } //小于
bool operator== (BinNode const& bn) { return data == bn.data; } //等于
};
BinNode 接口
cpp
template <typename T> BinNodePosi<T> BinNode<T>::insertAsLC( T const& e )
{ return lc = new BinNode( e, this ); } //将e作为当前节点的左孩子插入二叉树
template <typename T> BinNodePosi<T> BinNode<T>::insertAsRC( T const& e )
{ return rc = new BinNode( e, this ); } //将e作为当前节点的右孩子插入二叉树
template <typename T> Rank BinNode<T>::size() { //统计当前节点后代总数,即以其为根的子树规模
Rank s = 1; //计入本身
if ( lc ) s += lc->size(); //递归计入左子树规模
if ( rc ) s += rc->size(); //递归计入右子树规模
return s;
}
孩子接入都可以在 O ( 1 ) \mathcal O(1) O(1)时间内完成,而获取 size()
需要遍历,因此需要时间为 O ( n ) \mathcal O(n) O(n)
BinTree 模板类
cpp
#include "BinNode.h" //引入二叉树节点类
template <typename T> class BinTree { //二叉树模板类
protected:
Rank _size; BinNodePosi<T> _root; //规模、根节点
virtual Rank updateHeight(BinNodePosi<T> x); //更新节点x的高度
void updateHeightAbove(BinNodePosi<T> x); //更新节点x及其祖先的高度
public:
BinTree() : _size(0), _root(NULL) {} //构造函数
~BinTree() { if (0 < _size) remove(_root); } //析构函数
Rank size() const { return _size; } //规模
bool empty() const { return !_root; } //判空
BinNodePosi<T> root() const { return _root; } //树根
BinNodePosi<T> insert(T const&); //插入根节点
BinNodePosi<T> insert(T const&, BinNodePosi<T>); //插入左孩子
BinNodePosi<T> insert(BinNodePosi<T>, T const&); //插入右孩子
BinNodePosi<T> attach(BinTree<T>*&, BinNodePosi<T>); //接入左子树
BinNodePosi<T> attach(BinNodePosi<T>, BinTree<T>*&); //接入右子树
Rank remove(BinNodePosi<T>); //子树删除
BinTree<T>* secede(BinNodePosi<T>); //子树分离
template < typename VST> //操作器
void travLevel(VST & visit) { if (_root) _root->travLevel(visit); } //层次遍历
template < typename VST> //操作器
void travPre(VST & visit) { if (_root) _root->travPre(visit); } //先序遍历
template < typename VST> //操作器
void travIn(VST & visit) { if (_root) _root->travIn(visit); } //中序遍历
template < typename VST> //操作器
void travPost(VST & visit) { if (_root) _root->travPost(visit); } //后序遍历
bool operator<(BinTree<T> const& t) //比较器(其余自行补充)
{ return _root && t._root && lt(_root, t._root); }
bool operator==(BinTree<T> const& t) //判等器
{ return _root && t._root && (_root == t._root); }
}; //BinTree
高度更新
二叉树任一节点的高度,都等于其孩子节点的最大高度加一。
节点自身很难发现后代的变化,因此采用另一处理策略:一旦有节点加入或离开二叉树,则更新其所有祖先的高度。
在每一节点v
处,只需读出其左、右孩子的高度并取二者之间的大者,再计入当前节点本身,就得到了v
的新高度。通常,接下来还需要从v
出发沿parent
指针逆行向上,依次更新各代祖先的高度记录。
cpp
template <typename T> Rank BinTree<T>::updateHeight( BinNodePosi<T> x ) //更新节点x高度
{ return x->height = 1 + max( stature( x->lc ), stature( x->rc ) ); } //具体规则,因树而异
template <typename T> void BinTree<T>::updateHeightAbove( BinNodePosi<T> x ) //更新高度
{ while ( x ) { updateHeight( x ); x = x->parent; } } //从x出发,覆盖历代祖先。可优化
更新每一节点本身的高度,只需执行两次getHeight()
操作、两次加法以及两次取最大操作,不过常数时间,故updateHeight()
算法总体运行时间也是O(depth(v) + 1)
,其中depth(v)
为节点v
的深度。
节点插入
cpp
template <typename T> BinNodePosi<T> BinTree<T>::insert( T const& e )
{ _size = 1; return _root = new BinNode<T>( e ); } //将e当作根节点插入空的二叉树
template <typename T> BinNodePosi<T> BinTree<T>::insert( T const& e, BinNodePosi<T> x )
{ _size++; x->insertAsLC( e ); updateHeightAbove( x ); return x->lc; } // e插入为x的左孩子
template <typename T> BinNodePosi<T> BinTree<T>::insert( BinNodePosi<T> x, T const& e )
{ _size++; x->insertAsRC( e ); updateHeightAbove( x ); return x->rc; } // e插入为x的右孩子
若二叉树T
中某节点x
的右孩子为空,则可为其添加一个右孩子。可调用x->insertAsRC()
接口,将二者按照父子关系相互联接,同时通 过updateHeightAbove()
接口更新x
所有祖先的高度,并更新全树规模。左侧节点的插入过程与此相仿,可对称地调用insertAsLC()
完成。
先序遍历
遍历: 按照某种次序访问树中各节点,每个节点被访问恰好一次。
二叉树遍历的全局次序由局部次序规则确定。
按惯例左孩子优先于右孩子,故若将节点及其孩子分别记作V、L和R,则如图所示,局部访问的次序有VLR、LVR和LRV三种选择。根据节点V在其中的访问次序,这三种策略也相应地分别称作先序遍历、中序遍历和后序遍历。
cpp
template <typename T, typename VST> //元素类型、操作器
void travPre_R ( BinNodePosi(T) x, VST& visit ) { //二叉树先序遍历算法(逑弻版)
if ( !x ) return;
visit ( x->data );
travPre_R ( x->lc, visit );
travPre_R ( x->rc, visit );
}//T(n)=O(n)
迭代实现(1)
使用辅助栈的特性实现
cpp
template <typename T, typename VST> //元素类型、操作器
void travPre_I1( BinNodePosi<T> x, VST& visit ) { //二叉树先序遍历算法(迭代版#1)
Stack<BinNodePosi<T>> S; //辅助栈
if ( x ) S.push( x ); //根节点入栈
while ( !S.empty() ) { //在栈变空之前反复循环
x = S.pop(); visit( x->data ); //弹出并访问当前节点,其非空孩子的入栈次序为先右后左
if ( HasRChild( *x ) ) S.push( x->rc );//右孩子先入后出
if ( HasLChild( *x ) ) S.push( x->lc );//左孩子后入先出
}
}
迭代实现(2)
cpp
//从当前节点出发,沿左分支不断深入,直至没有左分支的节点;沿途节点遇到后立即访问
template<typename T,typename VST> //分摊O(1)
static void visitAlongLeftBranch(BinNodePosi(T) x,VST & visit,Stack<BinNodePosi(T)>& S){
while(x){ //反复地
visit(x->data); //访问当前节点
S.push(x->rChild); //右孩子(右子树)入栈(将来逆序出栈)
x=x->lChild; //沿左侧链下行
} //只有右孩子,NULL可能入栈
}
先序遍历序列可分解为两段:沿最左侧通路自顶而下访问的各节点,以及自底而上遍历的对应右子树。
cpp
template <typename T, typename VST> //元素类型、操作器
void travPre_I2 ( BinNodePosi(T) x, VST& visit ) { //二叉树先序遍历算法(迭代版#2)
Stack<BinNodePosi(T)> S; //辅助栈
while ( true ) {
visitAlongLeftBranch ( x, visit, S ); //从当前节点出发,逐批讵问
if ( S.empty() ) break; //直到栈空
x = S.pop(); //弹出下一批的起点
}
}
使用了一个辅助栈,逆序记录最左侧通路上的节点,以便确定其对应右子树自底而上的遍历次序
中序遍历
按照中序遍历规则,为遍历非空的(子)树x,将依次递归遍历其左子树、访问节点x、递归遍历其右子树
cpp
template <typename T, typename VST> //元素类型、操作器
void travIn_R ( BinNodePosi(T) x, VST& visit ) { //二叉树中序遍历算法(递归版)
if ( !x ) return;
travIn_R ( x->lc, visit );
visit ( x->data );
travIn_R ( x->rc, visit );
}//O(n)
从根出发沿左分支下行,直至最深的节点
cpp
template <typename T> //从当前节点出发,沿左分支不断深入,直至没有左分支的节点
static void goAlongLeftBranch ( BinNodePosi(T) x, Stack<BinNodePosi(T)>& S ) {
while ( x ) { S.push ( x ); x = x->lc; } //当前节点入栈后随即向左侧分支深入,迭代直到无左孩子
}
template <typename T, typename VST> //元素类型、操作器
void travIn_I1 ( BinNodePosi(T) x, VST& visit ) { //二叉树中序遍历算法(迭代版#1)
Stack<BinNodePosi(T)> S; //辅助栈
while ( true ) {
goAlongLeftBranch ( x, S ); //从当前节点出发,逐批入栈
if ( S.empty() ) break; //直至所有节点处理完毕
x = S.pop(); visit ( x->data ); //弹出栈顶节点并访问之
x = x->rc; //转向右子树(可能为空)
}
}
在全树及其中每一棵子树的根节点处,该算法首先调用函数goAlongLeftBranch()
,沿最左侧通路自顶而下抵达末端节点 L d L_d Ld 。在此过程中,利用辅助栈逆序地记录和保存沿途经过的各个节点,以便确定自底而上各段遍历子序列最终在宏观上的拼接次序。
后序遍历
与先序与中序遍历类似地,自底而上地沿着该通路,整个后序遍历序列也可分解为若干个片段。每一片段,分别起始于通路上的一个节点,并包括三步:访问当前节点,遍历以其右兄弟(若存在)为根的子树,以及向上回溯至其父节点(若存在)并转入下一片段
cpp
template <typename T, typename VST> //元素类型、操作器
void travPost_R ( BinNodePosi(T) x, VST& visit ) { //二叉树后序遍历算法(逑弻版)
if ( !x ) return;
travPost_R ( x->lc, visit );
travPost_R ( x->rc, visit );
visit ( x->data );
}
cpp
template <typename T> //在以S栈顶节点为根的子树中,找到最高左侧可见叶节点
static void gotoHLVFL ( Stack<BinNodePosi(T)>& S ) { //沿途所遇节点依次入栈
while ( BinNodePosi(T) x = S.top() ) //自顶而下,反复检查当前节点(即栈顶)
if ( HasLChild ( *x ) ) { //尽可能向左
if ( HasRChild ( *x ) ) S.push ( x->rc ); //若有右孩子,优先入栈
S.push ( x->lc ); //然后才转至左孩子
}
else //实不得已
S.push ( x->rc ); //才向右
S.pop(); //迒回乀前,弹出栈顶癿空节点
}
template <typename T, typename VST>
void travPost_I ( BinNodePosi(T) x, VST& visit ) { //二叉树的后序遍历(迭代版)
Stack<BinNodePosi(T)> S; //辅助栈
if ( x ) S.push ( x ); //根节点入栈
while ( !S.empty() ) {
if ( S.top() != x->parent ) //若栈顶非当前节点之父(则必为其右兄),此时需
gotoHLVFL ( S ); //在以其右兄为根的子树中,找刡HLVFL(相当于递归深入其中)
x = S.pop(); visit ( x->data ); //弹出栈顶(即前一节点之后继),并访问问之
}
}
可见,在每一棵(子)树的根节点,该算法都首先定位对应的HLVFL节点。同时在此过程中,依然利用辅助栈逆序地保存沿途所经各节点,以确定遍历序列各个片段在宏观上的拼接次序。
层次遍历
迭代式层次遍历则需要使用与栈对称的队列结构
cpp
template <typename T> template <typename VST> //元素类型、操作器
void BinNode<T>::travLevel ( VST& visit ) { //二叉树层次遍历算法
Queue<BinNodePosi(T)> Q; //辅助队列
Q.enqueue ( this ); //根节点入队
while ( !Q.empty() ) { //在队列再次变空之前,反复迭代
BinNodePosi(T) x = Q.dequeue(); visit ( x->data ); //取出队首节点幵讵问之
if ( HasLChild ( *x ) ) Q.enqueue ( x->lc ); //左孩子入队
if ( HasRChild ( *x ) ) Q.enqueue ( x->rc ); //右孩子入队
}
}
Huffman 树
Prefix-free code:不同字符的编码互不为前缀,故不致歧义。
Huffman树(也称为霍夫曼树)是一种用于数据压缩的二叉树,通常用于编码和解码数据。
Huffman树的主要思想是使用不等长的编码来表示不等概率的字符或符号,以便更频繁出现的字符使用较短的编码,从而实现数据压缩。
Huffman树的构建过程通常包括以下步骤:
1)统计每个字符或符号在数据中出现的频率,然后将它们作为叶子节点构建初始的一棵二叉树。每个叶子节点的权重等于字符的频率。
2)创建一个优先队列(通常使用最小堆或最小优先队列),并将所有的叶子节点插入到队列中,根据权重进行排序。
3)从优先队列中取出权重最小的两个节点,合并它们,然后将合并后的节点插入回队列中。合并后的节点的权重等于两个子节点的权重之和。
4)重复步骤3,直到队列中只剩下一个节点,这个节点就是Huffman树的根节点。
5)构建完整的Huffman树后,为每个字符或符号分配一个唯一的编码,编码规则是:从根节点出发,左子树路径标记为0,右子树路径标记为1,直到到达叶子节点。这样,每个字符都有一个不同长度的二进制编码。
Huffman树的一个关键特性是,具有较高频率的字符拥有较短的编码,而较低频率的字符拥有较长的编码。这种编码方式可以有效地减小数据的存储或传输空间,因为常用字符的编码较短,不常用字符的编码较长。
若考虑出现频率,完全二叉树或满树未必最优。
若考虑字符各自的出现频率,则可将带权平均编码长度取作编码树 T 的叶节点带权平均深度(weighted average leaf
depth): w a l d ( T ) = ∑ x ∈ ∑ p ( x ) ⋅ ∣ r p s ( x ) ∣ wald(T) = \sum_{x\in \sum}p(x)\cdot |rps(x)| wald(T)=∑x∈∑p(x)⋅∣rps(x)∣