【二叉搜索树】

[本节目标]

  • 1. 二叉搜索树实现

  • 2.二叉树搜索树应用分析

  • 3. 二叉树进阶面试题

1. 二叉搜索树

1.1 二叉搜索树概念

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

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

2.2 二叉搜索树操作及实现

下面就是一颗二叉搜索树,我们下面的模拟实现都按照这棵树来实现。

 

int a[] = {8, 3, 1, 10, 6, 4, 7, 14, 13};

根据我们之前学习的二叉树章节,我们是用左孩子右兄弟来描述一个结点的,所以我们这里也先来描述一下结点的信息,之前我们使用的是struct,这里我们就是用class来描述结点信息。首先使用结构体+模板来创建结点,里面需要给出左子树,右子树,结点的值。只需要写一个构造函数对其值赋初值就行了。

// T - type   K - 关键字
template <class K>
//struct BinarySearchTreeNode
struct BSTreeNode
{
	typedef BSTreeNode<K> Node;

	Node* _left;
	Node* _right;
	K _key;

	BSTreeNode(const K& key)
		:_left(nullptr)
		,_right(nullptr)
		,_key(key)
	{}
};

1. 二叉搜索树的查找

        a、从根开始比较,查找,比根大则往右边走查找,比根小则往左边走查找。

        b、最多查找高度次,走到到空,还没找到,这个值不存在。

对比我们之前的二分查找算法,我们来看看二分查找算法的缺陷

我们之前学习的查找就是二分查找,但是该查找的前提必须要求数组有序,所以每次插入一个数据,就可能要导致重新排序,并且在数组中还要挪动数据,非常难以维护,而对于搜索二叉树成本低,查找效率也快,最坏情况查找也只会走二叉搜索树的高度次,而且搜索二叉树的中序遍历就是有序的。

bool Find(const K& key)//查找key值
{
	Node* _cur = _root;
	while (_cur)
	{
		if (_cur->_key < key)
		{
			_cur = _cur->_right;
		}
		else if(_cur->_key >  key)
		{
			_cur = _cur->_left;
		}
		else
		{
			//找到了key值
			return true;
		}
	}
	//此时cur以已经为空,说明找不到key值
	return false;
}

2. 二叉搜索树的插入

注意:如果插入的key值已经存在二叉搜索树中了,此时我们就认为不能插入了。

        a. 树为空,则直接新增节点,赋值给root指针

        b. 树不空,按二叉搜索树性质查找插入位置,插入新节点

然后我们来看一下我们的代码有没有什么问题?

bool Insert(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 false;
		}
	}
	//此时_cur为空
	_cur = new Node(key);
	return true;
}

看上去挺对,但是我们忽略了一个问题,我们申请的结点给到_cur指针了,而且它时一个局部变量,出了作用域不仅消耗了,而且还会出现内存泄漏,我们此时要解决问题,就要与父指针进行链接,所以此时我们要找到_cur的父节点。

bool Insert(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
		{
			//如果要插入的值和当前值相等,那就不能插入了
			return false;
		}
	}
	//此时_cur为空
	_cur = new Node(key);
	if (_parent->_key < key)//要插入的值比当前值大
	{
		_parent->_right = _cur;
	}
	else//要插入的值比当前值小
	{
		_parent->_left = _cur;
	}
	return true;
}

但是我们的代码还存在一个小bug,如果我们的树一开始一个结点都没有,此时为空树,那么while循环体就没有进入,那么此时的_parent就还是空指针,那么此时访问_parent结点里面的元素就会出现错误。

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
		{
			//如果要插入的值和当前值相等,那就不能插入了
			return false;
		}
	}
	//此时_cur为空
	_cur = new Node(key);
	if (_parent->_key < key)//要插入的值比当前值大
	{
		_parent->_right = cur;
	}
	else//要插入的值比当前值小
	{
		_parent->_left = cur;
	}
	return true;
}

根据二叉搜索树的中序是有序的,所以我们这里通过走一遍中序来测试我们的程序,所以我们这里先来实现一下中序。

void InOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	InOrder(root->_left);
	cout << root->_key << " ";
	InOrder(root->_right);
}

但是我们这里的中序无法调用,因为我们的中序要求传入根结点,如果不传入根节点我们这里就无法递归了,我们这里有两种解决方法,第一个方法是写一个获取根节点的函数,因为我们的根节点是私有的,或者我们还可以套一层子函数

