【问题标题】:Return a new string that sorts between two given strings返回一个在两个给定字符串之间排序的新字符串
【发布时间】:2016-12-19 18:53:45
【问题描述】:

给定两个字符串 a 和 b,其中 a 按字典顺序

有这方面的实用算法吗?

【问题讨论】:

  • 你可能想先定义“字典上的<”,这个问题真的取决于这个定义!
  • 例如,如果a < axax < b,那么附加单个字符将是一个简单的解决方案
  • 谢谢马库斯。那么我将如何在 a 和 ax 之间插入一个新节点?我正在寻找能够在未来插入中继续工作的东西。
  • 我指的是lexicograhically <.>
  • 你想限制字符串的长度吗(在实践中我会这么认为)?然后你可以枚举它们,所以使用字符串与使用整数作为键没有什么不同。如果您已经使用 10 和 20 作为键,则两者之间只有 9 个选项。如果你不断在两个值之间插入新的键,你会在某个时候用完键,除非你允许无限​​长的键。

标签: string algorithm sorting


【解决方案1】:

最小化字符串长度

如果您想将字符串长度保持在最短,您可以创建一个按字典顺序排列在左右字符串中间的字符串,以便有空间插入额外的字符串,并且仅在绝对必要时创建更长的字符串.

我将假设一个字母表 [a-z] 和一个字典顺序,其中一个空格位于 'a' 之前,例如"ab" 在 "abc" 之前。

基本案例

首先从字符串的开头复制字符,直到遇到第一个差异,这可能是两个不同的字符,也可能是左侧字符串的结尾:

abcde ~ abchi  ->  abc  +  d ~ h  
abc   ~ abchi  ->  abc  +  _ ~ h  

然后通过在左侧字符(或字母表的开头)和右侧字符之间附加字母表中间的字符来创建新字符串:

abcde ~ abchi  ->  abc  +  d ~ h  ->  abcf  
abc   ~ abchi  ->  abc  +  _ ~ h  ->  abcd  

连续字符

如果两个不同的字符在字典上是连续的,首先复制左边的字符,然后在左边字符串的下一个字符和字母表末尾之间的中间追加字符:

abhs ~ abit  ->  ab  +  h ~ i  ->  abh  +  s ~ _  ->  abhw
abh  ~ abit  ->  ab  +  h ~ i  ->  abh  +  _ ~ _  ->  abhn

如果左侧字符串中的下一个字符是一个或多个 z,则复制它们并将字符附加在第一个非 z 字符和字母表末尾之间的中间:

abhz   ~ abit  ->  ab  +  h ~ i  ->  abh  +  z ~ _  ->  abhz  +  _ ~ _  ->  abhzn  
abhzs  ~ abit  ->  ab  +  h ~ i  ->  abh  +  z ~ _  ->  abhz  +  s ~ _  ->  abhzw  
abhzz  ~ abit  ->  ab  +  h ~ i  ->  abh  +  z ~ _  ->  ... ->  abhzz  +  _ ~ _  ->  abhzzn

右边的字符是a或b

你不应该通过在左边的字符串后面附加一个“a”来创建一个字符串,因为这会创建两个按字典顺序连续的字符串,在它们之间不能再添加更多的字符串。解决方案是始终在字母表开头和右侧字符串的下一个字符之间附加一个附加字符:

abc  ~ abcah   ->  abc  +  _ ~ a  ->  abca  +  _ ~ h  ->  abcad  
abc  ~ abcab   ->  abc  +  _ ~ a  ->  abca  +  _ ~ b  ->  abcaa  +  _ ~ _  ->  abcaan  
abc  ~ abcaah  ->  abc  +  _ ~ a  ->  abca  +  _ ~ a  ->  abcaa  +  _ ~ h  ->  abcaad  
abc  ~ abcb    ->  abc  +  _ ~ b  ->  abca  +  _ ~ _  ->  abcan

代码示例

