字典树、线段树

本文详细介绍了字典树(Trie)的概念、核心思想以及在搜索引擎、字符串检索、前缀统计和最长公共前缀问题中的应用。同时,对比了线段树的特性,重点讲解了如何通过线段树优化查找和修改操作的时间复杂度。
摘要由CSDN通过智能技术生成
字典树Trie

Trie 摘自单词Retrieval,称为字典树或者前缀树,是一种排序树的数据结构。字典树,顾名思义,也就是像字典一样的树。它是字典的一种存储方式。字典中的每个单词在字典树中表现为一条从根节点出发的路径,路径相连的边上的字母连起来就形成对应的字符串。**字典树设计的核心思想 **:利用空间换时间,利用字符串的公共前缀来降低查询时间的开销,最大限度的减少无谓的字符串比较,以达到提高效率的目的。

如果字符串比较长、相同前缀较多那么使用字典树可以大大减少内存的使用和效率。比如当你想查询是否有apple这个词,当你输入到app时,字典树此刻的查询位置就可以达到p这个位置,那么再输入l时光查询l就可以了。字典树并不用等你完全输入apple完毕后才查询。

TIre的属性:
1.根节点不包含字符,除了根节点每个节点都只包含一个字符。root节点不含字符这样做的目的是为了能够包括所有字符串。

2.从根节点到某一个节点,路过字符串起来就是该节点对应的字符串。

3.每个节点的子节点字符不同,也就是找到对应单词、字符是唯一的

字典树的结构比较简单,其本质上就是一个用于字符串快速检索的多叉树,树上每个节点都包含多字符指针。将从根节点到某一节点路径上经过的字符连接起来,就是该节点对应的字符串。

​Tire的基本操作:

  1. 插入节点

  1. 搜索节点

  1. 删除节点(最不常用)

字典树的应用:
典型的应用场景:搜索引擎。

除此之外,我们可以把字典树的应用分为以下几种:

字符串检索:事先将已知的⼀些字符串(字典)的有关信息存储到字典树⾥, 查找⼀些字符串是否出现过、出现的频率。

前缀统计:统计⼀个串所有前缀单词的个数,只需统计从根节点到叶子节点路径上单词出现的个数,也可以判断⼀个单词是否为另⼀个单词的前缀。

最长公共前缀问题:利用字典树求解多个字符串的最长公共前缀问题。将⼤量字符串都存储到⼀棵字典树上时, 可以快速得到某些字符串的公共前缀。对所有字符串都建⽴字典树,两个串的最长公共前缀的长度就是它们所在节点最近公共祖先的长度,于是转变为最近公共祖先问题。

字符串排序:利⽤字典树进⾏串排序。例如,给定多个互不相同的仅由⼀个单词构成的英⽂名,将它们按字典序从⼩到⼤输出。采⽤数组⽅式创建字典树,字典树中每个节点的所有⼦节点都是按照其字母⼤⼩排序的。然后对字典树进⾏先序遍历,输出的相应字符串就是按字典序排序的结果。

线段树

对于query来说改一个值的时间复杂度是O(1),查找一段数据的值的和时间度是O(n),而sum_query则刚好相反。改一个值的时间复杂度是O(n),查找一段数据的值的和时间度是O(1),因为sum_query是一个记录之前数的和的数组。那么怎么才可以更加高效的方法呢,那就是线段树,它的查找和计算都是O(log n)。

线段树的根节点存储的值是所有值的和,左节点是当前父节点的一半值的和,右节点也是一半的值的和,不过左节点是前半段,右节点是后半段。如果线段为奇数,那么左节点比右节点包含的值多1个。

对于线段树的搜索也是分开搜索。修改是从子节点到父节点的修改。

线段树可以用数组表示,类似于用平衡二叉树表示数组那样。左子节点=2*父节点+1。右子节点=2*父节点+2.(该式子是下标的计算)。

在线段树里面储存数据是父节点是子节点的和,没有子节点的节点存储的值是数组存储的值,如下图所示:

下面是线段树是代码实现:

这段代码是一个实现线段树的示例。

首先,get_tree_size函数用于计算线段树的大小。线段树的高度等于向上取整(log2(n)),其中n是线段树中存储的元素的数量。线段树的最大节点数等于2^(height+1)-1。

build_tree函数用于构建线段树。它接受两个参数:lines和tree。lines是一个存储线段树元素的向量,tree是一个存储线段树节点值的向量。node表示当前节点的索引,start和end表示当前节点表示的范围。如果范围为一个点(即start=end),则将tree[node]赋值为lines[start]。否则,计算mid为(start+end)/2,然后递归调用build_tree函数构建左子树和右子树,并将它们的和赋值给tree[node]。

