二叉树和其他树

截止目前,我们已经知道了线性数据结构(数组、链表、栈、队列)和表数据结构(哈希表),但是这些数据结构不适合具有层次结构的数据。

定义-1一棵树t是一个非空的有限元素的集合,其中一个元素为根,其余的元素组成t的子树。

另外,还需要了解的专有名词有:孩子节点、双亲节点、兄弟节点、祖先节点、子孙节点、内部节点、外部节点。

在这本书中,对深度、高度、级的定义是一致的,也都是从1开始。详细定义参考这里面的定义。

一个元素的度指的是其孩子的个数,比如叶节点的度就是0.一棵树的度是其元素的度的最大值。

二叉树

定义-2一棵二叉树t是有限个元素的集合(可以为空)。当二叉树为非空时,其中有一个元素为根,余下元素被划分成两个二叉树,分别称为t的左子树和右子树。

二叉树和树的根本区别是:

  1. 二叉树的每个元素都恰好有两棵子树,其中一个或两个可能为空。而树的每个元素可有任意数量的子树;
  2. 在二叉树中,每个元素的子树都是有序的,也就是说,有左子树和右子树之分。而树的子树是无序的。

二叉树的特性

这里的特性是基于根的深度为1,叶节点的高度为1。

特性-1 一棵二叉树有n个元素, n > 0 n\gt0 n>0,它有n-1条边。

特性-2 一棵二叉树的高度为h, h ≥ 0 h\ge0 h0,它最少有h个元素,最多有 2 h − 1 2^h-1 2h1个元素(满二叉树)。

特性-3 一棵二叉树有n个元素, n > 0 n\gt0 n>0,它的高度最大为n,最小高度为 ⌈ log ⁡ 2 ( n + 1 ) ⌉ \lceil\log_2(n+1)\rceil log2(n+1)(满二叉树)。

当高度为h的二叉树恰好有 2 h − 1 2^h-1 2h1个元素时,称其为满二叉树(1+2+4+…)。

关于完全二叉树的定义:对一棵满二叉树,从上到下,从左到右(广度优先的方式)进行编号,假设从满二叉树中删除k个其编号为 2 h − i 2^h-i 2hi元素,称所得到的树为完全二叉树。

特性-4 设完全二叉树的一元素其编号为i, 1 ≤ i ≤ n 1\le i\le n 1in。有以下关系成立:

  1. 如果 i = 1 i=1 i=1,则该元素为二叉树的根。若 i > 1 i\gt1 i>1,则其父节点的编号为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor i/2
  2. 如果 2 i > n 2i\gt n 2i>n,则该元素无做孩子。否则其左孩子的编号为2i;
  3. 如果 2 i + 1 > n 2i+1\gt n 2i+1>n,则该元素无右孩子。否则其右孩子的编号为2i+1。

二叉树的描述

数组描述

二叉树的数组表示利用了特性-4,把二叉树看做是缺少了部分元素的完全二叉树。

可以看出这种方法十分的浪费空间,对于有n个元素的二叉树,最多需要 2 n 2^n 2n个空间存储(考虑到数组的0位置,因为4的编号从1开始)。

在这里插入图片描述

链表描述

二叉树最常用的表示方法是用指针。每个元素用一个节点表示,节点有两个指针域,分别称为leftChild和rightChild,此外还有一个element域。

实现方式如下所示:

template <class T>
struct binaryTreeNode
{
	T element;
	binaryTreeNode<T>* leftChild;
	binaryTreeNode<T>* rightChild;
	// 默认构造函数,元素随着T决定,指针需要避免迷途
	binaryTreeNode() { leftChild = rightChild = NULL; }
	binaryTreeNode(const T& theElement) { element = theElement; leftChild = rightChild = NULL; }
	binaryTreeNode(const T& theElement,
		binaryTreeNode<T>* theLeftChild,
		binaryTreeNode<T>* theRightChild) :
		element(theElement),
		leftChild(theLeftChild),
		rightChild(theRightChild)
	{};
};

二叉树的遍历

分为前序遍历、中序遍历、后序遍历和层次遍历。

// 从根开始,先左后右
template<class T>
void preOrder(binaryTreeNode<T> *t) {
	visit(t); // 访问树根
	preOrder(t->leftChild); // 访问左子树
	preOrder(t->rightChild); // 访问右子树
}
// 从左子树开始,后中再右
template<class T>
void inOrder(binaryTreeNode<T>* t) {
	inOrder(t->leftChild);
	visit(t);
	inOrder(t->rightChild);
}
// 从左子树开始,后右再中
template<class T>
void postOrder(binaryTreeNode<T>* t) {
	postOrder(t->leftChild);
	postOrder(t->rightChild);
	visit(t);
}
// 层次遍历、广度优先
template<class T>
void levelOrder(binaryTreeNode<T>* t) {
	std::queue<binaryTreeNode*, deque<binaryTreeNode*>> q;
	while (t != NULL) {
		visit(t);
		if (t->element != NULL) q.push(t->leftChild);
		if (t->rightChild != NULL) q.push(t->rightChild);
		try { t = q.front(); }
		catch (...) { return; }
		q.pop();
	}
}

