洞察探索如何通过一套代码实现跨平台小程序开发与高效管理,助力企业数字化转型
639
2022-11-28
BST二叉搜索树常见遍历操作以及面试常见编程题
由于是复习,理论部分不过多赘述
文章目录
一、BST二叉搜索树插入删除查询
1. 非递归插入2. 递归插入3. 非递归删除4. 递归删除5. 非递归查询6. 递归查询7. 获取树的节点数
二、BST树的遍历
1. 递归前序遍历2. 非递归前序遍历3. 递归中序遍历4. 非递归中序遍历5. 递归后序遍历6. 非递归后序遍历7. 递归层序遍历8. 非递归层序遍历
三、常见面试题
1. 找到二叉树所在区间内所有的元素2. 判断一棵树是否是二叉搜索树3. BST求子树问题4. 二叉搜索树最近公共祖先LCA5. 镜像翻转6. 判断是否为镜像树7. 先序中序重建二叉树8. 判断二叉树是否是平衡树9. 求中序遍历倒数第k个节点10. 层序遍历析构二叉树11. 子树问题
一、BST二叉搜索树插入删除查询
1. 非递归插入
根结点为空,则构造根结点;根结点不空,则找到正确的位置进行插入
void non_cur_insert(const T& val) { // 树为空,生成根结点 if (root_ == nullptr) { root_ = new Node(val); return; } Node* cur = root_; Node* parent = nullptr; while (nullptr != cur) { if (cur->data_ == val) { // 不插入已存在的元素,结束 return; } else if (comp_(val, cur->data_)) { parent = cur; cur = cur->left_; } else{ parent = cur; cur = cur->right_; } } cur = new Node(val); if (comp_(val, parent->data_)) { parent->left_ = cur; } else { parent->right_ = cur; } }
2. 递归插入
// 用户调用接口void cur_insert(const T& val) { root_ = cur_insert(root_, val);}// 在以node为根结点的树中插入val,并返回插入节点后的树的根结点Node* cur_insert(Node* node, const T& val) { // 找到合适的位置,生成新节点,并返回节点地址 if (nullptr == node) { return new Node(val); } if (val == node->data_) { // 不插入相同数据 return node; } else if (comp_(val, node->data_)) { node->left_ = cur_insert(node->left_, val); } else { node->right_ = cur_insert(node->right_, val); } return node;}
3. 非递归删除
前驱节点: 当前节点左子树中值最大的节点后继节点: 当前节点右子树中值最小的节点
前驱节点和后继节点特点:最多有一个孩子
待删除节点没有孩子,父节点对应的地址域置空待删除节点有1个孩子,这个孩子节点的地址写入(待删除节点的)父节点对应的地址域待删除节点有2个孩子,用当前节点的前驱节点(后继节点)的值替换掉待删除节点的值(替换后不会影响BST树的性质),然后删除前驱节点(后继节点)
用63(78)把64覆盖后,删除63(78)即可
void non_cur_del(const T& val) { if (root_ == nullptr) { return; } Node* parent = nullptr; Node* cur = root_; while (cur != nullptr) { if (cur->data_ == val) { break; } else if (comp_(val, cur->data_)) { parent = cur; cur = cur->left_; } else { parent = cur; cur = cur->right_; } } // 没找到val节点 if (cur == nullptr) { return; } // 此时cur指向待删除节点 // 若有两个孩子,则寻找前驱节点,用前驱节点的值替换待删除节点的值 if (cur->left_ != nullptr && cur->right_ != nullptr) { parent = cur; Node* pre = cur->left_; while (pre->right_ != nullptr) { parent = pre; pre = pre->right_; } cur->data_ = pre->data_; cur = pre; } Node* child = cur->left_ == nullptr ? cur->right_ : cur->left_; if (parent == nullptr) { delete root_; root_ = child; } else { if (parent->left_ == cur) { parent->left_ = child; } else { parent->right_ = child; } } delete cur;}
4. 递归删除
void cur_del(const T& val) { root_ = cur_del(root_, val);}// 在以node为根结点的树中寻找值为val的节点删除// 删除指定节点后,需要把删除节点的孩子节点的地址返回给对应父节点Node* cur_del(Node* node, const T& val) { if (nullptr == node) { return nullptr; } if (node->data_ == val) { // 找到后,情况3 if (nullptr != node->left_ && nullptr != node->right_) { // 找前驱节点,替换当前节点的值后,删除前驱节点 Node* pre = node->left_; while (nullptr != pre->right_) { pre = pre->right_; } node->data_ = pre->data_; node->left_ = cur_del(node->left_, pre->data_); /* 替代递归node->left_ = cur_del(node->left_, pre->data_)的代码: Node* child = (nullptr == pre->left_) ? pre->right_ : pre->left_; if (parent == nullptr) { delete root_; root_ = child; } else { if (parent->left_ == pre) { parent->left_ = child; } else { parent->right_ = child; } } delete pre; */ } else { // 情况1、2 if (nullptr != node->left_) { // 左孩子不空,删除当前节点后返回左孩子的地址 Node* left = node->left_; delete node; return left; } else if (nullptr != node->right_){ // 右孩子不空,删除当前节点后返回右孩子的地址 Node* right = node->right_; delete node; return right; } else { // 删除叶子节点 delete node; return nullptr; } } } else if (comp_(val, node->data_)) { node->left_ = cur_del(node->left_, val); } else { node->right_ = cur_del(node->right_, val); } // 每次调用函数,都是进入了一个节点,回溯的时候将当前节点的地址返回给对应父节点 return node;}
5. 非递归查询
bool non_cur_search(const T& val) const{ if (root_ == nullptr) { return false; } Node* cur = root_; while (cur != nullptr) { if (val == cur->data_) { return true; } else if (comp_(val, cur->data_)) { cur = cur->left_; } else { cur = cur->right_; } } return false;}
6. 递归查询
bool cur_search(const T& val) const { return nullptr != cur_search(root_, val);}// 当然这里写成bool cur_search(Node* node, const T& val) const也可以Node* cur_search(Node* node, const T& val) const { if (nullptr == node) { return nullptr; } if (val == node->data_) { return node; } else if (comp_(val, node->data_)) { return cur_search(node->left_, val); } else { return cur_search(node->right_, val); }}
7. 获取树的节点数
// 用户接口int get_tree_num() { return get_tree_num(root_);}// 递归接口int get_tree_num(Node* node) { if (nullptr == node) { return 0; } int left = get_tree_num(node->left_); int right = get_tree_num(node->right_); return left + right + 1;}
二、BST树的遍历
1. 递归前序遍历
void cur_pre_order() const { cout << "递归前序遍历:"; cur_pre_order(root_); cout << endl;} void cur_pre_order(Node* node) const { if (nullptr != node) { cout << node->data_ << " "; cur_pre_order(node->left_); cur_pre_order(node->right_); }}
2. 非递归前序遍历
先序遍历:根左右,对于每一个节点而言都是先访问根结点
先入右孩子,后入左孩子
void non_cur_pre_order() const { cout << "非递归前序遍历:"; stack
3. 递归中序遍历
void cur_in_order() const { cout << "递归中序遍历:"; cur_in_order(root_); cout << endl;}void cur_in_order(Node* node) const { if (nullptr != node) { cur_in_order(node->left_); cout << node->data_ << " "; cur_in_order(node->right_); }}
4. 非递归中序遍历
中序遍历:左根右,对于每一个节点而言都是先访问左孩子,左孩子没访问完不能访问根结点
思想: L V R L:从根结点开始把所有的左孩子入栈,直到左孩子为空。 V:然后从栈顶取出元素访问 R:把V的右孩子入栈,以右孩子为根结点把所有的左孩子入栈,直到左孩子为空。
void non_cur_in_order() const { cout << "非递归中序遍历:"; if (nullptr == root_) { return ; } stack
void non_cur_in_order() const { cout << "非递归中序遍历:"; if (nullptr == root_) { return ; } stack
5. 递归后序遍历
void cur_post_order() const { cout << "递归后序遍历:"; cur_post_order(root_); cout << endl;}void cur_post_order(Node* node) const { if (nullptr != node) { cur_post_order(node->left_); cur_post_order(node->right_); cout << node->data_ << " "; }}
6. 非递归后序遍历
思考: 后序遍历是LRV,和中序遍历一样,先把所有的左孩子入栈,然后取出栈顶节点(这时候不能访问),再把该节点的右子树的节点入栈。入栈完成后还需要再取栈顶元素访问R,这时候就无法找到V了。所以后序遍历无法像中序遍历一样用一个栈完成
我们将LRV -> VRL,然后再用一个栈保存出栈的结果,最后打印即可
void non_cur_post_order() const { cout << "非递归后序遍历:"; if (nullptr == root_) { return; } // LRV -> VRL stack
7. 递归层序遍历
void cur_level_order() const { cout << "递归层序遍历:"; int tree_high = high(); for (int i = 0; i < tree_high; i++) { cur_level_order(root_, i); } cout << endl;}// 可输出以node为根节点的第i层的节点void cur_level_order(Node* node, int i) const { // 当树的某个分支比其他的分支都要长,就可能出现在某个第i层i不为0,node为nullptr的情况 if (nullptr == node) { return; } if (i == 0) { cout << node->data_ << " "; return; } cur_level_order(node->left_, i - 1); cur_level_order(node->right_, i - 1);}
8. 非递归层序遍历
广度优先遍历:使用队列
void non_cur_level_order() const { cout << "非递归层序遍历:"; if (nullptr == root_) { return; } queue
三、常见面试题
1. 找到二叉树所在区间内所有的元素
中序遍历的结果是一个升序序列,可加入适当的条件提前停止递归
void find_values(Node* node, vector
2. 判断一棵树是否是二叉搜索树
使用先序遍历判断是否是BST树
错误写法:
bool isBST(Node* node) { // V if (nullptr == node) { return true; } if (nullptr != node->left_ && comp_(node->data_, node->left_->data_)) { return false; } if (nullptr != node->right_ && comp_(node->right_->data_, node->data_)) { return false; } // L if (!isBST(node->left_)) { return false; } // R return isBST(node->right_);}
错误原因: 仅仅判断了当前节点以及当前节点的左右孩子是否满足大小关系(局部),无法正确判断如下的二叉树
10 / \ 5 20 / \ 8 30
正确写法: 利用中序遍历BST树是升序的特点,比较前驱节点和当前节点的大小关系
bool isBST() { Node* pre = nullptr; return isBST(root_, pre);} bool isBST(Node* node, Node* &pre) { if (nullptr == node) { return true; } // L if (!isBST(node->left_, pre)) { return false; } // V if (nullptr != pre) { if (comp_(node->data_, pre->data_)) { return false; } } // 更新中序遍历的前驱节点 pre = node; // R return isBST(node->right_, pre);}
3. BST求子树问题
所谓子树,必须被某个树包含了所有节点。子树的节点必须和该树节点完全重合
比如判断树B是否是树A的子树,先在树B中找到与树A根节点值相同的节点1,从节点1开始遍历树A,也要同时遍历树B,遍历的同时判断即可。
bool is_child_tree(const BSTree
4. 二叉搜索树最近公共祖先LCA
假定: 所给的值都在BST树中存在
BST树中两个节点的最近公共祖先,一定是从根结点往下深入的时候碰到的第一个值介于两者之间的节点
T getLCA(const T& val1, const T& val2) { if (nullptr == root_) { throw "root nullptr"; } Node* lca = getLCA(root_, val1, val2); if (nullptr == lca) { throw "no LCA"; } return lca->data_;}Node* getLCA(Node* node, const T& val1, const T& val2) { if (nullptr == node) { return nullptr; } if (comp_(node->data_, val1) && comp_(node->data_, val2)) { return getLCA(node->right_, val1, val2); }else if(comp_(val1, node->data_) && comp_(val2, node->data_)){ return getLCA(node->left_, val1, val2); } else { return node; }}
5. 镜像翻转
遍历每个节点,遍历的时候都交换节点的两个孩子
void mirror_reverse() { mirror_reverse(root_);}// 由于是遍历每个节点的时候,分别交换左右孩子,所以参数只需要一个节点即可void mirror_reverse(Node* node) { if (nullptr == node) { return; } // V Node* tmp = node->left_; node->left_ = node->right_; node->right_ = tmp; // L mirror_reverse(node->left_); // R mirror_reverse(node->right_);}
6. 判断是否为镜像树
由于每次判断需要两个节点比较,所以递归函数的形参传递两个
bool is_mirror_symmetry() { if (nullptr == root_) { return true; } return is_mirror_symmetry(root_->left_, root_->right_);}bool is_mirror_symmetry(Node* l , Node* r) { if (nullptr == l && nullptr == r) { return true; } if (nullptr != l && nullptr != r) { // 两个节点都不空 if (l->data_ != r->data_) { return false; } return is_mirror_symmetry(l->left_, r->right_) && is_mirror_symmetry(l->right_, r->left_); } return false;}
7. 先序中序重建二叉树
根据先序序列确定当前的根结点,根据中序序列将左子树和右子树分开
重新构造左右子树的先序序列以及中序序列,递归重建二叉树
可参考:根据先序中序还原二叉树
void rebuild(int pre[], int i, int j, int in[], int m, int n) { root_ = cur_rebuild(pre, i, j, in, m, n);}// 利用序列pre区间[i, j]元素和序列in区间[m, n]元素重建二叉树Node* cur_rebuild(int pre[], int i, int j, int in[], int m, int n) { if (i > j || m > n) { return nullptr; } Node* node = new Node(pre[i]); for (int k = m; k <= n; k++) { if (pre[i] == in[k]) { // 在中序遍历序列中找到当前的结点 node->left_ = cur_rebuild(pre, i + 1, i + ( k - m ), in, m, k - 1); node->right_ = cur_rebuild(pre, i + (k - m) + 1, j, in, k + 1, n); return node; } } // 若在中序遍历序列中无法找到当前的结点,则说明先序和中序不匹配 return nullptr;}
8. 判断二叉树是否是平衡树
平衡树: 任意节点左右子树的高度差不超过1
先递归,往上回溯的时候检测当前节点是否平衡,因为回溯的时候子节点才能把高度返回给父节点,即遍历顺序为:LRV
bool is_balance() { return is_balance(root_);}bool is_balance(Node* node) { if (nullptr == node) { return true; } if (!is_balance(node->left_)) { return false; } if (!is_balance(node->right_)) { return false; } return abs(high(node->left_) - high(node->right_)) <= 1;}
计算左右孩子的高度也需要递归,复杂度比较高
改进方法:
每个递归函数就对应遍历到了一个节点,通过局部变量可记录当前节点的level,然后把返回给父节点(即调用处),父节点就可以比较两个子树的高度。同时在整个递归过程中添加一个bool变量,一旦判定遍历的当前节点失衡,将这个bool变量置为false就可以结束函数,不再向下递
bool is_balance() { int level = 0; bool flag = true; // 假定当前树为平衡树 is_balance(root_, level, flag); return flag;}int is_balance(Node* node, int level, bool& flag) { if (nullptr == node) { return level; } int left = is_balance(node->left_, level + 1, flag); if (!flag) { // 执行到这里,返回值是多少不重要,返回是为了提前结束函数,不再继续递归 // flag已经被值为false,就可以判定当前树不平衡 return -1; } int right = is_balance(node->right_, level + 1, flag); if (!flag) { return -1; } // 判断当前节点是否失衡 if (abs(left - right) > 1) { flag = false; } // 返回子树level的大值,也即返回了当前节点的子树中最大的深度 return max(left, right);}
9. 求中序遍历倒数第k个节点
LVR倒数第k个节点就是RVL正数第k个节点在整个递归过程中添加一个计数器cnt,记录当前遍历的是第几个节点,一旦cnt == k则表示找到了该节点
// 返回中序遍历LVR倒数第k个节点,也即RVL正数第k个节点T get_k_node(const int k) { int cnt = 0; Node* node = get_k_node(root_, cnt, k); if (nullptr != node) { return node->data_; } try { string error = "不存在中序遍历倒数第"; error += to_string(k); error += "个节点"; throw error; } catch(string error){ cout << error<
10. 层序遍历析构二叉树
~BSTree() { if (nullptr != root_) { queue
完整代码上传至:tree-code
11. 子树问题
剑指 Offer 26. 树的子结构
bool compare(TreeNode* node1, TreeNode* node2){ if(node2 == nullptr){ // node2先为空 return true; } if(node1 == nullptr){ // node2不空,node1先空了 return false; } return (node1->val == node2->val) && compare(node1->left, node2->left) && compare(node1->right, node2->right); } bool isSubStructure(TreeNode* A, TreeNode* B) { if(A == nullptr || B == nullptr){ // 约定nullptr不是任何树的子树 return false; } if(A->val == B->val){ // 如果在A中找到和B根节点值相同的节点,则使用compare分别比较它们的左右子树的值是否都相等 // 如果左右子树的值也相等,则可以返回true // 若左右子树的值不相等,则继续寻找 if(compare(A->left, B->left) && compare(A->right, B->right)){ return true; } } return isSubStructure(A->left, B) || isSubStructure(A->right, B); }
先在大树A中找到和小树B根节点值相同的节点node,然后使用compare函数递归地比较大树A中从node节点开始的子树是否包含小树B
bool isSubStructure(TreeNode* A, TreeNode* B) { if(A == nullptr || B == nullptr){ return false; } if(compare(A, B)){ return true; } return isSubStructure(A->left, B) || isSubStructure(A->right, B); // return compare(A, B) || isSubStructure(A->left, B) || isSubStructure(A->right, B); }
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~