下面是演示该方法的代码sn-p。因为 JavaScript 有点繁琐,但实际上并不复杂。要生成第一个字符串,请使用两个空字符串调用该函数;这将生成字符串“n”。要在最左边的字符串之前或最右边的字符串之后插入一个字符串,请使用该字符串和一个空字符串调用该函数。

function midString(prev, next) {
    var p, n, pos, str;
    for (pos = 0; p == n; pos++) {               // find leftmost non-matching character
        p = pos < prev.length ? prev.charCodeAt(pos) : 96;
        n = pos < next.length ? next.charCodeAt(pos) : 123;
    }
    str = prev.slice(0, pos - 1);                // copy identical part of string
    if (p == 96) {                               // prev string equals beginning of next
        while (n == 97) {                        // next character is 'a'
            n = pos < next.length ? next.charCodeAt(pos++) : 123;  // get char from next
            str += 'a';                          // insert an 'a' to match the 'a'
        }
        if (n == 98) {                           // next character is 'b'
            str += 'a';                          // insert an 'a' to match the 'b'
            n = 123;                             // set to end of alphabet
        }
    }
    else if (p + 1 == n) {                       // found consecutive characters
        str += String.fromCharCode(p);           // insert character from prev
        n = 123;                                 // set to end of alphabet
        while ((p = pos < prev.length ? prev.charCodeAt(pos++) : 96) == 122) {  // p='z'
            str += 'z';                          // insert 'z' to match 'z'
        }
    }
    return str + String.fromCharCode(Math.ceil((p + n) / 2)); // append middle character
}

var strings = ["", ""];
while (strings.length < 100) {
    var rnd = Math.floor(Math.random() * (strings.length - 1));
    strings.splice(rnd + 1, 0, midString(strings[rnd], strings[rnd + 1]));
    document.write(strings + "<br>");
}

下面是对 C 的直接翻译。使用以空 null 结尾的字符串调用该函数以生成第一个字符串,或者在最左边的字符串之前或最右边的字符串之后插入。字符串缓冲区buf 应该足够大以容纳一个额外的字符。

int midstring(const char *prev, const char *next, char *buf) {
    char p = 0, n = 0;
    int len = 0;
    while (p == n) {                                           // copy identical part
        p = prev[len] ? prev[len] : 'a' - 1;
        n = next[len] ? next[len] : 'z' + 1;
        if (p == n) buf[len++] = p;
    }
    if (p == 'a' - 1) {                                        // end of left string
        while (n == 'a') {                                     // handle a's
            buf[len++] = 'a';
            n = next[len] ? next[len] : 'z' + 1;
        }
        if (n == 'b') {                                        // handle b
            buf[len++] = 'a';
            n = 'z' + 1;
        }
    }
    else if (p + 1 == n) {                                     // consecutive characters
        n = 'z' + 1;
        buf[len++] = p;
        while ((p = prev[len] ? prev[len] : 'a' - 1) == 'z') { // handle z's
            buf[len++] = 'z';
        }
    }
    buf[len++] = n - (n - p) / 2;                              // append middle character
    buf[len] = '\0';
    return len;
}

平均字符串长度

最好的情况是元素以随机顺序插入。在实践中,当以伪随机顺序生成 65,536 个字符串时,平均字符串长度约为 4.74 个字符(理论最小值,在移动到更长的字符串之前使用每个组合,将是 3.71)。

最坏的情况是按顺序插入元素时,总是生成一个新的最右边或最左边的字符串;这将导致重复出现的模式:

n, u, x, z, zn, zu, zx, zz, zzn, zzu, zzx, zzz, zzzn, zzzu, zzzx, zzzz...  
n, g, d, b, an, ag, ad, ab, aan, aag, aad, aab, aaan, aaag, aaad, aaab...  

每四个字符串后添加一个额外的字符。


如果您想要为其生成密钥的现有有序列表,请使用如下算法生成按字典顺序等间距的密钥,然后在插入新元素时使用上述算法生成新密钥。

