2012-01-11 3 views
4

Может ли кто-нибудь дать мне совет о том, как создать рекурсивную версию GetEnumerator()? Известный Towers of Hanoi problem может служить примером, который сопоставим с реальной проблемой, которую я имею. Простой алгоритм, чтобы показать все ходы за стопкой дисков высоты п является:C# Как сделать рекурсивную версию GetEnumerator()

void MoveTower0 (int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTower0 (n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTower0 (n - 1, temp, finish, start); 
    } 
} 

То, что я на самом деле хочу сделать, это создать класс HanoiTowerMoves, который реализует IEnumerable и что позволяет мне перебрать все ходы следующим :

foreach (Move m in HanoiTowerMoves) Console.WriteLine (m); 

Первым шагом на пути реализации GetEnumerator(), кажется, избавиться от параметров MoveTower. Это можно легко сделать, используя стек. Я также представил класс Move, который объединяет параметры в одну переменную.

class Move 
{ 
    public int N { private set; get; } 
    public Needle Start { private set; get; } 
    public Needle Finish { private set; get; } 
    public Needle Temp { private set; get; } 

    public Move (int n, Needle start, Needle finish, Needle temp) 
    { 
    N = n; 
    Start = start; 
    Finish = finish; 
    Temp = temp; 
    } 

    public override string ToString() 
    { 
    return string.Format ("Moving disk from {0} to {1}", Start, Finish); 
    } 
} 

Теперь MoveTower можно переписать следующим образом:

void MoveTower1() 
{ 
    Move m = varStack.Pop(); 

    if (m.N > 0) 
    { 
    varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
    MoveTower1(); 
    Console.WriteLine (m); 
    varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
    MoveTower1(); 
    } 
} 

Эта версия должна быть вызвана следующим образом:

varStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
MoveTower1(); 

Следующим шагом на пути к итератора версии реализовать класс:

class HanoiTowerMoves : IEnumerable<Move> 
{ 
    Stack<Move> varStack; 
    int n; // number of disks 

    public HanoiTowerMoves (int n) 
    { 
    this.n = n; 
    varStack = new Stack<Move>(); 
    } 

    public IEnumerator<Move> GetEnumerator() 
    { 
    // ???????????????????????????? } 

    // required by the compiler: 
    IEnumerator IEnumerable.GetEnumerator() 
    { 
    return GetEnumerator(); 
    } 
} 

Теперь для меня большой вопрос: как выглядит тело GetEnumerator()? Может ли кто-нибудь решить эту тайну для меня?

Ниже приведен код программы Programs.cs консольного приложения, которое я создал.

using System; 
using System.Collections.Generic; 
using System.Collections; 

/* Towers of Hanoi 
* =============== 
* Suppose you have a tower of N disks on needle A, which are supposed to end up on needle B. 
* The big picture is to first move the entire stack of the top N-1 disks to the Temp needle, 
* then move the N-th disk to B, then move the Temp stack to B using A as the new Temp needle. 
* This is reflected in the way the recursion is set up. 
*/ 

namespace ConsoleApplication1 
{ 
    static class main 
    { 
    static void Main (string [] args) 
    { 
     int n; 
     Console.WriteLine ("Towers of Hanoi"); 

     while (true) 
     { 
     Console.Write ("\r\nEnter number of disks: "); 

     if (!int.TryParse (Console.ReadLine(), out n)) 
     { 
      break; 
     } 

     HanoiTowerMoves moves = new HanoiTowerMoves (n); 
     moves.Run (1); // algorithm version number, see below 
     } 
    } 
    } 

    class Move 
    { 
    public int N { private set; get; } 
    public Needle Start { private set; get; } 
    public Needle Finish { private set; get; } 
    public Needle Temp { private set; get; } 

    public Move (int n, Needle start, Needle finish, Needle temp) 
    { 
     N = n; 
     Start = start; 
     Finish = finish; 
     Temp = temp; 
    } 

    public override string ToString() 
    { 
     return string.Format ("Moving disk from {0} to {1}", Start, Finish); 
    } 
    } 

    enum Needle { A, B, Temp } 

    class HanoiTowerMoves : IEnumerable<Move> 
    { 
    Stack<Move> varStack; 
    int n;   // number of disks 

    public HanoiTowerMoves (int n) 
    { 
     this.n = n; 
     varStack = new Stack<Move>(); 
    } 

    public void Run (int version) 
    { 
     switch (version) 
     { 
     case 0: // Original version 
      MoveTower0 (n, Needle.A, Needle.B, Needle.Temp); 
      break; 

     case 1: // No parameters (i.e. argument values passed via stack) 
      varStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
      MoveTower1(); 
      break; 

     case 2: // Enumeration 
      foreach (Move m in this) 
      { 
      Console.WriteLine (m); 
      } 

      break; 
     } 
    } 

