【C++】二叉搜索树

目录

  • [1. 概念](#1. 概念)
  • [2. 树的实现](#2. 树的实现)
  • [3. 应用](#3. 应用)
  • [4. OJ题](#4. OJ题)

1. 概念

二叉搜索树又称二叉排序树,它或者是一棵空树,或者是具有以下性质的二叉树:

  • 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  • 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  • 它的左右子树也分别为二叉搜索树

上图中每颗子树都满足上述特点。

之所以称之为搜索树是因为该树非常适合用来查找某些元素,比如以上图为例,若要查找元素6,先和根节点的值5进行比较,比5大则可以直接去它的右子树中去找,因为左子树都比5小一定查找不到,然后重复上述操作,直到查到或者到空。

这时可以发现利用搜索树查找的效率相较于暴力搜索一颗树而言提高了不少,因为上图这种情况最多只会搜索该树的高度(logN)次即可知道该元素是否存在,当然这种是最理想的情况 ,如果是下图所示的这种极端搜索树:

它的查找效率则与暴力搜索没有区别了,它的高度次即N次。

查找效率代表了二叉搜索树中各个操作的性能,针对这种极端情况后续会介绍两种树avl和红黑树,通过反转等操作将其变的平衡,这样搜索效率始终为logN次。

由于左比跟小右比跟大,因此对二叉搜索树进行中序遍历时得到的序列自然是升序排列。

2. 树的实现

cpp 复制代码
//树节点的结构定义
template<class K>
class BSTreeNode {
public:
	BSTreeNode(const K& key = K()) 
		:_k(key), _left(nullptr), _right(nullptr)
	{
		;
	}
	K _k;
	BSTreeNode<K>* _left;
	BSTreeNode<K>* _right;
};

template<class K>
class BSTree {
	typedef BSTreeNode<K> Node;
public:
	BSTree() :_root(nullptr) {
		;
	}

	BSTree(const BSTree<K>& t) {
		_root = _copy(t._root);
	}

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

	~BSTree() {
		destroyTree(_root);
	}

	//非递归插入
	bool insert(const K& key) {
		if (!_root) {
			_root = new Node(key);
			return true;
		}	 
		Node** cur = &_root;
		while (*cur) {
			if ((*cur)->_k < key) {
				cur = &((*cur)->_right);
			}
			else if ((*cur)->_k > key) {
				cur = &((*cur)->_left);
			}
			else {
				return false;
			}
		}
		*cur = new Node(key);
		//需要记录当前结点的父亲节点
		//Node* father = nullptr;
		//Node* cur = _root;
		//while (cur) {
		//	father = cur;
		//	if (cur->_k < key) {
		//		cur = cur->_right;
		//	}
		//	else if (cur->_k > key) {
		//		cur = cur->_left;
		//	}
		//	else {
		//		return false;
		//	}
		//}
		最后比较选择插入位置
		//if (father->_k > key) {
		//	father->_left = new Node(key);
		//}
		//else {
		//	 father->_right = new Node(key);
		//}
		return true;
	}

	//递归插入
	bool insert_R(const K& key) {
		return _insert_R(_root, key);
	}

	//非递归删除
	bool erase(const K& key) {
		Node* father = nullptr;
		Node* cur = _root;
		while (cur) {
			if (cur->_k > key) {
				father = cur;
				cur = cur->_left;
			}
			else if (cur->_k < key) {
				father = cur;
				cur = cur->_right;
			}
			else {	 
				//没有或者只有一个孩子的情况
				if (cur->_left == nullptr) {
					//首先需要判断cur是否为根节点
					if (cur == _root) {
						//因为左子树为空因此可以直接修改root
						_root = _root->_right;
					}
					else {
						//不是根节点,需要判断cur是father的左孩子还是右孩子
						if (father->_left == cur) {
							father->_left = cur->_right;
						}
						else {
							father->_right = cur->_right;
						}
					}
				}
				else if (cur->_right == nullptr) {
					//同样的思路
					if (cur == _root) {
						_root = _root->_left;
					}
					else {
						if (father->_left == cur) {
							father->_left = cur->_left;
						}
						else {
							father->_right = cur->_left;
						}
					}
				}
				//两个孩子的情况
				else {
					//用替换法进行删除
					//即找到一个合适的节点进行替换
					//合适的节点为当前根的左子树的最大(最右)节点
					//或者右子树的最小(最左)节点
					
					//下面的做法是找到左子树的最大节点进行替换删除
					Node* prev = cur;
					Node* letfMax = cur->_left;
					while (letfMax->_right) {
						prev = letfMax;
						letfMax = letfMax->_right;
					}
					cur->_k = letfMax->_k;
					//若左子树没有右孩子需要特殊处理
					if (prev == cur) {
						prev->_left = letfMax->_left;
					}
					else {
						prev->_right = letfMax->_left;
					}
					cur = letfMax;
				}
				delete cur;
				return true;
			}
		}
		return false;
	}

	//递归删除
	bool erase_R(const K& key) {
		return _erase_R(_root, key);
	}

	//非递归查找
	bool find(const K& key) {
		Node* cur = _root;
		while (cur) {
			if (cur->_k > key) {
				cur = cur->_left;
			}
			else if (cur->_k < key) {
				cur = cur->_right;
			}
			else {
				return true;
			}
		}
		return false;
	}
	//递归查找
	bool find_R(const K& key) {
		//需要子函数传递_root进行递归
		return _find_R(_root, key);
	}

	void inorder() {
		_inorder(_root);
		cout << endl;
	}
private:
	//递归插入
	//传递每个节点指针的引用
	//因此修改root就是修改对应的左右指针,无需再进行判断
	bool _insert_R(Node*& root, const K& key) {
		if (!root) {
			root = new Node(key);
			return true;
		}
		if (root->_k > key) {
			return 	_insert_R(root->_left, key);
		}
		else if (root->_k < key) {
			return _insert_R(root->_right, key);
		}
		return false;
	}

	//递归删除
	bool _erase_R(Node*& root, const K& key) {
		if (!root) {
			return false;
		}
		if (root->_k > key) {
			return _erase_R(root->_left, key);
		}
		else if (root->_k < key) {
			return _erase_R(root->_right, key);
		}

		//同样的情况
		//1.没有或者只有一个孩子
		//2.有两个孩子

		//先保存要删除的节点
		Node* del = root;
		//传递的引用可以直接修改
		if (root->_left == nullptr) {
			root = root->_right;
		}
		else if (root->_right == nullptr) {
			root = root->_left;
		}
		else {
			//找到左子树的最大(最右)节点
			Node* leftMax = root->_left;
			while (leftMax->_right) {
				leftMax = leftMax->_right;
			}

			root->_k = leftMax->_k;
			//递归删除当前根的左子树的最右节点
			//最终的子问题会回到没有或者只有一个孩子的情况
			return _erase_R(root->_left, root->_k);
		}
		
		delete del;
		return true;
	}

	//递归查找子函数
	bool _find_R(Node* root, const K& key) {
		if (!root) {
			return false;
		}
		if (root->_k > key) {
			return _find_R(root->_left, key);
		}
		return root->_k == key || _find_R(root->_right, key);
	}
	void _inorder(Node* root) {
		if (root) {
			_inorder(root->_left);
			cout << root->_k << ' ';
			_inorder(root->_right);
		}
	}

	Node* _copy(Node* root) {
		if (!root) return root;
		Node* cproot = new Node(root->_k);
		cproot->_left = _copy(root->_left);
		cproot->_right = _copy(root->_right);
		return cproot;
	}

	void destroyTree(Node* root) {
		if (!root) return;
		destroyTree(root->_left);
		destroyTree(root->_right);
		delete root;
	}
private:
	Node* _root;
};

3. 应用

对于搜索树一般有两种存储模型:

  1. K模型,即上面实现的结构,只存储关键字K值即可,这种模型主要是用来快速检查一个事物在不在搜索树中

    比如:检查该学号的学生是否是本校的学生,做法如下:

    把一个学校中所有学生的学号为key值构建一颗搜索树,在搜索树中检查该学号是否存在,若存在则说明是本校学生,否则不是。

  2. K/V模型,每一个关键字K都对应一个值V,即<Key, Value>的键值对,这种结构的实现与第一种基本无异,只是在存储K的同时顺带存储V即可,因此这种模型不仅可以快速检查一个事物是否在树中,同时也可以取到关键字K对应值的信息

    比如:英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对。

    ...

第一种模型对应模板库中的set容器,而第二种则是map容器

4. OJ题

  1. 二叉树的最近公共祖先

使用两个栈分别保存从根节点到目标节点路径中的每个节点,由于题目保证树中一定存在目标节点,因此两个栈里保存的路径节点中一定有它们的公共祖先。

依次保存完后,两个栈中保存的节点个数可能不同,所以若不同需要让节点多的先进行pop操作,直至删除到两个栈中的节点个数相等,最后循环判断栈顶的节点是否相同,若相同就是它们的最近公共祖先,否则一起pop直到相同为止:

cpp 复制代码
class Solution {
public:
    bool find(TreeNode* root, TreeNode* x, stack<TreeNode*>& st) {
        if(!root) return false;
        st.push(root);
        if(root == x) return true;
        //先去右子树去找目标节点
        if(find(root->left, x, st)) return true;
        //找不到再去左子树去找
        if(find(root->right, x, st)) return true;
        //都找不到就说明当前节点一定不是目标节点路径上的节点
        //可以删除当前节点
        st.pop();
        return false;
    }
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        find(root, p, ppush);
        find(root, q, qpush);
        //删除至节点个数相同
        while(ppush.size() > qpush.size()) {
            ppush.pop();
        }
        while(ppush.size() < qpush.size()) {
            qpush.pop();
        }
        //直到栈顶节点的值相同即为最近公共祖先
        while(ppush.top() != qpush.top()) {
            ppush.pop();
            qpush.pop();
        }
        return ppush.top();
    }
    stack<TreeNode*> ppush, qpush;
};
  1. 将二叉搜索树转化为排序双向链表

    使用中序遍历这颗搜索树得到的序列自然是升序,在遍历的同时需要记录当前结点的前驱(上一个)结点prev,让当前结点cur的左指针指向prev(中序遍历时,对于当前结点cur,其左子树已经遍历完成了,此时cur->left可以被修改),前一个结点很容易找到,因为遍历过了,而cur的后继结点该如何找到呢?
    就好比对于今天来说,自己知道昨天发生了什么,而对于明天的事情则无从知晓,但是对于昨天来说,今天是昨天的明天,这个思路也可以很契合地套用到此题上,对于cur来说,不知道它的后继结点是什么,但是对于prev来说,它的后继结点就是cur,因此让prev->right要指向当前结点cur,此时就完成了前驱和后继的链接操作:
cpp 复制代码
class Solution {
public:
	//创建全局变量prev,初始化为空
	TreeNode* prev = nullptr;
	void inorder(TreeNode* cur) {
		if(!cur) return;
		//先遍历左子树
		inorder(cur->left);
		//此时prev保存的就是cur的前驱结点,修改指向
		cur->left = prev;
		//prev为空说明它此时还没有指向任何一个结点
		//只有不为空才可以让其指向它的后继结点cur
		if(prev) {
			prev->right = cur;
		}
		//递归到下一个结点后,cur就变成了前驱结点
		//因此赋值给prev
		prev = cur;
		inorder(cur->right);

	}
    TreeNode* Convert(TreeNode* root) {
		if(!root) return root;
		inorder(root);
		//最左边的结点即为链表表头
		TreeNode* ret = root;
		while(ret->left) {
			ret = ret->left;
		}
		return ret;
    }
};
  1. 二叉树的非递归前序遍历
    前序遍历是先访问根然后左子树最后右子树,因此在遍历过程中需要用到栈来保存访问到的每个结点,待根和左子树访问完毕后需要从栈中取出对应的结点再去遍历它的右子树。
cpp 复制代码
class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> v;
        if(!root) return v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || st.size()) {
            //1.依次访问并保存根的和根的左子树
            //2.第一步结束后,依次取出栈顶结点
            //其若有右子树,则做法同第一步
            while(cur) {
                //访问根
                v.push_back(cur->val);
                //保存当前结点
                st.push(cur);
                //访问左子树
                cur = cur->left;
            }
            //此时根和左子树都访问完毕
            //取出栈顶元素,若它的右子树不为空则去遍历它的右子树
            auto top = st.top();
            st.pop();
            //同样的方法
            cur = top->right;
        }
        return v;
    }
};
  1. 二叉树的非递归中序遍历
    非递归中序遍历的做法与前序非常相似,只是访问根的时机不同,需要先访问左子树再访问根最后是右子树:
cpp 复制代码
class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> v;
        if(!root) return v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || st.size()) {
            //先访问并保存左子树的结点
            while(cur) {
                st.push(cur);
                cur = cur->left;
            }
            //到这里左子树访问完毕
            //再访问根
            auto top = st.top();
            st.pop();
            v.push_back(top->val);
            //若当前结点有右子树
            //则用相同的方法去遍历它的右子树
            cur = top->right;
        }
        return v;
    }
};
  1. 二叉树的非递归后续遍历
    后续与前面两种做法有些许不同,前序和中序对于取出的结点只会访问一次,但是后续要求左右子树都访问完后才能访问根,那也就是说根节点是有可能会访问两次的,如何判断是否能访问根(或者是左右子数都访问完毕)才是关键,一种做法是栈中保存结构体,结构体中定义类似于标志位的变量来判断,但是这种写法有些复杂,更为简单的一种方法为定义一个前驱结点prev,prev中始终保存的是上一个访问到的结点,每当回到当前根的时候,判断若prev不等于根的右节点,说明右子树还没有访问,因此当前根也就不能访问,否则说明右子树已经遍历结束,可以访问当前根节点:
cpp 复制代码
class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> v;
        if(!root) return v;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        TreeNode* prev = nullptr;
        while(cur || st.size()) {
            //先访问它的左子树
            while(cur) {
                st.push(cur);
                cur = cur->left;
            }
            auto top = st.top();
            //左子树访问完毕后
            //若当前根结点没有右子树或者右子树==prev
            //都说明可以访问当前根节点
            if(top->right == nullptr || top->right == prev) {
                //当前结点访问完后top就变成了前驱结点
                prev = top;
                v.push_back(top->val);
                st.pop();
            }
            else {
       			//否则去访问它的右子树
                cur = top->right;
            }
        }
        return v;
    }
};
相关推荐
7年老菜鸡15 分钟前
策略模式(C++)三分钟读懂
c++·qt·策略模式
Ni-Guvara23 分钟前
函数对象笔记
c++·算法
似霰28 分钟前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
芊寻(嵌入式)38 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
獨枭40 分钟前
C++ 项目中使用 .dll 和 .def 文件的操作指南
c++
霁月风42 分钟前
设计模式——观察者模式
c++·观察者模式·设计模式
橘色的喵43 分钟前
C++编程:避免因编译优化引发的多线程死锁问题
c++·多线程·memory·死锁·内存屏障·内存栅栏·memory barrier
何曾参静谧1 小时前
「C/C++」C/C++ 之 变量作用域详解
c语言·开发语言·c++
AI街潜水的八角1 小时前
基于C++的决策树C4.5机器学习算法(不调包)
c++·算法·决策树·机器学习
JSU_曾是此间年少2 小时前
数据结构——线性表与链表
数据结构·c++·算法