C语言实现线索二叉树和搜索树以及平衡树

线索二叉树

论述

背景:一棵节点数目为n的二叉树,如果采用二叉链表的形势进行存储,每个节点有两个指针域,共有n-1个有效分支路径,那么则二叉链表中存在2n-(n-1) = n + 1个 空指针域

我们对它进行中序遍历,得到顺序:DBEAC,可以得到A的前驱是E,A的后继是C,那么我们可不可以在遍历之前就得到节点的前驱和后继呢?如果得到,那么是不是查找效率就会大大提升。

我们将某节点的空指针域指向它的前驱和后继。有如下定义:
若节点左子树为空,该节点的左孩子指针指向它的前驱,若节点右子树为空,该节点的右孩子指针指向它的后继。这种指向前驱和后继的指针称为线索,将二叉树以某种次序遍历,添加线索的过程称之为线索化

进行线索化后可以得到:

那么我们应该如果区分每个节点的左右孩子指向的是其他节点还是前驱和后继,为了解决,需要增添标志位ltag和rtag,当ltag为0时,指向左孩子,为1时指向前驱,当rtag=0时,指向右孩子,为1时指向后继,那么一个节点可以定义如下:

注意:当没有左孩子时,那么指向前驱,为了寻找前驱,在遍历过程中,需要设置指针pre,时刻访问当前节点的前一个节点。

优势:加快查找节点的前驱和后继速度,当数据量很大时,性能提升很明显。

代码

首先定义它的节点以及树,节点包括数据,左指针,右指针,左标记,右标记,树包括指向头节点的指针以及节点的数量。

// Element是为了容易修改类型,这里是int,即data的类型为int
typedef int Element;
typedef struct treeNode {
    Element data;
    struct treeNode *left;
    struct treeNode *right;
    int lTag;
    int rTag;
}TreeNode;

typedef struct {
    TreeNode *root;
    int count;
}ThreadedBTree;

树的初始化以及节点的初始化,没什么好说的,直接看代码

ThreadedBTree* createThreadedBTree(TreeNode *root) {
    ThreadedBTree *tree = (ThreadedBTree *)malloc(sizeof (ThreadedBTree));
    if(tree == NULL) {
        printf("the tree err\n");
        return NULL;
    }
    if(root) {
        tree->root = root;
        tree->count = 1;
    } else {
        tree->count = 0;
        tree->root = NULL;
    }
    return tree;
}

TreeNode *createTreeNode(Element e) {
    TreeNode *node = (TreeNode *) malloc(sizeof (TreeNode));
    if(node == NULL) {
        printf("The node err\n");
        return NULL;
    }
    node->data = e;
    node->rTag = node->lTag = 0;
    node->left =  node->right = NULL;
    return node;
}

删除节点删除树:对于节点,运用递归,当lTag和rTag为0时,即都是指向的孩子节点,那么递归删除左右节点,最后释放节点以及数量的减少。

插入节点:
我们定义的方法传的参数有:树,父节点,左右孩子,那么这个插入就非常的简单,代码如下:

void insertTBTNode(ThreadedBTree *tree,TreeNode *parent,TreeNode *left,TreeNode *right) {
    if (tree && parent) {
        parent->right = right;
        parent->left = left;
        if(left) {
            ++tree->count;
        }
        if(right) {
            ++tree->count;
        }
    }
}

中序线索化:
我们以中序遍历的方法来构建中序线索化,这里是用的递归,因此我们可以得到:

void inorder(TreeNode *node) {
    if(node){
        inorder(node->left);
        // 处理当前节点
        inorder(node->right);
    }
}

那么对于当前节点应该怎么写?由于上面的递归一直向左走,因此此时的node->left是null,那么我们将它指向前驱pre,以及ltag设定成1,即:

if(node->left == NULL) {
    node->left = pre;
    node->lTag = 1;
}

对于右边来说,我们找不到后继节点,因此我们用前驱节点代替当前节点,后继节点为当前节点,那么就有,对于上面的那棵树,把D当作前驱,D的right要连接B,即(pre->right=node),D不是空,而且右孩子为空

if (pre && pre->right == NULL) {
    pre->right = node;
    pre->rTag = 1;
}

我们需要把处理过的节点给pre,因此有pre=node,

总体代码:

