数据结构和算法(3):列表

news2024/11/28 14:52:12

列表是一种线性数据结构,它允许在其中存储多个元素,并且可以动态地添加或删除元素。

循秩访问

可通过重载下标操作符,实现寻秩访问

template <typename T> // assert: 0 <= r < size
T List<T>::operator[](Rank r) const { //O(r),效率低下,可偶尔为之,却不宜常用
	Posi(T) p = first(); //从首节点出发
	while (0 < r--) p = p->succ; //顺数第r个节点即是
	return p->data;//目标节点
}//任一节点的秩,亦即其前驱的总数

效率比较低,总和为 O ( n 2 ) \mathcal O(n^2) O(n2),平摊到每个元素上为 O ( n ) \mathcal O(n) O(n)

接口与实现

根据是否修改数据结构,所有操作大致分为两类方式:
1)静态︰仅读取,数据结构的内容及组成一般不变:getsearch
2)动态:需写入,数据结构的局部或整体将改变:insertremove

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

列表(List) 是采用动态存储策略的典型结构。其中的元素称作节点(node),各节点通过指针或引用彼此联接,在逻辑上构成一个线性序列
相邻节点彼此互称前驱或后继。前驱或后继若存在,则必然唯一。没有前驱/后继的唯一节点称作首/末节点。

//listnode.h
template <typename T> struct ListNode;
template <typename T> using ListNodePosi = ListNode<T>*; //列表节点位置
template <typename T> struct ListNode { //列表节点模板类(以双向链表形式实现)
// 成员
   T data; ListNodePosi<T> pred, succ; //数值、前驱、后继
// 构造函数
   ListNode() {} //针对header和trailer的构造
   ListNode ( T e, ListNodePosi<T> p = NULL, ListNodePosi<T> s = NULL )
      : data( e ), pred( p ), succ( s ) {} //默认构造器
// 操作接口
   ListNodePosi<T> insertAsPred( T const& e ); //紧靠当前节点之前插入新节点
   ListNodePosi<T> insertAsSucc( T const& e ); //紧随当前节点之后插入新节点
};

列表节点:ADT接口

作为列表的基本元素,列表节点首先需要独立地“封装”实现
为此,可设置并约定若干基本的操作接口

