【问题标题】:Inplace string replacement in C在 C 中就地字符串替换
【发布时间】:2015-09-03 05:52:29
【问题描述】:

写一个函数

void inplace(char *str, 
             const char pattern, 
             const char* replacement, 
             size_t mlen)

输入:
str:以\0 结尾的字符串。输入表明我们需要一个就地算法。

pattern:一封信。

replacement:一个字符串。

mlen:内存大小保存字符串str从内存开头开始,mlen应该大于strlen(str)


最终结果还是由str指向的。

注意,所有出现的模式都应该被替换。

例如,

helelo\0...........

这里的“helelo”是要在末尾替换为'\0' 的字符串。在'\0' 之后还有 L 个有效字节。我们想用“123”替换“e”。

一个简单的方法是这样工作的,我们通过str,当匹配到一个模式时,我们将所有剩余的位置与填充替换字符串的位置一起移动,然后通过替换替换模式。

如果原始字符串的长度为n,并且只包含e,我们需要(n-1) + (n-2) + ... + 1 移位。

是否有一种算法可以只扫描一次字符串且内存成本不变?

【问题讨论】:

  • "如果原始字符串的长度为 n 并且只包含 e,我们需要 2(n-1) + 2(n-2) + ... + 2 个班次"。不,这是不正确的。每个字母只移动一次。示例:“abcdef”。右移一个字母表示,将“f”向下复制一个字符,将“e”向下复制一个字符,等等。您正在从字符串的末尾开始工作。这并不意味着像您暗示的那样从字符串的前面连续扫描。
  • 如果原字符串是eeeec,那么新字符串应该是123123123123c。如果我们知道新字符串的长度,我们可以直接将c移动到最后的位置,然后在前面一一加上123。在不知道长度的情况下,当第一个 e 匹配时,我们将所有其余的 eeec 向右移动 2 个字节,这需要 4 次移动。当我们遇到第二个 e 时,我们需要另外 3 个动作..
  • 好吧,你是对的。但那是因为您的问题仍未明确指定(即使您已经开始了一个新问题)。好吧,至少我不清楚你想替换所有出现的情况(是的,我可能应该假设 - 但这就是为什么应该始终清楚明确地指定需求,而不是为假设留出空间)。

标签: c algorithm string-algorithm


【解决方案1】:

我认为最少要通过两次。在第一遍中,计算将被替换的字符数。鉴于count 和替换字符串的长度,您可以计算最终字符串的长度。 (并且您应该验证它是否适合缓冲区。)

在第二遍中,您向后扫描字符串(从最后一个字符开始),将字符复制到它们的最终位置。当您遇到搜索字符时,将替换字符串复制到该位置。

在你的例子中,长度的增加是 2。所以你会

copy str[5] which is '\0' to str[7]
copy str[4] which is 'o' to str[6]
copy str[3] which is 'l' to str[5]
copy str[2] which is 'l' to str[4]
at str[1] you find the 'e' so str[3]='3' str[2]='2' str[1]='1'

此时输出索引与输入索引相同,因此可以打破循环。


正如@chux 在 cmets 中指出的那样,替换字符串为空或只有一个字符的情况可以通过字符串的一次前向传递来处理。所以代码应该分别处理这些情况。

【讨论】:

  • 我有一个问题。您假设在向后扫描字符串时,我们需要将字符移动替换字符串的长度。但是,如果存在连续的“e”,则情况并非如此,那么我们需要将连续的“e”移位替换字符串的长度。由于您正在向后扫描,因此无法知道“e”是否连续。我们只知道“e”的计数。
  • @SumitTrehan 'e' 是否连续并不重要。在替换字符串长度为 3 的示例中,每个 'e' 将字符串长度加 2,因为一个字符 'e' 被三个“123”替换。
  • 我假设你的意思是说我们可以计算最终字符串的长度,那么我们应该将最后一个字符移动到计算出的长度-1,然后对每个其他字符进行移位!
  • @SumitTrehan 是的,“计算最终字符串的长度”“将字符复制到它们的最终位置”就是我说的。
  • 不同意“最少两次通过”,如answer 所示。除非我不得不使用单通道解决方案,否则这个答案确实概述了一个好的方法。角落弱点:复制的顺序取决于图案替换的长度。如果strlen(replacement) >= 1 这个答案有效。但是是*replacement == 0,在第二遍时,代码应该向前扫描/替换字符串。
【解决方案2】:

一个候选的单通道解决方案。

对于str 中的每个字符,递归。递归完成后,进行替换。

确实大量递归。

#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>

