字典树、前缀树

博文引用:

参考博文

前言:

        “字典树”也被称为“前缀树”,它可以利用公共前缀、已知信息从而实现快速的插入、查找功能,从而降低了算法的时间复杂度和空间复杂度。

        例如在文字游戏的开发过程中,判断某一个单词是否存在于词库文件中,最常见、无脑的方式就是采用for循环进行挨个对比。这种暴力算法的效率极低,此时我们便可以使用“字典树”的优势来解决我们需要处理的问题。

        在游戏开发中经常遇到的一种情形是:给出一系列的奖项以及对应的权重,随机生成一个数,通过权重来确定要给与哪一个奖项。很自然想到的一种方式就是for循环不断的累计权重的值,直至累计值满足当前的随机数就是我们得到的奖项。但是这种算法的效率极低,有一种更优的算法是“前缀树”+“二分查找”的方式。

        对于字符串的“KMP模式匹配算法”,实际也是采用了“前缀树”的方式来进行处理的,重复利用已知信息,提高数据复用率。

示例:

        比如,我们需要使用字典树存下以下单词:"abc"、"abb"、"bca"、"bc",则字典树的存储结构如下:

字典树、前缀树_第1张图片

        在图中,红色代表以此节点为终点的单词。如果我们需要查找“abc”、“ab”、“bb”,则查询结果如下:

字典树、前缀树_第2张图片

  1. “abc”都在字典树中可以被查到,并且最后一个点是红色,表示有一个以此为结束的单词,查询成功。
  2. “bb”的第二个字母没有在相应的位置被查询到,因此“bb”不在字典中。
  3. “ab”虽然单词中的每个点都不查询到了,但是由于结束的字母在树中没有被标红,因此它也不在字典树中。

字典树代码实现:

#include 
#include 
#include 
using namespace std;

/*
* 字典树、前缀树:快捷插入、查找
*	利用公共前缀,已知信息,降低时间复杂度和空间复杂度。
*   游戏开发过程中,可以用于检索词库,判断某个但是是否存在于词库中。而不是采用
* 低效率的暴力循环算法。
*	字符串的的KMP模式匹配算法,也是采用的是前缀树的方式来进行处理的。
*	游戏开发过程中,给出一些列的奖项以及对应的权重,随机生成一个数,通过权重来
* 确定给与哪一个奖项。这里不用暴力循环,而是采用前缀树+二分查找的方式,算法效率
* 更高。
*/

// 最大节点数量
const int g_MaxSize = 1000050;	
// 字典树
int g_dictTree[g_MaxSize][26];
// 标记字典树中的节点是否是一个结束节点
int g_finishFlag[g_MaxSize];
// 
int g_id;


// 插入操作:字典树的建立过程
void dictTreeInsert(std::string wordStr)
{
	int curNodeIdx = 0; // 当前操作的上方节点编号
	for (auto i = 0; i < wordStr.size(); i++) {
		int x = wordStr[i] - 'a'; // 确定下方连接字母的索引编号:a,b,c对应0,1,2,
		if (0 == g_dictTree[curNodeIdx][x]) {
			// 表示当前上方节点的连接节点中没有当前想要查找的字母wordStr[i];
			// 那就新建一个节点,并且给出该节点的编号为:++g_id;
			g_dictTree[curNodeIdx][x] = ++g_id;
		}
		// 当前查询的字母wordStr[i]存在或者已经新建完毕之后,继续往下查找下一个字符wordStr[i+1]
		// 此时就需要更新当前的上方节点编号为g_dictTree[curNodeIdx][x];
		curNodeIdx = g_dictTree[curNodeIdx][x];
	}
	// 当前单词的所有字符插入到节点树完毕之后,最后一个节点增加尾节点标记
	g_finishFlag[curNodeIdx]++;

	return;
}

