目录
- 一、内容安排说明
- 二、 二叉搜索树
- 2.1 二叉搜索树概念
- 2.2 二叉搜索树操作
- 2.2.1 二叉搜索树的查找
- 2.2.2 二叉搜索树的插入
- 2.2.3 二叉搜索树的删除
- 2.3 二叉搜索树的代码实现
- 2.3.1 二叉搜索树的节点设置
- 2.3.2 二叉搜索树类的框架
- 2.3.3 二叉搜索树的查找函数
- 2.3.3.1 非递归方式实现
- 2.3.3.2 递归方式实现
- 2.3.4 二叉搜索树的插入函数
- 2.3.4.1 非递归方式实现
- 2.3.4.2 递归方式实现
- 2.3.5 二叉搜索树的中序遍历
- 2.3.6 二叉搜索树的删除函数
- 2.3.6.1 非递归实现
- 2.3.6.2 递归实现
- 2.3.7 二叉搜索树代码的整体实现
- 2.4 二叉搜索树的应用
- 2.5 二叉搜索树的性能分析
- 三、二叉树进阶题目
- 3.1 根据二叉树创建字符串[【OJ题目】](https://leetcode.cn/problems/construct-string-from-binary-tree/description/)
- 3.2 二叉树的层序遍历Ⅰ[【OJ题目】](https://leetcode.cn/problems/binary-tree-level-order-traversal/description/)
- 3.3 二叉树的层序遍历Ⅱ[【OJ题目】](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/description/)
- 3.4 二叉树的最近公共祖先[【OJ题目】](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/description/)
- 3.5 二叉搜索树与双向链表[【OJ题目】](https://www.nowcoder.com/practice/947f6eb80d944a84850b0538bf0ec3a5?tpId=13&&tqId=11179&rp=1&ru=/activity/oj&qru=/ta/coding-interviews/question-ranking)
- 3.6 从前序与中序遍历序列构造二叉树[【OJ题目】](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/description/)
- 3.7 从中序与后序遍历序列构造二叉树[【OJ题目】](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/description/)
- 3.8 二叉树的前序遍历,非递归迭代实现[【OJ题目】](https://leetcode.cn/problems/binary-tree-preorder-traversal/)
- 3.9 二叉树的中序遍历 ,非递归迭代实现[【OJ题目】](https://leetcode.cn/problems/binary-tree-inorder-traversal/description/)
- 3.10 二叉树的后序遍历 ,非递归迭代实现 [【OJ题目】](https://leetcode.cn/problems/binary-tree-postorder-traversal/description/)
- 结尾
一、内容安排说明
二叉树在前面C数据结构阶段已经讲过,本节取名二叉树进阶是因为:
map
和set
特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构- 二叉搜索树的特性了解,有助于更好的理解map和set的特性
- 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘
- 有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。
因此本节借二叉树搜索树,对二叉树部分进行收尾总结。
大家可以看一下这篇文章进行简单的复习
【数据结构】非线性结构之树结构(含堆)
二、 二叉搜索树
2.1 二叉搜索树概念
二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:
- 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
- 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
- 它的左右子树也分别为二叉搜索树
2.2 二叉搜索树操作
2.2.1 二叉搜索树的查找
- 从根节点开始查找,若查找值比根节点小,继续向左子树寻找,若查找值比根节点打,继续向右子树寻找。
- 最多查找高度次,若查找到空,那么在这棵树中没有这个值。
2.2.2 二叉搜索树的插入
- 若树为空,创建一个节点,赋值给root。
- 若树不为空,按照二叉搜索树的性质查找插入位置,插入新节点
2.2.3 二叉搜索树的删除
首先需要查找该节点是否存在,若不存在,则返回不删除,若存在,那么要删除该节点需要分为以下四种情况。
- 该节点为叶子节点
- 该节点有只有左孩子节点
- 该节点有只有右孩子节点
- 该节点左孩子节点、右孩子节点都有
看起来有待删除节点有4中情况,实际情况1可以与情况2或者3合并起来,因此真正的删除过程如下:
-
情况2 :让该节点的父节点指向该节点的左孩子节点并删除该节点 —— 直接删除
-
情况3 :让该节点的父节点指向该节点的右孩子节点并删除该节点 —— 直接删除
-
情况4:找到该节点左树最右节点(最大节点)或者右树的最左节点(最小节点),交换该节点和找到的节点的关键码,再删除交换前找到的节点。
2.3 二叉搜索树的代码实现
2.3.1 二叉搜索树的节点设置
// 二叉搜索树的节点设置
template<class K>
struct BSTreeNode
{BSTreeNode<K>* _left;BSTreeNode<K>* _right;K _key; // 关键码BSTreeNode(const K& key):_key(key), _left(nullptr), _right(nullptr){}
};
2.3.2 二叉搜索树类的框架
template<class K>
class BSTree
{typedef BSTreeNode<K> Node;// ......// 函数的实现// ......
public:// 插入函数,非递归版本bool Insert(const K& key){};// 查找函数,非递归版本bool Find(const K& key){};// 删除函数,非递归版本bool Erase(const K& key)// 查找函数void InOrder(){};// 析构函数~BSTree(){Destory(_root);}// 构造函数// C++ 11新增使用方法// BSTree() = default; BSTree(){}// 拷贝构造BSTree(const BSTree<K>& T){_root = Copy(T._root);}// 由于实参传个形参会创建一个新的对象// 这个对象是我们想要的,而原来对象不需要// 当函数结束后形参会被释放,将两个对象的地址交换后// 形参的内容就改变成我们不需要的内容,操作系统会帮我们释放// 而this指向的内容是我们需要的,从而完成赋值// 赋值重载BSTree<K>& operator=(BSTree<K> T){swap(_root, T._root);return *this;}
private:// 封装拷贝构造函数Node* Copy(Node* Troot){if (Troot == nullptr)return nullptr;Node* root = new Node(Troot->_key);root->_left = Copy(Troot->_left);root->_right = Copy(Troot->_right);return root;}// 封装析构函数void Destory(Node*& root){if(root == nullptr)return;Destory(root->_left);Destory(root->_right);delete root;root = nullptr;}
private:// 成员变量Node* _root = nullptr;
};
2.3.3 二叉搜索树的查找函数
- 从根节点开始查找,若查找值比根节点小,继续向左子树寻找,若查找值比根节点打,继续向右子树寻找。
- 最多查找高度次,若查找到空,那么在这棵树中没有这个值。
2.3.3.1 非递归方式实现
template<class K>
class BSTree
{typedef BSTreeNode<K> Node;// ......// 函数的实现// ......
public:// 查找函数,非递归版本bool Find(const K& key){Node* cur = _root;while (cur){// 当查找的值比节点的值小,继续向左子树查找if (cur->_key > key){cur = cur->_left;}// 当查找的值比节点的值大,继续向右子树查找else if (cur->_key < key){cur = cur->_right;}// 找到了else{return true;}}// 找不到return false;}
private:// 成员变量Node* _root = nullptr;
};
2.3.3.2 递归方式实现
template<class K>
class BSTree
{typedef BSTreeNode<K> Node;// ......// 函数的实现// ......
public:// 查找 , 递归版本bool FindR(const K& key){return _FindR(_root, key);}
private:bool _FindR(Node* root, const K& key){// 找不到if (root == nullptr)return false;// 当查找的值比节点的值小,递归子问题在左子树查找if (root->_key > key)return _FindR(root->_left, key);// 当查找的值比节点的值大,递归子问题在右子树查找else if (root->_key < key)return _FindR(root->_right, key);// 找到了elsereturn true;}// 成员变量Node* _root = nullptr;
};
2.3.4 二叉搜索树的插入函数
- 若树为空,创建一个节点,赋值给root。
- 若树不为空,按照二叉搜索树的性质查找插入位置,插入新节点
2.3.4.1 非递归方式实现
以非递归方式实现,在寻找该节点需要放的位置时,需要一个变量记录他当前位置的父亲节点,这样找到位置时,更方便的链接这个新节点。
template<class K>
class BSTree
{typedef BSTreeNode<K> Node;
public:// 插入,非递归版本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 (cur->_key > key){cur = cur->_left;}// 当当前节点的值小于要插入的值,那么向右找else if (cur->_key < key){cur = cur->_right;}// 二叉搜索树不允许相同的值存在,当这个值已经存在过了,那么返回false,即插入失败else{return false;}}if (prev->_key > key){prev->_left = new Node(key);return true;}else{prev->_right = new Node(key);return true;}return false;}
private://成员变量Node* _root = nullptr;
}
2.3.4.2 递归方式实现
当使用递归方式实现时,有个很巧妙的函数头设置 bool _InsertR(Node*& root, const K& key)
, 这里面的Node*& root
使得我们不需要记录当前位置的父亲节点,因为记录父亲节点的目的就是为了方便链接新节点,而这里的&
就使得当前节点就是它父亲节点的左孩子节点/右孩子节点,改变这个节点就是改变它父亲节点的左孩子节点/右孩子节点。
template<class K>
class BSTree
{typedef BSTreeNode<K> Node;
public:// 插入,递归版本bool InsertR(const K& key){return _InsertR(_root, key);}
private:bool _InsertR(Node*& root, const K& key){// 当树为空时,新插入的节点就是根节点if (root == nullptr){root = new Node(key);return true;}if (root->_key > key)return _InsertR(root->_left, key);else if (root->_key < key)return _InsertR(root->_right, key);// 出现相同的数字,返回falseelsereturn false;}//成员变量Node* _root = nullptr;
}
2.3.5 二叉搜索树的中序遍历
二叉树的中序遍历是:先递归遍历左子树,再访问根节点,最后递归遍历右子树。
template<class K>
class BSTree
{typedef BSTreeNode<K> Node;
public:void InOrder(){_InOrder(_root);cout << endl;}private:// 封装中序遍历void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << ' ';_InOrder(root->_right);}
private:// 成员变量Node* _root = nullptr;
}
2.3.6 二叉搜索树的删除函数
2.3.6.1 非递归实现
当我们需要删除一个节点时找到删除节点,需要记录当前节点的父亲节点,防止删除的当前节点时,防止当前的节点的父亲节点中的指针变为野指针。
template<class K>
class BSTree
{typedef BSTreeNode<K> Node;
public:// 删除,非递归版本bool Erase(const K& key){Node* cur = _root;Node* prev = nullptr;// 找到需要删除的节点while (cur){if (cur->_key > key){prev = cur;cur = cur->_left;}else if (cur->_key < key){prev = cur;cur = cur->_right;}else{break;}}// 当当前根节点的左树为空时,那么链接右边if (cur->_left == nullptr){// 当需要删除的节点是根节点,而根节点又没有父亲// 那么直接让右孩子节点成为根节点if (cur == _root){_root = cur->_left;delete cur;return true;}else{if (prev->_left == cur)prev->_left = cur->_right;elseprev->_right = cur->_right;delete cur;cur = nullptr;return true;}}// 当当前根节点的右树为空时,那么链接左边else if (cur->_right == nullptr){// 当需要删除的节点是根节点,而根节点又没有父亲// 那么直接让左孩子节点成为根节点if (cur == _root){_root = cur->_left;delete cur;return true;}else{if (prev->_left == cur)prev->_left = cur->_left;elseprev->_right = cur->_left;delete cur;cur = nullptr;return true;}}// 当当前根节点的左树和右树都不为空时// 寻找当前节点左树的最右节点(最大节点),或是右树的最左节点(最小节点)// 来替换当前根节点,删除根节点// 我这里找右树的最左节点(最小节点)else{prev = cur;Node* del = cur->_right;while (del->_left != nullptr){prev = del;del = del->_left;}swap(del->_key, cur->_key); 一开始的想法,复用上面的代码//if (del->_left == nullptr)//{// if (prev->_left == del)// prev->_left = del->_right;// else// prev->_right = del->_right;// delete del;// del = nullptr;// return true;//} 当当前根节点的右树为空时,那么链接左边//else if (del->_right == nullptr) //{// if (prev->_left == del)// prev->_left = del->_left;// else// prev->_right = del->_left;// delete del;// del = nullptr;// return true;//}// 后面的想法,一棵树的最左节点,这个节点的左树一定为空// 一棵树的最左节点并不一定是根节点的左节点,也有可能是根节点// 所以这里我们并不能直接判断,使用根节点的左指针链接还是右指针链接if (prev->_left == del)prev->_left = del->_right;elseprev->_right = del->_right;delete del;del = nullptr;return true;}return false;}private:// 成员变量Node* _root = nullptr;
};
2.3.6.2 递归实现
这里bool _EraseR(Node*& root, const K& key)
中&
的作用也是通过改变当前节点直接改变父亲节点的指向。
template<class K>
class BSTree
{typedef BSTreeNode<K> Node;
public:// 删除、递归版本bool EraseR(const K& key){return _EraseR(_root, key);}private:bool _EraseR(Node*& root, const K& key){// 没有需要删除的节点if (root == nullptr)return false;if (root->_key > key)return _EraseR(root->_left, key);else if (root->_key < key)return _EraseR(root->_right, key);// 找到了删除的节点else{// 当当前根节点的左树为空时,那么链接右边if (root->_left == nullptr){Node* del = root;root = root->_right;delete del;del = nullptr;return true;}// 当当前根节点的右树为空时,那么链接左边else if (root->_right == nullptr){Node* del = root;root = root->_left;delete del;del = nullptr;return true;}else{// 开始的想法是,与上面的非递归思想相同/*Node* prev = root;Node* del = root->_right;while (del->_left != nullptr){prev = del;del = del->_left;}swap(del->_key, root->_key);if (prev->_left == del)prev->_left = del->_right;elseprev->_right = del->_right;delete del;del = nullptr;return true;*/// 后面的想法是,将替代的节点与该删除的节点交换值// 再在使用递归子问题,寻找原该删除节点的右树上与删除值相同的节点删除Node* del = root->_right;while (del->_left != nullptr){del = del->_left;}swap(del->_key, root->_key);return _EraseR(root->_right,key);}}}// 成员变量Node* _root = nullptr;
};
2.3.7 二叉搜索树代码的整体实现
template<class K>
struct BSTreeNode
{BSTreeNode<K>* _left;BSTreeNode<K>* _right;K _key; // 关键码BSTreeNode(const K& key):_key(key), _left(nullptr), _right(nullptr){}
};template<class K>
class BSTree
{typedef BSTreeNode<K> Node;
public:
/// 插入,非递归版本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 (cur->_key > key){cur = cur->_left;}// 当当前节点的值小于要插入的值,那么向右找else if (cur->_key < key){cur = cur->_right;}// 二叉搜索树不允许相同的值存在,当这个值已经存在过了,那么返回false,即插入失败else{return false;}}if (prev->_key > key){prev->_left = new Node(key);return true;}else{prev->_right = new Node(key);return true;}return false;}// 插入,递归版本bool InsertR(const K& key){return _InsertR(_root, key);}/// 查找,非递归版本bool Find(const K& key){Node* cur = _root;while (cur){if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){cur = cur->_right;}// 找得到else{return true;}}// 找不到return false;}// 查找 , 递归版本bool FindR(const K& key){return _FindR(_root, key);}/// 删除,非递归版本bool Erase(const K& key){Node* cur = _root;Node* prev = nullptr;// 找到需要删除的节点while (cur){if (cur->_key > key){prev = cur;cur = cur->_left;}else if (cur->_key < key){prev = cur;cur = cur->_right;}else{break;}}// 当当前根节点的左树为空时,那么链接右边if (cur->_left == nullptr){if (cur == _root){_root = cur->_left;delete cur;return true;}else{if (prev->_left == cur)prev->_left = cur->_right;elseprev->_right = cur->_right;delete cur;cur = nullptr;return true;}}// 当当前根节点的右树为空时,那么链接左边else if (cur->_right == nullptr){if (cur == _root){_root = cur->_left;delete cur;return true;}else{if (prev->_left == cur)prev->_left = cur->_left;elseprev->_right = cur->_left;delete cur;cur = nullptr;return true;}}// 当当前根节点的左树和右树都不为空时// 寻找当前节点左树的最右节点(最大节点),或是右树的最左节点(最小节点)// 来替换当前根节点,删除根节点// 我这里找右树的最左节点(最小节点)else{prev = cur;Node* del = cur->_right;while (del->_left != nullptr){prev = del;del = del->_left;}swap(del->_key, cur->_key);// 一棵树的最左节点,这个节点的左树一定为空// 一棵树的最左节点并不一定是根节点的左节点,也有可能是根节点// 所以这里我们并不能直接判断,使用根节点的左指针链接还是右指针链接if (prev->_left == del)prev->_left = del->_right;elseprev->_right = del->_right;delete del;del = nullptr;return true;}return false;}// 删除、递归版本bool EraseR(const K& key){return _EraseR(_root, key);}//void InOrder(){_InOrder(_root);cout << endl;}// 析构函数~BSTree(){Destory(_root);}// 构造函数// C++ 11新增使用方法// BSTree() = default; BSTree(){}// 拷贝构造BSTree(const BSTree<K>& T){_root = Copy(T._root);}// 赋值重载BSTree<K>& operator=(BSTree<K> T){swap(_root, T._root);return *this;}private:Node* Copy(Node* Troot){if (Troot == nullptr)return nullptr;Node* root = new Node(Troot->_key);root->_left = Copy(Troot->_left);root->_right = Copy(Troot->_right);return root;}void Destory(Node*& root){if(root == nullptr)return;Destory(root->_left);Destory(root->_right);delete root;root = nullptr;}bool _InsertR(Node*& root, const K& key){if (root == nullptr){root = new Node(key);return true;}if (root->_key > key)return _InsertR(root->_left, key);else if (root->_key < key)return _InsertR(root->_right, key);// 出现相同的数字,返回falseelsereturn false;}//bool _FindR(Node* root, const K& key){if (root == nullptr)return false;if (root->_key > key)return _FindR(root->_left, key);else if (root->_key < key)return _FindR(root->_right, key);elsereturn true;}//bool _EraseR(Node*& root, const K& key){// 没有需要删除的节点if (root == nullptr)return false;if (root->_key > key)return _EraseR(root->_left, key);else if (root->_key < key)return _EraseR(root->_right, key);// 找到了删除的节点else{// 当当前根节点的左树为空时,那么链接右边if (root->_left == nullptr){Node* del = root;root = root->_right;delete del;del = nullptr;return true;}// 当当前根节点的右树为空时,那么链接左边else if (root->_right == nullptr){Node* del = root;root = root->_left;delete del;del = nullptr;return true;}else{// 开始的想法是,与上面的非递归思想相同/*Node* prev = root;Node* del = root->_right;while (del->_left != nullptr){prev = del;del = del->_left;}swap(del->_key, root->_key);if (prev->_left == del)prev->_left = del->_right;elseprev->_right = del->_right;delete del;del = nullptr;return true;*/// 后面的想法是,将替代的节点与该删除的节点交换值// 再在使用递归子问题,寻找原该删除节点的右树上与删除值相同的节点删除Node* del = root->_right;while (del->_left != nullptr){del = del->_left;}swap(del->_key, root->_key);return _EraseR(root->_right,key);}}}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << ' ';_InOrder(root->_right);}
private:// 成员变量Node* _root = nullptr;
};// 测试二叉搜索树
void TestBSTree()
{BSTree<int> dict;// 8, 3, 1, 10, 6, 4, 7, 14, 13int arr[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){dict.InsertR(arr[i]);}dict.InOrder();/*dict.EraseR(3);dict.InOrder();dict.EraseR(8);dict.InOrder();dict.EraseR(14);dict.InOrder();int i = 0;while (i < sizeof(arr) / sizeof(arr[0])){auto ret = dict.FindR(arr[i]);if (ret){dict.EraseR(arr[i]);dict.InOrder();}i++;}*/BSTree<int> copy(dict);copy.InOrder();BSTree<int> copy1;copy1 = copy;copy1.InOrder();
}
2.4 二叉搜索树的应用
- K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。
比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:
- 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
- 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
- KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。该种方式在现实生活中非常常见:
- 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;
- 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。
上面2.3
中的代码是K模型,下面的代码是KV模型。
其实K模型与KV模型的代码大体是一样的,K模型中的节点只有 _key
一个关键码,而KV模型中有_key
, _value
两个关键码。
template<class K, class V>
struct BSTreeNode
{BSTreeNode<K, V>* _left;BSTreeNode<K, V>* _right;K _key;V _value;BSTreeNode(const K& key, const V& value):_key(key), _value(value), _left(nullptr), _right(nullptr){}
};template<class K, class V>
class BSTree
{typedef BSTreeNode<K, V> Node;
public:
bool Insert(const K& key, const V& value){// 树为空则,该节点为根if (_root == nullptr){_root = new Node(key, value);return true;}Node* cur = _root;Node* prev = nullptr;while (cur){prev = cur;// 当当前节点的值大于要插入的值,那么向左找if (cur->_key > key){cur = cur->_left;}// 当当前节点的值小于要插入的值,那么向右找else if (cur->_key < key){cur = cur->_right;}// 二叉搜索树不允许相同的值存在,当这个值已经存在过了,那么返回false,即插入失败else{return false;}}if (prev->_key > key){prev->_left = new Node(key, value);return true;}else{prev->_right = new Node(key, value);return true;}return false;}
Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_key > key){cur = cur->_left;}else if (cur->_key < key){cur = cur->_right;}// 找得到else{return cur;}}// 找不到return nullptr;}// 删除,非递归版本bool Erase(const K& key){Node* cur = _root;Node* prev = nullptr;// 找到需要删除的节点while (cur){if (cur->_key > key){prev = cur;cur = cur->_left;}else if (cur->_key < key){prev = cur;cur = cur->_right;}else{break;}}// 当当前根节点的左树为空时,那么链接右边if (cur->_left == nullptr){if (cur == _root){_root = cur->_left;delete cur;return true;}else{if (prev->_left == cur)prev->_left = cur->_right;elseprev->_right = cur->_right;delete cur;cur = nullptr;return true;}}// 当当前根节点的右树为空时,那么链接左边else if (cur->_right == nullptr){if (cur == _root){_root = cur->_left;delete cur;return true;}else{if (prev->_left == cur)prev->_left = cur->_left;elseprev->_right = cur->_left;delete cur;cur = nullptr;return true;}}else{prev = cur;Node* del = cur->_right;while (del->_left != nullptr){prev = del;del = del->_left;}swap(del->_key, cur->_key);if (prev->_left == del)prev->_left = del->_right;elseprev->_right = del->_right;delete del;del = nullptr;return true;}return false;}
void InOrder(){_InOrder(_root);cout << endl;}~BSTree(){Destory(_root);}BSTree() {}BSTree(const BSTree<K, V>& T){_root = Copy(T._root);}BSTree<K, V>& operator=(BSTree<K, V> T){swap(_root, T._root);return *this;}private:Node* Copy(Node* Troot){if (Troot == nullptr)return nullptr;Node* root = new Node(Troot->_key);root->_left = Copy(Troot->_left);root->_right = Copy(Troot->_right);return root;}void Destory(Node*& root){if (root == nullptr)return;Destory(root->_left);Destory(root->_right);delete root;root = nullptr;}bool _InsertR(Node*& root, const K& key){if (root == nullptr){root = new Node(key);return true;}if (root->_key > key)return _InsertR(root->_left, key);else if (root->_key < key)return _InsertR(root->_right, key);// 出现相同的数字,返回falseelsereturn false;}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_key << ':' << root->_value << endl;_InOrder(root->_right);}Node* _root = nullptr;
};void TestBSTree()
{BSTree<string, string> dict;dict.Insert("insert", "插入");dict.Insert("erase", "删除");dict.Insert("left", "左边");dict.Insert("string", "字符串");dict.InOrder();string str;while (cin >> str){auto ret = dict.Find(str);if (ret){cout << str << ":" << ret->_value << endl;}else{cout << "单词拼写错误" << endl;}}string strs[] = { "苹果", "西瓜", "苹果", "樱桃", "苹果", "樱桃", "苹果", "樱桃", "苹果" };// 统计水果出现的次BSTree<string, int> countTree;for (auto str : strs){auto ret = countTree.Find(str);if (ret == NULL){countTree.Insert(str, 1);}else{ret->_value++;}}countTree.InOrder();
}
2.5 二叉搜索树的性能分析
插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。
对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。
但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:
最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:logN
(log以2为底N的对数)
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N
问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。
三、二叉树进阶题目
3.1 根据二叉树创建字符串【OJ题目】
讲解:本道题目中并不是要求简单的前序遍历,他需要将二叉树转化为一个由括号和整数组成的字符串,并且这个括号有要求:
(1)当左孩子节点和右孩子节点不为空时,那么两个括号都不能省略。
(2)当左孩子节点不为空,并且右孩子节点为空时,那么左孩子的括号可以省略,右孩子的括号不能省略。
(3)当左孩子节点为空,并且右孩子节点不为空时,那么左孩子和右孩子的括号都不可以省略。
(4)当左孩子节点为空,并且右孩子节点为空时,那么左孩子和右孩子的括号可以省略。
class Solution {
public:string ans;void _tree2str(TreeNode* root) {if(root == nullptr)return;// 这里的数字并不是简单的 0 ~ 9 ,所以不能 +=数字 + '0'ans += to_string(root->val);// 这里的条件和巧妙// 这里的条件应该是 root->left || (root->left == nullptr && root->right)// root 不为 nullptr 就是 nulltr , 如果root 为 nullptr 那么就不会判断第二个条件// 剩下一种情况就是 root 必定是 nullptr ,如果第一个条件不符合,// 那么第二个条件的第一个条件必定符合,那么这个条件就可以省略了if(root->left || root->right){ans += '(';_tree2str(root->left);ans += ')';}if(root->right){ans += '(';_tree2str(root->right);ans += ')';}}string tree2str(TreeNode* root) {_tree2str(root);return ans;}
};
3.2 二叉树的层序遍历Ⅰ【OJ题目】
讲解:层序遍历是按照每一层的顺序来输出节点的遍历方法。
使用一个vector
变量来存储遍历结果。
对于层序遍历,我们需要遍历一层节点时,并且遍历当前节点时,需要记录下一层的节点,并且记录节点的时候还要有顺序,这时候就需要队列(queue)
来记录每一层的节点。
但是单单这样并不能确定当前节点是上一层的还是当前层的,我们需要一个变量count来记录每一层有多少个节点,循环变量队列中的节点,遍历一个节点count--
,当这个count == 0
时,那么队列中的节点为下一层的节点。重复上面部分,当队列为空时,那么这棵树则遍历完成。
class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> ans;queue<TreeNode*> qu;vector<int> v;if(root == nullptr)return ans;qu.push(root);int count = 0; // 记录每一层有多少个数据while(!qu.empty()){count = qu.size();while(count--){TreeNode* top = qu.front();qu.pop();v.push_back(top->val);if(top->left != nullptr)qu.push(top->left);if(top->right != nullptr)qu.push(top->right);}ans.push_back(v);v.clear();}return ans;}
};
3.3 二叉树的层序遍历Ⅱ【OJ题目】
讲解:本题思路与上面一题思路相同,只需要按照上面一题的步骤遍历完后,将记录结果的vector逆转一下,就是这个题目的结果。
class Solution {
public:vector<vector<int>> levelOrderBottom(TreeNode* root) {vector<vector<int>> ans;queue<TreeNode*> qu;vector<int> v;if(root == nullptr)return ans;qu.push(root);int count = 0; // 记录每一层有多少个数据while(!qu.empty()){count = qu.size();while(count--){TreeNode* top = qu.front();qu.pop();v.push_back(top->val);if(top->left != nullptr)qu.push(top->left);if(top->right != nullptr)qu.push(top->right);}ans.push_back(v);v.clear();}reverse(ans.begin() , ans.end());return ans;}
};
3.4 二叉树的最近公共祖先【OJ题目】
讲解:
方法一,两个节点在从根节点来看有四种情况
一:当两个节点一个节点是根节点,另一个节点在左子树/右子树中,那么根节点就是最近祖先
二:两个节点都在左子树,那么最近公共祖先一定在左树中,分解为子问题,向一、四靠拢
三:两个节点都在右子树,那么最近公共祖先一定在右树中,分解为子问题,向一、四靠拢
四:当两个节点,一个节点在左子树,一个节点在右子树,那么根节点就是最近祖先
方法二,寻找两个节点的路径
再找到相同的节点,那么这个节点就是两个节点的最近的公共祖先
// 方法一
class Solution {
public:// 判断该节点是否在树中bool Find(TreeNode* root , TreeNode* x){if(root == nullptr)return false;if(root == x)return true;bool left , right;left = Find(root->left , x);if(left) return true;right = Find(root->right , x);if(right) return true;return false;}TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {if(root == nullptr)return nullptr;// 情况一if(p == root || q == root)return root;// 记录两个节点是否在左树bool pleft = Find(root->left , p);bool qleft = Find(root->left , q);// p 在左树,q在右树 || p 在右树 ,q在左树if((pleft && !qleft) || (!pleft && qleft))return root;// 两个节点都在左树else if(pleft && qleft)return lowestCommonAncestor(root->left,p,q);// 两个节点都在右树else // (!pleft && !rigth)return lowestCommonAncestor(root->right,p,q);}
};// 方法二
class Solution {
public:bool Find(stack<TreeNode*>& path , TreeNode* root , TreeNode* x){if(root == nullptr)return false;path.push(root);if(root == x)return true;bool left , right;left = Find(path , root->left , x);if(left) return true;right = Find(path , root->right , x);if(right) return true;path.pop();return false;} TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {stack<TreeNode*> Ppath , Qpath;Find(Ppath , root , p);Find(Qpath , root , q);while(!Ppath.empty()){if(Ppath.size() > Qpath.size()){Ppath.pop();}else if(Ppath.size() < Qpath.size()){Qpath.pop();}else{if(Ppath.top() == Qpath.top()){return Ppath.top();}else{Qpath.pop();Ppath.pop();}}}return nullptr;}
};
3.5 二叉搜索树与双向链表【OJ题目】
讲解:
本题有空间复杂度的限制,所以不能通过vector
前序变量记录节点,再链接,也不能创建新节点。
这个题有个很巧妙的方法就是定义一个变量来记录上一个访问的节点,前序遍历当前树,遍历当前节点时用上一个节点的右指针指向当前节点,当前节点的左指针指向上一个节点,当这棵树的遍历完后即完成了二叉搜索树和双向链表的转化。
class Solution {
public:TreeNode* prev = nullptr;void _Convert(TreeNode* root) {if(root == nullptr)return;_Convert(root->left);if(prev != nullptr){prev->right = root;root->left = prev;}prev = root;_Convert(root->right);}TreeNode* Convert(TreeNode* pRootOfTree) {if(pRootOfTree == nullptr)return nullptr;_Convert(pRootOfTree);TreeNode* cur = pRootOfTree;while(cur->left){cur = cur->left;}return cur;}
};
3.6 从前序与中序遍历序列构造二叉树【OJ题目】
讲解:
学过二叉树的都知道二叉树的前序遍历确定根节点,中序遍历确定左右区间。
那么就可以使用一个变量记录前序遍历的位置,两个变量来记录左右区间的范围。当区间不存在的时候那么当前子树则构建完成,当返回完后,则这棵树构建完成。
class Solution {
public:TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder , int& pos , int begin , int end) {int div = begin;for(int i = begin ; i <= end ; i++){if(inorder[i] == preorder[pos]){div = i;break;}}TreeNode* root = new TreeNode(preorder[pos]);if(begin <= div - 1)root->left = _buildTree(preorder , inorder , ++pos , begin , div - 1);if(div + 1 <= end)root->right = _buildTree(preorder , inorder , ++pos , div + 1 , end);return root;}TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {int prei = 0;return _buildTree(preorder , inorder , prei , 0 , preorder.size()-1);}
};
3.7 从中序与后序遍历序列构造二叉树【OJ题目】
讲解:这一题与上面题的思路一致,只是这里是后序确定根节点,中序确定左右区间,并且需要先创建右孩子节点。
class Solution {
public:TreeNode* _buildTree(vector<int>& inorder, vector<int>& postorder , int& pos , int begin , int end) {int div = 0;for(int i = begin ; i <= end ; i++){if(postorder[pos] == inorder[i]){div = i;break;}}TreeNode* root = new TreeNode(postorder[pos]);if(div + 1 <= end)root->right = _buildTree(inorder , postorder , --pos , div + 1 , end);if(begin <= div - 1)root->left = _buildTree(inorder , postorder , --pos , begin , div - 1);return root;}TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {int prei = postorder.size()-1;return _buildTree(inorder , postorder , prei , 0 , inorder.size()-1);}
};
3.8 二叉树的前序遍历,非递归迭代实现【OJ题目】
讲解:二叉树的前序遍历顺序是根、左子树、右子树,非递归的遍历可以看作两步:
- 将当前树的左路节点入栈并在入栈时访问当前节点;
- 遍历左路节点的右子树。
定义一个栈来存储没访问右子树的左路节点,定义一个vector来存储答案。首先遍历当前树的左路节点,因为这里是前序遍历,需要先遍历根节点,所以每存储一个节点在栈中,就将访问这个节点并将值存入vector中,当前树的所有左路节点都被存储后,那么再从栈中栈顶元素,从栈中取出来的元素就说明这个节点的左树已经访问完了,然后再访问它的右树,重复上面的步骤,直到栈为空并且当前节点也为空时,则完成二叉树前序遍历的非递归的实现。
class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {stack<TreeNode*> st;vector<int> ans;TreeNode* cur = root;while(cur || !st.empty()){// 左路节点的入栈并访问while(cur){st.push(cur);ans.push_back(cur->val);cur = cur -> left;}// 子问题访问左路节点的右子树cur = st.top()->right;st.pop();}return ans;}
};
3.9 二叉树的中序遍历 ,非递归迭代实现【OJ题目】
讲解:二叉树的中序遍历顺序是左子树、根、右子树,非递归的遍历可以看作两步:
- 入栈当前树的左路节点并在出栈时访问当前节点;
- 遍历左路节点的右子树。
本题的逻辑与上题的非常相似,只是访问节点的时机不同,前序遍历访问的时间是在节点入栈的时候,而中序遍历访问的时间是在出栈的时候。
class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {stack<TreeNode*> st;vector<int> ans;TreeNode* cur = root;while(cur || !st.empty()){while(cur){st.push(cur);cur = cur->left;}ans.push_back(st.top()->val);cur = st.top()->right;st.pop();}return ans;}
};
3.10 二叉树的后序遍历 ,非递归迭代实现 【OJ题目】
讲解:二叉树的后序遍历顺序是左子树、右子树、根,非递归的遍历可以看作两步:
- 入栈当前树的左路节点并在栈顶元素的右为空 或是 栈顶元素的右是上一个访问的节点;
- 遍历左路节点的右子树。
定义一个栈来存储没访问右子树的左路节点,定义一个vector来存储答案,定义一个prev来记录上一个访问的节点。首先遍历当前树的左路节点,因为这里是前序遍历,需要先遍历根节点,所以每存储一个节点在栈中,当前树的所有左路节点都被存储后,那么再从栈中栈顶元素,从栈中取出来的元素就说明这个节点的左树已经访问完了,若栈顶元素的右为空 或是 栈顶元素的右是上一个访问的节点,就访问这个栈顶元素并将值存储到vector中,否则就访问访问它的右树,重复上面的步骤,直到栈为空并且当前节点也为空时,则完成二叉树前序遍历的非递归的实现。
class Solution {
public:vector<int> postorderTraversal(TreeNode* root) {stack<TreeNode*> st;vector<int> ans;TreeNode* cur = root;// prev记录上一个访问的节点TreeNode* prev = nullptr;while(cur || !st.empty()){// 将当前树的左路节点全部入栈while(cur){st.push(cur);cur = cur->left;}TreeNode* top = st.top();// 若栈顶元素的右为空 或是 栈顶元素的右是上一个访问的节点// 说明这棵树的左边已经访问完// 并且右边没有节点不需要访问或是右边有节点已经访问过了if(top -> right == nullptr || top -> right == prev){ans.push_back(top->val);prev = top;st.pop();}// 右子树不为空且没访问过// 看成子问题访问右树else{cur = st.top()->right;}}return ans;}
};
结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,请大家给一个三连支持一下!!🌹🌹