小星学DSA丨一文学完二叉树-进阶篇

这是小星学DSA系列的第三篇,我会记录我学习的过程与理解,希望能够帮到你。

本篇文章的思维导图如下,在文章的末尾,我会给出更加详细的思维导图。

上篇文章中我们介绍了二叉树的基础知识,这篇我们来介绍常用的二叉树进阶篇。

二叉搜索树

二叉搜索树的定义

虽然我们已经在前面两篇文章中都涉及到了二叉搜索树,这里还是再次给二叉搜索树正式的定义。

所有节点的值唯一,且左子树的值都小于该节点的值,右子树的值都大于该节点的值。

二叉搜索树的操作

小星学DSA丨一文学完红黑树篇中,我们其实已经介绍过二叉搜索树的操作,这里我们将这些操作再次介绍一遍。

查找

二叉搜索树是有序的,因此二叉搜索树的查找不需要遍历整个树,只需要根据需要查找的值与当前节点的值比较即可,可以大大降低查找的时间复杂度。

插入

二叉搜索树的插入不同于二叉树的插入,一个节点插入的位置是固定的。

我们用搜索的方式去遍历二叉搜索树,找到小于插入值的最大叶子节点,或大于插入值的最小叶子节点,这个节点的下方就是我们要插入的位置。

删除

二叉搜索树的删除首先仍是通过查找的方式,找到需要删除的节点。

由于在删除后仍需要保持二叉搜索树的性质,因此对于非叶子节点,需要找到合适的值替代,具体替代方式我在红黑树的删除也讲过一次。

主要分为以下三种情况

性能分析

平衡情况(接近完全二叉树)非平衡情况(退化成单链表)
搜索O(logn)O(n)
插入O(logn)O(n)
删除O(logn)O(n)

二叉平衡树

定义

二叉平衡树是左子树和右子树高度差至多为1的二叉搜索树。

平衡因子

为了更好的描述二叉平衡树,我们引入了平衡因子的概念,它的计算公式为

平衡因子 = 左子树高度 − 右子树高度 平衡因子=左子树高度-右子树高度 平衡因子=左子树高度右子树高度

有了平衡因子之后,我们就能重新定义二叉平衡树,即平衡因子为-1,0或1的二叉搜索树。

二叉平衡树的操作

左旋与右旋

在二叉平衡树中,为了维持平衡,我们也需要左旋与右旋这一操作。

💫小星说丨还记得我们在红黑树一节说的,左旋即:被左旋的节点成为左节点;右旋:被右旋的节点成为右节点

左旋与右旋都分两步:1. 把准父节点中,多余的子节点接到原父节点上 2. 准父节点与原父节点的父子关系调整,如下图所示

搜索

由于二叉平衡树也是二叉搜索树,因此二叉平衡树的搜索与二叉搜索树的搜索完全相同

插入

二叉平衡树,由于需要维持平衡,因此其插入过程分两步:

  1. 按照二叉搜索树的方式插入
  2. 平衡修正

当我们向二叉平衡树中插入节点后,若导致不平衡,则这一不平衡肯定最早出现在插入节点的祖父节点上,因此我们要对插入节点、父节点与祖父节点的关系进行平衡修正。

主要有以下几种情况:

  1. 祖父节点平衡因子>1,则插入节点和插入节点的父节点必然出现在祖父节点的左边,此时
    1. 若插入节点>父节点,父子方向不一致,则需要先将插入节点的父节点左旋使二者朝向一致,此时来到情况b
    2. 若插入节点与父节点方向一致,直接右旋祖父节点即可

  1. 祖父节点平衡因子< -1,则说明右边的链路更长,此时的处理与上面对称
    1. 若插入节点<父节点,父子方向不一致,则需要先将插入节点的父节点右旋
    2. 若父子方向一致,直接左旋插入节点的祖父节点

> 💫小星说丨插入只影响到了父节点和祖父节点,因此只需要根据这三个节点的形状进行适当的左右旋,使之变为“**^”**这样的形状就好了
> 

删除

二叉平衡树的删除同样分为两步:

  1. 按照二叉搜索树的方式删除
  2. 平衡修正

