实现表的另一种方法是用指针将存储表元素的那些单元依次串联在一起。这种方法避免了在数组中用连续的单元存储元素的缺点,因而在执行插入或删除运算时,不再需要移动元素来腾出空间或填补空缺。然而我们为此付出的代价是,需要在每个单元中设置指针来表示表中元素之间的逻辑关系,因而增加了额外的存储空间的开销。
为了将存储表元素的所有单元用指针串联起来,我们让每个单元包含一个元素域和一个指针域,其中的指针指向表中下一个元素所在的单元。例如,如果表是a1,a2,…,an ,那么含有元素ai的那个单元中的指针应指向含有元素ai+1的单元(i=1,2,…,n-1)。含有an的那个单元中的指针是空指针nil。此外,通常我们还为每一个表设置一个表头单元header,其中的指针指向开始元素中所在的单元,但表头单元header中不含任何元素。设置表头单元的目的是为了使表运算中的一些边界条件更容易处理。这一点我们在后面可以看到。如果我们愿意单独地处理诸如在表的第一个位置上进行插人与删除操作等边界情况,也可以简单地用一个指向表的第一个单元的指针来代替表头单元。
上述这种用指针来表示表的结构通常称为单链接表,或简称为单链表或链表。单链表的逻辑结构如图1所示。表示空表的单链表只有一个单元,即表头单元header,其中的指针是空指针nil。
图1 单链表示意图
为了便于实现表的各种运算,在单链表中位置变量的意义与用数组实现的表不同。在单链表中位置i是一个指针,它所指向的单元是元素ai-1所在的单元,而不是元素ai所在的单元(i=2,3,…,n)。位置1是指向表头单元header的指针。位置End(L)是指向单链表L中最后一个单元的指针。这样做的目的是为了避免在修改单链表指针时需要找一个元素的前驱元素的麻烦,因为在单链表中只设置指向后继元素的指针,而没有设置指向前驱元素的指针。
在单链表中,表类型TList和位置类型TPosition一样,都是指向某一单元的指针。尤其可以是指向表头单元的指针。
单链表结构的主要类型可形式地定义为:
type CellType=record element:TElement; next:^CellType; end; TList=^CellType; TPosition=^CellType;
在单链表中,函数End(L)可实现如下
Function End(L:TList):TPosition; var q: TPosition; begin q:=L; while q^.next<>nil do q:=q^.next; return(q) end; |
上述算法中的指针q指向需要检查的单元。由于检查要从header开始,而L是指向表头单元header的指针,所以q的初值为L。如果q所指的单元中的指针不是空指针,说明该单元不是表中的结束单元,因此要将q移向该单元的指针所指的下一单元,以便检查下一个单元。直到q所指的单元中的指针为nil时,那时的q值才是应返回的函数值。若表的长度为n,按这样计算End(L)需要扫描整个链表,因此需要O(n)时间,效率很低。如果需要频繁地调用函数End,我们可以采用下面的两种方法之一来提高效率。
-
在链表结构中多设一个指向链表结束单元的表尾指针。对此,通过表尾指针就可以在O(1)时间内实现End(L)。
-
尽可能避免调用End(L)。例如Purge程序的第(2)行中判断条件p<>End(L)应该用p.next<>nil来代替。
下面讨论单链表中Insert,Delete和Locate三种运算的实现。
函数 Insert(x,p) 功能
实现
说明
(a) (b)
复杂性
|
函数 Delete(p) 功能
实现
说明
图3 Delete过程的指针修改示意图
复杂性
|
函数 Locate(x,L) 功能
实现
说明
复杂性
|
对于其他基本的表运算实现起来都比较简单,这里从略。至于时间复杂性,在最坏情况下,PrintList显然需要O(n),而Previous由于单链表没有设置指向前驱的指针,得从头到尾扫描,因此也需要O(n)。