12

我最近参加了一次 C++ 技术面试,在那里我得到了一些简单的字符串操作代码,该代码旨在获取一个字符串并返回一个由第一个和最后一个 n 个字符组成的字符串,然后继续更正任何错误并且也使功能尽可能高效,我想出了下面的解决方案,但是面试官声称有一个更快更优化的方法:

原始代码:

std::string first_last_n(int n, std::string s)
{
   std::string first_n = s.substr(0,n);
   std::string last_n = s.substr(s.size()-n-1,n);
   return first_n + last_n;
}

我的代码:

bool first_last_n(const std::size_t& n, const std::string& s, std::string& r)
{
   if (s.size() < n)
      return false;
   r.reserve(2 * n);
   r.resize(0);
   r.append(s.data(),s.data() + n);
   r.append(s.data() + (s.size() - n), s.data() + s.size());
   return true;
}

我的变化总结:

  • 更改了接口以将返回字符串作为参考(假设 RVO 和右值尚不可用)

  • 删除了通过 substr 构造的临时字符串

  • 将输入字符串作为常量引用传递,以绕过输入的临时实例化

  • 修复了 last_n 字符串中的 off-by-1 错误

  • 将每个角色的触地次数减少到一次或两次(在重叠场景的情况下)

  • 在字符串 s 的大小小于 n 的情况下进行检查,失败返回 false。

假设只允许使用本机 C++,是否有其他方法可以更有效或更优化地完成上述操作?

注 1:原始输入字符串实例不可修改。

注2:所有解决方案必须通过以下测试用例,否则无效。

void test()
{
   {
      std::string s = "0123456789";
      std::string r = first_last_n(10,s);
      assert(r == "01234567890123456789");
   }

   {
      std::string s = "0123456789ABC0123456789";
      std::string r = first_last_n(10,s);
      assert(r == "01234567890123456789");
   }

   {
      std::string s = "1234321";
      std::string r = first_last_n(5,s);
      assert(r == "1234334321");
   }

}
4

7 回答 7

6

这个实现应该很快:

inline std::string first_last_n(std::string::size_type n, const std::string& s)
{
    n = std::min(n, s.size());
    std::string ret;
    ret.reserve(2*n);
    ret.append(s.begin(), s.begin() + n);
    ret.append(s.end() - n, s.end());
    return ret;
}

通过了所有三个单元测试

使用 GNU libstdc++ 时,声明和初始化的行ret非常快,因为 libstdc++ 使用全局“空字符串”变量。因此,它只是一个指针副本。调用beginand endons也很快,因为它们会解析为beginand 和and的 const 版本end,因此内部表示不会“泄露”。使用 libstdc++,is是一个指针类型和随机访问迭代器。因此,当调用获取输入范围的长度时,就是指针差分操作。此外,结果类似于. 最后,该操作确保有足够的内存可用于返回值。begin() constend() constsstd::string::const_iteratorconst char*std::string::append<const char*>(const char*, const char*)std::distancestd::string::append<const char*>(const char*, const char*)memmovereserve

编辑: 出于好奇,这里是retMinGW g++ 4.5.0 的程序集输出中的初始化:

    movl    $__ZNSs4_Rep20_S_empty_rep_storageE+12, (%ebx)

它只是将指针复制到全局“空表示”。

EDIT2: 好的。我现在已经使用 g++ 4.5.0 和 Visual C++ 16.00.30319.01 测试了四个变体:

变体 1(“c_str 变体”):

inline std::string first_last_n(std::string::size_type n, const std::string& s)
{
   std::string::size_type s_size = s.size();
   n = std::min(n, s_size);
   std::string ret;
   ret.reserve(2*n);
   const char *s_cStr = s.c_str(), *s_cStr_end = s_cStr + s_size;
   ret.append(s_cStr, s_cStr + n);
   ret.append(s_cStr_end - n, s_cStr_end);
   return ret;
}

变体 2(“数据字符串”变体):

inline std::string first_last_n(std::string::size_type n, const std::string& s)
{
   std::string::size_type s_size = s.size();
   n = std::min(n, s_size);
   std::string ret;
   ret.reserve(2*n);
   const char *s_data = s.data(), *s_data_end = s_data + s_size;
   ret.append(s_data, s_data + n);
   ret.append(s_data_end - n, s_data_end);
   return ret;
}

变体 3:

inline std::string first_last_n(std::string::size_type n, const std::string& s)
{
   std::string::size_type s_size = s.size();
   n = std::min(n, s_size);
   std::string ret(s);
   std::string::size_type d = s_size - n;
   return ret.replace(n, d, s, d, n);
}

变体 4(我的原始代码):

inline std::string first_last_n(std::string::size_type n, const std::string& s)
{
   n = std::min(n, s.size());
   std::string ret;
   ret.reserve(2*n);
   ret.append(s.begin(), s.begin() + n);
   ret.append(s.end() - n, s.end());
   return ret;
}

