数据结构(c++)学习笔记--列表

文章详细介绍了列表的动态存储策略,包括从静态向量到动态列表的转变,以及列表节点的ADT接口和实现。接着讨论了无序列表的插入、删除、查找和去重操作,并介绍了选择排序和插入排序的原理和性能分析。此外,还涉及有序列表的唯一化和查找,以及利用归并排序统计逆序对的方法。
摘要由CSDN通过智能技术生成


一、循位置访问

1.从静态到动态

  • 根据是否修改数据结构,所有操作大致分为两类方式

    • 静态: 仅读取,数据结构的内容及组成一般不变:get、search
    • 动态: 需写入,数据结构的局部或整体将改变:put、insert、remove
  • 与操作方式相对应地,数据元素的存储与组织方式也分为两种

    • 静态
      • 数据空间整体创建或销毁
      • 数据元素的物理存储次序与其逻辑次序严格一致;可支持高效的静态操作
      • 比如向量,元素的物理地址与其逻辑次序线性对应
    • 动态
      • 为各数据元素动态地分配和回收的物理空间
      • 相邻元素记录彼此的物理地址,在逻辑上形成一个整体;可支持高效的动态操作

2.从向量到列表

  • 列表(list)是采用动态储存策略的典型结构

    • 其中的元素称作节点(node),通过指针或引用彼此连接
    • 在逻辑上构成一个线性序列:L = { a0, a1, …, an-1 }
  • 相邻节点彼此互称前驱(predecessor)或后继(successor)

    • 没有前驱/后继的节点称作首(first/front)/末(last/rear)节点

pSGm8mT.png

3.从秩到位置

  • 向量支持循秩访问(call-by-rank):根据元素的秩,可在O(1)时间内直接确定其物理地址

pSGmJ7F.png

  • 然而,此时的循秩访问成本过高,已不合时宜;因此,应改用循位置访问(call-by-position)的方式,亦即,转而利用节点之间的相互引用,找到特定的节点

二、接口与实现

1.列表节点:ADT接口

  • 作为列表的基本元素 列表节点首先需要 独立地“封装”实现

  • 基本操作接口
    |操作接口 |功能 |
    |–|–|
    |pred() |当前节点前驱节点的位置 |
    |succ() |当前节点后继节点的位置|
    |data() |当前节点所存数据对象 |
    |insertAsPred(e) |插入前驱节点,存入被引用对象e,返回新节点位置|
    |insertAsSucc(e) |插入后继节点,存入被引用对象e,返回新节点位置|

  • ListNode模板

template<typename T> using ListNodePosi = ListNode*; //列表节点位置(C++.0x) 
template<typename T> struct ListNode { //简洁起见,完全开放而不再严格封装 
    T data; 
    ListNodePosi pred; 
    ListNodePosi succ; 
    ListNode() {} //针对header和trailer的构造 
    ListNode(T e, ListNodePosi p = NULL, ListNodePosi s = NULL)
        : data(e), pred(p), succ(s) {} //默认构造器 ListNodePosi 
    insertAsPred( T const & e ); //前插入 
    ListNodePosi insertAsSucc( T const & e ); //后插入 
};

pSGndUg.png

2.列表:ADT接口

  • ADT接口
    |操作接口 |功能 |适用对象 |
    |–|–|–|
    |size() |报告列表当前的规模(节点总数) |列表 |
    |first(), last() |返回首、末节点的位置| 列表 |
    |insertAsFirst(e), insertAsLast(e) |将e当作首、末节点插入 |列表|
    |insert(p, e), insert(e, p) |将e当作节点p的直接后继、前驱插入 |列表|
    |remove§ |删除位置p处的节点,返回其中数据项 |列表 |
    |disordered() |判断所有节点是否已按非降序排列 |列表|
    |sort() |调整各节点的位置,使之按非降序排列 |列表|
    |find(e) |查找目标元素e,失败时返回NULL |列表|
    |search(e)| 查找e,返回不大于e且秩最大的节点| 有序列|
    |deduplicate(), uniquify() |剔除重复节点| 列表/有序列表|
    |traverse() |遍历列表 |列表|

  • List模板类

#include "listNode.h" //引入列表节点类 
template<typename T> class List { //列表模板类
private: 
    int _size; ListNodePosi header, trailer; //哨兵 //头、首、末、尾节点的秩,可分别理解为-1、0、n-1、n 
protected: /* ... 内部函数 */ 
public: /* ... 构造函数、析构函数、只读接口、可写接口、遍历接口 */
};

