1. 树的定义与性质
- 树是一种无环连通的数据结构,规定最多只有一个根节点
- 树可以没有结点,该树称为空树
- 树的层次从根结点开始计算,起始值为1
- 结点的子树个数为结点的度,树的度为结点的度的最大值
- n个结点的树,一定有n-1条边
- 满足连通、边数等于结点数减一的结构一定是树
- 没有子树的结点称为叶子结点
- 结点深度:从根节点出发到该结点的路径中经过的结点数(包括根结点)
- 结点高度:从叶子节点出发到该结点的路径中经过的结点数(包括叶子结点)
- 树的深/高度 = 最大结点深/高度
- 树的深度 = 树的高度、结点的深度不一定等于结点的高度
2. 二叉树的定义与性质
递归定义:
- 二叉树为空树
- 二叉树具有根节点,左、右子树,且左右子树均为二叉树
二叉树左右子树不允许调换顺序
两种特殊的二叉树
- 满二叉树:每一层的结点个数都达到该层能达到的最大结点数(2^(该层深度 - 1))
- 完全二叉树: 除最底一层外,其余层的结点个数都达到达到该层能达到的最大结点数,且最底层从左到右连续存在若干结点,这些连续结点的右侧不存在结点
3. 二叉树的存储结构与基本操作
二叉树中的结点存储结构:
struct node{
typename data;
node* left; // 左子树指针
node* right; // 右子树指针
}
新建结点的操作:
node* newNode(int v){
node* n = new node;
n->data = v;
n->left = n->right = NULL;
return n;
}
二叉树结点的查找:
void search(node* root, int x){
// 边界条件
if(root == NULL){
return;
}
// 结点逻辑
if(root->data == x){
...
}
// 递归式
search(root->left, x);
search(root->right, x);
}
完全二叉树的存储结构:
如果我们按层次将完全二叉树中的结点进行编号(从1开始),如下图所示:
![image-20200518143018244](/Users/chunlin97/Library/Application Support/typora-user-images/image-20200518143018244.png)
那么我们可以发现以下规律:
- 若当前结点编号为x,则其左孩子的结点编号一定为2x
- 若当前结点编号为x,则其右孩子的结点编号一定为2x+1
因此我们可以使用数组对完全二叉树进行存储
由于结点编号从1开始,因此数组的大小为结点个数+1
使用数组存储完全二叉树时有以下几个特点:
- 遍历该数组时正好是二叉树的层次遍历序列
- 判断某个结点x是否为叶子结点:判断其左孩子结点2x是否大于数组长度n
- 判断某个结点x是否为空结点:判断编码x是否大于数组长度n
4. 二叉树的遍历
4.1 先序遍历
套模板:
- 递归条件:遇到空结点
- 结点逻辑:输出结点值
- 递归式:左子树、右子树
代码实现:
void preOrder(node* root){
// 递归条件
if(root == NULL){
return;
}
// 结点逻辑
printf("%d", root->data);
// 递归式
preOrder(root->left);
preOrder(root->right);
}
先序遍历性质:
- 先序遍历序列的第一个结点一定是根结点(树或子树)
4.2 中序遍历
实现思路与先序遍历相同
代码实现:
void inOrder(node* root){
// 递归条件
if(root == NULL){
return;
}
// 递归式
inOrder(root->left);
// 结点逻辑
printf("%d", root->data);
inOrder(root->right);
}
中序遍历性质:
- 如果找到中序遍历序列中的根节点,则可以区分根节点的左子树与右子树
4.3 后序遍历
实现思路与先序遍历相同
代码实现:
void postOrder(node* root){
// 递归条件
if(root == NULL){
return;
}
// 递归式
postOrder(root->left);
postOrder(root->right);
// 结点逻辑
printf("%d", root->data);
}
后序遍历性质:
- 后序遍历序列的第一个结点一定是根结点(树或子树)
4.4 层次遍历
二叉树的层次遍历过程采用了先前提到的广度优先搜索的思想,它的思路是当遍历到一个结点时,就将它的子结点(非空)按顺序加入到队列中
因此我们使用BFS模板实现二叉树的层次遍历:
void levelOrder(node* root){
// 将根节点加入队列
queue<node*> q;
q.push(root);
while(!q.empty()){
// 1. 取出队首结点
node* top = q.front();
// 2. 将队首结点出列
q.pop();
// 3. 实现结点逻辑
if(top->left != NULL) q.push(top->left);
if(top->right != NULL) q.push(top->right);
printf("%d", top->data);
}
}
4.5 重新构建二叉树
- 先/后序遍历与中序遍历序列结合重新构建二叉树
实现思路(以先序遍历为例):
- 由于先序序列的第一个结点为根结点,因此我们遍历中序序列,寻找与先序序列根结点值相同的结点
- 我们以中序序列中的根结点为界,根节点左侧为左子树的中序序列,根节点右侧为右子树的中序序列
- 根据先序序列的性质,我们可以得到,先序序列根结点后面的结点为左子树的根节点。因此我们根据中序序列中左子树的序列长度与先序序列中左子树的根节点即可以确定左子树的先序序列。确定完左子树的先序序列后,先序序列中剩余的部分即为右子树的先序序列。
实现示意图如下:
![a0770c22f8b841ec0134c1bfed5e0dcc.png](https://img-blog.csdnimg.cn/img_convert/a0770c22f8b841ec0134c1bfed5e0dcc.png)
实现代码如下:
int pre[MAXN], in[MAXN]; // 先序与中序遍历
int preL, preR, inL, inR; // 两个序列的边界
root* create(int preL, int preR, int inL, int inR){
// 边界条件
if(preL > preR){
return;
}
// 递归逻辑:找出左子树与右子树的先序序列
int root = pre[preL];
int i; // 中序序列中的根节点
for(i = inL; i <= inR; i++){
if(in[i] == root){
break;
}
}
int len = i - inL; // 左子树中的结点数
// 此时左子树的先序序列为[preL + 1, preL + len],右子树的先序序列为[preL + len + 1, preR]
// 递归式
root->left = create(preL + 1, preL + len, inL, i - 1);
root->right = create(preL + len + 1, preR, i + 1, inR);
return root;
}
- 层序遍历与中序遍历序列重新构建二叉树
实现思路:
- 层次遍历与先序/后序遍历一样,序列中第一个结点一定为根节点。因此我们遍历中序序列,寻找与层次序列根结点值相同的结点
- 我们以中序序列中的根结点为界,根节点左侧为左子树的中序序列,根节点右侧为右子树的中序序列
- 层次序列与先序序列的区别在于,左子树与右子树的元素在序列中并不是分开的。因此我们需要对层次序列中的每一个元素进行判断,判断其属于左子树还是右子树。在构建二叉树过程中我们需要重新生成左子树和右子树的层次序列,而使用先序序列时只需要在原序列上进行划分。
实现代码:
int levelOrder[MAXN], inOrder[MAXN];
int ll, lr, il, ir;
root* create(int ll, int lr, int il, int ir, int* levelOrder){
// 边界条件
if(ll == lr){
return;
}
// 递归逻辑,生成左子树与右子树的层次序列
int root = levelOrder[0];
int i;
int leftLevel[MAXN], rightLevel[MAXN];
int leftLen = 0, rightLen = 0;
for(i = il; i <= ir; i++){
if(inOrder[i] == root){
break;
}
}
// 判断层次序列中的每一个元素,属于左子树还是右子树
for(int j = ll; j <= lr; j++){
// 左子树判断
for(int k = il; k < i; k++){
if(in[k] == levelOrder[j]){
leftLevel[leftLen++] = in[k];
break;
}
}
// 右子树判断
for(int k = i + 1; k <= ir; k++){
if(in[k] == levelOrder[j]){
rightLevel[rightLen++] = in[k];
break;
}
}
}
// 递归式
root->left = create(0, leftlen - 1, il, i - 1, leftLevel);
root->right = create(0, rightlen - 1, i + 1, ir, rightLevel);
return root;
}
4.6 翻转二叉树
示意图:
![0a3a19089a5c4ebd23222644ea20d310.png](https://img-blog.csdnimg.cn/img_convert/0a3a19089a5c4ebd23222644ea20d310.png)
实现思路:
- 借鉴先序遍历的思路,每当遍历一个根节点时就交换它的左右子树
- 借鉴后序遍历的思路,当遍历完左/右子树后再交换
实现代码:
(1) 先序遍历思路
void invert(node* root){
// 边界条件
if(root == NULL){
return;
}
// 递归逻辑,交换左子树右子树
swap(root->left, root->right);
// 递归式
invert(root->left);
invert(root->right);
}
(2) 后序遍历思路
void invert(node* root){
// 边界条件
if(root == NULL){
return;
}
// 递归式
invert(root->left);
invert(root->right);
// 递归逻辑,交换左子树右子树
swap(root->left, root->right);
}
5. 树的遍历
本节讨论的树为一般意义上的树,即子节点个数不限且子节点没有先后次序的树,而不是上文中讨论的二叉树
树的静态存储结构:
struct node{
typename data;
vector<int> child; // 使用动态数组节省内存空间
}nodes[MAXN];
树的先根遍历(DFS思想):
void DFS(int root){
printf("%d", nodes[root].data);
for(int i = 0; i < nodes[root].child.size(); i++){
DFS(nodes[root].child[i]);
}
}
树的层次遍历(BFS思想):
void BFS(int root){
queue<int> q;
q.push(root);
while(!q.empty()){
int top = q.top();
q.pop();
printf("%d", nodes[root].data);
for(int i = 0; i < nodes[root].child.size(); i++){
q.push(nodes[root].child[i]);
}
}
}