BST树面试常见问题

一、二叉树和BST树理论

在这里插入图片描述

二、BST树插入删除查询操作

1. 非递归插入操作

  • 若BST树为空,root指向新生成的节点;
  • 若BST树不为空,从根节点开始进行比较,找到合适的位置,生成新的节点,并把节点的地址写入父节点相应的地址域当中。

2. 非递归删除操作

  • 没有孩子的节点,父节点地址域nullptr
  • 有一个孩子,孩子写入父节点地址域
  • 删除节点有两个孩子:找到删除节点的前驱节点(或者后继节点),用前驱或者后继节点的值覆盖掉,然后直接删除前驱或者后继节点就可以了
    (前驱节点或者后继节点最多有一个孩子,或者没有孩子,情况1或者情况2)

前驱节点:当前节点左子树中值最大的节点
后继节点:当前节点右子树中值最小的节点

为什么要找前驱或者后继节点?
因为前驱或者后继节点放到这里不影响BST树的性质

3. 代码

#include<iostream>
#include<functional>
using namespace std;

template<typename T,typename comp=less<T>>
class BSTree
{
private:
	struct Node
	{
		//T data=new T()是零构造:如果是编译器内置类型就是0或者0.0,对于指针就是空,对于自定义类型相当于调用默认构造对象。
		Node(T data=new T())	
			:_data(data),_left(nullptr),_right(nullptr){}
		int _data;
		Node* _left;
		Node* _right;
	};
	Node* _root;
	comp _comp;
public:
	BSTree():_root(nullptr){}
	~BSTree(){}

	//非递归的插入操作2
	void n_insert(int val)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		//树为空,生成根节点
		if (_root == nullptr)
		{
			_root = new Node(val);
			return;
		}
		//搜索合适的插入位置,记录父节点的位置
		while(cur != nullptr)
		{
			if (cur->_data == val)//不插入元素相同的值
			{
				return;
			}
			else if (_comp(cur->_data , val))//小于
			{
				parent = cur;
				cur = cur->_right;
			}
			else//大于
			{
				parent = cur;
				cur = cur->_left;
			}
		}
		//把新节点插入到parent节点的孩子上
		if (_comp(parent->_data , val))
		{
			parent->_right = new Node(val);
		}
		else
		{
			parent->_left = new Node(val);
		}
	}
	//非递归删除操作
	void n_remove(int val)
	{
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur != nullptr)
		{
			if (cur->_data == val)//找到待删除节点
			{
				break;
			}
			if (_comp(cur->_data, val))
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				parent = cur;
				cur = cur->_left;
			}
		}
		if (cur == nullptr)//没找到待删除节点
		{
			return;
		}
		//情况3
		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;//让cur指向前驱节点转化成情况1、2
		}
		
		
		if(parent == nullptr){
			//cur没有parent说明cur是根结点
            return cur->left ? cur->left : cur->right;
        }
		
		//cur指向删除节点,parent指向其父节点统一处理情况1、2
		Node* child = cur->_left;
		if (child == nullptr)
		{
			child = cur->_right;
		}
		//把待删除节点(nullptr或者不空)写入其父亲节点相应的地址域中
		if (parent->_left == cur)
		{
			parent->_left = child;
		}
		else
		{
			parent->_right = child;
		}
		//删除当前节点;
		delete cur;
	}
	//非递归查询操作
	bool n_query(int val)
	{
		Node* cur = _root;
		while (cur != nullptr)
		{
			if (cur->_data == val)
			{
				return true;
			}
			else if (_comp(cur->_data, val))
			{
				cur = cur->_right;
			}
			else
			{
				cur = cur->_left;
			}
		}
		return false;
	}

};
int main()
{
	int arr[] = { 58,24,67,0,34,62,69,5,41,64,78 };
	BSTree<int> bst;
	for (int v : arr)
	{
		bst.n_insert(v);
	}
	bst.n_insert(12);
	bst.n_remove(58);
	cout << bst.n_query(26) << endl;
	return 0;
}

