字典树(前缀树)

目录:

1:字典树介绍:

2:字典树应用场景及相关例题:


字典树介绍:

     字典树又称前缀树(trie),主要用于字符串的存储与查询,与哈希表相比有近乎相同的时间效率和空间效率,而且两者差异很大,其能完成一些用哈希很难解决又有较高时间要求的问题。

       字典树是由树节点构成,我们来看每个树的节点该如何定义:

typedef struct TreeNode
{
	int val;
	struct TreeNode* next[26];
}node;

       我们利用val来记录当前字母有几个公共前缀,用26个树节点指针来记录,这里我们根据英文的26个字母(假设记录的都是英语),根节点不记录任何数据。

//新建节点
node* NewNode()
{
	node* root = (node*)malloc(sizeof(node));
	int i = 0;
	for (i = 0; i < 26; i++)
	{
		root->next[i] = NULL;
	}
	root->val = 0;
	return root;
}

      此时我们往里面插入数据,比如插入一个“brad”字符串。

//插入节点
void Insert(node* root, char* s)
{
	int i = 0;
	for (i = 0; s[i] != '\0'; i++)
	{
		if (root->next[s[i] - 'a'] == NULL)
		{
			root->next[s[i] - 'a'] = NewNode();
		}
		//根节点的值为0
		root = root->next[s[i] - 'a']; //先迭代后赋值
		root->val++;
	}
}

      我们在遍历字符串的同时,去访问它是否有该位置的的指针,如果没有,就去创建;有则把节点val值增加1,标记其字母增加了1次。       此时我们再向字典树中插入单词“bread”,它会在原来的基础上,存在则迭代根节点继续向下寻找,并把标记增加1;否则就开辟新的节点。       此时我们可以看到,字典树中有多少共同前缀,都一一记录清楚,比如我们再向其中插入“bed”,并直接给出结果:       此时我们可以查阅一个字符串有多少个共同前缀:

//搜索,返回标记即可
int Search(node* root, char* s)
{
	int i = 0;
	for (i = 0; s[i] != '\0'; i++)
	{
		if (root->next[s[i] - 'a'] == NULL)
		{
			return 0;
		}
		else
		{
			root = root->next[s[i] - 'a'];
		}
	}
	return root->val;
}

       比如我们当前搜索字符串“b”,应当返回“b”节点的val = 3;查询“bed”应当返回val = 1。       最后我们要释放节点,则利用递归逐个释放:

//释放
void Realse(node* root)
{
	//递归销毁
	int i = 0;
	for (i = 0; i < 26; i++)
	{
		if (root->next[i] != NULL)
		{
			Realse(root->next[i]);
		}
	}

	free(root);
}

       这里我们给出全部字典树相关代码:

#include<stdio.h>
#include<stdlib.h>

typedef struct TreeNode
{
	int val;
	struct TreeNode* next[26];
}node;

//新建节点
node* NewNode()
{
	node* root = (node*)malloc(sizeof(node));
	int i = 0;
	for (i = 0; i < 26; i++)
	{
		root->next[i] = NULL;
	}
	root->val = 0;
	return root;
}

//插入节点
void Insert(node* root, char* s)
{
	int i = 0;
	for (i = 0; s[i] != '\0'; i++)
	{
		if (root->next[s[i] - 'a'] == NULL)
		{
			root->next[s[i] - 'a'] = NewNode();
		}
		//根节点的值为0
		root = root->next[s[i] - 'a']; //先迭代后赋值
		root->val++;
	}
}

//搜索,返回标记即可
int Search(node* root, char* s)
{
	int i = 0;
	for (i = 0; s[i] != '\0'; i++)
	{
		if (root->next[s[i] - 'a'] == NULL)
		{
			return 0;
		}
		else
		{
			root = root->next[s[i] - 'a'];
		}
	}
	return root->val;
}

//释放
void Realse(node* root)
{
	//递归销毁
	int i = 0;
	for (i = 0; i < 26; i++)
	{
		if (root->next[i] != NULL)
		{
			Realse(root->next[i]);
		}
	}

	free(root);
}

