你所能用到的数据结构(九)
十二、为了count的最终胜利
在介绍完最基本的堆栈模型之后,下面要继续的是第二种最基本的模型,队列。队列,在现实生活中经常可以看到(不过考虑到在我国大部分人都不排队的事实,可能还真不是能经常看到),计算机最开始需要这样一个模型是为了解决在计算机的初期,放入计算机执行的作业排队的问题。现在队列同样也在计算机中有着广泛的应用,windows的消息队列,操作系统进行调度的作业等等,和堆栈一样,基础的东西往往在构成庞大系统中发挥着重要的作用。
先来介绍一下队列的模型的概念,如果你看到现实生活中的队列能抽象出什么特点?数学家和一般人差别就是他们能够从任何常见的事物中抽象出模型,然后用数字公式描绘他们,所以我们假定很多很多年以前,一位数学家兼软件工程师在自己的实验室中正苦思冥想如何将现实中常见的队列用在数学上或者计算机等等神马的上面,首先最简单的,正如我们看到的,队列也是一种容器,在现实中,这种容器里面的元素基本上是人,所以在编程的时候不免要用某一种更底层的结构,我们暂时定为数列吧,然后排队为了什么,为了个顺序,为了个公平,先排上队的人能够先得到相应的服务,后来的人只能排到末尾(考虑到大家都是有素质的情况下,计算机就是由素质的,从来不插队,但是也是因为计算机只能按照特定的指令执行,某种意义上也是因为计算机“傻”的原因,所以很多人把插队的借口也解释为懂得变通),这种模式可以用“先排队先服务”来描述,现实中排队先完成服务的人就先闪了,这种同样也可以叫做“先进先出”,最后,比如在银行排队,银行大厅就那么大,如何摆出队列的造型来让更多的人能够排上队和更高效的进行服务往往是最需要考虑的问题。
基于上述的三个考虑,一个一个的具体到程序上,最开始,用什么作为底层的数据结构,既然你有了堆栈的基础,那么无非是两种,一个数组,一个是链式的结构,为了保持和上一篇的高度一致,先尝试用数组,第二个问题,怎样能够完成“先进先出”这个模式呢?对于一个数组,先进先出无非是从某一段插入元素从另一端取出元素,比如我一直往数组的末端插入元素,一直从数组的前端插入元素,或者反过来,但是考虑反过来如果实现起来不太符合人类正常的思维习惯,采用从下标小的那一端取出元素,下标大的那一端插入元素好了,第三个问题,如何摆造型,计算机的内存地址就是一块连续“平坦”的区域,怎么样还能摆出造型来?看起来很不可思议,但是数学之所以叫抽象学科,就是能够抽象出模型解决实际问题,我觉得整个队列就这里是其程序实现上的闪光点。
对于第三个问题,我们先摆一摆,我们来看一看,我们如何实现队列的计数问题,也就是说怎么样得到队列里面的元素的数量?最原始的方法,自然是声明一个成员变量进行计数,那么先从这个最原始的办法开始,队列的头文件如下所示:
1 #ifndef QUEUE_H 2 #define QUEUE_H 3 class Queue{ 4 public : 5 Queue(int size); 6 ~Queue(); 7 8 void InQueue(int ele); 9 int DeQueue(); 10 void Print(); 11 bool CheckEmpty(); 12 int GetCount(); 13 14 private: 15 int *queueArray; 16 int front; 17 int back; 18 int count;//计数变量 19 int arraySize; 20 }; 21 #endif
和堆栈的头文件相似,在这个头文件里有一个成员变量count,我们用它来统计队列里元素的个数,front是指向队列头部的下标,back是指向队列尾部的下标,这两个变量存在的意义是要完成从头部取出元素,从尾部插入元素,这里我还实现了一个print的成员函数,用于形象的输出队列里面的内容和数组实际上的内容。先看看我们第一次实现的队列类。
#include <iostream> #include <string> #include <fstream> #include "Queue.h" using namespace std; Queue::Queue(int size) { queueArray=new int[size]; for(int i=0;i<size;i++) queueArray[i]=0; count=0; front=back=0; arraySize=size; } Queue::~Queue() { delete []queueArray; front=back=0; count=0; } bool Queue::CheckEmpty() { return (count==0); } void Queue::InQueue(int ele) { if(GetCount()==arraySize) {cout<<"The queue has been full"<<endl;return;} queueArray[count]=ele; back=count; count++; } int Queue::DeQueue() { if(back==front) {cout<<"The queue has been empty"<<endl;exit(0);} front++; count--; int ele=queueArray[front-1]; queueArray[front-1]=0; return ele; } int Queue::GetCount() { return count;//修改count的地方 } void Queue::Print() { cout<<"The real situation of the array:"<<endl; for(int i=0;i<arraySize;i++) cout<<"|"<<queueArray[i]; cout<<endl; //cout<<" "; for(int i=0;i<front;i++) cout<<" "; cout<<"f"; for(int i=front;i<back;i++) cout<<" "; cout<<"b"<<endl; cout<<"What you see from the queue:"<<endl; for(int i=front;i<front+GetCount();i++) cout<<"|"<<queueArray[i]; cout<<endl; } void main() { Queue q(10); for(int i=0;i<10;i++) { q.InQueue(i); //if(i%2==0) } q.DeQueue(); q.DeQueue(); q.Print(); int i; cin>>i; }
但从实现上,遵从了上面所说的思路,对于count,如果入队一个元素,count就加一,如果要出队一个元素,count就减一,看看这样实现的结果是个什么样子的。
可以看到在连续入队十次然后出队两次,可以看到队头f在2处,队尾b在9处,这样构造出的队列如就是在后面输出的样子,从这里可以看到两个问题,第一个是count其实可以通过队尾的下标和队头的下标经过运算得到,不需要特别声明一个成员变量来记录,从图中可以看出如果将b的下标减去f的下标加1就是count的值,那么我们先试试看,把注释修改count的地方改成return back-front+1;再运行一次看看是不是和我们猜想的一样。
这是再次运行后的结果(我没有骗你,我真的再一次的运行了),看起来是和我们看到的是一样的,所以说我们可以把类实现和类头文件中涉及到count的都删去,再略作修改。那么再进行观察会发现第二个问题,就是在实现队列的数组中实际上还有没有用到的位置,因为出队和入队的关系(前两个元素目前是空的),但是我们现在已经不能忘数组中添加元素了,因为队尾的下标已经到达了最后一个位置,分配的数组空间已经用完了,如果添加的话势必会出错,看到在InQueue中,我们是通过count是否等于数组大小来进行判断的,此时count不等于数组大小,但是整个数组的空间已经用完了,所以会发生不可预料的错误,有兴趣可以用上面的程序执行试一下看看。
这个问题如何解决呢?神奇的大牛们想出了一个“重新排列数组造型的方法”,现在看到队尾的指针b已经到了最后一个位置,而最前面一个位置还没有用过,那么如果我能在下一个元素入队的时候将尾标示移动到头部,重新指向未被使用的位置上,这样岂不是可以满足要求?看起来这是一种循环啊!剩下来的就是怎样用计算机程序实现这个要求了,怎么样能够使一种计算一直在一个圈子里面转呢?比如说1 2 3 1 2 3 ,永远都只能是1 2 3,这种运算不能产生其余的数,很自然的我们想到了%(取余)运算,当一个数mod 3的时候,那么他永远只能产生0 1 2 三个数,应用于我们的数组中,对于上面的实现,数组有10个数,如果想让尾下标在递增的过程中直接指回头元素,那么我们其实可以采用(back+1)%10,来验证一下先,现在的back等于的是9,加1等于10,mod 10正好等于0,这样就神奇的回到了数组的头部,按照这个理论,我们修改我们的程序,同时在inqueue和dequeue中都要修改,因为front也有可能到最后一个元素的。我现在把inqueue和dequeue修改如下:
1 void Queue::InQueue(int ele) 2 { 3 if(GetCount()==arraySize) 4 {cout<<"The queue has been full"<<endl;return;} 5 6 7 queueArray[back]=ele; 8 back=(back+1)%10; 9 //count=(count+1)%10; 10 11 } 12 13 int Queue::DeQueue() 14 { 15 if(back==front) 16 {cout<<"The queue has been empty"<<endl;exit(0);} 17 int ele=queueArray[front]; 18 queueArray[front]=0; 19 front=(front+1)%10; 20 /*count--;*/ 21 22 return ele; 23 }
注意在新版程序中,back指的是下一个插入位置啦。所以说getcount的加一也不要了,下面思考一个问题,这样简单的修改getcount是不是对的?思考这样一个问题,如果back现在是数组的最后一个元素,通过加一之后标示了第一个元素,假设front是在2这个位置,那么如果back-front就等于-2,这明显不对的,此时的元素个数应该是8个,参考前面的截图(注意在新版程序中,back已经是下一个入队的位置。),这时候一观察就可以发现数组的长度10加上这个-2就能得到正确的答案,而如果采用判断这个数是不是负数而加上10,这样的话就太过于繁琐了,所以我们还是使用mod(在计算机中取余和抑或这两个操作,我一直认为是最神奇的操作)。getcount改成 return (back-front+10)%10;这样就可以完美的解决上面的问题。如果你把这些都修改完了,执行上面的程序,会发现执行之后屏幕会一闪而过,这是为什么?注意到dequeue中,程序采用的front是否等于back来判断队列是否为空,初步看起来没有问题,因为如果队尾和队头一样的,当然队列里面没有元素,但是实际上在我们采用环形结构以后,这样做就不正确了,可以用现实的这个情况来看,现在back指向的是第一个元素,front同样也是,而这个时候队列是满的,而不是空的,而这种情况和队列为空时完全一致,所以说如果想判断队列为空就不能这么简单了,比较常用的一种方法是空出一个元素不用,也就是10个数组只用9个,比如第一个空位不用,初始状况下front和back都等于1(下标从0开始),如果back+1等于front的话就说明满了,如果back=front的话就是空的,原理很简单,只要对着上面的元素慢慢推演一下就可以了。程序我想先不写出来,思考毕竟是进步的阶梯,下一次我再把这个程序贴出来好了。