C++进阶--二叉树进阶

目录

1、说明

2、二叉搜索树实现

2.1、概念:

2.2、二叉搜索树的操作

2.3、二叉搜索树的实现

3、二叉树搜索树应用分析

4、二叉树进阶面试题


1、说明

首先,我们在C数据结构已经讲过了,这里再次讲进阶有以下几个原因:

a、map和set特性需要先铺垫二叉搜索树。

b、二叉树中部分面试题稍微优点难度,再次进行理解。

c、有些oj题使用c语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦。

2、二叉搜索树实现

2.1、概念:

二叉搜索树又称二叉排序树,它或者是一棵空树 ,或者是具有以下性质的二叉树 :
若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
它的左右子树也分别为二叉搜索树

2.2、二叉搜索树的操作

1. 二叉搜索树的查找
a 、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。
b 、最多查找高度次,走到到空,还没找到,这个值不存在。
2. 二叉搜索树的插入
插入的具体过程如下:
a. 树为空,则直接新增节点,赋值给 root 指针
b. 树不空,按二叉搜索树性质查找插入位置,插入新节点
3. ** 二叉搜索树的删除
首先查找元素是否在二叉搜索树中,如果不存在,则返回 , 否则要删除的结点可能分下面四种情
况:
a. 要删除的结点无孩子结点
b. 要删除的结点只有左孩子结点
c. 要删除的结点只有右孩子结点
d. 要删除的结点有左、右孩子结点
看起来有待删除节点有 4 中情况,实际情况 a 可以与情况 b 或者 c 合并起来,因此真正的删除过程
如下:
情况 b :删除该结点且使被删除节点的双亲结点指向被删除节点的左孩子结点 -- 直接删除
情况 c :删除该结点且使被删除节点的双亲结点指向被删除结点的右孩子结点 -- 直接删除
情况 d :在它的右子树中寻找中序下的第一个结点 ( 关键码最小 ) ,用它的值填补到被删除节点
中,再来处理该结点的删除问题 -- 替换法删除
如果有两个孩子,替换法:找左子树的最大节点(即最右节点),右子树的最小节点(即最左节点)

2.3、二叉搜索树的实现

循环

template<class K>
struct BSTreeNode
{
    BSTreeNode<K>* _left;
    BSTreeNode<K>* _right;
    K _key;
    BSTreeNode(const K& key)
        :_left(nullptr)
        ,_right(nullptr)
        ,_key(key)
    {}
};
template<class K>
class BSTree
{
    typedef BSTNode<K> Node;
public:
    BSTree()
    :root(nullptr)
  {}
    //查找
    bool Find(const K& key)
    {
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			cur = cur->_right;

		}
		else if (cur->_key > key)
		{
			cur = cur->_left;
		}
		else
		{
			return true;
		}		
   }
   return false;
   }
//插入
bool Insert(const K& key)
{
	if (_root == nullptr)
	{
		_root = new Node(key);
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)  //插入的值大于本来的 往右走
		{
			parent = cur;
			cur = cur->_right;
		}
		else if(cur->_key > key)  //小于往左走
		{
			parent = cur;
			cur = cur->_left;
		}
		else  //相等 直接返回false
		{
			return false;  
		}
	}
	//找到了父亲
	cur = new Node(key);
	if (parent->_key < key)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}

}
    //删除
bool Erase(const K& key)   //需要考虑要删除的节点是根节点的情况 这种要分情况讨论
{
	Node* parent = nullptr;
	Node* cur = _root;

	while (cur)
	{
		if (cur->_key < key)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_key > key)
		{
			parent = cur;
			cur = cur->_left;
		}
		else //找到了
		{
			
			if (cur->_left == nullptr)  //要删除的节点左为空 然后还要判断它是它的父亲的左孩子还是右孩子
			{
				if (cur == _root)  //要删除的是根
				{
					_root = cur->_right;
				}
				else
				{
					if (parent->_right == cur)  //是右孩子,直接让父亲的右指向它的右
					{
						partent->_right = cur->_right;
					}
					else
					{
						partent->_left = cur->_right;
					}
				}

			}
			if (cur->_right == nullptr) //要删除的右节点为空
			{
				if (cur == root)
				{
					_root = cur->_left;
				}
				else
				{
					if (parent->_right = cur)
					{
						parent->_right = cur->_left;
					}
					else
					{
						parent->_left = cur->_left;
					}
				}
			}
			else//要删除的左右都不为空
			{
				//找替代节点
				Node* parent = cur;
				Node* leftMax = cur->_left; //可以去左面找 找左面的最大值(最大值在最右边)
				while (leftMax->_right)
				{
					parent = leftMax;
					leftMax = leftMax->_right;  //一直往右边走
				}
				swap(cur->_key, leftMax->_key); //交换值

				if (parent->_left == leftMax)   //这是leftMax没有 右 时
				{
					parent->_key = leftMax->_left;
				}
				else
				{
					parent->_right = leftMax->_left;
				}
				cur = leftMax;//交换指针

			}
			delete cur;  
			return true;
		}
	}
	return false;
}

