Украинское сообщество программистов

Трудный вопрос на собеседовании #4

Александр Скакунов
Опубликовано 23.06.2008 в Статьи

Каким образом можно циклично сдвинуть строку длиной n на m символов при константной дополнительной памяти за линейное время?

UPDATE: ответ.

Теги: ,

1 звезда2 звезды3 звезды4 звезды5 звезд (13 голосов, средний: 2.15 из 5)
Загрузка ... Загрузка ...
Распределение голосов

Понравилась статья? Подпишись на обновления по RSS/E-mail

Подписаться, не оставляя комментарий

Все комментарии (54) к “Трудный вопрос на собеседовании #4” RSS

  1. 1mdm

    Псевдокод:

    shift = m % n;
    str = str.substr(shift) + str.substr(0, shift-1);

    Сначала вычисляем на сколько символов нужно реально сдвинуть (если вдруг m > n),
    а затем меняем местами части строки: start..(shift-1) shift..end.

    Вроде так : )

  2. 1mdm

    Символы «съелись». Короче, свопаем вот так: swap(start..(shift-1), shift..end), где «..» это диапазон.

  3. Скакунов Александр

    Задачка сама по себе не очень сложная – сделать из “колбаса” “басакол”.

    str = str.substr(shift) + str.substr(0, shift-1);

    А вот решить ее, соблюдая ограничения – уже труднее:

    при константной дополнительной памяти за линейное время

  4. Скакунов Александр

    Кстати, чтобы код был секси, надо заключать его в теги pre+code.

  5. wheleph

    В некотором смысле, эта задача есть частным случаем предыдущей.

  6. 1mdm

    Условие «за линейное время» в моём решение соблюдено.
    А условие «при константной доп. памяти» решается копированием символов
    из первой части строки в конец второй части с использованием фиксированного
    по размеру буфера. Если буфер будет равен одному символу, то мы решаем эту
    задачу на O(m) времени.

  7. Всеволод Дёмкин

    2 1mdm:
    мне кажется, вы не совсем правильно поняли, что значит “при константной доп. памяти”. :)
    Константной должна быть вся память, которая понадобится для решения задачи, не считая исходной строки, т.е. у нас есть n байт + k байт, где k не зависит от m и n. Если “копировать символы
    из первой части строки в конец второй части”, то будет дополнительно использованно m байт, так ведь?

  8. Igor

    2 1mdm

    А мне кажется что этот код не будет работать вообще

  9. 1mdm

    А ведь-таки так! Тогда нужно с начала строки и до shift-1 копировать символы в буфер размером k байт, затем сдвигать всю строку влево на k байт и символы из буфера копировать в конец строки. Или наоборот.

  10. 1mdm

    2 Igor
    О каком коде вы говорите? В первом комментарии я писал псевдокод.

  11. Всеволод Дёмкин

    2 1mdm:
    тогда не получится линейного времени, поскольку на каждое копирование у нас уйдет k операций, на сдвиг еще (n-k) и на обратное копирование — снова k. Итого: n+k. А сделать это нужно будет m/k раз. Т.е. выходит, если я не ошибаюсь, квадратичное время выполнения: (n+k)*m/k…

  12. 1mdm

    2 Всеволод
    Наверное вы правы.
    Тогда уместно будет использовать XOR для перестановки соответствующих символов в строке. Время будет = O(m/k).

  13. Olexiy

    Класна задача, раніше такої не бачив.
    Будемо вважати, що зсуваємо вліво.
    Яка ідея придумалась:
    Йдемо зліва направо, і обмінюємо символ з номером і на символ з номером і + m, допоки не дійдемо до моменту, коли цього зробити не можна, бо і + m вилазить за межі.
    Наприклад, зсуваємо рядок “abcdefg” на 3 символи вліво:
    abcdefg -> dbcaefg -> decabfg -> defabcg -> defgbca

    Тепер запускаємо рекурсивно ту саму операцію на хвості, але з іншим m (в даному випадку – зсуваємо bca на два символи вліво). Якщо тепер акуратно реалізувати, то виходить:

    void doMove(string& s, int start, int m) {
    m %= (s.length() – start);
    if (m == 0)
    return;
    int last = s.length() – m;
    for (int i = start; i < last; i++)
    swap(s[i], s[i + m]);
    doMove(s, last, m – ((i – start) % m));
    }

    На д/з лишаємо доведення того, що час виконання буде лінійним.

    ПС
    Дана реалізація, насправді, теоретично може жерти лінійну пам’ять, якщо підібрати правильні Н та М (питання – які? :) через те, що траплятиметься забагато вкладених викликів методу, але цей самий метод можна легко переписати ітеративно, а не рекурсивно.

  14. Olexiy

    2 mdm
    >> Тогда нужно с начала строки и до shift-1 копировать символы в буфер размером kбайт, затем сдвигать всю строку влево на k байт и символы из буфера копировать в конец строки.

    Так а що робити, якщо вони не влізуть в буфер розміром к байт, бо к менше, ніж shift?

  15. Olexiy


    void doMove(string& s, int start, int m) {
    m %= (s.length() - start);
    if (m == 0)
    return;
    int last = s.length() - m;
    for (int i = start; i < last; i++)
    swap(s[i], s[i + m]);
    doMove(s, last, m - ((i - start) % m));
    }

  16. Olexiy


    void doMove(string& s, int start, int m) {
    m %= (s.length() - start);
    if (m == 0)
    return;
    int last = s.length() - m;
    for (int i = start; i < last; i++)
    swap(s[i], s[i + m]);
    doMove(s, last, m - ((i - start) % m));
    }

  17. Olexiy

    >> Кстати, чтобы код был секси, надо заключать его в теги pre+code

    Щось ніяк не виходить.

  18. Всеволод Дёмкин

    Пока m < n(i)/2, меняем местами первые и последние m символов подстроки s(i). Это требует у нас m операций swap.
    s(0) = s
    s(i) = substr(s, m)
    Таких замен нам понадобится выполнить n/m-1. Т.е., в общем, n-m замен.
    Послдений сдвиг будет, когда m = n(i).

    Короче, в целом, нам нужно < n-m+m+1 = n+1 замен и 1 байт дополнительной памяти.

  19. Всеволод Дёмкин

    Забыл дописать, что когда m становится больше n(i), делаем сдвиг только уже в обратную сторону на n(i)-m.

  20. Щетинин Сергей

    @4, не поможет ли code { white-space: pre } ? Если это портит малину для кода внутри строки, то можно добавлять pre автоматически если внутри code есть переносы строки. Ану орлы, бегом задачку решите, не только ж байты переставлять. =))

  21. Vlad


    char *shiftStr(char *str, size_t m, size_t n)
    {
    size_t i, j;
    size_t guard = 0;

    if (m > n)
    m %= n;

    for (i = 0; i < m && guard < n; ++i) {
    char tmp = str[i];
    for (j = i+m; j != i && guard < n;) {
    char tmp_inner = str[j];
    str[j] = tmp;
    tmp = tmp_inner;
    j = (j + m < n ? j + m : m - (n - j));
    ++guard;
    }
    str[j] = tmp;
    ++guard;
    }

    return str;
    }

    Автар, либо показывай красивое решение, либо получаешь одну звезду. И вопрос как для собеседование плоховат, если не знать сразу со всеми подводными камнями не напишеш…

  22. 1mdm

    2 Vlad
    Условие if (m > n) избыточно.

  23. Pro100Oleh

    Сдвиг вправо для C#:

    public static string Shift(string s, int m)
    {
    char[] input = s.ToCharArray();
    int n = s.Length;
    int k = [Наибольший общий делитель](n, m);
    int count = n / k;
    for (int shift = 0; shift < k; shift++)
    {
    char old = input[shift];
    for (int index = 1; index < count; index++)
    {
    int currentIndex = (shift + index * m) % n;
    char tmp = input[currentIndex];
    input[currentIndex] = old;
    old = tmp;
    }
    input[shift] = old;
    }
    return new string(input);
    }

  24. Pro100Oleh

    Опоздал, Vlad уже опубликовал :(

  25. Olexiy

    2 Vlad
    А що твій код робить? Можеш словами пояснити?

  26. Vlad

    2 1mdm
    if (m < n) это защита от дурака. Много сэкономим если кто-то решит сдвинуть 100-символьную строку на 876345 символов :)

  27. Olexiy

    if (m > n) m %= n;
    еквівалентно
    m %= n;

  28. Olexiy

    Принаймні при додатніх m :)

  29. Vlad

    2 Pro100Oleh

    > int k = [Наибольший общий делитель](n, m);

    Может я что-то забыл, но нахождение GCD вроде как в лучшем случае O(log n), тоесть Ваш алгоритм вобщем – O(n log n). Правда ума не приложу зачем Вам GCD…

  30. Vlad

    O(n + log n) конечно же. Можно отбросить логарифм, как некоторое число, значительно меньшее чем собственно сдвиг линейной сложности, но все же ума не приложу зачем ждесь GCD :)

  31. Olexiy

    2 Vlad
    Він розбиває перестановку букв на незалежні циклічні перестановки, і переставляє кожну таку циклічну перестановку окремо (красиво, до речі, вийшло :)

  32. Vlad


    void doMove(string& s, int start, int m) {
    m %= (s.length() - start);
    if (m == 0)
    return;
    int last = s.length() - m;
    for (int i = start; i < last; i++)
    swap(s[i], s[i + m]);
    doMove(s, last, m - ((i - start) % m));
    }

    Вас не смущает, что переменная i используется за пределами ее области видимости?

  33. Olexiy

    2 Влад
    А, вірно, треба замінити на
    doMove(s, last, m – ((i – start) % m));
    на
    doMove(s, last, m – ((last – start) % m));

    Хоча в сьомій студії таке працювало, коли я тестува (змінна і зберігає значення, якого вона набула в циклі, а з областями видимості у них проблеми)

  34. biltar

    Для кожного елемента(на позиції i) копіюємо його в j =((i+m) mod n), а елемент який був на цьому місці запам’ятовуємо і вставляємо його в позицію яку обраховуємо за тою ж формулою де i:=j(присвоюємо), якщо ж попадаємо на клітинку де вже забрали елемент і ще нічого не поставили, то i:=(i+1)mod n.
    Позиції з яких забрали елемент і ще не поставили можна просто очищати.

  35. Vlad

    >Хоча в сьомій студії таке працювало, коли я тестува (змінна і зберігає значення, якого вона набула в циклі, а з областями видимості у них проблеми)

    А если между циклом и рекурсивным вызовом добавить еще какой-нибудь вызов процедуры, что-бы перезаписать эту область стека все равно работать будет? Не имею возможности в Линуксе проверить это в какой либо из студий…

  36. biltar

    Маленька поправка, якщо m і n взаємно прості, то получиться лінійний час, в іншому випадку мабіть трохи більше.

  37. Olexiy

    2 Влад
    Не знаю, думаю, що просто глюки з областями видимості.

  38. Евгений

    Вот версия без вложенных циклов и рекурсий


    void move(std::string& str, int n)
    {
    const int m=str.length();
    int pos=0, start=0;
    char ch=str[0];
    for(int i=0;i<m;i++)
    {
    pos=(pos+n)%m;
    std::swap(str[pos], ch);
    if(pos==start)
    {
    pos=start+=1;
    ch=str[start];
    }
    }
    }

  39. Евгений

    Правда у Vladа алгоритм тоже вроде как линейный. По крайней мере количество перестановок при любом n одинаково.

  40. Vlad

    Евгений, Ваш компилятор не ругается на всякие “const int m=str.length();”?

  41. Vlad

    Правда у Vladа алгоритм тоже вроде как линейный. По крайней мере количество перестановок при любом n одинаково.

    Если количество перестановок было одинаково при любом n, то алгоритм был бы с константной сложностью а не линейной ;)

  42. Евгений

    Я имел ввиду, что количество обменов зависит от длины строки, а не от велечины сдвига.
    А чем конструкция “const int m=str.length();” плоха? Я специально ее и вводил, чтобы если ненароком захочу изменить m, то компилятор выдал бы мне предупреждение. Стараюсь стремиться к безопасному программированию так сказать…

  43. Евгений

    Vlad, а Ваш варнинг выдал?

  44. Vlad

    Евгений, для Вас есть ЛегкийВопросНаСобеседовании :) Даже 2. Результат какого типа возращиет std::string::length() и на каких системах int не совпадает по размерам с этим типом?

  45. Евгений

    Ага, возвращает std::string::size_type или size_t. Исправлюсь.
    А по второму вопросу, анализ хидерных файлов студии показал что это тип unsigned. Как по ГОСТУ, я к сожалению не знаю

  46. the que

    Остання спроба _http://img57.imageshack.us/img57/8567/codewh0.png

  47. sudres

    при буфере в 1 char, время выполнения = O(n-(m/2)):


    public static void main(String[] args) {
    char[] str = {'0','1','2','3','4','5','6','7','8','9'};
    int n = str.length;
    int m=4;

    if (n/2>=m) {
    for(int i=0;i=0;i--){
    swap(str,i,((n-m)+(i%m)));
    }
    }
    System.err.println(Arrays.toString(str));

    }

    static void swap(char[] str, int i1, int i2 ){
    char t = str[i1];
    str[i1] = str[i2];
    str[i2] = t;
    }

  48. sudres

    кудато else прапало при посте

    } else {
    m = n-m;
    for(int i=n-(m/2)-1;i>=0;i--){
    swap(str,i,((n-m)+(i%m)));
    }
    }

  49. raggzy

    Ну, наверное ниче нового не внесу, но как еще один вариант можно рассмотреть.
    Идея
    1) сдвиг на m равносилен сдвигу на m%n
    2) сдвигать можно в стиле:
    Идейно цикл:
    запомнить то, на чем сейчас, перейти на m%n символов влево/вправо, влупить туда то, что запомнил, перед этим запомнить то, что там было.

    Но тут появляются приколы. Решение математикой.
    Если m и n не взаимопростые, то цикл пройдется не по всем елементам, а по классу одинаковых остатков стартового символа.
    Для этого (не вычисляя никаких НСД/НСК) мы просто итерируем все классы одинаковы х остатков. Можно начинать с 0,потом 1,…и так пока не переитерируем все символы, используя каунтер уже сдвинутиых символов. Можно было б вычислить шаги сразу – но нет смысла,т.к там надо будет НСК\НСД…

    Ну вот такое решение на JAVA. Влом было красиво оформлять с параметрами…

  50. raggzy


    char s[] = "asdasfmnsbd sdf".toCharArray();
    int n = s.length;
    int m = 3;

    int count = 0;
    int j = 0,i;
    char dop,tmp;
    // пока не обработали все
    while (count<n)
    {
    i=j;
    dop = s[i];
    do
    {
    tmp = s[(n+i-m%n)%n];
    s[(n+i-m%n)%n] = dop;
    i=(n+i-m%n)%n;
    dop = tmp;
    count++;
    }
    while (i!=j); // пока не пришли на начало цикла

    j++; // увеличиваем остаток рассматриваемого класса
    }

    System.out.println(s);

  51. Евгений

    Мда… Александру накидали тут кусков кода. Поди разберись чей как работает :-)

  52. pako

    Вы слишком усложняете.
    Сдвиг влево на М символов есть перестановка местами двух подстрок из М и N-M символов.
    Перестановка без дополнительной памяти делается следующим образом:
    АБЦД1234567 -> ДЦБА1234567 -> ДЦБА7654321 -> 1234567АБЦД
    Как все уже догадались, на каждом шаге мы разворачиваем некоторую подстроку задом наперед.
    Вот мы и свели задачу к предыдущей – как обратить строку за линейное время без доп.памяти уже бурно обсуждалась при трудоустройстве в стартапы.

    Так-то.

  53. Скакунов Александр

    Правильный ответ:

    Определите, на каком символе от начала строки вы хотите быть после сдвига. Затем разделите строку на две части так, чтобы этот символ был первым во второй “половине” строки. Оберните каждую часть на месте, затем оберните всю результирующую строку.

  54. sudres

    для правильного ответа 2n перестановок– медлено.

Оставить комментарий

Указать свой сайт могут только зарегистрированные пользователи. Регистрация или вход.

Архив

Добавить статью

Станьте автором нашего сайта!

Какие материалы подходят для публикации? — Такие.

Присылайте статьи на editors@developers.org.ua.

Подробнее.

Популярные теги

Все теги

Комментарии

Последние комментарии

интернет магазин бытовая техника магазин Laptoper