二叉搜索树的介绍:
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树
二叉搜索树操作
1.二叉搜索树的查找:
若根结点不为空:
如果根节点的val == 查找的val直接返回true
如果根节点的val > 查找的val那就去根节点的右子树去寻找
如果根节点的val < 查找的val那就去根节点的左子树去寻找
否则返回false
2.二叉搜索树的插入
二叉树的插入位置只有三种可能
1》左子树为空的结点
2》右子树为空的结点
3》叶子结点
插入的结点一定不会是左右孩子都在的结点,这个是由二叉树的性质来决定的,要是还是没有办法理解的话可以画个图看一看
a.删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点
b.删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点
c.在它的右子树中寻找中序下的第一个结点(关键码最小),用它的值填补到被删除节点中,再来处理该结点的删除问题
template<class T>
struct BSTNode
{
BSTNode(T _value = T())
:left(nullptr), right(nullptr)
, value(_value)
{}
BSTNode<T>* left;
BSTNode<T>* right;
T value;
};
template<class T>
class BSTree
{
typedef BSTNode<T> Node;
public:
BSTree()
:root(nullptr)
{}
~BSTree()
{
_destory(root);
}
BSTree(const BSTree& tree)
{
this->root = _copy(tree.root);
}
Node* _copy(Node* _root)
{
if (_root == nullptr)
return nullptr;
Node* temp = new Node(_root->value);
temp->left = _copy(_root->left);
temp->right = _copy(_root->right);
return temp;
}
BSTree& operator=(const BSTree& tree)
{
if (this != &tree)
{
_destory(this->root);
this->root = _copy(tree.root);
}
return *this;
}
bool Insert(const T& val)
{
if (root == nullptr)
{
root = new Node(val);
return true;
}
else
{
Node* pCur = root; //用来查找结点
Node* pParent = nullptr; //用来记录当前正在查找结点的双亲结点
while (pCur)
{
pParent = pCur;
if (pCur->value > val)
pCur = pCur->left;
else if (pCur->value < val)
pCur = pCur->right;
else
return false; //默认是不能插入重复的元素
}
pCur = new Node(val);
if (pParent->value > val)
pParent->left = pCur;
else
pParent->right = pCur;
}
return true;
}
Node* Find(const T& val)
{
if (root == nullptr)
return nullptr;
Node* pCur = root;
while (pCur)
{
if (pCur->value > val)
pCur = pCur->left;
else if (pCur->value < val)
pCur = pCur->right;
else
return pCur;
}
return nullptr;
}
bool Erase(const T& val)
{
if (root == nullptr)
return false;
//遍历整个二叉搜索树,找val
Node* pCur = root;
Node* pParent = nullptr;
while (pCur)
{
if (pCur->value == val)
break;
else if (pCur->value > val)
{
pParent = pCur; //这一快只能这么写,之前我把pParent = pCur放在外面,但是但是错了,这里的逻辑就是这样,不理解的话可以画图
pCur = pCur->left;
}
else
{
pParent = pCur;
pCur = pCur->right;
}
}
//不存在val
if (pCur == nullptr)
return false;
//1.待删除结点的左孩子为空
//2.待删除结点的右孩子为空
//3.待删除结点的左右孩子都不为空
//4.待删除结点的左右孩子都为空
if (pCur->left == nullptr && pCur->right == nullptr) //左右孩子都为空
{
if (pParent->left == pCur)
pParent->left = nullptr;
else
pParent->right = nullptr;
delete pCur;
}
else if (pCur->left == nullptr) //待删除的结点是左孩子为空的情况
{
if (pCur->value == val) //刚好根节点就是要删除的结点
{
root = pCur->right; //因为没有左孩子所以树的头指针,直接指向根节点的右孩子就可以
}
else //删除的结点不是头结点
{
//判断删除结点是双亲结点的左孩子还是右孩子
if (pParent->left == pCur)
pParent->left = pCur->right; //如果是删除结点是双亲结点的左孩子,那就让删除节点的双亲结点的左孩子指向删除结点的右孩子
else
pParent->right = pCur->right; //反之相反
}
delete pCur;
}
else if (pCur->right == nullptr) //待删除结点的右孩子为空
{
if (pCur->value == val)
{
root = pCur->left;
}
else //删除的结点不是头结点
{
//判断删除结点是双亲结点的左孩子还是右孩子
if (pParent->left == pCur)
pParent->left = pCur->left; //如果是删除结点是双亲结点的左孩子,那就让删除节点的双亲结点的左孩子指向删除结点的右孩子
else
pParent->right = pCur->left; //反之相反
}
delete pCur;
}
else //待删除结点的左右节点都不为空的时候
{
if (pCur->left != nullptr && pCur->right != nullptr)
{
//去找待删除结点的右孩子中的最小值,或者左孩子中的最大值,其实都一样,这里我们就使用右孩子中的最小值
Node* p = pCur->right;
pParent = pCur; //标记双亲结点,这里的标记主要是为了防止没有进入下面while循环的情况
while (p->left)
{
pParent = p; //标记双亲结点
p = p->left;
}
pCur->value = p->value;//替换
//下面这两种情况比较特殊需要拿出来讨论
if (p == pParent->left)
{
pParent->left = p->right;
}
else
{
pParent->right = p->right;
}
delete p;
}
return true;
}
return false;
}
void Inorder()
{
_Inorder(this->root);
}
private:
void _Inorder(Node* tree)
{
if (tree == nullptr)
return;
_Inorder(tree->left);
cout << tree->value << " ";
_Inorder(tree->right);
}
void _destory(Node*& tree) //为了在内部改变外部的指针,这里使用的就是指针的引用
{
if (tree)
{
_destory(tree->left);
_destory(tree->right);
delete tree;
tree = nullptr;
}
}
private:
Node* root;
};
int main()
{
BSTree<int> BST;
BST.Insert(1);
BST.Insert(3);
BST.Insert(7);
BST.Insert(5);
BST.Insert(4);
BST.Insert(6);
BST.Insert(2);
BST.Inorder();
BST.Erase(1);
cout << endl;
BST.Inorder();
cout << endl;
BST.Erase(2);
BST.Inorder();
cout << endl;
BST.Erase(5);
BST.Inorder();
cout << endl;
BST.Erase(4);
BST.Inorder();
cout << endl;
cout << BST.Find(7)<< endl;
BSTree<int> BST2;
BST2 = BST;
BST2.Inorder();
BSTree<int> BST3(BST2);
BST3.Inorder();
return 0;
}
先面对这段代码的中我出现的问题进行分析:
首先说的是删除结点中的一种情况,就是左右子树都不为空的情况
需要画两张图
两种情况已经给出。
其次是下面这两段代码
上面这两段代码中,一个是插入结点的代码,一个是删除结点的代码,在插入和删除的过程中,都需要涉及的是,找结点,在插入结点的时候需要寻找的是合适的结点,这个合适结点的左边,或者右边
在寻找合适的结点的过程中需要时刻记录当前遍历结点的双亲结点,但是保存双亲节点的方式不同。
在插入结点的时候,找到最后pCure就是一个空结点,而真正要的是,在pCure的双亲结点pParent的左边或者右边进行插入。
而在删除结点时候,同样需要先遍历整个树,在遍历整个树的时候,需要保存当前正在遍历结点的双亲结点,而不一样的是在删除结点的时候,pCure不再是空结点,而是真正的需要被删除的点,而pParent也就是待删除结点的双亲结点,所以这两段代码是不一样的。
还有什么问题就是扣代码。
二叉数的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树,其平均比较次数为:log n;
最差情况下,二叉搜索树退化为单支树,其平均比较次数为:n / 2;
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,都可以使二叉搜索树的性能最佳?
AVL(平衡二叉搜索树)