// return 0:success else 1:fail
static int inplace_help(char *dest, const char *src, int pattern,
        const char* replacement, size_t rlen, size_t mlen) {
  printf("'%p' '%s' %c\n", dest, src, pattern);
  if (*src == pattern) {
    if (rlen > mlen) return 1;
    if (inplace_help(dest + rlen, src + 1, pattern, replacement, rlen,
            mlen - rlen)) return 1;
    memcpy(dest, replacement, rlen);
    return 0;
  }
  if (mlen == 0) return 1;
  int replace1 = *src;
  if (*src) {
    if (inplace_help(dest + 1, src + 1, pattern, replacement, rlen, mlen - 1)) {
      return 1;
    }
  }
  *dest = replace1;
  return 0;
}

void inplace(char *str, const char pattern, const char* replacement,
        size_t mlen) {
  if (pattern == 0) return;
  if (mlen == 0) return;
  if (*replacement == 0) return;  // Insure str does not shrink.
  inplace_help(str, str, pattern, replacement, strlen(replacement), mlen - 1);
}

int main(void) {
  char str[1000] = "eeeeec";
  inplace(str, 'e', "1234", sizeof str);
  printf("'%s'\n", str);  // --> '12341234123412341234c'
  return 0;
}

【讨论】:

  • 有趣的设计。它确实使用了一次,尽管由于递归,它的实际内存成本不是恒定的。
  • 其实一开始调用的strlen需要一次传回值。我们可以不调用 strlen 吗?
  • @Joe C 1) 内存需求与strlen(str) 成正比。 2) 仍然是单程。 1 次通过strinplace_help(),1 次通过replacementstrlen(replacement)。它不是通过相同的字符串进行 2 次传递。
  • @Joe 3) 这是一个例子,其中 1-pass 可以 完成,但其他人建议的 2-pass 可能更实用。实际上,重要的不是传递次数,而是算法在执行和内存使用中的复杂性顺序O()。我这样做是为了展示 1-pass 是如何发生的。
  • @user3386109 像for(i=0;i&lt;n;i++) { foo1()} for(i=0;i&lt;n;i++) { foo2()} 这样的代码肯定是2-pass。这个答案是f(i) { foo3(); if (i) f(i-1); foo4(); },我断言它是单次通过,因为它是O(n),并且一旦第一次完成就不会开始第二次通过。在任何情况下:真正的编码目标不是提高通过次数,而是降低执行复杂度,您的答案和我的答案是O(n)。内存使用量也很低;这是O(n),我认为你的O(1)更优秀。同样,我的目标是演示 1-pass 解决方案,而不是生产质量解决方案。
【解决方案3】:

以下假设分配给字符串的内存已在某个时间点初始化为某个值,因为标准 C 似乎不允许访问未初始化的内存。在实践中,它会正常工作。

它精确地进行两次扫描:第一次扫描整个分配的空间,并将字符串移动到空间的右侧边缘。第二次扫描是在字符串本身上进行的,它在进行替换时将其移回左侧边缘。

我将原型更改为成功返回 0; -1 失败。我也允许模式是一个字符串。 (也许一个字符是故意的?无论如何很容易改变。)(如所写,模式的长度不能为零。应该检查。)

int inplace(char *str, 
            const char* pattern, 
            const char* replacement, 
            size_t mlen) {
  /* We don't know how long the string is, but we know that it ends
     with a NUL byte, so every time we hit a NUL byte, we reset
     the output pointer.
   */
  char* left = str + mlen;
  char* right = left;
  while (left > str) {
    if (!*--left) right = str + mlen;
    *--right = *left;
  }

  /* Naive left-to-right scan. KMP or BM would be more efficient. */

  size_t patlen = strlen(pattern);
  size_t replen = strlen(replacement);
  for (;;) {
    if (0 == strncmp(pattern, right, patlen)) {
      right += patlen;
      if (right - left < replen) return -1;
      memcpy(left, replacement, replen);
      left += replen;
    } else {
      if (!(*left++ = *right++)) break;
    }
  }
  return 0;
}

【讨论】:

  • 我认为 c only 不允许读取未初始化的值。如果我们先写后读应该没问题。
  • @JoeC:没错。但是,上面的代码确实在写入之前读取,因为它读取给定字符串的整个分配区域,而不仅仅是构成字符串一部分的字节。如果该区域最初分配有calloc,那会很好,但如果该区域最初分配有malloc,那可能就不好了。如果您使用 valgrind 等检测未初始化读取的工具运行代码,您只会注意到该问题。
猜你喜欢
  • 2015-01-27
  • 2012-04-19
  • 2020-11-21
  • 2023-04-02
  • 1970-01-01
  • 2015-05-21
  • 2011-06-14
  • 2021-07-18
  • 2011-10-20
相关资源
最近更新 更多