在这里插入图片描述`

#include "listNode.h" //引入列表节点类

template <typename T> class List { //列表模板类

private:
   Rank _size; ListNodePosi<T> header, trailer; //规模、头哨兵、尾哨兵
protected:
   void init(); //列表创建时的初始化
   Rank clear(); //清除所有节点
   void copyNodes( ListNodePosi<T>, Rank ); //复制列表中自位置p起的n项
   ListNodePosi<T> merge( ListNodePosi<T>, Rank, List<T>&, ListNodePosi<T>, Rank ); //归并
   void mergeSort( ListNodePosi<T>&, Rank ); //对从p开始连续的n个节点归并排序
   void selectionSort( ListNodePosi<T>, Rank ); //对从p开始连续的n个节点选择排序
   void insertionSort( ListNodePosi<T>, Rank ); //对从p开始连续的n个节点插入排序
   void radixSort( ListNodePosi<T>, Rank ); //对从p开始连续的n个节点基数排序

public:
// 构造函数
   List() { init(); } //默认
   List( List<T> const& L ); //整体复制列表L
   List( List<T> const& L, Rank r, Rank n ); //复制列表L中自第r项起的n项
   List( ListNodePosi<T> p, Rank n ); //复制列表中自位置p起的n项
   // 析构函数
   ~List(); //释放(包含头、尾哨兵在内的)所有节点
// 只读访问接口
   Rank size() const { return _size; } //规模
   bool empty() const { return _size <= 0; } //判空
   ListNodePosi<T> operator[]( Rank r ) const; //重载,支持循秩访问(效率低)
   ListNodePosi<T> first() const { return header->succ; } //首节点位置
   ListNodePosi<T> last() const { return trailer->pred; } //末节点位置
   bool valid( ListNodePosi<T> p ) //判断位置p是否对外合法
   { return p && ( trailer != p ) && ( header != p ); } //将头、尾节点等同于NULL
   ListNodePosi<T> find( T const& e ) const //无序列表查找
   { return find( e, _size, trailer ); }
   ListNodePosi<T> find( T const& e, Rank n, ListNodePosi<T> p ) const; //无序区间查找
   ListNodePosi<T> search( T const& e ) const //有序列表查找
   { return search( e, _size, trailer ); }
   ListNodePosi<T> search( T const& e, Rank n, ListNodePosi<T> p ) const; //有序区间查找
   ListNodePosi<T> selectMax( ListNodePosi<T> p, Rank n ); //在p及其n-1个后继中选出最大者
   ListNodePosi<T> selectMax() { return selectMax( header->succ, _size ); } //整体最大者
// 可写访问接口
   ListNodePosi<T> insertAsFirst( T const& e ); //将e当作首节点插入
   ListNodePosi<T> insertAsLast( T const& e ); //将e当作末节点插入
   ListNodePosi<T> insert( ListNodePosi<T> p, T const& e ); //将e当作p的后继插入
   ListNodePosi<T> insert( T const& e, ListNodePosi<T> p ); //将e当作p的前驱插入
   T remove( ListNodePosi<T> p ); //删除合法位置p处的节点,返回被删除节点
   void merge( List<T>& L ) { merge( header->succ, _size, L, L.header->succ, L._size ); } //全列表归并
   void sort( ListNodePosi<T>, Rank ); //列表区间排序
   void sort() { sort( first(), _size ); } //列表整体排序
   Rank dedup(); //无序去重
   Rank uniquify(); //有序去重
   void reverse(); //前后倒置(习题)
// 遍历
   void traverse( void ( * )( T& ) ); //依次实施visit操作(函数指针)
   template <typename VST> void traverse( VST& ); //依次实施visit操作(函数对象)
}; //List

创建列表

在这里插入图片描述

template <typename T> void List<T>::init() { //列表初始化,在创建列表对象时统一调用
   header = new ListNode<T>; trailer = new ListNode<T>; //创建头、尾哨兵节点
   header->succ = trailer; header->pred = NULL; //向前链接
   trailer->pred = header; trailer->succ = NULL; //向后链接
   _size = 0; //记录规模
}

无序列表

插入与构造

//插入
template <typename T> //将e紧靠当前节点之前插入于当前节点所属列表(设有哨兵头节点header)
ListNodePosi<T> ListNode<T>::insertAsPred( T const& e ) {
   ListNodePosi<T> x = new ListNode( e, pred, this ); //创建新节点
   pred->succ = x; pred = x; //设置正向链接
   return x; //返回新节点的位置
}
//基于复制的构造
template <typename T> //列表内部方法:复制列表中自位置p起的n项
void List<T>::copyNodes( ListNodePosi<T> p, Rank n ) { // p合法,且至少有n-1个真后继
   init(); //创建头、尾哨兵节点并做初始化
   while ( n-- ) { insertAsLast( p->data ); p = p->succ; } //将起自p的n项依次作为末节点插入
}

//insertAsLast 就相当于 insertBefore(trailer)

在列表中插入一个新节点 node 作为 p 的直接前驱,顺序为:
node->succ = p
node->pred = p->pred
p->pred->succ = node
p->pred = node

p->pred->succ = nodenode->pred = p->pred 的正确执行需要能够定位p原先的直接前驱p->pred,而 p->pred = node 会破坏 p 到其的链接,故 p->pred->succ = nodenode->pred = p->pred 必须在 p->pred = node 之前执行

删除和析构

//删除
template <typename T> T List<T>::remove( ListNodePosi<T> p ) { //删除合法节点p
   T e = p->data; //备份待删除节点的数值(假定T类型可直接赋值)
   p->pred->succ = p->succ; p->succ->pred = p->pred; //短路联接
   delete p; _size--; //释放节点,更新规模
   return e; //返回备份的数值
} //O(1)
//析构
template <typename T> List<T>::~List() //列表析构器
{ clear(); delete header; delete trailer; } //清空列表,释放头、尾哨兵节点

template <typename T> Rank List<T>::clear() { //清空列表
   Rank oldSize = _size;
   while ( 0 < _size ) remove ( header->succ ); //反复删除首节点,直至列表变空
   return oldSize;
}//O(n),线性正比于列表规模

查找

template <typename T> //在无序列表内节点p(可能是trailer)的n个(真)前驱中,找到等于e的最后者
ListNodePosi<T> List<T>::find( T const& e, Rank n, ListNodePosi<T> p ) const {
   while ( 0 < n-- ) //(0 <= n <= Rank(p) < _size)对于p的最近的n个前驱,从右向左
      if ( e == ( p = p->pred )->data ) return p; //逐个比对,直至命中或范围越界
   return NULL; //p越出左边界意味着区间内不含e,查找失败
} //失败时,返回NULL

当存在多个目标时,会停止最靠后的元素节点。

去重

template <typename T> int List<T>::deduplicate() {	//剔除无序列表中的重复节点
	if (_size < 2) return	//平凡列表自然无重复
	int oldsize = _size;	//记录原规模
	Posi(T) p = first(); Rank r = 1;	//p从首节点起
	while (trailer != (p = p->succ ))	//依次直到末节点
		Posi(T) q = find(p->data,r, p);	//在p的r个(真)前驱中,查找与之雷同者
		q ? remove(q):r++;	//若的确存在,则删除之;否则秩递增——可否remove(p)?不可!因为后面要指向其后继
	}	//assert:循环过程中的任意时刻,p的所有前驱互不相同
return oldSize - _size;	//列表规模变化量,即被删除元素总数
}//正确性及效率分析的方法与结论,与Vector::deduplicate()相同

有序列表

唯一化(去重)

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

查找

template <typename T>//在有序列表内节点p的n个(真)前驱中,找到不大于e的最后者
Posi(T) List<T>::search(T const & e, int n, Posi(T) p) const {
	while ( e <= n-- )//对于p的最近的n个前驱,从右向左
		if((( p = p->pred ) -> data <= e ) break;//逐个比较
	return p;//直至命中、数值越界或范围界后,返回查找终止的位置
}//最好o(1),最坏o(n);等概率时平均o(n),正比于区间宽度

列表的循位置访问和向量的循秩访问有着根本区别,前者靠的是位置,后置靠的是秩。

选择排序

基本思路是在未排序的部分中找到最小(或最大)的元素,然后将其与未排序部分的第一个元素交换位置,以此类推,直到整个序列排序完成。

基本思路和步骤:

  1. 初始状态:将整个序列分为两部分,已排序部分和未排序部分。一开始,已排序部分为空,未排序部分包含整个序列。
  2. 找到最小元素:在未排序部分中找到最小的元素,并记录其位置(索引)。
  3. 交换位置:将找到的最小元素与未排序部分的第一个元素交换位置。
  4. 更新已排序部分:将已排序部分的末尾扩展,包括刚刚交换的元素。
  5. 重复步骤2至4:重复执行步骤2至4,直到未排序部分为空。
  6. 排序完成:当未排序部分为空时,整个序列就被排序完成。
template <typename T> //对列表中起始于位置p、宽度为n的区间做选择排序
void List<T>::selectionSort( ListNodePosi<T> p, Rank n ) { // valid(p) && Rank(p) + n <= size
   ListNodePosi<T> head = p->pred, tail = p;
   for ( Rank i = 0; i < n; i++ ) tail = tail->succ; //待排序区间为(head, tail)
   while ( 1 < n ) { //在至少还剩两个节点之前,在待排序区间内
      ListNodePosi<T> max = selectMax ( head->succ, n ); //找出最大者(歧义时后者优先)
      insert( remove( max ), tail ); //将其移至无序区间末尾(作为有序区间新的首元素)
      tail = tail->pred; n--;
   }
}


template <typename T> //从起始于位置p的n个元素中选出最大者
ListNodePosi<T> List<T>::selectMax( ListNodePosi<T> p, Rank n ) {
   ListNodePosi<T> max = p; //最大者暂定为首节点p
   for ( ListNodePosi<T> cur = p; 1 < n; n-- ) //从首节点p出发,将后续节点逐一与max比较
      if ( !lt( ( cur = cur->succ )->data, max->data ) ) //若当前元素不小于max,则
         max = cur; //更新最大元素位置记录
   return max; //返回最大节点位置
}

总共迭代 n 次,在第 k 次迭代中, selectmax() O ( n − k ) \mathcal O(n-k) O(nk)remove()insertBefore() 均为 O ( 1 ) \mathcal O(1) O(1),故总体复杂度应为 O ( n 2 ) \mathcal O(n^2) O(n2)
尽管如此,元素移动操作远远少于冒泡排序,也就是说 O ( n 2 ) \mathcal O(n^2) O(n2) 主要来自比较操作.

尽管它不如一些高级排序算法(如快速排序或归并排序)快,但它简单直观,对于小型数据集来说是一个有效的排序方法。然而,对于大型数据集,选择排序的性能可能不够理想。

插入排序

基本思路是将一个序列分成已排序部分和未排序部分。初始时,已排序部分只包含第一个元素,然后逐步将未排序部分的元素插入到已排序部分,保持已排序部分的有序性。这个过程类似于我们在打牌时对手中的牌进行排序,每次将一张新牌插入到已经有序的牌中。

基本思路和步骤:

  1. 初始状态:将整个序列分为两部分,已排序部分和未排序部分。一开始,已排序部分只包含第一个元素,未排序部分包含其余的元素。
  2. 从未排序部分选择一个元素:从未排序部分选择第一个元素,将其视为待插入的元素。
  3. 向已排序部分插入元素:将待插入元素与已排序部分的元素从右向左逐个比较,直到找到合适的位置。在比较过程中,较大的元素会向右移动,为待插入元素腾出空间。
  4. 插入元素:一旦找到了合适的位置,将待插入元素插入到已排序部分。
  5. 更新已排序部分:已排序部分扩展一个位置,包括刚刚插入的元素。
  6. 重复步骤2至5:重复执行步骤2至5,直到未排序部分为空。
  7. 排序完成:当未排序部分为空时,整个序列就被排序完成。
template <typename T> //对列表中起始于位置p、宽度为n的区间做插入排序
void List<T>::insertionSort( ListNodePosi<T> p, Rank n ) { // valid(p) && Rank(p) + n <= size
   for ( Rank r = 0; r < n; r++ ) { //逐一为各节点
      insert( search( p->data, r, p ), p->data ); //查找适当的位置并插入
      p = p->succ; remove( p->pred ); //转向下一节点
   }	//n 次迭代,每次O(r+1)
}	//仅使用 O(1) 辅助空间,属于就地算法

最好情况:完全(或)几乎有序,每次迭代,只需 1 次比较, 0 次交换,累计 O ( n ) \mathcal O(n) O(n)
最坏要 O ( n 2 ) \mathcal O(n^2) O(n2)

逆序对

逆序对(Inverse Pairs) 是一个在数组或序列中常见的概念。在一个序列中,如果两个元素的顺序与它们在原始序列中的顺序相反,就称这两个元素构成了一个逆序对。
通常情况下,逆序对用于衡量一个序列的有序程度,逆序对越多,序列越无序。

考虑一个简单的整数数组 [2, 4, 1, 3, 5],其中逆序对包括 (2, 1) 和 (4, 1),因为这些元素的顺序在原始数组中相反。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/992366.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

easyrecovery 2023年最好用的数据恢复软件

EasyRecovery是一款操作简单、功能强大数据恢复软件,通过easyrecovery可以从硬盘、光盘、U盘、数码相机、手机等各种设备中恢复被删除或丢失的文件、图片、音频、视频等数据文件。 easyrecovery数据恢复软件&#xff0c;是国内顶尖工作室的技术杰作。它是一个硬盘数据恢复工具&…

[NLP]LLM---大模型指令微调中的“Prompt”

一 指令微调数据集形式太多 大家有没有分析过 prompt对模型训练或者推理的影响&#xff1f;之前推理的时候&#xff0c;发现不加训练的时候prompt&#xff0c;直接输入模型性能会变差的&#xff0c;这个倒是可以理解。假如不加prompt直接训练&#xff0c;是不是测试的时候不加…

1. Flink简述

Flink与Spark Streaming对比 数据模型和处理模型 ​ Spark 的数据模型是 RDD&#xff0c;很多时候 RDD 可以实现为分布式共享内存或者完全虚拟化&#xff08;即有的中间结果 RDD 当下游处理完全在本地时可以直接优化省略掉&#xff09;。这样可以省掉很多不必要的 I/O。 ​ …

LinkWeChat 私域管理平台基于企业微信的开源 SCRM

LinkWeChat 是国内首个基于企业微信的开源 SCRM&#xff0c;在集成了企微强大的开放能力的基础上&#xff0c;进一步升级拓展灵活高效的客户运营能力及多元化精准营销能力&#xff0c;让客户与企业之间建立强链接&#xff0c;帮助企业提高客户运营效率&#xff0c;强化营销能力…

ERROR: Failed building wheel for mpi4py

在深度学习虚拟环境中使用pip方式安装mpi4py时&#xff0c;出现错误&#xff1a; 无法安装成功时&#xff0c;可以尝试使用conda的方式&#xff1a;conda install mpi4py。

4. 广播变量

一、分区规则&#xff08;DataStream Broadcast&#xff09;和广播变量&#xff08;Flink Broadcast&#xff09; 1.1 DataStream Broadcast&#xff08;分区规则&#xff09; ​ 分区规则是把元素广播给所有的分区&#xff0c;数据会被重复处理。 DataStream.broadcast()1.…

揭秘#AI Grant 第二期项目,我是如何用AI获取灵感的?

hi&#xff0c;大家好&#xff0c;最近看到一篇文章&#xff0c;介绍了 AI版YC的二期项目&#xff0c;里面的项目非常值得我们去研究&#xff0c;推荐给大家&#xff1a; aigrant.com AI版YC 指的是 AI Grant&#xff0c;这是一家&#xff1a; 提供资金和支持的加速器项目由Nat…

Redis高并发分布式锁实战

高并发场景秒杀抢购超卖bug实战重现 秒杀抢购场景下实战JVM级别锁与分布式锁 大厂分布式锁Resisson框架实战 Lua脚本语言快速入门与使用注意事项 Redisson分布式锁源码剖析 Redis主从架构锁失效问题解析 从CAP角度剖析Redis与Zookeeper分布式锁区别 Redlock分布式锁原理与…

PostGreSQL:时间戳时区问题

时间|日期类型 PostGreSQL数据库内置的时间类型如下&#xff0c;注意到&#xff1a;内置的时间类型被分为了with time zone-带时区、without time zone-不带时区两种类型&#xff0c; time、timestamp和interval都可以接受一个可选的精度值 p&#xff08;取值&#xff1a;0-6&a…

探索云计算和大数据分析的崛起:API行业的机遇与挑战【电商大数据与电商API接入】

I. 引言 随着云计算和大数据分析技术的快速发展&#xff0c;企业和个人对数据分析和处理的需求不断增加。在这个信息爆炸的时代&#xff0c;数据已成为企业决策和战略规划的重要基础。云计算提供了强大的计算和存储能力&#xff0c;使得大规模数据的处理和分析变得更加容易和高…

Java 基于 SpringBoot 的高校点餐系统,附源码,数据库

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W,Csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 文章目录 1、效果演示2、 前言介绍3、主要技术4 系统设计4.1 系统概述**4.2 系统结构设计****4.3数据库设计…

第23章 信号量实验(iTOP-RK3568开发板驱动开发指南 )

在上面两个章节对自旋锁和自旋锁死锁进行了学习&#xff0c;自旋锁会让请求的任务原地“自旋”&#xff0c;在等待的过程中会循环检测自旋锁的状态&#xff0c;进而占用系统资源&#xff0c;而本章节要讲解的信号量也是解决竞争的一种常用方法&#xff0c;与自旋锁不同的是&…

vcruntime140.dll找不到要怎么解决?修复vcruntime140.dll的方法分享

最近挺多朋友反映说vcruntime140.dll找不到&#xff0c;不知道要怎么去解决&#xff0c;其实这一类的问题&#xff0c;之前就说过很多次了&#xff0c;首先vcruntime140.dll就是一个dll文件&#xff0c;所以它的解决方法都是差不多的&#xff0c;好了&#xff0c;今天就再来给大…

FLV封装格式

摘要&#xff1a;本文描述了FLV的文件格式。   关键字&#xff1a;FLV 1 简介 FLV流媒体格式是sorenson公司开发的一种视频格式&#xff0c;全称为Flash Video。 它的出现有效地解决了视频文件导入Flash后&#xff0c;使导出的SWF文件体积庞大&#xff0c;不能在网络上很好的…

2023国赛 B题论文 基于多波束测深技术的海洋探测建模与分析

因为一些不可抗力&#xff0c;下面仅展示小部分论文&#xff0c;其余看文末 一、问题重述 1.1 问题背景 海洋测深是测定水体深度与海底地形的重要任务&#xff0c;有两种主要技术&#xff1a;单波束测深与多波束测深。单波束适用于简单任务&#xff0c;但多波束可提供更精确…

Java锁lock的应用

从Java 5之后&#xff0c;在java.util.concurrent.locks包下提供了另外一种方式来实现同步访问&#xff0c;那就是Lock。   也许有朋友会问&#xff0c;既然都可以通过synchronized来实现同步访问了&#xff0c;那么为什么还需要提供Lock&#xff1f;这个问题将在下面进行阐述…

linux(centos7)配置SSH免密登录

给三台机器配置主机名映射 在Windows系统中修改hosts文件&#xff0c;新增以下内容&#xff1b; 192.168.xxx.xxx bigdata_node1 192.168.xxx.xxx bigdata_node2 192.168.xxx.xxx bigdata_node33台Linux的/etc/hosts文件中&#xff0c;填入如下内容。 192.168.xxx.xxx bigda…

C#学习 - 初识类与名称空间

类&#xff08;class&#xff09;& 名称空间&#xff08;namespace&#xff09; 类是最基础的 C# 类型&#xff0c;是一个数据结构&#xff0c;是构成程序的主体 名称空间以树型结构组织类 using System; //前面的using就是引用名称空间 //相当于C语言的 #include <..…

数据驱动的数字营销与消费者运营

引言&#xff1a;基于海洋馆文旅企业在推广宣传中&#xff0c;如何通过指标体系量化分析广告收益对业务带来的收益价值的思考&#xff1f; 第一部分:前链路引流投放的策略与实战 1.1 动态广告的实现: 偶然与必然 动态广告是一种基于实时数据和用户行为的广告形式&#xff0c;它…

MJ绘制「酱香拿铁」可爱壁纸;LLM产品团队招聘预告;FlowGPT提示词大赛第3季;台大深度学习音乐分析与生成最新课程 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f525; 蹭「酱香拿铁」热点的Midjouney绘图创意&#xff0c;好可爱的手机壁纸 小红书作者 美学孤诣 使用 Midjourney 制作了「上个茅班」的手…