数据结构 | 第3章 列表(中)

101 阅读5分钟

3.3 列表

  • 头、尾节点: image-20220623144340250.png 就内部结构而言,头节点(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 }
    

    image-20220623145851344.png

    该链表对外的有效部分初始为空,哨兵节点对外不可见,此后引入的新节点都将陆续插入于这一对哨兵节点之间

  • 由秩到位置的转换:

    鉴于偶尔可能需要通过秩来指定列表节点,可通过重载操作符“[]”,提供一个转换接口。

     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 }
    

    image-20220623154318145.png

    后插入:

     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 }
    

    image-20220623161829477.png

    复杂度:不计入此前查找所消耗的时间,O(1)

  • 析构:

    作用:释放资源及清除节点

    复杂度:O(n)

  • 唯一化:

    List::deduplicate()

    与Vector::deduplicate()类似,这里也是自前向后依次处理各节点p,一旦通过find()接口在p的前驱中查到雷同者,则随即调用remove()接口将其删除。

    复杂度O(n2)O(n^2)

    相对于无序向量,尽管此处节点删除操作所需的时间减少,但总体渐进复杂度并无改进。

  • 遍历

    List::traverse()

    该接口的设计思路与实现方式,与向量的对应接口(2.5节)如出一辙,复杂度也相同

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情