代码检查需要多少个字符,最低有效数字需要多少个不同的字符,然后在字母表中的两个选项之间切换以获得正确数量的键。例如。有两个字符的键可以有 676 个不同的值,所以如果你要求 1600 个键,即每个两个字符组合有 1.37 个额外的键,所以在每个两个字符的键之后附加一个 ('n') 或两个 ('j' ,'r') 字符被附加,即:aan ab abj abr ac acn ad adn ae aej aer af afn ...(跳过初始的'aa')。

function seqString(num) {
    var chars = Math.floor(Math.log(num) / Math.log(26)) + 1;
    var prev = Math.pow(26, chars - 1);
    var ratio = chars > 1 ? (num + 1 - prev) / prev : num;
    var part = Math.floor(ratio);
    var alpha = [partialAlphabet(part), partialAlphabet(part + 1)];
    var leap_step = ratio % 1, leap_total = 0.5;
    var first = true;
    var strings = [];
    generateStrings(chars - 1, "");
    return strings;

    function generateStrings(full, str) {
        if (full) {
            for (var i = 0; i < 26; i++) {
                generateStrings(full - 1, str + String.fromCharCode(97 + i));
            }
        }
        else {
            if (!first) strings.push(stripTrailingAs(str));
            else first = false;
            var leap = Math.floor(leap_total += leap_step);
            leap_total %= 1;
            for (var i = 0; i < part + leap; i++) {
                strings.push(str + alpha[leap][i]);
            }
        }
    }
    function stripTrailingAs(str) {
        var last = str.length - 1;
        while (str.charAt(last) == 'a') --last;
        return str.slice(0, last + 1);
    }
    function partialAlphabet(num) {
        var magic = [0, 4096, 65792, 528416, 1081872, 2167048, 2376776, 4756004,
                     4794660, 5411476, 9775442, 11097386, 11184810, 22369621];
        var bits = num < 13 ? magic[num] : 33554431 - magic[25 - num];
        var chars = [];
        for (var i = 1; i < 26; i++, bits >>= 1) {
            if (bits & 1) chars.push(String.fromCharCode(97 + i));
        }
        return chars;
    }

}
document.write(seqString(1600).join(' '));

【讨论】:

  • 不错的答案!这或多或少是我对我的ab-only答案的概括,但我担心这会成为一个冗长的故事。似乎直觉是对的:)
  • 这太酷了!以随机顺序生成 10k 个字符串,平均长度为 3.87 个字符,最大值为 7 个。在病态的情况下,总是在开头插入 10k 个字符串,最大字符串长度为 2500。可以表达你知道你要去哪里的想法依次生成N 字符串(一个接一个),在那些N 之后,可以假设进一步的插入位于随机位置?这样,如果N=5000(您首先“按顺序”生成 5k 个字符串)然后再生成另外 5000 个字符串,现在每个字符串在原始 5k 之间随机生成,您仍然会得到 ~7 的最大长度?
  • @AhmedFasih 是的,如果您要转换已排序的列表,最好为您已有的数据生成均匀分布的键,然后在插入其他键时使用上述方法。 (注意:如果您首先使用另一种方法生成密钥,请务必确保没有密钥以“a”结尾;否则,您可能会造成无法生成或仅生成有限数量的新密钥以适应的情况有一定的差距。)
  • @AhmedFasih 您可以将键视为 base-26 中的数字;例如如果您需要 676 到 17576 个键,请使用 3 个字符,并将数字 1、2、3 ... x (17576 / N) 转换为 base-26。 (同样,避免在键的末尾使用“a”。)
  • 我已经做了一个简单的 m69 算法的 C# 端口:nuget.org/packages/StringBetween
【解决方案2】:

这是实现此目标的一种非常简单的方法,并且可能远非最佳(当然取决于您所说的最佳)。

我只使用ab。我想您可以将其概括为使用更多字母。

两个简单的观察:

  1. 在另一个字符串之后创建一个新字符串很容易:只需附加一个或多个字母。例如,abba abbab。
  2. 只有当xb 结尾时,才能保证在另一个字符串x 之前创建一个新字符串之前。现在,将 b 替换为 a 并附加一个或多个字母。例如,abbab > abbaab

