【问题标题】:How to cut off parts of a string, which every string in a collection has如何切断字符串的一部分,集合中的每个字符串都有
【发布时间】:2016-08-24 11:06:43
【问题描述】:

我目前的问题如下: 我有一个std::vector 文件的完整路径名。 现在我想切断所有字符串的公共前缀。

示例

如果向量中有这 3 个字符串:

/home/user/foo.txt
/home/user/bar.txt
/home/baz.txt

我想从向量中的每个字符串中删除/home/

问题

一般来说有什么方法可以实现这一点吗? 我想要一个删除所有字符串的公共前缀的算法。 我目前只有一个想法,用 n 字符串和 m 解决 O(n m) 中的这个问题最长的字符串长度,只需逐个字符地遍历每个字符串。 有没有更快或更优雅的方法解决这个问题?

【问题讨论】:

  • 给定/home/user/martin, /home/user/mike,您希望结果是artin, ike 还是martin, mike?你的标题是 string,但你的问题是 path names
  • 很抱歉造成误解。两种结果都可以,如果其中一个可以比另一个更好地实现,我更喜欢这个
  • 我无法想象两个结果都可以的用例
  • @Exagon 你能检查我的答案吗?这是你想要的吗?
  • @KostasRim 不,我很抱歉这是一般的足够了

标签: c++ string c++11 c++14 matching


【解决方案1】:

这可以完全使用 std:: 算法来完成。

简介:

  1. 如果尚未排序,则对输入范围进行排序。排序范围内的第一个和最后一个路径 将是最不同的。最好的情况是 O(N),最坏的情况是 O(N + N.logN)

  2. 使用std::mismatch 来确定 两条最不同的路径[微不足道]

  3. 遍历每条路径,擦除前 COUNT 个字符,其中 COUNT 是最长公共序列中的字符数。 O (N)

最佳情况时间复杂度:O(2N),最坏情况 O(2N + N.logN)(有人可以检查吗?)

#include <iostream>
#include <algorithm>
#include <string>
#include <vector>

std::string common_substring(const std::string& l, const std::string& r)
{
    return std::string(l.begin(),
                       std::mismatch(l.begin(), l.end(),
                                     r.begin(), r.end()).first);
}

std::string mutating_common_substring(std::vector<std::string>& range)
{
    if (range.empty())
        return std::string();
    else
    {
        if (not std::is_sorted(range.begin(), range.end()))
            std::sort(range.begin(), range.end());
        return common_substring(range.front(), range.back());
    }
}

std::vector<std::string> chop(std::vector<std::string> samples)
{
    auto str = mutating_common_substring(samples);
    for (auto& s : samples)
    {
        s.erase(s.begin(), std::next(s.begin(), str.size()));
    }
    return samples;
}

int main()
{
    std::vector<std::string> samples = {
        "/home/user/foo.txt",
        "/home/user/bar.txt",
        "/home/baz.txt"
    };

    samples = chop(std::move(samples));

    for (auto& s : samples)
    {
        std::cout << s << std::endl;
    }   
}

预期:

baz.txt
user/bar.txt
user/foo.txt

这是一个不需要排序的替代“common_substring”。时间复杂度在理论上是 O(N),但在实践中是否更快,您必须检查:

std::string common_substring(const std::vector<std::string>& range)
{
    if (range.empty())
    {
        return {};
    }

    return std::accumulate(std::next(range.begin(), 1), range.end(), range.front(),
                           [](auto const& best, const auto& sample)
                           {
                               return common_substring(best, sample);
                           });
}

更新:

抛开优雅不谈,这可能是最快的方式,因为它避免了任何内存分配,就地执行所有转换。对于大多数架构和样本量而言,这比任何其他性能考虑都更重要。

#include <iostream>
#include <vector>
#include <string>

void reduce_to_common(std::string& best, const std::string& sample)
{
    best.erase(std::mismatch(best.begin(), best.end(),
                             sample.begin(), sample.end()).first,
               best.end());

}

void remove_common_prefix(std::vector<std::string>& range)
{
    if (range.size())
    {
        auto iter = range.begin();
        auto best = *iter;
        for ( ; ++iter != range.end() ; )
        {
            reduce_to_common(best, *iter);
        }

        auto prefix_length = best.size();

        for (auto& s : range)
        {
            s.erase(s.begin(), std::next(s.begin(), prefix_length));
        }
    }
}


int main()
{
    std::vector<std::string> samples = {
        "/home/user/foo.txt",
        "/home/user/bar.txt",
        "/home/baz.txt"
    };

    remove_common_prefix(samples);

    for (auto& s : samples)
    {
        std::cout << s << std::endl;
    }   
}

【讨论】:

  • 这样对字符串进行排序,排序为O(n log n),比OP的算法O(n)慢。 (这里忽略m,这对两者都是通用的。)
  • 当然,这可能更优雅,问题是“有没有更快更优雅的方法来解决这个问题?”。 :)
  • @MartinNyolt 如果最初对字符串进行了排序,那肯定会更快。排序是一个后备。
  • “后备”取决于预期的输入。在最坏的情况下,这会更慢。如果输入通常是随机顺序的(例如文件创建顺序),那么最坏的情况就是默认情况,你的算法肯定比“幼稚”的方法慢。
  • 非常感谢您的精彩回答
