有序二叉树的释放(要点剖析)

二叉树如何释放?

        如果我们直接释放根节点那么我们将无法找到后续节点,那么遗留的数据将会成为脏数据           正确的做法应该是通过递归先释放这颗子树的左孩子再释放右孩子最后再释放其根节点,根据这个顺序我们想到了利用后序遍历,一级子树一级子树的释放,接下来看实现过程。

二叉树的释放

正确做法:

//二叉树的释放
void tree_release(node_t** n) { 
    if (*n == NULL) {      
        return;
}
     tree_release(&(*n)->left);
     tree_release(&(*n)->right);
     free(*n);
     *n = NULL;  // 这次将原始指针置为 NULL
}


        这样做的好处是,你不仅能释放内存,还能确保调用者的指针在释放后被设置为 NULL,从而避免继续使用已经无效的指针。

       调用这个函数时要传入指针的地址:

tree_release(&tree.root);

错误做法:

//释放二叉树
void tree_release(node_t* n){
    if(n == NULL){
        return;
    }
    tree_release(n->left);
    tree_release(n->right);
    free(n);
    n = NULL;
}

        

1. 指针的局部性

        首先,理解什么是 值传递指针的局部性 非常重要。

值传递

        在C语言中,当你调用一个函数并传递参数时,默认情况下这些参数是通过 值传递 的。值传递意味着,在函数内部,你操作的是原始变量的一个副本,而不是原始变量本身。

例如,考虑下面的代码:

        

void modifyValue(int x) {
    x = 10;
}

int main() {
    int y = 5;
    modifyValue(y);
    printf("%d\n", y);  // 输出仍然是5
    return 0;
}

        在这个例子中,modifyValue 函数修改了 x,但是 main 函数中的 y 并没有改变。原因是 modifyValue 中的 xy 的一个副本,对 x 的修改不会影响 y。

指针的局部性

        当你在函数中传递指针时,情况也是类似的。虽然你传递的是一个指针,但这个指针本身在函数内部也是一个副本。

例如:

void modifyPointer(int* p) {
    p = NULL;
}

int main() {
    int a = 5;
    int* ptr = &a;
    modifyPointer(ptr);
    printf("%p\n", ptr);  // 输出的仍然是指向a的地址,而不是NULL
    return 0;
}

        在这个例子中,modifyPointer 函数将 p 设置为 NULL,但这并不影响 main 函数中的 ptr,因为 pptr 的副本,modifyPointer 中的操作只影响这个副本,不影响原始指针。

2. 释放内存后的行为

        当你调用 free() 函数时,它会释放指针所指向的内存,但它并不会改变指针本身的值。也就是说,指针仍然会指向原来的地址,只不过那个地址上的内存已经被释放了。

未定义行为

        如果你尝试访问一个已经被释放的内存地址,结果是 未定义行为。未定义行为意味着程序可能会:

  • 崩溃:访问无效内存通常会导致程序崩溃。
  • 输出随机数据:有时,已经释放的内存区域并不会立即被系统回收,你可能会看到原来的数据,但这些数据是无效的,因为内存已经不再属于你的程序。
  • 其他不可预测的行为:未定义行为意味着程序可能做任何事情,完全不受控制。

3. 在 tree_release 函数中的应用

现在回到tree_release 函数:

void tree_release(node_t* n){
    if(n == NULL){
        return;
    }
    tree_release(n->left);
    tree_release(n->right);
    free(n);
    n = NULL;  // 这里将局部变量 n 置为 NULL
}

        在这个函数中,n 是通过值传递传入的,即 n 是调用者传递的指针的副本。即使你在函数中将 n 设置为 NULL,这个修改也只影响 n 的副本,并不会改变调用者的指针。因此,调用者的指针在函数返回后依然指向之前分配的内存地址,而这段内存实际上已经被释放。

        这就解释了为什么在调用 tree_release 后,你的遍历函数还能遍历出数据。因为这些数据虽然来自已经释放的内存,但内存的内容尚未被覆盖或回收,所以你能看到这些数据,但这是危险的,继续使用这些数据可能导致崩溃或其他问题。

