数据结构(c++)学习笔记--二叉树


一、树

1.动机

  • 层次结构的表示
    • 表达式
    • 文件系统
    • URL

pStcbQg.png

  • 【数据结构】综合性

    • 兼具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=vVdegree(v)=n1=Θ(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.路径 + 环路

pStgIB9.png

  • 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),...,(vk1,vk)}
  • 路径长度即所含边数:| π π π|=k

  • 环路(cycle/loop): v k = v 0 v_k=v_0 vk=v0(如果覆盖所有节点各一次,则称作周游(tour))

5.连通 + 无环

pStgXcD.png

  • 连通图:节点之间均有路径(connected) 不含环路,称作无环图(acyclic)

  • 树 = 无环连通图 = 极小连通图 = 极大无环图

  • 任一节点v与根之间存在唯一路径 path(v, r) = path(v)

  • 以|path(v)|为指标可对所有节点做等价类划分

6.深度 + 层次

pSt2KNq.png

  • 不致歧义时,路径、节点和子树可相互指代

    • 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.父节点

pSt23gU.pngpSt28vF.png
  • 除根外,任一节点有且仅有一个父节点

  • 节点组织为一个序列,各自记录:

    • data 本身信息
    • parent 父节点的秩或位置
  • 树根:R ~ parent(4) = 4

3.孩子节点

pSt20C6.pngpStRuse.png
  • 同一节点的所有孩子,各成一个序列

  • 各序列的长度,即对应节点的度数 孩子节点

  • 查找孩子很快,但parent()很慢

4.父节点 + 孩子节点

pSt2TKg.png

三、有根有序树 = 二叉树

1.二叉树

  • 二叉树:节点度数不超过2

pStRCqJ.png

  • 孩子(子树)可以左、右区分(隐含有序)
    • lc() ~ lSubtree()
    • rc() ~ rSubtree()

pStRFaR.png

2.描绘多叉树

2.1 长子-兄弟表示法

  • 有根且有序的多叉树,均可转化并表示为二叉树

  • 长子 ~ 左孩子 firstChild() ~ lc()

  • 兄弟 ~ 右孩子 nextSibling() ~ rc()

[pStRZRK.png]pStRKqH.pngYTejZ.png

2.2 基数:设度数为0、1和2的节点,各有 n 0 、 n 1 和 n 2 n_0 、n_1和n_2 n0n1n2

  • 边数 e = n − 1 = n 1 + 2 n 2 e = n − 1 = n_1 + 2n_2 e=n1=n1+2n2
    • 1/2度节点各对应于1/2条入边

YTzeJ.png

  • 叶节点数 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同步递增 n1n0无关: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=2n2n0=n2+1=(n+1)/2,此时,节点度数均为偶数,不含单分支节点

YTD61.png

2.3 满树
YTaUn.png

  • 深度为k的节点,至多 2 k 2^k 2k

  • n个节点、高h的二叉树满足 h + 1 ≤ n ≤ 2 h + 1 − 1 h+1≤n≤2^{h+1}-1 h+1n2h+11

  • 特殊情况

    • 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)

YT4il.png

  • 验证:如此转换之后,全树自身的复杂度并未实质增加

四、二叉树实现

1.BinNode模板类

YT7F7.png

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接口实现

YTMy2.png

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.节点插入

YTOYP.png

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.子树接入

YTkpD.png

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 LxR

YWheL.png

  • 遍历:结果 ~ 过程 ~ 次序 ~ 策略

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,允许的递归深度有限
    YWC6t.png

1.3 观察
YWqbq.png

1.4 藤缠树

  • 沿着左侧藤,整个遍历过程可分解为:

    • 自上而下访问藤上节点,再自下而上遍历各右子树
  • 各右子树的遍历彼此独立自成一个子任务

YWvim.png

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) 
}

Y2Vq4.png

六、中序遍历

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)

Y2dEW.png

1.2 观察

Y2fZG.png

1.3 藤缠树

  • 沿着左侧藤,遍历可自底而上分解为d+1步迭代:访问藤上节点,再遍历其右子树

  • 各右子树的遍历彼此独立,自成一个子任务

Y2r1z.png

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; //再转向其右子树(可能为空,留意处理手法) 
    } 
}

Y2yr5.png

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() )

Y2J48.png

  • 直接后继
//在中序遍历意义下的直接后继 
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)

Y2sKU.png

七、后序遍历

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)

Y2Ejw.png

1.2 藤缠树

  • 从根出发下行,尽可能沿左分支 实不得已,才沿右分支

  • 最后一个节点必是叶子,而且是按中序遍历次序最靠左者,也是递归版中visit()首次执行处

Y2ULZ.png

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.实例

Y2tvJ.png

4.分析

4.1 正确性:数学归纳

  • 每个节点出栈后,以之为根的子树已经完全遍历,而且其右兄弟r若存在,必恰在栈顶
  • 此时正可以开始遍历子树r 故只需从r出发

Y25Zn.png

