(C) Рекурсивный strcpy (), который принимает только 1 параметр - PullRequest
0 голосов
/ 14 декабря 2018

Позвольте мне быть ясным с самого начала, это не обман, я объясню, как.Итак, я поручил себе написать функцию, которая имитирует strcpy, но с двумя условиями:

  1. она должна быть рекурсивной
  2. она должна принимать один параметр (который является исходнымстрока)

Функция должна возвращать указатель на вновь скопированную строку.Это то, что я пробовал до сих пор:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char * my_strcpy(char *original);

int main(void) {
  char *string = my_strcpy("alpine");
  printf("string = <%s>\n", string);
  return 0;
}

char * my_strcpy(char *original){
  char *string = (char *)malloc(10);
  if(*original == '\0') {
    return string;
  }
  *string++ = *original;
  my_strcpy(original + 1);
}

Проблема несколько очевидна, string получает malloc -ед каждый раз, когда вызывается my_strcpy().Одним из решений, которое я мог бы придумать, было бы выделение памяти для string только при первом вызове функции.Поскольку мне разрешено иметь только 1 параметр, единственное, о чем я мог подумать, это проверить стек вызовов, но я не знаю, разрешено ли это, и это действительно похоже на мошенничество.Есть ли логическое решение этой проблемы?

Ответы [ 3 ]

0 голосов
/ 14 декабря 2018

Вы не сказали, что мы не можем использовать strcat.Таким образом, здесь есть логичный (хотя и несколько бесполезный) ответ, использующий рекурсию, чтобы ничего не делать, кроме как отрубить последний символ и снова добавить его.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

char * my_strcpy(char *original);

int main(void) {
  char *string = my_strcpy("alpine");
  printf("string = <%s>\n", string);
  return 0;
}

char * my_strcpy(char *original){
  if(*original == '\0') {
    return original;
  }
  int len = strlen(original);
  char *string = (char *)malloc(len+1);
  char *result = (char *)malloc(len+1);
  string[0] = result[0] = '\0';

  strcat (string, original);
  len--;
  char store[2] = {string[len] , '\0'}; // save last char
  string[len] = '\0';                   // cut it off
  strcat (result, my_strcpy(string));

  strcat (result, store);               // add it back
  return result;
}
0 голосов
/ 15 декабря 2018

Рекурсия - методика анализа проблем.То есть вы начинаете с проблемы и думаете о том, какой может быть рекурсивная структура решения.Вы не начинаете с рекурсивной структуры, а затем пытаетесь придумать в ней свою проблему.

Другими словами, хорошо бы практиковать рекурсивный анализ, но задача, которую вы поставили перед собой -заставить решение иметь форму однопараметрической функции - это не способ сделать это.Если вы начнете созерцать глобальные или статические переменные или извлекать контекст времени выполнения, разбивая стек вызовов, у вас будет довольно хороший намек на то, что вы еще не нашли подходящий рекурсивный анализ.

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

Ясно, что если у нас уже есть непрерывная структура данных в памяти, делаянепрерывная копия не является сложной задачей.Если мы не знаем, насколько он велик, мы можем сделать два обхода: один, чтобы найти его размер, после чего мы можем выделить необходимую память, и другой, чтобы сделать копию.Обе эти задачи являются простыми циклами, что является одной из форм рекурсии.

Существенная природа рекурсивного решения состоит в том, чтобы подумать о том, как перейти от проблемы к (немного) более простой или меньшей задаче.Или, чаще, небольшое количество более мелких или более простых задач.

Такова природа одной из самых классических рекурсивных задач: сортировка последовательности чисел.Основная структура: разделите последовательность на две примерно равные части;отсортируйте каждую часть (рекурсивный шаг) и соедините результаты вместе, чтобы комбинация была отсортирована.Этот базовый план имеет по крайней мере два интересных (и очень разных) проявления:

  • Разделите последовательность произвольно на две почти равные части, поместив альтернативные элементы в альтернативные части или поместив первую половинув одной части, а остальные в другой части.(Первый будет хорошо работать, если мы не будем заранее знать, насколько велика последовательность.) Чтобы собрать отсортированные части вместе, мы должны чередовать («объединять»).(Это сортировка слиянием).

  • Разделите последовательность на два диапазона путем оценки среднего значения и помещения всех меньших значений в одну часть, а всех больших значений - в другую часть.Чтобы собрать отсортированные части, мы просто объединяем их.(Это быстрая сортировка.)

