回想刚学二叉树的时候,被二叉树删除搞的要死要活的,后来学习了AVL树、红黑树才慢慢熟练了二叉树的基础操作。篇幅会有点长~慢慢欣赏~
我就当各位都是明白什么是树,什么叫二叉树。在这里我就不多解释了~如果这些都不清楚的话,可能需要先补充些相关知识再看会比较好。
一.创建结构体
struct Node {
int key;
int count;
Node* parent;
Node* left;
Node* right;
};
二叉树结构的基本构成,数据key,父节点parent,左子节点left,右子节点right。
我擅自加了个count,由于我们做数据排序的时候,有时候会遇到相同的key值,那么这个时候传统的操作是当成重复数据直接抛掉,我个人觉得抛掉有点可惜,那么就加个count来记录插入相同数据的个数。
顺便一提,初学的时候会觉得parent好像没什么用,实际上不用parent也是可以实现二叉树的,但是有了parent一些操作会变得更加简单,例如删除操作(下面会提到)。
当然,上面的count和parent不是必须的。具体结构根据实际需要进行调整即可。
二.建立根节点
Node* root;
void InitRoot() {
root = (Node*)malloc(sizeof(Node));
root->key = 0;
root->count = 0;
root->parent = NULL;
root->left = NULL;
root->right = NULL;
}
这里就不多说,很简单。
三.插入数据
首先二叉树是有个规则的,就是对于树中的每个子树的根节点,左边的节点都要小于根节点,右边节点都要大于根节点。也就是有下面这个结构:
中
/ \
小 大
举个例子,我们插入[7,3,5,1,9,8,2,4]时,按照上面规则来排,最终得到这么一棵树:
7
/ \
3 9
/ \ /
1 5 8
\ /
2 4
有兴趣可以自己排一排练习下看看我们是不是排成一样的。
根据这个规则我们来写插入函数:
void InsertTree(Node *root, int key) {
//首先判断根节点是否有存数据
if (root->count == 0) {
root->key = key;
root->count++;
}
else {
Node* node = root;//创建子节点
Node* parent = root->parent;//子节点的父节点
//找到子节点的父节点
while (node != NULL) {
parent = node;
if (key == node->key) {//假如该节点在树中已有,count+1返回
node->count++;
return;
}
else if(key < node->key){//小于时往左节点寻找
node = node->left;
}
else if (key > node->key) {//大于时向右节点寻找
node = node->right;
}
}
//给子节点赋值
node = (Node*)malloc(sizeof(Node));
node->key = key;
node->count = 1;
node->parent = parent;
node->left = NULL;
node->right = NULL;
//把子节点挂到父节点上
if (key < parent->key) {
parent->left = node;
}
else {
parent->right = node;
}
}
}
四.查找数据
查找的情况和插入很相似,同样运用了左小右大的规则,看代码!
Node* Find(Node* root, int key) {
if (root->count != 0) {//判断根节点是否有数据
Node* node = root;
while (node!=NULL)
{
if (key == node->key) {//找到节点,直接返回
return node;
}
else if(key < node->key) {//小于时,找向左节点
node = node->left;
}
else if(key > node->key){//大于时,找向右节点
node = node->right;
}
}
}
return NULL;
}
相信学会了插入,查找就难不倒你了。
五.删除数据
重头戏来了!删除是应该是最难的部分了,为什么难?因为删除的时候有三种情况:
1.被删除的是叶子节点,左右子节点都为空
2.被删除的节点,左右子节点有一个为空
3.被删除的节点,左右子节点都不为空
我们一个一个来分别处理。
首先,被删除的是叶子节点,我们只需要让父亲节点连接的左或右节点为空即可
父 父
/ /
删 --> NULL
/ \
NULL NULL
代码如下:
if (node->left == NULL&&node->right == NULL) {
Node* parent = node->parent;//获取父节点
if (parent == NULL) {//判断是不是根节点
root->key = 0;
root->count = 0;
}
else {
if (node == parent->left) {
parent->left = NULL;
}
else {
parent->right = NULL;
}
free(node);//释放
}
}
当被删除的节点只有一个子节点时,我们只需要把子节点替代删除节点即可:
父 父
/ /
删 --> 子
/ \
NULL 子
代码如下:
if((node->left!=NULL&&node->right==NULL)||(node->left==NULL&&node->right!=NULL)){
Node* parent = node->parent;
Node* child = node->left == NULL ? node->right : node->left;
child->parent = parent;
if (parent == NULL) {//判断是不是根节点
root = child;
}
else {
if (node == parent->left) {
parent->left = child;
}
else {
parent->right = child;
}
free(node);
}
}
当被删除节点的两个子节点都不为空时,这时候我们需要找一个节点来替代删除节点。同时我们又需要使得节点左小右大的性质不被破坏,所以我们可以以删除结点为根节点,可以找其左子树中的最大节点或者右子树中的最小节点来替代:
父 父 父
/ / /
删 --> 左MAX 或者 右MIN
/ \ / \ / \
左子树 右子树 左子树-Max 右子树 左子树 右子树-MIN
上代码:
if (node->left != NULL&&node->right != NULL) {
Node* right_min=node->right;
while (right_min->left == NULL) {//找到右子树中的最小值
right_min = right_min->left;
}
int num= right_min->key;
node->count = right_min->count;
DeletTree(num); //先删除右子树最小节点
node->key = num; //将删除节点的值进行替换
}
我选择找右子树的最小节点。
可能有人会疑问这里的DeleteTree(num),因为我们要先删除找到的右子树最小节点,再给当前节点赋值,假如先赋值再删除,就只会把当前节点删除(因为此时的key已经改变了),这样不仅没达到效果,还会引起错误。
上删除的完整代码:
void DeletTree(int key) {
Node* node = Find(root, key); //找到需要删除的节点
if (node == NULL) { //判断该节点是不是存在
return;
}
Node* parent=node->parent;
if (node->left != NULL&&node->right != NULL) { //子节点都存在时
Node* right_min = node->right;
while (right_min->left != NULL) { //找到其右子树最小节点
right_min = right_min->right;
}
int num = right_min->key;
node->count = right_min->count;
DeletTree(num); //删除右子树最小节点
node->key = num; //将右子树最小值赋给该节点
}
else if(node->left==NULL&&node->right==NULL){ //当子节点都为空时
if (parent == NULL) { //先判断是否为根节点
root->key = 0;
root->count = 0;
}
else {
if (node == parent->left) {
parent->left = NULL;
}
else {
parent->right = NULL;
}
free(node);
}
}
else { //剩下情况是一个节点为空
Node* child = node->left == NULL ? node->right : node->left;
child->parent = parent;
if (parent == NULL) {
root = child;
}
else {
if (node == parent->left) {
parent->left = child;
}
else {
parent->right = child;
}
free(node);
}
}
}
PS:由于考虑到有的同学可能递归学的比较好,再提供一份递归版的删除
Node* DeletTree2(int key, Node* node) {
if (node != NULL) {
if (key == node->key) {
if (node->left != NULL&&node->right != NULL) {
Node* right_min = node->right;
while (right_min->left != NULL) {
right_min = right_min->left;
}
node->key = right_min->key;
node->count = right_min->key;
right_min->parent = DeletTree2(right_min->key, right_min->parent); //注意这里,巧妙运用了parent
}
else {
if (node->left == NULL&&node->right == NULL) {
free(node);
node = NULL;
}
else {
Node* child = node->left == NULL ? node->right : node->left;
free(node);
node = child;
}
}
}
else if (key < node->key) {
node->left = DeletTree2(key, node->left);
}
else {
node->right = DeletTree2(key, node->right);
}
}
return node;
}
使用的时候注意为 root=(DeletTree2(key,root)),防止删除的点刚好是root,此时root会改变。
大致思路和前面的是一样的,不多加注释了,有兴趣可以研究研究。
六.数据遍历
到了这里就很轻松了。遍历的情况分为前序遍历,中序遍历,后序遍历。具体是看我们访问节点和其两个子节点的顺序来决定。代码很简单,但是由于运用到了递归,可能在理解上会多少有些抽象,我尽量解释清楚吧。先上代码:
// N=node , L=left , R=right;
//NLR 前序遍历
//LNR 中序遍历
//LRN 后序遍历
void NLR(Node* node) { //前序遍历
if (node == NULL) {
return;
}
for (int i = 0; i < node->count; i++) {
printf("%d", node->key);
}
NLR(node->left);
NLR(node->right);
}
void LNR(Node* node) { //中序遍历
if (node == NULL) {
return;
}
LNR(node->left);
for (int i = 0; i < node->count; i++) {
printf("%d", node->key);
}
LNR(node->right);
}
void LRN(Node* node) { //后序遍历
if (node == NULL) {
return;
}
LRN(node->left);
LRN(node->right);
for (int i = 0; i < node->count; i++) {
printf("%d", node->key);
}
}
可以看出来,其实就是中间一段代码的执行顺序不同:
for (int i = 0; i < node->count; i++) {
printf("%d", node->key);
}
以第三节的[7,3,5,1,9,8,2,4]为例:
7
/ \
3 9
/ \ /
1 5 8
\ /
2 4
输入:[7,3,5,1,9,8,2,4]
前序:[7,3,1,2,5,4,9,8]
中序:[1,2,3,4,5,7,8,9]
后序:[2,1,4,5,3,8,9,7]
为什么仅仅调整了顺序就会有很大的不同呢?这就是递归的妙处所在,以中序输出为例:
void LNR(Node* node) {
if (node == NULL) {
return;
}
LNR(node->left) {
if (node == NULL) {
return;
}
LNR(node->left) {
......//重复
};
for (int i = 0; i < node->count; i++) {
printf("%d", node->key);
}
LNR(node->right);
};
for (int i = 0; i < node->count; i++) {
printf("%d", node->key);
}
LNR(node->right);
}
实际上在函数 LNR()递归调用自身的时候,我们就进入另一个函数了,只不过这个函数的操作步骤和 LNR()自己本身一样,这样不断递归下去,那么直到 某一次递归的node=NULL时才开始执行其他操作,所以最先访问的是二叉树最左端的数据。
不知道这样解释能不能理解,不行就自己多敲点代码吧~~~熟能生巧
七、总结
好了终于又到说拜拜的时候了,这里主要实现了二叉树的增删查和遍历,没有改(对于二叉树要写改的话就是先删再增)。写的不太好的地方多包涵。