Подтвердить что ты не робот

Вопрос о Cring String

Недавно я был в техническом интервью на С++, где мне был предоставлен немного простой код манипуляции строкой, который предназначен для ввода строки и возврата строки, состоящей из первого и последнего 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

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

  • Размещена проверка в случае, если размер строки s меньше n, возвращая false для отказа.

Предполагая, что разрешен только собственный С++, есть ли другой способ сделать это более эффективно или оптимально?

Примечание 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");
   }

}
4b9b3361

Ответ 1

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

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 libstdС++ строка, которая объявляет и инициализирует ret, чрезвычайно быстро, потому что libstdС++ использует глобальную переменную "пустая строка". Таким образом, это просто копия указателя. Вызовы begin и end на s также бывают быстрыми, потому что они будут разрешены для версий const begin и end, begin() const и end() const, поэтому внутреннее представление s равно не "просочился". С libstdС++ 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 С++ 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)

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

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

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

Одна вещь, интересная из результатов VС++, заключается в том, что быстрее использовать 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 (вариант строки данных) при использовании VС++:

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

Ответ 2

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

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

Ответ 3

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

Ответ 4

// 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).

Ответ 5

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

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

Ответ 6

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

ИЗМЕНИТЬ Поэтому я удалил другую. Это не обман.

Ответ 7

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

Итак, измените тесты и измените подпись.

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 меньшим или равным размеру строки. Я заменяю проверку предварительным условием для функции. Предпосылки быстрее, чем проверки: у них есть нулевые служебные данные.