今天来详细介绍下二叉搜索树的一些操作以及代码实现;
参考书籍:《算法导论第三版》(二叉搜索树的相关章节),《数据结构与算法》
一,二叉搜索树的定义
一颗二叉搜索树是以一颗二叉树来组织的,这样的一棵树可以使用一个链表数据结构来表示:
struct TreeNode
{
TreeNode* le; //需要注意,C语言中需要这样写:struct TreeNode* le,下同,写惯了C++的人这里是一个小坑
TreeNode* rh;
int value;
//有些时候会加上父亲节点 TreeNode* pa;
}
现在我们有了树的数据结构,可以给出二叉树的定义了: 设 X是二叉搜索树中的一个节点。如果Y是 X左子树中的一个节点,那么 必定有: Y.value<= X.value。 如果Y是X右子树中的一个节点,那么,必定有Y.value>=X.value ;
这是算法导论上的定义,翻译一下就是对于任意一个二叉搜索树中的节点,左子树中的任意一个节点的值都会小于它的值,右子树中的任意一个节点的值都会大于它的
值
二叉搜索树本身是自排序的,看定义就明白了,一直往右是最大值,一直往左是最小值,因此二叉搜索树可以作为一个优先级队列,这个以后介绍。
二叉搜索树的基本操作所花费的时间和这个树的高度是成正比的,对于一个有N个节点的二叉搜索树的各种操作的平均时间是O(logn); 当极度不平衡时,即任意节点没
有左子树或者右子树时,退化为链表;
二,二叉搜索树的插入操作
插入操作挺好理解的,就是从根节点开始往下走,如果插入的值大于等于该节点的值就往右子树走,否则就往左子树走,这里我就直接贴代码了,代码中有详细的注释
#include<iostream>
using namespace std;
struct TreeNode
{
TreeNode* le;
TreeNode* rh;
int value;
};
void addNodeToSearchBT(TreeNode** ptrPtrRoot, int k)
{
TreeNode* pa = nullptr; //用于保存插入位置的父节点,因为下面循环中出来的节点是nullptr
auto root = *ptrPtrRoot;
while (root != nullptr)
{
//循环遍历比较,判断该往左走还是往右走
pa = root;
if (k >= root->value)
root = root->rh;
else if (k < root->value)
root = root->le;
}
TreeNode* newNode = new TreeNode;
newNode->le = nullptr;
newNode->rh = nullptr;
newNode->value = k;
//这儿就是简单的判断大小然后插入就可以了
if (pa == nullptr)
{
*ptrPtrRoot = newNode; //二叉树为空
return;
}
if (k >= pa->value)
pa->rh = newNode;
else if (k < pa->value)
pa->le = newNode;
}
三,二叉搜索树的删除操作
这里感觉第一次看还是比较复杂的,会详细的介绍,一般来说都是使用被删除节点的右子树中的最小值作为替换节点,所以下面就不讨论使用左子树的最大值作为替换
节点的情况了;
首先第一步当然是找到这个要被删除的节点,假设这个节点是D, 需要注意的是,我们这里并没有使用双向链,所需需要保存D节点的父亲节点,因为,拼接子树的时候
需要用到父亲节点,这里假设为Dpa;,被用来替换的节点假设为X;
1.如果D是叶子节点,直接删除就OK,见图一
2.如果D有子树,并且只有右子树,那也就是可以直接将D的右子树接到Dpa上就可以,见图二
3.如果D有子树,并且只有左子树,那么直接将D的左子树接到Dpa上就可以,见图三
4.如果D有两个子树,那么就找到右子树中的最小值,也就是从D开始一直往左走,直到走到叶子左子树为空的节点,那么我们就找到可以被用来替换的节点了,见图四
图一 图二
图三 图四
接下来只需要把X接到D的位置上,并且把X的右子树接到它的父节点的左子树上就可以了;下面把代码贴上来,代码中注释的很详细
TreeNode* findNodeInSearchBT(TreeNode*root, int k, TreeNode**pa = nullptr)
{
//这里是不需要进行null判断的,因为后面隐式的判断了是否为null
while (root != nullptr)
{
if (k > root->value)
{
*pa = root;
root = root->rh;
}
else if (k < root->value)
{
*pa = root;
root = root->le;
}
else
break; //找到了相应的节点
}
//找到了相应的节点就返回该节点,若不存在该节点的父节点则返回nullptr
return root;
}
bool deleteNodeInSearchBT(TreeNode*root, int k)
{
TreeNode*PatoDelete = nullptr; //用于保存D的父节点
TreeNode* toDelete = findNodeInSearchBT(root, k, &PatoDelete); //找到D节点
//需要先判断一下是否找到了这个节点,没找到就直接返回false
if (nullptr == toDelete)
return false;
//D节点没有左子树,或者左右子树都没有 第一第二中情况
if (toDelete->le == nullptr)
{
if (k < PatoDelete->value)
PatoDelete->le = toDelete->rh; //需要把这个父节点的左节点替换为用来替换的节点
else
PatoDelete->rh = toDelete->rh; //这里就是右子树需要被替换了
delete toDelete;
return true;
}
//D节点没有右子树第三种情况
if (toDelete->rh == nullptr)
{
if (k < PatoDelete->value)
PatoDelete->le = toDelete->le; //需要把这个父节点的左节点替换为用来替换的节点
else
PatoDelete->rh = toDelete->le; //这里就是右子树需要被替换了
delete toDelete;
return true;
}
//D节点左右子树都有,第四种情况
auto minInRh = toDelete;
TreeNode* minInRhpa = nullptr; //这里用来保存用来替换的节点的父节点
while (minInRh->le != nullptr)
{
minInRhpa = minInRh;
minInRh = minInRh->le;
} //找到X节点,保存了其父节点
//其实这里就是对X做了一次删除操作,只不过这个X一定是第一种或者第二种情况
minInRhpa->le = minInRh->rh; //这里右子树是否非空是没有关系的;直接对X使用第一或者第二中方法删除,然后用X替换D
//需要把用来替换的节点的左右子树换成被删除子树的左右子树,不然就把树给弄断了
minInRh->le = toDelete->le;
minInRh->rh = toDelete->rh;
//一切准备就绪,可以替换了
if (k < PatoDelete->value)
PatoDelete->le = minInRh; //需要把这个父节点的左节点替换为用来替换的节点
else
PatoDelete->rh = minInRh; //这里就是右子树需要被替换了
//注意不要忘记释放空间了
delete toDelete;
//走到了这里说明一切都很顺利,可以返回true了
return true;
}
三,遍历操作
中序遍历:左子节点--父节点--右子节点
void midTraverse(TreeNode*root) { if (root == nullptr) return; midTraverse(root->le); cout << root->value; midTraverse(root->rh); }
前序遍历:父节点--左子节点--右子节点
void midTraverse(TreeNode*root)
{
if (root == nullptr) return;
cout << root->value;
midTraverse(root->le);
midTraverse(root->rh);
}
后序遍历:左子节点--右子节点--父节点
void midTraverse(TreeNode*root)
{
if (root == nullptr) return;
midTraverse(root->le);
midTraverse(root->rh);
cout << root->value;
}
四,二叉树的重建---通过三种遍历方式中的两种重建二叉树
待续。。。。。。