数据结构之二叉搜索树详解(附C++代码实现查找、插入、删除操作)

  最近在分析分析红黑树时,感觉上来就挑最难的树结构之一进行分析,难度太大,所以特意写这篇二叉搜索树分析的博客作为铺垫。那么为啥挑二叉搜索树进行分析捏?其实红黑树也是一种更为复杂的二叉搜索树,建议阅读一下我的另外一篇博客 数据结构之二叉树、AVL树、红黑树、Trie树、B树、B+树、B*树浅析 。为了帮助大家理解红黑树,先写这篇博客分析二叉搜索树。下面将主要分析二叉搜索树的查找、插入、删除三种操作,并且附上C++代码实现。

一、二叉搜索树简述

  二叉搜索树大致定义为二叉树的左子树任意节点的值小于根节点的值,右子树任意节点的值大于根节点的值,并且左子树、右子树同样也符合二叉搜索树的定义(递归定义)。中序遍历顺序为左根右,所以二叉搜索树的典型特征是中序遍历序列有序。
在这里插入图片描述

二、二叉搜索树相关操作

  为了能让大家更好的理解二叉搜索树,将提供C++的编码实现。下面是TreeNode结构体实现:

/**
 * 这里额外添加了parent指针,主要是为了访问父节点方便
 */
struct TreeNode {
    int value;
    // 三个指针分别指向父节点、左子树根节点、右子树根节点
    TreeNode *parent;
    TreeNode *left;
    TreeNode *right;
    TreeNode(int value) : value(value), parent(NULL), left(NULL), right(NULL) {}
    TreeNode(int value, TreeNode *parent, TreeNode *left, TreeNode *right) {
        this->value = value;
        this->parent = parent;
        this->left = left;
        this->right =right;
    }
};

\color{red}温馨提示:查找插入删除节点三个操作的复杂度依次增加,如果觉得有点压力,请按照顺序依次阅读,稳扎稳打。

1、二叉搜索树中节点的查找

  二叉搜索树设计成左 < 根 < 右(中序遍历有序),一个很重要的动机就是快速查找。有过一点算法基础的应该能想到有一种搜索策略非常相似,没错就是二分搜索,每次将target与搜索区间(递增有序)的中间值mid比较,如果target < mid则缩小搜索区间为[left, mid - 1],如果target > mid则缩小搜索区间为[mid + 1, right],否则target == mid。辣么再来看看二叉搜索树中查找的伪代码。

root 指向二叉搜索树的根,target为需要搜索的值
while (root != NULL) {
	if root->value == target
		成功搜索到了 return
	else if root->value > target
		// 注意二叉搜索树的特征:root的右子树比root->value都大,root的左子树比root->value都小
		// 既然root->value > target,那么只可能出现在左子树,转移root到左子树
		root = root->left
	else
		// 否则root->value < target
		// 注意二叉搜索树的特征:root的右子树比root->value都大,root的左子树比root->value都小
		// 既然root->value < target,那么只可能出现在右子树,转移root到右子树
		root = root->right
}
// root == NULL,二叉搜索树中没有target

C++代码实现:

// 在二叉搜索树中查找target,存在返回对应的指针,否则返NULL
TreeNode *searchNode(TreeNode *root, int target){
    while (root != NULL) {
        if (root->value == target) {
            break;
        }
        else if (root->value > target) {
            // 注意二叉搜索树的特征:root的右子树比root->value都大,root的左子树比root->value都小
            // 既然root->value > target,那么只可能出现在左子树,转移root到左子树
            root = root->left;
        }
        else {
            // 否则root->value < target
            // 注意二叉搜索树的特征:root的右子树比root->value都大,root的左子树比root->value都小
            // 既然root->value < target,那么只可能出现在右子树,转移root到右子树
            root = root->right;
        }
    }
    return root;
}

2、二叉搜索树中节点的插入

  二叉搜索树中查找充分利用左 < 根 < 右特性,辣么插入也能用上这个特性么?答案是显然的。
  首先思考一下,我们插入节点后是不是还需要维持二叉树仍然满足二叉搜索树特性,这是必须的,要不能你的二叉搜索树就变成一次性的了。辣么我们就先要找到它真实应该插入的位置,保证中序遍历为递增有序。下面是删除的伪代码。