    void MoveTower0 (int n, Needle start, Needle finish, Needle temp) 
    { 
     if (n > 0) 
     { 
     MoveTower0 (n - 1, start, temp, finish); 
     Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
     MoveTower0 (n - 1, temp, finish, start); 
     } 
    } 

    void MoveTower1() 
    { 
     Move m = varStack.Pop(); 

     if (m.N > 0) 
     { 
     varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
     MoveTower1(); 
     Console.WriteLine (m); 
     varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
     MoveTower1(); 
     } 
    } 

    public IEnumerator<Move> GetEnumerator() 
    { 
     yield break; // ???????????????????????????? 
    } 

    /* 
     void MoveTower1() 
     { 
     Move m = varStack.Pop(); 

     if (m.N > 0) 
     { 
      varStack.Push (new Move (m.N - 1, m.Start, m.Temp, m.Finish)); 
      MoveTower1(); 
      Console.WriteLine (m); ? yield return m; 
      varStack.Push (new Move (m.N - 1, m.Temp, m.Finish, m.Start)); 
      MoveTower1(); 
     } 
     } 
    */ 

    // required by the compiler: 
    IEnumerator IEnumerable.GetEnumerator() 
    { 
     return GetEnumerator(); 
    } 
    } 
} 
+1

Я не понимаю точку вызова GetEnumerator рекурсивно. Просто напишите рекурсивную функцию, которая вызывает этот метод, а не жесткий. –

ответ

12

Ваш подход очень хорош, но я думаю, что вы слишком много думаете о проблеме. Давайте сделаем шаг назад. У вас есть рекурсивный алгоритм:

void MoveTowerConsole (int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTowerConsole (n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTowerConsole (n - 1, temp, finish, start); 
    } 
} 

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

Прежде всего, мы переименуем его. Во-вторых, его тип возврата не может быть недействительным. Оно должно быть IEnumerable<string>:

IEnumerable<string> MoveTower(int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    MoveTower(n - 1, start, temp, finish); 
    Console.WriteLine ("Moving disk from {0} to {1}", start, finish); 
    MoveTower(n - 1, temp, finish, start); 
    } 
} 

Это право? Нет. Мы ничего не возвращаем, мы все еще сбрасываем на консоль. Что мы хотим, чтобы итератор уступил? Мы хотим итератор выход:

  • всех хода, необходимыми для первого рекурсивного шага
  • текущего ход
  • всех ходов, необходимые для второго рекурсивного шага

Так мы изменяем алгоритм для получения таких результатов:

IEnumerable<string> MoveTower(int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    foreach(string move in MoveTower(n - 1, start, temp, finish)) 
     yield return move; 
    yield return string.Format("Moving disk from {0} to {1}", start, finish); 
    foreach(string move in MoveTower(n - 1, temp, finish, start)) 
     yield return move; 
    } 
} 

И все готово! Легко. Нет необходимости определять целостный класс, чтобы превратить рекурсивный алгоритм в рекурсивный перечислитель; пусть компилятор сделает это для вас.

Если вы хотите изменить это в метод, который перечисляет «движется», то сделать это:

IEnumerable<Move> MoveTower(int n, Needle start, Needle finish, Needle temp) 
{ 
    if (n > 0) 
    { 
    foreach(Move move in MoveTower(n - 1, start, temp, finish)) 
     yield return move; 
    yield return new Move(start, finish); 
    foreach(Move move in MoveTower(n - 1, temp, finish, start)) 
     yield return move; 
    } 
} 

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

Упражнение: Перепишите блок итератора выше, так что он не делает рекурсию на всех. Ваше решение, использующее явный стек, является шагом в правильном направлении, но оно все равно выполняет рекурсию. Можете ли вы адаптировать его так, чтобы не было рекурсии?

Если вы вознамерились писать класс, который реализует IEnumerable<Move>, то вы можете адаптировать код выше простым способом:

class MoveIterator : IEnumerable<Move> 
{ 
    public IEnumerator<Move> GetEnumerator() 
    { 
     foreach(Move move in MoveTower(whatever)) 
      yield return move; 
    } 

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

+0

1+ Быстрее меня, набрав решение. –

+0

Hello Eric, Большое спасибо за проницательный и шаг за шагом вы представляете свое решение. На самом деле это вопрос неправильного взгляда на проблему, из-за которой я не мог понять, куда идти. Очень поучительный и информативный! У меня на самом деле было нерекурсивное решение, которое использует два стека: см. Ответ ниже. Еще раз спасибо! John Pool Amersfoort Netherland –

+0

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

0

нерекурсивны версия:

// Non-recursive version -- state engine 
//rta.Push (State.Exit); 
//parameters.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
//MoveTower3(); 

enum State { Init, Call1, Call2, Rtrn, Exit } 

{ 
    ... 

