清华邓俊辉数据结构学习笔记(3) - 二叉树、图

第五章 二叉树

(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] = ∅

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值