void _InOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
	_InOrder(root->_left);
	cout << root->_key << " ";
	_InOrder(root->_right);
}

void InOrder()
{
	_InOrder(_root);
    cout << endl;
}

现在我们来测试一下此时输出的结果。

根据二叉搜索树的中序是有序的,所以我们上面的结果是正确的。对于我们的插入和查找都是比较简单的,而对于二叉搜索树的删除才是最棘手的。

3. 二叉搜索树的删除

我们首先来看第一种情况,当左为空,将右托付给父亲,此时是托付给父亲的右还是父亲的左呢?

所以此时我们还要进行判断,若要删除的结点左为空,就将要删除结点的右托付给父亲。

再来看一下第二种情况,当右为空,将左托付给父亲,此时是托付给父亲的右还是父亲的左呢?同样的要分情况讨论。

现在我们再来看一下我们的第三种情况,它也是最复杂的,我们下面采用第二种:右子树的最左结点,此时该节点一定是左为空,右不一定为空。

我们能不能直接将结点3和结点4进行交换,然后再删除结点3呢?这样是不行的,因为交换之后按照二叉搜索树的特点,我们就不能找到3结点了。然后我们再来看看我们上面的写法有没有上面问题。如果我们删除的是根节点8呢?

如果我们删除的是根节点8,此时右树的根就为最左结点,此时循环体就没有进入,由于rightMinParent赋值为空指针,后面访问就会出现崩溃。所以我们要想解决,就不能将rightMinParent赋值为空,而需要将其赋值为_cur,并且加一个判断,rightMin有可能为rightMinParent的左,有可能为右,然后单独链接rightMin的右,这样即可解决。

这样就解决了问题,我们来测试一下。

我们再来测试一下其他情况,

如果我们把所有的值都删除呢?

此时我们发现我们的程序崩溃了,为什么呢?

我们发现此时删除最后一个结点出现了问题,父结点是空的,因为我们此时没有进入循环,直接走的else语句,由于我们赋值parent是空指针,所以此时就出现了问题,这个问题就好比我们下面的场景。

第一种情况要删除的节点左为空,需要将右托付给父亲;第二种情况要删除的节点右为空,需要将左托付给父亲,但是由于此时是根节点,父指针为空,此时要解决就要单独处理。

此时我们再来测试一下结果

现在我们再来写一下递归的形式,但是要注意这里的递归都必须要套一层,因为我们这里的_root是私有的,外部不能使用。