template void List::init() { //初始化,创建列表对象时统一调用 
    header = new ListNode; 
    trailer = new ListNode; 
    header->succ = trailer; header->pred = NULL; 
    trailer->pred = header; trailer->succ = NULL; 
    _size = 0;
}

pSGu9Mt.png

3.循秩访问

  • 重载下标操作符,可模仿向量的循秩访问方式
template<typename T> //assert: 0 <= r < size 
T List::operator[]( Rank r ) const {
ListNodePosi p = first(); //从首节点出发 
while ( 0 < r-- ) p = p->succ; //顺数第r个节点即是 
return p->data; //目标节点 
} //秩 == 前驱的总数

pSGu5TS.png

  • 时间复杂度为O®;均匀分布时,期望复杂度为(1+2+3+…+n)/n=O(n)

三、无序列表

1.插入与删除

  • 插入
template<typename T> //前插入算法(后插入算法完全对称) 
ListNodePosi ListNode::insertAsPred( T const & e ) { //O(1) 
    ListNodePosi x = new ListNode( e, pred, this ); //创建 
    pred->succ = x; pred = x; //次序不可颠倒 
    return x; //建立链接,返回新节点的位置 
} //得益于哨兵,即便this为首节点亦不必特殊处理——此时等效于insertAsFirst(e)

pSGKZTO.png

  • 删除
template<typename T> //删除合法位置p处节点,返回其数值 T 
List::remove( ListNodePosi p ) { //O(1) 
    T e = p->data; //备份待删除节点数值(设类型T可直接赋值) 
    p->pred->succ = p->succ; p->succ->pred = p->pred; //短路 
    delete p; 
    _size--; 
    return e; //返回备份数值
}

pSGKQ1A.png

2.构造与析构

  • copyNodes() + 构造
template<typename T> 
void List::copyNodes( ListNodePosi p, int n ) { //O(n) 
    init(); //创建头、尾哨兵节点并做初始化 
    while ( n-- ) { //将起自p的n项依次作为末节点 
        insertAsLast( p->data ); 
        p = p->succ; 
    } 
}
  • clear() + 析构
template<typename T> 
List::~List() //列表析构 { 
    clear(); delete header; delete trailer; 
} //清空列表,释放头、尾哨兵节点 

template<typename T> 
int List::clear() { //清空列表 
    int oldSize = _size; 
    while ( 0 < _size ) //反复删除首节点,O(n) 
    remove( header->succ ); 
    return oldSize; 
}

3.查找与去重

  • 查找
template<typename T> 
ListNodePosi List::find( T const & e, int n, ListNodePosi p ) const { 
    while ( 0 < n-- ) //自后向前 
    if ( e == ( p = p->pred ) ->data ) //逐个比对(假定类型T已重载“==”) 
        return p; //在p的n个前驱中,等于e的最靠后者 
    return NULL; 
} //O(n)
  • 去重
template<typename T> int List::deduplicate() { 
    int oldSize = _size;
    ListNodePosi p = first(); 
    for ( Rank r = 0; p != trailer; p = p->succ ) //O(n) 
        if ( ListNodePosi q = find( p->data, r, p ) ) //O(n) 
            remove ( q ); 
        else r++; //无重前缀的长度 
    return oldSize - _size; //删除元素总数 
} //正确性及效率分析的方法与结论,与Vector::deduplicate()相同

4.遍历

//函数指针
template<typename T> void List::traverse( void ( * visit )( T & ) ) { 
    for(NodePosi p = header->succ; p != trailer; p = p->succ) 
        visit( p->data ); 
}

//函数对象
template<typename T> template<typename VST> void List::traverse( VST & visit ) { 
    for( NodePosi p = header->succ; p != trailer; p = p->succ ) 
        visit( p->data ); 
}

四、有序列表

1.唯一化

template<typename T> 
int List::uniquify() { 
    if ( _size < 2 ) return 0; //平凡列表自然无重复 
    int oldSize = _size; //记录原规模 
    ListNodePosi p = first(); ListNodePosi q; //各区段起点及其直接后继 
    while ( trailer != ( q = p->succ ) ) //反复考查紧邻的节点对(p,q) 
        if ( p->data != q->data ) p = q; //若互异,则转向下一对 
        else remove(q); //否则(雷同)直接删除后者,不必如向量那样间接地完成删除 
    return oldSize - _size; //规模变化量,即被删除元素总数 
} //只需遍历整个列表一趟,O(n)

