【问题标题】:Find kth element in an expanding string在扩展字符串中查找第 k 个元素
【发布时间】:2014-12-30 22:23:07
【问题描述】:

给定一个AB2C3 形式的字符串和一个int k。将字符串展开为ABABC3,然后是ABABCABABCABABC。任务是找到第 k 个元素。您的内存有限,因此无法展开整个字符串。你只需要找到第 k 个元素。

我不知道该怎么做。这是在一次编码面试中被问到我的朋友的,我对此进行了很多思考,但我没有得到一个有效的解决方案。

【问题讨论】:

  • 想一想。你会如何在纸上做到这一点? (A)80(BC)10(D)10 的第 90 个字母是什么?哪个是相关部分,它将是该部分的哪封信?

标签: c++ string algorithm


【解决方案1】:

更新:以下是O(1) 空格和O(N) 时间版本。见下文。


原方案使用O(1)空格和O(N log k)时间,其中n是未展开字符串的大小:

char find_kth_expanded(const char* s, unsigned long k) {
  /* n is the number of characters in the expanded string which we've
   * moved over.
   */
  unsigned long n = 0;
  const char *p = s;
  for (;;) {
    char ch = *p++;
    if (isdigit(ch)) {
      int reps = ch - '0';
      if (n * reps <= k)
        n *= reps;
      else {
        /* Restart the loop. See below. */
        k = k % n;
        p = s;
        n = 0;
      }
    }
    else if (ch == 0 || n++ == k)
      return ch;
  }
}

该函数只是在字符串中从左到右移动,跟踪它在扩展字符串中传递了多少个字符。如果该值达到k,那么我们在扩展字符串中找到了kth 字符。如果重复会跳过字符 k,那么我们将 k 减少到重复中的索引,然后重新开始扫描。

很明显,它使用了O(1) 空间。为了证明它在O(N log k) 中运行,我们需要计算循环重新启动的次数。如果我们正在重新启动,那么k≥n,因为否则我们之前会返回n 处的字符。如果k≥2n 那么n≤k/2 那么k%n≤k/2。如果k&lt;2nk%n = k-n。但是n&gt;k/2,所以k-n&lt;k-k/2,因此k%n&lt;k/2

所以当我们重新启动时,k 的新值最多是旧值的一半。所以在最坏的情况下,我们会重新启动log<sub>2</sub>k 次。


虽然上述解决方案很容易理解,但我们实际上可以做得更好。一旦我们扫描过去的k(扩展)字符,我们就可以向后扫描,而不是重新开始扫描。在向后扫描期间,我们需要始终将k 校正为当前段中的范围,方法是将其模数作为段长度的基数:

/* Unlike the above version, this one returns the point in the input
 * string corresponding to the kth expanded character.
 */
const char* find_kth_expanded(const char* s, unsigned long k) {
  unsigned long n = 0;
  while (*s && k >= n) {
    if (isdigit(*s))
      n *= *s - '0';
    else
      ++n;
    ++s;
  }
  while (k < n) {
    --s;
    if (isdigit(*s)) {
      n /= *s - '0';
      k %= n;
    }
    else
      --n;
  }
  return s;
}

上述函数都不能正确处理乘数为 0 且 k 小于分段长度乘以 0 的情况。如果 0 是合法乘数,一个简单的解决方案是反向扫描最后一个 0 的字符串,并在下一个字符处开始 find_kth_expanded。由于反向扫描是O(N),所以时间复杂度没有变化。

【讨论】:

  • 一个很好的答案。我运行它并验证它可以工作。
  • 非常紧凑且易于理解......我同意的好答案:)
【解决方案2】:

首先,看一下字符串。您的字符串由两部分组成:数据部分和信息部分。数据部分包含实际要重复的字符串,信息部分包含实际的重复次数。

如果你理解了这一点,你就已经理解了数据的模式。

下一步是处理特殊情况,例如负重复数、实重复数而不是整数。您实际上可以说 repeat 是在最后找到的字符串的子字符串,并由它只能包含数字的规则定义。如果您这样考虑,那么您将有两种情况:字符串以数字结尾,或者字符串不以数字结尾。在第一种情况下,我们有一个有效的重复次数,在第二种情况下,我们必须抛出异常。

如果我们仍然有一个有效的重复数字,那么它可能有多个数字,因此,您必须探索您的字符串以找到与数字无关的最后一个索引。该索引之后的子字符串是信息部分,即 rp(重复数)。此外,这个索引实际上等于你的数据部分的长度 - 1,我们称之为长度 L。

如果你有一个有效的rp,那么结果字符串的实际长度是L * rp。

现在,如果 k 是一个 int,如果它是负数,你仍然必须抛出异常,另外,k

如果一切都有效,那么实际值的索引是通过以下方式计算的:

k % L

您不必实际计算结果字符串来确定第 k 个字符,因为您可以使用重复模式这一事实。