g++ 4.5.0 的结果是:

  • 变体 4 是最快的
  • 变体 3 次之(比变体 4 慢 5%)
  • 变体 1 排在第三位(比变体 3 慢 2%)
  • 变体 2 是第四个(比变体 1 慢 0.2%)

VC++ 16.00.30319.01 的结果是:

  • 变体 1 是最快的
  • 变体 2 次之(比变体 1 慢 3%)
  • 变体 4 排在第三位(比变体 2 慢 4%)
  • 变体 3 是第四个(比变体 4 慢 17%)

不出所料,最快的变体取决于编译器。但是,不知道会使用哪个编译器,我认为我的变体是最好的,因为它是 C++ 的熟悉风格,使用 g++ 时速度最快,使用 VC++ 时也不比变体 1 或 2 慢多少。

VC++ 结果中有趣的一件事是使用c_str而不是data更快。也许这就是为什么你的面试官说有比你的实施更快的方法。

编辑3:

实际上,我只是想到了另一种变体:

变体 5:

inline std::string first_last_n(std::string::size_type n, const std::string& s)
{
   n = std::min(n, s.size());
   std::string ret;
   ret.reserve(2*n);
   std::string::const_iterator s_begin = s.begin(), s_end = s.end();
   ret.append(s_begin, s_begin + n);
   ret.append(s_end - n, s_end);
   return ret;
}

它就像变体 4 一样,只是s保存了 for 的开始和结束迭代器。

在测试变体 5 时,它实际上在使用 VC++ 时击败了变体 2(数据字符串变体):

  • 变体 1 是最快的
  • 变体 5 次之(比变体 1 慢 1.6%)
  • 变体 2 排在第三位(比变体 5 慢 1.4%)
  • 变体 4 排在第三位(比变体 2 慢 4%)
  • 变体 3 是第四个(比变体 4 慢 17%)
于 2010-12-21T00:15:20.073 回答
3

如果您不需要维护原始字符串的内容,那么您可以将最后 n 个字符复制到[n+1, 2n]原始字符串的位置并在2n. 您必须小心首先扩展字符串,如果字符串短于2n.

这将使构造字符串的操作数量减半,并且无需创建新字符串。所以理论上它的速度要快 2 到 4 倍。但是当然你刚刚破坏了原始字符串,你必须询问面试官是否可以接受。

于 2010-12-20T23:14:59.110 回答
1
// compiled with cl /Ox first_last_n.cpp /W4 /EHsc

inline void
first_last_n2(string::size_type n, const std::string &s, string &out)  // method 2
{
  // check against degenerate input
  assert(n > 0);
  assert(n <= s.size());

  out.reserve(2*n);
  out.assign(s, 0, n);
  out.append(s, s.size()-n, n);
}

时间:

method 1:  // original method
2.281
method 2:  // my method
0.687
method 3:  // your code.
0.782

注意:计时专门测试“长”字符串。即不使用短字符串优化的那些。(我的字符串长度为 100)。

于 2010-12-21T00:50:58.293 回答
1

如何删除中间的 N-2n 个字符,其中 N 是源字符串的长度?

于 2010-12-21T00:38:34.170 回答
0

我唯一的想法是,如果仅使用 C 空终止字符串调用此函数,则您可能需要为参数“s”额外构造 std::string。

可能“更”有效的方法是允许传入 std::string 或 const char *s。

于 2010-12-20T23:32:50.077 回答
0

如果您必须通过测试,那么您将不得不编写低效的代码,因为您必须返回一个字符串的副本。这意味着您必须使用动态分配,可能因为副本而多次使用。

所以更改测试并更改签名。

template<class Out>
Out first_last_n(const std::string::size_type& n, const std::string& s, Out r)
{
    r = copy_n(s.begin(), n, r);
    std::string::const_iterator pos(s.end());
    std::advance(pos, -n);
    return copy_n(pos, n, r);
}

然后像这样调用它:

std::string s("Hello world!");
char r[5];
r[4] = 0;
first_last_n(2, s, r);

这允许您使用动态编程,并且它消除了函数中动态分配的需要。

我喜欢我的算法极简主义,我故意消除了n小于或等于字符串大小的检查。我用该功能的先决条件替换检查。先决条件比检查更快:它们的开销为零。

于 2010-12-21T00:35:32.417 回答
0

Memcpy 是骗子?

#include <cstring>
#include <iostream>
#include <string>

std::string first_last_n(int n, const std::string& s)
{
  if (s.size() < n)
      return "";

    char str[n*2];
    memcpy(str, s.data(), n);
    memcpy(str+n, s.data() + s.size()-n, n);

    return (const char *)str;
}

int main()
{
    std::cout << first_last_n(2, "123454321") << std::endl;
}

编辑 所以我删除了另一个。这不是作弊。

于 2010-12-20T23:53:40.153 回答