C ++字符串访谈问题

时间:2010-12-20 23:08:18

标签: c++ optimization string processing-efficiency

我最近在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和rvalues尚不可用)

  • 删除了通过substr

  • 构建的临时字符串
  • 将输入字符串作为const引用传递,以便绕过输入的临时实例化

  • 修复了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");
   }

}

7 个答案:

答案 0 :(得分: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;
}

passes all three unit tests

使用GNU libstdc ++时,声明&amp;初始化ret非常快,因为libstdc ++使用全局“空字符串”变量。因此,它只是一个指针副本。 begin上的ends来电也很快,因为他们会解析为beginendbegin() const和{的常量版本{1}},因此end() const的内部表示不会“泄露”。使用libstdc ++,sstd::string::const_iterator,它是指针类型和随机访问迭代器。因此,当const char*调用std::distance以获得输入范围的长度时,它是指针差异操作。此外,std::string::append<const char*>(const char*, const char*)会产生std::string::append<const char*>(const char*, const char*)之类的内容。最后,memmove操作确保有足够的内存可用于返回值。

修改 对于好奇,这里是MinGW g ++ 4.5.0的汇编输出中reserve的初始化:

ret

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

<强> EDIT2: 好的。我现在用g ++ 4.5.0和Visual C ++ 16.00.30319.01测试了四个变种:

变体1(“c_str变体”):

    movl    $__ZNSs4_Rep20_S_empty_rep_storageE+12, (%ebx)

变式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_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;
}

变式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;
   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;
}

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

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);
}

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慢得多。 / p>

VC ++结果的一个有趣之处在于使用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; } 而不是c_str更快。也许这就是为什么你的面试官说有一种比你实施更快的方式。

<强> EDIT3:

实际上,我只考虑了另一种变体:

变式5:

data

它与变体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); 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; } 的开始和结束迭代器。

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

  • 变体1是最快的
  • 变体5是第二个(比变体1慢1.6%)
  • 变体2是第三个(比变体5慢1.4%)
  • 变体4是第三个(比变体2慢4%)
  • 变体3是第四个(比变体4慢17%)

答案 1 :(得分:3)

如果您不需要维护原始字符串的内容,则可以将最后n个字符复制到原始字符串的[n+1, 2n]位置,并将其截断为2n。如果字符串短于2n,则必须小心首先展开字符串并注意不要在写入字符串之前覆盖任何字符。

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

答案 2 :(得分:1)

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

答案 3 :(得分: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长度。)

答案 4 :(得分:0)

我唯一想到的是,如果只使用C以null结尾的字符串调用此函数,则可能需要为参数's'额外构造std :: string。

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

答案 5 :(得分: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;
}

修改 所以我删除了另一个。这不是骗子。

答案 6 :(得分: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小于或等于字符串大小的检查。我用该功能的前提条件替换了支票。前提条件比检查更快:它们的开销为零。