3.3 列表
-
头、尾节点:
就内部结构而言,头节点(header)紧邻于首节点(first node)之前,尾节点(trailer)紧邻于末节点(last node)之后。这类经封装之后从外部不可见的节点,称作哨兵节点(sentinel node)。由代码3.2中List::valid()关于合法节点位置的判别准则可见,此处的两个哨兵节点从外部被等效地视作NULL。
尽管哨兵节点也需占用一定的空间,但只不过是常数规模,其成本远远低于由此带来的便利。
-
默认构造方法:
0001 template <typename T> void List<T>::init() { //列表初始化,在创建列表对象时统一调用 0002 header = new ListNode<T>; //创建头哨兵节点 0003 trailer = new ListNode<T>; //创建尾哨兵节点 0004 header->succ = trailer; header->pred = NULL; 0005 trailer->pred = header; trailer->succ = NULL; 0006 _size = 0; //记录规模 0007 }该链表对外的有效部分初始为空,哨兵节点对外不可见,此后引入的新节点都将陆续插入于这一对哨兵节点之间
-
由秩到位置的转换:
鉴于偶尔可能需要通过秩来指定列表节点,可通过重载操作符“[]”,提供一个转换接口。
0001 template <typename T> //重载下标操作符,以通过秩直接访问列表节点(虽方便,效率低,需慎用) 0002 T& List<T>::operator[] ( Rank r ) const { //assert: 0 <= r < size 0003 ListNodePosi<T> p = first(); //从首节点出发 0004 while ( 0 < r-- ) p = p->succ; //顺数第r个节点即是 0005 return p->data; //目标节点,返回其中所存元素 0006 }复杂度:O(r + 1)
-
查找:
List::find()
代码3.2中,列表ADT针对整体和区间查找,重载了操作接口find(e)和find(e, p, n)。其中,前者作为特例,可以直接调用后者。
0001 template <typename T> //在无序列表内节点p(可能是trailer)的n个(真)前驱中,找到等于e的最后者 0002 ListNodePosi<T> List<T>::find ( T const& e, int n, ListNodePosi<T> p ) const { 0003 while ( 0 < n-- ) //(0 <= n <= rank(p) < _size)对于p的最近的n个前驱,从右向左 0004 if ( e == ( p = p->pred )->data ) return p; //逐个比对,直至命中或范围越界 0005 return NULL; //p越出左边界意味着区间内不含e,查找失败 0006 } //失败时,返回NULL复杂度:O(n)
-
插入:
接口
0001 template <typename T> ListNodePosi<T> List<T>::insertAsFirst ( T const& e ) 0002 { _size++; return header->insertAsSucc ( e ); } //e当作首节点插入 0003 0004 template <typename T> ListNodePosi<T> List<T>::insertAsLast ( T const& e ) 0005 { _size++; return trailer->insertAsPred ( e ); } //e当作末节点插入 0006 0007 template <typename T> ListNodePosi<T> List<T>::insert ( ListNodePosi<T> p, T const& e ) 0008 { _size++; return p->insertAsSucc ( e ); } //e当作p的后继插入 0009 0010 template <typename T> ListNodePosi<T> List<T>::insert ( T const& e, ListNodePosi<T> p ) 0011 { _size++; return p->insertAsPred ( e ); } //e当作p的前驱插入前插入:
0001 template <typename T> //将e紧靠当前节点之前插入于当前节点所属列表(设有哨兵头节点header) 0002 ListNodePosi<T> ListNode<T>::insertAsPred ( T const& e ) { 0003 ListNodePosi<T> x = new ListNode ( e, pred, this ); //创建新节点并设好前驱后继(对应图中步骤b) 0004 pred->succ = x; pred = x; //设置正向链接(对应图中步骤c) 0005 return x; //返回新节点的位置 0006 }后插入:
0001 template <typename T> //将e紧随当前节点之后插入于当前节点所属列表(设有哨兵尾节点trailer) 0002 ListNodePosi<T> ListNode<T>::insertAsSucc ( T const& e ) { 0003 ListNodePosi<T> x = new ListNode ( e, this, succ ); //创建新节点 0004 succ->pred = x; succ = x; //设置逆向链接 0005 return x; //返回新节点的位置 0006 }复杂度:不计入此前查找所消耗的时间,O(1)
-
基于复制的构造:
与向量一样,列表的内部结构也是动态创建的,故利用默认的构造方法并不能真正地完成新列表的复制创建。为此,需要专门编写相应的构造方法,通过复制某一已有列表来构造新列表。
copyNode
0001 template <typename T> //列表内部方法:复制列表中自位置p起的n项 0002 void List<T>::copyNodes ( ListNodePosi<T> p, int n ) { //p合法,且至少有n-1个真后继节点 0003 init(); //创建头、尾哨兵节点并做初始化 0004 while ( n-- ) { insertAsLast ( p->data ); p = p->succ; } //将起自p的n项依次作为末节点插入 0005 }0001 template <typename T> //复制列表中自位置p起的n项(assert: p为合法位置,且至少有n-1个后继节点) 0002 List<T>::List ( ListNodePosi<T> p, int n ) { copyNodes ( p, n ); } 0003 0004 template <typename T> //整体复制列表L 0005 List<T>::List ( List<T> const& L ) { copyNodes ( L.first(), L._size ); } 0006 0007 template <typename T> //复制L中自第r项起的n项(assert: r+n <= L._size) 0008 List<T>::List ( List<T> const& L, int r, int n ) { 0009 copyNodes(L[r], n); 0010 } -
删除:
0001 template <typename T> T List<T>::remove ( ListNodePosi<T> p ) { //删除合法节点p,返回其数值 0002 T e = p->data; //备份待删除节点的数值(假定T类型可直接赋值) 0003 p->pred->succ = p->succ; p->succ->pred = p->pred; //后继、前驱 0004 delete p; _size--; //释放节点,更新规模 0005 return e; //返回备份的数值 0006 }复杂度:不计入此前查找所消耗的时间,O(1)
-
析构:
作用:释放资源及清除节点
复杂度:O(n)
-
唯一化:
List::deduplicate()
与Vector::deduplicate()类似,这里也是自前向后依次处理各节点p,一旦通过find()接口在p的前驱中查到雷同者,则随即调用remove()接口将其删除。
复杂度
相对于无序向量,尽管此处节点删除操作所需的时间减少,但总体渐进复杂度并无改进。
-
遍历
List::traverse()
该接口的设计思路与实现方式,与向量的对应接口(2.5节)如出一辙,复杂度也相同
“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情”