如何对数字串进行数字排序


最近,我给了a talk about ArangoDB在一群数学家面前。我在广告中说,几乎任意的数据都可以很容易地存储在基于JSON的文档存储中。当我说出“容易”这个词的时候,其中一个人问及长整数。如果一个数学家说“长整数”,他们不是指64位,而是“适当的长。”他实际上想存储有限组的订单。我说过应该使用JSON UTF-8字符串,但是我应该已经看到了下一个问题——然后他想要一个排序的索引,这个索引可以根据字符串中存储的数值对文档进行排序。但是大多数数据库——阿朗戈数据库也不例外——将按照字典顺序比较UTF-8字符串。

因此,今天的问题

如何将长整型值存储为UTF-8字符串,从而使通常的UTF-8字节字典序比较实际上根据数值对值进行排序?

所以这篇文章的真正标题应该是,“整数集合到字典排序的UTF-8字符串集合中的保序嵌入。”但是谁会点击它呢?毕竟,这是一个博客,不是数学杂志。

这个问题有一个令人惊讶的简单解决方案,至少对于正数集是这样的:存储数字N,简单地存储N给这封信打电话x。越大N字符串变得越长。词典排序表明,在这种情况下,字母越多的长字符串越大。

这个答案在数学上是完全正确的,因为它是完全无用的,因为谁愿意为1000000这个数字浪费近一兆字节的数据呢?我们必须做得更好!

如果我们把自己限制在最多k数字,我们可以在左边填入0,然后存储1000000,比如:

00000000000000000000000000000000000000000001000000

但这也是一种浪费,一旦我们决定了最大位数,肯定会有人想存储更大的数字。因此,我们真正想要的是一个具有以下特性的方案:

  • 使用的空间不要超过通常符号所需要的空间
  • 为任意长的数字工作
  • 可以很容易地转换成常用的十进制表示法
  • 易于解释和理解

这篇文章暗示了这样一种编码。

让我们先做非负数

对于最多92位的数字,我们只需将数字的位数放在第一位(作为一个字符,使用Unicode代码点34到125,将33加到位数上
获取代码点),然后是一个空格,然后是通常表示法中的实际数字。

这里有几个例子:

" 0              is the number 0
" 1              is the number 1
# 42             is the number 42
. 1623463626463" is the number 1623463626463

为什么这是可行的并且保留了数字顺序?显然,“较短”的正数比较长的正数小(在通常的符号中没有前导零)。但是在这种情况下,字符串的字典顺序会比较第一个字符中正确的位数(字母的Unicode码点数比数字高)。对于具有相同位数的两个数字,初始字符和空格是相同的,然后对实际数字进行字典式比较。请注意,格式之间的转换很容易,因为实际的符号被一字不差地包含在空格字符之后,这使得它同时具有可读性。

这将花费我们多达1092-1,这显然是一个很大的数字,但对于数学家来说还不够。

对于较大的数字(Y> = 1092),我们使用以下技巧。

到目前为止,我们已经用第一个字符对数字的位数进行了编码。现在我们简单地放一个波浪号字符~为了表示更大的数字(颚化符的Unicode代码点是126,因此比我们上面使用的任何一个都大),然后在上面的符号中放入数字(没有空格),后跟一个空格,然后是实际的数字。

那就是:

"~cX Y"

...在哪里c是的位数的字符X和92 <X< 1092在上面的符号和Y准确地说X不以0开头的数字。这是因为上面有一个非常相似的论点:所有这些数字都大于1092-1,并且所有字符串在词典上比没有颚化符的字符串大。如果两个这样的数字Y你好有一个不同的位数,那么位数较少的那个较小。但是相应的字符串"~cX"在词典上小于"~cX'"因为我们之前的争论。如果Y你好有相同的位数,那么他们的"~cX"部分是相同的,字典序根据数值对字符串进行正确排序。

这包括范围1092< =Y< 10-1,这对所有实际情况都有好处,因为在这个宇宙中没有一台计算机可以存储10的字符串92-1个字符。

因此,我们的问题实际上解决了。然而,我一开始就告诉过你,这些观众是由数学家组成的。如果他们说“对于所有整数”,他们中间它。

那么我们将如何做更多的数字呢?

实际上,只有一个颚化符字符的情况只是一个特例。一般来说,我们存储这种形式的字符串:

"~~~~~~ca[0]a[1]a[2]... a[n]"

...其中包括n波浪号字符(n个正整数),后跟一个字符c,然后n+1十进制数a[0],a[1],a[2], ...a[n],在最后一个空格前加一个空格。a[0]以上面的c符号编码,并且小于1092,a[1]准确地说a[0]数字,a[2]准确地说a[1]数字等等,直到a[n]准确地说a[n-1]数字。

我们声称这将任意大的正整数嵌入到所有UTF-8字符串的集合中,并且是保序的。证据的细节留给读者,但是关于排序的主要论点如上:比较ab分别是:要么ab具有相同数量的数字,那么这两个字符串将具有相同数量的波浪号字符和相同数量的数字,并且在这两种情况下,除了最后的数字之外,所有数字都是成对相同的。然后是词典学的比较a[n]b[n]工作正常。如果它们有不同的位数,那么较短的一个是较小的一个,我们前面的论证表明词典式比较是正确的。

现在这是一个数学上令人满意的问题解决方案。但是停下来!我们忘记了负数。幸运的是,我们漏掉了!Unicode代码点为33的字符,比我们上面使用的任何初始字符都要小。所以我们可以简单地预先准备一个!符号,并跟随绝对值的编码,所有负数都早于所有正数。然而,有一个小问题:我们得到了字符串!" 6比较小于!" 7这与-7小于-6相矛盾。

因此,我们必须找到一种方法来反转我们用于正数的编码顺序。幸运的是,这很容易。因为我们只使用了代码点33到126之间的字符,所以我们可以简单地通过保留空格和颠倒顺序来翻译一切。那
是,代码点33和126互换,34和125,以此类推,直到79和80。那么字典顺序将是相反的。那就是:

`!} m` stands for -2 (since `" 2` is the encoding of +2)
`!| jl` stands for -53 (since `# 53` is the encoding of +53)

数学解到此结束。为了完整起见,这里有一些进行编码和解码的JavaScript函数。

请注意,我们忽略了字符串超过10的罕见情况([1o^92]-1]-1位数字:

  function encodeNonNegative(s, pad) {
      let l = s.length;
      if (l <= 92) { return String.fromCodePoint(33+l) + pad + s; }
      return '~' + encodeNonNegative(l.toString(10), '') + ' ' + s;
    }
 
    function translate(s) {
      let r = [];
      for (i = 0; i < s.length; ++i) {
        let c = s.charCodeAt(i);
        r.push(c === 32 ? ' ' : String.fromCodePoint(159 - c));
      }
      return r.join('');
    }
 
    function encodeLong(s) {
      if (s[0] !== '-') { return encodeNonNegative(s, ' '); }
      let p = encodeNonNegative(s.slice(1), ' ');
      return '-' + translate(p);
    }
 
    function decodeNonNegative(s) {
      return s.slice(s.indexOf(" ")+1);
    }
 
    function decodeLong(s) {
      if (s[0] !== '-') { return decodeNonNegatve(s); }
      let p = decodeNonNegative(translate(s.slice(1)));
      return '-' + p;
    }

问题解决了!我们可以将任何整数嵌入到UTF-8字符串中,并保持适当的顺序。