单次通过搜索和替换

Single-pass search & replace

本文关键字:替换 搜索 单次通      更新时间:2023-10-16

有人知道如何进行单程搜索&替换为文本?我正在开发一个高性能程序,其中每一个微观优化都很重要。下面是一个例子,说明了我目前所做的事情:

#include <iostream>
#include <string>
/*!
brief Replaces all common character escape sequences with text representations
note Some character seqences messes up the trace output and must be replaces
* with text represantions, ie. the newline character will the replaced with "n"
* etc.
returns The formatted string
*/
std::wstring ReplaceAll(std::wstring &str)
{
    SearchAndReplace(str, L"a", L"\a");   // control-g, C-g
    SearchAndReplace(str, L"b", L"\b");   // backspace, <BS>, C-h
    SearchAndReplace(str, L"t", L"\t");   // tab, <TAB>, C-i
    SearchAndReplace(str, L"n", L"\n");   // newline, C-j
    SearchAndReplace(str, L"v", L"\v");   // vertical tab, C-k
    SearchAndReplace(str, L"f", L"\f");   // formfeed character, C-l
    SearchAndReplace(str, L"r", L"\r");   // carriage return, <RET>, C-m
    return str;
}
/*!
brief Wide string search and replace
param str [in, out] String to search & replace
param oldStr [in] Old string
param newStr [in] New string
*/
std::wstring SearchAndReplace(std::wstring &str, const wchar_t *oldStr, const wchar_t *newStr) const
{
    size_t oldStrLen = wcslen(oldStr);
    size_t newStrLen = wcslen(newStr);
    size_t pos = 0;
    while((pos = str.find(oldStr, pos)) != string::npos)
    {
        str.replace(pos, oldStrLen, newStr);
        pos += newStrLen;
    }
    return str;
}
int main()
{
    std::wstring myStr(L"tThe quick brown fox jumps over the lazy dog.ntThe quick brown fox jumps over the lazy dognn");
    std::wcout << L"Before replace: " << myStr;
    std::wcout << L"After replace: " << ReplaceAll(myStr);
    return 0;
}

上面的代码显然效率很低,因为它需要多次通过同一个字符串。单程搜索&replace函数应该非常灵活,可以处理不同的字符数组来替换(即,不仅仅是ReplaceAll()中列出的转义字符)。

您可以使用哈希表来存储所有对<from,to>,并在字符串上运行一次。

对于每个字符,检查它是否存在于哈希表中,如果存在,则替换它

它将一次性完成任务。

对于手头的任务,您不需要任何复杂的算法!首先,您搜索要替换的"字符串"实际上是字符和不同的字符(另一个回复中提到的更复杂的算法是处理与序列匹配的字符串列表)。此外,你的主要问题是你一直在调整你的序列大小。无论如何,你都不能在适当的位置进行替换,因为字符串会随着每次替换而增长。一个相当简单的方法应该比您当前的方法有更好的性能,而且,据我所知,您离启动微优化还有很长的路要走,您需要首先让代码以大致正确的方式执行操作。例如,我会尝试一些很长的东西:

struct match_first
{
    wchar_t d_c;
    match_first(wchar_t c): d_c(c) {}
    template <typename P>
    bool operator()(P const& p) const { return p.first == this->d_c; }
};
void Replace(std::wstring& value)
{
    std::wstring result;
    result.reserve(value.size());
    std::wstring special(L"abfnrtv");
    std::pair<wchar_t, std::wstring> const replacements[] = {
        std::pair<wchar_t, std::wstring>(L'a', L"\a"),
        std::pair<wchar_t, std::wstring>(L'b', L"\b"),
        std::pair<wchar_t, std::wstring>(L'f', L"\f"),
        std::pair<wchar_t, std::wstring>(L'n', L"\n"),
        std::pair<wchar_t, std::wstring>(L'r', L"\r"),
        std::pair<wchar_t, std::wstring>(L't', L"\t"),
        std::pair<wchar_t, std::wstring>(L'v', L"\v")
    };
    std::wstring::size_type cur(0);
    for (std::wstring::size_type found(cur);
         std::wstring::npos != (found = value.find_first_of(special, cur));
         cur = found + 1) {
        result.insert(result.end(),
                      value.begin() + cur, value.begin() + found);
        std::pair<wchar_t, std::wstring> const* replacement
            = std::find_if(std::begin(replacements), std::end(replacements),
                           match_first(value[found]));
        result.insert(result.end(),
                      replacement->second.begin(), replacement->second.end());
    }
    result.insert(result.end(), value.begin() + cur, value.end());
    value.swap(result);
}

该算法的思想是对源字符串进行一次遍历,找到所有需要替换的字符串,如果找到,则将不需要替换的字符和替换字符串的部分复制到正在构建的新字符串中。有一些东西可以通过一些努力变得更快,但这一个只移动每个字符一次,而不是原始代码,原始代码不断地将没有看到的字符的尾部向前移动一个字符,每个找到的字符都要被替换。

有许多算法是为在线性时间内执行字符串搜索而设计的。尽管它们大多是字符串搜索算法,但您可以实现它们来在线性时间内执行字符串搜索和替换。它们的线性运行时间需要一些预处理,请务必仔细阅读的预处理条件。列表:

  • KPM字符串搜索算法O(n)
  • Rabin-Karth算法O(nm),然而这是一个非常好的平均/最佳情况

