C++ 树进阶系列之深度剖析字典(trie)树

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 树进阶系列之深度剖析字典(trie)树相关的知识,希望对你有一定的参考价值。

1. 前文

本文和大家一起聊聊字典树,从字典二字可知,于功能而言,字典树是类似于英汉字典的一棵信息树。字典树有 2 大特点:

  • 有容乃大。能存储大量的数据信息。
  • 提供有基于关键字的查询、检索机制。

常用字典树存储字符串(单词)信息,使用字典树能方便实现字符串的存储、查询、统计、排序……一系列操作。

2. 字典树特点

字典树是树结构的典型应用,如下图所示,为一棵字典树。字典树的叶节点起标志性作用,标记字符串的结束,类似于C++字符串的结束符号\\0

通过对结构的观察,可以大致了解字典树的几个特点:

  • 字典树不是二叉树,字典树的每一个节点可以有多个子节点,除根节点外每个节点存储一个字符信息(常用于字符信息存储,但不仅限于字符信息)。
  • 顺着根节点向子节点连接,可以找到一个字符串信息。正因为这个特性,字典树如其名,可存储大量的单词。如从根节点开始,找到它的第一个子节点a,然后找到a的子节点c,再顺着 c找到e。这样就能得到子符串ace
  • 具有公共前缀的字符串不需要重复存储,公用共同的祖先,如aceact的公共祖先节点是ac。这也是字典树的一大特点,相比较其它的存储方案,具有高度的空间利用率。

3. 字典树的物理存储

字典树常用的API插入查询,其它可根据应用场景的需要进行扩展。

字典树的物理实现可以使用矩阵链式存储 2 种方案,本文分别探讨这 2 种方案的实现过程。

3.1 链式存储

为了简化操作,实现过程中使用了STLvector容器存储任一节点的子节点,当然,也可以使用带有头节点的链表。

3.1.1 结构类型

节点类型: 存储字典树的节点信息。

#include <iostream>
#include <vector>
using namespace std;
/*
*字典树节点
*/
struct TrieNode 
	//节点对应字符
	char data;
	//所有子节点指针
	vector<TrieNode*> childs;
    TrieNode()
		this->data=#;
	 
;

字典类: 维护字典的常用API,此处先提供基本的API,后面根据需要再扩展。

/*
*字典树类
*/
class Trie 
	private:
		//根节点
		TrieNode* root;
		//字典树上所有字符串(单词)
		vector<string> words;
	public:
		/*
		*构建函数
		*/
		Trie() 
			//初始化节结点
			this->root=new TrieNode();
		
		/*
		*返回节结点
		*/
		TrieNode* getRoot() 
			return this->root;
		
		/*
		* 查询节点是否包括值为 ch 的子节点
		*/
		TrieNode* findChild(TrieNode* parent,char ch);
		/*
		*插入新的单词(字符串)
		*/
		void insert(string word);
		/*
		*查询字典树是否存在指定的字符串(单词)
		*/
		TrieNode* search(string word);
		/*
		* 查询字典树上所有字符串(单词)
		*/
		void getAllWords(TrieNode *node,string word);
		/*
		*显示字典树上的所有单词
		*/
		void showAllWords()

3.1.2 常规 API

3.1.2.1 insert函数

功能描述: 提供添加字符串(单词)的功能,是构建字典树的第一重要环节。

**实现流程:**现以添加abc字符串为例,讲解添加函数的实现过程。

  • 首先构建根节点。根节点不需要存储具体的数据信息。

  • 分解字符串abc,先读入字符a,以根节点为当前节点,查询当前节点是否存在值为a的子节点。

  • 因字典树刚创建,此子节点不存在,则为节结点新建值为a的子节点,并且当前节点指针指向新建的节点。

  • 继续从字符串中分割出字符b,检查当前节点是否存在值为b的子节点。没有则为当前节点创建值为b的新节点,重设当前节点的指针为新建节点。

  • 同理,分割出c,因当前节点不存在值为c的子节点,为当前节点构建新节点。且重设当前节点的指针。

  • 当字符串分割完毕后,最后添加值为#的叶节点作为结束标志符。

  • 在现有字典树上继续添加abd字符串(单词)时,当前节点需要重置为根结点。分割abd字符串时,因为值为ab的节点已经存在,则仅让当前节点向下滑动。

  • 分割出d字符,因当前节点不存在值为d的子节点,新建值为d的子节点。字符串分割完毕,为d节点添加结束子节点。如下图所示。

