C ++ String Интервью Вопрос - PullRequest
12 голосов
/ 21 декабря 2010

Недавно я участвовал в техническом интервью на 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 и r еще не доступны)

  • Удалены временные строки, создаваемые через substr

  • Передала входную строку в качестве константного ссылочного порядка, чтобы обойти временную реализацию ввода

  • Исправлена ​​ошибка off-1 в строке last_n

  • Уменьшено количество раз, когда каждый персонаж приземляется до одного или двух раз (в случае перекрывающегося сценария)

  • Установлена ​​проверка в случае, если размер строки 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 ]

6 голосов
/ 21 декабря 2010

Эта реализация должна быть быстрой:

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 ++ использует глобальную переменную «пустая строка». Таким образом, это просто копия указателя. Вызовы begin и end на s также бывают быстрыми, поскольку они преобразуются в постоянные версии begin и end, begin() const и end() const, поэтому внутреннее представление s не "утечка". В libstdc ++ std::string::const_iterator равен const char*, который является типом указателя и итератором произвольного доступа. Таким образом, когда std::string::append<const char*>(const char*, const char*) вызывает std::distance для получения длины входного диапазона, это операция разности указателей. Кроме того, std::string::append<const char*>(const char*, const char*) приводит к чему-то вроде memmove. Наконец, операция reserve обеспечивает наличие достаточного объема памяти для возвращаемого значения.

EDIT: Для любопытных вот инициализация ret в выводе сборки MinGW 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 - второй (на 5% медленнее, чем вариант 4)
  • Вариант 1 третий (на 2% медленнее, чем вариант 3)
  • Вариант 2 четвертый (на 0,2% медленнее, чем вариант 1)

Результаты для VC ++ 16.00.30319.01:

  • Вариант 1 самый быстрый
  • Вариант 2 - второй (на 3% медленнее, чем вариант 1)
  • Вариант 4 третий (на 4% медленнее, чем вариант 2)
  • Вариант 3 - четвертый (на 17% медленнее, чем вариант 4)

Неудивительно, что самый быстрый вариант зависит от компилятора. Однако, не зная, какой компилятор будет использоваться, я думаю, что мой вариант лучше, потому что это знакомый стиль C ++, он самый быстрый при использовании g ++, и он не намного медленнее, чем варианты 1 или 2 при использовании VC ++.

Из результатов VC ++ интересно то, что использование c_str вместо data быстрее. Возможно, именно поэтому ваш интервьюер сказал, что есть более быстрый способ, чем ваша реализация.

EDIT3:

Собственно, я просто подумал о другом варианте:

Вариант 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 сохранены.

Когда проверяется вариант 5, он на самом деле превосходит вариант 2 (вариант строки данных) при использовании VC ++:

  • Вариант 1 самый быстрый
  • Вариант 5 - второй (на 1,6% медленнее, чем вариант 1)
  • Вариант 2 третий (на 1,4% медленнее, чем вариант 5)
  • Вариант 4 - третий (на 4% медленнее, чем вариант 2)
  • Вариант 3 - четвертый (на 17% медленнее, чем вариант 4)
3 голосов
/ 21 декабря 2010

Если вам не нужно сохранять содержимое исходной строки, вы можете скопировать последние n символов в позиции [n+1, 2n] исходной строки и обрезать ее до 2n.Вы должны быть осторожны, чтобы сначала раскрыть строку, а также стараться не перезаписывать какие-либо символы перед записью в них, если строка короче 2n.

Это уменьшит вдвое количество операций для построенияСтрока, а также удалить необходимость создания новой строки.Так что теоретически это в 2-4 раза быстрее.Но, конечно, вы только что уничтожили оригинальную строку, которую вы должны спросить у интервьюера, если она приемлема.

1 голос
/ 21 декабря 2010
// 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);
}

Times:

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

Примечание: синхронизация специально проверяет "длинные" строки. То есть те, где оптимизация коротких строк не используется. (Мои строки были длиной 100).

1 голос
/ 21 декабря 2010

Как насчет удаления средних N-2n символов, где N - длина исходной строки?

0 голосов
/ 21 декабря 2010

Если вы должны пройти тесты, вам придется писать неэффективный код, потому что вы должны вернуть копию строки.Это означает, что вы должны использовать динамическое размещение, возможно, несколько раз из-за копии.

Поэтому измените тесты и измените подпись.

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

Затем назовите это так:1007 *

Это позволяет использовать динамическое программирование и устраняет необходимость динамического выделения в функции.

Мне нравятся мои минималистичные алгоритмы, и я специально исключил проверку того, что n меньше илиравен размеру строки.Я заменяю проверку предварительным условием для функции.Предварительные условия выполняются быстрее, чем проверки: они не требуют дополнительных затрат.

0 голосов
/ 21 декабря 2010

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

EDIT Так что я удалил другой. Это не чит.

0 голосов
/ 21 декабря 2010

Я думал только о том, что если эта функция вызывается только со строками с нулевым символом в конце, вам может потребоваться дополнительная конструкция std :: string для параметра 's'.

Возможно, болееЭффективный метод должен позволять передавать либо std :: string, либо const char * s.

...