需要保存哪些信息?和普通线性表不同,调用栈的Push,Pop方法时,被操作的元素所处的位置是调用方和被调用方之间的一种约定,这里约定为栈顶的元素。类似的,双方约定调用队列的Enqueue、Dequeue所操作的元素分别为队尾和队头。既然这一信息不是由参数传入,就需要类型自行维护。总结一下,需要保存的是待操作元素的位置信息。每个栈有一个,每个队列有两个。
逻辑层信息和存储层信息,选择保存哪一个?假设你有一个菜谱,如果你想照着它炒出一盘菜,你还需要存放和操作食材的厨房。每个逻辑数据结构就好像一个菜谱,如果你想用程序实现它并运行起来,你还需要一个存储和操作它的介质,那就是物理存储结构,比如数组。数组是存储层的结构,而栈是逻辑层的结构,随之而产生的是每个元素都有两个位置信息,我叫它们逻辑层下标和存储层下标。两者构成一对映射,通常是满射。通过映射法则,两者可以互相求得。所以我们只需要在数据成员中保存一方,就可以在需要时计算出另一方。我在上面的实现中,都选择了保存逻辑下标,并将计算存储下标的工作封装在一个函数中,这样做的好处是:1. 几乎每个接口都有逻辑层的处理,但不是每个都需要动用存储层逻辑,所以保存逻辑层信息可以使得接口在逻辑层的处理更直接高效。例如,IsEmpty接口就无需计算存储层下标;2. 当你想改换一种物理存储结构时,例如从数组改为链表,你只需要修改从逻辑层下标到物理层下标的映射过程,灵活性较好。
映射法则可以很灵活。通常,考虑效率和易读性,栈下标x到数组下标f(x)的映射法则往往定义为:f(x) = x。如果你很任性,就想玩些花样,其实你完全可以把数组当做环形数组,将数组的任意位置作为起始点,比如f(x) = x + 2,就是用数组的第三个元素存储第一个入栈的元素,满栈前最后两个入栈的元素放在数组第一个,第二个元素上。或者你还可以倒过来存储,f(x) = 数组总长度 - 1 - x;甚至你可以毫无规律地将逻辑下标{0, 1, 2, 3}映射成存储下标{3, 1, 2, 0},只要它是满射,只要你开心。为什么我要这样折腾这个映射法则呢?因为有时候,它可以帮助我们灵活地解决问题。例如下面的习题10.1-2,如何将两个栈的逻辑下标映射到一个数组的存储下标上去,既要彼此不干扰,又可以最大限度利用数组,这便是映射法则的用武之地了。具体实现见函数DoubleStack::TopIndexInArray。
10.1-4 同例题2
10.1-5 双端队列
按照前面总结的规律,需要保存的是待操作元素的位置信息,这里有两个待操作元素的位置会移动,所以保存它们在逻辑层的下标,m_begin, m_end,分别表示队头和队尾元素的下一个元素的下标。未完待续...