一、问题描述
车厢重排,不再赘述。
二、问题分析与解决
话不多说,先贴main函数代码:
int main()
{
int p[10] = {-1, 3, 6, 9, 2, 4, 7, 1, 8, 5 }; //入轨时,列车车厢的排列顺序
int n = 9;
int k = 5;
Stack *H;
int i, NowOut = 1, minH = n + 1, minS;
H = (Stack *)calloc((k + 1), sizeof(Stack) * (k + 1));
for (i = 0; i < k + 1; i++) {
Initial(&H[i]);
}//对栈的初始化,令其top为-1,方便后面的函数正常返回其值
//车厢重排程序
for (i = 1; i <= n; i++) {
if (p[i] == NowOut) {
printf("移动车厢%d从入口到出口\n", p[i]);
NowOut++;
while (minH == NowOut) {
Output(&minH, &minS, H, k, n);
NowOut++;
}
}
else {
if (!Hold(p[i], &minH, &minS, H, k, n))
return 0;
}
}
return 0;
}
让我们先对main函数分析,以整体感知解决该问题的思路。
第一步,先定义我们要处理的入轨车厢的序号排列,即p数组。其中第一个元素是-1的原因是,我们后面的操作都是从下标为以1开始的,目的是方便缓冲轨道序列号与其同步(这句话要是有点晕可以跳过理解下面的)。
第二步,定义整个问题中最关键的部分——缓冲轨道。缓冲轨道是栈类型,定义k+1个的原因,同样是因为我们操作时,是从下标1开始。紧接着利用,calloc,Initial函数,对其进行初始化。
第三步,就是车厢重排部分,其中包含着两个函数:一是Output函数,是该问题中的主要输出函数。二是Hold函数,是该问题中的主要排序函数。
因为车厢重拍问题比较庞大,对于笔者而言(我相信对于很多像我一样的初学者也是这样),最佳的分析办法就是先一直沿着程序,实际的“走”几个循环,看一看在内存中到底发生了什么是十分有助于理解的。
(二)对Hold函数的分析,同时看函数整体原理
先将Hold函数代码贴上来,然后跟着笔者的思路先走几个循环。
int Hold(int c, int *minH, int *minS, Stack H[], int k, int n)
{
int i;
int BestTrack = 0; //拥有最小栈顶的栈的索引,直接指向的是最佳栈的序号,方便操作。
int BestTop = n + 1; //这个变量保存的是当前执行的最佳top,即k个缓冲轨道栈中的最小的栈顶!程序运行期间会更新。
int x; //当前执行的缓冲轨道的最顶端元素。
//扫描缓冲轨道
for (i = 1; i < k; i++) {
if (!Isempty(&H[i])) {
//即当当前缓冲轨道不是空的时候
x = Top(&H[i]); //取当前缓冲轨道的栈顶元素的值。
if (c < x && x < BestTop) {
BestTop = x; //对最佳栈顶元素的更新 (与Beatstrack是对应的)
BestTrack = i; //对最佳缓冲轨道的更新
}
}
else {
if (!BestTrack)
BestTrack = i;
if (!BestTrack)
return 0; //73-76行代码是为了在第一次执行函数的时候,都是所有缓冲轨道都是空轨道的情况。把第一个最佳缓冲轨道给了第一个轨道
Push(&H[BestTrack], c);
printf("将%d车厢从入口移动到缓冲轨道%d\n", c, BestTrack);
if (c < *minH)
{
*minH = c;
*minS = BestTrack;
}
return 1;
}
}
}
首先,第一次hold传入的是“3”这个值,minH,minS都还是最初始无用的值,只起到限制作用。k,n两个值不需要再多赘述,而H[]是拥有k+1结构体的结构体数组,均是普通的栈结构。
当程序运行入hold中的时候,第一个循环,执行else中的代码块(H[1]是空栈),经过两个if,BestTrack的值更新为1,c(即“3”)被push入下标为BestTrack的缓冲轨道结构体数组H中,如下所示,是push操作后H[1]中的情况:
接着程序往下走,有必要更新minH,minS的值,因为显然,push操作结束后,最佳的栈是H[1],最佳的栈顶元素也就是3,即H[1].data[top];
到此,本次的Hold操作结束。
我们继续访问的第二个元素,是入轨中的“6”。当传入hold函数中后,我们不难发现,在上一次操作后的minH,minS是没有变的,因为它们是按照指针的形式传入了hold函数中去。而在hold函数中的BestTrack,BestTop都是每调用一次Hold函数,就重新定义一次,这是一种对该算法的宏观设计,我们很难在刚开始设计这种算法的时候就十分明确的想到这样十分具有全局意义的变量,有关此类算法的设计切入点与思考方式,在本文末会给出专门的思考,在此不提,暂且先继续往下运行程序。
“6”传入后,我们运行的是if后的代码块。先用变量x接受H[1]的栈顶元素,即上一个push操作被我们压入的元素“3”。经“if (c < x && x < BestTop)”判断,因“6”不满足这个条件,于是继续下一次循环,即访问第二个缓冲轨道然后执行else后面的代码块,这就与第一次push操作类似,操作后两个缓冲轨道栈的情况如下图:
然后,执行minH,minS的必要更新,但是,发现并不符合if (c < *minH)中的判断条件,即不满足更新的要求,也就是不需要更新。
程序到这里,可能很多像笔者这样的初学者仍然是云里雾里,一头雾水,尤其是minH,minS,BestTrack,BestTop的更新操作,是此问题解决的难重点。笔者这就结合我个人对此问题的看法,与过程中的疑惑来给大家做进一步分析。
(三)对算法的宏观性理解
在对此算法的探索过程中,最难的就是掌控算法中各操作的宏观性功能,我们初学者是很难具有这种视野的,解决这种情况的办法,就是对算法进行一步一步的演算,前面我们带着大家运行了入轨中的“3”、“6”两个元素,现在,笔者给大家一个宏观的思维图。
1、在缓冲轨道中对栈的性质的应用。
首先我们明确,最后,出轨上的元素排列,应当是[1 - 9]的顺序排列,而且,应当是1先出,依次为2,3,4... ...等。根据栈的LIFO(last in first off)性质,在缓冲轨道上的元素排列应当是从栈底到栈顶依次减小。那么很自然的想到,看懂算法的突破点,就在它在接受到一个c时,如何将它正确分配到合理的缓冲轨道中,且利于后续的输出轨道操作。
2、控制元素分配的算法结构解析
关键点,在于BestTrack,BestTop的运用。
首先,Hold函数中的if代码块,它的作用,是遍历H[]中的所有非空栈,寻找最佳的已存在的栈顶,即为接受的变量c,找一个比c大,且在H[]中已存在栈顶元素中最小的栈顶元素。然后,当遍历到空栈的时候,就不再执行if后的代码块,而跳到else后的代码块中,运行push操作。而push操作的对象,是变量c,即将push进入的栈,是用if代码块中已经选定的下标BeatTrack对应的H[]数组中的结构体。即H[BesstTrack]。完成后,进行对minH,minS的更新,因为在原来最佳的栈顶的上面,又push了一个新的,更小的元素。这个算法中比较难分析的就是,for循环中的if代码块是有时候有BestTrack的更新,有时候没有,笔者称它为算法中,变量更新的实步虚步问题,在约瑟夫环中,也有遇到过类似的情况。今后要养成对实步虚步算法的宏观把控理解。
(四)对OutPut函数的解析
先贴代码:
void Output(int *minH, int *minS, Stack H[], int k, int n)
{
int c, i;
c = Pop(&H[*minS]);
printf("移动车厢%d从入口到出口\n", c);
*minH = n + 2;//在pop操作结束后需要对minH,minS进行一次更新,下面的for循环就是为了遍历整个缓冲轨道栈的栈顶元素(因为栈顶肯定都是最小的元素)来实现对minH,minS的更新。
for (i = 1; i <= k; i++) {
if (!Isempty(&H[i]) && (c = Top(&H[i])) < *minH) {
*minH = c;
*minS = i;
}
}
}
在所有的push操作结束后(到“1”为止),我们可以画出最后的缓冲轨道情况:
结合main函数,output函数主要是对当minH存储的数据与nowout值相等时的处理,其功能是配合main函数中的while循环实现的,而Output函数的地位,其实是在识别到“1”的时候,对其后续元素的一项操作。下面我们进行一个一个输出,以便理解该函数。
当读取到1时,我们就可以将其直接从入轨弹到出轨。接着,循环检测minH,minS存储的元素,是否符合下一个输出数,也就是2。发现minH,minS中存储的内容,就是缓冲轨道1的栈顶元素——2,所以进行第二次弹出。然后第三次——元素3,第四次——元素4。完成后,因为5元素仍然在入轨中,故我们不能进行下一项的弹出,仍然需要Hold操作一次——把8元素添加入第四个缓冲轨道中,再开始弹出5元素到出轨,依次往后推... ...
到此大家应该对该算法有了一定的宏观了解。总结一句就是,Hold操作用来将入轨中的各元素弹入缓冲轨道中,并且通过BestTrack,BestTop两个变量来识别、记录最佳的缓冲轨道以及最佳缓冲轨道的栈顶元素,而minH,minS则用来记录每次Hold操作结束后的真正的最佳栈顶以及最佳栈的栈顶元素,目的是为了在主函数中的Output函数输出。即BestTrack,BestTop是工具,minH,minS是实际存储。
三、程序运行结果
附图:
四、总结
车厢重排问题,不免对于数据结构初学者来说是一个十分复杂的算法结构,它要求我们队整个算法有宏观把控,各个变量的作用都需要我们又十分深刻的理解。而在分析过程中,最好的办法就是用实际数据沿着算法的逻辑“走一走”,画出内存中变量图,好好理解一下就会对算法有了一定宏观认识。
PS:初来乍到,同属于处女作,只是希望能够把学习过程中的思考与感悟找个地方写出来,多多积累。