4. 递归插入查询和删除代码

#include<iostream>
#include<functional>
using namespace std;

template<typename T,typename comp=less<T>>
class BSTree
{
private:
	struct Node
	{
		Node(T data=new T())	
			:_data(data),_left(nullptr),_right(nullptr){}
		int _data;
		Node* _left;
		Node* _right;
	};
	Node* _root;
	comp _comp;
public:
	BSTree():_root(nullptr){}
	~BSTree(){}

	//递归插入操作
	void insert(const T& val)
	{
		_root = insert(_root, val);
	}
	//递归查询操作
	bool query(const T& val)
	{
		return nullptr != query(_root, val);
	}
	//递归删除操作
	void remove(const T& val)
	{
		_root = remove(_root, val);
	}
private:
	// 在以node为根结点的树中插入val,并返回插入节点后的树的根结点
	Node* insert(Node* node, const T& val)
	{
		// 找到合适的位置,生成新节点,并返回节点地址
		if (node == nullptr)
		{
			return new Node(val);
		}
		// 不插入相同数据
		if (node->_data == val)
		{
			return node;
		}
		else if (_comp(node->_data, val))
		{
			node->_right = insert(node->_right, val);
		}
		else
		{
			node->_left = insert(node->_left, val);
		}
		return node;
	}
	Node* query(Node* node, const T& val)
	{
		if (node == nullptr)
		{
			return nullptr;
		}
		if (node->_data == val)
		{
			return node;
		}
		else if (_comp(node->_data, val))
		{
			return	query(node->_right, val);
		}
		else
		{
			return query(node->_left, val);
		}
	}
	// 在以node为根结点的树中寻找值为val的节点删除
	// 删除指定节点后,需要把删除节点的孩子节点的地址返回给对应父节点
	Node* remove(Node* node, const T& val)
	{
		if (node == nullptr)
		{
			return nullptr;
		}
		if (node->_data == val)
		{
			//情况3
			if (node->_left != nullptr && node->_right != nullptr)
			{
				Node* pre = node->_left;
				while (pre->_right != nullptr)
				{
					pre = pre->_right;
				}
				node->_data = pre->_data;
				node->_left = remove(node->_left, pre->_data);
			}
			//情况1、2
			else
			{
				Node* childNode = node->_left;
				if (node->_right != nullptr)
				{
					childNode = node->_right;
				}
				delete node;
				return childNode;
			}
		}
		else if (_comp(node->_data, val))
		{
			node->_right = remove(node->_right, val);
		}
		else
		{
			node->_left = remove(node->_left, val);
		}
		return node;
	}
};

int main()
{
	int arr[] = { 58,24,67,0,34,62,69,5,41,64,78 };
	BSTree<int> bst;
	for (int v : arr)
	{
		bst.insert(v);
	}
	bst.insert(12);
	bst.remove(58);
	cout << bst.query(58) << endl;
	return 0;
}


三、BST树前中后序遍历

1. 前中后序遍历的递归方法

在这里插入图片描述

//递归前序遍历搜索
void preOrder()
{
	cout << "[递归]前序遍历搜索:";
	preOrder(_root);
	cout << endl;
}
//递归中序遍历搜索
void inOrder()
{
	cout << "[递归]中序遍历搜索:";
	inOrder(_root);
	cout << endl;
}
//递归后序遍历搜索
void pastOrder()
{
	cout << "[递归]后序遍历搜索:";
	pastOrder(_root);
	cout << endl;
}
//递归前序遍历实现
void preOrder(Node* node)
{
	if (node != nullptr)
	{
		cout << node->_data << " ";
		preOrder(node->_left);
		preOrder(node->_right);
	}
}
//递归中序遍历实现
void inOrder(Node* node)
{
	if (node != nullptr)
	{
		inOrder(node->_left);
		cout << node->_data << " ";
		inOrder(node->_right);
	}
}
//递归后序遍历实现
void pastOrder(Node* node)
{
	if (node != nullptr)
	{
		pastOrder(node->_left);
		pastOrder(node->_right);
		cout << node->_data << " ";
	}
}

