📜  是双数组 (1)

📅  最后修改于: 2023-12-03 14:55:11.185000             🧑  作者: Mango

双数组

双数组是一种数据结构,常用于压缩存储字符串或者其他类型的数据。双数组可以将一个字符串压缩成一个整数数组,同时保证在不影响查询性能的前提下,压缩后的结果尽可能小。

定义

对于一个长度为n的字符串S,我们可以将其转化为字符编码序列C,即C = [c1, c2, ..., cn],其中ci是S中第i个字符的asc码。

然后我们可以定义一个双数组T = (base, check)。其中base和check都是整数数组,长度都是2n。其中base[i]和check[i]对应表示双数组中的节点i,因此我们可以将T看做是一个由n个节点构成的树结构。双数组的构建过程需要两个步骤:构建Trie树和构建双数组。

构建Trie树

Trie树是一种树形数据结构,它的特点在于所有存在公共前缀的字符串共享相同的前缀节点。如下图所示:

对于一个以字符串"cat"和"cow"为例的Trie树,其中绿色的节点表示某个字符串的结束节点。所有的字符串都是从根节点到结束节点的一条路径。如上图,序列[3, 1, 20]和序列[3, 15, 23]都可以在Trie树中找到对应的路径。

在构建Trie树的过程中,我们首先需要知道每个字符串的长度。其次,我们定义一个root节点,用以表示所有字符串的共同的前缀。然后,我们从左到右遍历每个字符串,创建一个新的节点并将其加入到Trie树中。

具体的构建方法是:将字符串依次加入到Trie树中,并将该字符串的每个字符从根节点开始遍历。如果在Trie树中存在这个字符对应的节点,就遍历下一个字符。否则,就创建新的节点,并将其添加到树中,然后遍历下一个字符。最后,在字符串对应的最后一个字符上,将该节点标记为绿色,表示一个字符串的结束。

构建双数组

在构建双数组之前,需要先对Trie树进行一些预处理。对于Trie树中所有的节点,我们可以为它们编号,从1到n。同样地,对于Trie树中每个节点ptr,我们也可以记录下它在arrays中的位置idx,并且有base[idx] = base[ptr]和check[idx] = check[ptr]。

我们可以利用广度优先遍历BFS的思想,对于Trie树中的每个节点ptr(Id为i),分配一个编号(Id为j),从而建立起一个二叉树结构。另外,我们可以对每个节点ptr记录下它对应的字符串在Trie树上的相对位置rel_pos。如下图所示:

在这张图中,红色数字代表每个节点的编号,蓝色数字代表该节点在Trie树上的相对位置。例如,节点4对应字符串“ma”,在Trie树上的相对位置是2。

在上图的例子中,每个节点只有两个孩子,即左孩子和右孩子。实际上,在双数组中,每个节点的孩子可能有多个。为了描述方便,我们称左边第一个孩子为“边0”,左边第二个孩子为“边1”,右边第一个孩子为“边2”……以此类推。

接下来,我们需要从树的叶子节点开始向根节点依次计算出base和check数组的值。对于基础边base和对应的check值,定义的公式是base = rel_pos + 1,check = father_id。例如,对于节点7(字符串为“me”),它的父亲节点是3,字符串位置是3,因此有base[7] = 3 + 1 = 4,check[7] = 3。对于其他孩子节点,我们还需要对它们的对应边(edge)进行计算,定义公式如下:

  • edge[i] = k,表示从当前节点出发,edge[i]对应的字符的asc码是k。如果edge[i]为-1,则表示这是一个无效的(不连通的)边。
  • v = base[curr] + k。其中,curr是当前节点的编号,而v是当前节点i的子节点edge[i]在arrays中的编号。
  • [ans_start: ans_end]是当前节点的子节点中编号最大和最小的两个数之间的区间。
  • check[v] = curr,表示我们在将当前节点i的一个孩子节点v添加到arrays中时,需要将check[v]的值也从当前节点i的Id更新为其父亲节点q的Id。
  • base[v] = ans_start - k,表示我们将当前节点i的一个孩子节点v添加到arrays中时,需要重新计算它的base值。

以上两个子节点基础边计算的过程还需要判断是否会出现base值冲突,容易出现的base冲突的情况是:base[v1] + c = base[v2]。如果出现此情况,需要重新计算v2的base值,直到不存在冲突。

查询字符串

在双数组中查询字符串的基本思路是,将字符串转换为chr[]数组,然后从根节点开始,沿着匹配的边向下遍历树。直到遇到一个check值不等于当前节点的Id,或者走到了字符串的最后一个字符,停止遍历。如果当前节点为一个已经成功匹配的字符串的结束节点,则输出该字符串的下标。

特点

双数组具有两个重要的性质:

  1. 双数组中每个节点都能表示一个字符序列或者多个字符序列的公共前缀。

  2. 双数组中每一个节点的编号与数组中的位置是一一对应的。

这两个性质使得双数组在字符串匹配和压缩存储方面有很好的应用。在双数组中查找特定字符串的时间复杂度是O(1),在双数组中查询前缀匹配的字符串时间复杂度是O(m),其中m为匹配的字符串长度。

参考资料
  1. 双数组

  2. Trie 树和双数组的基本概念及实现

  3. Double-Array Trie数据结构的原理及实现