上海交大ACM班C++算法与数据结构——数据结构之二叉树
1.树
- 基本操作:
- 建树create()
- 清空clear()
- 判空IsEmpty()
- 统计结点数size()
- 找根结点root():找出树的根结点值;如果树是空树,则返回一个特殊值
- 找父结点parent(x):找出结点x的父结点值;如果x不存在或x是根结点,则返回一个特殊值
- 找子结点child(x,i):找结点x的第i个子结点值; 如果x不存在或x的第i个儿子不存在,则返回一个特殊值
- 剪枝remove(x,i):删除结点x的第i棵子树
- 遍历traverse() - 抽象类实现
template<class T>
class tree {
public:
virtual void clear() = 0;
virtual bool isEmpty() const = 0;
virtual int size() = 0;
virtual T root(T flag) const = 0;
virtual T parent(T x, T flag) const = 0;
virtual T child(T x, int i, T flag) const = 0;
virtual void remove(T x, int i) = 0;
virtual void traverse() const = 0;
virtual ~tree() {}
};
- 例题
- 结点数比结点总度数多1
- 已知一棵度为3的树有2个度为1的结点,3个度为2的结点,4个度为3的结点,则该树中有_____个叶结点((1-1)*2+(2-1)*3+(3-1)*4=12)((度数-1)*个数)
2.二叉树
- 有左子树和右子树之分
- 在二叉树结点的前序序列、中序序列和后序序列中,所有叶子结点的先后顺序完全相同
- 基本操作:与树基本相同,只是有关子树的操作变成了左子树和右子树的相关操作;遍历操作具体化为四种遍历:前序、中序、后序、层次(前序+中序,后序+中序可以唯一确定一棵二叉树,前/后序确定根结点,中序分左右结点)
- 建树create()
- 清空clear()
- 判空IsEmpty()
- 统计结点数size()
- 找根结点root():找出树的根结点值;如果树是空树,则返回一个特殊值
- 找左孩子lchild(x):找结点x的左孩子结点值;如果x不存在或x的左儿子不存在,则返回一个特殊值
- 找右孩子rchild(x):找结点x的右孩子结点值;如果x不存在或x的右儿子不存在,则返回一个特殊值
- 删除左子树delLeft(x):删除结点x的左子树
- 删除右子树delRight(x):删除结点x的右子树
- 找子结点child(x,i):找结点x的第i个子结点值; 如果x不存在或x的第i个儿子不存在,则返回一个特殊值
- 剪枝remove(x,i):删除结点x的第i棵子树
- 前序遍历preOrder():前序遍历二叉树上的每一个结点
如果二叉树为空,则操作为空;否则:
访问根结点
前序遍历左子树
前序遍历右子树 - 中序遍历midOrder():中序遍历二叉树上的每一个结点
如果二叉树为空,则操作为空;否则:
中序遍历左子树
访问根结点
中序遍历右子树 - 后序遍历postOrder():后序遍历二叉树上的每一个结点
如果二叉树为空,则操作为空;否则:
后序遍历左子树
后序遍历右子树
访问根结点 - 层次遍历levelOrder():层次遍历二叉树上的每个结点
先访问根结点,然后按从左到右的次序访问第二层的结点。在访问了第k层的所有结点后,再按从左到右的次序访问第k+1层。以此类推,直到最后一层。
- 二叉树性质
- 一棵非空二叉树的第 i 层上最多有2^(i-1)个结点
- 一棵高度为k的二叉树,最多具有2^k-1个结点。
- 对于一棵非空二叉树,如果叶子结点数为n0,度数为2的结点数为n2,则有:n0=n2+1
- 具有n个结点的完全二叉树的高度为 k = ⌊logn⌋+1
- 对于有n个结点的完全二叉树,结点i的左孩子为2i,右孩子为2i+1,父亲为⌊i/2⌋
3.顺序存储
- 补全成完全二叉树,按层编号,映射成线性关系(用特殊值表示空结点)
- 浪费空间,一般只用于一些特殊的场合,如静态的并且结点个数已知的完全二叉树或接近完全二叉树的二叉树。
-
习题
顺序存储二叉树的节点数量统计
现有一棵根节点为1且按照顺序存储的完全二叉树。我们已知完全二叉树的最后一个结点是n。我们想知道,以结点m为根节点,其所在的子树中一共包括多少个结点。输入描述:
一行两个整数n m。
对于100%的数据满足1 ≤ m ≤ n ≤ 10^5。
输出描述:
输出一行,该行包含一个整数,表示结点m所在子树中包括的结点的数目。
示例 1:
输入:
7 3
输出:
3提示:递归,所求子树结点数=左子树结点数+右子树结点数+1
#include <iostream>
using namespace std;
int n, m, ans;
void getAns(int pos) {
ans++;
if (pos * 2 <= n)
getAns(pos * 2);
if (pos * 2 + 1 <= n)
getAns(pos * 2 + 1);
return;
}
int main() {
cin >> n >> m;
getAns(m);
cout << ans;
return 0;
}
4.链接存储
-
二叉链表 指出儿子结点:类似单链表,也称为二叉树的标准存储方式
-
三叉链表同时指出父亲和儿子结点:类似双链表
-
实际应用过程中很少需要找父结点,所以较多使用二叉链表,所以二叉链表也称为二叉树的标准存储方式
-
例题
子树的大小
有一棵有n个结点的二叉树,节点编号为1 ~ n,已知所有结点的左右子节点和根节点的编号r,现在我们想知道每个节点所在的子树中一共包括多少个结点。对于100%的数据满足1 ≤ r ≤ n ≤ 10^5。
输入描述:
第一行两个整数n r,分别表示节点的数量和根节点的编号。
接下来n行,每行两个整数,第i行表示节点i的左右子树,若为0则表示为空。
输出描述:
n行,每行一个整数,第i行表示以结点i为根节点,其所在的子树中一共包括多少个结点。
示例 1:
输入:
5 1
2 3
4 5
0 0
0 0
0 0
输出:
5
3
1
1
1
#include <iostream>
#define N 100010
using namespace std;
int lson[N], rson[N], val[N];
void getAns(int pos) {
if (lson[pos])
getAns(lson[pos]);
if (rson[pos])
getAns(rson[pos]);
val[pos] = val[lson[pos]] + val[rson[pos]] + 1;
return;
}
int main() {
int n, r;
cin >> n >> r;
for (int i = 1; i <= n; i++)
cin >> lson[i] >> rson[i];
getAns(r);
for (int i = 1; i <= n; i++)
cout << val[i] << endl;
return 0;
}
- 类设计
- 结点类:
数据成员:数据及左右孩子的指针。
操作:构造和析构
是树类的私有内嵌类
- 二叉树类:
数据成员:指向根结点的指针
成员函数设计 - 类定义
template<class T>
class binaryTree : public bTree<T> {
friend void printTree(const binaryTree &t, T flag);
private:
struct Node { //二叉树的结点类
Node *left , *right ;
T data;
Node() : left(NULL), right(NULL) { }
Node(T item, Node *L = NULL, Node * R =NULL) : data(item), left(L), right(R) { }
~Node() {}
};
Node *root;
public:
// 创造空二叉树
binaryTree() : root(NULL) {}
// 创造只有根结点的二叉树
binaryTree(T x) { root = new Node(x); }
~binaryTree() { clear() ; }
void clear() ;
bool isEmpty() const{return root == NULL;}
int size() const;
T Root(T flag) const;
T lchild(T x, T flag) const;
T rchild(T x, T flag) const;
void delLeft(T x) ;
void delRight(T x);
void preOrder() const;
void midOrder() const;
void postOrder() const;
void levelOrder() const;
void createTree(T flag);
private:
Node *find(T x, Node *t ) const;
// 同名公有函数递归调用以下函数实现
void clear(Node *&t);
int size(Node *t) const;
void preOrder(Node *t) const;
void midOrder(Node *t) const;
void postOrder(Node *t) const;
};
- root函数:返回root指向的结点的数据部分。如果二叉树是空树,则返回一个特殊的标记。
template <class T>
T binaryTree<T>::Root(T flag) const {
if (root == NULL) return flag;
else return root->data;
}
- size函数:树的结点数 = 左子树结点数 +右子树结点数 + 1
template<class T>
int binaryTree<T>::size(binaryTree<T>::Node *t) const {
if (t == NULL) return 0;
else return size(t->left) + size(t->right) + 1;
}
template<class T>
int binaryTree<T>::size() const {
return size(root);
}
- clear函数:删除一棵树 = 删除左子树 + 删除右子树 + 删除根
void binaryTree<T>::clear(binaryTree<T>::Node *&t) {
if (t == NULL) return;
clear(t->left);
clear(t->right);
delete t;
t = NULL;
}
template<class T>
void binaryTree<T>::clear() {
clear(root);
}
-
前序遍历
if (空树) return;
访问根结点;
前序遍历左子树;
前序遍历右子树;
template<class T>
void binaryTree<T>::preOrder(binaryTree<T>::Node *t) const {
if (t == NULL) return;
cout << t->data << ' ';
preOrder(t->left);
preOrder(t->right);
}
template<class T>
void binaryTree<T>::preOrder() const {
cout << "\n前序遍历:";
preOrder(root);
}
-
中序遍历
if (空树) return;
中序遍历左子树;
访问根结点;
中序遍历右子树;
template<class T>
void binaryTree<T>::midOrder (binaryTree<T>::Node *t) const {
if (t == NULL) return;
midOrder(t->left);
cout << t->data << ' ';
midOrder(t->right);
}
template<class T>
void binaryTree<T>::midOrder() const {
cout << "\n中序遍历:";
midOrder(root);
}
-
后序遍历
if(空树)return;
访问根结点;
后序遍历左子树;
后序遍历右子树;
template<class T>
void binaryTree<T>::postOrder (binaryTree<T>::Node *t) const {
if (t == NULL) return;
postOrder(t->left);
postOrder(t->right);
cout << t->data << ' ';
}
template<class T>
void binaryTree<T>::postOrder() const {
cout << "\n后序遍历:";
postOrder(root);
}
-
层次遍历
访问过程
根节点
根节点的儿子
根节点的儿子的儿子
关键问题
如何储存已经可以被访问的结点
使用队列
工作过程:
根结点入队
While(队列非空) {
出队并访问
将儿子入队
}
template<class T>
void binaryTree<T>::levelOrder() const {
linkQueue< Node * > que;
Node *tmp;
cout << “\n层次遍历:”;
que.enQueue(root);
while (!que.isEmpty()) {
tmp = que.deQueue();
cout << tmp->data << ' ';
if (tmp->left)
que.enQueue(tmp->left);
if (tmp->right)
que.enQueue(tmp->right);
}
}
-
find函数:找结点x:遍历,可以采用任一种遍历
工作过程:采用前序遍历
if (根结点是x) 返回根结点地址
tmp = 左子树递归调用find函数
if (tmp不是空指针) return tmp;
else return 对右子树递归调用find的结果
binaryTree<T>::Node *binaryTree<T>::find(T x, binaryTree<T>::Node *t) const {
Node *tmp;
if (t == NULL) return NULL;
if (t->data == x) return t;
if (tmp = find(x, t->left) ) return tmp;
else return find(x, t->right);
}
- lchild:返回结点x的left值
template <class T>
T binaryTree<T>::lchild(T x, T flag) const {
Node * tmp = find(x, root);
if (tmp == NULL || tmp->left == NULL)
return flag;
return tmp->left->data;
}
- rchild:返回结点x的right值
template <class T>
T binaryTree<T>::rchild(T x, T flag) const {
Node * tmp = find(x, root);
if (tmp == NULL || tmp->right == NULL)
return flag;
return tmp->right->data;
}
- delLeft:对左子树调用clear函数删除左子树,然后将结点x的left置为NULL。
template <class T>
void binaryTree<T>::delLeft(T x) {
Node *tmp = find(x, root);
if (tmp == NULL) return;
clear(tmp->left);
}
- delRight:对右子树调用clear函数删除右子树,然后将结点x的right置为NULL。
template <class T>
void binaryTree<T>::delLeft(T x) {
Node *tmp = find(x, root);
if (tmp == NULL) return;
clear(tmp->left);
}
-
createTree:创建一棵树
创建过程
按层次遍历输入结点
先输入根结点;对已输入的每个结点,依次输入它的两个儿子的值。如果没有儿子,则输入一个特定值
template <class Type>
void BinaryTree<Type>::createTree(Type flag) {
linkQueue<Node *> que;
Node *tmp;
Type x, ldata, rdata;
cout << "\n输入根结点:";
cin >> x;
root = new Node(x);
que.enQueue(root);
while (!que.isEmpty()) {
tmp = que.deQueue();
cout << "\n输入" << tmp->data << "的两个儿子(" << flag << "表示空结点):";
cin >> ldata >> rdata;
if (ldata != flag)
que.enQueue(tmp->left = new Node(ldata));
if (rdata != flag)
que.enQueue(tmp->right = new Node(rdata));
}
cout << "create completed!\n";
}
- printTree:输出一棵树,以层次遍历的次序输出每个结点和它的左右孩子
template <class T>
void printTree(const binaryTree<T> &t, T flag) {
linkQueue<T> q;
q.enQueue(t.root->data);
cout << endl;
while (!q.isEmpty()) {
char p, l, r;
p = q.deQueue();
l = t.lchild(p, flag);
r = t.rchild(p, flag);
cout << p << " " << l << " " << r << endl;
if (l != flag) q.enQueue(l);
if (r != flag) q.enQueue(r);
}
}
-
例题
第一行两个整数n r (1 ≤ r ≤ n ≤ 10^5),分别表示节点的数量和根节点的编号。
接下来n行,每行两个整数,第i行表示节点i的左右子树,若为0则表示为空。
输出描述:
三行,每行n个整数,整数间用一个空格隔开。
第一行表示这棵二叉树的前序遍历,第二行表示这棵二叉树的中序遍历,第三行表示这棵二叉树的后序遍历。
示例 1:
输入:
5 1
2 3
4 5
0 0
0 0
0 0
输出:
1 2 4 5 3
4 2 5 1 3
4 5 2 3 1
#include <iostream>
#define N 100010
using namespace std;
int lson[N], rson[N];
void getAns1(int pos) {
cout << pos << " ";
if (lson[pos])
getAns1(lson[pos]);
if (rson[pos])
getAns1(rson[pos]);
return;
}
void getAns2(int pos) {
if (lson[pos])
getAns2(lson[pos]);
cout << pos << " ";
if (rson[pos])
getAns2(rson[pos]);
return;
}
void getAns3(int pos) {
if (lson[pos])
getAns3(lson[pos]);
if (rson[pos])
getAns3(rson[pos]);
cout << pos << " ";
return;
}
int main() {
int n, r;
cin >> n >> r;
for (int i = 1; i <= n; i++)
cin >> lson[i] >> rson[i];
getAns1(r);
cout << endl;
getAns2(r);
cout << endl;
getAns3(r);
cout << endl;
return 0;
}