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个结果.