xmxoxo 2006-11-14 at XiaMen
类的基础也学习了部份;现在正逐渐养成用类来描述程序和问题的习惯。
群里一位朋友的问题引发了类对象中的一个问题。
有一个T字型铁轨,标号为1,2,。。。,n的车厢位于铁轨的左边,
当所有车厢移动到铁轨的右边时,要求重新排序车厢的顺序。规则是支线上的
车厢可以不动,也可以移动到铁轨的右边。例如,如果n=3,车厢1,2,3在铁
轨的左边,依次进行如下的过程:3号车厢进入支线,2号车厢进入支线,2号车
厢进入铁轨的右边,3号车厢进入铁轨右边,1号车厢进入支线,1号车厢进入铁
轨右边。最后获得新的顺序1,3,2。
对于任意n值,请列出所有的可能排序。
这应该是一道古老的题目了,记得在很久以前的某份报纸上看到这道题。
那时候还没学过数据结构,感觉可能性很难把握。使用栈结构应该是很方便描述
各种状态的。
首先,使用单向链表来保存栈中的数据:
{
int dat;
node *next;
};
然后定义一个栈类(stack),来描述栈对象,并将方法包装进去。
在stack类里,定义了两个private变量,m_length用来描述栈的大小;
top指针用来指向栈顶,也就是单向链表的头结点。
栈类的方法加了常用的这几个:
isempty()用来判断栈是否为空
length()用来返回栈的大小;
pop()用来出栈;
push(int)用来将数据压入栈.
同时添加了复制函数,这正是本文的重点,在下面会详细说明。
{
private:
int m_length; //栈大小
node *top; //头结点指针
public:
bool push(int dat);
int pop();
int length();
bool isempty();
stack(); //构造函数
//stack(const stack &s); //复制函数
virtual ~stack(); //析构函数
};
刚开始的时候并没有给类添加复制函数,于是stack.cpp内容如下:
// 栈类,实现出栈,进栈等
// 版本:v1.0
// 日期:2006.11.14
// 作者:xmxoxo at XiaMen
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "stack.h"
#include "stdlib.h"
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
stack::stack()
{
m_length = 0; //大小为0;
top = NULL; //初始化指针
}
stack::~stack()
{
//terminate
if (m_length!=0)
{
while (m_length>0)
{
pop();
}
}
}
//judge stack is empty
bool stack::isempty()
{
if (top==NULL)
{
return 1;
}
else
{
return 0;
}
}
//return size of stack
int stack::length()
{
return m_length;
}
//pop data
int stack::pop()
{
int ret=0;
node *tmp;
if (m_length!=0)
{
//pop node
tmp=top;
ret=tmp->dat;
top = tmp->next;
//delete node
delete tmp;
// size of
m_length--;
}
return ret;
}
//push data
bool stack::push(int dat)
{
bool ret=0;
node *tmp;
tmp = new node;
tmp->dat = dat;
tmp->next = top;
top=tmp;
// size of stack ++
m_length++;
return ret;
}
描述完栈类,接下来分析题目,根据题意,左边(用stack left来表示)的火车可以进入
支线(用stack tmp来表示),也可以到右边(用stack right来表示,注意,得到的结果
就是right里保存的内容,但在输出的时候直接pop后,顺序是颠倒的),当然,如果不用
栈对象,也可以使用字串或者数组进行处理,但是表达上可能更复杂,也不太好理解。
现在我们得到三个栈对象,left,tmp,和right,来分析一下火车的行走情况,left
出栈的火车,有两种情况,一是进入支线tmp,我们把这个行为简单记为“进”;二是直接
到右边(right),把这个行为记为“右”;而在支线(tmp)出线的火车只有一种行为,就是
出栈后进入right,把这个行为记为“出";显然,“进”后马上“出”,就相当于“右”。
现在先考虑1列火车的情况,根据上面的推理,1列火车时只有一种情况,就是“右”。
再来考虑n列火车的情况,假设现在是第i列火车在left准备出栈,那么有这么几种
情况:
1、"右“。直接到right
2、“进”。根据上面的推理,“进”完后不能马上“出”了,否则就跟“右”是一样的了。
3、当tmp不为空时,left不“右”也不“进”,而是“出”,即支线火车进入右边。
枚举了所有的情况,就可以使用递归来进行编程了。递归的方法很简单,主程序如下:
'----------------------------------------------------------
//
// 火车排列问题
//作者: xmxoxo 2006.11.13 at xiamen
//当前版本 v1.0
#include "stdafx.h"
#include "stack.h"
#include "iostream.h"
//输出结果
void output(stack& p)
{
int len,i;
len=p.length();
for (i=0;i<len;i++)
{
cout<<outtmp.pop();
if (i!=len-1)
cout<<",";
}
cout<<endl;
}
//递归过程
//参数: 左栈,临时栈,输出栈
void foo(stack left,stack tmp, stack out)
{
int i=0;
if (left.length()==1)
{
//如果左栈只有一个数,则输出这个数到输出栈
//cout<<"右 ";
out.push(left.pop());
//临时栈全部出栈
while (!tmp.isempty())
{
out.push(tmp.pop());
}
//并输出结果
output(out);
}
else
{
//分情况
i=left.pop();
//cout<<"["<<i<<"]"<<"进,";
//1.左栈进临时栈
tmp.push(i);
//递归,传值
foo(left, tmp, out);
//恢复临时栈
i=tmp.pop();
//2.左栈出栈到输出栈
//cout<<"右,";
out.push(i);
//递归
foo(left,tmp,out);
//3.临时栈出栈到输出栈
if (!tmp.isempty ())
{
out.push(tmp.pop());
foo (left,tmp,out);
}
}
}
//定义左栈
stack objleft;
//定义栈
stack objstack;
//定义输出栈
stack objout;
//主程序
int main(int argc, char* argv[])
{
int n;
int i;
//I/O 处理
cout<<"Please Input Number:";
cin>>n;
//初始化
//建立左栈
for (i=1;i<=n;i++)
{
objleft.push (i);
}
//初始化结束
//开始处理
foo(objleft,objstack,objout);
return 0;
}
'----------------------------------------------------------
程序根据N的情况进行了递归,但是运行后却发现结果不正确,经过跟踪发现
对象作为函数的参数传递的时候,传递得不正确。只有在主程序中的
foo(objleft,objstack,objout);这一句运行的时候,有正确的传递,而在
foo()函数里进行的递归调用时传递就错误了。foo函数定义的是值参,也就是
说,调用的参数会复制产生一个新的变量,在函数中使用。但是foo的三个参
数都是stack类的实例对象,在stack类中,我们定义了一个单向链表来保存栈
的数据,其中,node结构里使用到了一个指针next,正是在类中的指针导致了
对象实例在复制中的错误,在跟踪程序中发现,当递归引用foo函数时,程序
确实也复制了新的对象,连指针也复制了,但这个指针复制后,还是指向原来
的位置,而不是先复制出指针指向的空间,比如一个指针,值为0x00431D30,复制后
还是0x00431D30,还是指向同一个地址,而指向的地址并没有复制。所以无论尝试
foo(stack &left...)形参方式还是foo(stack left)值参方式,复制的新对象中
的指针仍然是指向相同的空间。
查询了关于类的教程,原来在类里除了构造函数及析构函数,还有一个
复制函数。用于复制对象时调用,来产生新的对象,这个复制函数是在新的对象中
运行的,也就是要产生的那个对象。函数的原型是:stack::stack(const stack &s)
这里是使用形参的方式,并且使用了const前缀。根据思路,现在可以自定义出复制对象
的过程,要注意的是:在这个函数里可以访问&s这个形参的所有成员及成员函数。
(注:const前缀保证了不会对原有变量进行修改;而&s形参,则表示了引用,保证了
该函数不需要对s变量进行复制,否则,在调用复制函数时又调用复制了函数,进入了死
循环,摘自优快云)
明白了对象的复制函数,就可以自己构造一个复制函数了,由于stack中的top指针
是头指针,在复制的过程中不能再使用push函数相同的方法来把数据加到链表顶端,而是
要加在链表的尾部,所以定义了一个临时的tail尾指针。
{
m_length = 0;
top = NULL;
node *tmp;
node *tail; //尾指针
node *newnode;
tmp=s.top;
tail=NULL;
while (tmp!=NULL)
{
newnode = new node;
newnode->dat = tmp->dat;
newnode->next =NULL;
if (top!=NULL)
{
tail->next =newnode;
tail=newnode;
}
else
{
top=newnode;
tail=newnode;
}
// size of stack ++
m_length++;
tmp=tmp->next;
}
}
加入了复制函数后,stack类可以正常工作了,主程序不需要修改。
以下是n=3及n=4的输出结果:
Please Input Number:3
3,2,1
3,1,2
1,3,2
2,1,3
1,2,3
Press any key to continue
Please Input Number:4
4,3,2,1
4,3,1,2
4,1,3,2
4,2,1,3
4,1,2,3
1,4,2,3
2,1,4,3
1,2,4,3
3,2,1,4
3,1,2,4
1,3,2,4
2,1,3,4
1,2,3,4
Press any key to continue
可以看出,输出的结果很有规律,正是按上面我们分析的“进”,“右”,“出”方式得到的结果。
11.17 补记:
上面的主程序在情况分析有误,情况1和情况2会出现重复,可以合并处理,另外,原先是判断n=1
时输出结果,也可以直接合并到递归里,判断左堆栈和临时栈同时为空即可输出结果。修改后得到了
以下的程序及结果:
//
// 火车排列问题
//作者: xmxoxo 2006.11.13 at xiamen
//当前版本 v1.0
#include "stdafx.h"
#include "stack.h"
#include "iostream.h"
//输出结果
void output(stack p)
{
int len,i;
len=p.length();
for (i=0;i<len;i++)
{
cout<<p.pop();
if (i!=len-1)
cout<<",";
}
cout<<endl;
}
//递归过程
//参数: 左栈,临时栈,输出栈
int foo(stack left,stack tmp, stack out)
{
static count;
int i=0;
//分情况
//1.左栈进临时栈
if (!left.isempty())
{
tmp.push(left.pop());
//cout<<"进,";
//递归,传值
foo(left, tmp, out);
//恢复左栈
left.push(tmp.pop());
}
//2.临时栈出栈到输出栈
if (!tmp.isempty())
{
//cout<<"出,";
out.push(tmp.pop());
foo (left,tmp,out);
//恢复状态
tmp.push (out.pop());
}
if (left.isempty()&&tmp.isempty())
{
output(out);
count++;
}
return count;
}
//定义栈
stack objstack;
//定义左栈
stack objleft;
//定义输出栈
stack objout;
//主程序
int main(int argc, char* argv[])
{
int n;
int i;
int count=0;
//I/O 处理
cout<<"Please Input Number:";
cin>>n;
//初始化
//建立左栈
for (i=1;i<=n;i++)
{
objleft.push (i);
}
//初始化结束
//开始处理
count=foo(objleft,objstack,objout);
cout << "共 " << count << "个结果."<<endl;
return 0;
}
Please Input Number:3
3,2,1
3,1,2
1,3,2
2,1,3
1,2,3
共 5个结果.
Please Input Number:4
4,3,2,1
4,3,1,2
4,1,3,2
1,4,3,2
4,2,1,3
4,1,2,3
1,4,2,3
2,1,4,3
1,2,4,3
3,2,1,4
3,1,2,4
1,3,2,4
2,1,3,4
1,2,3,4
共 14个结果.