自学内容网 自学内容网

【C++】二叉搜索树

目录

1. 二叉搜索数概念

2. 二叉搜索树模拟实现

2.1 树节点类初始结构

2.2 树类初始结构

2.3 Insert(插入)

2.5 Find(查找)

2.6 Erase(删除)

2.7 中序遍历 

2.8 析构

2.9 拷贝构造

2.10 赋值重载

3. 二叉搜索树的性能

4. 二叉搜索树的应用 

5. 编程题

5.1  二叉树的最近公共祖先

5.2 二叉搜索树与双向链表

5.3 从前序与中序遍历序列构造二叉树

5.4 二叉树的前序遍历(非递归版本)


1. 二叉搜索数概念

2. 二叉搜索树模拟实现

2.1 树节点类初始结构

1. 一个指针指向左子树。

2. 一个指针指向右子树。

3. 一个变量存放数据。 

4. 构造函数:传入数据,初始化列表进行初始化。

template<class K>
struct BSTreeNode
{
//左右指向和数据
BSTreeNode<K>* _left;
BSTreeNode<K>* _right;
K _key;

//构造
BSTreeNode(const K& key)
:_left(nullptr)
,_right(nullptr)
,_key(key)
{}
};

2.2 树类初始结构

1. 私有成员变量只有一个根节点的指针。 

2.3 Insert(插入)

1. 如果树为空,那么用待插入的值创建节点作为根节点。插入成功返回true。

2. 如果树不为空,用cur标记根节点的位置,插入的值和cur位置的值比较,比你大,cur就去右边,比你小,cur就去左边,继续比较,直到cur走到空,就在cur位置创建一个节点。连接的话需要一个前驱指针,每次cur移动之前赋值给前驱指针。

3. 如果值相等就不插入,返回false。

4. 创建完新节点后需要判断插入在上面节点的哪一边,用值比较,如果比上面大就插在右边,小就插在左边。 

bool Insert(const K& key)
{
//判空
if (_root == nullptr)
{
_root = new Node(key);
return true;
}

Node* cur = _root;
Node* prev = nullptr;
//找位置
while (cur)
{
prev = cur;

if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return false;
}
}

//建节点
cur = new Node(key);
if (key > prev->_key)
{
prev->_right = cur;
}
else
{
prev->_left = cur;
}
return true;
}

【递归版本】

1. 从根节点开始比较,你比我大就去我的右子树,你比我小就去我的左子树,相等就不插入,走到空就插入,参数是节点的引用。

bool InsertR()
{
return _InsertR(_root);
}

bool _InsertR(Node*& root, const K& key)
{
//结束条件
if (root == nullptr)
{
root = new Node(key);
return true;
}

//找位置
if (key > root->_key)
{
return _InsertR(root->_right, key);
}
else if(key < root->_key)
{
return _InsertR(root->_left, key);
}
else
{
return false;
}
}

2.5 Find(查找)

1. 值比较,从根节点开始比,比你小就继续往左边比,比你大就往右边,遇到相等就是找到了,走到空就是没有。 

bool Find(const K& key)
{
Node* cur = _root;
while (cur)
{
if (key > cur->_key)
{
cur = cur->_right;
}
else if (key < cur->_key)
{
cur = cur->_left;
}
else
{
return true;
}
}
return false;
}

【递归版本】

1. 比我小就去我的左子树找,比我大就去我的右子树找,相等就返回true,找到空就返回false。

bool FindR(const K& key)
{
return _FindR(_root, key);
}

bool _FindR(Node* root, const K& key)
{
//结束条件
if (root == nullptr)
{
return false;
}

if (key > root->_key)
{
return _FindR(root->_right, key);
}
else if (key < root->_key)
{
return _FindR(root->_left, key);
}
else
{
return true;
}
}

2.6 Erase(删除)

删除的节点有四种情况:

a:节点无孩子

b:节点有左孩子

c:节点有右孩子

d:节点有两个孩子

1. 先找到要删除的节点,记录前驱指针。

2. 如果删除的节点左为空,那么前驱指针指向这个节点的右,释放这个节点,同时要判断前驱指针的哪一边指向你。特殊情况:如果这个节点是根节点,那么就将它的右作为根节点。 

3. 如果删除的节点右为空,和上面同理。

4. 如果删除的节点左右都空,和上面同理。

5. 如果删除的节点左右都不为空,需要替换法,一般选择左边最大或右边最小交换,先去右子树找最左边的节点,记得保留前驱指针,找到后和删除节点值交换,然后删除这个节点,前驱节点指向你的右边,记得判断前驱节点哪边指向你。

