I think that I shall never see
A poem lovely as a tree.
我想我永不会见到一首诗
会像一棵树一样可爱。
目录
计算机程序通常都是对一些信息表进行操作。在大多数情况下,这些信息表并不仅仅是杂乱无章的数值;它们含有数据元素之间重要的结构关系。
一个线性表是 n >= 0 个节点 X[1],X[2],.....,X[n] 的一个序列,当这个序列的所有项出现在一行中时,这个序列的结构性质仅仅涉及这些项的相对位置。对于这样一个结构,我们关心的事情是:如果 n >0,X[1] 是第一个节点,X[n] 是最后的节点;如果 1 < k < n,则第 k 个节点是在 X[k - 1] 的后面且在 X[k + 1] 的前面。
(在这篇文章中,作者几经修改后,对于线性表的下标选择用 1 开始而不是 0 开始,这样是不符合计算机和编程语言的实际情况的,然而却对我们后面的讨论有极大的方便性,希望各位读者原谅。)
我们对线性表施加的运算有下面这些:
(1)查询表的第 k 个节点的内容或改变表的第 k 个节点的内容;
(2)在第 k 个节点的前面或后面插入一个节点;
(3)删除第 k 个节点;
(4)把两个或两个以上的线性表合并成一个表;
(5)把一个线性表分成两个或两个以上的表;
(6)复制一个线性表;
(7)确定一个表中的节点个数;
(8)基于节点的某些字段把表的节点排成递增顺序;
(9)在表中查找在某些字段中具有特定值的一个节点。
在(1)(2)(3)的操作中,k = 1 和 k = n 的特殊情况具有同等重要性,因为一个表的第一项和最后一项比其他的项更容易得到。
我们一般依照要实行的主要操作来区分线性表的种类。
我们经常遇到的线性表操作有:
(1)在线性表的第一个节点和最后一个节点插入值;
(2)查找线性表的第一个节点和最后一个节点的值;
(3)删除线性表的第一个节点或最后一个节点的值。
因此我们给这种线性表取特殊的名字:
(1)栈:栈是所有的插入操作和删除操作(通常查找操作也是这样)都在表的一端进行的一种线性表;
(2)队列:队列是所有的插入操作都在表的一端进行,但是所有的删除操作(通常查找操作也是这样)在表的另一端进行的一种线性表;
(3)双端队列:双端队列是所有的插入操作和删除操作(而且通常所有的查找操作也是一样)在表的两端进行的一种线性表。
因为双端队列比队列和栈更为一般,它和一组扑克牌某些具有相同的性质。
当然,我们也区分输入受限和输出受限的双端队列,即仅允许在一端进行插入操作和删除操作的双端队列。
对于一个栈,总是删去当前线性表中 “最新的” 项,即线性表中的最后一项;对于一个队列,则是反过来,总是删去当前线性表中 “最老的” 项,对于队列来说,节点离开表的顺序和它们进入表的顺序相同。
当引用这些结构时,我们一般使用特殊的术语:当栈添加数据时,我们说把一项数据压入栈的顶部,当栈删除一项数据时,我们说把一项数据从栈的顶部弹出。栈相对应于顶部还有一个底部,栈的底部是最少访问的项,在其它项被删除之前它不会被删除。
对于队列,我们说队列具有前端和后端,数据进入后端,当它们到达前端的位置时被删去。
对于双端队列,我们说双端队列具有左端和右端。
顶部、底部、前端和后端这些概念有时也用于输入受限或者输出受限的双端队列中,而且没有标准约定顶部、底部、前端和后端应当出现在左边还是右边。
所以出现了丰富多样的描述性词汇:对于栈,使用 “上下” 这样的术语,对于队列,使用 “在队列中等候” 这样的术语,对于双端队列使用 “左右” 这样的术语。
为了处理栈和队列,我们再增加一点附加的几号是方便的,我们设:
(1)
当 A 是一个栈时,(1) 表示把元素 x 插入到栈 A 的顶部;
当 A 是一个队列时,(1) 表示把元素 x 插入到队列 A 的尾部。
类似的,记号:
(2)
表示从(栈或队列)A 中删除元素 x,当 A 为空时,记号 (2) 毫无意义。
如果 A 是非空的栈,我们用记号:
来表示栈 A 中的顶部元素。
在一台计算机内保存一个线性表最简单和最自然的方法是把线性表中的数据项放进连续的单元中,即一个挨一个地存放。于是我们可以自然地得到一个结论:
是用来表示变量在内存中地址的一个函数符号,例如
表示
在内存中的地址。
是每个节点的字数,通常
,当
时,有时会把一个表分为
个 “平行” 的表。
所以我们可以得到一个更一般地结论:
(1)
其中, 是被称为基地址的常数,基地址就是线性表的头一个节点,即
所在的内存地址。
这一表示线性表的技术如此明显和简单,似乎不需要再对它费任何口舌。但是,我们以后会接触到许多更复杂的表示方法,因此首先观察简单情况,是一个好想法。重要的是既要理解顺序分配的效力,也要弄清楚它的局限。
顺序分配对于处理栈来说是非常方便的,我们简单地设定一个叫做栈指针的变量 T。当栈为空时,令 T = 0。为了把一个新元素放到栈的顶部,我们置:
(2)
当栈非空时,我们可以令 Y 等于顶部节点以及通过逆转上面的操作删去该节点:
(3)
一个队列的表示是需要一些技巧的。一个简单的方法就是设定两个指针,比如 F 和 R(表示队列的前端和后端),当队列为空时,我们得到:F = 0,R = 0,于是把一个元素插入队列后端,则是这么表示的:
(4)
在前端删去节点(F 刚好指向前端),则是:
如果 ,则置
(5)
但要注意到会发生这种情况,如果 R 总是在 F 之前(说明队列中总是至少有一个节点),则队列所使用的线性表的表项是 X[1], X[2] .... X[1000], ..... 直到无穷,这是对于存储空间极其糟糕的浪费。因此 操作(4) 和 操作(5) 的方法只能用在 F 经常赶上 R ---- 例如,把队列清空时,才加以使用。
为了避免队列过多地占用存储器的问题,我们可以把 M 个节点 X[1] .... X[M] 放置在一个圆圈上,并使得 X[1] 紧挨着 X[M]。那么上面的 操作(4) 和 操作(5) 就会变成:
如果 ,那么令
,否则令
(6)
如果 ,那么令
,否则令
(7)
我们迄今为止的讨论是非常不实际的,因为在我们讨论这些操作时,我们暗中假定,什么东西都不出错。当从一个栈或一个队列删除一个节点时,我们暗中假定至少有一个节点存在。当向一个栈或一个队列插入一个节点时,我们也是暗中假定在存储器中有足够存放节点的空间。但是我们很清楚,在 操作(6) 和 操作(7) 中,我们允许整个队列最多有 M 个节点存在。而在操作 (2)(3)(4)(5)中,我们允许 T 和 R 达到在任何给定的计算机程序内的一个确定的最大值。下列方法说明我们在普通的情况下怎样重写这些操作:
(节点 Y 插入栈 X)
;
如果 ,那么报
(上溢) 警告;
否则,令 (2a)
(从栈 X 删去节点 Y)
如果 ,那么报
(下溢)警告;
否则,令 (3b)
(节点 Y 插入队列 X)
如果 ,那么令
,否则,令
;
如果 ,那么报
(上溢) 警告;
否则,令 (6a)
(从队列 X 中删除节点 Y)
如果 ,那么报
(下溢) 警告;
如果 ,则令
,否则令
;
(7a)
这里我们假定X[1],X[2],...... , X[M] 是线性表总共的空间数量, 和
是指项的富余和不足。当我们使用 (6a) 和 (7a) 时对队列指针
这个初始设定不再是正确的,因为当
时溢出检测不出来。我们应当以
开始。