第五章 二叉树
(a)树
树能够结合向量的优点(search)和列表的优点(insert、remove),构成List< List >。
有根树
树是特殊的图 T = (V, E),节点数 |V| = n,边数 |E| = e,指定任一节点 r ∈ V作为根后,T即称作有根树。
若T1,T2,…,Td为有根树,则 T = ( (∪Vi) ∪ {r}, (∪Ei) ∪ { <r, ri> | 1 ≤ i ≤ d }) 也是。相对于T,Ti称作以ri为根的子树,记作Ti = subtree(ri)。
有序树
ri称作r的孩子,ri之间互称兄弟,r为其父亲,d=degree( r )为r的(出)度,可归纳证明:e = ∑degree( r ) = n - 1 = O(n),故在衡量复杂度时,可以n为参照,若指定Ti作为T的第i棵子树,ri作为r的第i个孩子,则T称作有序树。
路径+环路
V中的k+1个节点,通过E中的k条边依次相联,构成一条路径/通路,π = {(V0, V1), (V1, V2), …, (Vk-1,Vk)},路径长度 |π| = 边数 = k,对于环路vk = v0。
连通+无环
节点之间均有路径称作连通图,不含环路称为无环图。树是无环连通图、极小连通图、极大无环图,故任一节点v与根之间存在唯一路径 path(v, r) = path(v)。
深度+层次
不致歧义时,路径、节点和子树可相互指代:path(v) ~ v ~ subtree(v)
v的深度:depth(v) = | path(v) |
path(v)上节点,均为v的祖先,v是他们的后代,其中除自身以外是真祖先/后代。半线性:在任一深度v的祖先/后代若存在,则必然/未必唯一。
根节点是所有节点的公共祖先,深度为0。没有祖先的节点称作叶子,所有叶子深度中的最大者称作(子)树(根)的高度 height(v) = height( subtree(v) ),空树的高度取作-1。
depth(v) + height(v) ≤ height(T)
(b)树的表示
接口
root() //根节点
parent() //父节点
firstChild() //长子
nextSibling() //兄弟
insert(i,e) //将e作为第i个孩子插入
remove(i) //删除第i个孩子(及其后代)
traverse() //遍历
父节点
观察:除根外任一节点有且仅有一个父节点,因此构思将节点组织为序列,各节点分别记录data(本身信息)、parent(父节点的秩或位置)。
空间性能:O(n) (两列表格)
时间性能:parent():O(1)
root():O(n)或O(1)
firstChild():O(n)
nextSibling():O(n)
孩子节点
观察:任一节点的孩子,O(1)时间。
父节点+孩子节点
∑ di = e = n - 1,每个节点设两个引用 firstChild()和nextSibling()
(c)二叉树概述
节点度数不超过2的树称作二叉树,同一节点的孩子和子树,均以左、右区分 lChild() ~ lSubtree(), rChild() ~ rSubtree()。
基数:深度为k的节点,至多2^k个,含n个节点、高度为h的二叉树中 h < n < 2^(h+1)。
描述多叉树:在有根且有序时,多叉树可转化并表示为二叉树,利用长子兄弟表示法+旋转。
(d)二叉树实现
BinNode模板类
#define BinNodePosi(T) BinNode<T>* //节点位置
template <typename T> struct BinNode{
BinNodePosi(T) parent, lChild, rChild; //父亲、孩子
T data; int height; int size(); //高度、子树规模
BinNodePosi(T) insertAsLC( T const & ); //作为左孩子插入新节点
BinNodePosi(T) insertAsRC( T const & ); //作为右孩子插入新节点
BinNodePosi(T) succ(); //(中序遍历意义下)当前节点的直接后继
template <typename VST> void traLevel(VST &); //子树层次遍历
template <typename VST> void travPre(VST &); //子树先序遍历
template <typename VST> void travIn(VST &); //子树中序遍历
template <typename VST> void travPost(VST &); //子树后序遍历
}
BinNode接口实现
template <typename T> BinNodePosi(T) BinNode<T>::insertAsLC(T const & e)
{return lChild = new BinNode( e, this);}
template <typename T> BinNodePosi(T) BinNode<T>::insertAsRC(T const & e)
{return rChild = new BinNode( e, this);}
template <typename T> int BinNode<T>::size(){//后代总数,以其为根的子树的规模
int s = 1; //计入本身
if (lChild) s += lChild->size(); //递归计入左子树规模
if (rChild) s += rChild->size(); //递归计入右子树规模
return s;
}//O(n=|size|)
BinTree模板类
template <typename T> class BinTree{
protected:
int _size;//规模
BinNodePosi(T) _root; //根节点
virtual int updateHeight( BinNodePosi(T) x); //更新节点x的高度
void updateHeightAbove( BinNodePosi(T) x); //更新x及祖先的高度
public:
int size() const { return _size; } //规模
bool empty() const { return !_root;} //判空
BinNodePosi(T) root() const {return _root; } //树根
/*...子树接入、删除和分离接口...*/
/*...遍历接口...*/
}
高度更新
#define stature(p) ((p)? (p)->height : -1) //节点高度——约定空树高度为-1
template <typename T> int BinTree<T>::updateHeight(BinNodePosi(T) x){
return x->height = 1 + max( stature( x->lChild ), stature( x->rChild ));
} //此处采用常规二叉树规则,O(1)
template <typename T> void BinTree<T>::updateHeightAbove( BinNodePosi(T) x){
while (x) //可优化:一旦高度未变,即可终止
{ updateHeight(x); x = x->parent;}
} //O(n=depth(x))
节点插入
template <typename T> BinNodePosi(T)
BinTree<T>::insertAsRC( BinNodePosi(T) x, T const & e){//insertAsLC对称
_size++; x->insertAsRC(e); //x的祖先高度可能增加,其余节点必然不变
updateHeightAbove(x);
return x->rChild;
}
(e1)先序遍历
遍历:按照某种次序访问树中各节点,每个节点被访问恰好一次,T = V ∪ L ∪ R 。
先序遍历: V L R
中序遍历: L V R
后续遍历: L R V
层次遍历(广度): 自上而下,先左后右
递归实现(先序遍历)
template <typename T,typename VST> void traverse(BinNodePosi(T) x, VST & visit){
if (!x) return;
visit(x->data);
traverse(x->lChild,visit);
traverse(x->rChild,visit);
}//T(n)=O(1)+T(a)+T(n-a-1)=O(n)
迭代1:实现(先序遍历)
template <typename T,typename VST> void travPre_I1(BinNodePosi(T) x, VST & visit){
Stack <BinNodePosi(T)> S; //辅助栈
if (x) S.push(x); //根节点入栈
while(!S.empty()){
x = S.pop(); visit(x->data);
if (HasRChild( *x )) S.push(x->rChild); //右孩子先入后出
if (HasLChild( *x )) S.push(x->lChild); //左孩子后入先出
}
}
迭代2(先序遍历)
思路:沿最左支从顶向下访问到最深节点,再子底向上访问右子枝。
实现
template <typename T, typename VST> //分摊O(1)
static void vistAlongLeftBranch(BinNodePosi(T) x, VST & visit, Stack <BinNodePosi(T)> & S){
while(x){//反复地
visit(x->data); //访问当前节点
S.push(x->rChild); //右孩子入栈,将来逆序出栈
x=x->lChild; //沿左侧链下行
}//只有右孩子,NULL可能入栈
}
template <typename T, typename VST> void travPre_I2(BinNodePosi(T) x, VST & visit){
Stack <BinNodePosi(T) S>; //辅助栈
while(true){
visitAlongLeftBranch(x,visit,S);//访问子树x的左侧链,右子树入栈缓冲
if (S.empty()) break; //栈空即退出
x = S.pop(); //弹出下一子树的根
} //#pop=#push=#visit=O(n)=分摊O(1)
}
(e2)中序遍历
递归实现(中序遍历)
template <typename T, typename VST> void traverse(BinNodePosi(T) x, VST & visit){
if(!x) return;
traverse(x->lChild,visit);
visit(x->data);
traverse(x-rChild,visit);
}//T(n)=T(a)+O(1)+T(n-a-1)=O(n)
观察:从根出发沿左分支下行,直到最深的节点,它是全局最先被访问者。
实现
template <typename T> staic void goAlongLeftBranch(BinNodePosi(T) x, Stack <BinNodePosi(T)> & S)
{ while (x) {S.push(x); x=x->lChild;}} //反复入栈,沿左分支深入
template <typename T, typename V> void travIn_I1( BinNodePosi(T) x, V& visit){
Stack <BinNodePosi(T)> S; //辅助栈
while (true) {//反复地
goAlongLeftBranch(x,S); //从当前节点出发,逐批入栈
if (S.empty()) break; //直至所有节点处理完毕
x = S.pop(); //x的左子树为空,或已遍历(等效为空),所以可以立刻访问
visit( x->data );
x = x->rChild; //转向其右子树
}
}
(e3)层次遍历
实现
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->lChild ); //左孩子入队
if ( HasRChild(*x)) Q.enqueue( x->rChild ); //右孩子入队
}
}
(e4)重构
第六章 图
(a)基本术语
G = ( V; E ) vertex: n = |V| edge | arc: e = |E|
相邻的v~v:adjacency邻接
相邻的v~e:incidence关联
无向图/有向图:若邻接顶点u和v的次序无所谓,则 (u, v)为无向边(例:若u是v的好友,则v也是u的好友),所有边均无方向的图为无向图。反之,有向图中均为有向边,u、v分别称作(u,v)的尾、头。
路径/环路:路径 π = <v0, v1, …, vk>,长度 |π| = k。
简单路径:vi = vj 除非 i = j。
环/环路:v0 = vk。
有向无环图(DAG)
(b1)邻接矩阵
Graph模板类
template <typename Tv, typename Te> class Graph{//顶点类型、边类型
private:
void reset(){//所有顶点、边的辅助信息复位
for (int i = 0; i < n; i++){//顶点
status(i) = UNDISCOVERED; dTime(i) = fTime(i) = -1;
parent(i) = -1; priority(i) = INT_MAX;
for (int j = 0; j < n; j++) //边
if (exists(i,j)) status(i,j)=UNDETERMINED;
}
}
public: /*...顶点操作、边操作、图算法:无论如何实现,接口必须统一*/
}
邻接矩阵与关联矩阵
w( i, j ) = 1 / 0 (无向图的邻接矩阵是对称的)
Vertex
typedef enum { UNDISCOVERED, DISCOVERED, VISITED} Vstatus;
template <typename Tv> struct Vertex{//顶点对象(并未严格封装)
Tv data; int inDegree, outDegree; //数据、出入度数
Vstatus status; //状态
int dTime, fTime; //时间标签
int parent; //遍历树中的父节点
int priority; //遍历树中的优先级(最短通路、极短跨边等)
Vertex( Tv const & d): //构造新顶点
data(d), inDegree(0), outDegree(0), status(UNDISCOVERED), dTime(-1), fTime(-1), parent(-1), priority(INT_MAX){}
};
Edge
typedef enum { UNDETERMIINED, TREE, CROSS, FORWARD, BACKWARD} EStatus;
template <typename Te> struct Edge{ //边对象(并未严格封装)
Te data; //数据
int weight; //权重
EStatus status; //类型
Edge(Te const & d, int w): //构造新边
data(d),weight(w),status(UNDETERMINED){}
};
GraphMatrix
template <typename Tv, typename Te> class GraphMatrix: public Graph<Tv, Te>{
private:
Vector< Vertex<Tv> > V; //顶点集
Vector< Vector< Edge<Te>* > > E; //边集
public:
/*操作接口:顶点相关、边相关、...*/
GraphMatrix() { n = e = 0; } //构造
~GraphMatrix() {//析构
for (int j = 0; j < n; j++)
for (int k = 0; k < n; k++)
delete E[j][k]; //清除所有动态申请的边记录
}
}
顶点操作
Tv & vertex(int i){ return V[i].data; } //数据
int inDegree(int i){ return V[i].inDegree;} //入度
int outDegree(int i){ return V[i].outDegree;} //出度
Vstatus & status(int i){ return V[i].status;} //状态
int & dTime(int i){return V[i].dTime;} //时间标签dTime
int & fTime(int i){return V[i].fTime;} //时间标签fTime
int & parent(int i){return V[i].parent;} //在遍历树中的父亲
int & priority(int i){return V[i].priority;} //优先级数
对于任意顶点i,如何枚举其所有的邻接顶点neighbor?
int nextNbr(int i, int j){//若已枚举至邻居j,则转向下一邻居
while ( (-1 < j ) && !exits(i,j--) ); //逆向顺序查找:O(n)
return j;
} //改用邻接表可提高至O(1+outDegree(i))
int firstNbr(int i){
return nextNbr(i,n);
}//首个邻居
边操作
bool exists(int i, int j) {//判断边(i,j)是否存在
return ( 0 <= i ) && ( i<n ) && ( 0 <= j ) && (j < n ) && E[i][j]!=NULL; //短路求值
} //以下假定exists(i,j)
Te & edge(int i, int j) //边(i,j)的数据
{return E[i][j]->data;} //O(1)
Estatus & status(int i, int j) //边(i,j)的状态
{return E[i][j]->status;} //O(1)
int & weight(int i, int j) //边(i,j)的权重
{return E[i][j]->weight;} //O(1)
边插入
void insert(Te const& edge, int w, int i, int j){//插入(i,j,w)
if (exists(i,j)) return; //忽略已有的边
E[i][j] = new Edge<Te>(edge, w); //创建新边
e++; //更新边计数
V[i].outDegree++; //更新关联顶点i的出度
V[j].inDegree++; //更新关联顶点j的出度
}
边删除
Te remove(int i, int j){//删除顶点i和j之间的联边(exists(i,j))
Te eBak = edge(i,j); //备份边(i,j)的信息
delete E[i][j]; E[i][j] = NULL; //删除边(i,j)
e--; //更新边计数
V[i].outDegree--; //更新关联顶点i的出度
V[j].inDegree--; //更新关联顶点j的入度
return eBak; //返回被删除边的信息
}
顶点插入
int insert(Tv const & vertex){//插入顶点,返回编号
for (int j = 0; j < n; j++) E[j].insert(NULL);n++;
E.insert( Vector< Edge<Te>* >(n, n, NULL));
return V.insert( Vertex<Tv>(vertex) ); //插入顶点,边的二维规模都增加
}
顶点删除
Tv remove(int i){//删除顶点及其关联边,返回该顶点信息
for (int j = 0; j < n; j++)
if (exists(i,j)) //删除所有出边
{delete E[i][j]; V[j].inDegree--;}
E.remove(i);n--; //删除第i行
for (int j = 0; j < n; j++)
if (exists(j,i)) //删除所有入边及第i列
{delete E[j].remove(i); V[j].outDegree--;}
Tv vBak = vertex(i); //备份顶点i的信息
V.remove(i); //删除顶点i
return vBak; //返回被删除顶点的信息
}
优点:(1)直观,易于理解和实现;(2)适用范围广泛:digraph / network / cyclic / …,尤其适用于稠密图;(3)判断两点之间是否存在联边:O(1);(4)获取顶点的(出/入)度数:O(1),添加、删除边后更新度数:O(1);(5)拓展性:得益于Vector良好的空间控制策略,空间溢出等情况可以“透明地”予以处理。
平面图:可嵌入于平面的图,符合欧拉公式 v - e + f - c = 1,此时空间利用率 = 1/n。
(c)广度优先搜索
图→树→序列
始自顶点s的广度优先搜索:(1)访问顶点s;(2)依次访问s所有尚未访问的邻接顶点;(3)依次访问它们尚未访问的邻接顶点,如此反复,直至没有尚未访问的邻接顶点。
Graph::BFS()
template <typename Tv, typename Te> //顶点类型、边类型
void Graph<Tv, Te>::BFS( int v, int & clock){
Queue<int> Q; status(v) = DISCOVERED; Q.enqueue(v); //初始化O(n+e)
while (!Q.empty()){//反复地
int v = Q.dequeue();
for ( int u = firstNbr(v); -1 < u; u = nextNbr(v,u))//考查v的每一邻居u
/*...视u的状态分别处理...*/
if( UNDISCOVERED == status(u) ){//若u尚未被发现,则
status(u) = DISCOVERED; Q.enqueue(u); //发现该顶点
status(v, u) = TREE; parent(u) = v; //引入树边
}else //若u已被发现(正在队列中),或者已经访问完毕
status(v, u) = CROSS; //将(v,u)归类于跨边
status(v) = VISITED; //至此当前顶点访问完毕
}
}
Graph::bfs()
template <typename Tv, typename Te> //顶点类型、边类型
void Graph<Tv,Te>::bfs(int s){//s为起始顶点
reset(); int clock = 0; int v = s; //初始化 O(n+e)
do
if ( UNDISCOVERED == status(v)) //累积O(n)
BFS(v,clock); //即从该顶点出发启动一次BFS
while ( s != ( v = ( ++v % n) ) ) //按序号访问,故不漏不重
}//无论共有多少连通/可达分量...
优点:(1)连续、规则、紧凑的组织形式,利于高速缓冲机制发挥作用;(2)存储级别之间巨大的速度差异,在实际应用中往往更为举足轻重。
(d)深度优先搜索
DFS(s)(始自顶点s的深度优先搜索):访问顶点s,若s尚有未被访问的邻居,则任取其一,递归执行DFS(u),否则返回。
Graph::DFS()
template <typename Tv, typename Te> //顶点类型、边类型
void Graph<Tv, Te>::DFS(int v, int & clock){
dTime(v) = ++clock; status(v) = DISCOVERED; //发现当前顶点v
for (int u = firstNbr(v); -1<u; u = nextNbr(v, u)) //枚举v的每一邻居u
/*...视u的状态,分别处理...*/
/*...与BFS不同,含有递归...*/
switch ( status(u) ){
case UNDISCOVERED: //u尚未发现,意味着支撑树可在此拓展
status(v,u) = TREE; parent(u)=v; DFS(u,clock);break; //递归
case DISCOVERED: //u已被发现但尚未访问完毕,应属被后代指向的祖先
status(v,u) = BACKWARD;break;
default: //u已访问完(VISITED,有向图),则视承袭关系为前向边或跨边
status(v,u) = dTime(v) < dTime(u)?FORWARD:CROSS;break;
}//switch
status(v) = VISITED; fTime(v) = ++clock; //至此,当前顶点v访问完毕
}
括号引理
顶点的活动期:active[u] = ( dTime[u], fTime[u] )
给定有向图G = (V, E)及其任一DFS森林,则:
u是v的后代 iff active[u] ∈ active[v]
u是v的祖先 iff active[v] ∈ active[u]
u与v无关 iff active[u] ∩ active[v] = ∅