2. 前中后序遍历的非递归实现

(1)前序遍历:先入右孩子,后入左孩子

(2)中序遍历:思想 L V R

  • L:从根结点开始把所有的左孩子入栈,直到左孩子为空。
  • V:然后从栈顶取出元素访问
  • R:把V的右孩子入栈,以右孩子为根结点把所有的左孩子入栈,直到左孩子为空。

(3)后序遍历:

后序遍历是LRV,和中序遍历一样,先把所有的左孩子入栈,然后取出栈顶节点(这时候不能访问),再把该节点的右子树的节点入栈。入栈完成后还需要再取栈顶元素访问R,这时候就无法找到V了。所以后序遍历无法像中序遍历一样用一个栈完成
我们将LRV -> VLR,然后再用一个栈保存出栈的结果,最后打印即可

//非递归前序遍历
void n_preOrder()
{
	cout << "非递归前序遍历:";
	stack<Node*> s;
	if (_root == nullptr)
	{
		return;
	}
	s.push(_root);
	while (!s.empty())
	{
		Node* top = s.top();
		s.pop();
		cout << top->_data << " ";
		if (top->_right != nullptr)
		{
			s.push(top->_right);
		}
		if (top->_left != nullptr)
		{
			s.push(top->_left);
		}
	}
	cout << endl;
}
//非递归中序遍历
void n_inOrder()
{
	cout << "非递归中序遍历:";
	stack<Node*> s;
	if (_root == nullptr)
	{
		return;
	}
	Node* cur = _root;
	while (!s.empty() || cur != nullptr)
	{
		if(cur != nullptr)
		{
			s.push(cur);
			cur = cur->_left;
		}
		else
		{
			Node* top = s.top();
			s.pop();
			cout << top->_data << " ";
			cur = top->_right;
		}
	}
	cout << endl;
}
//非递归后序遍历
void n_postOrder()
{
	cout << "非递归后序遍历:";
	stack<Node*> s1;
	stack<Node*> s2;
	if (_root == nullptr)
	{
		return;
	}
	s1.push(_root);
	while (!s1.empty())
	{
		Node* top = s1.top();
		s1.pop();
		s2.push(top);
		if (top->_left != nullptr)
		{
			s1.push(top->_left);
		}
		if (top->_right != nullptr)
		{
			s1.push(top->_right);
		}
	}
	while (!s2.empty())
	{
		cout << s2.top()->_data << " ";
		s2.pop();
	}
}

四、BST树层次遍历

1. 递归实现

//求树的高度
int high()
{
	return high(_root);
}
//层次遍历
void cur_level_order(int treeHigh)const
{
	cout << "递归层次遍历:";
	for (int i = 0; i < treeHigh; i++)
	{
		cur_level_order(_root, i);
	}
}
//树的高度代码实现
int high(Node* node)
{
	if (node == nullptr)
		return 0;
	int leftHigh = high(node->_left);
	int rightHigh = high(node->_right);
	if (leftHigh >= rightHigh)
	{
		return leftHigh + 1;
	}
	return rightHigh + 1;
}
//层次遍历代码实现,可输出以node为根节点的第i层的节点
void cur_level_order(Node* node, int i)const
{
	// 当树的某个分支比其他的分支都要长,就可能出现在某个第i层i不为0,node为nullptr的情况
	if (node == nullptr)
	{
		return;
	}
	if (i == 0)
	{
		cout << node->_data << " ";
		return;
	}
	cur_level_order(node->_left, i - 1);
	cur_level_order(node->_right, i - 1);
}

2. 非递归实现

