【问题标题】:Fastest algorithm for splitting a string in C?用C分割字符串的最快算法?
【发布时间】:2017-02-22 20:19:15
【问题描述】:

我正在编写一个 CLI 输入解析器,我想知道将字符串拆分为标记的最快算法是什么。

规则:

  • 空格表示令牌的结尾。
  • 任何字符都可以用反斜杠转义,这意味着我按原样接受它,没有任何特殊含义。 (目前只用于转义空格)

这是我目前使用的代码:

#define POINTER_ARRAY_STEP 10
#define STRING_STEP 255

char **parse_line(char *line)
{
    char **array;
    size_t array_len;
    size_t array_index;
    size_t token_len;
    size_t token_index;

    array_len = POINTER_ARRAY_STEP;
    array = malloc((array_len + 1) * sizeof(char*)); /* +1 to leave space for NULL */
    array_index = 0;

    token_len = STRING_STEP;
    array[array_index] = malloc(token_len + 1);
    token_index = 0;

    for (; *line; ++line) {
        if (array_index == array_len) {
            array_len += POINTER_ARRAY_STEP;
            array = realloc(array, (array_len + 1) * sizeof(char*));
        }
        if (token_index == token_len) {
            token_len += STRING_STEP;
            array[array_index] = realloc(array[array_index], token_len + 1);
        }
        if (*line == '\\') {
            array[array_index][token_index++] = *++line;
            continue;
        }
        if (*line == ' ') {
            array[array_index][token_index] = 0;
            array[++array_index] = malloc(token_len + 1);
            token_index = 0;
            continue;
        }
        array[array_index][token_index++] = *line;
    }
    /* Null terminate the last array element */
    array[array_index][token_index] = 0;

    /* Null terminate the array */
    array[array_index + 1] = NULL;
    return array;
}

【问题讨论】:

  • foo = realloc(foo, some_size) 总是一个坏主意:如果调用失败,您必须释放您刚刚覆盖的原始指针 foo
  • @DanielJour 是的,为了性能,一个一个地重新分配也不是最优的。这就是大小和容量的区别。
  • 哪些算法中最快的?这取决于您是否准备像strtok 那样牺牲输入字符串。我的选择是使用 strchr 遍历字符串,并将 nul 终止符写入(副本)字符串的空格中。
  • 您不需要重新分配。你可以只记住单词的开头,找到结尾,然后分配你需要的内存。
  • 最后:当询问“最快的算法”时,总是有一个问题:您是否分析了您的(工作?)实现?真的是瓶颈吗?

标签: c string algorithm command-line-interface tokenize


【解决方案1】:

你的方法既不安全又低效:你没有检查内存分配失败,你调用realloc()的次数太多了。

这是另一种方法:

  • 首先计算令牌和转义的数量,
  • 为标记分配指针数组和缓冲区
  • 进行第二次传递,将字符复制到缓冲区中,拆分标记并使指针数组指向标记。
  • 返回数组指针。

稍后可以通过在指针数组及其第一个元素上调用 free() 来释放内存。

代码如下:

#include <stdlib.h>

char **parse_line(const char *line) {
    size_t len, i, j, k, items = 1, escapes = 0;
    char **array;
    char *buf;

    for (len = 0; line[len]; len++) {
        if (line[len] == '\\') {
            escapes++;
            if (line[++len] == '\0')
                break;
        } else
        if (line[len] == ' ') {
            items++;
        }
    }
    if (len == escapes) {
        /* special case empty line */
        array = malloc(sizeof(*array));
        if (array != NULL) {
            array[0] = NULL;
        }
        return array;
    }
    array = malloc((items + 1) * sizeof(*array));
    buf = malloc(len + 1 - escapes);

    if (array == NULL || buf == NULL) {
        free(array);
        free(buf);
        return NULL;
    }
    items[0] = buf;
    k = 1;
    for (i = j = 0; i < len; i++) {
        if (line[i] == '\\') {
            if (++i == len)
                break;
            buf[j++] = line[i];
        } else
        if (line[i] == ' ') {
            buf[j++] = '\0';
            items[k++] = buf + j;
        } else {
            buf[j++] = line[i];
        }
    }
    buf[j] = '\0';
    items[k] = NULL;
    return items;
}

【讨论】:

  • 我会说这是非常聪明的设计。 (建议free(array); --> free(array);free(buf);
  • 顺便说一句:我认为items 1 太小了。考虑没有空格/没有转义的行。
  • @chux:好点,我们不知道哪个malloc() 失败了。我已经更新了空行的特殊情况。
【解决方案2】:

建议的最快算法

只进行 2 次传球。一次找到所需的令牌计数和缓冲区长度。

第二次切掉重复的行。

标记数组指向单个分配的内存,因此完成后,tokens[0]tokens 需要被释放。

由于 OP 要求更快的算法,下面是 $pseudo code$/code。

char **parse_line(const char *line) {
  size_t token_count = 1;
  const char *p = line;
  $Let `p` each element in `line`$
    $if `*p` is a separator, increment `token_count`
    $else `*p` is an escape not followed by a \0$
      $advance `p`$
    }
  }

  $ `p`, at the end of the string, so allocate enough for a    $
  $ duplicate +1 based on the difference of `p` and the start. $
  $ Check allocation success                                   $ 
  $ Copy into `char *line_buffer`                              $

  // The token count is known, get enough pointers + 1 and check
  char **tokens = malloc(sizeof *tokens * (token_count + 1));

  // More complex code could perform only 1 allocation for `tokens` and `line_buffer`

  $ Let `q` be first element in line_buffer
  $ Let `d` be the same (destination pointer)
  $ set begin_token flag $
  size_t token_index = 0;
  for (;;) {
    $ if begin_token flag set $ {
      $ clear begin_token $
      tokens[token_index] = q;
      d = q;
    }
    $ if `*q` separator (space or \0) $ {
      *d = '\0';
      token_index++;
      $ if *q at end of string $ break;
      $ set begin_token flag $
    $else {
      if `*q` is an escape not followed by a \0$
        $advance q$
      }
      $copy *q to *d, advance both pointers.
    }
  }
  $set last tokens[] to NULL$
  return tokens;
}

【讨论】:

  • 对不起,我想出了同样的想法,然后看到你删除的答案......
  • 只是想知道,你为什么要写伪代码?你认为 OP 是在寻求家庭作业帮助吗?
  • @chqrlie 帖子是关于算法的,代码看起来很有趣,所以我做了,但不想放弃太多。
  • 确实,OP 要求 最快的算法,这与 一种有效的实现并不完全相同,尽管后者可能就是他的样子真的要求。我倾向于阅读问题不如阅读代码。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-16
  • 2011-08-09
  • 2015-12-09
  • 1970-01-01
  • 2022-09-28
  • 1970-01-01
相关资源
最近更新 更多