2009-04-22 1 views
2

Я написал рекурсивные функции с руководством друга, который учит меня C++ (как первый язык). Однако я не совсем понимаю, что происходит. Он помог мне (и сообщество SO, также) написать функцию сортировки слияния.Насколько рекурсия выполняется в вашей функции на C++?

std::vector<int> mergeSort(std::vector<int> original) 

//code here to create two vectors, farray and sarray, out of the 
//original std::vector<int> original ; each is half the length, 
//note: if the size % 2 != 0, sarray takes the odd int 

farray = mergeSort(farray); 
sarray = mergeSort(sarray); 

//code here merges the two into a single, final sorted std::vector 

В этой функции я задаю:

farray = mergeSort(farray); 
sarray = mergeSort(sarray); 

Что именно здесь происходит? Он вызывает mergeSort с farray и sarray как параметры и изменяет значение. Как далеко mergeSort выполняет в себе рекурсивно? Просто до вызова рекурсивной функции?

ответ

16

Каждый раз, когда вы вызываете функцию рекурсивно, она эффективно создает новую копию необходимой ей информации и продолжает.

У вас может быть программа, которая повторяется «бесконечно», то есть до тех пор, пока она не исчерпает ресурсы, обычно это пространство стека - пространство, в котором идут эти копии. Это будет выглядеть

void recur(){ 
    recur(); 
} 

int main(){ 
    recur(); 
    exit(0); /* won't ever really get here.*/ 
} 

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

#include <iostream> 
using namespace std; 
void recurSome(int count){ 
    cout << "RecurSome called with " << count << endl; 
    if (count > 0){ 
     recurSome(count-1); 
     cout << "Back at " << count << endl; 
    }else{ 
     cout << "Bottom." << endl; 
    } 
    return; 
} 

int main(){ 
    recurSome(10); 
    exit(0); /* now it will get here. */ 
} 

Если скомпилировать и запустить, что, скажем, с:

bash $ g++ -Wall -o rc recursome.cpp 
bash $ ./rc 

Вы получите результаты:

RecurSome called with 10 
RecurSome called with 9 
RecurSome called with 8 
RecurSome called with 7 
RecurSome called with 6 
RecurSome called with 5 
RecurSome called with 4 
RecurSome called with 3 
RecurSome called with 2 
RecurSome called with 1 
RecurSome called with 0 
Bottom. 
Back at 1 
Back at 2 
Back at 3 
Back at 4 
Back at 5 
Back at 6 
Back at 7 
Back at 8 
Back at 9 
Back at 10 
bash $ 

Посмотрите, как это получает вызов на 10, затем 9 и т. д., а затем после того, как он достигает дна, он показывает, что он возвращается на 1, затем 2 и т. д. обратно до 10?

Основное правило заключается в том, что каждый рекурсивная функция должна иметь что-то, что делает базовый вариант, тот, который делает называть себя снова. В этом, базовый случай count == 0 и на самом деле мы могли бы написать это как рекурсивное определение

recursome:
если с = 0: печать снизу
если с> 0: счетчик печати и recursome (c-1)

Вы увидите много рекурсивных определений этого типа, когда будете двигаться по математике.

Вот несколько niftier версия C с лучшим выходом:

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

int max = 10; 

void recurSome(int count){ 
    printf("RecurSome %*c Called with %d\n", max-count+1, ' ', count); 
    if (count > 0){ 
     recurSome(count-1); 
     printf("RecurSome %*c Back at %d\n", max-count+1, ' ', count); 
    }else{ 
     printf("RecurSome %*c Bottom.\n", 2*max, ' '); 
     printf("RecurSome %*c Back at %d\n", max-count+1, ' ', count); 
    } 
    return; 
} 

int main(){ 
    recurSome(max); 
    exit(0); /* now it will get here. */ 
} 

Выход:

RecurSome Called with 10 
RecurSome Called with 9 
RecurSome  Called with 8 
RecurSome  Called with 7 
RecurSome  Called with 6 
RecurSome  Called with 5 
RecurSome   Called with 4 
RecurSome   Called with 3 
RecurSome   Called with 2 
RecurSome   Called with 1 
RecurSome    Called with 0 
RecurSome      Bottom. 
RecurSome    Back at 0 
RecurSome   Back at 1 
RecurSome   Back at 2 
RecurSome   Back at 3 
RecurSome   Back at 4 
RecurSome  Back at 5 
RecurSome  Back at 6 
RecurSome  Back at 7 
RecurSome  Back at 8 
RecurSome Back at 9 
RecurSome Back at 10 
+1

Отличный пример: +1. Я сменил язык на C++, так как это тот язык, о котором спрашивал OP. Надеюсь, вы не возражаете! –

+0

Нет большой. Шахта также компилируется как C++. Спасибо за выполнение работы по ее изменению. –

+1

«Нижнее» - приятное прикосновение. – JeffH

0

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