void n_levelOrder()
{
	cout << " 非递归层次遍历:";
	queue<Node*> que;
	if (_root == nullptr)
	{
		return;
	}
	que.push(_root);
	while (!que.empty())
	{
		Node* front = que.front();
		que.pop();
		cout << front->_data << " ";
		if (front->_left != nullptr)
		{
			que.push(front->_left);
		}
		if (front->_right != nullptr)
		{
			que.push(front->_right);
		}
	}
}

四、常见面试题

1. BST树区间元素查找

中序遍历的结果是一个升序序列,可加入适当的条件提前停止递归

void findValues(vector<T> &vec, int i, int j)
{
	findValues(_root, vec, i, j);
}
void findValues(Node* node, vector<T> &vec, int i, int j)
{
	if (node == nullptr)
	{
		return;
	}
	if (node->_data > i)
	{
		findValues(node->_left, vec, i, j);
	}
	if (node->_data >= i && node->_data <= j)
	{
		vec.push_back(node->_data);
	}
	if (node->_data < j)
	{
		findValues(node->_right, vec, i, j);
	}
}

2. 判断一棵树是否是二叉搜索树

错误写法

bool isBST(Node* node)
	{
		if (node == nullptr)
		{
			return true;
		}
		if (node->_left != nullptr && _comp(node->_data, node->_left->_data))
		{
			return false;
		}
		if (node->_right != nnullptr && _comp(node->_right->_data, node->_data))
		{
			return false;
		}
		if (!isBST(node->_left))
		{
			return false;
		}
		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 (node == nullptr)
	{
		return true;
	}
	if (!isBST(node->_left, pre))
	{
		return false;
	}
	if (pre != nullptr)
	{
		if (_comp(node->_data , pre->_data))
		{
			return false;
		}
	}
	//更新中序遍历前驱节点
	pre = node;
	return isBST(node->_right, pre);
}

3. BST树求子树问题

所谓子树,必须被某个树包含了所有节点。子树的节点必须和该树节点完全重合。

比如判断树B是否是树A的子树,先在树B中找到与树A根节点值相同的节点1,从节点1开始遍历树A,也要同时遍历树B,遍历的同时判断即可。

bool isChildTree(const BSTree<T, comp>& child)const
{
	if (child._root == nullptr)
	{
		return true;
	}
	Node* cur = _root;
	while (cur != nullptr)
	{
		if (cur->_data == child._root->_data)
		{
			break;
		}
		else if (_comp(cur->_data, child._root->_data))
		{
			cur = cur->_right;
		}
		else
		{
			cur = cur->_left;
		}
	}
	//没找到子树的根节点
	if (cur == nullptr)
	{
		return false;
	}
	return isChildTree(cur, child._root);
}
bool isChildTree(Node* father, Node* child)const
{
	if (father == nullptr && child == nullptr)
	{
		return true;
	}
	if (father == nullptr)
	{
		return false;
	}
	if (child == nullptr)
	{
		return true;
	}
	if (father->_data != child->_data)
	{
		return false;
	}
	return isChildTree(father->_left, child->_left)
		&& isChildTree(father->_right, child->_right);

}

4. 二叉搜索树最近公共祖先LCA

假定: 所给的值都在BST树中存在

BST树中两个节点的最近公共祖先,一定是从根结点往下深入的时候碰到的第一个值介于两者之间的节点