В обоих случаях нам также необходимо использовать тот факт, что одноэлементная последовательность (тривиально) отсортирована, поэтому больше не требуется никакой обработки.Если мы достаточно часто разделяем последовательность на две части, гарантируя, что ни одна часть не пуста, мы должны в конечном итоге достичь части, содержащей один элемент.(Если нам удастся точно выполнить деление, это произойдет довольно скоро.)

Интересно реализовать эти две стратегии с использованием односвязных списков, так что длины на самом деле не так легко узнать.Оба могут быть реализованы таким образом, и реализации раскрывают что-то важное о природе сортировки.

Но давайте вернемся к гораздо более простой проблеме - копированию последовательности во вновь распределенный непрерывный массив.Чтобы сделать задачу более интересной, мы не будем предполагать, что последовательность уже сохранена непрерывно, или что мы можем пройти ее дважды.

Для начала нам нужно найти длину последовательности, которую мы можемсделать, наблюдая, что пустая последовательность имеет нулевую длину, и любая другая последовательность имеет еще один элемент, чем подпоследовательность, начинающаяся после первого элемента («хвоста» последовательности.)

Length(seq):
    If seq is empty, return 0.
    Else, return 1 + Length(Tail(seq))

Теперь предположим, что мы выделили хранилище для копии.Теперь мы можем копировать, наблюдая, что пустая последовательность полностью скопирована, и любую другую последовательность можно скопировать, поместив первый элемент в выделенное хранилище и затем скопировав хвост последовательности в хранилище, начиная со второй позиции: (иэта процедура логически принимает два аргумента)

Copy(destination, seq):
    If seq is not empty:
        Put Head(seq) into the location destination
        Call Copy (destination+1, Tail(seq))

Но мы не можем просто соединить эти две процедуры, потому что это будет пересекать последовательность дважды, что, как мы сказали, мы не можем сделать.Поэтому нам нужно как-то вложить эти алгоритмы.

Чтобы сделать это, мы должны начать с передачи накопленной длины через рекурсию, чтобы мы могли использовать ее для выделения памяти, когда мы знаем, насколько велик объект,Затем на обратном пути нам нужно скопировать элемент, который мы посчитали на пути вниз:

Copy(seq, length):
    If seq is not empty:
        Set item to its first element (that is, Head(seq))
        Set destination to Copy(Tail(seq), length + 1)
        Store item at location destination - 1
        Return destination - 1
    Otherwise: (seq is empty)
        Set destination to Allocate(length)
        # (see important note below)
        Return destination + length

Чтобы правильно запустить рекурсию, нам нужно передать 0 в качестве начальной длины.Неправильно заставлять пользователя вставлять «магические числа», поэтому обычно мы оборачиваем функцию драйвером с одним аргументом:

Strdup(seq):
    Return Copy (seq, 0)

Важное примечание : если бы это было написанов C, используя строки, нам нужно NUL-завершить копию.Это означает выделение length+1 байтов вместо length, а затем сохранение 0 в destination+length.

0 голосов
/ 14 декабря 2018

Вы написали его как хвостовую рекурсивную, но я думаю, что если вы не сделаете функцию не входящей, ваша единственная возможность - сделать головку функции рекурсивной и повторно вызывать realloc для возвращаемого значения рекурсивного вызова, чтобы развернуть ее, затемдобавить в один символ.Это та же проблема, что и при простом вызове strlen для выделения: он делает что-то линейное по длине входной строки в каждом рекурсивном вызове и оказывается неявно n-квадратным алгоритмом (0.5 * n * (n + 1).)).Вы можете улучшить его, улучшив сложность амортизированного времени, расширив строку с коэффициентом и увеличивая ее только тогда, когда существующий буфер заполнен, но он все еще невелик.

Есть причина, по которой вы не будете использоватьрекурсия для этой задачи (которую вы, вероятно, знаете): глубина стека будет равна длине входной строки, а весь выдвинутый кадр стека и инструкция вызова для каждого скопированного символа - большие затраты.Тем не менее, вы бы не сделали это рекурсивно с одним аргументом, если бы вы действительно собирались сделать это рекурсивно: вы бы сделали функцию с одним аргументом, которая объявляет некоторые локальные объекты и вызывает рекурсивную функцию с несколькими аргументами.

Даже с трюком realloc будет трудно или невозможно посчитать символы в оригинале по мере того, как вы идете, чтобы вы могли соответствующим образом вызвать realloc, помня, что другие функции stdlib "str *" запрещены, потому что они, вероятно,сделайте всю свою функцию n-квадратной, что, как я предполагал, мы пытались избежать.

Уродливые уловки, такие как проверка длины строки указателя и замена первых нескольких символов указателем на memcpyчто делает базовый сценарий для рекурсии более сложным, но хм.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...