算法现在非常简单。以ab 作为哨兵开始。在两个现有密钥xy 之间插入一个新密钥:

  • 如果xy 的前缀:新键是y,结尾b 替换为ab
  • 如果x 不是y 的前缀:新键是x,并附加了b

示例运行:

a, b
a, ab*, b
a, aab*, ab, b
a, aab, ab, abb*, b
a, aab, ab, abab*, abb, b
a, aaab*, aab, ab, abab, abb, b

【讨论】:

  • “aa”介于“a”和“aaa”之间,但您的回答表明这是不可能的。
  • @PaulHankin 使用“aa”或“aaa”意味着把自己画到角落里,因为“a”、“aa”、“aaa”……在字典上是连续的,所以你不能插入以后他们之间的任何事情。
  • @PaulHankin 问题不是:请告诉我在哪里插入aa,问题是在两个现有密钥之间生成一个新密钥。该算法生成的每个密钥都以a 开头并以b 结尾,原因在@m69 中提到
【解决方案3】:

这是直接在我的 PostgreSQL 数据库中实现的 m69 答案的等效函数,使用 PL/pgSQL:

create or replace function app_public.mid_string(prev text, next text) returns text as $$
declare
  v_p int;
  v_n int;
  v_pos int := 0;
  v_str text;
begin
  LOOP -- find leftmost non-matching character
    v_p := CASE WHEN v_pos < char_length(prev) THEN ascii(substring(prev from v_pos + 1)) ELSE 96 END;
    v_n := CASE WHEN v_pos < char_length(next) THEN ascii(substring(next from v_pos + 1)) ELSE 123 END;
    v_pos := v_pos + 1;
    EXIT WHEN NOT (v_p = v_n);
  END LOOP;
  v_str := left(prev, v_pos-1);   -- copy identical part of string
  IF v_p = 96 THEN                -- prev string equals beginning of next
    WHILE v_n = 97 LOOP           -- next character is 'a'
      -- get char from next
      v_n = CASE WHEN v_pos < char_length(next) THEN ascii(substring(next from v_pos + 1)) ELSE 123 END;
      v_str := v_str || 'a';      -- insert an 'a' to match the 'a'
      v_pos := v_pos + 1;
    END LOOP;
    IF v_n = 98 THEN              -- next character is 'b'
      v_str := v_str || 'a';      -- insert an 'a' to match the 'b'
      v_n := 123;                 -- set to end of alphabet
    END IF;
  ELSIF (v_p + 1) = v_n THEN    -- found consecutive characters
    v_str := v_str || chr(v_p); -- insert character from prev
    v_n = 123;                  -- set to end of alphabet
    v_p := CASE WHEN v_pos < char_length(prev) THEN ascii(substring(prev from v_pos + 1)) ELSE 96 END;
    WHILE v_p = 122 LOOP
      v_pos := v_pos + 1;
      v_str := v_str || 'z';    -- insert 'z' to match 'z'
      v_p := CASE WHEN v_pos < char_length(prev) THEN ascii(substring(prev from v_pos + 1)) ELSE 96 END;
    END LOOP;
  END IF;
  return v_str || chr(ceil((v_p + v_n) / 2.0)::int);
end;
$$ language plpgsql strict volatile;

用这个功能测试过:

create or replace function app_public.test() returns text[] as $$
declare
  v_strings text[];
  v_rnd int;
begin
  v_strings := array_append(v_strings, app_public.mid_string('', ''));

  FOR counter IN 1..100 LOOP
    v_strings := v_strings || app_public.mid_string(v_strings[counter], '');
  END LOOP;
  return v_strings;
end;
$$ language plpgsql strict volatile;

结果:

"strings": [
  "n",
  "u",
  "x",
  "z",
  "zn",
  "zu",
  "zx",
  "zz",
  "zzn",
  "zzu",
  "zzx",
  "zzz",
  "zzzn",
  "zzzu",
  "zzzx",
  "zzzz",
  "...etc...",
  "zzzzzzzzzzzzzzzzzzzzzzzzn",
  "zzzzzzzzzzzzzzzzzzzzzzzzu",
  "zzzzzzzzzzzzzzzzzzzzzzzzx",
  "zzzzzzzzzzzzzzzzzzzzzzzzz",
  "zzzzzzzzzzzzzzzzzzzzzzzzzn"
]