【解决方案2】:

您必须搜索列表中的每个字符串。但是,您不需要比较每个字符串中的所有字符。通用前缀只能变短,所以你只需要和“迄今为止的通用前缀”进行比较。我不认为这会改变 big-O 复杂性 - 但它会对实际速度产生很大影响。

此外,这些看起来像文件名。它们是否已排序(请记住,许多文件系统倾向于按排序顺序返回内容)?如果是这样,您只需要考虑第一个和最后一个元素。如果它们是可能大部分排序的,则考虑第一个和最后一个的公共前缀,然后遍历所有其他字符串,根据需要进一步缩短前缀。 p>

【讨论】:

  • 我认为大多数操作系统 API 调用不会按排序顺序返回文件名。 find / -maxdepth 1 | head 是我系统上的一个很好的例子。只有其他用户友好的命令(如 unix 中的 ls)才会排序。所以这取决于文件名的来源。 (并且排序字符串会比在未排序的字符串上找到公共前缀要慢。)
【解决方案3】:

你只需要遍历每个字符串。您只能通过利用前缀只能缩短这一事实来避免不必要地遍历字符串的全长:

#include <iostream>
#include <string>
#include <vector>

std::string common_prefix(const std::vector<std::string> &ss) {
    if (ss.empty())
        // no prefix
        return "";

    std::string prefix = ss[0];

    for (size_t i = 1; i < ss.size(); i++) {
        size_t c = 0; // index after which the string differ
        for (; c < prefix.length(); c++) {
            if (prefix[c] != ss[i][c]) {
                // strings differ from character c on
                break;
            }
        }

        if (c == 0)
            // no common prefix
            return "";

        // the prefix is only up to character c-1, so resize prefix
        prefix.resize(c);
    }

    return prefix;
}

void strip_common_prefix(std::vector<std::string> &ss) {
    std::string prefix = common_prefix(ss);
    if (prefix.empty())
        // no common prefix, nothing to do
        return;

    // drop the common part, which are always the first prefix.length() characters
    for (std::string &s: ss) {
        s = s.substr(prefix.length());
    }
}

int main()
{
    std::vector<std::string> ss { "/home/user/foo.txt", "/home/user/bar.txt", "/home/baz.txt"};
    strip_common_prefix(ss);
    for (std::string &s: ss)
        std::cout << s << "\n";
}

借鉴Martin Bonner's answer 的提示,如果您对输入有更多的先验知识,则可以实现更有效的算法。 特别是,如果您知道您的输入已排序,则只需比较第一个和最后一个字符串(请参阅Richard's answer)。

【讨论】:

  • 这是假设重复部分总是在字符串的开头
  • @slawekwin:我认为这就是 OP 所追求的。
  • 而不是prefix = prefix.substr(0, c);,使用prefix.resize(c)
  • std::mismatch也可以用来替换内循环。
  • 非常感谢
【解决方案4】:

i - 找到文件夹深度最小的文件(即 baz.txt) - 它的根路径是 home ii - 然后检查其他字符串,看看它们是否以该根开头。 iii - 如果是,则从所有字符串中删除根。

【讨论】:

    【解决方案5】:

    std::size_t index=0; 开头。扫描列表以查看该索引处的字符是否匹配(注意:末尾不匹配)。如果是,请推进索引并重复。

    完成后,索引将具有前缀长度的值。

    此时,我建议您编写或查找string_view 类型。如果这样做,只需为每个字符串 str 创建一个 string_view,并以 index, str.size() 开头/结尾。

    总成本:O(|prefix|*N+N),这也是确认你的答案正确的成本。

    如果您不想写 string_view,只需在向量中的每个 str 上调用 str.erase(str.begin(), str.begin()+index)

    总成本为O(|total string length|+N)。必须访问前缀以确认它,然后必须重写字符串的尾部。

    现在,广度优先的代价是局部性,因为您要在各处触及记忆。在实践中,分块进行可能会更有效,您可以在其中扫描前 K 个字符串直到长度为 Q 并找到公共前缀,然后将该公共前缀链接到下一个块。这不会改变 O 表示法,但会提高内存引用的局部性。

    【讨论】:

      【解决方案6】:
      for(vector<string>::iterator itr=V.begin(); itr!=V.end(); ++itr)
          itr->erase(0,6);
      

      【讨论】:

      • 这还不够笼统。问题是“喜欢如果我有……”。他正在寻找一种能够自动找到“/home/”部分的算法。您还应该解释您的答案,而不仅仅是提供代码。
      • 我不明白反对意见。这对我来说似乎是最直接的答案。 OP 说 all 集合中的字符串共享相同的前缀。先找到那个前缀的长度,然后做@Beta 做的事情,这是很合理的。
      • @MartinNyolt:我发帖后才看到你的评论。所以 OP 提出的真正问题是首先找到最长的公共前缀,然后将其砍掉?
      猜你喜欢
      • 2021-04-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-06-07
      • 1970-01-01
      • 2013-04-23
      • 2015-08-18
      • 2021-08-22
      相关资源
      最近更新 更多