bool Erase(const K& key)
{
Node* cur = _root; 
Node* prev = nullptr;
while (cur)
{
if (key > cur->_key)
{
prev = cur;
cur = cur->_right;
}
else if (key < cur->_key)
{
prev = cur;
cur = cur->_left;
}
//开始删除
else
{
//左为空
if (cur->_left == nullptr)
{
if (cur == _root)
{
_root = cur->_right;
}
else
{
if (prev->_right == cur)
{
prev->_right = cur->_right;
}
else
{
prev->_left = cur->_right;
}
}
delete cur;
}
//右为空
else if (cur->_right == nullptr)
{
if (cur == _root)
{
_root = cur->_left;
}
else
{
if (prev->_right == cur)
{
prev->_right = cur->_left;
}
else
{
prev->_left = cur->_left;
}
}
delete cur;
}
//都不为空
else
{
//找替换
Node* change = cur->_right;
Node* prev = cur;
while (change->_left)
{
prev = change;
change = change->_left;
}

swap(cur->_key, change->_key);

if (prev->_right == change)
{
prev->_right = change->_right;
}
else
{
prev->_left = change->_right;
}
delete change;
}
return true;
}
}
return false;
}

【递归版本】

1. 比我大就去右子树,比我小就去左子树,空就删除失败,相等就开始删除。

2. 删除的节点左为空,先记录当前节点,在把节点的右边赋值给自己,因为当前root是上一层的别名。

3. 删除节点右为空同理。

4. 删除节点左右都不为空,先找到右子树的最小节点,然后替换,再去交给右子树删除。

bool EraseR(const K& key)
{
return _EraseR(_root, key);
}

bool _EraseR(Node*& root, const K& key)
{
if (root == nullptr)
{
return false;
}

if (key > root->_key)
{
return _EraseR(root->_right, key);
}
else if(key < root->_key)
{
return _EraseR(root->_left, key);
}
else
{
if (root->_left == nullptr)
{
Node* cur = root;
root = root->_right;
delete cur;
}
else if (root->_right == nullptr)
{
Node* cur = root;
root = root->_left;
delete cur;
}
else
{
Node* change = root->_right;
while (change->_left)
{
change = change->_left;
}

swap(root->_key, change->_key);

return _EraseR(root->_right, key);
}
}
}

2.7 中序遍历 

1. 左根右,遇到空就返回。

2. 先走左边,然后打印,然后走右边。

3. 由于需要传根节点,但外面拿不到,所以套一层无参的。

void InOrder()
{
_InOrder(_root);
}

void _InOrder(Node* root)
{
if (root == nullptr)
{
return;
}

_InOrder(root->_left);
cout << root->_key << " ";
_InOrder(root->_right);
}

2.8 析构

1. 利用后序思想,先释放左再释放右,最后释放自己,然后置空,参数可以传引用。 

~BSTree()
{
_Destroy(_root);
}

void _Destroy(Node*& root)
{
if (root == nullptr)
{
return;
}

_Destroy(root->_left);
_Destroy(root->_right);
delete root;
root = nullptr;
}

2.9 拷贝构造

1. 递归拷贝,先拷贝当前节点,再拷贝节点的左边,然后拷贝节点的右边。 

BSTree(const BSTree<K>& bst)
{
_root = _Copy(bst._root);
}

Node* _Copy(Node* bst)
{
if (bst == nullptr)
{
return nullptr;
}

Node* newroot = new Node(bst->_key);
newroot->left = _Copy(bst->_left);
newroot->_right = _Copy(bst->_right);

return newroot;
}

2.10 赋值重载

 1. 传值传参,利用拷贝构造传给参数,然后和参数交换。

BSTree<K>& operator=(BSTree<K> bst)
{
swap(_root, bst._root);
return *this;
}

3. 二叉搜索树的性能

1. 插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。 但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:logN

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N

4. 二叉搜索树的应用 

1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。本质是判断一个值在不在。

2. KV模型:每一个关键码key,都有与之对应的值Value,既可以判断key在不在,也可以通过key查找value。

5. 编程题

5.1  二叉树的最近公共祖先

链接:. - 力扣(LeetCode)

【思路1】

1. 两个节点都在公共祖先的两边,当某个节点是祖先是特殊情况。

2. 所以分三种情况节点在两边(或者节点是祖先),节点都在右边,节点都在左边,那么祖先从根节点开始不断往下调整。

