表的游标实现
所谓游标就是指示数组单元地址的下标值,它属整数类型。我们可以用游标来模拟指针,将TElement类型的元素所组成的表用一个数组来实现,数组单元是记录类型,记录中包含一个表元素和一个作为游标的整数;具体说明如下:
Var
SPACE: array[1..maxlength] of
record
element:TElement;
next:integer;
end;
对于一个表L,我们用一个整型变量Lhead作为L的表头游标。SPACE[Lhead]就是L的表头单元,其中的elemnt域是空的,next域中的游标指示L的第一个元素在数组SPACE中的存储地址(数组下标值)。这样,我们就可以用游标来模拟指针,实现单链表中的各种运算。照此,我们虽然是用数组来存储表中的元素,但在作表的插人和删除运算时,不需要移动元素,只要修改游标,从而保持了用指针实现表的优点。因此,有时也将这种用游标实现的表称为静态链表。
下面我们介绍用游标实现表的另一种方式。这种方式不用表头单元,因此在表的第一个位置进行插入或删除时,需要进行特殊处理。这与在单链表中不用表头单元的情形类似。设L是一个表。我们用Lhead指示L的第一个元素,即L的第一个元素存放于SPACE[Lhead].element中,而SPACE[Lhead].next为L的第二个元素所在单元的下标值。其后每个元素的后继元素所在单元以类似的方式给出。如果Lhead或者某单元中next域的值为0,则表示这是一个空指针,即该游标不指示任何单元。表L可以用它的表头变量Lhead来代表。由于表头变量是整型变量,所以表的类型为整型。位置变量类型TPosition也是整型。与单链表中位置变量的意义相类似。我们约定,当i>1时,表示第i个位置的位置变量pi的值是数组SPACE中存储表L的第i-1个元素的单元的下标值,即SPACE[pi].next是指示第i个元素在SPACE中的下标。当i=1时,pi=O。
类型定义如下:
Type
TList=integer;
TPosition=integer;
|
|
SPACE
|
|
|
|
d
|
7
|
|
|
4
|
|
c
|
0
|
|
|
6
|
|
a
|
8
|
|
|
0
|
|
e
|
0
|
|
b
|
3
|
|
|
10
|
|
|
2
|
|
图4
用游标实现表
在图4中,两个表L(包含元素依次为a,b,c)和M(包含元素依次为d,e)存放于同一数组SPACE中,其中的maxlength=10。数组SPACE中末被占用的所有单元组成了另一个表available,由这个表提供L和M的备用单元。当我们要在表L或M中插入一个元素时,所用的新单元就取自表available。反之,从两个表中删除的单元要回收(插入)到表available中备用。
初始时,我们将数组SPACE中所有单元链接成表available备用。这个过程可实现如下。
|
procedure
Initialize;
Var
i:Integer;
begin
for i:=maxlength-l downto 1 do SPACE[i].next:=i+l;
available:=1;
{表available的第一个可用单元即表头在SPACE中的下标为1}
SPACE[maxlength].next:=0;
end;
|
要在表L中插入一个元素x,可将表available的第一个单元摘除,并将这个备用单元插入L的适当位置,再将这个新单元的element域赋值为x。
|
procedure
Insert(x:TElement;p:TPosition;var L:TLIST);
begin
if p=O then
(在第一个位置插入)
begin
if
move(available,L) then SPACE[L].element:=x
else error;
end
else {不在第一个位置插入}
if move(available,SPACE[p].next)
then SPACE[SPACE[p].next].element:=x
else error
end;
|
由于我们没有使用表头单元,所以必须单独处理在第一个位置插入的情形。另外,上述过程中用到了一个函数move(p,q),其功能是从某一链表中将游标p所指的单元C摘除,并将这个单元C插入到另一链表中游标q所指的单元之前,成功则返回true。我们可以先将q改为指向单元C,然后再将p改为指向单元C的下一单元,最后再将C中的游标改为指向q原来所指的单元即可。这个游标的修改过程如图5所示。
图5
两个链表之间的单元转移
图5中的实线和虚线分别表示单元转移前后的游标。当单元C存在时,函数move取值为true,并施行单元C的转移;当单元C不存在时,函数move取值为false。
|
function
move(var p,q;integer):boolean;
var
temp:integer;
begin
if
p=0 then return(false)
e1se
begin
temp:=q;
q:=p;
p:=SPACE[p].next;
SPACE[q].next:=temp;
return(true);
end
end;
|
要从表中删除位置p的元素,可以将L中位置p所指示的那个单元摘除,并将它插入表available的头一个位置备用。
|
procedrue
Delete(p:TPosition;var L:TList);
begin
if p=O then
move(L,available)
else move(SPACE[p]^.next,available)
end;
|
与单链表中的情形类似,为要删除表L中的一个元素x,先要找到x在表L中的位置。这可以用下面的函数Locate来实现。在表L中找到第一个与x相同的元素时,Locate返回x所在单元的位置,否则返回表尾位置。
|
function
Locate (x:TElement;L:TList):TPosition;
Var
p:TPosition;
begin
p:=L;
if p=0
then error('List is empty.');
if
SPACE[p].element=x then return(0);
while
SPACE[P].next<>0 do
if SPACE[SPACE[P].next].element=x then
return(p)
else
p:=SPACE[P].next;
return(p);
end;
|
由于我们是用游标来模拟指针的,上述各运算的复杂性分析与单链表中的情形是类似的。另外,上述程序中都省略了检查错误的语句。
循环链表
我们在用指针实现表时,表中最后一个元素所在单元的指针域(next)为空指针nil。如果将这个空指针改为指向表头单元的指针就使整个链表形成一个环。这种首尾相接的链表称为循环链表。在循环链表中,从任意一个单元出发可以找到表中其他单元。图6所示的是一个单链的循环链表或简称为单循环链表。
|
|
|
|
(a)非空表
|
(b)空表
|
图6单循环链表
在单循环链表上实现表的各种运算的算法与单链表的情形是类似的。它们仅在循环终止条件上有所不同:前者是p或p^.next指向表头单元;后者是p或p^.next指向空(nil)。
在单链表中我们用指向表头单元的指针表示一个表L,这样就可以在O(1)时间内找到表L中的第一个元素。然而要找到表L中最后一个元素就要花O(n)时间遍历整个链表。在单循环链表中,我们也可以用指向表头单元的指针表示一个表L。但是,如果我们用指向表尾的指针表示一个表L时,我们就可以在O(1)时间内找到表上中最后一个元素。同时通过表尾单元中指向表头单元的指针,我们也可以在O(1)时间内找到表L中的第一个元素。在许多情况下,用这种表示方法可以简化一些关于表的运算。例如要将图7(a)中两个表L1和L2合并成一个表时,只要修改两个指针值即可,运算时间为O(1)。合并后的表如图7(b)所示。
图7
两个单循环链表的合并
双链表
在单循环链表中,虽然从任一单元出发,可以找到其前驱单元,但需要O(n)时间。如果我们希望能快速确定表中任一元素的前驱和后继元素所在的单元,可以在链表的每个单元中设置两个指针,一个指向后继,另一个指向前驱,形成图8所示的双向链表或简称为双链表。
图8
双链表
由于在双链表中可以直接确定一个单元的前驱单元和后继单元,我们可以用一种更自然的方式表示元素的位置,即用指向双链表中第i个单元而不是指向其前一个单元的指针来表示表的第i个位置。
双链表的单元类型定义如下。
|
type
celltype=record
element:TElement;
next,previous:celltype;
end;
TPosition=^celltype;
|
和单链的循环表类似,双链表也可以有相应的循环表。我们用一个表头单元将双链表首尾相接,即将表头单元中的previous指针指向表尾,并将表尾单元的next指针指向表头单元,构成如图9所示的双向循环链表。
图9
双向循环链表
下面仅以双向循环链表为例给出三种基本运算的实现。
(1)在双向循环链表L的位置p处插入一个新元素x的过程Insert可实现如下。
|
procedure
Insert(x:TElement;p:TPosition;var L:TList);
Var
q:TPosition;
begin
new(q);
q^.element:=x;
q^.previous:=p^.previous;
q^.next:=p;
p^.previous^.next:=q;
p^.previous:=q;
end;
|
上述算法对链表指针的修改情况见图10。
图10
在双向循环链表中插入一个元素
算法Insert中没有检查位置p的合法性,因此在调用Insert之前应保证位置p的合法性。由于插入通常是在表的头尾两端进行的,所以容易检查位置p的合法性。
(2)从双向循环链表L中删除位置p处的元素可实现如下:
|
procedure
Delete(p:TPosition;var L:TList);
begin
if (p<>nil)and(p<>L) then
begin
p^.previous^.next:=p^.next;
p^.next^.previous:=p^.previous;
dispose(p^);
end;
end;
|
上述算法对链表指针的修改情况见图11。
图11
从双向循环链表中删除一个元素
与单链表中的删除算法类似,上述算法是在已知要删除元素在链表中的位置p时,将该位置所指的单元删去。若要从一个表中删除一个元素x但不知道它在表中的位置,则应先用定位函数Locate(x,L)找出要删除元素的位置,然后再用Delete删除之。
(3)在双向循环链表中,定位函数Locate可实现如下。
|
function
Locate(x:TElement;L:TList):TPosition;
Var
p:TPosition;
begin
p:=L^.next;
while
p<>L do
if p^.element=x then return(p)
else
p:=p^.next;
return(nil);
end;
|
算法Locate在最坏情况下需要的时间显然为O(n),n为表L的长度。算法Insert和Delete需要O(1)时间。在双向循环链表中,其他表的运算容易实现,这里就不一一说明了。另外,我们也可以用游标来模拟指针,从而实现双向链表和双向循环链表。