二叉树完整代码:

tree.h

//二叉树的头文件
#ifndef __TREE_H__
#define __TREE_H__
//节点
typedef struct node{
    int data;//数据域
    struct node* left;//记录做指针域指向节点的地址
    struct node* right;//记录右指针域指向节点的地址
}node_t;

//二叉树
typedef struct tree{
    node_t* root;//记录根节点地址
    int cnt;//记录元素个数
}tree_t;

//tree_t tree;

//初始化
void tree_init(tree_t* t);
//插入节点
void tree_insert(tree_t* t,int data);
// 删除节点
void tree_del(tree_t* t,int data);
//前序遍历
void tree_first(node_t* n); 
//中序遍历
void tree_mid(node_t* n);
//后序遍历
void tree_last(node_t* n);
//二叉树的释放
void tree_release(node_t** n);
#endif

tree.c 

//树的实现
#include<stdio.h>
#include<stdlib.h>
#include"tree.h"

//初始化
//tree_t tree; tree.root tree.cnt
void tree_init(tree_t* t){
    t->root = NULL;
    t->cnt = 0;
}
//插入节点
void tree_insert(tree_t* t,int data){
    //创建新节点
    node_t* new = malloc(sizeof(node_t));
    new->data = data;
    new->left = NULL;
    new->right = NULL;
    //如果root为空,说明还没有根节点
    //此时的新节点即作为根节点
    if(t->root == NULL){
        t->root = new;
        t->cnt++;
        return;
    }
    //找位置插入
    //p1,p2确定插入位置
    node_t* p1,*p2;
    p1 = p2 = t->root;
    while(p2 != NULL){
        //p1 慢 p2 一步
        p1 = p2;
        if(p2->data > data){
            p2 = p2->left;
        }else{
            p2 = p2->right;
        }
    }
    //循环结束p2指向NULL,p1指向的节点即为新节点的父节点
    //判断新节点,挂在父节点的哪一侧
    if(p1->data > data){
        p1->left = new;
    }else{
        p1->right = new;
    }
    //计数加一
    t->cnt++;
}

//删除节点
void tree_del(tree_t* t,int data){
    node_t* ptar = t->root;//要删除的节点
    node_t* pnode = t->root;// 要删除节点的父节点
    while(ptar->data != data || ptar == NULL){
        //pnode 慢 ptar 一步
        pnode = ptar;
        //ptar先走,找要删除的节点
        if(ptar->data > data){
            ptar = ptar->left;
        }else{
            ptar = ptar->right;
        }
    }
    //如果ptar指向节点的数据和要删除节点的数据相同,则找到删除目标
    //ptar指向要删除的目标,pnode指向的就是目标父节点/
    //如果循环结束后,ptar为空,则要删除的节点不存在
    if(ptar == NULL){
        printf("没找到要删除的节点\n");
        return;
    }
    
    //left记录要删除节点的左子树
    //right记录要删除节点的右子树
    node_t* left = ptar->left;
    node_t* right = ptar->right;
    
    //左右子树都为空
    if(left == NULL && right == NULL){
        //让父节点 指向 目标节点的指针为空
        if(ptar->data > pnode->data){
            pnode->right = NULL;
        }else{
            pnode->left = NULL;
        }
        //释放目标节点
        free(ptar);
        ptar = NULL;
        t->cnt--;
    }

    //目标节点只有右子树
    if(left == NULL && right != NULL){
        //判断目标节点的右子树应该挂在父节点的哪一侧
        if(ptar->data > pnode->data){
            pnode->right = right;
        }else{
            pnode->left = right;
        }
        //释放节点
        free(ptar);
        ptar = NULL;
        t->cnt--;
    }

    //目标节点只有左子树
    if(left != NULL && right == NULL){
        //判断目标节点的右子树应该挂在父节点的哪一侧
        if(ptar->data > pnode->data){
            pnode->right = left;
        }else{
            pnode->left = left;
        }
        //释放节点
         free(ptar);
         ptar = NULL;
         t->cnt--;
    }

    //目标节点既有左子树也有右子树
    if(left != NULL && right != NULL){
        //将左右子树合成一颗新树
        //找到右子树最左侧的位置,将左子树挂上
        node_t* p1,*p2;
        p1 = p2 = right;
        while(p2 != NULL){
            //p1 慢 p2 一步
            p1 = p2;
            //p2不停左移
            p2 = p2->left;
        }
        //循环结束后,p2为NULL,p1记录右子树上最左侧位置
        //将左子树挂在右子树最左侧位置
        //right表示新合成的树
        p1->left = left;
        
        //新树挂在父节点的哪一侧?
        if(ptar->data < pnode->data){
            pnode->left = right;
        }else{
            pnode->right = right;
        }
        //释放节点
        free(ptar);
        ptar = NULL;
        t->cnt--;
    }
}