bool _InsertR(Node*& root, const K& key)
{
	//当走到空,就可以插入了
	if (root == nullptr)
	{
		// 如何与父亲进行链接呢?可以在传root使用传引用
		// 此时要进行链接的时候,root刚好是root->_left或者root->_right的别名
		// 此时刚好就可以把要插入的结点与父节点相链接
		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;
}

图解:

然后我们再来看一下删除的递归写法:

情况1:删除的节点右为空

情况2:删除的节点左为空

情况3:删除的节点左右都不为空

假如我们要删除的是节点是3,此时节点3刚好是root,能不能使用引用的方法,让右树的最左节点4成为root的别名,然后再替换删除节点呢?

我们这里思路是依然找到右树的最小节点rightMin,然后将rightMin与root交换,然后复用删除的代码即可

bool _EraseR(Node*& root, const K& key)
{
	//没找到
	if (root == nullptr)
		return false;

	if (root->_key < key)
		return _EraseR(root->_right, key);
	else if (root->_key > key)
		return _EraseR(root->_left, key);
	else
	{
		Node* del = root;//保存要删除的节点
		//找到了
		if (root->_right == nullptr)
			root = root->_left;
		else if (root->_left == nullptr)
			root = root->_right;
		else
		{
			// 这里不能加引用
			// 引用不能改变指向
				
			// 找右树的最小节点
			Node* rightMin = root->_right;

			Node*& rightMin = root->_right;
			while (rightMin->_left)
			{
				rightMin = rightMin->_left;
			}

			swap(root->_key, rightMin->_key);

			return _EraseR(root->_right, key);
		}

		delete del;
		return true;
	}
}

此时测试一下我们的程序

现在我们再来写一下Destory函数

void Destory(Node* root)
{
	if (root == nullptr)
		return;
	Destory(root->_left);
	Destory(root->_right);
}

通过这个Destory函数来完成我们的析构函数。

~BSTree()
{
	Destory(_root);
}

如果我们要对二叉搜索树进行拷贝呢?很明显,我们这里没有写拷贝构造函数,那就使用的是默认的拷贝构造函数,此时是浅拷贝,必然会出现问题,我们这里要使用深拷贝的写法。

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

Node* Copy(Node* root)
{
	if (root == nullptr)
		return nullptr;
	Node* newRoot = new Node(root->_key);
	newRoot->_left = Copy(root->_left);
	newRoot->_right = Copy(root->_right);
	return newRoot;
}

我们来测试一下

对于赋值的话就比较简单了,直接用一个现代写法就足够了

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

4 二叉搜索树的应用

1. K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到 的值。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:

  • 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  • 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。

2. KV模型:每一个关键码key,都有与之对应的值Value,即的键值对。该种方 式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英 文单词与其对应的中文就构成一种键值对;
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出 现次数就是就构成一种键值对。

我们上面的写的二叉搜索树的模型就是我们的K模型,现在我们来改造一下,让它是一个KV模型的数。修改我们对于插入的逻辑没有什么变化,但是在查找的时候我们的返回值需要改一下,因为我们还要找打到value,其他的变化就存储结点的结构体变化一下

// T - type   K - 关键字
template <class K, class V>
//struct BinarySearchTreeNode
struct BSTreeNode
{
	typedef BSTreeNode<K,V> Node;

	Node* _left;
	Node* _right;
	K _key;
	V _value;

	BSTreeNode(const K& key, const V& value)
		: _left(nullptr)
		, _right(nullptr)
		, _key(key)
		, _value(value)
	{}
};

我们来测试一下

void TestBSTree1()
{
	// 输入单词,查找单词对应的中文翻译
	keyvalue::BSTree<string, string> dict;
	dict.Insert("string", "字符串");
	dict.Insert("tree", "树");
	dict.Insert("left", "左边、剩余");
	dict.Insert("right", "右边");
	dict.Insert("sort", "排序");
	// 插入词库中所有单词
	string str;
	while (cin >> str)
	{
		keyvalue::BSTreeNode<string, string>* ret = dict.Find(str);
		if (ret == nullptr)
		{
			cout << "单词拼写错误,词库中没有这个单词:" << str << endl;
		}
		else
		{
			cout << str << "中文翻译:" << ret->_value << endl;
		}
	}
}

运行结果:

同时我们还可以使用KV模型统计次数

void TestBSTree4()
{
	// 统计水果出现的次数
	string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜",
   "苹果", "香蕉", "苹果", "香蕉" };
	keyvalue::BSTree<string, int> countTree;
	for (const auto& str : arr)
	{
		// 先查找水果在不在搜索树中
		// 1、不在,说明水果第一次出现,则插入<水果, 1>
		// 2、在,则查找到的节点中水果对应的次数++
		//BSTreeNode<string, int>* ret = countTree.Find(str);
		auto ret = countTree.Find(str);
		if (ret == NULL)
		{
			countTree.Insert(str, 1);
		}
		else
		{
			ret->_value++;
		}
	}
	countTree.InOrder();
}

我们来看一下运行结果

为什么我们这里的数字无序呢?中序不是有序的嘛?这里要注意一点,中序有序值得是key有序,而不是value有序。

5 二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找长度是结点在二 叉搜索树的深度的函数,即结点越深,则比较次数越多

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log(N)

最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N

问题:如果退化成单支树(插入的数据有序就出现单边树),二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插 入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。

3. 二叉树进阶面试题

3.1. 二叉树创建字符串

我们首先来看这个题目,首先题目已经告诉了我们使用前序的方法去解决这个问题,题目上要求对左子树和右子树使用括号括起来,这就涉及到了字符串进行相关操作,由于C++提供了string容器,这里拼接字符串就可以直接使用+=,而对于c语言来解决这个题目就需要使用字符数组,c语言也能解决,但是字符数组开辟多大空间呢?所以还是使用C++更方便,使用+=运算符重载就可以直接处理好空间不够的问题。

此时我们就是实现了字符串输出前序遍历的结果,但是题目要求我们给左子树和右子树带上括号,好滴,我们加上。

此时括号就出现了,但是题目又要求如果当前树的左子树和右子树为空,就要省略略括号,也就意味着只要左子树和右子树中有一个为空或者都不为空,就要加上括号。

但是我们此时的程序还没有通过,因为我们忽略了一种情况,如果当前树的右子树不为空的时候,我们不能省略左子树的括号,此时如果省略树的结构恢复就会乱套。