表达式树

用算术符号和变量构成的二叉树。

对一棵表达式树分别进行中序、前序和 后序遍历,结果便是表达式的中缀、前缀和后缀形式。

前缀:+ab;中缀:a+b;后缀:ab+。

中缀表达式需要加上优先级规则才能避免歧义。

在后缀和前缀表达式中,不必采用优先级规则,遇到一个操作符,则将其与栈顶的操作数相匹配。把这些操作数弹出栈,由操作符执行响应的操作,然后将计算结果压入操作数栈。

抽象数据类型BinaryTree

二叉树的操作有:

  1. empty():若树为空,则返回true,否则返回false;
  2. size():返回二叉树的节点/元素个数;
  3. preOrder(visit)等共计四种遍历函数。
template <class T>
class binaryTree{
public:
	virtual ~binaryTree() {}
	virtual bool empty() const = 0;
	virtual int size() const = 0;
	virtual void proOrder(void (*) (T*)) = 0;
	virtual void inOrder(void (*) (T*)) = 0;
	virtual void postOrder(void (*) (T*)) = 0;
	virtual void levelOrder(void (*) (T*)) = 0;
}

其中,二叉树遍历方法的参数类型为void (*) (T*)

这是一种函数类型,这种函数的返回值类型是void,它的参数类型是T*

linkedBinaryTree

template<class E>
class linkedBinaryTree : public binaryTree<binaryTreeNode<E>> {
public:
	linkedBinaryTree() { root = NULL; treeSize = 0; }
	~linkedBinaryTree() { erase(); };
	bool empty() const { return treeSize == 0; }
	int size() const { return treeSize; }
	void preOrder(void(*theVisit)(binaryTreeNode<E>*)) { visit = theVisit; preOrder(root); }
	void inOrder(void(*theVisit)(binaryTreeNode<E>*)) { visit = theVisit; inOrder(root); }
	void postOrder(void(*theVisit)(binaryTreeNode<E>*)) { visit = theVisit; postOrder(root); }
	void levelOrder(void(*theVisit)(binaryTreeNode<E>*))) { visit = theVisit; postOrder(root);
	void erase() {
		postOrder(dispose);
		root = NULL;
		treeSize = 0;
	}
private:
	binaryTreeNode<E>* root;
	int treeSize;
	static void (*visit)(binaryTreeNode<E>*);
	static void preOrder(binaryTreeNode<E>* t);
	static void inOrder(binaryTreeNode<E>* t);
	static void postOrder(binaryTreeNode<E>* t);
	static void levelOrder(binaryTreeNode<E>* t);
	static void dispose(binaryTreeNode<E>* t) { delete t; }
};

这其中最令人眼前一亮的应该是函数指针作为形参的操作。

我们首先在private部分定义了一个函数指针,之后将preOrder等遍历函数进行重载,以接收访问函数的指针。

详细内容可参照这里

基于树结构的并查集

之前,我们已经介绍过了数组和链表结构的并查集。

在此处,我们要介绍树结构的并查集,其中每个集合被表示为一棵树。

集合的树形描述

任何一个集合S都可以描述为一棵具有 ∣ S ∣ |S| S个节点的树,一个节点代表一个元素。任何一个元素都可以作为根元素;剩余元素的任何子集可以作为根元素的孩子;再剩余元素的任何子集可以作为根元素的孙子。

每个非根节点都有一个指针指向其父节点。指向父节点的指针之所以需要,是因为查找操作需要向上搜索一棵树。查找与合并操作都需要向下移动。

求解策略

把每一个集合表示为一棵树。查找时,我们把根元素作为集合标示符。

对于find函数来说,为了确定元素theElement属于哪一个集合,我们从元素theElement节点开始,沿着节点到其父节点向上移动,直到根节点为止。

在合并时,我们假设在调用语句unite(classA,classB)中,classA和classB分别是两个不同树的集合的根。为了把两个集合合并,让一棵树成为另一棵树的子树。

C++实现

int* parent;

void initialize(int numberOfElements) {
	// 初始化numberOfElements棵树,每棵树一个元素
	parent = new int[numberOfElements + 1];
	for (int e = 0; e < numberOfElements + 1; e++)
		parent[e] = 0;
}