root 指向二叉搜索树的根,value为需要插入的值
if root == NULL
	// 二叉搜索树为空,插入的节点即是根节点
	root = new TreeNode(value)
else
	// 我们需要找到插入的位置
	TreeNode *ptr = root;
	while (true) {
		if ptr->value == value
			// 树中已经存在这个value,不进行插入(这里简化逻辑)
			break
		else if ptr->value > value
			// 注意二叉搜索树的特征:ptr的右子树比ptr->value都大,ptr的左子树比ptr->value都小
			// 既然ptr->value > value,value只能插入ptr左子树
			if ptr->left == NULL
				// 如果ptr左子树为空,则插入的节点正好做左子树的根
				ptr->left = new TreeNode(value)
				break
			else
				// 否则转移到左子树,继续查找
				ptr = ptr->left
		else
			// 注意二叉搜索树的特征:ptr的右子树比ptr->value都大,ptr的左子树比ptr->value都小
			// 既然ptr->value < value,value只能插入ptr右子树
			if ptr->right == NULL
				// 如果ptr右子树为空,则插入的节点正好做右子树的根
				ptr->right = new TreeNode(value)
				break
			else
				// 否则转移到右子树,继续查找
				ptr = ptr->right
	}
return root

C++代码实现:

// 在二叉搜索树中插入value,如果二叉树中已经存在则不进行插入(简化处理逻辑)
TreeNode *searchNode(TreeNode *root, int value) {
    if (root == NULL) {
        // 二叉搜索树为空,插入的节点即是根节点
        root = new TreeNode(value);
    }
    else {
        // 我们需要找到插入的位置
        TreeNode *ptr = root;
        while (true) {
            if (ptr->value == value) {
                // 树中已经存在这个value,不进行插入(这里简化逻辑)
                break;
            }
            else if (ptr->value > value) {
                // 注意二叉搜索树的特征:ptr的右子树比ptr->value都大,ptr的左子树比ptr->value都小
                // 既然ptr->value > value,value只能插入ptr左子树
                if (ptr->left == NULL) {
                    // 如果ptr左子树为空,则插入的节点正好做左子树的根
                    ptr->left = new TreeNode(value);
                    break;
                }
                else {
                    // 否则转移到左子树,继续查找
                    ptr = ptr->left;
                }
            }
            else {
                // 注意二叉搜索树的特征:ptr的右子树比ptr->value都大,ptr的左子树比ptr->value都小
                // 既然ptr->value < value,value只能插入ptr右子树
                if (ptr->right == NULL) {
                    // 如果ptr右子树为空,则插入的节点正好做右子树的根
                    ptr->right = new TreeNode(value);
                    break;
                }
                else {
                    // 否则转移到右子树,继续查找
                    ptr = ptr->right;
                }
            }
        }
    }
    return root;
}

3、二叉搜索树中节点的删除

  在二叉搜索树删除节点,首先我们应该在树中查找到这个节点的位置吧,然后将其移除,并且移除后我们需要进行调整,使其任然满足二叉搜索树。这个删除操作可以分成好几种情况,需要分别讨论。

①、删除叶节点

  删除叶节点,只要将其移除即可,不需要进行任何调整操作。
在这里插入图片描述

②、删除非叶节点

  删除非叶节点可以细分为两种,第一种是删除有右子树的节点,删除节点后需要将右子树中序遍历第一个节点填充到删除节点A位置(为什么选右子树中序遍历第一个节点?因为整棵树的中序遍历序列中,节点A的下一个节点就是其右子树中序遍历的第一个节点)。
  右子树中序遍历第一个节点为某个节点的左子节点,直接将最左的左子节点填补到已删除节点的位置。
在这里插入图片描述

a、删除节点有右子树

  右子树中序遍历序列中的第一个节点为某个没有左子节点的节点B。将节点B填入已删除节点的位置,并且将节点B的右子树置于节点B的位置。
右子树

b、删除节点没有右子树

  第二种是删除没有右子树的节点A,此时寻找整颗二叉树中序遍历中节点A的下一个节点,稍微复杂一点,需要利用parent指针。找到远祖父节点B,并且使得节点A在远祖父节点B的左子树中!远祖父节点B.value替换到节点A.value后,它自己也需要它的下一个节点来填充。