В вашем случае, если вы разделите переданный вектор и закончите с двумя векторами, каждый из которых содержит только 1 элемент, имеет ли смысл называть mergeSort() на них? №

Вы можете справиться с этим, проверив размер долота и саррея и решив, следует ли вызывать mergeSort() на обоих или обоих, прежде чем объединять их и возвращать комбинацию.

Что делать, если у одного есть 2 предмета, а у одного есть 1? Вызвать mergeSort() на размер 2, но не на размер 1.

Когда вызов mergeSort() не вызывает mergeSort() на любом трейлере или сарере перед возвратом, рекурсия начнет разворачиваться.

2

Проверьте это в словаре:

рекурсию: существительное. см. рекурсия

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

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

В качестве простого примера вычисление n-й степени заданного целого i может быть функцией power(int number, int power). Как вы могли его реализовать? во многих отношениях. Простейшие являются вызовом к библиотеке, а затем цикл, или вы могли бы определить функцию с точкой зрения самого по себе:

int power(int number, unsigned int pow) 
{ 
    // Basic trivial case, cuts the recursion: 
    if (pow == 0) return 1; 

    // Implement power in terms of itself: 
    return number * power(number, pow-1); 
} 
int main() 
{ 
    int power = power(2, 10); 
} 

Мы определили функцию с точкой зрения самого по себе. Вы начинаете с самого основного случая (n^0 = 1). Если мы не в простейшем случае, вы можете выразить свой алгоритм в терминах самого себя.

Программа начнется в основном по телефону power(2, 10) что бы рекурсии и вызывать power(2, 9) сведение задачи к меньше проблеме и затем составить окончательный ответ в терминах простой задачи.

actuall след вызова будет:

power(2, 5) 
    power(2, 4) 
    power(2, 3) 
     power(2, 2) 
     power(2, 1) 
      power(2, 0) // simplest case: return 1 
     return 2 * 1 -> 2 // obtain next solution in terms of previous 
     return 2 * 2 -> 4 
    return 2 * 4 -> 8 
    return 2 * 8 -> 16 
return 2 * 16 -> 32 

При разработке рекурсивных алгоритмов обычно помог мне поверить, что я уже имел алгоритм и работаю, и просто работать на понижающем/составе нового результата.

+0

Отличный комментарий «отрезает рекурсию» и полезную визуальную трассировку! – JeffH

+0

Ницца. Кто-то должен ответить LISP сейчас ;-) –

0

Дополнительную информацию о том, что вы пытаетесь сделать, см. На странице wikipedia для merge sort.

Как оповещение, обратите внимание, что вы делаете копию каждого вектора, который вы указываете в качестве параметра. Используйте вектор <> const &.

+0

Я незнаком с оператором const и адреса, используемым в тандеме. Вы бы это объяснили? – jkeys

+0

@Benoit: чтобы избежать того, чтобы сделать хотя бы одну копию вектора на каждом уровне повторения, не должен ли параметр быть неконстантным? –

+0

@Michael Burr: да, но я фактически избегаю одного из двух экземпляров на каждом уровне рекурсии. Поскольку возможно (но сложно) применить этот алгоритм на месте, использование итераторов в исходном векторе было бы достаточным, и const можно было бы избежать. As-is, по моему предложению есть еще один новый вектор на уровень рекурсии, и это необходимо. –

2

Рекурсия может быть понята как практическое применение принципа индукции.Чтобы доказать утверждение Р (п) для всех п, где Р (п) означает «Сумма чисел от 1 до п равно п (п + 1)/2» мы должны доказать две вещи:

  1. Базовый регистр: P (n) выполняется для определенного целого числа. P (1) истинно, потому что 1 = 1 (1 + 1)/2.
  2. Индуктивный корпус: Если P (n) истинно, то P (n + 1) должно быть истинным. 1 + ... + n + (n + 1) = n (n + 1)/2 + (n + 1) = n (n + 1)/2 + 2 (n + 1)/2 = (n + 1) ((n + 1) +1)/2, который является статусом P (n + 1).

Аналогично, в рекурсивной функции, как нам нужно слияние обрабатывать два случая:

  1. Базового случай: Если массив имеет один элемент, он отсортирован; в противном случае
  2. Рекурсивный чехол: Разделите массив на два меньших массива, объедините каждый из них и объедините их вместе.

Ключ в том, что два массива являются меньше, чем оригинал, в противном случае один из них никогда не попал в базовый случай. Поскольку массивы разделены примерно наполовину, глубина рекурсии будет в этом случае примерно log2 (n).

Насколько код в вопросе идет, есть три проблемы:

  1. Базовый вариант отсутствует.
  2. Повторное использование переменных farray и sarray не является строго необходимым и может ввести в заблуждение.
  3. Код будет очень медленным из-за количества созданных и уничтоженных std :: векторов. Лучший интерфейс для mergeSort принимает в качестве входных данных два векторных :: итератора, аналогичных функции std :: sort().

Новые программисты должны попробовать запустить mergeSort с бумагой и карандашом.