private:
    Node* root;
};

递归

	bool _FindR(const K& key)
	{
		return _FindR(_root, key);
	}
	bool InsertR(const K& key)
	{
		return _InsertR(_root, key);
	}
	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 _Erase(root->_right, key);  //传参上去,root就是一个节点右指针的别名

		}
		else(root->_key > key)
		{
			return _Erase(root->light, key);
		}
		else //找到了相等的
		{
			//1、左为空
			//2、右为空
			//3、左右都不为空

			Node* del = root;

			if (root->_left == nullptr)
			{
				root = root->_right;
			}
			else if (root->_right == nullptr)
			{
				root = root->_left;  // root:要删除的节点的的父节点的右指针  ( root-> )就是要删除的节点 root->_left 是它的左节点
			}
			else
			{
				Node* leftMax = root->_left;
				while (leftMax->_right)
				{
					leftMax = leftMax->_right;
				}
				swap(leftMax->_key, root->_key);

				return _EraseR(root ->left, key); //然后再到左树去删除这个节点就ok了
			}

			delete del;
			return true;
		}
	}
	bool _InsertR(Node*& root, const K& key)  //加&  root是上一个节点指针的别名
	{
		if (root == nullptr)
		{
			root = new Node(key); //传下来的就是指针,所以直接指向就好了  //语法层面&不开空间 底层开空间,因为&是用指针实现的
			return true;
		}
		if (root->_key < key)
		{
			return InsertR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return InsertR(root->_left, key);
		}
		else
		{
			return false;
		}
	}
	bool _FindR(Node* root, const K& key)
	{
		if (root == nullptr)
		{
			return false;
		}
		if (root->_key < key)
		{
			return _FindR(root->_right, key);
		}
		else if (root->_key > key)
		{
			return _FindR(root->_left, key);
		}
		else
		{
			return true;
		}
	}
	Node* _root;
	
};

3、二叉树搜索树应用分析

1. K 模型: K 模型即只有 key 作为关键码,结构中只需要存储 Key 即可,关键码即为需要搜索到
的值
比如: 给一个单词 word ,判断该单词是否拼写正确 ,具体方式如下:
以词库中所有单词集合中的每个单词作为 key ,构建一棵二叉搜索树
在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
2. KV 模型:每一个关键码 key ,都有与之对应的值 Value ,即 <Key, Value> 的键值对 。该种方
式在现实生活中非常常见:
比如 英汉词典就是英文与中文的对应关系 ,通过英文可以快速找到与其对应的中文,英
文单词与其对应的中文 <word, chinese> 就构成一种键值对;
再比如 统计单词次数 ,统计成功后,给定单词就可快速找到其出现的次数, 单词与其出
现次数就是 <word, count> 就构成一种键值对

4、二叉树进阶面试题

1. 二叉树创建字符串

给你二叉树的根节点 root ,请你采用前序遍历的方式,将二叉树转化为一个由括号和整数组成的字符串,返回构造出的字符串。空节点使用一对空括号对 "()" 表示,转化后需要省略所有不影响字符串与原始二叉树之间的一对一映射关系的空括号对。

转化为:1(2(4))(3)        转化为:1(2()(4))(3)
class Solution {
public:
    string tree2str(TreeNode* root) 
    {
        if(root == nullptr)
            return "";

        string str = to_string(root->val);

        if(root->left || root->right)
        {
        str += '(';
        str += tree2str(root->left);
        str += ')'; 
        }
        
        if(root->right)
        {
        str += '(';
        str += tree2str(root->right);
        str += ')';
        }
        return str;
    }
};
2. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。
例如7的祖先是:2、 5、 3 就这样依次往根走
  a、p或q指向根,这个根是祖先
b、一旦发现p在左,q在右,这个节点就是祖先 
//找共同的祖先
//1、如果p、q发现在左右 那么这个节点就是根
//2、p、q都在左 再递归调用这个函数,传入的参数是左边的
//3、都在右 递归 传入右
//4、如果root等于p或q的一个 这个节点就是祖先(要先判断)

