一、树
1.动机
- 层次结构的表示
- 表达式
- 文件系统
- URL
-
【数据结构】综合性
- 兼具Vector和List的优点
- 兼顾高效的查找、插入、删除
-
【半线性】
- 不再是简单的线性结构,在确定某种次序之后,具有线性特征
2.有根树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gqR0OiOM-1674651834780)(null)]
-
树是极小连通图、极大无环图T=(V;E):节点数n=|V|,边数e=|E|
-
指定任一节点r∈V作为根后,T即称作有根树
-
若 T 1 , T 2 , T 3 , . . . , T d 为有根树,则 T = ( ( U i V i ) 若T_1,T_2,T_3,...,T_d为有根树,则T=((U_iV_i) 若T1,T2,T3,...,Td为有根树,则T=((UiVi)∪{r}, ( U i E i ) ∪ (U_iE_i)∪ (UiEi)∪{ < r , r i > <r,r_i> <r,ri> | 1≤i≤d}
-
相对于T, T i 称作以 r i T_i称作以r_i Ti称作以ri为根的子树(subtree rooted at r i r_i ri),记 T i = s u b t r e e ( r i ) T_i=subtree(r_i) Ti=subtree(ri)
3.有序树
-
称作 r 的孩子( c h i l d ), r i 之间互称兄弟( s i b l i n g ) , r 为其父亲( p a r e n t ), d = d e g r e e ( r ) 为 r 的(出)度( d e g r e e ) 称作r的孩子(child),r_i之间互称兄弟(sibling),r为其父亲(parent),d=degree(r)为r的(出)度(degree) 称作r的孩子(child),ri之间互称兄弟(sibling),r为其父亲(parent),d=degree(r)为r的(出)度(degree)
-
可归纳证明: e = ∑ v ∈ V d e g r e e ( v ) = n − 1 = Θ ( n ) e=\sum_{v∈V}degree(v)=n-1=Θ(n) e=∑v∈Vdegree(v)=n−1=Θ(n),故在衡量相关复杂度时,可以n作为参照
-
若指定 T i 作为 T 的第 i 棵子树, r i 作为 r 的第 i 个孩子,则 T 称作有序树 若指定T_i作为T的第i棵子树,r_i作为r的第i个孩子,则T称作有序树 若指定Ti作为T的第i棵子树,ri作为r的第i个孩子,则T称作有序树
4.路径 + 环路
-
V中的k+1个节点,通过V中的k条边依次相联,构成一条路径/通路(path)
- π π π={ ( v 0 , v 1 ) , ( v 1 , v 2 ) , . . . , ( v k − 1 , v k ) (v_0,v_1),(v_1,v_2),...,(v_k-1,v_k) (v0,v1),(v1,v2),...,(vk−1,vk)}
-
路径长度即所含边数:| π π π|=k
-
环路(cycle/loop): v k = v 0 v_k=v_0 vk=v0(如果覆盖所有节点各一次,则称作周游(tour))
5.连通 + 无环
-
连通图:节点之间均有路径(connected) 不含环路,称作无环图(acyclic)
-
树 = 无环连通图 = 极小连通图 = 极大无环图
-
任一节点v与根之间存在唯一路径 path(v, r) = path(v)
-
以|path(v)|为指标可对所有节点做等价类划分
6.深度 + 层次
-
不致歧义时,路径、节点和子树可相互指代
- path(v) ~ v ~ subtree(v)
-
v的深度:depth(v) = |path(v)
-
path(v)上节点,均为v的祖先(ancestor)v是它们的后代(descendent),其中除自身以外,是真(proper)祖先/后代
-
半线性:在任一深度,v的祖先/后代若存在,则必然/未必唯一
-
根节点是所有节点的公共祖先,深度为0
-
没有后代的节点称作叶子(leaf)
-
所有叶子深度中的最大者称作(子)树(根)的高度
- height(v) = height( subtree(v) )
-
特别地,空树的高度取作-1
-
depth(v) + height(v) ≤height(T)
二、树的表示
1.接口
节点 | 功能 |
---|---|
root() | 根节点 |
parent() | 父节点 |
firstChild() | 长子 |
nextSibling() | 兄弟 |
insert(i, e) | 将e作为第i个孩子插入 |
remove(i) | 删除第i个孩子(及其后代) |
traverse() | 遍历 |
2.父节点
-
除根外,任一节点有且仅有一个父节点
-
节点组织为一个序列,各自记录:
- data 本身信息
- parent 父节点的秩或位置
-
树根:R ~ parent(4) = 4
3.孩子节点
-
同一节点的所有孩子,各成一个序列
-
各序列的长度,即对应节点的度数 孩子节点
-
查找孩子很快,但parent()很慢
4.父节点 + 孩子节点
三、有根有序树 = 二叉树
1.二叉树
- 二叉树:节点度数不超过2
- 孩子(子树)可以左、右区分(隐含有序)
- lc() ~ lSubtree()
- rc() ~ rSubtree()
2.描绘多叉树
2.1 长子-兄弟表示法
-
有根且有序的多叉树,均可转化并表示为二叉树
-
长子 ~ 左孩子 firstChild() ~ lc()
-
兄弟 ~ 右孩子 nextSibling() ~ rc()
[] |
---|
2.2 基数:设度数为0、1和2的节点,各有 n 0 、 n 1 和 n 2 n_0 、n_1和n_2 n0、n1和n2个
- 边数
e
=
n
−
1
=
n
1
+
2
n
2
e = n − 1 = n_1 + 2n_2
e=n−1=n1+2n2
- 1/2度节点各对应于1/2条入边
-
叶节点数 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1
- n 1 与 n 0 无关: h = 0 时, 1 = 0 + 1 ;此后, n + 0 与随 n 2 同步递增 n_1与n_0无关:h = 0时,1 = 0 + 1;此后,n+0与随n_2同步递增 n1与n0无关:h=0时,1=0+1;此后,n+0与随n2同步递增
-
节点数 n = n 0 + n 1 + n 2 = 1 + n 1 + 2 n 2 n = n_0 + n_1 + n_2 = 1 + n_1 + 2n_2 n=n0+n1+n2=1+n1+2n2
-
特别地, 当 n 1 = 0 时,有 e = 2 n 2 和 n 0 = n 2 + 1 = ( n + 1 ) / 2 当n_1 = 0时,有 e = 2n_2 和 n_0 = n_2 + 1 = (n + 1)/2 当n1=0时,有e=2n2和n0=n2+1=(n+1)/2,此时,节点度数均为偶数,不含单分支节点
2.3 满树
-
深度为k的节点,至多 2 k 2^k 2k个
-
n个节点、高h的二叉树满足 h + 1 ≤ n ≤ 2 h + 1 − 1 h+1≤n≤2^{h+1}-1 h+1≤n≤2h+1−1
-
特殊情况
- n = h + 1:退化为一条单链
- n = 2 h + 1 2^{h+1} 2h+1 - 1:即所谓满二叉树
2.4 真二叉树
- 通过引入 n 1 + 2 n 0 n_1 + 2n_0 n1+2n0个外部节点 可使原有节点度数统一为2,如此,即可将任一二叉树 转化为真二叉树(proper binary tree)
- 验证:如此转换之后,全树自身的复杂度并未实质增加
四、二叉树实现
1.BinNode模板类
template<typename T> using BinNodePosi = BinNode*; //节点位置
template<typename T> struct BinNode {
BinNodePosi<T> parent, lc, rc;
T data; int height; int 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 & ); //后序遍历
};
2.BinNode接口实现
template<typename T> BinNodePosi BinNode::insertAsLC( T const & e )
{ return lc = new BinNode( e, this ); }
template<typename T> BinNodePosi BinNode::insertAsRC( T const & e )
{ return rc = new BinNode( e, this ); }
template<typename T> int BinNode::size() { //后代总数
int s = 1; //计入本身
if (lc) s += lc->size(); //递归计入左子树规模
if (rc) s += rc->size(); //递归计入右子树规模
return s;
} //O( n = |size| )
3.BinTree模板类
template<typename T> class BinTree {
protected:
int _size;
BinNodePosi _root; //根节点
virtual int updateHeight( BinNodePosi x ); //更新节点x的高度
void updateHeightAbove( BinNodePosi x ); //更新x及祖先的高度
public:
int size() const { return _size; }
bool empty() const { return !_root; }
BinNodePosi root() const { return _root; } //树根
/* ... 子树接入、删除和分离接口;遍历接口 ... */
}
4.节点插入
BinNodePosi BinTree::insert( BinNodePosi x, T const & e ); //作为右孩子
BinNodePosi BinTree::insert( T const & e, BinNodePosi x ) { //作为左孩子
_size++;
x->insertAsLC( e );
updateHeightAbove( x );
return x->lc;
}
5.子树接入
BinNodePosi BinTree::attach( BinTree* &S, BinNodePosi x ); //接入左子树
BinNodePosi BinTree::attach( BinNodePosi x, BinTree* &S ) { //接入右子树
if ( x->rc = S->_root ) x->rc->parent = x;
_size += S->_size;
updateHeightAbove(x);
S->_root = NULL;
S->_size = 0;
release(S);
S = NULL;
return x;
}
6.高度更新
#define stature(p) ( (p) ? (p)->height : -1 ) //节点高度——空树 ~ -1
template<typename T> //更新节点x高度,具体规则因树不同而异
int BinTree::updateHeight( BinNodePosi x ) //此处采用常规二叉树规则,O(1)
{ return x->height = 1 + max( stature( x->lc ), stature( x->rc ) ); }
template<typename T> //更新节点及其历代祖先的高度
void BinTree::updateHeightAbove( BinNodePosi x ) //O( n = depth(x) )
{ while (x) { updateHeight(x); x = x->parent; } } //可优化
7.子树删除
template<typename T> int BinTree::remove( BinNodePosi x ) {
FromParentTo( * x ) = NULL;
updateHeightAbove( x->parent ); //更新祖先高度(其余节点亦不变)
int n = removeAt(x); _size -= n; return n;
}
template<typename T> static int removeAt( BinNodePosi x ) {
if ( ! x ) return 0;
int n = 1 + removeAt( x->lc ) + removeAt( x->rc );
release(x->data); release(x); return n;
}
8.子树分离
template<typename T> BinTree* BinTree::secede( BinNodePosi x ) {
FromParentTo( * x ) = NULL;
updateHeightAbove( x->parent );
// 以上与BinTree::remove()一致;以下还需对分离出来的子树重新封装
BinTree * S = new BinTree; //创建空树
S->_root = x; x->parent = NULL; //新树以x为根
S->_size = x->size(); _size -= S->_size; //更新规模
return S; //返回封装后的子树
}
五、先序遍历
1.递归实现
1.1 遍历:按照某种次序访问树中各节点,每个节点被访问恰好一次
- T= L ∪ x ∪ R L∪x∪R L∪x∪R
- 遍历:结果 ~ 过程 ~ 次序 ~ 策略
1.2 递归实现
- 应用:先序输出文件树结构:
c:\> tree.com
c:\windows
template<typename T> void traverse( BinNodePosi x, VST & visit ) {
if ( ! x ) return;
visit( x->data );
traverse( x->lc, visit );
traverse( x->rc, visit );
} //T(n)=T(1)+T(a)+T(n-a-1)=O(n)
- 制约:使用默认的Call Stack,允许的递归深度有限
1.3 观察
1.4 藤缠树
-
沿着左侧藤,整个遍历过程可分解为:
- 自上而下访问藤上节点,再自下而上遍历各右子树
-
各右子树的遍历彼此独立自成一个子任务
2.迭代算法
template<typename T, typename VST>
void travPre_I2( BinNodePosi x, VST & visit ) {
Stack < BinNodePosi > S;
while ( true ) { //以右子树为单位,逐批访问节点
visitAlongVine( x, visit, S ); //访问子树x的藤蔓,各右子树(根)入栈缓冲
if ( S.empty() ) break; //栈空即退出
x = S.pop(); //弹出下一右子树(根)
} //#pop = #push = #visit = O(n) = 分摊O(1)
}
六、中序遍历
1.递归实现
1.1 递归实现
- 应用:中序输出文件树结构:printBinTree()
template<typename T, typename VST>
void traverse( BinNodePosi x, VST & visit ) {
if ( !x ) return;
traverse( x->lc, visit );
visit( x->data );
traverse( x->rc, visit ); //tail
}//T(n)=O(1)+T(a)+T(n-a-1)=O(n)
1.2 观察
1.3 藤缠树
-
沿着左侧藤,遍历可自底而上分解为d+1步迭代:访问藤上节点,再遍历其右子树
-
各右子树的遍历彼此独立,自成一个子任务
2.迭代算法
template<typename T, typename V>
void travIn_I1( BinNodePosi x, V& visit ) {
Stack < BinNodePosi > S; //辅助栈
while ( true ) {
goAlongVine( x, S ); //从当前节点出发,逐批入栈
if ( S.empty() ) break; //直至所有节点处理完毕
x = S.pop(); //x的左子树或为空,或已遍历(等效于空),故可以
visit( x->data ); //立即访问之
x = x->rc; //再转向其右子树(可能为空,留意处理手法)
}
}
3.分析
3.1 正确性:数学归纳
- 每个节点出栈时,其左子树或不存在,或已完全遍历,而右子树尚未入栈
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4vOgcrlc-1674651834933)(null)]
- 于是,每当有节点出栈,只需访问它,然后从其右孩子出发
3.2 效率:分摊分析
-
是否O(n),取决于以下条件
- 每次迭代,都恰有一个节点出栈并被访问 //满足
- 每个节点入栈一次且仅一次 //满足
- 每次迭代只需O(1)时间 //不再满足
-
单次调用goAlongVine()就可能需做Ω(n)次入栈操作,共需Ω(n)时间
-
总体将需要O(n)时间
4.后继与前驱
for ( BinNodePosi<T> t = first(); t; t = t->succ() )
- 直接后继
//在中序遍历意义下的直接后继
template <typename T>
BinNodePosi<T> BinNode<T>::succ() {
BinNodePosi<T> s = this;
//右后代
if ( rc ) { //若有右孩子,则
s = rc; //直接后继必是右子树中的
while ( HasLChild( * s ) )
s = s->lc; //最小节点
}
//左父亲
else { //否则
//后继应是“以当前节点为直接前驱者”
while ( IsRChild( * s ) )
s = s->parent; //不断朝左上移动
//最后再朝右上移动一步
s = s->parent; //可能是NULL
}
return s;
} //当前节点的高度与深度,不过O(h)
七、后序遍历
1.观察
1.1 递归实现
- 应用:BinNode::size() + BinTree::updateHeight()
template <typename T, typename VST>
void traverse( BinNodePosi<T> x, VST & visit ) {
if ( ! x ) return;
traverse( x->lc, visit );
traverse( x->rc, visit );
visit( x->data );
}//T(n)=O(1)+T(a)+T(n-a-1)=O(n)
1.2 藤缠树
-
从根出发下行,尽可能沿左分支 实不得已,才沿右分支
-
最后一个节点必是叶子,而且是按中序遍历次序最靠左者,也是递归版中visit()首次执行处
2.迭代算法
template<typename T, typename V>
void travPost_I( BinNodePosi x, V & visit ) {
Stack < BinNodePosi > S;
if(x) S.push( x ); //根节点首先入栈
while ( ! S.empty() ) { //x始终为当前节点
if ( S.top() != x->parent ) //若栈顶非x之父(而为右兄),则
gotoLeftmostLeaf( S ); //在其右兄子树中找到最靠左的叶子
x = S.pop(); //弹出栈顶(即前一节点之后继)以更新x
visit( x->data ); //并随即访问之
}
}
3.实例
4.分析
4.1 正确性:数学归纳
- 每个节点出栈后,以之为根的子树已经完全遍历,而且其右兄弟r若存在,必恰在栈顶
- 此时正可以开始遍历子树r 故只需从r出发
4.2 效率:分摊分析
-
是否O(n),取决于以下条件
- 每次迭代,都有一个节点出栈并被访问 //满足
- 每个节点入栈一次且仅一次 //满足
- 每次迭代只需O(1)时间 //不再满足
-
单次调用goAlongVine()就可能需做Ω(n)次入栈操作,共需Ω(n)时间
5.表达式树
八、层次遍历
1.算法及分析
template<typename T> template<typename VST>
void BinNode::travLevel( VST & visit ) { //二叉树层次遍历
Queue< BinNodePosi > Q; //引入辅助队列
Q.enqueue( this ); //根节点入队
while ( ! Q.empty() ) { //在队列再次变空之前,反复迭代
BinNodePosi x = Q.dequeue(); //取出队首节点,并随即
visit( x->data ); //访问之
if ( HasLChild( * x ) ) Q.enqueue( x->lc ); //左孩子入队
if ( HasRChild( * x ) ) Q.enqueue( x->rc ); //右孩子入队
}
}
2.完全二叉树
2.1 完全二叉树 ~ 紧凑表示 ~ 以向量实现
- 叶节点仅限于最低两层,底层叶子,均居于次底层叶子左侧(相对于LCA),除末节点的父亲,内部节点均有双子
- 叶节点不致少于内部节点,但至多多出一个
2.2 层次遍历
-
前 ⌈ n / 2 ⌉ \lceil n/2\rceil ⌈n/2⌉-1 步迭代中,均有右孩子入队;前 ⌊ n / 2 ⌋ \lfloor n/2 \rfloor ⌊n/2⌋-1 步迭代中,都有左孩子入队
-
累计至少n-1次入队
2.3 辅助队列的规模
-
先增后减,单峰且对称
-
最大规模 = ⌈ n / 2 ⌉ \lceil n/2\rceil ⌈n/2⌉(前 ⌈ n / 2 ⌉ \lceil n/2\rceil ⌈n/2⌉ - 1 次均出1入2)
-
最大规模可能出现2次
2.4 完全 ~ 满
九、重构
1.[ 先序 | 后序 ] + 中序
2.增强序列
-
假想地认为,每个NULL也是“真实”节点,并在遍历时一并输出 每次递归返回,同时输出一个事先约定的元字符“^”
-
若将遍历序列表示为一个Iterator,则可将其定义为
Vector< BinNode * >
,于是在增强的遍历序列中,这类“节点”可统一记作NULL -
可归纳证明:在增强的先序、中序、后序遍历序列中任一子树依然对应于一个子序列,而且其中的NULL节点恰比非NULL节点多一个
-
如此,通过对增强序列分而治之,即可重构原树
时空性能 + 稳定性
十、Huffman编码树
1.算法
1.1 编码
-
通讯 / 编码 / 译码
-
二进制编码
- 组成数据文件的字符来自字符集 ∑ \sum ∑
- 字符被赋予互异的二进制串
-
文件的大小取决于:字符的数量 x 各字符编码的长短
1.2 PFC编码
-
将 ∑ \sum ∑中的字符组织成一棵二叉树,以0/1表示左/右孩子,各字符x分别存放于对应的叶子v(x)中
-
字符x的编码串 r p s ( v ( x ) ) = r p s ( x ) rps(v(x))=rps(x) rps(v(x))=rps(x),由根到v(x)的通路(root path)确定
-
字符编码不必等长,而且同字符的编码互不为前缀,故不致歧义 (Prefix-Free Code)
1.3 编码长度vs.叶节点平均深度
-
平均编码长度 a l d ( T ) = ∑ x ∈ ∑ d e p t h ( v ( x ) ) / ∣ ∑ ∣ ald(T)=\sum_{x∈\sum}depth(v(x))/|\sum| ald(T)=∑x∈∑depth(v(x))/∣∑∣
-
对于特定的\sum,ald()最小者即为最优编码树 T o p t T_{opt} Topt
1.4 最优编码树
∀ v ∈ T o p t , d e g ( v ) = 0 ∀ v∈T_{opt},deg(v)=0 ∀v∈Topt,deg(v)=0 only if d e p t h ( v ) ≥ d e p t h ( T o p t ) − 1 depth(v)≥depth(T_{opt})-1 depth(v)≥depth(Topt)−1
亦即,叶子只能出现在倒数两层以内——否则,通过节点交换即可
1.5 字符频率
- 实际上,字符的出现概率或频度不尽相同 甚至,往往相差极大
1.6 带权编码长度 vs. 叶节点平均带权深度
- 文件长度∝平均带权深度 w a l d ( T ) = ∑ x r p s ( x ) wald(T)=\sum_{x}rps(x) wald(T)=∑xrps(x) · w(x)
- 此时,完全树未必就是最优编码树
1.7 最优带权编码树
- 同样,频率高/低的(超)字符,应尽可能放在高/低处
- 故此,通过适当交换,同样可以缩短wald(T)
1.8 Huffman算法
- 贪婪策略:频率低的字符优先引入,位置亦更低
为每个字符创建一棵单节点的树,组成森林F
按照出现频率,对所有树排序
while ( F中的树不止一棵 )
取出频率最小的两棵树:T 和1 T2 将它们合并成一棵新树T,并令:
lc(T) = T1 且 rc(T) = T2
w( root(T) ) = w( root(T1) ) + w( root(T2) )
- 尽管贪心策略未必总能得到最优解,但非常幸运,如上算法的确能够得到最优编码树之一
2.正确性
2.1 双子性
- 最优编码树的特征
- 首先,每一内部节点都有两个孩子——节点度数均为偶数(0或2),即真二叉树
- 否则,将1度节点替换为其唯一的孩子,则新树的wald将更小
2.2 不唯一性
-
对任一内部节点而言 左、右子树互换之后wald不变
-
上述算法中,兄弟子树的次序系随机选取
-
为消除这种歧义,可以(比如)明确要求左子树的频率更低
2.3 层次性
- 出现频率最低的字符 x 和 y ,必在某棵最优编码树中处于最底层,且互为兄弟
- 否则,任取一棵最优编码树,并在其最底层任取一对兄弟 a 和 b 于是, a 和 x 、 b 和 y 交换之后,wald绝不会增加
2.4 数学归纳
- 对 ∣ ∑ ∣ 做归纳可证: H u f f m a n 算法所生成的,必是一棵最优编码树! ∣ ∑ ∣ = 2 时显然 对|\sum|做归纳可证:Huffman算法所生成的,必是一棵最优编码树!|\sum| = 2时显然 对∣∑∣做归纳可证:Huffman算法所生成的,必是一棵最优编码树!∣∑∣=2时显然
- 设算法在 ∣ ∑ ∣ < n 时均正确。现设 ∣ ∑ ∣ = n ,取 ∑ 中频率最低的 x 、 y (不妨就设二者互为兄弟) 设算法在|\sum|<n时均正确。现设|\sum|=n,取\sum中频率最低的 x 、 y (不妨就设二者互为兄弟) 设算法在∣∑∣<n时均正确。现设∣∑∣=n,取∑中频率最低的x、y(不妨就设二者互为兄弟)
- 令 ∑ ′ = ( ∑ \sum'=(\sum ∑′=(∑{x,y}) ∪ ∪ ∪{z},w(z)=w(x)+w(y)
2.5 定差
-
对于 ∑ ′ \sum' ∑′的任一编码树T’,只要为z添加孩子x和y,即可得到 ∑ \sum ∑的一棵编码树T,且 w d ( T ) − w d ( T ′ ) = w ( x ) + w ( y ) = w ( z ) wd(T)-wd(T')=w(x)+w(y)=w(z) wd(T)−wd(T′)=w(x)+w(y)=w(z)
-
可见,如此对应的 T 和 T ′ , w d 之差与 T T和T',wd之差与T T和T′,wd之差与T的具体形态无关
2.6 从最优,到最优
-
因此,只要 T ′ 是 ∑ ′ 的最优编码树,则 T 也必是 ∑ 的最优编码树(之一) 因此,只要T'是\sum'的最优编码树,则T也必是\sum的最优编码树(之一) 因此,只要T′是∑′的最优编码树,则T也必是∑的最优编码树(之一)
-
实际上, H u f f m a n 算法的过程,与上述归纳过程完全一致——每一步迭代都可视作,从某棵 T 转入对应的 T ′ 实际上,Huffman算法的过程,与上述归纳过程完全一致 —— 每一步迭代都可视作,从某棵T转入对应的T' 实际上,Huffman算法的过程,与上述归纳过程完全一致——每一步迭代都可视作,从某棵T转入对应的T′
3.算法实现
3.1 数据结构与算法
3.2 Huffman(超)字符
#define N_CHAR (0x80 - 0x20) //仅以可打印字符为例
struct HuffChar { //Huffman(超)字符
char ch; int weight; //字符、频率
HuffChar ( char c = '^', int w = 0 ) : ch ( c ), weight ( w ) {};
bool operator< ( HuffChar const& hc ) { return weight > hc.weight; } //比较器
bool operator== ( HuffChar const& hc ) { return weight == hc.weight; } //判等器 Huffman
};
3.3 Huffman树与森林
-
Huffman(子)树
using HuffTree = BinTree< HuffChar >
-
Huffman森林
using HuffForest = List< HuffTree* >
-
待日后掌握了更多数据结构之后,可改用更为高效的方式,比如:
using HuffForest = PQ_List< HuffTree* >
基于列表的优先级队列using HuffForest = PQ_ComplHeap< HuffTree* >
完全二叉堆using HuffForest = PQ_LeftHeap< HuffTree* >
左式堆
-
得益于已定义的统一接口,支撑Huffman算法的这些底层数据结构可直接彼此替换
3.4 构造编码树
HuffTree* generateTree( HuffForest * forest ) {
while ( 1 < forest->size() ) { //反复迭代,直至森林中仅含一棵树
HuffTree *T1 = minHChar( forest ), *T2 = minHChar( forest );
HuffTree *S = new HuffTree(); //创建新树,准备合并T1和T2
S->insert( HuffChar( '^', //根节点权重,取作T1与T2之和
T1->root()->data.weight + T2->root()->data.weight ) );
S->attach( T1, S->root() ); S->attach( S->root(), T2 );
forest->insertAsLast( S ); //T1与T2合并后,重新插回森林
} //assert: 循环结束时,森林中唯一的那棵树即Huffman编码树
return forest->first()->data; //故直接返回之
}
3.5 查找最小超字符
HuffTree* minHChar( HuffForest * forest ) { //此版本仅达到O(n),故整体为O(n^2)
ListNodePosi( HuffTree* ) p = forest->first(); //从首节点出发
ListNodePosi( HuffTree* ) minChar = p; //记录最小树的位置及其
int minWeight = p->data->root()->data.weight; //对应的权重
while ( forest->valid( p = p->succ ) ) //遍历所有节点
if( minWeight > p->data->root()->data.weight ) { //如必要,则
minWeight = p->data->root()->data.weight; minChar = p; //更新记录
}
return forest->remove( minChar ); //从森林中摘除该树,并返回
} //Huffman编码的整体效率,直接决定于minHChar()的效率
3.6 构造编码表
#include "Hashtable.h"
using HuffTable = Hashtable< char, char* >;
static void generateCT //通过遍历获取各字符的编码
( Bitmap* code, int length, HuffTable* table, BinNodePosi( HuffChar ) v ) {
if ( IsLeaf( * v ) ) //若是叶节点(还有多种方法可以判断)
{ table->put( v->data.ch, code->bits2string( length ) ); return; }
if ( HasLChild( * v ) ) //Left = 0,深入遍历
{ code->clear( length ); generateCT( code, length + 1, table, v->lc ); }
if ( HasRChild( * v ) ) //Right = 1
{ code->set( length ); generateCT( code, length + 1, table, v->rc ); }
} //总体O(n)
4.改进
4.1 向量 + 列表 + 优先级队列
- 方案1: (
O
(
n
2
)
O(n^2)
O(n2))
- 初始化时,通过排序得到一个非升序向量 // O ( n log n ) O(n\log n) O(nlogn)
- 每次(从后端)取出频率最低的两个节点 //O(1)
- 将合并得到的新树插入向量,并保持有序 //O(n)
- 方案2: (
O
(
n
2
)
O(n^2)
O(n2))
- 初始化时,通过排序得到一个非降序列表 // O ( n log n ) O(n\log n) O(nlogn)
- 每次(从前端)取出频率最低的两个节点 //O(1)
- 将合并得到的新树插入列表,并保持有序 //O(n)
- 方案3:
(
O
(
n
log
n
)
)
(O(n\log n))
(O(nlogn))
- 初始化时,将所有树组织为一个优先级队列(第12章)//O(n) O(nlogn)
- 取出频率最低的两个节点,合并得到的新树插入队列 / ( O ( n log n ) ) + O ( log n ) (O(n\log n))+O(\log n) (O(nlogn))+O(logn)
4.2 预排序 x (栈 + 队列)
- 方案4:
- 所有字符按频率排序,构成一个栈 //$O(n\log n) $
- 维护另一个有序队列 //O(n)
十一、下界
1.代数判定树
1.1 难度与下界
-
由前述实例可见,同一问题的不同算法,复杂度可能相差悬殊
-
两个方面着手:设计复杂度更低的算法 + 证明更高的问题难度下界
-
一旦算法的复杂度达到难度下界,则说明就大O记号的意义而言,算法已经最优
1.2 时空性能 + 稳定性
-
多种角度估算的时间、空间复杂度
- 最好 / best-case
- 最坏 / worst-case
- 平均 / average-case
- 分摊 / amortized
-
其中,对最坏情况的估计最保守、最稳妥 因此,首先应考虑最坏情况最优的算法
-
排序所需的时间,主要取决于
- 关键码比较次数 / # {key comparison}
- 元素交换次数 / # {data swap}
-
就地(in-place): 除输入数据本身外,只需O(1)附加空间
-
稳定(stability): 关键码雷同的元素,在排序后相对位置保持
1.3 最坏情况最优 + 基于比较
-
基于比较的算法(comparison-based algorithm) 算法执行的进程,取决于一系列的数值(这里即关键码)比对结果
-
任何CBA在最坏情况下,都需Ω(nlogn)时间才能完成排序
1.4 判定树
- 每个CBA算法都对应于一棵决策树(Algebraic Decision Tree),每一可能的输出,都对应于至少一个叶节点 每一次运行过程,都对应于起始于根的某条路径
1.5 代数判定树
-
决策树
- 针对“比较-判定”式算法的计算模型
- 给定输入的规模,将所有可能的输入 所对应的一系列判断表示出来
-
代数判定:
- 使用某一常次数代数多项式 将任意一组关键码做为变量,对多项式求值
- 根据结果的符号,确定算法推进方向
-
比较树(Comparison Tree):最简单的ADT,二元一次多项式,形如: k i − k j k_i-k_j ki−kj
1.6 下界:Ω(nlogn)
-
比较树是三叉树(ternary tree),内部节点至多三个分支(+|0|-)
-
每一叶节点,各对应于
- 起自根节点的一条通路
- 某一可能的运行过程
- 运行所得的输出
-
叶节点深度 ~ 比较次数 ~ 计算成本
-
树高 ~ 最坏情况时的计算成本
-
树高的下界 ~ 所有CBA的时间复杂度下界
-
对于排序算法所对应的ADT,必有N≥n!
- ADT的每一输出(叶子),对应于某一置换 依此置换,可将输入序列转换为有序序列
- 算法的输出,须覆盖所有可能的输入
-
包含N个叶节点的排序算法ADT,高度不低于 log 3 N ≥ log 3 n ! = log 3 e ⋅ [ n ln n − n + O ( ln n ) ] = Ω ( n ⋅ log n ) \log_3 N≥\log_3 n!=\log_3e·[n\ln n-n+O(\ln n)]=Ω(n·\log n) log3N≥log3n!=log3e⋅[nlnn−n+O(lnn)]=Ω(n⋅logn)
2.归约
- 线性归约(Linear-Time Reduction)
- 除了(代数)判定树,归约(reduction)也是确定下界的有力工具