T getLCA(const T& val1, const T& val2)
{
	if (_root == nullptr)
	{
		throw "no LCA";
	}
	Node* lca = getLCA(_root, val1, val2);
	if (lca == nullptr)
	{
		throw "no lca";
	}
	return lca->_data;
}
Node* getLCA(Node* node, const T& val1, const T& val2)
{
	if (node == nullptr)
	{
		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 mirrorReverse()
{
	mirrorReverse(_root);
}
void mirrorReverse(Node* node)
{
	if (node == nullptr)
	{
		return;
	}
	Node* tmp = node->_left;
	node->_left = node->_right;
	node->_right = tmp;
	mirrorReverse(node->_left);
	mirrorReverse(node->_right);
}

6. 判断镜像对称

在这里插入图片描述

bool mirrorSymmetry()
{
	if (_root == nullptr)
	{
		return true;
	}
	return mirrorSymmetry(_root->_left, _root->_right);
}
bool mirrorSymmetry(Node* l, Node* r)
{
	if (l == nullptr && r == nullptr)
	{
		return true;
	}
	if (l == nullptr || r == nullptr)
	{
		return false;
	}
	if (l->_data != r->_data)
	{
		return false;
	}
	return mirrorSymmetry(l->_left, r->_right)
		&& mirrorSymmetry(l->_right, r->_left);
}

7. 前序中序遍历重建二叉树

在这里插入图片描述
根据先序序列确定当前的根结点,根据中序序列将左子树和右子树分开
重新构造左右子树的先序序列以及中序序列,递归重建二叉树

void rebuild(int pre[], int i, int j, int in[], int m, int n)
{
	_root = _rebuild(pre, i, j, in, m, n);
}

//重建二叉树递归实现
Node* _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 = _rebuild(pre, i + 1, i + (k - m), in, m, k - 1);
			node->_right = _rebuild(pre, i + (k - m) + 1, j, in, k + 1, n);
			return node;
		}
	}
	return node;
}

8. 判断二叉树是否为平衡树

平衡树:任意节点的左右子树高度差不超过1

先递归,往上回溯的时候检测当前节点是否平衡,因为回溯的时候子节点才能把高度返回给父节点,即遍历顺序为:LRV

bool isBalance()
{
	return isBalance(_root);
}
bool isBalance(Node* node)
{
	if (node == nullptr)
	{
		return true;
	}
	if (!isBalance(node->_left))
	{
		return false;
	}
	if (!isBalance(node->_right))
	{
		return false;
	}
	int left = high(node->_left);
	int right = high(node->_right);
	return abs(left - right) <= 1;
}

计算左右子树的高度也需要递归,复杂度高

改进方法

  1. 每个递归函数就对应遍历到了一个节点,通过局部变量可记录当前节点的level,然后把返回给父节点(即调用处),父节点就可以比较两个子树的高度。
  2. 同时在整个递归过程中添加一个bool变量,一旦判定遍历的当前节点失衡,将这个bool变量置为false就可以结束函数,不再向下递
bool isBalance()
{
	int level = 0;
	bool flag = true;
	isBalance(_root, level, flag);
	return flag;
}
int isBalance(Node* node, int level, bool &flag)
{
	if (node == nullptr)
	{
		return level;
	}
	int left = isBalance(node->_left, level+1, flag);
	if (!flag)
	{
		flag = false;
	}
	int right = isBalance(node->_right, level+1, flag);
	if (!flag)
	{
		flag = false;
	}
	if (abs(left - right) > 1)
	{
		flag = false;
	}
	return max(left, right);
}

9. 求中序遍历倒数第k个节点

  1. LVR倒数第k个节点就是RVL正数第k个节点
  2. 在整个递归过程中添加一个计数器cnt,记录当前遍历的是第几个节点,一旦i == k则表示找到了该节点
int getVal(int k)
{
	int i = 0;
	Node* node = getVal(_root, k,i);
	if (node != nullptr)
	{
		return node->_data;
	}
	else
	{
		string err = "no No.";
		err += k;
		throw err;
	}
}
Node* getVal(Node* node, int k,int &i)
{
	if (node == nullptr)
	{
		return nullptr;
	}
	Node* right = getVal(node->_right, k,i);
	if (right != nullptr)
	{
		return right;
	}
	if (++i == k)
	{
		return node;
	}
	return getVal(node->_left, k,i);
}

10. 层次遍历实现BST树的析构

~BSTree()
{
	queue<Node*> que;
	que.push(_root);
	while (!que.empty())
	{
		Node* front = que.front();
		que.pop();
		if (front->_left != nullptr)
		{
			que.push(front->_left);
		}
		if (front->_right != nullptr)
		{
			que.push(front->_right);
		}
		delete front;
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值