链表(linked list)是一种这样的数据结构,其中的各对象按线性顺序排列。数组的线性顺序是由数组下标决定的,然而与数组不同的是,链表的顺序是由各个对象里的指针决定的。链表为动态集合提供了一种简单而灵活的表示方法,并且能支持10.1节中列出的所有操作(但未必非常有效)。 如图10-3所示,双向链表(doubly linked list)L的每个元素都是一个对象,每个对象有一个关键字key和两个指针:next和prev。对象中还可以包含其他辅助数据(或称卫星数据)。设x为链表的一个元素,x.next指向它在链表中的后继元素,x.prev则指向它的前驱元素。如果x.prev = NIL,则元素x没有前驱,因此是链表的第一个元素,即链表的头(head)。如果x.next = NIL,则元素x没有后继,因此是链表的最后一个元素,即链表的尾(tail)。属性L.head指向链表的第一个元素。如果L.head = NIL,则链表为空 链表可以有多种形式。它可以是单链接的或双链接的,可以是已排序的或未排序的,可以是循环的或非循环的。如果一个链表是单链接的(singly linked),则省略每个元素中的prev指针。 如果链表是已排序的(sorted)的,则链表的线性顺序与链表元素中关键字的线性顺序一致;据此,最小的元素就是表头元素,而最大的元素则是表尾元素。如果链表是未排序的(unsorted)的,则各元素可以以任何顺序出现。在循环链表(circular list)中,表头元素的prev指针指向表尾元素,而表尾元素的next指针则指向表头元素。我们可以将循环链表想象成一个名元素组成的圆环。在本节余下的部分,我们假设所处理的链表都是未排序的且是双链接的。
过程LIST-SEARCH(L, k)采用简单的线性搜索方法,用于查找链表L中第一个关键字为k的元素,并返回指向该元素的指针。如果链表中没有关键字为k的对象,则该过程返回NIL。对于图10-3(a)中的链表,调用LIST-SEARCH(L, 4)返回指向第三个元素的指针,而调用LIST-SEARCH(L, 7)则返回NIL。
LIST-SEARCH(L, k) x = L.head while x ≠ NIL and x.key ≠ k x = x.next return x要搜索一个有n个对象的链表,过程LIST-SEARCH在最坏情况下的运行时间为 Θ(n),因为可能需要搜索整个链表
给定一个已设置好关键字key的元素x,过程LIST-INSERT将x“连接入”到链表的前端,如图10-3(b)所示。
LIST-INSERT(L, x) x.next = L.head if L.head ≠ NIL L.head.prev = x L.head = x x.prev = NIL(我们知道属性符号是可以嵌套的,因此L.head.prev表示的是L.head所指向的对象的prev属性。)在一个含n个元素的链表上执行LIST-INSERT的运行时间是O(1)。
LIST-DELETE(L,x) if x.prev ≠ NIL x.prev.next = x.next else L.head = x.next if x.next ≠ NIL x.next.prev = x.prev图10-3(c)展示了从链表中删除一个元素的操作。LIST-DELETE的运行时间为O(1)。但如果要删除具有给定关键字的元素,则最坏情况下需要的时间为Θ(n),因为需要先调用LIST-SEARCH找到该元素。
如果可以忽视表头和表尾外的边界条件,则LIST-DELETE的代码可以更简些:
LIST-DELETE(L, x) x.prev.next = x.next x.next.prev = x.prev哨兵(sentinel)是一个哑对象,其作用是简化边界条件的处理。例如,假设在链表L中设置一个对象L.nil,该对象代表NIL,但也具有和其他对象相同的各个属性。对于链表代码中出现的每一外对NIL的引用,都代之以对哨兵L.nil的引用。如图10-4所示,这样的调整将一个常规的双向链表转变为一个有哨兵的双向循环链表(circular, doubly linked list with a sentinel),哨兵L.nil位于表头和表尾之间。属性L.nil.next指向表头,L.nil.prev指向表尾。类似地,表尾的next属性和表头的prev属性同时指向L.nil。因为L.nil.next指向表头,我们就可以去掉属性L.head,并把对它的引用代替为对L.nil.next的引用。图10-4(a)显示,一个空的链表只由一个哨兵构成,L.nil.next和L.nil.prev同时指向L.nil。 LIST-SEARCH的代码和之前基本保持不变,只是将对NIL和L.head的引用如前所述加以调整:
LIST-SEARCH(L, k) x.next = L.nil.next while x ≠ L.nil and x.key ≠ k x = x.next return x我们使用前述的仅含两行代码的过程LIST-DELETE可以实现元素的删除。下面的过程则实现元素的插入:
LIST-INSERT(L, x) x.next = L.nil.next L.nil.next.prev = x L.nil.next = x x.prev = L.nil图10-4展示了LIST-INSERT和LIST-DELETE在该链表实例上执行结果。 哨兵基本不能降低数据结构相关操作的渐近时间界,但可以降低常数因子。在循环语句中使用哨兵的好处往往在于可以使代码简洁,而非提高速度。举例来如,使用哨兵使链表的代码变得简洁了,但在LIST-INSERT和LIST-DELETE过程上仅节约了O(1)的时间。然而,在另一些情况下,哨兵的使用使循环语句的代码更紧凑,从而降低了运行时间中n或n^2等项的系数。
我们应当慎用哨兵。假如有许多个很短的链表,它们的哨兵所占用的额外的存储空间会造成严重的存储浪费。本书中,仅当可以真正简化代码时才使用哨兵。
算法导论 10.2链表