二叉树平衡树中最终删除的必定是叶子节点的位置,因此这一不平衡肯定最早出现在被删除节点的父结点处,因此我们要对这一节点进行平衡修正。

  1. 该节点平衡因子>1,则左链路更长,考虑左链路
    1. 该节点左子节点的平衡因子<0,即对左子节点而言,其右链路更长,因此我们需要先对左子节点进行左旋,以补偿左子节点 的左链路,来到情况b
    2. 该节点左子节点的平衡因子≥0, 则我们对该节点进行右旋,以补偿该节点的右链路

  1. 该节点平衡因子< -1,则右链路更长
    1. 该节点右子节点平衡因子>0,对右子节点来说,其左链路更长,因此需要先补偿右子节点的左链路
    2. 该节点右子节点平衡因子≤0,直接左旋补偿左链路

其实到这里我们发现,删除的修正与插入的修正都是一样的,即对平衡因子绝对值大于1的节点通过左旋或右旋补偿较短的那条链路,如果子节点需要补偿的方向与该节点一致则可直接补偿,否则,需要先补偿子节点,再补偿该节点。

性能分析

二叉平衡树由于能够保证树左右高度差不超过1,因此其操作的时间复杂度能够保持在最优

平衡情况(接近完全二叉树)
搜索O(logn)
插入O(logn)
删除O(logn)

手撕二叉进阶树

手撕二叉搜索树

节点定义

二叉搜索树的节点定义与二叉树相同,不需要额外的值

struct Node
{
    int data;
    Node *left;
    Node *right;
};

搜索

用递归的方式实现搜索

NodePtr searchTreeHelper(NodePtr node, int key)
    {
        if (node == nullptr || key == node->data)
        {
            return node;
        }

        if (key < node->data)
        {
            return searchTreeHelper(node->left, key);
        }
        return searchTreeHelper(node->right, key);
    }
NodePtr searchTree(int k)
    {
        return searchTreeHelper(this->root, k);
    }

插入

同样用递归的方式插入, 找到插入点

