树
树介于线性结构和非线性结构,属于半线性结构。树不是简单的线性结构,但确定某种次序后(遍历),具有线性特征。同时,树是 极小连通图、极大无环图,属于图状结构的特殊情况。
1. 树的图形特征
图:节点集 V V V 和边集 E E E 组成,记为 G = ( V , E ) G=(V,E) G=(V,E),顶点数 n = ∣ V ∣ n=|V| n=∣V∣,边数 e = ∣ E ∣ e=|E| e=∣E∣。
路径: 节点集 V = { v 0 , v 1 , v 2 , . . . , v k } V=\{v_0, v_1, v_2, ... , v_k\} V={
v0,v1,v2,...,vk},通过 k k k 条路径依次相连,构成一条路径 π = { ( v 0 , v 1 ) , ( v 1 , v 2 ) , . . . , ( v k − 1 , v k ) } \pi = \{(v_0, v_1), (v_1, v_2), ... , (v_{k-1}, v_k) \} π={
(v0,v1),(v1,v2),...,(vk−1,vk)}。路径长度 ∣ π ∣ |\pi| ∣π∣ 即所含边数 k k k,环路是 v k = v 0 v_k=v_0 vk=v0
连通图 是节点之间均有路径,而 无环图 是不含环路,所以树是无环连通图。树缺一条边则有节点成为孤点,所以树成为极小连通图;树多一条边则产生环路,所以树成为极大无环图。
2. 树的线性特征
树具有前驱和后继节点的线性特征。根节点没有前驱节点,叶子节点没有后继节点,分支节点有前驱和后继节点。树的分支节点的前驱有且仅有一个,后继节点则不唯一。
树的任一节点和根存在唯一路径,所以可以根据路径长短对所有节点划分。同一阶层的互为兄弟节点,到根节点的所有前驱节点称为祖先节点,直接前驱称为父节点;所有后继节点称为子孙节点,直接后继称为孩子节点。
节点深度是从根节点往下数,该节点位于的阶层;节点高度是从叶子节点往上数,该节点位于的阶层。
3. 树的表示
线性结构节点的关系包含前驱和后继,向量可以通过秩访问,链表可以通过双向指针访问。树节点的关系包含父节点、孩子节点、兄弟节点,所以树节点设置便于访问上述节点。
首先,尝试使用两个向量存储树结构,一个向量存储节点内容 d a t a data data,另一个向量存储父节点的秩,循秩访问节点。此时,访问父节点的时间复杂度是 O ( 1 ) O(1) O(1)。
因为孩子节点个数不唯一,不可以使用向量存储孩子节点,所以访问孩子节点需要遍历整个向量,时间复杂度是 O ( n ) O(n) O(n)。此时可以使用链表存储孩子节点,树节点设置指针 f i r s t C h i l d ( ) firstChild() firstChild() 指向第一个孩子节点,循位置访问。此时,访问孩子节点不需要遍历整棵树。
同理,可以为树节点再添加一个指针指向兄弟节点。这样树节点就有一个纵向指针指向孩子节点,一个横向指针指向兄弟节点。为统一数据结构,改用指针指向父节点。
二叉树
树节点的度是节点所含的孩子节点个数,节点度数不超过2的数称为二叉树,从而可区分左右孩子和左右子树。
1. 二叉树的特征
-
单层节点:根节点为 第 0 0 0 层,个数为 1 ( 2 0 ) 1(2^0) 1(20);第 2 2 2 层至多 2 ( 2 1 ) 2(2^1) 2(21) 个节点,以此类推二叉树第 k k k 层的节点数至多有 2 k 2^k 2k。
-
节点总数 n n n 和树高度 h h h:
-
节点 n n n 和度数 e e e:设度数为0、1、2的节点数分别 n 0 , n 1 , n 2 n_0, n_1, n_2 n0,n1,n2 个,叶子节点数 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1 节 点 总 数 : n = n 0 + n 1 + n 2 度 数 : e = n − 1 = n 1 + 2 n 2 节点总数:n=n_0+n_1+n_2 \qquad 度数:e=n-1=n_1+2n_2 节点总数:n=n0+n1+n2度数:e=n−1=n1+2n2
2. 二叉树的原型
#define BinNodePosi(T) BinNode<T>* // 节点位置
template <typename T>
struct BinNode {
BinNodePosi(T) parent, lc, rc; // 父亲、孩子
T data; int height; // 高度、子树规模
};
2. 先序遍历
- 递归算法
先序遍历的操作过程:二叉树为空,则什么也不做;否则访问根节点、遍历左子树、遍历右子树
template <typename T>
void PreOrder( BinNodePosi(T) x ) {
if (!x) return;
visit( x->data );
PreOrder( x->lc );
PreOrder( x->rc );
}
- 迭代算法
自上而下访问遇到的节点并将右孩子入栈,然后再自下而上地遍历右子树,所以需要设置一个辅助栈保存右子树节点。
template <typename T>
void PreOrder( BinNodePosi(T) x ) {
Stack < BinNodePosi(T) > S; // 辅助栈
while ( x || !S.empty() ) {
/* 自上而下 */
while ( x ) {
visit( x->data ); // 根:访问当前根节点, 右孩子入栈
S.push( x->rc );
x = x->lc; // 左:沿藤下行至底部
}
/* 自上而下 */
x = S.pop(); // 右:弹出并遍历右子树
}
}
3. 中序遍历
- 递归算法
中序遍历的操作过程:二叉树为空,则什么也不做;否则遍历左子树、访问根节点、遍历右子树
template <typename T>
void InOrder( BinNodePosi(T) x ) {
if (!x) return;
InOrder( x->lc );
visit( x->data );
InOrder( x->rc );
}
- 迭代算法
先序遍历的迭代需要自上而下探到左子树底部,然后再自下而上访问根节点,所以需要设置一个辅助栈保存遇到的节点。
template <typename T>
void InOrder( BinNodePosi(T) x ) {
Stack < BinNodePosi(T) > S; // 辅助栈
while ( x || !S.empty() ) {
/* 自上而下 */
while ( x ) {
// 左:所遇节点入栈, 沿藤下行至底部
S.push( x );
x = x->lc;
}
/* 自下而上 */
x = S.pop(); // 根:根节点出栈并访问
visit( x->data );
x = x->rc; // 右:转遍历当前根节点的右子树
}
}
4. 后序遍历
- 递归算法
后序遍历的操作过程:二叉树为空,则什么也不做;否则遍历左子树、遍历右子树、访问根节点
template <typename T>
void PreOrder( BinNodePosi(T) x ) {
if (!x) return;
PreOrder( x->lc );
PreOrder( x->rc );
visit( x->data );
}
- 迭代算法
template <typename T>
void PostOrder( BinNodePosi(T) x ) {
Stack < BinNodePosi(T) > S; // 辅助栈
BinNodePosi(T) r;
while ( x || !S.empty() ) {
/* 自上而下 */
while ( x ) {
// 左:所遇节点入栈, 沿藤下行至底部
S.push( x );
x = x->lc;
}
/* 自下而上 */
x = S.top();
if ( x->rc && x->rc != r) {
// 右:右子树存在,未被访问则转向右子树
x = x->rc;
}
else {
// 根:右子树不存在或已访问过,则访问当前节点
x = S.pop();
visit( x->data );
r = x; x = nullptr; // 记录访问完的节点
}
}
}
5. 层序遍历
template <typename T>
void LevelOrder( BinNodePosi(T) x ) {
Queue< BinNodePosi(T) > Q; // 辅助队列
Q.enqueue( x ); // 根节点入队
while ( ! Q.empty() ) {
BinNodePosi(T) x = Q.dequeue(); // 取出队首节点
visit( x->data ); // 访问队首节点
if ( x->lc ) Q.enqueue( x->lc ); // 左孩子入队
if ( x->rc ) Q.enqueue( x->rc ); // 右孩子入队
}
}