有人可以就如何创建GetEnumerator()的递归版本给我建议吗? 众所周知的Towers of Hanoi problem可以作为一个与我遇到的实际问题相当的例子。显示高度为n的磁盘堆栈的所有移动的简单算法是:
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);
}
}
我真正想要做的是设置一个实现IEnumerable的HanoiTowerMoves类,这使我可以按如下方式迭代所有动作:
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()的主体是什么样的? 有人可以为我解开这个谜吗?
以下是我创建的控制台应用程序的Program.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 ();
}
}
}
答案 0 :(得分: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;
}
您可以使用yield return来实现一个返回枚举器或可枚举的方法。
答案 1 :(得分: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));
}
}
您已经确切知道在当前递归步骤之后需要做什么工作,因此不需要在交换机周围跳动以将三位工作放在堆栈上。只需在给定步骤中立即对所有工作进行排队。
答案 2 :(得分: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
}