所以我们要修改我们的程序,如果当前树的右子树为空,我们直接省略括号,而对于左子树如果当前树的左子树为空,右子树为空,我们省略括号;右子树为空,我们省略括号。

class Solution {
public:
    string tree2str(TreeNode* root) {
        if(root == nullptr)
            return "";
        string ret = to_string(root->val);
        //左子树不为空,我们不能省略括号                1 1/0
        //左子树为空,右子树不为空,我们也不能省略括号   0 1 - 实例二
        if(root->left || root->right)
        {
            ret += '(';
            ret += tree2str(root->left);
            ret += ')';
        }
        //右子树不为空,不能省略括号
        if(root->right)
        {
            ret += '(';
            ret += tree2str(root->right);
            ret += ')';
        }
        return ret;
    }
};

此时我们就解决了这个问题。

 3.4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先

我们首先可以根据上面的图片知道最近公共祖先的特点是两个节点分别在自己的左子树个右子树上,其实对于这个题目如果每个节点都带有其父亲节点,那么这个题目就会很简单,只需要找到两个节点的父亲节点的交点,就是最近公共祖先,或者如果这个树是一颗搜索二叉树,那么也好办,最近公共祖先的特点是一个孩子在左,一个孩子在由,那么这个节点就是最近公共祖先,此时只要在二叉搜索树上从根节点开始找,找到一个节点比左边大,比右边小,那么就是最近公共祖先。

但是题目上的树都不满足上面的要求,所以我们就可以根据最近公共祖先的特征去解题,最近公共祖先的特点是一个孩子在左,一个孩子在由,那么这个节点就是最近公共祖先,如果当前节点大于p和q两个节点,就递归左树,当前节点小于p和q两个节点,就递归右树。

class Solution {
public:
    // 判断x节点是否在树中
    bool IsInTree(TreeNode* root,TreeNode* x)
    {
        if(root == nullptr)
            return false;
        if(root == x)
            return true;
        return IsInTree(root->left,x) || IsInTree(root->right,x);
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 空树直接返回
        if(root == nullptr) 
            return nullptr;

        // 一个节点在另外一个节点的子树上
        if(root == p || root == q)
        {
            return root;
        }

        bool pInLeft,pInRight,qInLeft,qInRight;
        // 对于当前节点,p在左数还是右数
        pInLeft = IsInTree(root->left,p);
        pInRight = !pInLeft;

        // 对于当前节点,q在左数还是右数
        qInLeft = IsInTree(root->left,q);
        qInRight = !qInLeft;

        // 情况
        // 1.一个在左树,一个在右树
        if((pInLeft && qInRight) || (pInRight && qInLeft))
        {
            return root;
        }
        // 2.如果都在右树,递归右树寻找
        else if(pInRight && qInRight)
        {
            return lowestCommonAncestor(root->right,p,q);
        }
        // 3.如果都在左树,递归左树寻找
        else if(pInLeft && qInLeft)
        {
            return lowestCommonAncestor(root->left,p,q);
        }
        
    }
};

此时我们去提交我们的代码,发现出现了问题。

上面提示的错误是并不是所有的控制路径都有返回值,它的意思是程序如果走到了else,是没有返回值的,此时程序就会报错,但是按照我们上面的逻辑,程序是不会走到else语句的,我们可以看到上面报错的是编译错误,程序在编译的时候不会去看程序的逻辑,编译期间只会检查语法,我们的逻辑在运行的时候才会检查。

class Solution {
public:
    // 判断x节点是否在树中
    bool IsInTree(TreeNode* root,TreeNode* x)
    {
        if(root == nullptr)
            return false;
        if(root == x)
            return true;
        return IsInTree(root->left,x) || IsInTree(root->right,x);
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        // 空树直接返回
        if(root == nullptr) 
            return nullptr;

        // 一个节点在另外一个节点的子树上
        if(root == p || root == q)
        {
            return root;
        }

        bool pInLeft,pInRight,qInLeft,qInRight;
        // 对于当前节点,p在左数还是右数
        pInLeft = IsInTree(root->left,p);
        pInRight = !pInLeft;

        // 对于当前节点,q在左数还是右数
        qInLeft = IsInTree(root->left,q);
        qInRight = !qInLeft;

        // 情况
        // 1.一个在左树,一个在右树
        if((pInLeft && qInRight) || (pInRight && qInLeft))
        {
            return root;
        }
        // 2.如果都在右树,递归右树寻找
        else if(pInRight && qInRight)
        {
            return lowestCommonAncestor(root->right,p,q);
        }
        // 3.如果都在左树,递归左树寻找
        else if(pInLeft && qInLeft)
        {
            return lowestCommonAncestor(root->left,p,q);
        }

        assert(false);//走到这里程序一定出问题
        return nullptr;
    }
};