static void inOrderThreading(TreeNode *node) {
    if (node) {
        inOrderThreading(node->left);
        if(node->left == NULL) {
            node->left = pre;
            node->lTag = 1;
        }
        if (pre && pre->right == NULL) {
            pre->right = node;
            pre->rTag = 1;
        }
        pre = node;
        inOrderThreading(node->right);
    }
}

中序线索化遍历:

先找到第一个的最左边,直到左边的节点ltag=1;为什么?因为当ltag = 1时,他才是指向前驱,换句话说,当ltag = 0时,才是左孩子,才能向左走。当走到node->left=null时停下(即ltag = 1),这时去打印node,为什么呢?因为这个就是第一个节点!!!这是中序遍历。左边为空或者处理ok后,就打印当前节点。如果是有右孩子,那么就直接向右走,直到没有右孩子。当后继存在时,向后继走,并打印后继,因为在构建线索树时就已经知道了后继!!!代码如下:

void inOrderTBTree(ThreadedBTree *tree) {
    TreeNode *node = tree->root;
    while (node) {
        while (node->lTag == 0) {
            node = node->left;
        }
        visitTreeNode(node);
        while (node->rTag && node->right) {
            node = node->right;
            visitTreeNode(node);
        }
        node = node->right;
    }
}

二叉搜索树

论述:

如果左子树不空,左子树所有节点值小于根节点值,如果右子树不为空,右子树所有节点值大于根节点值,左右树也是搜索树。

插入:插入新节点在叶节点的位置,要建立父节点与新节点的关系,一般用递归进行实现,插入的值比当前节点小,放到左树,比它大放到右树
删除:(重点)

1.删除的是叶子节点:不影响二叉树排序规则,直接删。例如删除115
2.删除度为1的节点,直接返回它度的根节点,如果要删除120,90后面直接接上110
其实2和1可以看作一个,1是2的特殊情况,因此我们放在一起讨论
3.删除度为2的节点,找到它左树的最大值或者是右树的最小值进行替换,删除那个替换的值。
为什么这样子找进行替换,因为这样子删除时不影响树的结构。例如这里要删除30,用80进行替换,删除掉80就可以了

代码:

插入数据的递归代码:就是和当前节点比较大小,大的往右走,小的往左走

static BSNode *insertBSNodeRecur(BSTree *tree,BSNode *node,Element e) {
    if (node == NULL) {
        ++tree->count;
        return createBSNode(e);
    }  // 没有节点先进行创建节点
    if(e > node->data) {
        node->right = insertBSNodeRecur(tree,node->right,e);
    } else if(e < node->data){
        node->left = insertBSNodeRecur(tree,node->left,e);
    }
    return node;
}

删除数据:

如果当前数据比给定值大,向右子树递归,如果当前数据比给定值小,向左树递归。

static BSNode *deleteBSNode(BSTree *tree,BSNode*node,Element e) {
    if (node ==  NULL) {
        return NULL;
    }
    if (e > node->data) {
        node->right = deleteBSNode(tree,node->right,e);
    } else if (e < node->data) {
        node->left = deleteBSNode(tree,node->left,e);
    } else {
        // 相等的情况
    }
    return node;
}

找到相同的值,如果是只有单一分支或者没有分支,我们只需要用tmp节点来记录有分支的那一支,返回就行了。举个例子如图:

那么我们可以得到如下代码:

 if (node->left == NULL){
    tmp = node->right;
    --tree->count;
    return tmp;
    } 
if(node->right == NULL) {
    tmp = node->left;
    --tree->count;
    return tmp;
    }

对于两个分支的情况,我们这里是找右子树的最小值用以代替,因此把右子树传入一直向左走就可以了,用tmp进行接收,tmp的data赋给node的data,然后删除右子树那个最小的节点:

tmp = miniBSNodeRecur(node->right);
node->data = tmp->data;
node->right = deleteBSNode(tree,node->right,tmp->data);

平衡树

论述:

由于二叉搜索树在数列有序是会退化成链表,因此搜索效率低,为了解决二叉搜索树的这个问题,引出平衡树。

平衡因子:左子树高度-右子树高度;平衡树中只能取-1,0,1
对于失衡情况:我们分为左失衡和右失衡,对此我们有右旋和左旋来进行调整,可以把左旋和右旋看成两个工具。

右旋:找出失衡点,向右旋转失衡点,失衡点的左孩子节点的右孩子接入失衡点:代码如下(传入的为失衡点):