int find(int theElement) {
	// 返回元素theElement所在树的根
	while (parent[theElement] != 0)
		theElement = parent[theElement];
}

int unite(int rootA, int rootB) {
	// 合并两棵其根节点不同的树
	parent[rootB] = rootA;
}

我们将树结构的并查集和数组结构的并查集对比一下:

  1. 在初始化上,思想都是各自为类,所以树并查集的每一个节点都指向了空节点0,而数组并查集每一个节点的代表元素都是本身;
  2. 在find上,树并查集需要层层递进,找到根节点,数组并查集只需要返回数组索引位置处的元素即可;
  3. 在unite上,树并查集只需要更改根节点的parent即可,而数组并查集需要把每个classB的成员改成classA,比较麻烦。

关于链表结构的并查集,看着好烦,这里就不比较了。

性能分析

结构构造findunite
数组 Θ ( n ) \Theta(n) Θ(n) Θ ( 1 ) \Theta(1) Θ(1) Θ ( n ) \Theta(n) Θ(n)
链表 Θ ( n ) \Theta(n) Θ(n) Θ ( 1 ) \Theta(1) Θ(1) O ( n ) O(n) O(n)
Θ ( n ) \Theta(n) Θ(n) O ( h ) O(h) O(h) Θ ( 1 ) \Theta(1) Θ(1)

比如说,一个树结构的并查集进行了f次查找和u次合并,那么一个系列的操作的时间为 O ( f u ) O(fu) O(fu)

这是因为,每一个合并,都给某些树增加了高度,潜在地增加了查找的时间。

合并函数的性能改进

在对根i和根j的树进行合并操作时,利用重量规则和高度规则,可以提高并查集算法的性能。

重量规则:若根为i的树的节点数少于根为j的树的节点数,则将j作为i的父节点。否则,将i作为j的父节点。

高度规则:若根为i的树高度小于根为j的树的高度,则将j作为i的父节点。否则,将i作为j的父节点。

为了把重量规则应用到合并算法中,在每个节点增加一个布尔域root。当且仅当每一个节点是当前根节点时,它的root域为true。

每个根节点的parent域用来记录该树的节点总数。

struct unionFindNode{
	int parent;
	bool root;

	unionFindNode()
		{parent = 1; root = true;}
};

初始化、查找和合并函数仍然采用先前的程序。

struct unionFindNode {
	int parent;
	bool root;

	unionFindNode()
	{
		parent = 1; root = true;
	}
};

unionFindNode* node;

void initialize(int n) {
	node = new unionFindNode[n + 1];
}

int find(int theElement) {
	while (!node[theElement].parent)
		theElement = node[theElement].parent;
	return theElement;
}

void unite(int rootA, int rootB) {
	if (node[rootA].parent > node[rootB].parent) {
		node[rootA].parent += node[rootB].parent;
		node[rootB].parent = rootA;
		node[rootB].root = false;
	}
	else {
		node[rootB].parent += node[rootA].parent;
		node[rootA].parent = rootB;
		node[rootA].root = false;
	}
}

之所以采用重量规则是基于如下引理:

引理-1【重力规则引理】:假设从单元素集合出发,用重量规则进行合并操作。若以此方式构建一棵具有p个节点的树t,则树的高度最多为 ⌊ log ⁡ 2 p ⌋ + 1 \lfloor\log_2p\rfloor+1 log2p+1

如果采用高度规则,此结论依然成立。

查找函数的性能改进

修改方法是缩短从元素e到根的查找路径。这个方法利用了路径压缩。这个过程的实现至少有三种不同的途径:路径紧缩、路径分割和路径对折。

在路径紧缩中,从待查节点到根节点的路径上,所有节点的parent指针都被改为指向根节点。

int find(int theElement){
// 返回元素theElement所在树的根
// 紧缩从元素theElement到根的路径
	// theRoot最终是根
	int theRoot = theElement;
	while (!node[theRoot].root)
		theRoot = theElement;
	
	// 紧缩从theElement到theRoot的路径
	int currentNode = theElement;
	while(currentNode != theRoot){
		int parentNode = node[currentNode].parent;
		node[currentNode].parent = theRoot;
		currentNode = parentNode;
	}
	return theRoot;
}

在路径分割中,从e节点到根节点的路径上,除根节点和其子节点之外,每个节点的parent指针都被改为指向各自的祖父。

在路径对折中,从e节点到根节点的路径上,除根节点和其子节点之外,每隔一个节点,其parent指针都被改为指向各自的祖父。

路径压缩的过程不会改变树的重量,但是会改变树的高度,于是乎,路径压缩和高度规则一起使用是很麻烦的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

右边是我女神

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值