【讨论】:

    【解决方案4】:

    以防万一有人需要。这是 Kotlin 中的相同算法。它可以工作,但可能可以用更好的方式编写。

        fun midString(prev: String?, next: String?): String {
            val localPrev = prev ?: ""
            val localNext = next ?: ""
        
            var p: Int
            var n: Int
            var str: String
        
            // Find leftmost non-matching character
            var pos = 0
            do {
                p = if (pos < localPrev.length) localPrev[pos].toInt() else 96
                n = if (pos < localNext.length) localNext[pos].toInt() else 123
                pos++
            } while (p == n)
        
            str = localPrev.substring(0, pos - 1)           // Copy identical part of string
            if (p == 96) {                                  // Prev string equals beginning of next
                while (n == 97) {                           // Next character is 'a'
                    n = if (pos < localNext.length) localNext[pos++].toInt() else 123 // Get char from next
                    str += 'a'                              // Insert an 'a' to match the 'a'
                }
                if (n == 98) {                              // Next character is 'b'
                    str += 'a'                              // Insert an 'a' to match the 'b'
                    n = 123                                 // Set to end of alphabet
                }
            }
            else if (p + 1 == n) {                          // Found consecutive characters
                str += p.toChar()                           // Insert character from prev
                n = 123                                     // Set to end of alphabet
        
                p = if (pos < localPrev.length) localPrev[pos++].toInt() else 96
                while (p == 122) { // p='z'
                    str += 'z'                              // Insert 'z' to match 'z'
                    p = if (pos < localPrev.length) localPrev[pos++].toInt() else 96
                }
            }
            return str + ceil((p + n) / 2.0).toChar()   // Append middle character
        }
    

    【讨论】:

      【解决方案5】:

      m69 ''snarky and unwelcoming''提供的算法的F#实现:

      /// Returns a string that sorts 'midway' between the provided strings
      /// to allow ordering of a list of items.
      /// Pass None for one or both of the strings, as the case may be, to
      /// sort before or after a single item, or if it is the first item in the list.
      let midString (s1O : Option<string>) (s2O : Option<string>) =
        let firstSymbol = 'a' |> int
        let lastSymbol = 'z' |> int
        let middleSymbol = (firstSymbol + lastSymbol + 1) / 2
      
        let halfwayToFirstFrom c = (firstSymbol + c) / 2
        let halfwayToLastFrom c = (c + lastSymbol + 1) / 2
        let halfwayBetween c1 c2 = (c1 + c2 + 1) / 2
      
        let stringToIntList = Seq.toList >> List.map int
        let reverseAndMakeString = List.map char >> Seq.rev >> System.String.Concat
      
        let rec inner acc l1 l2 =
          match l1, l2 with
          | head1::tail1, head2::tail2 ->
              if head1 = head2 then inner (head1::acc) tail1 tail2          // keep looking for first difference
              elif head2 - head1 = 1 then inner (head1::acc) tail1 []       // tail2 no longer relevant, already sorting before it
              elif head2 - head1 > 1 then (halfwayBetween head1 head2)::acc // done
              else failwith "unreachable"
          | head1::tail1, [] ->                   // find the correct string to sort after s1 (already sorting before s2)
              if head1 = lastSymbol then
                inner (head1::acc) tail1 []       // already on last character in alphabet at this position, move to next position
              else (halfwayToLastFrom head1)::acc // suitable character is available - done.
          | [], head2::tail2 ->                              // strings were identical for whole of first string
              if halfwayToFirstFrom head2 = firstSymbol then
                inner (firstSymbol::acc) [] tail2            // no space in alphabet, move to next position
              else (halfwayToFirstFrom head2)::acc           // done.
          | [], [] -> middleSymbol::acc
      
        match s1O, s2O with
          | None, None -> [middleSymbol]
          | Some s1, Some s2 ->
              if s1 < s2 then inner [] (stringToIntList s1) (stringToIntList s2)
              else failwith "Invalid input - s1 must sort before s2"
          | Some s1, None -> inner [] (stringToIntList s1) (stringToIntList "")
          | None, Some s2 -> inner [] (stringToIntList "") (stringToIntList s2)
        |> reverseAndMakeString
      
      
      
      
      /// Tests of examples provided above, and some extras.
      let testsData = [
          (Some "abcde", "abcf"  , Some "abchi" )
          (Some "abc"  , "abcd"  , Some "abchi" )
          (Some "abhs" , "abhw"  , Some "abit"  )
          (Some "abh"  , "abhn"  , Some "abit"  )
          (Some "abhz" , "abhzn" , Some "abit"  )
          (Some "abhzs", "abhzw" , Some "abit"  )
          (Some "abhzz", "abhzzn", Some "abit"  )
          (Some "abc"  , "abcad" , Some "abcah" )
          (Some "abc"  , "abcaan", Some "abcab" )
          (Some "abc"  , "abcaad", Some "abcaah")
          (Some "abc"  , "abcan" , Some "abcb"  )
          (Some "abc"  , "n"     , None         )
          (Some "n"    , "t"     , None         )
          (Some "t"    , "w"     , None         )
          (Some "w"    , "y"     , None         )
          (Some "y"    , "z"     , None         )
          (Some "z"    , "zn"    , None         )
          (None        , "g"     , Some "n"     )
          (None        , "d"     , Some "g"     )
          (None        , "b"     , Some "d"     )
          (None        , "an"    , Some "b"     )
          (None        , "ag"    , Some "an"    )
          (None        , "ad"    , Some "ag"    )
          (None        , "ab"    , Some "ad"    )
          (None        , "aan"   , Some "ab"    )
           ]
      
      testsData
      |> List.map (fun (before, expected, after) ->
           let actual = midString before after
           printfn $"Before, after, expected, actual, pass:  {(before, after, expected, actual, actual = expected)}"
           actual = expected )
      
      
      

      【讨论】:

        【解决方案6】:

        据我了解,字符串的格式可以任意设置。 我宁愿相信,对于小数部分(即十进制数数字顺序和字典顺序。字符串和数字之间存在保序双射。

        0
        0.2
        0.225
        0.3
        0.45
        0.7
        0.75
        ...
        

        要在两个现有字符串之间插入一个字符串,同时保留字典顺序,我们可以:

        1. 将字符串转换为浮点数
        2. 添加两个数字之间差值的一半(或者如果我们想分别在末尾或开头追加,则在数字与 1 或 0 之间)
        3. 将生成的浮点数转换为字符串

        在 Javascript 中:

        function getLexicographicInsert(a, b) {
            const x = a ? parseFloat(a) : 0;
            const y = b ? parseFloat(b) : 1;
            return `${x + (y - x) / 2}`;
        }
        

        【讨论】:

          【解决方案7】:

          @m69 的答案的简化/修改。

          假设字符串不以“零”结尾(这通常是必要的,因为在 s 和 s 的某个零扩展之间只有有限数量的字符串),字符串与 [0, 1)。所以我会用十进制来讨论,但同样的原则也适用于任意字母。

          我们可以零扩展左字符串 (0.123 = 0.123000...) 和 9 扩展右字符串 (0.123 = 0.122999...),这自然会导致

          // Copyright 2021 Google LLC.
          // SPDX-License-Identifier: Apache-2.0
          template <typename Str, typename Digit>
          Str midpoint(const Str left, const Str right, Digit zero, Digit nine) {
            Str mid;
            for (auto i = left.size() - left.size();; ++i) {
              Digit l = i < left.size() ? left[i] : zero;
              Digit r = i < right.size() ? right[i] : nine;
              if (i == right.size() - 1) --r;
              // This is mid += (l + r + 1)/2
              // without needing Digit to be wider than nine.
              r -= l;
              mid += l + r/2 + (r&1);
              if (mid.back() != l) break;
            }
            return mid;
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-01-04
            • 2019-08-18
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多