class Solution
{
public:

bool Find(Node* tree,Node* ptr)
{
    if(tree == nullptr)
    {
        return false;
    }
    return tree == tree || Find(tree->right,ptr) || Find(tree ->left , ptr);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) 
{
    if(root == nullptr)
    {
        return NULL;
    }
    if(root == p || root == q)
    {
        return root;
    }
    bool pInLeft,PInRight, qInLeft,qInRight;//设置bool值
    pInLeft = Find(pInLeft,p); //找到了才返回1
    pInRight =! pInLeft;
    qInLeft = Find(qInLeft, q);
    qInRight =! qInRight;

    if(pInLeft && qInLeft ) //都在左边
    {
        //root = lowestCommonAncestor(root ->left,p, q);//写法错误,
//因为都在左需要传入新的根再去执行这个函数,找到后就直接结束了,
//不需要再执行没有执行完的剩余的函数。
        return lowestCommonAncestor(root ->left,p, q);
    }
    else if(pInRight && qInRight)
    {
        return  lowestCommonAncestor(root -> right, p, q);
    }
    else
    return root;
}
};

3. 二叉树搜索树转换成排序双向链表
注意:不能创建新节点,必须在原树上操作。
class Solution 
{
public:
	void InOrder(TreeNode* cur, TreeNode*& prev) //传入指向节点的指针的引用,这意味着在函数内部对prev值进行修改会反映到函数外部
//通过引用传递 prev,InOrder 函数能够记录并更新当前节点的前一个节点。
//这种方法允许在递归调用中追踪和更新前一个节点的状态,而不需要返回值来传递这个信息。
	{
		if(cur == nullptr)
			return;

		InOrder(cur->left,prev);
		//当前left指向前一个
		cur->left = prev;
		//前一个right指向当前节点
		if(prev)
			prev->right = cur;


		prev = cur;

		InOrder(cur->right,prev);

	}
    TreeNode* Convert(TreeNode* pRootOfTree) 
	{
        TreeNode* prev = nullptr;
		InOrder(pRootOfTree,prev);

		TreeNode* head = pRootOfTree; //
		while(head && head->left)
		{
			head = head->left;        //获取链表头节点
		}
		return head;
    }
};

上面代码的几点说明:
1、传指针的&:prev在函数内部修改可以反应到函数外。 允许在递归调用中追踪和更新前一个节点的状态,而不需要返回值来传递这种信息。

2、在if(prev)的前提下执行    prev->right = cur;的原因:

a、确保 prev 指针在使用之前已经被正确初始化,并且它确实指向一个有效的对象。prev可能为nullptr如果直接prev->right 就会解引用空指针,这将导致未定义行为或者程序崩溃

b、保证逻辑的正确:只有当prev有效时,才能将 cur 正确地连接到前一个节点的右子树上。

3、当前的left指向前(prev) prev的right指向当前

4、InOrder不需要返回值的原因:只需要在原树上对指针的指向进行改变,就好了,而且不需要在此函数中拿到头节点。

 
题目分析:
前序:根左右  确定根
中序:左根右  由确定的根分左右区间
前序遍历数组中要有一个数组索引,而且必须传&,这样可以在递归的时候这个值进行更新
前序遍历数组中,这个这个指针(prei)指向的节点就是根
所以是先在前序确定根,再在中序中一次遍历,找的那个值的位置,然后将中序的数组按照这个位置进行划分,确定左 右树的范围。 确定好小范围之后,在对小范围进行递归。
class Solution
{
public:
    TreeNode* build(vector<int>& perorder, vector<int>& inorder,
        int& prei,int inbegin, int inend)
    {
        if(inbegin > inend)
        {
                return NULL;
        }
        TreeNode* root = new TreeNode(perorder[prei]);
        int rooti = inbegin;
        while(rooti <= inend)
        {
        if(perorder[prei] == inorder[rooti])
            break; //在中序的数组里面找到根了

            ++rooti;//while循环必须有循环跳出的条件
        }
         ++prei; //找到根之后 再到前序数组里面往后挪一个
    root->left = build(perorder, inorder, prei, inbegin, rooti-1);
    root->right= build(perorder,inorder, prei, rooti+1, inend);
    return root;
    }
    TreeNode* buildTree(vector<int>& perorder, vector<int>& inorder)
    {
        int i = 0;
        return build(perorder,  inorder,i,0, inorder.size()-1);
    }
};

​​​​
区别:根构建完之后先构建右子树再构建左子树

class Solution {
public:
    TreeNode* bulid(vector<int>& inorder,vector<int>& postorder,int& posi,
                    int inbegin, int inend)
    {
        if(inbegin > inend)
        {
            return NULL;
        }
        TreeNode* root = new TreeNode(postorder[posi]);
        int rooti = inbegin;
       
        while(rooti <= inend)
        {
            if(postorder[posi] == inorder[rooti])
            break;

            ++rooti;
        }
        --posi;
        //注意,根构建完之后先构建右子树再构建左子树
        root->right = bulid(inorder,postorder, posi,rooti+1,inend);
        root->left = bulid(inorder,postorder,posi,inbegin,rooti-1);
       
        return root;
    }
    TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder)
    {
        int i = postorder.size()-1;
       return bulid(inorder,postorder,i,0,inorder.size()-1);
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值