文章目录

  • 一、树
    • 1.动机
    • 2.有根树
    • 3.有序树
    • 4.路径 + 环路
    • 5.连通 + 无环
    • 6.深度 + 层次
  • 二、树的表示
    • 1.接口
    • 2.父节点
    • 3.孩子节点
    • 4.父节点 + 孩子节点
  • 三、有根有序树 = 二叉树
    • 1.二叉树
    • 2.描绘多叉树
  • 四、二叉树实现
    • 1.BinNode模板类
    • 2.BinNode接口实现
    • 3.BinTree模板类
    • 4.节点插入
    • 5.子树接入
    • 6.高度更新
    • 7.子树删除
    • 8.子树分离
  • 五、先序遍历
    • 1.递归实现
    • 2.迭代算法
  • 六、中序遍历
    • 1.递归实现
    • 2.迭代算法
    • 3.分析
    • 4.后继与前驱
  • 七、后序遍历
    • 1.观察
    • 2.迭代算法
    • 3.实例
    • 4.分析
    • 5.表达式树
  • 八、层次遍历
    • 1.算法及分析
    • 2.完全二叉树
  • 九、重构
    • 1.[ 先序 | 后序 ] + 中序
    • 2.增强序列
  • 十、Huffman编码树
    • 1.算法
    • 2.正确性
    • 3.算法实现
    • 4.改进
  • 十一、下界
    • 1.代数判定树
    • 2.归约

一、树

1.动机

  • 层次结构的表示

    • 表达式
    • 文件系统
    • URL

  • 【数据结构】综合性

    • 兼具Vector和List的优点
    • 兼顾高效的查找、插入、删除
  • 【半线性】

    • 不再是简单的线性结构,在确定某种次序之后,具有线性特征