编码实现:

实现辅助函数findChild

/*
* 查询节点是否包括值为 ch 的子节点
*/
TrieNode* Trie::findChild(TrieNode* parent,char ch)
    //获取当前节点的所有子节点
    vector<TrieNode*> childs= parent->childs;
    //遍历查询
    for(int i=0; i<childs.size(); i++) 
        if( childs[i]->data==ch  ) 
            //存在
            return childs[i];
        
    
    //不存在
    return NULL;

实现insert函数:

/*
* 插入新的单词(字符串)
* 参数:word 需要添加的字符串
*/
void Trie::insert(string word) 
    //从根节点开始
    TrieNode* currentNode=this->root;
    //分割字符串
    for(int i=0; i<word.size(); i++  ) 
        //查询子节点是否存在
        TrieNode* childNode= Trie::findChild(currentNode,word[i]);
        if(childNode==NULL  ) 
            //子节点不存在,构建新节点
            TrieNode* newNode=new TrieNode();
            newNode->data=word[i];
            //成为当前节点的子节点
            currentNode->childs.push_back(newNode);
            //重设当前节点
            currentNode= newNode;
         else 
            //节点存在
            currentNode= childNode;
        
    
    //分割完毕,添加标志性节点
    TrieNode* flagNode=new TrieNode();
    currentNode->childs.push_back(flagNode);

测试插入函数:

int main(int argc, char** argv) 
	Trie* trie=new Trie();
	trie->insert("abc");
	trie->insert("abd");
	TrieNode* root=trie->getRoot();
	cout<<"根节点的子节点:"<<root->childs[0]->data<<endl;
	cout<<"根节点的子节点的子节点:"<<root->childs[0]->childs[0]->data<<endl;
	cout<<"节点的子节点的子节点的子节点:"<<root->childs[0]->childs[0]->childs[0]->data<<endl;
	cout<<"节点的子节点的子节点的子节点:"<<root->childs[0]->childs[0]->childs[1]->data<<endl;
	return 0;

输出结果:

3.1.2.2 search函数

功能描述: 查询给定的字符串(单词)是否存在于字典树中。

实现流程: 查询和插入流程相似。如果检查到存在与分割出来的字符值相等的子节点便继续向下查询,否则,认为查询失败。

不要求一定的是完整字符串(单词),仅是前缀也可以。

如下图所示,虽然字典树中不存在ab字符串(单词),因为存在ab前缀,也认为是存在的。

编码实现:

/*
* 查询字典树是否存在指定的字符串(单词)
*/
TrieNode* Trie::search(string word) 
    //从节结点开始
    TrieNode* currentNode=this->root;
    //分割需要查询的字符串
    for(int i=0; i<word.size(); i++) 
        //子节点是否存在
        TrieNode* childNode= Trie::findChild(currentNode,word[i]);
        if(childNode!=NULL) 
            //继续查询
            currentNode=childNode;
         else 
            //不存在
            return NULL;
        
    
    //查询成功,只要字符串和字典树上的字符一一对应便可
    return currentNode;

测试查询:

int main(int argc, char** argv) 
	Trie* trie=new Trie();
	trie->insert("abc");
	trie->insert("abd");
	TrieNode* root=trie->getRoot();
	TrieNode* node= trie->search("ab");
	if(node!=NULL)
		cout<<"字典树中存在 ab"<<endl; 
	else
		cout<<"字典树上不存 ab"<<endl;
	
	return 0;

输出结果:

3.1.2.3 getAllWords函数

功能描述: 返回字典树上的所有字符串(单词)。

实现流程: 对于整棵树的搜索常用的方案有深度广度搜索。针对此需求使用深度搜索便能查询出树上的所有单词。

编码实现:

实现辅助函数 showAllWords:显示字典树上的所有单词。

/*
*显示所有单词
*/
void showAllWords() 
    cout<<"字典树上的所有单词:"<<endl; 
    for(int i=0; i<this->words.size(); i++) 
        cout<<this->words[i]<<endl;
    

实现getAllWords函数:

/*
* 显示字典树上所有字符串
*/
void Trie::getAllWords(TrieNode *node,string word) 
    //当前节点
    TrieNode* currentNode=node;
    //当前节点的子节点
    vector<TrieNode*> childs=currentNode->childs;
    for(int i=0; i<childs.size(); i++) 
        if(childs[i]->data==#) 
            //如果节点值为结束符号,则获取到了完整单词
            this->words.push_back(word);
         else 
            //否则,继续递归
            string word_=word;
            word_.append( childs[i]->data );
            getAllWords(childs[i], word_ );
        
    

测试代码:

int main(int argc, char** argv) 
	Trie* trie=new Trie();
	trie->insert("abc");
	trie->insert("abd");
	TrieNode* root=trie->getRoot();
	trie->getAllWords(root,"");
	trie->showAllWords();
	return 0;

输出结果:

3.2 矩阵存储

使用矩阵存储树节点之间的关系也是一种常见方案。

基本存储思想:

  • 对每一个节点进行编号。

  • 以父节点的编号为矩阵的行号,子节点的编号为列号,行与列对应的单元格中存储节点的关系描述(或值、权重)。

在存储字典树信息时,对上述的存储方案可以稍加改进一下。

  • 如先确定根节点的编号为0,子节点a在矩阵中的列号由其对应的ASCII码决定,当然,会对其范围缩小。

    对应单元格中存储字符出现的顺序编号,并以此编号为此结点的唯一标志符号(类似于主键)。

    这个编号与字符添加顺序有关,与字符本身无关。如下图所示:

  • 字符b是字符a的子节点。添加过程如下图所示。

  • 继续完成其它节点关系的存储。

上述存储的优点:

  • 可以把矩阵的列数限制在 26 之内。
  • 查询任一节点的子节点时,可以根据字符本身所携带信息找到其存储位置。如果b节点下还有字符w的子节点,便能轻易知道其存储位置是 [2][w-a]并能获取w节点的编号。

编码实现:

与前文的链式存储相毕较,仅是改变了存储方式,逻辑是没有发生任何变化。

#include <iostream>
#include <vector>
using namespace std;
/*
* 字典树类
*/
class Trie 
	private:
		/*
		* 矩阵:存储结点之间的关系
		* 1、矩阵的行数由结点数量决定, 简化问题,此处设置为 100
		* 2、矩阵的列数由字符数量决定
		*/
		int matrix[100][26];
		//所有节点由内部统一编号
		int number;
		//字符串的节束标志
		char endFlag[100];
		//存储字典树上的所有单词
		vector<string> allWords;
	public:
        /*
        * 初始化
        */
		Trie() 
			this->number=0;
			for(int i=0; i<100; i++)
				for(int j=0; j<26; j++)
					matrix[i][j]=0;
		
		/*
		* 插入函数
		*/
		int insert(string word) 
			//当前节点指向节结点
			int current=0;
			//遍历字符串
			for(int i=0; i<word.size(); i++) 
				//检查当前节点下是否存在值为 word[i] 的子节点
				if( matrix[current][ word[i]-a ]==0 ) 
					//不存在
					matrix[current][ word[i]-a ]=++this->number;
				
				//重设当前节点
				current=matrix[current][ word[i]-a ];
			
			//添加结束标志
			endFlag[current]=#;
		

		/*
		*查询指定的字符串是否存在字典树中
		*/
		int search(string word) 
			//从根节点开始
			int current=0;
			//遍历字符串
			for(int i=0; i<word.size(); i++) 
				if( matrix[current][ word[i]-a ]==0 ) 
					//没找到
					return -1;
				
				//继续
				current= matrix[current][ word[i]-a ];
			
			return current;
		

		/*
		* 使用深度搜索算法获取树上的所有单词
		*/
		void getAllWords(int parent,string word) 
			int current=parent;
			//查询所有子节点
			for(int i=0; i<26; i++) 
				if(matrix[current][i]!=0  ) 
					if( endFlag[matrix[current][i]]==#  ) 
                         //注意此处需要添加一下
						word.append(   char(i+a)   );
						this->allWords.push_back(word);
                          //恢复
						word.pop_back();
					 else 
						string word_=word;
						word_.append(   char(i+a)   );
                          //递归调用
						getAllWords( matrix[current][i], word_);
					
				
			
		
		/*
		*显示字典树上的所有单词
		*/
		void showAllWords() 
			cout<<"字典树上的所有单词:"<<endl;
			for(int i=0; i<this->allWords.size(); i++ )
				cout<<this->allWords[i]<<endl;
        
;
//测试
int main() 
	Trie*  trie=new Trie();
	trie->insert("abc");
	trie->insert("abd");
	int res= trie->search("ab");
	cout<<res<<endl;
	trie->getAllWords(0,"");
	trie->showAllWords();
	return 0;

输出结果:

使用数组存储的优点:

  • 在查询所有单词时,会自动对其按字典进行排序。
  • 实现起来较直观,易理解。

4. 字典树的应用

至此,想必对字典树有了较好的理解,下文再提供 2 个案例 ,深入体会字典树的神奇之处。

4.1 自动补全

所谓自动补全:指当用户输入单词前缀,则会显示所有与此前缀有关联的单词。此功能在关键字搜索应用中经常可以看到。

如果字典树中存在单词集["cat","caton","cater","this"],当用户输入cat时,则自动显示所有以cat为前缀的单词:catoncater

实现原理:

  • 在字典树中查找前缀是否存在。
  • 如果存在,以此前缀最后一个字符的节点为当前节点进行深度搜索。

编码实现:

在前文的矩阵实现方案中添加如下函数。

class Trie
    //省略…… 
    /*
    *自动补全函数
    */
    void autoComplete(string prefix) 
        //查询前缀字符串在字典树中是存在
        int nodeId= this->search(prefix);
        if(nodeId==-1) 
            return;
        
        //存在,则从此节点开始进行深度搜索
        this->getAllWords(nodeId,prefix);
    
    //省略……

//测试
int main() 
	Trie*  trie=new Trie();
	trie->insert("cat");
	trie->insert("caton");
	trie->insert("cater");
	trie->insert("this");
	trie->autoComplete("cat");
	trie->showAllWords();
	return 0;

输出结果:

4.2 求 2 个字符串的最长公共前缀

所谓字符串的公共前缀指字符串前面相同的部分。如catoncater的公共前缀是cat。与自动补全功能是相逆的操作。

基本思想:

  • 使用字典树存储所有字符串。
  • 两个字符串的最长公共前缀的长度即他们所在的节点的公共祖先个数,于是,问题就转化为求公共祖先问题。

在树中求解节点的公共祖先问题可以有多种方案,本文侧重于字典树的理解,仅提供下面的穷举算法。其它方案可以自行查阅相关书籍。

/*
* 穷举算法求解 2 个字符串的求公共前缀
*/
string getMaxPrefix(string word,string word_) 
    string prefix="";
    //指向根结点
    int current=0;
    int idx=0;
    //最多查询结点数
    int len=word.size()>word_.size()?word_.size():word.size();
    while(idx<len) 
        int ok=false;
        //查询当前结点的子结点
        for(int i=0; i<26; i++) 
            if(matrix[current][i]==0)continue;
            //存在子结点,且为同一个结点
            if( matrix[current][ word[idx]-a ]!=0 &&  matrix[current][ word[idx]-a ]==matrix[current][ word_[idx]-a ]  ) 
                ok=true;
                prefix.append( word[idx] );
                current=matrix[current][ word[idx]-a ];
                break;
            
        
        if(!ok)
            break;
        idx++;
    
    return prefix;

测试:

int main() 
	Trie*  trie=new Trie();
	trie->insert("cat");
	trie->insert("caten");
	trie->insert("cater");
	trie->insert("this");
	string prefix=trie->getMaxPrefix("caten","cater");
	cout<<"公共前缀:"<<prefix;
	return 0;

输出结果:

5. 总结

本文介绍了字典树的逻辑结构,并且使用链式和矩阵2种方案实现了字典的物理存储。并且通过 2个具有代表性的案例让大家更深入理解字典树的实际应用。

C++ 树进阶系列之平衡二叉查找树( AVL)的自平衡算法

1. 前言

树的深度与性能的关系。

二叉排序树上进行查找时,其时间复杂度理论上接近二分算法的时间复杂度O(logn)

但是,这里有一个问题,如果数列中的数字顺序不一样时,构建出来的二叉排序树的深度会有差异性,对最后评估时间性能会有影响。

如有数列 [36,45,67,28,20,40],其构建的二叉排序树如下图。

基于上面的树结构,查询任何一个结点的次数不会超过 3 次。

稍调整一下数列中数字的顺序 [20,28,36,40,45,67],由此构建出来的树结构会出现一边倒的现象,即增加了树的深度。此棵树的深度为6,最多查询次数是 6 次。

可知,二叉树上的查询时间与树的深度有关,所以,减少查找次数的最好办法,就是尽可能维护树左右子树之间的对称性,也就让其有平衡性。

什么是平衡二叉排序树?

所谓平衡二叉排序树,顾名思义,基于二叉排序树的基础之上,维护任一结点的左子树和右子树之间的深度之差不超过 1。把二叉树上任一结点的左子树深度减去右子树深度的值称为该结点的平衡因子

我们经常说的平衡树指AVL树,是Adelson-VelskiiLandis1962年提出的,它的定义如下:

  • 一颗空的二叉树就AVL树。
  • 如果T是一颗非空的二叉树,TLTR是其左子树和右子树,如果TLTRAVL树且|hL-hR|<=1,其中hL和hRTLTR的高。那么T树一定是平衡二叉树。

平衡树的平衡因子只可能是:

  • 0 :左、右子树深度一样。
  • 1:左子树深度大于右子树。
  • -1:左子树深度小于右子树。

如下图,就是平衡二叉排序树,根结点的左右子树深度相差为 0, 结点 28 的左右子树深度为 1,结点 45 的左右子树深度相差为 0

平衡树的意义何在?

平衡二叉树能保证在树上操作的时间复杂度始终为O(logn)

平衡二叉排序树本质还是二叉排序树,在此基础之上,其 API 多了维持平衡的算法。

2. 平衡算法

2.1 平衡二叉排序树的抽象数据结构

结点类:

#include <iostream>
using namespace std;
/*
*结点类
*/
template<typename T>
struct TreeNode 
	//结点上附加的值
	T value;
	//左子结点
	TreeNode<T>* leftChild;
	//右子结点
	TreeNode<T>* rightChild;
	//平衡因子,默认值为 0
	int  balance;
	//无参构造
	TreeNode() 
		this->leftChild=NULL;
		this->rightChild=NULL;
		this->balance=0;
	
	//有参构造
	TreeNode(T value) 
		this->value=value;
		this->leftChild=NULL;
		this->rightChild=NULL;
		this->balance=0;
	
;

二叉平衡排序树类: 主要强调维持自平衡的特征函数。

/*
*树类
*/
template<typename T>
class BalanceTree 
	private:
		//根结点
		TreeNode<T>* root;
	public:
		
		BalanceTree(T value) 
			this->root=new TreeNode<T>(value);
		
		
		TreeNode<T>* getRoot()
			return this->root;
		

		/*
		*LL型调整
		*/
		TreeNode<T>* llRotate(TreeNode<T>* node);

		/*
		*RR 型调整
		*/
		TreeNode<T>* rrRotate(TreeNode<T>* node);

		/*
		*LR型调整
		*/
		TreeNode<T>* lrRotate(TreeNode<T>* node);

		/*
		*RL型调整
		*/
		TreeNode<T>* rlRotate(TreeNode<T>* node);

		/*
		*插入新结点
		*/
		void insert(T value);

		/*
		*中序遍历
		*/
		void inorderTraversal(TreeNode<T>* root);

		bool isEmpty() 
			return this->root==NULL;
		
;

在插入或删除结点时,如果导致树结构发生了不平衡性,则需要调整让其达到平衡。这里的方案可以有 4种。

2.2 LL型调整(顺时针)

左边不平衡时,向右边旋转。

如下图所示,现在根结点 36 的平衡因子为 1

当插入值为 18 结点,定然是要作为结点 20 的左子结点,才能维持二叉排序树的有序性,但是破坏了根结点的平衡性。根结点的左子树深度变成 3,右子树深度为1,平衡性被打破,结点 36 的平衡因子变成了2

怎样旋转才能让树继续保持平衡?

旋转思路是既然左边不平衡,必然是左高右低,向右旋转(顺时针)方能维持平衡。

  • 让结点 28 成为新根结点,结点36成为结点28的左子结点(降维左子树)。

  • 新根结点的右子树29成为原根结点36的新左子结点。

  • 原根结点成为新根结点的右子树。

旋转后,树结构即满足了有序性,也满足了平衡性。

LL 旋转算法具体实现:

/*
*LL型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::llRotate(TreeNode<T>* parentRoot) 
	//原父结点的左子结点成为新父结点
	TreeNode<T>*  newparentRoot =parentRoot->leftChild;
	// 新父结点的右子结点成为原父结点的左子结点
	parentRoot->leftChild = newparentRoot->rightChild;
	// 原父结点成为新父结点的右子结点
	newparentRoot->rightChild =parentRoot;
	// 重置平衡因子
	parentRoot->balance = 0;
	newparentRoot->balance = 0;
	return newparentRoot;

2.3 RR 型调整(逆时针旋转)

RR旋转和 LL旋转的算法差不多,只是当右边不平衡时,向左边旋转。

如下图所示,结点 50 插入后,树的平衡性被打破。

这里使用左旋转(逆时针)方案。

  • 结点45成为新根结点,原根结点 36向左旋转,将成为根结点 45 的左子结点。

  • 先将结点45 原来的左子结点成为结点36的右子结点。

  • 再将原根结点作为新根结点的左子结点。逆时针旋转后,结点45的平衡因子为 0,结点36的平衡因子为0,结点 48 的平衡因子为 -1。树的有序性和平衡性得到保持。

RR 旋转算法具体实现:

/*
*RR 型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::rrRotate(TreeNode<T>* parentNode) 
	// 右子结点
	TreeNode<T>* newParentNode = parentNode->rightChild;
	parentNode->rightChild = newParentNode->leftChild;
	//原父结点成为新父结点的左子树
	newParentNode->leftChild = parentNode;
	// 重置平衡因子
	parentNode->balance = 0;
	newParentNode->balance = 0;
	return newParentNode;

2.4 LR型调整(先逆后顺)

如下图当插入结点 28 后,结点 36 的平衡因子变成 2,则可以使用 LR 旋转算法。

  • 以结点 29 作为新的根结点,结点27以结点29为旋转中心,逆时针旋转。

  • 结点36以结点29为旋转中心向顺时针旋转。

最后得到的树还是一棵二叉平衡排序树

LR 旋转算法实现:

/*
*LR型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::lrRotate(TreeNode<T>* p_node) 
	// 原根结点的左子结点
	TreeNode<T>* b = p_node->leftChild;
	//得到新的根结点
	TreeNode<T>* new_p_node = b->rightChild;
	//更新原根结点的左子结点
	p_node->leftChild = new_p_node->rightChild;
	b->rightChild = new_p_node->leftChild;
	//更新新根结点的左子结点
	new_p_node->leftChild = b;
	// 更新新根结点的右子结点
	new_p_node->rightChild = p_node;
	//重置平衡因子
	if (new_p_node->balance == 1) 
		p_node->balance = -1;
		b->balance = 0;
	 else if (new_p_node->balance == -1) 
		p_node->balance = 0;
		b->balance = 1;
	 else 
		p_node->balance = 0;
		b->balance = 0;
	
	new_p_node->balance = 0;
	return new_p_node;

2.5 RL型调整

如下图插入结点39 后,整棵树的平衡打破,这时可以使用 RL 旋转算法进行调整。

  • 把结点40设置为新的根结点,结点45以结点 40 为中心点顺时针旋转,结点36逆时针旋转。

RL 算法具体实现:

/*
*RL型调整
*/
template<typename T>
TreeNode<T>* BalanceTree<T>::rlRotate(TreeNode<T>* p_node) 
	//原根结点的右子树
	TreeNode<T>* b = p_node->rightChild;
	//新根结点
	TreeNode<T>* new_p_node = b->leftChild;
	//更新右子树
	p_node->rightChild = new_p_node->leftChild;
	b->leftChild = new_p_node->rightChild;
	new_p_node->leftChild = p_node;
	new_p_node->rightChild = b;
	if (new_p_node->balance == 1) 
		p_node->balance = 0;
		b->balance = -1;
	 else if (new_p_node->balance == -1) 
		p_node->balance = 1;
		b->balance = 0;
	 else 
		p_node->balance = 0;
		b->balance = 0;
	
	new_p_node->balance = 0;
	return new_p_node;

2.6 插入算法

编写完平衡算法后,就可以编写插入算法。在插入新结点时,需要检查是否破坏二叉平衡排序树的的平衡性,否则调用平衡算法。

当插入一个结点后,为了保持平衡,需要找到最小不平衡子树。

什么是最小不平衡子树?

指离插入结点最近,且平衡因子绝对值大于 1 的结点为根结点构成的子树。

如下图所示,树结构整体上是平衡的,但根结点的平衡因子是 1,其实是一个脆弱的临界值,插入或删除操作就有可能打破这个平衡因子。

如插入值为 20 的结点,因为小于根结点的值,必然会导致从插入位置一路向上,一直到根结点所有直接、间接父结点的平衡因子发生变化。此时,可以把根结点到插入的新结点之间的树称为最小不平衡子树

出现了最小不平衡树,就要考虑怎么旋转,方能维持平衡。

/*
*插入新结点
*/
template<typename T>
void BalanceTree<T>::insert(T value) 
	// 创建新结点
	TreeNode<T>* new_node =new TreeNode<T>(value);
	if (BalanceTree<T>::root==NULL) 
		//如果是空树
		BalanceTree<T>::root = new_node;
		return;
	
	//初始设定根结点为最小平衡树
	TreeNode<T>* min_b = BalanceTree<T>::root;
	//存储前驱结点
	TreeNode<T>* f_node = NULL;
	//移动指针
	TreeNode<T>* move_node = this->root;
	TreeNode<T>* f_move_node = NULL;
	//查找
	while (move_node!=NULL) 
		if (move_node->value == value)
			//结点已经存在
			return;
		if (move_node->balance != 0) 
			// 记录最小不平衡子树
			min_b = move_node;
			//记录其前驱
			f_node = f_move_node;
		
		//移动之前,记录前驱
		f_move_node = move_node;

		if (new_node->value < move_node->value)
		   //向左边移动	
            move_node = move_node->leftChild;
		else
            //向右边移动
			move_node = move_node->rightChild;
	

	if (new_node->value < f_move_node->value)
        //插入在左边
		f_move_node->leftChild = new_node;
	else
        //插入在右边
		f_move_node->rightChild = new_node;
    
    //开始更新最小不平衡树上各父结点的平衡因子
	move_node = min_b;
	// 修改相关结点的平衡因子
	while (move_node != new_node) 
		if (new_node->value < move_node->value) 
			move_node->balance++;
			move_node = move_node->leftChild;
		 else 
			move_node->balance--;
			move_node = move_node->rightChild;
		
	

	if (min_b->balance > -2 && min_b->balance < 2)
		//插入结点后没有破坏平衡性
		return;

	TreeNode<T>* b=NULL;
	if (min_b->balance == 2) 
		b = min_b->leftChild;
		if (b->balance == 1)
            //打破平衡,且左边高
			move_node = BalanceTree<T>:: llRotate(min_b);
		else
            //打破平衡,右边高
			move_node = BalanceTree<T>::lrRotate(min_b);
	 else 
		b = min_b->rightChild;
		if (b->balance == 1)
			move_node = BalanceTree<T>::rlRotate(min_b);
		else
			move_node = BalanceTree<T>::rrRotate(min_b);
	
	if (f_node==NULL)
		BalanceTree<T>::root = move_node;
	else if (f_node->leftChild == min_b)
		f_node->leftChild = move_node;
	else
		f_node->rightChild = move_node;

也可以在结点类中添加一个指向父指针的成员变量,插入数据后,由下向上查找且更新平衡因子。

中序遍历: 二叉平衡排序树本质还是二树排序树,使用中序遍历输出的数字应该是有序的。

/*
*中序遍历
*/
template<typename T>
void BalanceTree<T>::inorderTraversal(TreeNode<T>* root) 
	if (root==NULL)
		return;
	BalanceTree<T>::inorderTraversal(root->leftChild);
	cout<<root->value<<"->";
	BalanceTree<T>::inorderTraversal(root->rightChild);

测试代码。

int main(int argc, char** argv) 
    int	nums[] = 3, 12, 8, 10, 9, 1, 7;
    BalanceTree<int>* tree=new BalanceTree<int>(3);
    for (int i=1;i<sizeof(nums)/4;i++)
        tree->insert(nums[i]);
    // 中序遍历    
    tree->inorderTraversal(tree->getRoot());
	return 0;

输出结果:

3. 总结

利用二叉排序树的特性,可以实现动态查找。在添加、删除结点之后,理论上查找到某一个结点的时间复杂度与树的结点在树中的深度是相同的。

但是,在构建二叉排序树时,因原始数列中数字顺序的不同,则会影响二叉排序树的深度。

这里引用二叉平衡排序树,用来保持树的整体结构的平衡性,方能保证查询的时间复杂度为 Ologn(n 为结点的数量)。

以上是关于C++ 树进阶系列之深度剖析字典(trie)树的主要内容,如果未能解决你的问题,请参考以下文章

字典树(前缀树)--Trie

trie(字典树) C++版本 Python版本

C++ 树进阶系列之线段树和它的延迟更新

数据结构之Trie树

C++ 树进阶系列之树状数组的树形之路

ACM入门之字典树/Trie