【讨论】:

    【解决方案3】:

    这实际上是一个有趣的益智程序。

    这是一个用 C# 编写的答案。转换为 C++ 是一个练习!有 2 个递归函数,一个用于计算扩展字符串的长度,另一个用于查找给定字符串的第 k 个字符。它向后工作,从右到左,一次剥离一个字符。

    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace expander
    {
        class Program
        {
            static void Main(string[] args)
            {
                string y = "AB2C3";
                Console.WriteLine("length of expanded = {0} {1}", y, length(y));
                for(uint k=0;k<length(y);k++)
                {
                    Console.WriteLine("found {0} = {1}",k,find(k,y));
                }
            }
    
            static char find(uint k, string s)
            {
                string left = s.Substring(0, s.Length - 1);
                char   last = s[s.Length - 1];
                uint len = length(left);
                if (last >= '0' && last <= '9')
                {
                    if (k > Convert.ToInt32(last -'0') * len) throw new Exception("k out of range");
                    uint r = k % len;
                    return find(r, left );
                }
                if (k < len) return find(k, left);
                else if (k == len) return last;
                else throw new Exception("k out of range");
            }
            static uint length(string s)
            {
                if (s.Length == 0) return 0;
                char x = s[s.Length - 1];
                uint len = length(s.Substring(0, s.Length - 1));
                if (x >= '0' && x <= '9')
                {
                    return Convert.ToUInt32(x - '0') * len;
                }
                else
                {
                    return 1 + len;
                }
            }
        }
    }
    

    这是示例输出,它表明如果您遍历 k 的所有有效值(0 到 len-1),find 函数会复制扩展。

    length of expanded AB2C3 is 15
    if k=0, the character is A
    if k=1, the character is B
    if k=2, the character is A
    if k=3, the character is B
    if k=4, the character is C
    if k=5, the character is A
    if k=6, the character is B
    if k=7, the character is A
    if k=8, the character is B
    if k=9, the character is C
    if k=10, the character is A
    if k=11, the character is B
    if k=12, the character is A
    if k=13, the character is B
    if k=14, the character is C
    

    此程序的内存使用仅限于堆栈使用。堆栈深度将等于字符串的长度。在这个 C# 程序中,我一遍又一遍地复制字符串,这样会浪费内存。但即使管理不善,它也应该使用 O(N^2) 内存,其中 N 是字符串的长度。实际扩展的字符串可以长得多。比如“AB2C999999”只有N=10,所以应该使用O(100)个内存元素,但是展开后的字符串超过200万个字符。

    【讨论】:

    • rici 的回答比这个要好得多。我没有删除我的,因为当答案被删除时,我不喜欢它。
    【解决方案4】:

    我想问题的重点是弄清楚在获得kth 元素之前必须扩展多远。

    在这个0 &lt; k &lt;= 2 的示例中,假设第一个字符是索引 1,您根本不需要扩展。

    2 &lt; k &lt;= 5只需要展开第一部分即可。

    对于5 &lt; k &lt;= 10,您需要扩展unil ABABCABABC,对于10 &lt; k &lt;= 15,您需要进行完全扩展。

    【讨论】:

      【解决方案5】:

      在第一种情况下,字符串是'AB2C3',其中'2'从'AB2C3'中删除,并且字符串'AB2C3'中'2'('AB')的左侧重复'2'次。它变成“ABAC3”。

      在第二种情况下,字符串是 'ABAABC3',其中 '3' 从 'ABAABC3' 中删除,并且字符串 'ABABC3' 中的 '3' ('ABAABC') 的左侧重复了 '3' 次。它变成了'ABAABCABABABCABABC'。

      算法是这样的:

      1) READ ONE CHAR AT A TIME UNTIL END OF STRING
         IF CHAR IS AN INT THEN k := k - CHAR + 1
      2) RETURN STRING[k] 
      

      【讨论】:

      • k 不是原始字符串的一部分。它是一个自变量。 k 可以是 1;输出的第一个字符是“A”。 k 可以是 15;输出的第 15 个字符是“C”。
      • 那么'k'是什么意思呢?为什么给出?该字符串中已经有足够的信息。
      • k 是一个介于 1 和字符串扩展长度之间的数字。
      • 知道了...谢谢@MarkLakata
      【解决方案6】:

      给出这个问题的代码。

      public String repeater(String i_string, int k){
          String temp = ""; 
          for (int i=0; i < k; ++i)
              temp = temp + i_string.substring(0,k);
          temp = temp + i_string.substring(k, i_string.length());
          return temp;
      }
      

      我没有考虑内存有限的问题,因为没有提到任何明确的信息。

      您不需要任何额外的内存。您可以根据用户要求将数据打印到控制台。如果您只是显示,那么方法的返回类型也可以排除:) 您只需要一个临时字符串来保存处理过的数据。

      public void repeater2(String i_string, int k){
          String temp = i_string.substring(0,k);
          // Repeat and Print the first half as per requirements.
          for (int i=0; i < k; ++i)
              System.out.print(temp);
          // Print the second half of the string AS - IS. 
          System.out.print(i_string.substring(k, i_string.length()));
      }
      

      如果 K 值为 1,则字符串将被打印一次。根据要求。我们需要两次迭代。 C++ 或 Java 的代码几乎相同,只是稍作改动,我希望你能得到实际的逻辑。

      【讨论】:

      • 为什么不详细解释一下这个问题呢?我没有得到你上面的报价。代码应该重复K之前的元素吧?
      猜你喜欢
      • 2021-12-24
      • 1970-01-01
      • 1970-01-01
      • 2019-08-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-29
      • 1970-01-01
      相关资源
      最近更新 更多