但是无论我们提交多少次,程序的提交的时候击败的用户特别少,因为我们程序的时间复杂度最坏情况下O(N^2)。假如我们的树是一个单边树呢?只有完全二叉树和满二叉树高度才是logN。

第一次当前节点的值不等于root,由于我们是去左树中寻找,不算根节点和右树的一个节点,就只要在N-2个节点中找,找不到排除当前节点,就在N-3个节点中找,这是一个等差数列,时间复杂度在O(N2)。所以我们这里可以换一个思路,如果每个节点都带有其父亲节点,那么这个题目就会很简单,只需要找到两个节点的父亲节点的交点,就是最近公共祖先。我们可以根据这个思路,利用一个栈存储从p和q节点到根节点的路径,然后大的先走,最后同时走找交点。

此时最坏情况下就只会遍历一遍,时间复杂度也就减少到O(N)了。

class Solution {
public:
    bool GetPath(TreeNode* root,TreeNode* x,stack<TreeNode*>& path)
    {
        if(root == nullptr)
            return false;
        
        path.push(root);//直接入栈

        if(root == x)
           return true; 
        if(GetPath(root->left,x,path))
        {
            return true;
        }
        if(GetPath(root->right,x,path))
        {
            return true;
        }

        // 走到这里表示没有找打,即可出栈该数据
        path.pop();
        return false;
    }

    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        stack<TreeNode*> pPath,qPath;
        GetPath(root,p,pPath);
        GetPath(root,q,qPath);

        // 两个路径找交点
        while(pPath.size() != qPath.size())
        {
            // 找长的路径先走
            if(pPath.size() > qPath.size())
                pPath.pop();
            else
                qPath.pop();
        }
        //此时路径就会相等,找交点
        while(pPath.top() != qPath.top())
        {
            pPath.pop();
            qPath.pop();
        }
        return pPath.top();
    }
};

此时的效率也就比之前高了。

3.5. 二叉树搜索树转换成排序双向链表

看上去这个题目很简单,我们走一遍中序,然后把每个值尾插到双向链表中不就好了嘛!但是这样违背了题目的意思,题目要求将二叉搜索树转换为排序双向链表,树中节点的左指针需要指向前驱,树中节点的右指针需要指向后继。

class Solution {
public:
	void InOrderConvert(TreeNode* cur,TreeNode*& prev)
	{
		if(cur == nullptr)
			return;
		InOrderConvert(cur->left,prev);
		//cur是按照中序出现的,也就是有序
		//当前节点的左,指向前一个节点
		cur->left = prev;
		//前一个节点的右,指向当前节点
		if(prev)
		{
			prev->right = cur;
		}
		prev = cur;
		InOrderConvert(cur->right,prev);
	}

    TreeNode* Convert(TreeNode* pRootOfTree) {
		TreeNode* prev = nullptr;
        InOrderConvert(pRootOfTree,prev);
		
		TreeNode* head = pRootOfTree;
		while(head && head->left)
		{
			head = head->left;
		}
		return head;
    }
};

3.6. 根据一棵树的前序遍历与中序遍历构造二叉树。 

​​​​​​​​​​​​​​

我们这里的大思路就是通过前序来确定根,通过中序来确定左右子区间,如果区间不存在,就说明是空树,然后根据中序划分出左子树和右子树的区间,当前结点走完之后,前序进行加加就到了另一个棵树的根,然后根据第一次的方法进行递归即可。我们这里的9走完后走到20会不会将20链接到9的左边?这里是不会的,走到9的时候,此时9的左右区间都不满足begin < end,此时就是链接的空,从而此时20只会链接到3的右边。

class Solution 
{
    int prei = 0;//全局变量
public:

    TreeNode* _buildTree(vector<int>& preorder, vector<int>& inorder, int begin, int end)
    {
        if(begin > end)
        {
            return nullptr;
        }
        TreeNode* root = new TreeNode(preorder[prei++]);
        // 根据前序的根,找出中序中根的位置
        int rooti = begin;
        while(rooti <= end)
        {
            if(root->val == inorder[rooti]) 
                break;
            else
                ++rooti;
        }
        // 分隔中序左右区间
        // [begin,rooti-1][rooti][rooti+1,end]
        root->left = _buildTree(preorder,inorder,begin,rooti-1);
        root->right =  _buildTree(preorder,inorder,rooti+1,end);

        return root;
    }

    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) 
    {
        TreeNode* root = _buildTree(preorder, inorder, 0, preorder.size()-1);
        return root;
    }
};

我们来画一下递归图详细理解一下

3.7. 根据一棵树的中序遍历与后序遍历构造二叉树。

3.8. 二叉树的前序遍历,非递归迭代实现 。

这道题用递归写起来非常轻松,但是今天我们不使用递归的写法,而来使用我们的递归写法。我们这里的思路是将树分为左路结点和左路结点的右子树,先利用循环访问左路结点,然后访问的时候顺便入栈,当左路结点为空的时候,依次取栈里面的结点访问左路结点的右子树。

class Solution {
public:
    vector<int> preorderTraversal(TreeNode* root) {
        stack<TreeNode*> s;
        vector<int> v;
        TreeNode* cur = root;
        while(cur != nullptr || !s.empty()) //栈不为空代表还有左路节点的右子树没有访问
        {
            // 1.访问左路节点,左路节点入栈
            while(cur != nullptr)
            {
                v.push_back(cur->val);
                s.push(cur);
                cur = cur->left;
            }

            // 2.取出栈顶节点
            TreeNode* top = s.top();
            s.pop();

            // 3.依次访问左路节点的右子树 --- 子问题
            cur = top->right; //神之一手
            //cur可能为空,也可能不为空
        }
        return v;
    }
};

3.9. 二叉树中序遍历 ,非递归迭代实现。

由于中序是先访问左子树,所以我们就不能和上一个题目一样,cur每访问一个节点,我们就尾插到vector,我们这里要将左子树访问完才能尾插,那什么时候尾插呢?当栈为空的时候,此时左子树就已经访问完了,此时就可以尾插,所以根据上面的前序遍历,我们只需要改变尾插到vector的地方就可以。

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        stack<TreeNode*> s;
        vector<int> v;
        TreeNode* cur = root;
        while(cur != nullptr || !s.empty()) //栈不为空代表还有左路节点的右子树没有访问
        {
            // 1.访问左路节点,左路节点入栈
            while(cur != nullptr)
            {
                s.push(cur);
                cur = cur->left;
            }

            // 2.取出栈顶节点
            TreeNode* top = s.top();
            s.pop();

            // 从栈里面取出来的时候,左子树就已经访问完了
            v.push_back(top->val);

            // 3.依次访问左路节点的右子树 --- 子问题
            cur = top->right; //神之一手
            //cur可能为空,也可能不为空
        }
        return v;
    }
};

3.10. 二叉树的后序遍历 ,非递归迭代实现。

对于后序遍历,我们需要左子树已经访问完,并且右子树也需要访问完,才能访问根,这里我们就不能使用我们上面中序的逻辑,因为我们在访问左路节点的右子树的时候,此时我们取到栈里面的数据不能访问,但是不访问我们的程序又将栈里面的数据给pop了,然后再去访问根就会找不到,所以我们要对这里的pop进行控制一下,但是这里又会出现一个问题,

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        stack<TreeNode*> s;
        vector<int> v;
        TreeNode* cur = root;
        TreeNode* prev = nullptr;
        while(cur != nullptr || !s.empty()) //栈不为空代表还有左路节点的右子树没有访问
        {
            // 1.访问左路节点,左路节点入栈
            while(cur != nullptr)
            {
                s.push(cur);
                cur = cur->left;
            }

            // 2.取出栈顶节点
            TreeNode* top = s.top();

            // 栈里面取到的top代表top的左子树已经访问完了
            // 1.当前节点的右子树为空,则访问当前节点
            // 2.当前节点的右子树不为空,上一个访问的节点是右子树的根,代表右子树访问完了,此时可以访问
            // 3.都不满足,子问题访问右树
            if(top->right == nullptr || top->right == prev)
            {
                v.push_back(top->val);
                s.pop();

                prev = top;
            }
            else
            {
                // 3.依次访问左路节点的右子树 --- 子问题
                cur = top->right; //神之一手
                // cur可能为空,也可能不为空
            } 
        }
        return v;
    }
};

  • 25
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值