文章目录

  • 一、循位置访问
    • 1.从静态到动态
    • 2.从向量到列表
    • 3.从秩到位置
  • 二、接口与实现
    • 1.列表节点:ADT接口
    • 2.列表:ADT接口
    • 3.循秩访问
  • 三、无序列表
    • 1.插入与删除
    • 2.构造与析构
    • 3.查找与去重
    • 4.遍历
  • 四、有序列表
    • 1.唯一化
    • 2.查找
  • 五、选择排序
    • 1.代码
    • 2.稳定性
    • 3.性能分析
  • 六、循环节
    • 1.循环节
    • 2.单调性
    • 3.无效的交换
  • 七、插入排序
    • 1.减而治之
    • 2.代码
    • 3.平均性能:后向分析
  • 八、归并排序
  • 九、逆序对
    • 1.逆序对(Inversion)
    • 2.起泡排序
    • 3.插入排序
    • 4.计数
  • 十、游标实现
    • 1.动机与构思
    • 2.实例

一、循位置访问

1.从静态到动态

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

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

    • 静态

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

2.从向量到列表

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

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

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

3.从秩到位置

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

  • 然而,此时的循秩访问成本过高,已不合时宜;因此,应改用循位置访问(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 ); //后插入
};

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;
}

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; //目标节点
} //秩 == 前驱的总数

  • 时间复杂度为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)

  • 删除
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; //返回备份数值
}

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; //返回最大节点位置
}

2.稳定性

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

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

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

  • 循环节之间,互不相交

2.单调性

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

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

3.无效的交换

  • M已经就位,无需交换

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

七、插入排序

1.减而治之

  • 不变性

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

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

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=0rk/(r+1)=1+r/21+\sum_{k=0}^{r}k/(r+1) = 1+r/21+∑k=0r​k/(r+1)=1+r/2

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

八、归并排序

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=∑jI(j)≤(n2)=O(n2)I=\sum_{j}{}I(j)≤{n \choose 2}=O(n^2)I=∑j​I(j)≤(2n​)=O(n2)

2.起泡排序

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

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

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

3.插入排序

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

  • 若共含I个逆序对,则

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

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

4.计数

  • 任意给定一个序列,如何统计其中逆序对的总数?

    • 蛮力算法需要O(n2n^2n2)时间;而借助归并排序,仅需O(nlog⁡nn\log nnlogn)时间

十、游标实现

1.动机与构思

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

2.实例



数据结构(c++)学习笔记--列表相关推荐

  1. ES6基础4(数据结构)-学习笔记

    文章目录 ES6基础4(数据结构)-学习笔记 set map symbol ES6基础4(数据结构)-学习笔记 set //set 数据结构 类似数组 成员信息唯一性var s = new Set() ...

  2. Python 学习笔记 列表 range() xxx XXX

    Python 学习笔记 列表 range() xxx XXX print("-" * 30) for value in range(1, 5):print(value)number ...

  3. Python 学习笔记 列表 xxx XXX

    Python 学习笔记 列表 xxx XXX bicycles = ['trek', 'cannondale', 'redline', 'specialized'] print(bicycles) p ...

  4. Python 学习笔记 列表 排序 xxx XXX

    Python 学习笔记 列表 排序 xxx XXX print("-" * 30) cars = ['bmw', 'audi', 'toyota', 'subaru'] cars. ...

  5. 数据结构专题-学习笔记:李超线段树

    数据结构专题 - 学习笔记:李超线段树 1. 前言 2. 详解 3. 应用 4. 总结 5. 参考资料 1. 前言 本篇博文是博主学习李超线段树的学习笔记. 2020/12/21 的时候我在 线段树算 ...

  6. python的基本数据结构_Python学习笔记——基本数据结构

    列表list List是python的一个内置动态数组对象,它的基本使用方式如下: shoplist = ['apple', 'mango', 'carrot', 'banana'] print 'I ...

  7. python的基本数据结构_python学习笔记-基本数据结构

    Python 学习笔记-1 写在最前面,因为组内小伙伴要走,生信团队由原来的7个人,慢慢的变的只有我一个人了,需要紧急突击下python,因为有python的流程要交接维护 python 基本情况 代 ...

  8. Python学习笔记 | 列表和字典

    基于Python3版本的学习. 列表 用中括号[ ]把里面的各种数据框起来,里面的每一个数据叫作"元素". 每个元素之间都要用英文逗号隔开. list1=['小明',18,1.70 ...

  9. 数据结构课程学习笔记

    整理一下上数据结构课记录的笔记. 第一章 绪论 1.1 数据结构的基本概念 1.2 算法的基本概念 1.2.1 时间复杂度 事前预估算法时间开销T(n)与问题规模n的关系.分析算法操作的执行次数x和问 ...

最新文章

  1. Sql存储过程加密和解密
  2. angular 兼容ie7 bootstrap2兼容ie6
  3. iOS之深入解析“锁”的底层原理
  4. 其实我就是个技术迷-自身定位及展望
  5. linux Postfix + dovecot + extmail + extman + mysql
  6. LeetCode 605 种花问题
  7. phoenix的元数据一般存在哪里_ElasticSearch文档元数据(Metadata)
  8. Qt部件学习之-烧鹅
  9. 2022 老A卡密验证系统
  10. 【D3 API 中文手册】
  11. 【蓝桥杯真题】走迷宫算法
  12. matlab中与或非、等逻辑符号
  13. 真实如刀的洞见:和扶墙老师聊技术、组织和商业
  14. JZOJ 6297. 2019.08.10【NOIP提高组A】世界第一的猛汉王
  15. 研读 项目名-LinkMap-normal-x86_64.txt
  16. Alist保姆级搭建教程
  17. 微软官方的精简版Windows 7——Windows Thin PC
  18. 判断Checkbox选中两种方法
  19. 201803-4 棋局评估
  20. SRS(简单实时视频服务) 笔记(5)- 视频录制

热门文章

  1. 嵌入式工程师面试题集汇总
  2. Linux----网络传输层UDP/TCP
  3. 华为智慧屏x65鸿蒙,华为智慧屏 65英寸登录京东:用上鸿蒙OS
  4. 数据库学习——第三天
  5. 2022-2028年中国USB键盘行业市场竞争状况及发展趋向分析报告
  6. css 图片紧贴页面底部,CSS StickyFooter——当内容不足一屏时footer紧贴底部
  7. 云盘共享产品简单比较
  8. java矩形派生正方形_从矩形派生正方形是否违反了Liskov的替代原理?
  9. Python实现视频自动打码功能,避免看到羞羞的画面
  10. Barsetto百胜图TripressoCA美式便携咖啡机测评——随心玩乐,自由掌握