4.2 效率:分摊分析

  • 是否O(n),取决于以下条件

    • 每次迭代,都有一个节点出栈并被访问 //满足
    • 每个节点入栈一次且仅一次 //满足
    • 每次迭代只需O(1)时间 //不再满足
  • 单次调用goAlongVine()就可能需做Ω(n)次入栈操作,共需Ω(n)时间

5.表达式树

Y2jFl.png

八、层次遍历

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 ); //右孩子入队 
    } 
}

Y2Tr7.png

2.完全二叉树

2.1 完全二叉树 ~ 紧凑表示 ~ 以向量实现

  • 叶节点仅限于最低两层,底层叶子,均居于次底层叶子左侧(相对于LCA),除末节点的父亲,内部节点均有双子
  • 叶节点不致少于内部节点,但至多多出一个

Y2WR2.png

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次

Y2XpP.png

2.4 完全 ~ 满
Y29KD.png

九、重构

1.[ 先序 | 后序 ] + 中序

Y2Rjj.png

2.增强序列

  • 假想地认为,每个NULL也是“真实”节点,并在遍历时一并输出 每次递归返回,同时输出一个事先约定的元字符“^”

  • 若将遍历序列表示为一个Iterator,则可将其定义为 Vector< BinNode * >,于是在增强的遍历序列中,这类“节点”可统一记作NULL

  • 可归纳证明:在增强的先序、中序、后序遍历序列中任一子树依然对应于一个子序列,而且其中的NULL节点恰比非NULL节点多一个

  • 如此,通过对增强序列分而治之,即可重构原树

Y2YeS.png

Y2ovL.png
时空性能 + 稳定性

十、Huffman编码树

1.算法

1.1 编码

  • 通讯 / 编码 / 译码

  • 二进制编码

    • 组成数据文件的字符来自字符集 ∑ \sum
    • 字符被赋予互异的二进制串
  • 文件的大小取决于:字符的数量 x 各字符编码的长短
    Y2mys.png

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)

Y2xYQ.png

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)=xdepth(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 vTopt,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

亦即,叶子只能出现在倒数两层以内——否则,通过节点交换即可

Y28p3.png

1.5 字符频率

  • 实际上,字符的出现概率或频度不尽相同 甚至,往往相差极大

Y2LPy.png

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)
  • 此时,完全树未必就是最优编码树

Y2D6d.png

1.7 最优带权编码树

  • 同样,频率高/低的(超)字符,应尽可能放在高/低处
  • 故此,通过适当交换,同样可以缩短wald(T)

Y2aU4.png

1.8 Huffman算法

  • 贪婪策略:频率低的字符优先引入,位置亦更低

Y24iW.png

为每个字符创建一棵单节点的树,组成森林F 
按照出现频率,对所有树排序 
while ( F中的树不止一棵 ) 
    取出频率最小的两棵树:T1 T2 将它们合并成一棵新树T,并令: 
        lc(T) = T1rc(T) = T2 
        w( root(T) ) = w( root(T1) ) + w( root(T2) )
  • 尽管贪心策略未必总能得到最优解,但非常幸运,如上算法的确能够得到最优编码树之一

2.正确性

2.1 双子性

  • 最优编码树的特征
    • 首先,每一内部节点都有两个孩子——节点度数均为偶数(0或2),即真二叉树
    • 否则,将1度节点替换为其唯一的孩子,则新树的wald将更小

Y27QG.png

2.2 不唯一性

  • 对任一内部节点而言 左、右子树互换之后wald不变

  • 上述算法中,兄弟子树的次序系随机选取

  • 为消除这种歧义,可以(比如)明确要求左子树的频率更低

Y2Myz.png

2.3 层次性

  • 出现频率最低的字符 x 和 y ,必在某棵最优编码树中处于最底层,且互为兄弟
  • 否则,任取一棵最优编码树,并在其最底层任取一对兄弟 a 和 b 于是, a 和 x 、 b 和 y 交换之后,wald绝不会增加

Y2OY5.png

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,取中频率最低的xy(不妨就设二者互为兄弟)
  • ∑ ′ = ( ∑ \sum'=(\sum =({x,y}) ∪ ∪ {z},w(z)=w(x)+w(y)

Y2k76.png

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 TTwd之差与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 数据结构与算法
Y9FP8.png

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)
Y9QTU.pngY9hzw.png

十一、下界

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),每一可能的输出,都对应于至少一个叶节点 每一次运行过程,都对应于起始于根的某条路径

Y23Ut.png

1.5 代数判定树

  • 决策树

    • 针对“比较-判定”式算法的计算模型
    • 给定输入的规模,将所有可能的输入 所对应的一系列判断表示出来
  • 代数判定:

    • 使用某一常次数代数多项式 将任意一组关键码做为变量,对多项式求值
    • 根据结果的符号,确定算法推进方向
  • 比较树(Comparison Tree):最简单的ADT,二元一次多项式,形如: k i − k j k_i-k_j kikj

Y2Iiq.png

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) log3Nlog3n!=log3e[nlnnn+O(lnn)]=Ω(nlogn)

2.归约

  • 线性归约(Linear-Time Reduction)
    • 除了(代数)判定树,归约(reduction)也是确定下界的有力工具

Y2iFm.png

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值