目录
树的存储结构
- 树的存储方式有多种,既可采用顺序存储结构,又可采用链式存储结构,但无论采用何种存储方式,都要求能唯一地反映树中各结点之间的逻辑关系,这里介绍3种常用的存储结构。
双亲表示法(parent storage structure)—顺序存储
孩子表示法(child chain storage structure)—顺序存储+链式存储
按树的度设计结构
- 由于树中每个结点的子树个数(即结点的度)不同,如果按各个结点的度设计变长结构,则会因为结点的孩子结点的指针域个数不同而导致算法实现非常麻烦。孩子链存储结构可按树的度(即树中所有结点度的最大值)设计结点的孩子结点的指针域个数。
- 空指针域个数: n ( k − 1 ) + 1 n(k-1)+1 n(k−1)+1,其中n代表总的结点数,k代表树的度。
孩子兄弟表示法(child brother chain storage structure)—链式存储
树、森林与二叉树的转化
- 树、森林与二叉树之间有一个自然的对应关系,它们之间可以互相转换,即任何一个森林或一棵树都可以唯一地对应一棵二叉树,而任一棵二叉树也能唯一地对应到一个森林或一棵树上。正是由于有这样的对应关系,可以把在树中处理的问题对应到二叉树中进行处理,从而把问题简单化。
树转换为二叉树
- 规则:每个结点左指针指向它的第一个孩子,右指针指向它在树中的相邻右兄弟,这个规则又称“左孩子右兄弟”。由于根结点没有兄弟,所以对应的二叉树没有右子树。
- 树转换成二叉树的画法:①在兄弟结点之间加一连线;②对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉;③以树根为轴心,顺时针旋转45°。
森林转换为二叉树
- 规则:先将森林中的每棵树转换为二叉树,由于任何一棵树对应的二叉树的右子树必空,若把森林中第二棵树根视为第一棵树根的右兄弟,即将第二棵树对应的二叉树当作第一棵二叉树根的右子树,将第三棵树对应的二叉树当作第二棵二叉树根的右子树…以此类推,就可以将森林转换为二叉树。
- 森林转换成二叉树的画法:①将森林中的每棵树转换成相应的二叉树;②每棵树的根也可视为兄弟关系,在每棵树的根之间加一根连线;③以第一棵树的根为轴心顺时针旋转45°。
二叉树转换为森林
- 规则:若二叉树非空,则二叉树的根及其左子树为第一棵树的二叉树形式,故将根的右链断开。二叉树根的右子树又可视为一个由除第一棵树外的森林转换后的二叉树,应用同样的方法,直到最后只剩一棵没有右子树的二叉树为止,最后再将每棵二叉树依次转换成树,就得到了原森林。
树、森林的遍历
树的遍历
- 树的遍历是指用某种方式访问树中的每个结点,且仅访问一次。
先根遍历
- 若树非空,先访问根结点,再依次遍历根结点的每棵子树,遍历子树时仍遵循先根后子树的规则。其遍历序列与这棵树相应二叉树的先序序列相同。
后根遍历
- 若树非空,先依次遍历根结点的每棵子树,再访问根结点,遍历子树时仍遵循先子树后根的规则。其遍历序列与这棵树相应二叉树的中序序列相同。
层序遍历
- 与二叉树的层次遍历思想基本相同,即按层序依次访问各结点。
- 树的先根遍历以及后根遍历又可称为深度优先遍历;树的层序遍历又可称为广度优先遍历。
森林的遍历
先序遍历
- 方法一:若森林为非空,则按如下规则进行遍历:
- 访问森林中第一棵树的根结点。
- 先序遍历第一棵树中根结点的子树森林。
- 先序遍历除去第一棵树之后剩余的树构成的森林。
- 方法二:将森林转换为对应的二叉树,然后按照二叉树的先序遍历进行访问。
中序遍历
方法一:若森林为非空,则按如下规则进行遍历:
- 中序遍历森林中第一棵树的根结点的子树森林。
- 访问第一棵树的根结点。
- 中序遍历除去第一棵树之后剩余的树构成的森林。
- 方法二:将森林转换为对应的二叉树,然后按照二叉树的中序遍历进行访问。
总结
树 | 森林 | 二叉树 |
---|---|---|
先根遍历 | 先根遍历 | 先根遍历 |
后根遍历 | 中根遍历 | 中根遍历 |
二叉排序树
- 二叉排序树,又称二叉查找树(BST,Binary Search Tree)。
- 一棵二叉树或者是空二叉树,或者是具有如下性质的二叉树:左子树上所有结点的关键字均小于根结点的关键字;右子树上所有结点的关键字均大于根结点的关键字。左子树和右子树又各是一棵二叉排序树。
查找操作
- 二叉排序树的查找是从根结点开始,沿某个分支逐层向下比较的过程。若二叉排序树非空,先将给定值与根结点的关键字比较,若相等,则查找成功;若不等,如果小于根结点的关键字,则在根结点的左子树上查找,否则在根结点的右子树上查找。
递归算法
- 递归算法比较简单,但执行效率较低。
- 最坏空间复杂度:O(n)
非递归算法
- 执行效率高,最坏空间复杂度:O(h),其中h是二叉排序树的深度。
插入操作
- 二叉排序树作为一种动态树表,其特点是树的结构通常不是一次生成的,而是在查找过程中,当树中不存在关键字值等于给定值的结点时再进行插入的。
- 插入结点的过程如下:若原二叉排序树为空,则直接插入结点;否则,若关键字k小于根结点值,则插入到左子树,若关键字k大于根结点值,则插入到右子树。插入的结点一定是一个新添加的叶结点,且是查找失败时的查找路径上访问的最后一个结点的左孩子或右孩子。
二叉排序树的构造
- 从一棵树出发,依次输入元素,将它们插入二叉排序中的合适位置。
- 不同的关键字序列可能得到同款二叉排序树,也可能得到不同款二叉排序树。
删除操作
- 在二叉排序树中删除一个结点时,不能把以该结点为根的子树上的结点都删除,必须先把被删除结点从存储二叉排序树的链表上摘下,将因删除结点而断开的二叉链表重新链接起来,同时确保二叉排序树的性质不会丢失。删除操作的实现过程按3种情况来处理:
- 若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质。
②若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置。
③若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这个直接后继(或直接前驱),这样就转换成了第一或第二种情况。
用直接后继替代z
用直接前驱替代z
完整代码
#include<iostream>
using namespace std;
#include<stdio.h>
#include<malloc.h>
#define MaxSize 15
typedef int ElemType;
typedef struct BSTNode
{
ElemType data;
struct BSTNode* lchild, * rchild;
}BSTNode, * BSTree;
BSTNode* BST_Search1(BSTree t, ElemType key) //非递归查找
{
while (t != NULL && key != t->data) //若树空或等于根结点值,则结束循环
{
if (key < t->data) t = t->lchild; //小于,则在左子树上查找
else t = t->rchild; //大于,则在右子树上查找
}
return t;
}
BSTNode* BST_Search2(BSTree t, ElemType key) //递归查找
{
if (t == NULL) return NULL; //树空
if (key == t->data) return t; //查找成功
else if (key < t->data) return BST_Search2(t->lchild, key); //在左子树中找
else return BST_Search2(t->rchild, key); //在右子树中找
}
bool BST_Insert1(BSTree& t, ElemType key) //递归插入
{
if (t == NULL)
{
t = (BSTNode*)malloc(sizeof(BSTNode)); //原树为空,新插入的记录为根结点
t->data = key;
t->lchild = t->rchild = NULL;
return true; //插入成功
}
else if (t->data == key) return false; //树中存在相同关键字的结点,插入失败
else if (t->data > key) return BST_Insert1(t->lchild, key); //插入到t的左子树
else return BST_Insert1(t->rchild, key); //插入到t的右子树
}
bool BST_Insert2(BSTree& t, ElemType key) //非递归插入
{
BSTNode* pre = NULL, * p;
if (t == NULL) //空树
{
t = (BSTNode*)malloc(sizeof(BSTNode));
t->data = key;
t->lchild = t->rchild = NULL;
return true; //成功插入
}
BSTNode* q = t; //结点q指向根结点
while (q != NULL) //非空树
{
if (q->data == key) return false; //二叉排序树里不可能存在相同的结点,插入失败
else if (q->data > key)
{
pre = q;
q = q->lchild; //遍历左孩子
}
else
{
pre = q;
q = q->rchild; //遍历右孩子
}
}
if (pre->data > key) //插入到左子树
{
p = (BSTNode*)malloc(sizeof(BSTNode));
p->data = key;
p->lchild = p->rchild = NULL;
pre->lchild = p;
}
else //插入到右子树
{
p = (BSTNode*)malloc(sizeof(BSTNode));
p->data = key;
p->lchild = p->rchild = NULL;
pre->rchild = p;
}
}
void BST_Create(BSTree& t, int n) //二叉排序树的构造
{
t = NULL; //初始时t为空树
ElemType key;
printf("输入:");
while (n--)
{
scanf("%d", &key);
BST_Insert2(t, key); //依次将每个关键字插入到二叉排序树中
}
}
BSTNode* BST_Parent(BSTree t, ElemType key)
{
BSTNode* pre = NULL;
while (t != NULL && key != t->data) //若树空或等于根结点值,则结束循环
{
if (key < t->data)
{
pre = t;
t = t->lchild; //小于,则在左子树上查找
}
else
{
pre = t;
t = t->rchild; //大于,则在右子树上查找
}
}
if (t == NULL) pre = NULL;
return pre;
}
bool BST_Delete(BSTree& t, ElemType key)
{
BSTNode* p = t, * q = t, * s;
int flag;
p = BST_Parent(t, key);
if (t == NULL) return false; //空树
if (p == NULL) //父结点为空的情况:(一)要删除的是根结点 (二)关键字值在二叉排序树中查找不到
{
//(一)要删除的是根节点
//情况①:除根结点外无其他结点
if (t->data == key && t->lchild == NULL && t->rchild == NULL)
{
t = NULL; //删除根结点后变为空树
return true;
}
//情况②:除根结点外还有其他子树
else if (t->data == key) //如果右子树不空,可以让直接后继代替
{
s = t->rchild; //s是要删除结点的右子树中的根结点
while (s->rchild != NULL) s = s->lchild; //找到右子树中采用中序遍历第一个访问的结点
int data = s->data; //记录该结点的值
BST_Delete(t, s->data); //从二叉排序树中删去这个直接前驱
q->data = data; //最后令直接前驱替代要删除的结点
return true;
}
//else if (t->data == key) //如果左子树不空,可以让直接前驱代替
//{
// s = t->lchild; //s是要删除结点的左子树中的根结点
// while (s->rchild != NULL) s = s->rchild; //找到左子树中采用中序遍历最后一个访问的结点
// int data = s->data; //记录该结点的值
// BST_Delete(t, s->data); //从二叉排序树中删去这个直接前驱
// q->data = data; //最后令直接前驱替代要删除的结点
// return true;
//}
//(二)关键字值在二叉排序树中查找不到
else return false;
}
if (p->lchild->data == key)
{
flag = 0; //表示q是p的左孩子
q = p->lchild; //q即为要删除的结点
}
else if (p->rchild->data == key)
{
flag = 1; //表示q是p的右孩子
q = p->rchild; //q即为要删除的结点
}
//要删除的结点是叶结点
if (q->lchild == NULL && q->rchild == NULL) //q是叶子结点,直接删除
{
if (flag == 0) p->lchild = NULL; //要删除的结点是其父结点的左孩子
else p->rchild = NULL; //要删除的结点是其父结点的右孩子
}
//要删除的结点不是叶结点,并且该结点只有右子树
else if (q->lchild != NULL && q->rchild == NULL) //q没有左孩子,只有右孩子
{
if (flag == 0) p->lchild = q->lchild;
else p->rchild = q->lchild;
}
//要删除的结点不是叶结点,并且该结点只有左子树
else if (q->lchild == NULL && q->rchild != NULL) //q只有左孩子,没有右孩子
{
if (flag == 0) p->lchild = q->rchild;
else p->rchild = q->rchild;
}
//else //直接后继替代
//{
// s = q->rchild; //s是要删除结点的右子树中的根结点
// while (s->lchild != NULL) s = s->lchild;
// int data = s->data;
// BST_Delete(t, s->data); //从二叉排序树中删去这个直接后继
// q->data = data; //最后令直接后继替代要删除的结点
//}
else //直接前驱替代
{
s = q->lchild; //s是要删除结点的左子树中的根结点
while (s->rchild != NULL) s = s->rchild; //找到左子树中采用中序遍历最后一个访问的结点
int data = s->data; //记录该结点的值
BST_Delete(t, s->data); //从二叉排序树中删去这个直接前驱
q->data = data; //最后令直接前驱替代要删除的结点
}
}
typedef struct
{
BSTNode* data[MaxSize];
int front, rear;
}SqQueue;
void InitQueue(SqQueue*& qu)
{
qu = (SqQueue*)malloc(sizeof(SqQueue));
qu->rear = qu->front = 0;
}
bool EmptyQueue(SqQueue* qu)
{
return qu->rear == qu->front;
}
bool enQueue(SqQueue*& qu, BSTNode* b)
{
if ((qu->rear + 1) % MaxSize == qu->front) return false;
qu->data[qu->rear] = b;
qu->rear = (qu->rear + 1) % MaxSize;
}
bool deQueue(SqQueue*& qu, BSTNode*& b)
{
if (qu->rear == qu->front) return false;
b = qu->data[qu->front];
qu->front = (qu->front + 1) % MaxSize;
}
void DestroyQueue(SqQueue*& qu)
{
free(qu);
}
void LevelOrder(BSTree b) //层次遍历
{
BSTNode* p;
SqQueue* qu;
InitQueue(qu);
enQueue(qu, b);
while (!EmptyQueue(qu))
{
deQueue(qu, p);
printf("%d ", p->data);
if (p->lchild != NULL) enQueue(qu, p->lchild);
if (p->rchild != NULL) enQueue(qu, p->rchild);
}
DestroyQueue(qu);
}
int main()
{
BSTNode* t;
int n;
printf("输入要创建的结点的个数:");
scanf("%d", &n);
BST_Create(t, n);
printf("层序遍历二叉排序树:");
LevelOrder(t);
ElemType key;
printf("\n输入要删除的关键字的值:");
scanf("%d", &key);
BSTNode* parent = BST_Parent(t, key);
if (parent != NULL) printf("关键字值为%d的结点的双亲结点的关键字值为:%d\n", key, parent->data);
bool result = BST_Delete(t, key);
cout << boolalpha << "删除成功了吗?— " << result << endl;
if (result)
{
printf("删除关键字为%d的结点之后的二叉排序树(层序遍历):", key);
LevelOrder(t);
}
return 0;
}
运行结果
查找效率分析
- 查找长度—在查找运算中,需要对比关键字的次数称为查找长度,反映了查找操作时间复杂度。
- 若树高h,找到最下层的一个结点需要对比h次。
- 最坏情况:每个结点只有一个分支,树高h=结点数n。平均查找长度=O(n)
- 最好情况:n个结点的二叉树最小高度为 ⌊ l o g 2 n ⌋ + 1 ⌊log_2^n⌋+1 ⌊log2n⌋+1。平均查找长度= O( l o g 2 n log_2^n log2n)