query_tree函数用于查询线段树中指定范围内元素的和。它接受六个参数:tree、node、start、end、L和R。如果查询范围和当前节点范围不重叠,返回0。如果当前节点范围完全在查询范围内,返回tree[node]。否则,计算mid为(start+end)/2,然后递归调用query_tree函数查询左子树和右子树,并返回它们的和。

update_tree函数用于更新线段树中指定索引位置的元素值。它接受六个参数:tree、node、start、end、index和value。如果范围为一个点(即start=end),将tree[node]赋值为value。否则,计算mid为(start+end)/2,如果index在start和mid之间,则递归调用update_tree函数更新左子树,否则更新右子树。最后,更新tree[node]为左子树和右子树的和。

在main函数中,首先定义了一个lines向量,并计算出线段树的大小并创建一个tree向量。然后调用build_tree函数构建线段树,并输出线段树的值。接着调用update_tree函数更新第五个元素的值为6,并输出更新后的线段树。最后,调用query_tree函数查询索引2到5范围内元素的和,并输出结果。

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

int get_tree_size(int n) {
    int height = (int)ceil(log2(n)); // 计算线段树的高度
    int max_size = 2 * (int)pow(2, height) - 1; // 计算线段树的最大节点数
    return max_size;
}

void build_tree(const vector<int>& lines, vector<int>& tree, int node, int start, int end) {
    if(start == end) {
        tree[node] = lines[start];
    } else {
        int mid = (start + end) / 2;
        int left_node = 2 * node + 1;
        int right_node = 2 * node + 2;
        build_tree(lines, tree, left_node, start, mid);
        build_tree(lines, tree, right_node, mid + 1, end);
        tree[node] = tree[left_node] + tree[right_node];
    }
}

int query_tree(const vector<int>& tree, int node, int start, int end, int L, int R) {
    if(R < start || L > end) { // 查询范围和当前节点范围不重叠
        return 0;
    }
    if(L <= start && end <= R) { // 当前节点范围完全在查询范围内
        return tree[node];
    }
    int mid = (start + end) / 2;
    int left_node = 2 * node + 1;
    int right_node = 2 * node + 2;
    int sum_left = query_tree(tree, left_node, start, mid, L, R);
    int sum_right = query_tree(tree, right_node, mid + 1, end, L, R);
    return sum_left + sum_right;
}
void update_tree(vector<int>& tree, int node, int start, int end, int index, int value) {
    if(start == end) {
        tree[node] = value;
    } else {
        int mid = (start + end) / 2;
        int left_node = 2 * node + 1;
        int right_node = 2 * node + 2;
        if(index >= start && index <= mid) {
            update_tree(tree, left_node, start, mid, index, value);
        } else {
            update_tree(tree, right_node, mid + 1, end, index, value);
        }
        tree[node] = tree[left_node] + tree[right_node];
    }
}
int main() {
    vector<int> lines = {1, 3, 5, 7, 9, 11};
    int size = lines.size();
    int tree_size = get_tree_size(size); // 动态计算线段树的大小
    vector<int> tree(tree_size, 0);
    build_tree(lines, tree, 0, 0, size - 1);
    for(int i = 0; i < tree_size; i++) { // 使用计算出的线段树大小,并输出线段树
        cout << "tree[" << i << "] = " << tree[i] << endl;
    }
    puts("更新后--------------------------------");
    update_tree(tree, 0, 0, size - 1, 4, 6);//修改第5个元素的值为6
    for(int i = 0; i < tree_size; i++) { 
        cout << "tree[" << i << "] = " << tree[i] << endl;
    }
    int s = query_tree(tree, 0, 0, size - 1, 2, 5); // 查询索引2到5范围内元素的和
    cout << "s = " << s << endl;
    return 0;
}

结果: 

tree[0] = 36
tree[1] = 9
tree[2] = 27
tree[3] = 4
tree[4] = 5
tree[5] = 16
tree[6] = 11
tree[7] = 1
tree[8] = 3
tree[9] = 0
tree[10] = 0
tree[11] = 7
tree[12] = 9
tree[13] = 0
tree[14] = 0
更新后-------------------------------
tree[0] = 33
tree[1] = 9
tree[2] = 24
tree[3] = 4
tree[4] = 5
tree[5] = 13
tree[6] = 11
tree[7] = 1
tree[8] = 3
tree[9] = 0
tree[10] = 0
tree[11] = 7
tree[12] = 6
tree[13] = 0
tree[14] = 0
s = 29

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

years_GG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值