3. 这个方法最坏情况是N方,当树是单分支的时候。

【思路2】

1. 记录两个节点的路径(记录节点),用栈,从根节点开始入栈比较,如果找到了就不继续了,如果没找到,就去左边找,左边找到了也不找了,左边没找到就去右边找,遇到空就是没找到,每次比较都是先入栈再比较,都没找到就出栈。

2. 找到两个节点路径之后,让长的出栈,直到两个路径长度相等。

3. 路径长度相等之后,不断出栈,直到两个栈顶元素相同就停下。

class Solution 
{
public:
    bool path(TreeNode* root, TreeNode* key, stack<TreeNode*>& st)
    {
        //空节点
        if(root == nullptr)
        {
            return false;
        }

        //相等就返回
        st.push(root);
        if(st.top() == key)
        {
            return true;
        }

        //左子树找到就返回
        if(path(root->left, key, st))
        {
            return true;
        }

        //右子树找到就返回
        if(path(root->right, key, st))
        {
            return true;
        }

        //都没找到
        st.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) 
    {
        //获取路径信息
        stack<TreeNode*> pst;
        stack<TreeNode*> qst;
        path(root, p, pst);
        path(root, q, qst);

        //减去多余路径
        while(pst.size() != qst.size())
        {
            if(pst.size() > qst.size())
            {
                pst.pop();
            }
            else
            {
                qst.pop();
            }
        }

        //找出相同节点
        while(pst.top() != qst.top())
        {
            pst.pop();
            qst.pop();
        }

        return pst.top();
    }
};

5.2 二叉搜索树与双向链表

链接:二叉搜索树与双向链表_牛客题霸_牛客网 

【思路】

1. 利用中序遍历,前后指针修改节点指针指向。

2. 每一层需要改变prev,需要加引用。

class Solution 
{
public:
void InOrder(TreeNode* cur, TreeNode*& prev)
{
if(cur == nullptr)
{
return;
}

InOrder(cur->left, prev);
cur->left = prev;
if(prev)
{
prev->right = cur;
}
prev = cur;
InOrder(cur->right, prev);
}
    TreeNode* Convert(TreeNode* pRootOfTree) 
{
        TreeNode* cur = pRootOfTree;
TreeNode* prev = nullptr;

InOrder(cur, prev);
TreeNode* head = pRootOfTree;
while(head && head->left)
{
head = head->left;
}
return head;
    }
};

5.3 从前序与中序遍历序列构造二叉树

链接:. - 力扣(LeetCode)

【思路】

1. 前序确定根,中序确定左右子树。前序是根左右,所以先构建左子树的根。

2. 划分左右区间,不断递归找根,同时利用返回值建立连接。

    TreeNode* Bulid(vector<int>& preorder, vector<int>& inorder, int& preIndex, int begin, int end)
    {
        if(begin > end)
        {
            return nullptr;
        }

        //找根
        int rootIndex = 0;
        while(rootIndex <= end)
        {
            if(inorder[rootIndex] == preorder[preIndex])
            {
                break;
            }
            ++rootIndex;
        }

        //左右区间递归,顺便建立连接
        TreeNode* root = new TreeNode(inorder[rootIndex]);
        ++preIndex;
        root->left = Bulid(preorder, inorder, preIndex, begin, rootIndex-1);
        root->right = Bulid(preorder, inorder, preIndex, rootIndex+1, end);

        return root;
    }
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) 
    {
        int preIndex = 0;
        TreeNode* root = Bulid(preorder, inorder, preIndex, 0, inorder.size()-1);
        return root;
    }

5.4 二叉树的前序遍历(非递归版本)

链接:. - 力扣(LeetCode) 

【思路】

1. 前序是先走根,再走左,再走右。

2. 将每颗树或子树划分为左路节点和左路节点的右子树。

3. 先走左路节点,每走一个节点它就是根,然后继续走它的左边,一直到左边为空意味着根走完了左边也走完了,接着就是走右边了。

4. 通过栈实现回退的效果,下面的节点根左右都走完就回退上面的节点走它的右。

    vector<int> preorderTraversal(TreeNode* root) 
    {
        vector<int> v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || !st.empty())
        {
            while(cur)
            {
                v.push_back(cur->val);
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* top = st.top();
            st.pop();
            cur = top->right;
        }
        
        return v;
    }

原文地址:https://blog.csdn.net/m0_71164215/article/details/142672543

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!