字典树 —— 字符串分析算法_算法字符串算法分析,BAT这种大厂履历意味着什么

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

这里我们继续来编程训练,在《前端进阶》这个系列里面我们已经讲过一些字符串的算法了。然后这篇文章我们就来一起学习,剩下的几个字符串中比较细节的算法。

字符串分析算法

在开始之前我们先来看看字符串算法的一个整体目录。这里我们从简单到难的算法来排列,大概就分成这样一个顺序:

  • 字典树
  • 大量高重复字符串的储存与分析(完全匹配)
  • 比如说我们要处理 1 亿个字符串,这里面有多少出现频率前 50 的这样的字符串,1 亿这个量我们还是可以用字典树去处理的
  • 再比如说大家做搜索关键词,或者相同的字符串搜索类型的情况,很多时候我们就会需要用到类似字典树这样的一个结构
  • KMP
  • 在长字符串里找模式(部分匹配)
  • 它跟字典树最大的区别就是字典树是检查两个字符串是否完全匹配,而 KMP 是两个字符串中,一个字符串是两一个字符串的一部分,但是这个就会出现一个更为复杂的问题。
  • 如果我们有一个长度为 m 的字符串和一个长度为 n 的字符串,然后让他们两个互相匹配,这个时候我们有两种匹配方法
  • 第一种就是暴力破解法,它可能是m 乘以 n 的时间复杂度,显然这个算法的性能在大量的搜索字符的时候是不行的
  • 所以后面几位计算机专家研究出了 KMP 算法,而 KMP 就是三个人的名字的首字母,K 是高德纳,一个著名的写计算机程序设计的老爷子。加上另外两个计算机专家共同发明了 KMP 算法。这个算法就是在一个长字符串里面匹配一个短字符串,这个匹配算法的复杂度可以降到 m + n。所以这个算法还是非常的厉害的。
  • Wildcard
  • 在 KMP 的基础上加了通配符的字符串模式
  • 通配符包括问号 表示匹配任意字符,星号表示匹配任意数量的任意字符
  • 在我们做一些文件查找的时候可能就会运用到 Wildcard 的这种通配符
  • 我们也可以理解它为一个弱一点的正则表达式,因为相比正则它只有两种通配符,并且这些通配符与正则有一个显著的区别,就是 Wildcard 其实也是可以在 O(n) 或者 O(m+n)的时间复杂度内去处理的。这个现象是因为 Wildcard 当中有一个贪心算法,也是它非常神奇的原因。
  • 正则
  • 正则一般来说都是需要用到回溯的一个系统
  • 它可以说是字符串通用模式匹配的终极版本
  • 状态机
  • 通用的字符串分析
  • 与正则表达式相比,状态机会更强大
  • 正则表达式与有限状态机在理论上是完全等价的两种东西
  • 但是有限状态机不同的是,我们还可以往里面嵌代码,还可以给字符串做而外的处理
  • 另外就是正则写起来很方便,有限状态机写起来成本比较高
  • LL LR
  • 在简单的匹配和分析的基础上,如果我们要对字符串建立多层级的结构,我们就会使用 LL 和 LR 这样的语法分析的算法
  • LL 在上一篇文章我们已经学习过了,但是 LR 是还没有的,实际上 LR 是一个比 LL 更强大的一个语法分析
  • 但是通常我们简单写,就都用 LL 去写,因为 LR 它 的理论性比较强
  • 如果同学们还记得的话,我们在讲解 HTML 的语法分析的时候,我们用了一个 stack 去处理,这个其实就是 LR 算法的一个简化版。它其实是 LR(0) 的语法,但是一般来说我们去处理都会用 LR(1),而 LR(1) 是相等于 LL(n) 的这样一种非常强大的分析算法。

字典树

首先我们先了解字典树到底是一个什么东西。我们平时遇到不懂得字都会去查字典对不对?那么我去查字典的时候,我们往往会根据单词的第一个字母(一般是拼音首字母)作为索引去找到这个字大概在那一页,这里用到的就是字典序。

然后如果我们把这种索引寻找方法不断地重复。当我们找好了第一个字母之后,我们再去看它的第二个字母是属于字典中的哪一个部分,最后把这些一路找过来的 线索 变成一个树形的结构。换一句话说也可以理解为 “查字典的行为变成一个树形的结构”。—— 而这个树形结构就是我们的字典树了,字典树有一个英文的名字叫 “Trie”。

例子分析

接下来我们举个例子:

比如说现在我们有这 4 个字符串

[
‘3499’
‘0015’
‘0002’
‘0007’
]

这里它们都是等长的,不过不等长也没有关系的,等一下我们再来了解为什么。那么如果我们用字典树来保存这个 4 个字符串,因给怎么保存呢?


第一层

我们首先来看所有字符串的第一个字母,它们的第一个字母只有 03这两种字符,所以我们字典树的第一层就会分成 30 两个分支。


第二层

接下来我们看看第二层,这里有 40 两种字符的分支。我们可以看到第一个字符串,4 的前面是 3,并且在这个位置没有出现 4前面是另外一种字符的情况。