NodePtr insertNode(NodePtr node, int data)
    {
        if (node == nullptr)
            return newNode(data);

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

删除

我们也可以使用递归的方式实现删除,如下

其中,后继节点为右子树的最小节点,由findMin函数实现

void deleteNode(NodePtr &root, int value)
    {
        if (!root)
        {
            return;
        }
        if (value < root->data)
        {
            deleteNode(root->left, value);
        }
        else if (value > root->data)
        {
            deleteNode(root->right, value);
        }
        else
        {
            // 情况1&情况2的一部分:为叶子节点或只有右节点
            if (!root->left)
            {
                NodePtr temp = root->right;
                delete root;
                root = temp;
            }
            // 只有左节点
            else if (!root->right)
            {
                NodePtr temp = root->left;
                delete root;
                root = temp;
            }
            // 作于节点都没有,使用后继节点代替,并在右子树中删除后继节点
            else
            {
                NodePtr temp = findMin(root->right);
                root->data = temp->data;
                deleteNode(root->right, temp->data);
            }
        }
    }

NodePtr findMin(NodePtr node) {
    while (node->left) {
        node = node->left;
    }
    return node;
}

手撕二叉平衡树

节点定义

由于我们需要计算节点的平衡因子,因此在定义节点时,需要额外定义节点的高度,以方便平衡因子的计算。

struct Node
{
    int data;
    Node *left;
    Node *right;
    int height;
};

相应的,我们需要实现高度的计算与更新、平衡因子的计算等函数

int getHeight(NodePtr node)
    {
        if (!node)
        {
            return 0;
        }
        return node->height;
    }

    int getBalanceFactor(NodePtr node)
    {
        if (!node)
        {
            return 0;
        }
        return getHeight(node->left) - getHeight(node->right);
    }

    void updateHeight(NodePtr node)
    {
        node->height = max(getHeight(node->left), getHeight(node->right)) + 1;
    }

左旋与右旋

二叉平衡树的左旋与右旋除了需要实现定义好的行为之外,还需要对节点高度进行重新计算:

NodePtr leftRotate(NodePtr node)
    {
        // rotate
        NodePtr right = node->right;
        NodePtr rleft = right->left;

        right->left = node;
        node->right = rleft;

        // update height
        // only child-changed node need to update
        updateHeight(node);
				updateHeight(right);

        return right;
    }

    NodePtr rightRotate(NodePtr node)
    {
        // rotate
        NodePtr left = node->left;
        NodePtr lright = left->right;

        left->right = node;
        node->left = lright;

        // update height
        // only child-changed node need to update
        updateHeight(node);
				updateHeight(left);

        return left;
    }

查找

查找的实现与二叉搜索树相同

插入

我们使用递归的方式实现插入,在前序部分插入,后序部分处理平衡逻辑,这样就实现了自下至上的平衡。

NodePtr insertHelper(NodePtr node, int data)
    {
        // 1. 二叉搜索树式插入
        // 叶子节点插入位置
        if (!node)
        {
            return createNewNode(data);
        }
        if (data < node->data)
        {
            node->left = insertHelper(node->left, data);
        }
        else if (data > node->data)
        {
            node->right = insertHelper(node->right, data);
        }
        else
        {
            return node;
        }

        // 后序方式,更新当前节点平衡因子
        // 实际上在插入节点的祖父节点处才会进入以下的计算
        updateHeight(node);
        int balanceFactor = getBalanceFactor(node);

        // 情況1a
        if (balanceFactor > 1 && data > node->left->data)
        {
            node->left = leftRotate(node->left);
            return rightRotate(node);
        }
        // 情況1b
        if (balanceFactor > 1 && data < node->left->data)
        {
            return rightRotate(node);
        }
        // 情況2a
        if (balanceFactor < -1 && data < node->right->data)
        {
            node->right = rightRotate(node->right);
            return leftRotate(node);
        }
        // 情況2b
        if (balanceFactor < -1 && data > node->right->data)
        {
            return leftRotate(node);
        }

        return node;
    }

NodePtr insertNode(int data)
    {
        insertHelper(this->root, data);
    }

删除

NodePtr deleteHelper(NodePtr node, int data)
    {
        // 1. 二叉搜索树方式删除节点
        if (!node)
        {
            return node;
        }
        if (data < node->data)
        {
            node->left = deleteHelper(node->left, data);
        }
        else if (data > node->data)
        {
            node->right = deleteHelper(node->right, data);
        }
        else
        {
            if (!root->left)
            {
                NodePtr temp = root->right;
                delete root;
                root = temp;
            }
            else if (!root->right)
            {
                NodePtr temp = root->left;
                delete root;
                root = temp;
            }
            else
            {
                NodePtr temp = findMin(root->right);
                root->data = temp->data;
                deleteHelper(root->right, temp->data);
            }
        }

        // 2.后序方式,更新当前节点平衡因子并进行平衡修正
        updateHeight(node);
        int balanceFactor = getBalanceFactor(node);

        // 情況1a
        if (balanceFactor > 1 && getBalanceFactor(node->left) < 0)
        {
            node->left = leftRotate(node->left);
            return rightRotate(node);
        }
        // 情況1b
        if (balanceFactor > 1 && getBalanceFactor(node->left) >= 0)
        {
            return rightRotate(node);
        }

        // 情況2a
        if (balanceFactor < -1 && getBalanceFactor(node->right) > 0)
        {
            node->right = rightRotate(node->right);
            return leftRotate(node);
        }
        // 情況2b
        if (balanceFactor < -1 && getBalanceFactor(node->right) <= 0)
        {
            return leftRotate(node);
        }

        return node;
    }

public:
    NodePtr  insertNode(int data)
    {
        insertHelper(this->root, data);
    }

    NodePtr deleteNode(int data)
    {
        deleteHelper(this->root, data);
    }

总结

最后我们再用一张思维导图总结这篇博客的内容

源代码

本文代码已在github上开源,包含c++,python(待补充), golang(待补充)的红黑树代码

https://github.com/Yuxin1999/star-code

参考内容

  1. https://www.programiz.com/dsa/avl-tree
  2. 萨尼 S, 王立柱, 刘志红 数据结构、算法与应用 C++语言描述[M]. 北京: 机械工业出版社, 2015.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值