C++学习笔记之(对象复制的困惑)


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值,请列出所有的可能排序。
 这应该是一道古老的题目了,记得在很久以前的某份报纸上看到这道题。
那时候还没学过数据结构,感觉可能性很难把握。使用栈结构应该是很方便描述
各种状态的。
 首先,使用单向链表来保存栈中的数据:

struct  node
{
 
int  dat;
 node 
* next;
};

 

然后定义一个栈类(stack),来描述栈对象,并将方法包装进去。
在stack类里,定义了两个private变量,m_length用来描述栈的大小;
top指针用来指向栈顶,也就是单向链表的头结点。
栈类的方法加了常用的这几个:
isempty()用来判断栈是否为空
length()用来返回栈的大小;
pop()用来出栈;
push(int)用来将数据压入栈.
同时添加了复制函数,这正是本文的重点,在下面会详细说明。

 

class  stack  
{
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内容如下:


//  Stack.cpp: implementation of the Stack class.
//  栈类,实现出栈,进栈等 
//  版本: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不“右”也不“进”,而是“出”,即支线火车进入右边。

枚举了所有的情况,就可以使用递归来进行编程了。递归的方法很简单,主程序如下:

'----------------------------------------------------------

//  trainFoo.cpp : Defines the entry point for the console application.
//
//  火车排列问题
// 作者: 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变量进行复制,否则,在调用复制函数时又调用复制了函数,进入了死
循环,摘自CSDN)
 明白了对象的复制函数,就可以自己构造一个复制函数了,由于stack中的top指针
是头指针,在复制的过程中不能再使用push函数相同的方法来把数据加到链表顶端,而是
要加在链表的尾部,所以定义了一个临时的tail尾指针。

 

stack::stack( const  stack  & s)
{
 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
时输出结果,也可以直接合并到递归里,判断左堆栈和临时栈同时为空即可输出结果。修改后得到了
以下的程序及结果:

//  trainFoo.cpp : Defines the entry point for the console application.
//
//  火车排列问题
// 作者: 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个结果.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值