static AVLNode *rightRotate(AVLNode *y) {
    AVLNode *x = y->left;
    y->left = x->right;
    x->right = y;
    // 重新计算高度
    y->height = maxNum(h(y->left),h(y->right))+1;
    x->height = maxNum(h(x->left),h(x->right))+1;
    return x;
}

左旋:找出失衡点,向左旋转失衡点,失衡点的右孩子节点的左孩子接入失衡点:代码如下(传入的为失衡点)

static AVLNode *leftRotate(AVLNode *x) {
    AVLNode *y = x->right;
    x->right = y->left;
    y->left = x;
    // 重新计算高度
    x->height = maxNum(h(x->left),h(x->right))+1;
    y->height = maxNum(h(y->left),h(y->right))+1;
    return y;
}

其实真正的旋转分为四种情况:LL,LR,RL和RR。
第一个字母L/R表示它在失衡点的那一棵子树上;
如果是左树,那么看新插入导致失衡的点在左孩子的左边还是右边(或者说是左孩子的左子树还是右子树--这个就是第二个字母)
如果是右树,那么看新插入导致失衡的点在右孩子的左边还是右边(或者说是右孩子的左子树还是右子树--这个就是第二个字母)

对于LL(RR):直接对失衡节点进右旋(左旋)
对于LR:先对左孩子左旋,再对失衡节点右旋
对于RL:先对右孩子右旋,再对失衡节点左旋

代码:

这里演示平衡树的插入和删除

注意:这里旋转思路不要被函数名字给带偏!!!否则很难跳出

插入:

节点为空时,创建新的节点。传入的值大于节点的值,向右子树走,传入的值小于节点的值,向左子树走,相同时直接返回(平衡树中不能有相同节点)

if(node == null) {
    ++tree->count;
    return createNode(e);
}

if(e < node->data) {
    node->left = insertAVL(tree,node->left,e);
}
else if(e > node->data) {
    node->right = insertAVL(tree,node->right,e);
}
else {
    return node;
}

接下来处理平衡的情况;
获取节点的平衡状态,根据平衡状态来判断是否需要调整。若需要调整当左边高时(>1),需要右旋,右边高时(<-1),需要左旋。可以得到如下代码:(这里node是失衡点,从getBlance()就可以看出)

int balance = getBalance(node);
if(balance > 1) {  // 左边失衡,右旋
    return rightRotate(node);
} else if(balance < -1) {  // 右边失衡,左旋
    return leftRotate(node);
}

之后判断是在左(右)孩子的左(右)边:如果是左孩子的右边,对左孩子进行左旋;如果是右孩子的左边,对右孩子进行右旋。即

if(balance > 1) {
    if (e > node->left->data) {  // LR
        node->left = leftRotate(node->left);
    }
    return rightRotate(node);
} else if(balance < -1) {
    if (e < node->right->data) {
        node->right = rightRotate(node->right);  // RL
    }
    return leftRotate(node);
}

删除

这里的左右比较大小寻找删除的值以及平衡的调整和插入是一样的,这里我就不在多说了。直接讨论找到的情况。即(e==node.data)
这里和搜索树的删除逻辑相同,换一种写法:

当度为0或者1时:tmp只是一个辅助节点,tmp存储node的左(右)关系,将tmp的关系移到node,然后删除tmp,

AVLNode *tmp;
if (node->left == NULL || node->right == NULL) {
    tmp = node->left ? node->left :node->right;  // 哪一个节点存在要哪个,都没有是NULL
    // 度为0,直接删除
    if (tmp == NULL) {
        --tree->count;
        free(node);
        return NULL;
    } 
    // 度为1,直接连到下一个,相当与删除,以node->left为例
    else {
        node->data =tmp->data;     // node->data = node->left->data;
        node->left = tmp->left;    // node->left = node->left->left
        node->right = tmp->right;  // node->right = node->left->right;
        --tree->count;
        free(tmp);
    }
}

当度为2时:

这里我们找左子树的最大值(右子树的最小值也行),将得到的值赋给node,删除左数最大值,代码如下:

tmp = node->left;
while(tmp->right) {
    tmp = tmp->right;
}
node->data = tmp->data;
node->left = deleteNode(tree,node->left,tmp->data);

预告:下篇为C语言实现并查集和哈夫曼树
代码会到传到gitee上,下篇给地址(疯狂暗示)
(ง •_•)ง

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值