在扩展字符串中查找第k个元素

时间:2014-11-04 05:16:58

标签: c++ string algorithm

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

我不确定如何去做。有人在编码面试中向我的朋友询问,我已经考虑了很多,但我没有得到有效的解决方案。

6 个答案:

答案 0 :(得分:10)

更新:随后是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,那么我们在扩展字符串中找到了k个字符。如果重复将跳过字符k,则我们将k减少到重复内的索引,然后重新开始扫描。

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

因此,当我们重新启动时,k的新值最多为旧值的一半。因此,在最糟糕的情况下,我们将重新启动log2k次。


虽然上述解决方案易于理解,但我们实际上可以做得更好。我们可以在扫描过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),因此时间复杂度不会改变。

答案 1 :(得分:2)

这实际上是一个有趣的拼图程序。

这是一个用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是字符串的长度。实际扩展的字符串可以更长,更长。例如,&#34; AB2C999999&#34;只有N = 10,所以应该使用O(100)内存元素,但扩展后的字符串长度超过200万字符。

答案 2 :(得分:1)

在第一种情况下,字符串是&#39; AB2C3&#39;其中&#39; 2&#39;从&#39; AB2C3&#39;中删除和左边的&#39; 2&#39; (&#39; AB&#39;)在字符串&#39; AB2C3&#39;重复&#39; 2&#39;倍。它变成了ABABC3&#39;

在第二种情况下,字符串是&#39; ABABC3&#39;其中&#39; 3&#39;已从ABABC3&#39;删除和左边的&#39; 3&#39; (&#39; ABABC&#39;)在字符串&#39; ABABC3&#39;重复&#39; 3&#39;倍。它变成了ABABCABABCABABC&#39;。

算法将是这样的:

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] 

答案 3 :(得分:1)

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

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

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

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

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

现在,如果k是一个int,如果它是负数,你仍然必须抛出异常,同样,k&lt; L * rp是另一个重要的验证规则。

如果一切都有效,则实际值的索引计算如下:

k%L

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

答案 4 :(得分:1)

我想这个问题的重点在于确定在你能够获得k元素之前你必须扩展多远。

0 < k <= 2的示例中假设第一个字符是索引1,您根本不需要展开。

对于2 < k <= 5,您只需展开第一部分。

对于5 < k <= 10,您需要展开unil ABABCABABC,对于10 < k <= 15,您需要进行全面展开。

答案 5 :(得分:-1)

为此问题提供代码。

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,代码几乎相同,只需稍作修改,我希望你得到实际的逻辑。