2.查找

template<typename T> 
ListNodePosi List::search( T const & e, int n, ListNodePosi p ) const { 
    do { p = p->pred; n--; } //从右向左 
    while ( ( -1 < n ) && ( e < p->data ) ); //逐个比较,直至命中或越界 
    return p; //失败时,返回区间左边界的前驱(可能是header) 
}
  • 性能 + 拓展
    • 最好O(1),最坏O(n);等概率时平均O(n),正比于区间宽度

    •  语义与向量相似,便于插入排序等后续操作:insert( search( e, r, p ), e )

    • 按照循位置访问的方式,物理存储地址与其逻辑次序无关;依据秩的随机访问无法高效实现,而只能依据元素间的引用顺序访问

五、选择排序

1.代码

template<typename T> void List::selectionSort( ListNodePosi p, int n ) { 
    ListNodePosi head = p->pred, tail = p; 
    for ( int i = 0; i < n; i++ ) 
        tail = tail->succ; //待排序区间为(head, tail) 
    while ( 1 < n ) { //反复从(非平凡)待排序区间内找出最大者,并移至有序区间前端 
        insert(remove( selectMax( head->succ, n ) ), tail ); //可能就在原地... 
        tail = tail->pred; n--; //待排序区间、有序区间的范围,均同步更新 
    } 
}

template<typename T>  
ListNodePosi List::selectMax( ListNodePosi p, int n ) { //Θ(n) 
    ListNodePosi max = p; //最大者暂定为p 
    for ( ListNodePosi cur = p; 1 < n; n-- ) //后续节点逐一与max比较 
        if ( ! lt( (cur = cur->succ)->data, max->data ) ) //data≥max 
            max = cur; //则更新最大元素位置记录 
    return max; //返回最大节点位置 
}

pSGMMUU.png

pSGMD8H.png

2.稳定性

  • 稳定性:有多个元素同时命中时,约定返回其中特定的某一个(比如最靠后者)

  • 在这里,需要采用比较器!lt()或ge(),从而等效于后者优先;若采用平移法,如此即可保证,重复元素在列表中的相对次序,与其插入次序一致

pSGMfIS.png

3.性能分析

  • 共迭代n次,在第k次迭代中
    • selectMax() 为Θ(n - k)
    • swap()/remove() + insert() 为 O(1)

故总体复杂度应为Θ(n2)

  • 尽管如此,元素的移动操作远远少于起泡排序;也就是说,Θ(n2)主要来自于元素的比较操作(实际更为费时,成本相对更低)

  • 利用高级数据结构,selectMax()可改进至O(logn)

六、循环节

1.循环节

  • 任何一个序列A[0,n),都可以分解为若干个循环节

  • 任何一个序列A[0,n),都对应于一个有序序列S[0,n)

  • 元素A[k]在S中对应的秩,记作r(A[k])=r(k)∈[0,n)

  • 元素A[k]所属的循环节是:A[k],A[r(k)],A[r(r(k))],…,A[r(…(r(r(k))))]=A[k]

  • 每个循环节,长度均不超过n

  • 循环节之间,互不相交

image.png

2.单调性

  • 采用交换法,每迭代一步,M都会脱离原属的循环节,自成一个循环节

pSGQeRH.png

  • M原所属循环节,长度恰好减少一个单位;其余循环节,保持不变

3.无效的交换

  • M已经就位,无需交换

  • 最初有c个循环节,就出现c次 —— 最大值为n,期望Θ(logn)

七、插入排序

1.减而治之

  • 不变性

    • 序列总能视作两部分: S[0, r) + U[r, n)
  • 初始化:|S| = r = 0

  • 反复地,针对e = A[r] 在S中查找适当位置,以插入e

Y5TrU.png

Y5WRw.png

2.代码

template<typename T> void List::insertionSort( ListNodePosi p, int n ) { 
    for ( int r = 0; r < n; r++ ) { //逐一引入各节点,由S 得到 r Sr+1 
        insert( search( p->data, r, p ), p->data ); 
        p = p->succ; 
        remove( p->pred ); //转向下一节点 
    } //n次迭代,每次O(r + 1) 
} //仅使用O(1)辅助空间,属于就地算法
  • 紧邻于search()接口返回的位置之后插入当前节点,总是保持有序