int main()
{
	node* root = NewNode();
	Insert(root, "brad");
	Insert(root, "bread");
	Insert(root, "bed");
	int ret = Search(root, "bed");
	printf("ret = %d\n", ret);
	ret = Search(root, "b");
	printf("ret = %d\n", ret);
	//释放节点
	Realse(root);
	return 0;
}

字典树应用场景及相关例题:

       最后我们来看一道关于利用字典树来解决的一题:求数组中两个数的最大异或值。

       其实这一题很简答,我们可以直接使用暴力方法双重遍历去寻找两个数异或的最大值,得出结果即可。

       如果这题是简单题,暴力肯定随便过,但是!这是力扣的中等题,各位不要想着暴力了,会超时的。所以我们只能使用其他方法,此时我们就可以使用字典树,这里确实很难想到我大致说一下方法:假设数组中有 i 个元素,我们把数组中的 i - 1 个元素放入字典树,把第 i 个元素与字典树中的元素异或。因为异或的性质就是相同出零,相异出一,此时得出最大值则是异或最大。

       不多说,上图:

       定义一个返回值x。因为题目给出都是正整数,并且不会超过2^31,所以最多有31个bit位,所以最多左移30位,所以我们每次判断最高位。

  1. 判断当前位如果为1,则去字典树左边寻找(因为左边就是0),如果存在,则将 x = x * 2 +1(因为二进制位每次多一个1,十进制就 * 2 + 1);左边如果不存在,就只能去右边,则将 x = x * 2 (二进制位每多一个0,十进制 * 2)。
  2. 判断当前位如果为0,则去字典树右边寻找(因为右边就是1),如果存在,则将 x = x * 2 + 1(因为二进制位每次多一个1,十进制就 * 2 + 1);右边如果不存在,就只能去左边,则将 x = x * 2 (二进制位每多一个0,十进制 * 2)。

       此时不再做过多解释,上代码!

//其实基本思路就是将i-1(一共i个元素)个元素放在字典树中
//之后用最后一个元素nums[i] 与字典树比较得出最大值

//只有左右节点, 左节点表示0, 右节点表示1
typedef struct Node
{
    struct Node* left;
    struct Node* right;
}Trie;

//初始化
Trie* newNode()
{
    Trie* root = (Trie*)malloc(sizeof(Trie));
    root->left = NULL;
    root->right = NULL;
    return root;
}

//添加数字
void add(Trie* root, int n)
{
    //因为最多 31 位, 所以最多右移 30 位
    for (int count = 30; count >= 0; count--)
    {
        int bit = (n >> count) & 1;
        if (bit == 0)
        {
            //向左
            if (!root->left)
            {
                root->left = newNode();
            }
            root = root->left;
        }
        else
        {
            //向右
            if (!root->right)
            {
                root->right = newNode();
            }
            root = root->right;
        }
    }

}

//与字典树所有元素比较求异或最大
int check(Trie* root, int n)
{
    int x = 0;
    for (int k = 30; k >= 0; k--)
    {
        int bit = (n >> k) & 1;
        if (bit == 0)
        {
            //此时要去找 1 这样才能使结果最大
            //如果右边有, 则去右边
            if (root->right)
            {
                root = root->right;
                //如果一个数每次二进制位上加1
                //那么其实就是 * 2 + 1
                x = x * 2 + 1;
            }
            else
            {//没有右节点,没有不同比特位,只能找0
                root = root->left;
                x = x * 2;
            }
        }
        else
        {
            //此时找到1
            //去左边找0
            if (root->left)
            {
                root = root->left;
                x = x * 2 + 1;
            }
            else
            {
                root = root->right;
                x = x * 2;
            }
        }
    }

    return x;
}

int findMaximumXOR(int* nums, int numsSize){
    //先建树
    Trie* root = newNode();
    int x = 0;
    for (int i = 1; i < numsSize; i++)
    {
        add(root, nums[i - 1]); 
        //将 nums[i - 1] 放入字典树,此时nums[0 .. i - 1] 都在字典树中

        //之后 nums[i] 与 字典树中所有元素对比, 得出最大值
        x = fmax(x, check(root, nums[i]));
    }

    return x;
}

       如果能耐心看完这篇文章,我相信对你肯定有所帮助,水滴石穿。当然,有什么不足请在请在评论区指出,感谢各位!

  • 21
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值