绳索的有效重新散列

Posted

技术标签:

【中文标题】绳索的有效重新散列【英文标题】:Efficient re-hashing of a rope 【发布时间】:2017-06-25 21:07:17 【问题描述】:

给定一个rope,假设我们需要知道它的哈希(通过一些哈希函数传递所有叶子的连接)。

现在,当一个绳叶发生变化时,重新计算整个绳子的哈希值的有效方法是什么? IE。类似于 O(log n) 而不是 O(n)。

一种方法是使用Merkle tree。但是,这会导致诸如...的问题。

空的非叶节点或具有零长度子字符串的叶节点会影响哈希,即使它们对有效的绳索内容没有影响; 将节点从子树的右侧移动到该子树右侧兄弟的左侧会影响最终哈希,但不会影响有效的绳索内容。

有没有更好的算法呢?散列函数不需要是加密安全的,只要足够好就可以避免可能的冲突。

【问题讨论】:

【参考方案1】:

就像绳索的任何节点存储左子树的大小(如果它是叶子,或者它自己),任何节点都可以额外存储对应于左子树的字符串的多项式哈希(或者它自己,如果它是一个叶)。

当为一个节点重新计算权重时,也会为该节点重新计算哈希,具有相同的渐近复杂度。

例如,让节点和其中的值是:

    left     right    string     weight
1:                     abcd         4
2:    1        4                    4
3:                     ef           2
4:    3        5                    2
5:                     ghi          3

多项式哈希是,带有一些固定常数 p 和 q:

h (s[0] s[1] ... s[n-1]) = (s[0] * p^(n-1) + s[1] * p^(n-2) + ... + s[n-1] * p^0) mod q.

所以,我们存储了以下哈希值,全部以 q 为模:

         hash
1:  a*p^3 + b*p^2 + c*p^1 + d*p^0
2:  a*p^3 + b*p^2 + c*p^1 + d*p^0
3:  e*p^1 + f*p^0
4:  e*p^1 + f*p^0
5:  g*p^2 + h*p^1 + i*p^0

关于计算模 q 的说明。 在这里和下面,所有的加法和乘法都以 q 为模进行。 换句话说,我们在 ring of integers 模 q 中操作。 我们使用的事实是

(a ? b) mod q = ((a mod q) ? (b mod q)) mod q

对于 ?运算是加法、减法和乘法。 因此,每次我们执行这些操作之一时,我们都会立即附加一个mod q 以保持数字较小。 例如,如果 p 和 q 小于 230 = 1,073,741,824,则可以在 32 位整数类型中进行加减运算,而对于中间 64 位整数类型则可以进行乘法运算。 每次乘法后,我们立即将结果取模 q,使其再次适合 32 位整数。


现在,我们如何获取根的哈希值 - 例如,使其成为某个节点的左子节点,或者只是获取整个字符串的哈希值?

我们从根到右,我们必须添加权重并合并哈希。事实证明我们可以这样做(记住一切都是模 q):

(a*p^3 + b*p^2 + c*p^1 + d*p^0 * p^2 + e*p^1 + f*p^0) * p^3 + g*p^2 + h*p^1 + i*p^0

大括号中的值是存储在 out 节点中的值。 我们向右递归。 起床时,我们记住到目前为止收集的权重,将左侧哈希乘以该权重的幂(这就是 p^3 和 p^(3+2=5) 的来源),然后将累积的右侧哈希。

结果值等于整个字符串的哈希值:

a*p^8 + b*p^7 + c*p^6 + d*p^5 + e*p^4 + f*p^3 + g*p^2 + h*p^1 + i*p^0


这里有几点说明。

    我们必须预先计算 p 模 q 的幂,以便能够快速乘以它们。

    如果我们将整个子树的哈希值(不仅仅是左子树的哈希值)存储在一个节点中,整个结构可能会变得更加清晰。然而,这样一来,我们可能会失去绳索结构具有的 O(1) 级联可能性,使其降至通常的 O(log n),因此我们可能只使用了常规的 treap 而不是绳索。即使没有,在一个节点中缓存整个子树的哈希值也是绝对有可能的。

    如果我们颠倒哈希多项式中的幂顺序,则使其 h (s[0] s[1] ... s[n-1]) = (s[0] * p^0 + s[1] * p^1 + ... + s[n-1] * p^(n-1)) mod q, 数学是相似的,但是从节点的所有正确后代收集哈希可以迭代而不是递归完成。

【讨论】:

感谢您快速详细的回答。但是,考虑到存储在每个节点中的哈希是模 q,我不明白这将如何工作。 IE。 curly blackets 中的每个值都是模 q,所以 ap^3 + bp^2 + cp^1 + dp^0 * p^2 + ep^1 + fp^0 不一定等于 (ap ^3 + bp^2 + cp^1 + dp^0 mod q) * p^2 + (ep^1 + fp^0 mod q),除非 q 非常大。 错误,澄清一下,ap^3 + bp^2 + cp^1 + dp^0 * p^2 + ep^1 + fp^0 == ap^1 + bp^0 * p^4 + cp^3 + dp^2 + ep^1 + fp^0 但如果您将模 q 应用于大括号中的值,则不会。 @VladimirPanteleev 每个加法和乘法都应该是模 q,也就是说,我们在 Z/qZ 中运算,而不是所有整数。抱歉,我会进行编辑以使其更清晰。 太棒了,谢谢! Here is my implementation 只是散列(使用整数溢出作为 q)。我不精通数论,选择一个好的p有什么提示吗?我有限的研究使我认为最好的 p 是原始根模 q,但这是我所能得到的。 @VladimirPanteleev 首先,q 是 2 的幂对它有一定的性能吸引力,但它在 Thue-Morse 字符串中表现不佳:ru、en。如果您的应用程序自然会遇到这样的字符串(文本编辑器?库?),最好为q 选择一个大素数。其次,p 应该大于字母表的大小(一个字节?)并且与q 相对质数,但除此之外,大多数p 都可以。

以上是关于绳索的有效重新散列的主要内容,如果未能解决你的问题,请参考以下文章

设计一个重新散列函数......如何避免相同的散列?

散列更改时分离和重新附加元素

Java HashMap 内部数据结构在重新散列期间如何变化?

如何防止 Mongoose 在修改用户后重新散列用户密码?

在返回常量哈希码的情况下,Java8 Hashmap 重新散列

如何在删除元素时防止重新散列 std::unordered_map?