所以这里我们就可以把 4 放到上一个 3 的分支之下,然后 0 也是一样,前一个字符都是 0,所以放在我们的 0 的分支之下。(这里听的有点蒙不要紧,到了最后看着动画里面的效果来理解,就会更加明确了。)


第三层

0 后面的分支,发生了一个变化,第二行的 0 后面出现了一个 1,然后第一行的 4 后面又有一个 9

所以说我们最后出来的字典树,在 4 的后面产生了一个 9 的分支,并且在 0 的分支上会产生了 10两个分支。


第四层

同理,在第四层这里 0 的后面出现了 27 这两种情况,而 1 后面出现了 5 这一种情况。最后的 9 后面再次出现了 9,所以我们只需要再追加一个 9 的分支即可。


最后我们来看看整个字典树的生成过程!

代码实现

接下来我们看看在代码中,可以如何实现这棵字典树,以及看看字典树有什么样的应用场景。

首先我们来讨论一下字典树的存储机制,这里我们会用一个 空对象来保存字典树里面的值。因为我们字典树在实际场景里面就是一段字符串所以说我们会用一个对象来作为字典树的节点

当然如果大家愿意的话,用 Map 也是可以的, ObjectMap 就是在 JavaScript 中最适合用来保存字典树里面的分支这种数据结构的。

因为字典树里面只会存字符串,所以说用对象还是 Map 没有本质的区别。

Constructor 构造方法

首先我们来加入一个 Trie 类,然后实现一个构建函数 constructor(),这里为了干净我们就选择使用 Object.create(null) 来创建这个字符串。这样也可以避免受到 Object.prototype 原型上的一些污染。(不过因为我们每次存的是一个字符,也不存在污染的问题,但是这个写法是一个好的习惯,能干净还是尽量干净。)

class Trie {
/** 构建函数 **/
constructor() {
this.root = Object.create(null);
}
}

Insert 添加树节点方法

接下来我们需要编写一个 insert() 方法,这个方法的作用就是把一个字符串插入字典树里面。

这个插入逻辑其实很简单,我们去设一个变量 node(也就是一个节点) ,一开始让这个节点等于我们的 root (这里的 root 就是我们树结构的根节点) 。然后我们就从 root 根节点,逐级地把字符串放进这个树的子树节点里面去。

这里如果我们的主树不存在的话,我们就先创建主树,然后我们再让 node 到下一个层级去(相当于我们在查字典的时候,翻到对应的字母的位置)。

最后我们要注意的是,字符串是会有大量的重复的。比如我们的 ababc 其实它是两个不同的字符串,所以说 ab 后边我们要有一个截止符。这个截止符我们就用 $ 来表示。

其实这里我们用 $ 符是不合适的,因为如果我们的字符串本身就支持 $ 这个内容的话,这样就会出问题了。所以说其实一个更好的方案就是我们使用 Symbol()创建一个 Symbol。这里我们可以使用 Symbol 来处理,这样就不会和我们字符里面的 $ 符号冲突了。

使用了 Symbol 的这种不可重复的特点,那么我们就可以让 node 节点最后的截止符更加严谨一些。

这里就讲完 insert() 方法的逻辑思路了,接下来我们看看代码:

/** 创建 $ 唯一的截止符 symbol **/
let $ = Symbol(‘$’);

class Trie {
/** 构建函数 **/
constructor() {
this.root = Object.create(null);
}

/**
* 添加树节点
* @param {String} word 字符
*/
insert(word) {
let node = this.root;

for (let c of word) {
if (!node[c]) node[c] = Object.create(null);
node = node[c];
}

if (!($ in node)) node[$] = 0;

node[$]++;
}
}

randomWord 随机单词

这里我们做一个 randomWord() 函数,这个函数会产生一个随机的单词。然后结合我们的字典树,我们就可以轻易的分析一些字符的数据,比如说 “出现最多的单词” 之类的逻辑。

不多说,先点个赞!

这里我们来看看这个函数的实现代码:

function randomWord(length) {
var s = ‘’;
for (let i = 0; i < length; i++) {
s += String.fromCharCode(Math.random() * 26 + ‘a’.charCodeAt(0));
}
return s;
}

这里面的 String.fromCharCode(Math.random() * 26 + 'a'.charCodeAt(0)) 这一行代码做了什么呢?其实就是在 26 个字母的字符集里面随机拿一个字母出来,因为最大是 26 个,所以我们从字符 a 开始随机往后加入 随机数 * 26,这样我们就可以得到一个随机的数,并且这个数是在 0 - 26 之间。

好,有了这个随机生成单词的方法,我们就可以来生成大量的单词,然后使用我们的 字典树 来实现一个统计分析功能了。

这里我们来构建 10 万个随机单词:

let trie = new Trie();

for (let i = 0; i < 100000; i++) {
trie.insert(randomWord(4));
}

最后我们放入浏览器执行后,我们可以看到 字典树就生成好了:

这里代表什么呢?如果我们还记得在 “例子分析” 部分讲到的,这里意思就是说我们是有 aaadaaagaaamaaaxaaaz 这样的一些字符。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
得在 “例子分析” 部分讲到的,这里意思就是说我们是有 aaadaaagaaamaaaxaaaz 这样的一些字符。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-wnKjTZDk-1713211261635)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 20
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值