本文首先介绍堆栈的相关概念,接着利用C++语言实现堆栈类,最后利用堆栈来解决“火车车厢重排问题”。
1. 堆栈的概念
参考文献[1]中对堆栈的解释:堆栈数据结构是通过对线性表的插入和删除操作进行限制而得到的插入和删除操作都必须在表的同一端完成),因此,堆栈是一个后进先出(last-in-first-out, LIFO)的数据结构。
由此可知,堆栈就是一种特殊的线性表,那么线性表的相关操作,如元素的增删操作都可以用到堆栈上面。而且,堆栈中元素的增删只是对于栈顶的操作。
2.堆栈的C++描述
首先声明堆栈类Stack,在Stack类中实现一下操作:
- 堆栈的建立—构造函数实现
- 堆栈元素指针的删除—析构函数 实现
- 判断堆栈是否为空—IsEmpty()函数实现
- 判断堆栈是否已满—IsFull()函数实现
- 获取栈的大小—Length()函数实现
- 返回栈顶元素—Top()函数实现
- 向栈中添加元素(元素入栈)—Add()函数实现
- 删除栈顶元素(元素出栈)—Delete()函数实现
- 输入一个栈—重载“<<”操作符
- 输出一个栈—重载“>>”操作符
C++代码如下:
/*----------------------------公式化描述的栈--------------------------*/
template<class T>
class Stack
{
public:
Stack(int MaxSize = 10)
{
Maxtop = MaxSize - 1;
stack = new T[MaxSize];
top = -1;
}
~Stack() { delete[] stack; };
bool IsEmpty() { return top == -1; } //判断栈是否为空
bool IsFull() { return top == Maxtop;} //判断栈是否满
//获取栈的大小
int Length() {
return top + 1;
}
T Top() //返回栈顶元素
{
if (!IsEmpty()) //栈非空
return stack[top];
else
throw OutOfRange();
}
template<class T>
Stack<T>& Add(const T &x) //向栈中添加元素
{
if (!IsFull()) //还有添加空间
stack[++top] = x;
else
throw NoMerm();
return *this;
}
template<class T>
Stack<T> &Delete(T &x) //删除栈顶元素,并给x
{
if (!IsEmpty()) //栈非空
x = stack[top--];
else
throw OutOfRange();
return *this;
}
//将栈元素送入输入流
void Input(istream &in)
{
cout << "请输入栈中元素个数: " << endl;
cin >> size;
cout << "请输入栈中所有元素: ";
for (int i = 0; i < size; i++)
{
int value;
in >> value;
Add(value);
}
cout << endl;
}
//将栈元素送至输出流
void Output(ostream &out) const
{
for (int i = 0; i <= top; i++)
out << stack[i] << " ";
}
private:
int size; //栈中元素个数
int top; //栈顶
int Maxtop; //栈顶最大值
T *stack; //栈元素指针
};
template<class T>
ostream &operator<<(ostream &out, const Stack<T> &S)
{
S.Output(out);
return out;
}
template<class T>
istream &operator >>(istream &in, Stack<T> &S)
{
S.Input(in);
return in;
}
3. 堆栈的应用—火车车厢重排问题
3.1 问题重述
一列货运列车共有n节车厢,每节车厢将停放在不同的车站。假定n个车站的编号分别为1 ~n,货运列车按照第n站至第1站的次序经过这些车站。车厢的编号与它们的目的地相同。为了便于从列车上卸掉相应的车厢,必须重新排列车厢,使各车厢从前至后按编号1到n的次序排列。当所有的车厢都按照这种次序排列,在每个车站只需卸掉最后一节车厢即可。我们在一个转轨站里完成车厢的重排工作,在转轨站中有一个入轨、一个出轨和k个缓冲铁轨(位于入轨和出轨之间)。下图给出了一个转轨站,其中有k= 3个缓冲铁轨H1,H2和H3。开始时,n节车厢的货车从入轨处进入转轨站,转轨结束时各车厢从右到左按照编号1至编号n的次序离开转轨站(通过出轨处)。在图(a) 中,n= 9,车厢从后至前的初始次序为5,8,1,7,4,2,9,6,3。图(b) 给出了按所要求的次序重新排列后的结果。(引自参考文献[1])
3.2 问题分析
对于上述问题,由于火车车厢的编号是从1一直增大到n的,因此在输出端一定会是“1,2,3……,n-1,n”依次排列。那么,对于给定序列,我们可以依次从左至右遍历序列,若遇到需要输出的编号,则直接输出;否则,将其放入缓冲铁轨(堆栈)中。
从而引出一下问题:
问题1. 需要输出的编号怎样设置?——-初始需要输出的编号为1,此后只要输出一个(车厢)编号,则下一步需要输出的编号+1;
问题2. 怎么将无法直接输出的车厢编号放入缓冲铁轨?
设想给定k个缓冲铁轨(建立k个堆栈),编号为1~k。初始时每个缓冲铁轨(堆栈)都为空。当原始序列中的火车车厢编号无法直接输出时,将它压入第i个栈的条件为:
(1)此栈为空;
(2)此栈不为空,并且此栈的栈顶元素大于需要压入的车厢编号,同时,此栈的栈顶元素在所有的栈顶元素中是最小的。
从而,又引出下列问题:
问题3. 如何判断此栈顶元素是否是所有栈顶元素中的最小值?——已知k个栈,那么依次可以获取这k个栈的栈顶元素,定义最小栈顶元素值为min_value,最小元素所在的栈的编号为min_stack。初始时,设定min_value=n+1(即大于任何火车车厢编号),min_stack=0。在遍历所有栈的过程中,根据入栈条件依次更新min_value和min_stack的值。
通以上分析,可以整理出问题的解决思路:
上述流程图中,存在两个子流程“在缓冲铁轨栈中查找是否有需求值”和“将元素放入缓冲铁轨堆栈中”。因此,在设计程序时,可将这两个子流程作为两个子函数。
3.3 程序设计
3.3.1 主函数“ArrangeCar”
根据上述流程图,设计主函数如下:
bool ArrangeCar(int *a,int n,int k) //数组a为入站前车厢顺序,n为车厢数,k为缓冲铁轨数
{
int NowNeed = 1; //当前需要输出的车厢编号
Stack<int> *S;
S= new Stack<int>[k + 1]; //申请k个栈
int min_stack,min_value=n+1; //min_stack为最小的缓冲铁轨编号,min_value为所有缓冲铁轨上的最小值,初始值设为n+1,即所有车厢编号的最大值
for (int i = 1; i <= n; i++)
{
if (a[i] == NowNeed) //如果第i个车厢正好为当前需要输出的车厢,则输出
{
cout << "将车厢 " << a[i] << " 从输入端直接输出 "<< endl;
NowNeed++; //相应的需求车厢索引+1
while(min_value==NowNeed) //继续在缓冲铁轨中找
{
FindInStack(min_value, min_stack, S, k,n);
NowNeed++;
}
}
else //如果找不到想要的车厢索引值,则将其放入缓冲铁轨中
{
if (!ArrangeBox(a[i],min_stack,min_value, S, k,n))
return false;
}
}
return true;
}
3.3.2 子流程—在缓冲铁轨栈中查找是否有需求值
//从缓冲铁轨中找出需要的火车索引,注意min_value和min_stack都是变量的引用
void FindInStack(int &min_value,int &min_stack,Stack<int> *S,int k,int n)
{
int c; //车厢索引值
S[min_stack].Delete(c); //将当前的最优缓冲铁轨中的栈顶元素删除,即输出
//输出当前满足条件的车厢号和缓冲铁轨编号
cout << "将车厢 " << min_value << " 从缓冲铁轨 "
<< min_stack << " 输出" << endl;
//给定初值
min_value = n+2;
//遍历每个缓冲铁轨,更新最优铁轨编号和当前所有缓冲铁轨中的最小车厢编号
for (int i = 1; i <= k; i++)
{
if (!S[i].IsEmpty() && S[i].Top() < min_value)
{
min_value = S[i].Top();
min_stack = i;
}
}
}
3.3.3 子流程—将元素放入缓冲铁轨堆栈中
//向缓冲铁轨中放置车厢
bool ArrangeBox(int c,int &min_stack,int &min_value, Stack<int> *S,int k,int n)//参数n为当前需要放入的车厢索引值,S为缓冲铁轨,k为缓冲铁轨数
{
int bestTrack = 0, //最小车厢索引值所在的缓冲铁轨编号
bestTop = n + 1; //缓冲铁轨中的最小车厢编号
for (int i = 1; i <= k; i++) //遍历每个缓冲铁轨
{
if (!S[i].IsEmpty()) { //若缓冲铁轨不为空
//若当前缓冲铁轨的栈顶元素大于当前要放置的车厢编号,并且其栈顶元素小于当前缓冲铁轨中的最小编号
//则更新最优缓冲铁轨编号和缓冲铁轨中的最小车厢编号
if (S[i].Top() > c && bestTop > S[i].Top())
{
bestTop = S[i].Top();
bestTrack = i;
}
}
else //若当前缓冲铁轨为空,并且bestTrack值仍为初始值,即没有被改动,则更新最优缓冲铁轨编号
if(!bestTrack)
bestTrack = i;
}
//如果没有符合条件的缓冲铁轨,则不能继续放置,意味着不能完成车厢排序工作
if (bestTrack == 0) return false;
//若程序能继续运行,则将当前的待放置车厢入栈至最优缓冲铁轨中
S[bestTrack].Add(c);
cout << "将车厢 " << c << " 从输入端 "<< "送入缓冲铁轨 " << bestTrack << endl;
//更新当前的所有缓冲铁轨的最小值和最优的缓冲铁轨编号
//注意min_value和min_stack都是通过引用方式访问的,即在函数中的变化会导致变量值的变化
if (c < min_value)
{
min_value = c;
min_stack = bestTrack;
}
return true;
}
3.4 测试
3.4.1 考虑一下三个测试样本:
- 待排序车厢编号为: 5 8 1 7 4 2 9 6 3
- 待排序车厢编号为: 3 2 1 4 5 8 7 9 6
- 待排序车厢编号为: 3 6 9 2 4 7 1 8 5
3.4.2 测试代码
在数组中,多了第一个元素“0”,原因是在遍历每个待排序车厢序列时是从数组的第2个元素即a[1]开始的。
int a[] = { 0,5,8,1,7,4,2,9,6,3 };
cout << "输入的车厢编号为 0 5 8 1 7 4 2 9 6 3" << endl;
cout << ArrangeCar(a, 9, 2) << endl << endl;
int b[] = { 0,3,2,1,4,5,8,7,9,6 };
cout << "输入的车厢编号为 0 3 2 1 4 5 8 7 9 6" << endl;
cout << ArrangeCar(b, 9, 2)<<endl<<endl;
int c[10] = { 0, 3, 6, 9, 2, 4, 7, 1, 8, 5 };
cout << "输入的车厢编号为 0 3 6 9 2 4 7 1 8 5" << endl;
cout<<ArrangeCar(c, 9, 3)<<endl;
3.4.3 测试结果
参考文献:
[1] 数据结构算法与应用:C++描述(Data Structures, Algorithms and Applications in C++ 的中文版)