//前序遍历
void tree_first(node_t* n){
    if(n == NULL){
        return;
    }
    printf("%d ",n->data);
    tree_first(n->left);
    tree_first(n->right);
}
//中序遍历
void tree_mid(node_t* n){
    if(n == NULL){
        return;
    }
    tree_mid(n->left);
    printf("%d ",n->data);
    tree_mid(n->right);
}
//后序遍历
void tree_last(node_t* n){
    if(n == NULL){
        return;
    }
    tree_last(n->left);
    tree_last(n->right);
    printf("%d ",n->data);
}
//二叉树的释放
void tree_release(node_t** n) { 
    if (*n == NULL) {      
        return;
}
     tree_release(&(*n)->left);
     tree_release(&(*n)->right);
     free(*n);
     *n = NULL;  // 这次将原始指针置为 NULL
}


 main.c

//树的测试
#include<stdio.h>
#include"tree.h"


int main(){
    //定义树
    tree_t tree;
    //初始化树
    tree_init(&tree);
    //插入节点
    tree_insert(&tree,60);
    tree_insert(&tree,30);
    tree_insert(&tree,90);
    tree_insert(&tree,10);
    tree_insert(&tree,40);
    tree_insert(&tree,50);
    tree_insert(&tree,20);
    tree_insert(&tree,70);
    tree_insert(&tree,80);
    tree_insert(&tree,100);
    //遍历
    tree_first(tree.root);
    printf("\n");
    tree_mid(tree.root);
    printf("\n");
    tree_last(tree.root);
    printf("\n");
    //删除
    tree_del(&tree,30);
    //遍历
    tree_first(tree.root);
    printf("\n");
    tree_mid(tree.root);
    printf("\n");
    tree_last(tree.root);
    printf("\n");
    //释放二叉树
    tree_release(&tree.root);
    //遍历
    tree_first(tree.root);
    printf("\n");
    tree_mid(tree.root);
    printf("\n");
    tree_last(tree.root);
    printf("\n");
    return 0;
}

 运行结果

错误释放(后三行代码):

60 30 10 20 40 50 90 70 80 100 
10 20 30 40 50 60 70 80 90 100 
20 10 50 40 30 80 70 100 90 60 
60 40 10 20 50 90 70 80 100 
10 20 40 50 60 70 80 90 100 
20 10 50 40 80 70 100 90 60 
38305856 38305952 38305984 38305824 38305888 38306080 38306048 38305920 38306016 
38305984 38305824 38305952 38305888 38305856 38306048 38305920 38306080 38306016 
38305824 38305984 38305888 38305952 38305920 38306048 38306016 38306080 38305856


正确释放(后三行为空):
60 30 10 20 40 50 90 70 80 100 
10 20 30 40 50 60 70 80 90 100 
20 10 50 40 30 80 70 100 90 60 
60 40 10 20 50 90 70 80 100 
10 20 40 50 60 70 80 90 100 
20 10 50 40 80 70 100 90 60 



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值