要使用这些字符串搜索算法来实现搜索和替换算法,您需要执行以下操作(注意,我所做的分析是专门针对KMP的):

  1. 使用KMP查找给定单词在句子中的所有出现(跟踪它们的起始索引和找到的单词)。将这些存储在哈希图中,为我们提供不断的查找和插入
  2. 创建一个新的动态字符串(考虑一个动态数组ADT)
  3. 在句子中的每个字符和每个位置i上迭代,检查该位置是否在哈希图中。如果是,请找到单词的相应替换项(存储在另一个hashmap-O(1)查找中),并一次替换一个字符,直到j-i(其中j是您的下一个位置)大于替换项的字符串长度减1,然后继续并重复,直到您对整个句子进行了迭代

备注:对于步骤3,您将在逐个迭代时将字符复制到新字符串中。如果您找到一个单词,则复制替换并跳过k位置,其中k是匹配单词的字符串长度。

最后:释放与原始字符串相关的任何内存,然后返回新字符串或将指针设置为等于新字符串。

最终,这应该是2 * sum[i = 1 to n] of O(1),因此是线性时间。

搜索和替换的主要问题是,尝试就地执行通常效率很低。创建一个新字符串是O(n)时间和空间;用一个合适的替代品很难做到这一点。

可以在字符串上进行两次传递;第一个只是计算结果的长度(或者可能构建一个散点聚集列表)。完成后,如果需要,可以调整字符串的大小,然后可以进行替换过程,从字符串的末尾开始,一直到开头。然而,根据我的经验,这是一个非常少价值的大量编码。(此外,如果某些替换是删除的,则不起作用。)

因此,我会使用以下内容(它使用C++11 lambdas,只是因为),尽管这是次优的:最好是对替换向量进行排序,以便可以使用二进制搜索,或者——在替换控制字符的情况下——将它们放入由目标字符索引的向量中(具有最小值和最大值),以便查找只需要两次比较。(或者,您可以构建一个只需要一次比较的压缩表,但这也是一项艰巨的工作。)

#include <algorithm>
#include <initializer_list>
#include <string>
#include <utility>
#include <vector>
template<typename Char, typename String=std::basic_string<Char>>
class Translator {
  public:
    Translator(const std::initializer_list<std::pair<Char, String>> trans) {
      std::for_each(trans.begin(), trans.end(),
                    [&](const std::pair<Char, String>& fromto) {
                    from_.push_back(fromto.first);
                    to_.push_back(fromto.second);
                    });
    }
    void push_translated(String& res, Char ch) { 
      size_t pos = from_.find(ch);
      if (pos == String::npos) res += ch; else res += to_[pos];
    }
    String translate(const String& orig) {
      String rv;
      std::for_each(orig.begin(), orig.end(), [&](Char ch){push_translated(rv, ch);});
      return rv;
    }
  private:
    String from_;
    std::vector<String> to_;
};

上面使用的初始值设定项列表很可爱,但也应该有一个构造函数,它采用成对的std::mapstd::vector或类似的类型,因为有时您希望在运行时而不是编译时构建替换。

如果你想使用上面的代码,这里有一个简单的驱动程序(对于你的应用程序,你可能想要Translator<wchar_t>):

#include <iostream>
int main(int argc, char** argv) {
  Translator<char> trans{
    {'a', "\a"},
    {'b', "\b"},
    {'f', "\f"},
    {'n', "\n"},
    {'r', "\r"},
    {'t', "\t"},
    {'v', "\v"}
  };
  for (int i = 1; i < argc; ++i) {
    std::cout << trans.translate(argv[i]) << std::endl;
  }
  return 0;
}

如果您正在寻找更好的性能,您的代码可能会因为以下行而效率低下:

str.replace(位置、旧StrLen、新str);

这可能导致:

  1. 字符串重新分配(如果新字符串比旧字符串长)
  2. 内存移动操作(如果新字符串比旧字符串短或长,则它之后的所有字符都将移动到新位置)

愚蠢的STL实现甚至可以为每次替换动态分配缓冲区。内存分配可能很慢,很容易变成瓶颈。

将输入和输出字符串/缓冲区分开,并将输出缓冲区预分配为比输入缓冲区大的字符串,可能会更有效。如果您将输入和输出字符串分开,则可以保证您的程序将复制inputString.size()字符,并且只有一个初始内存分配(可预测的性能)。如果将字符替换到位,则可能不会移动任何字符,也不会发生重新分配,而且每次替换时,字符串中的每个字符都可能被多次移动(更难预测性能),并且会多次调用new/delete。

更换可以这样做:

  1. 预分配大于输入字符串的空输出字符串(使用reserve()
  2. 重复,直到字符用完:
    1.1读几个字
    2.2.看看它们是否与你想要更换的东西相匹配。(如果将可替换字符存储在map/hashtable中,查找速度可能为o(logn)或o(1),则可能非常快)
    3.3.如果它们与您想要替换的内容相匹配,请编写新的字符串。如果没有,则不更改字符

另请参见马尔可夫链