2.有根树

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gqR0OiOM-1674651834780)(null)]

  • 树是极小连通图、极大无环图T=(V;E):节点数n=|V|,边数e=|E|

  • 指定任一节点r∈V作为根后,T即称作有根树

  • 若T1,T2,T3,...,Td为有根树,则T=((UiVi)若T_1,T_2,T_3,...,T_d为有根树,则T=((U_iV_i)若T1​,T2​,T3​,...,Td​为有根树,则T=((Ui​Vi​)∪{r}, (UiEi)∪(U_iE_i)∪(Ui​Ei​)∪{<r,ri><r,r_i><r,ri​> | 1≤i≤d}

  • 相对于T,Ti称作以riT_i称作以r_iTi​称作以ri​为根的子树(subtree rooted at rir_iri​),记Ti=subtree(ri)T_i=subtree(r_i)Ti​=subtree(ri​)

3.有序树

  • 称作r的孩子(child),ri之间互称兄弟(sibling),r为其父亲(parent),d=degree(r)为r的(出)度(degree)称作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∈Vdegree(v)=n−1=Θ(n)e=\sum_{v∈V}degree(v)=n-1=Θ(n)e=∑v∈V​degree(v)=n−1=Θ(n),故在衡量相关复杂度时,可以n作为参照

  • 若指定Ti作为T的第i棵子树,ri作为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)

    • πππ={(v0,v1),(v1,v2),...,(vk−1,vk)(v_0,v_1),(v_1,v_2),...,(v_k-1,v_k)(v0​,v1​),(v1​,v2​),...,(vk​−1,vk​)}
  • 路径长度即所含边数:|πππ|=k

  • 环路(cycle/loop):vk=v0v_k=v_0vk​=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的节点,各有n0、n1和n2n_0 、n_1和n_2n0​、n1​和n2​个

  • 边数 e=n−1=n1+2n2e = n − 1 = n_1 + 2n_2e=n−1=n1​+2n2​

    • 1/2度节点各对应于1/2条入边

  • 叶节点数 n0=n2+1n_0 = n_2 + 1n0​=n2​+1

    • n1与n0无关:h=0时,1=0+1;此后,n+0与随n2同步递增n_1与n_0无关:h = 0时,1 = 0 + 1;此后,n+0与随n_2同步递增n1​与n0​无关:h=0时,1=0+1;此后,n+0与随n2​同步递增
  • 节点数n=n0+n1+n2=1+n1+2n2n = n_0 + n_1 + n_2 = 1 + n_1 + 2n_2n=n0​+n1​+n2​=1+n1​+2n2​

  • 特别地,当n1=0时,有e=2n2和n0=n2+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的节点,至多2k2^k2k个

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

  • 特殊情况

    • n = h + 1:退化为一条单链
    • n = 2h+12^{h+1}2h+1 - 1:即所谓满二叉树

2.4 真二叉树

  • 通过引入n1+2n0n_1 + 2n_0n1​+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∪RL∪x∪RL∪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的编码串rps(v(x))=rps(x)rps(v(x))=rps(x)rps(v(x))=rps(x),由根到v(x)的通路(root path)确定

  • 字符编码不必等长,而且同字符的编码互不为前缀,故不致歧义 (Prefix-Free Code)

1.3 编码长度vs.叶节点平均深度

  • 平均编码长度ald(T)=∑x∈∑depth(v(x))/∣∑∣ald(T)=\sum_{x∈\sum}depth(v(x))/|\sum|ald(T)=∑x∈∑​depth(v(x))/∣∑∣

  • 对于特定的\sum,ald()最小者即为最优编码树ToptT_{opt}Topt​

1.4 最优编码树

∀v∈Topt,deg(v)=0∀ v∈T_{opt},deg(v)=0∀v∈Topt​,deg(v)=0 only if depth(v)≥depth(Topt)−1depth(v)≥depth(T_{opt})-1depth(v)≥depth(Topt​)−1

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

1.5 字符频率

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

1.6 带权编码长度 vs. 叶节点平均带权深度

  • 文件长度∝平均带权深度wald(T)=∑xrps(x)wald(T)=\sum_{x}rps(x)wald(T)=∑x​rps(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 数学归纳

  • 对∣∑∣做归纳可证:Huffman算法所生成的,必是一棵最优编码树!∣∑∣=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,且wd(T)−wd(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′,wd之差与TT和T',wd之差与TT和T′,wd之差与T的具体形态无关

2.6 从最优,到最优

  • 因此,只要T′是∑′的最优编码树,则T也必是∑的最优编码树(之一)因此,只要T'是\sum'的最优编码树,则T也必是\sum的最优编码树(之一)因此,只要T′是∑′的最优编码树,则T也必是∑的最优编码树(之一)

  • 实际上,Huffman算法的过程,与上述归纳过程完全一致——每一步迭代都可视作,从某棵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(n2)O(n^2)O(n2))

    • 初始化时,通过排序得到一个非升序向量 //O(nlog⁡n)O(n\log n)O(nlogn)
    • 每次(从后端)取出频率最低的两个节点 //O(1)
    • 将合并得到的新树插入向量,并保持有序 //O(n)
  • 方案2: (O(n2)O(n^2)O(n2))
    • 初始化时,通过排序得到一个非降序列表 //O(nlog⁡n)O(n\log n)O(nlogn)
    • 每次(从前端)取出频率最低的两个节点 //O(1)
    • 将合并得到的新树插入列表,并保持有序 //O(n)
  • 方案3: (O(nlog⁡n))(O(n\log n))(O(nlogn))
    • 初始化时,将所有树组织为一个优先级队列(第12章)//O(n) O(nlogn)
    • 取出频率最低的两个节点,合并得到的新树插入队列 /(O(nlog⁡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,二元一次多项式,形如:ki−kjk_i-k_jki​−kj​

1.6 下界:Ω(nlogn)

  • 比较树是三叉树(ternary tree),内部节点至多三个分支(+|0|-)

  • 每一叶节点,各对应于

    • 起自根节点的一条通路
    • 某一可能的运行过程
    • 运行所得的输出
  • 叶节点深度 ~ 比较次数 ~ 计算成本

  • 树高 ~ 最坏情况时的计算成本

  • 树高的下界 ~ 所有CBA的时间复杂度下界

  • 对于排序算法所对应的ADT,必有N≥n!

    • ADT的每一输出(叶子),对应于某一置换 依此置换,可将输入序列转换为有序序列
    • 算法的输出,须覆盖所有可能的输入
  • 包含N个叶节点的排序算法ADT,高度不低于log⁡3N≥log⁡3n!=log⁡3e⋅[nln⁡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)log3​N≥log3​n!=log3​e⋅[nlnn−n+O(lnn)]=Ω(n⋅logn)

2.归约

  • 线性归约(Linear-Time Reduction)

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

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

  1. ES6基础4(数据结构)-学习笔记

    文章目录 ES6基础4(数据结构)-学习笔记 set map symbol ES6基础4(数据结构)-学习笔记 set //set 数据结构 类似数组 成员信息唯一性var s = new Set() ...

  2. 数据结构专题-学习笔记:李超线段树

    数据结构专题 - 学习笔记:李超线段树 1. 前言 2. 详解 3. 应用 4. 总结 5. 参考资料 1. 前言 本篇博文是博主学习李超线段树的学习笔记. 2020/12/21 的时候我在 线段树算 ...

  3. python的基本数据结构_Python学习笔记——基本数据结构

    列表list List是python的一个内置动态数组对象,它的基本使用方式如下: shoplist = ['apple', 'mango', 'carrot', 'banana'] print 'I ...

  4. python的基本数据结构_python学习笔记-基本数据结构

    Python 学习笔记-1 写在最前面,因为组内小伙伴要走,生信团队由原来的7个人,慢慢的变的只有我一个人了,需要紧急突击下python,因为有python的流程要交接维护 python 基本情况 代 ...

  5. 邓公数据结构C++语言版学习笔记——二叉树

    二叉树的遍历 一. preorder--先序遍历VLR 1. 递归先序遍历 2. 迭代先序遍历 3.先序遍历图解 二. inorder--先序遍历LVR 1. 递归中序遍历 2.迭代中序遍历 3.迭代 ...

  6. java数据结构学习笔记-二叉树前、中、后序遍历

    public class BinaryTreeDemo {public static void main(String args[]){Employee emp1= new Employee(1,&q ...

  7. 数据结构课程学习笔记

    整理一下上数据结构课记录的笔记. 第一章 绪论 1.1 数据结构的基本概念 1.2 算法的基本概念 1.2.1 时间复杂度 事前预估算法时间开销T(n)与问题规模n的关系.分析算法操作的执行次数x和问 ...

  8. 《数据结构》学习笔记一:绪论

    个人看法: 数据结构的重要性对于码农而言就像盖房子的图纸,做饭的菜谱,没有它,也许也能盖得成房子,也能做的熟菜,但是质量如何就不敢说了.我们从大学的时候就把<数据结构>作为重要的基础课程来 ...

  9. B站-王卓-数据结构课程-学习笔记

    使用C++语言实现B站王卓老师的数据结构公开课课程代码 使用说明 1-基础概念 思维导图笔记 很多人找我要思维导图,我在下载了3种不同格式的笔记(.pos .xmind .mm) 大家可以去我的Git ...

最新文章

  1. 京东运营插件_技术中台产品经理必知的那些易混词儿(1):组件、套件、 中间件、插件……...
  2. Python从入门到入土-Python3 File(文件) 方法
  3. linux的memmap函数_linux /proc下的statm、maps、memmap 内存信息文件分析
  4. Android持久化存储(1)文件存储
  5. Servlet使用适配器模式进行增删改查案例(IEmpService.java)
  6. Storm编程模型总结
  7. ubuntu20.10创建QT应用程序快捷方式 Terminal中输入命令直接打开QtCreator
  8. (王道408考研数据结构)第六章图-第四节7:关键路径(最早发生时间、最迟发生时间)
  9. 第1关:HDFS的基本操作
  10. 1、环境搭建、Helloworld
  11. 数据库分表处理设计思想和实现
  12. 【劲峰论道时空分析技术-学习笔记】3 时空演化树
  13. Windows Phone 学习 Web搜索组件
  14. cad计算机画图标准,CAD画图某些常用尺寸及作图习惯
  15. 浏览器语音附加背景音乐
  16. 短视频剪辑APP开发快速开发
  17. [转]Windows服务“允许服务与桌面交互”的使用和修改方法
  18. 上位机通信标准-OPC
  19. android 360度全景,android 360度全景展示
  20. 【Flutter】Flutter 应用生命周期 ( 前台状态 resumed | 后台状态 paused | 非活动状态 inactive | 组件分离状态 detached )

热门文章

  1. 3.计量模型的基础分析流程(数据分析学习DAY4)
  2. Android列表滑动卡顿分析与优化
  3. 清华北大「世界排名断崖式下跌」?
  4. 电磁明渠流量计测流系统
  5. 工具网站推荐 - 最好的8个免费下载Pbr贴图和材质的网站
  6. MOS管之开关控制与信号控制
  7. 磁盘无法挂载超过2T大小磁盘解决方法
  8. PPQ库中KLD算法实现代码解析
  9. 打cf提示计算机内存不足怎么办,win7中cf内存不足闪退怎么办?-处理win7下cf提示“内存不足”的方法 - 河东软件园...
  10. linux下 openssl证书签发