    #region Non-recursive version -- state engine 
    static void MoveTower3() 
    { 
    State s = State.Init; 
    Move m = null; 

    while (true) 
     switch (s) 
     { 
     case State.Init: 
      m = moveStack.Pop(); 
      s = (m.n <= 0) ? State.Rtrn : State.Call1; 
      break; 
     case State.Call1: 
      rta.Push (State.Call2); // where do I want to go after the call is finished 
      moveStack.Push (m); // save state for second call 
      moveStack.Push (new Move (m.n-1, m.start, m.temp, m.finish)); // parameters 
      s = State.Init; 
      break; 
     case State.Call2: 
      m = moveStack.Pop(); // restore state from just before first call 
      Console.WriteLine (m); 
      rta.Push (State.Rtrn); 
      moveStack.Push (new Move (m.n-1, m.temp, m.finish, m.start)); 
      s = State.Init; 
      break; 
     case State.Rtrn: 
      s = rta.Pop(); 
      break; 
     case State.Exit: 
      return; 
     } 
    } 
    #endregion 

    #region Enumeration 
    static IEnumerable<Move> GetEnumerable (int n) 
    { 
    Stack<Move> moveStack = new Stack<Move>(); 
    Stack<State> rta = new Stack<State>(); // 'return addresses' 
    rta.Push (State.Exit); 
    moveStack.Push (new Move (n, Needle.A, Needle.B, Needle.Temp)); 
    State s = State.Init; 
    Move m = null; 

    while (true) 
     switch (s) 
     { 
     case State.Init: 
      m = moveStack.Pop(); 
      s = (m.n <= 0) ? State.Rtrn : State.Call1; 
      break; 
     case State.Call1: 
      rta.Push (State.Call2); // where do I want to go after the call is finished 
      moveStack.Push (m); // save state for second call 
      moveStack.Push (new Move (m.n-1, m.start, m.temp, m.finish)); // parameters 
      s = State.Init; 
      break; 
     case State.Call2: 
      m = moveStack.Pop(); // restore state from just before first call 
      yield return m; 
      rta.Push (State.Rtrn); 
      moveStack.Push (new Move (m.n-1, m.temp, m.finish, m.start)); 
      s = State.Init; 
      break; 
     case State.Rtrn: 
      s = rta.Pop(); 
      break; 
     case State.Exit: 
      yield break; 
     } 
    } 
    #endregion 
} 
1

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

Однако в этом конкретном случае вам не нужно вытаскивать тяжелую машину пускового автомата с переключателем и текущим состоянием. Вы можете просто сделать это:

IEnumerable<Move> MoveTowerConsole (int size, Needle start, Needle finish, Needle temp) 
{ 
    if (size <= 0) yield break; 
    var stack = new Stack<Work>(); 
    stack.Push(new Work(size, start, finish, temp)); 
    while(stack.Count > 0) 
    { 
    var current = stack.Pop(); 
    if (current.Size == 1) 
     yield return new Move(current.Start, current.Finish); 
    else 
    { 
     // Push the work in the *opposite* order that it needs to be done. 
     stack.Push(new Work(current.Size - 1, current.Temp, current.Finish, current.Start)); 
     stack.Push(new Work(1, current.Start, current.Finish, current.Temp)); 
     stack.Push(new Work(current.Size - 1, current.Start, current.Temp, current.Finish)); 

    } 
} 

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

+0

Привет, Эрик, Спасибо четыре вашего нерекурсивного решения! Должен признаться, это немного похоже на волшебство. У меня есть смутное представление о большой идее: когда вы выбираете часть «Работы» с нетривиальным размером (т. Е.> 1) из очереди, вы разбиваете ее на три части - которые индивидуально меньше оригинальной части , иначе алгоритм не завершится - и вы нажмете их обратно в очередь ... –

+0

... Каждый раз, когда вы сталкиваетесь с тривиальной частью, вы ее уступаете, и вы повторяете это до тех пор, пока в очереди не останется ничего. Класс Work - это всего лишь контейнер для всех локальных переменных, которые будут обычно выполняться в стеке, когда будет использоваться рекурсия. Это правильный способ взглянуть на него? Можно ли переписать * любую * рекурсивную функцию таким образом? Есть ли у вас больше примеров или ссылка на статью по этому вопросу? Thanks - John –

+0

@JohnPool: Во-первых, мое решение было неправильным - я должен был использовать стек, а не очередь. Но да, это основная идея. Не всякая рекурсивная функция может быть переписана так же легко, как это. –

Смежные вопросы