在这里插入图片描述
  \color{red}备注:其实根本不需要进行删除操作,只要寻找到这个节点中序遍历序列的下一个节点,然后直接替换即可。
C++代码实现:

// 在二叉搜索树中插入value,如果二叉树中已经存在则不进行插入(简化处理逻辑)
TreeNode *deleteNode(TreeNode *root, TreeNode *targetPtr) {
    if (targetPtr == NULL || root == NULL) {
        return root;
    }
    if (root == targetPtr && root->right == NULL) {
        // 一、删除的是根节点,并且根节点没有右子树
        // 处理:切断targetPtr与左子树的关联,返回左子树,释放targetPtr
        root = root->left;
        if (root != NULL) {
            root->parent = NULL;
        }
        targetPtr->left = NULL;
        delete targetPtr;
    }
    else if (targetPtr->left == targetPtr->right) {
        // 二、删除的叶节点
        // targetPtr->left == targetPtr->right,只能是同时为NULL
        // 操作:切断parent与targetPtr的关联,返回root,释放targetPtr
        if (targetPtr->parent->left == targetPtr) {
            targetPtr->parent->left = NULL;
        } else {
            targetPtr->parent->right = NULL;
        }
        // 切断targetPtr 与targetPtr->parent的关联
        targetPtr->parent = NULL;
        delete targetPtr;
    } else if (targetPtr->right == NULL) {
        // 三、删除节点右子树为空      需要找到远祖父节点
        TreeNode *pParent = targetPtr;
        // 祖父节点B,并且使得节点targetPtr在远祖父节点B的左子树
        while (pParent->parent != NULL && pParent->parent->right == pParent) {
            pParent = pParent->parent;
        }
        if (pParent->parent == NULL) {
            // 如果targetPtr不存在一个远祖父节点B,使得leftPtr在远祖父B的左子树
            // 操作:只能删除这个节点,并且把左子树放到当前节点的位置
            targetPtr->parent->right = targetPtr->left;
            targetPtr->left->parent = targetPtr->parent;
            // 切断targetPtr与parent、left的关系
            targetPtr->parent = NULL;
            targetPtr->left = NULL;
            delete targetPtr;
        }
        else {
            // 否则pParent->parent->value替换targetPtr->value,再删除pParent->parent(递归)
            targetPtr->value = pParent->parent->value;
            root = deleteNode(root, pParent->parent);
        }
    }
    else {
        // 四、删除节点存在右子树,直接在右子树寻找中序遍历的第一个节点
        TreeNode * leftPtr = targetPtr->right;
        // 一直往left寻找
        while (leftPtr->left != NULL) {
            leftPtr = leftPtr->left;
        }
        // 将leftPtr->value替换到targetPtr->value
        targetPtr->value = leftPtr->value;
        // targetPtr->right就是leftPtr
        if (targetPtr->right == leftPtr) {
            // 将targetPtr->right指向leftPtr右子树
            targetPtr->right = leftPtr->right;
            // 如果targetPtr->right != NULL,还需要设置parent
            if (leftPtr->right != NULL) {
                leftPtr->right->parent = targetPtr;
            }
        }
        else {
            // 否则leftPtr->parent与leftPtr切断关系
            leftPtr->parent->left = NULL;
        }
        // 将leftPtr于其parent切断关系并释放
        leftPtr->parent = NULL;
        delete leftPtr;
    }
    return root;
}

TreeNode *deleteNodeByValue(TreeNode *root, int value) {
    // 首先查找value所在的位置
    TreeNode *targetPtr = searchNode(root, value);
    if (targetPtr == NULL) {
        // value都没找到还删除啥...
        return root;
    }
    return deleteNode(root, targetPtr);
}

三、思考与总结

  可以看出二叉搜索树的查找、插入还是比较简单的,删除稍微复杂一点。不过二叉搜索树可能存在退化成链表的缺陷,比如给你一个本来递增有序的序列让你插入到一颗空二叉搜索树中,这时就退化成链表了。
在这里插入图片描述
  因此我们需要将二叉搜索树增加平衡的特性,即AVL树红黑树。后面有时间会更新一篇红黑树的博客,敬请期待~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章