3.平均性能:后向分析

  • e=[r]刚插入完成的那一时刻,此时的有序前缀[0,r]中其中的r+1个元素均有可能是e,且概率均为1/(r+1)

  • 因此,在刚完成的这次迭代中为引入S[r]所花费时间的数学期望为 1 + ∑ k = 0 r k / ( r + 1 ) = 1 + r / 2 1+\sum_{k=0}^{r}k/(r+1) = 1+r/2 1+k=0rk/(r+1)=1+r/2

  • 于是,总体时间的数学期望为 ∑ r = 0 n − 1 ( r / 2 + 1 ) = O ( n 2 ) \sum_{r=0}^{n-1}(r/2+1) = O(n^2) r=0n1(r/2+1)=O(n2)

八、归并排序

Y59KJ.png

template<typename T> void List::mergeSort( ListNodePosi & p, int n ) { 
    if ( n < 2 ) return; //待排序范围足够小时直接返回,否则... 
    ListNodePosi q = p; int m = n >> 1; //以中点为界 
    for ( int i = 0; i < m; i++ ) q = q->succ; //均分列表:O(m) = O(n) 
    mergeSort( p, m ); mergeSort( q, n – m ); //子序列分别排序 
    p = merge( p, m, *this, q, n – m ); //归并 
} //若归并可在线性时间内完成,则总体运行时间亦为O(nlogn)

template<typename T> ListNodePosi<T> 
List::merge( ListNodePosi p, int n, List & L, ListNodePosi q, int m ) { 
    ListNodePosi pp = p->pred; //归并之后p或不再指向首节点,故需先记忆,以便返回前更新 
    while ( ( 0 < m ) && ( q != p ) ) //小者优先归入 
        if ( ( 0 < n ) && ( p->data <= q->data ) ) { 
            p = p->succ; n--; 
        } //p直接后移 
        else { 
            insert( L.remove( ( q = q->succ )->pred ) , p ) ); m--; 
        } //q转至p之前 
    return pp->succ; //更新的首节点 
} //运行时间O(n + m),线性正比于节点总数

九、逆序对

1.逆序对(Inversion)

  • 考查序列A[0, n),设元素之间可比较大小

    • <i,j> is called an inversion if 0≤i<j<n and A[i]>A[j]
  • 为便于统计,可将逆序对统一记到后者的账上

    • I(j)=||{0≤i<j | A[i]>A[j] and hence <i,j> is an inversion}||
  • 实例

    • A[] = { 5, 3, 1, 4, 2 } 中,共有 0 + 1 + 2 + 1 + 3 = 7 个逆序对
    • A[] = { 1, 2, 3, 4, 5 } 中,共有 0 + 0 + 0 + 0 + 0 = 0 个逆序对
    • A[] = { 5, 4, 3, 2, 1 } 中,共有 0 + 1 + 2 + 3 + 4 = 10 个逆序对
  • 显然,逆序对总数 I = ∑ j I ( j ) ≤ ( n 2 ) = O ( n 2 ) I=\sum_{j}{}I(j)≤{n \choose 2}=O(n^2) I=jI(j)(2n)=O(n2)

2.起泡排序

Y5YLn.png

  • 在序列中交换一对逆序元素,逆序对总数必然减少

Y5ovl.png

  • 在序列中交换一对紧邻的逆序元素,逆序对总数恰好减一

  • 因此对于Bubblesort算法而言,交换操作的次数恰等于若共含 个逆序对,则输入序列所含逆序对的总数

3.插入排序

Y53E7.png

  • 针对e=A[r]的那一步迭代恰好需要做I®次比较

  • 若共含I个逆序对,则

    • 关键码比较次数为O(I)

    • 运行时间为O(n+I)

4.计数

  • 任意给定一个序列,如何统计其中逆序对的总数?
    • 蛮力算法需要O( n 2 n^2 n2)时间;而借助归并排序,仅需O( n log ⁡ n n\log n nlogn)时间

Y5IZ2.png

十、游标实现

1.动机与构思

  • 某些语言不支持指针类型,即便支持 频繁的动态空间分配也影响总体效率
  • 利用线性数组,以游标方式模拟列表
    • elem[]:对外可见的数据项
    • link[]:数据项之间的引用
      Y5iFP.png
  • 维护逻辑上互补的列表data和free

2.实例

Y5mrD.png
Y5xRj.png
Y58pS.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值