// 查找操作:从字典树中查询单词
int dictTreeFind(std::string wordStr)
{
	int curNodeIdx = 0;	// 当前操作的上方节点的编号
	for (auto i = 0; i < wordStr.size(); i++) {
		int x = wordStr[i] - 'a'; // 确定下方连接字母的索引编号
		if (0 == g_dictTree[curNodeIdx][i]) {
			// 表示没有当前查找的字符
			return 0;
		}
		// 下方连接字母转换为当前操作的上方节点,继续向下查询
		curNodeIdx = g_dictTree[curNodeIdx][x];
	}

	return 0;
}



int main()
{
	// 所有数据初始化
	g_id = 0;
	for (auto i = 0; i < g_MaxSize; i++) {
		for (auto j = 0; j < 26; j++) {
			g_dictTree[i][j] = 0;
		}
		g_finishFlag[i] = 0;
	}
	// 创建字典树,插入数据

	// 从字典树中查找数据

	return 0;
}

        对于上述代码中使用到的变量,这里进行以下简单的分析:

  • g_id:对于字典树中的每一个节点,都有一个对应的编号,这个编号只与该节点插入到字典树中的先后顺序有关系。
  • g_dictTree[N][26]:二维数组,N表示字典树中的节点个数,26表示英文字母的最大数量。g_dictTree[x]表示第x个插入到字典树中的节点,g_dictTree[x][y]表示与第x个节点相连的下方节点。如果g_dictTree[x]、g_dictTree[x][y]是一个单词的两个字母,则g_dictTree[x][y]一定是g_dictTree[x]相邻的后一位。g_dictTree[x][y]的值表示的是下方节点在字典树中的插入顺序。例如:"0 == g_dictTree[x][y]"表示字典树中不存在这个下方节点与当前节点相邻;“g_dict[x][2]==9”表示第x个节点的下方有一个相连的节点'c',并且这个节点的编号是9。那么为什么是‘c’呢?因为‘c’-‘a’=2
  • g_finishFlag[N]:用于表示某一个节点是否结束点的标识。“0 == g_finishFlag[x]”表示编号为x的点不是一个单词的结束点,在上面的图中代表这个点不是空点,但是没有被标红;“0 != g_finishFlag[x]”表示编号为x的点是一个单词的结束点,即红点。g_finishFlag[x]不一定为0或者1,因为有可能多次输入同一个单词。
  • 函数中的curNodeIdx:curNodeIdx表示插入与查询操作时不断变化的当前节点编号,初始为0表示初始节点。在函数的循环过程中,我们首先确定将要查找的下一个字母,再通过变量‘x’来确定当前节点下是否有查询的目标字符与其相连接。通过每次确定‘x’,然后通过g_dictTree[curNodeIdx][x]来查找连接目标字母的的节点编号。查询过程中,如果节点存在就把curNodeIdx更新为目标节点的编号g_dictTree[curNodeIdx][x]。如果“0 == g_dictTree[curNode][x]”,代表字典树中没有这个节点,表示查询失败。插入过程中,我们就用++g_id来把这个点存入到字典树中。在插入、查找函数的最后通过g_finishFlag[curNodeIdx]来表示节点是否为结束节点、或者返回节点值。

        例如,如果我们想要插入单词“abc”、“abb”、“bca”、“bc”,那么插入流程结果如下:

字典树、前缀树_第3张图片

重点:g_dictTree[上方节点编号][下方连接字母] = 下方连接字母在字典树中的节点编号:

// 插入abc
g_dictTree[0][0] = 1;        
g_dictTree[1][1] = 2;        
g_dictTree[2][2] = 3;        
g_finishedFlag[3] = 1;
// 插入abb
g_dictTree[2][1] = 4;        
g_finishedFlag[4] = 1;    
// 插入bca
g_dictTree[0][1] = 5;        
g_dictTree[5][2] = 6;        
g_dictTree[6][0] = 7; 
g_finishedFlag[7] =  1;
// 插入bc
g_finishedFlag[6] =  1;

        此时,如果我们再向字典树中插入“bcd”,则字典树更新为:

字典树、前缀树_第4张图片

// 插入bcd
g_dictTree[6][3] = 8;
g_finishedFlag[8] = 1;

        此时,在字典树中进行查询如下:

字典树、前缀树_第5张图片

你可能感兴趣的:(数据结构与算法,c++,算法)