• 数据结构与算法(三)——列表


    数据结构与算法(三)——列表
      iwehdio的博客园:https://www.cnblogs.com/iwehdio/
    Github:https://github.com/iwehdio/DSA_THU_DJH_asJava

    导图

    列表的Java实现见:https://www.cnblogs.com/iwehdio/p/12621460.html

    1、列表的定义

    • 静态与动态:

      • 操作的静态与动态:
        • 静态:仅读取,数据结构的内容与组成一般不变。
        • 动态:需写入,数据结构的局部或整体将改变。
      • 数据元素存储与组织方式的静态与动态:
        • 静态:数据空间整体创建或销毁,数据元素的物理存储词语与其逻辑次序一致。 静态操作高效,动态操作相对低效。如向量。
        • 动态:为各数据元素单独分配和回收物理空间。逻辑上相邻的元素记录彼此的物理地址,在逻辑上形成一个整体。动态操作高效,静态操作相对低效。如列表。
    • 列表:采用动态存储的典型结构。

      • 每个元素称为节点( node )。
      • 各个节点通过指针或引用彼此联接,在逻辑上构成一个线性序列。
      • 相邻节点彼此互称前驱和后继。如果存在前驱和后继,那么必然是唯一的。
      • 没有前驱的节点称为首,没有后继的节点称为末。此外,可以认为头存在一个哨兵前驱称为头,末存在一个哨兵后继称为尾。
      • 可以认为 头、首、末、尾 节点的秩分别为 -1、0、n-1、n。
      • 在访问时尽量不使用循秩访问,而使用循位置访问。即利用节点之间的相互引用,找到特定的节点。
    • 列表元素的ADT接口:

    • 列表节点:ListNode 模板类。

      #define Posi(T) ListNode<T>*	//定义指向列表节点的数据类型(节点位置)
      template <typename T>
      struct ListNode{
          T data;			//数值内容
          Posi(T) pred;	//前驱
          Post(T) succ;	//后继
          ListNode() {}	//针对头尾哨兵的无参构造
          ListNode(T e, Posi(T) p=null, Posi(T) s=null)
              : data(e), pred(p), succ(s) {}	//默认构造器
          Posi(T) insertAsPred(T const& e);	//前插入
          Posi(T) insertAsSucc(T const& e);	//后插入	
      }
      
    • 列表的ADT接口:

    • 列表:List 模板类:

      #include "ListNode.h"	//引入列表节点类
      template <typename T>
      class List {
      private:	int _size;
          		Posi(T) header; Posi(T) trailer;	//头尾哨兵
      protected:	/*内部函数*/
      public:		/*构造函数、析构函数、对外的接口函数等*/
      }
      
    • 列表的初始化函数:

      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;		//记录规模
      }
      

    2、无序列表的操作

    • 循秩访问:

      • 列表也可以模仿向量的循秩访问(通过重载下标运算符)。

      • 但是效率比较低,时间复杂度为 O(n) 。

      • 实现:

        template <typename T>
        T List<T>::operator[](Rank r) const {
            Posi(T) p = first();	//从首节点出发
            while(0 < r++) p = p->succ;		//顺数第r个节点
            return p ->data;
        }
        
    • 查找操作:

      • 定义:在指定节点 p (可能为尾哨兵)的 n 个真前驱中找到等于 e 的最后者。

      • 实现:

        template <typename T>
        Posi(T) List<T>::find(T const& e, int n, Posi(T) p) const {
            while(0 < n--)	//从右到左逐个比较,直到命中或越界
                if(e == (p = p->pred)->data) return p;
            return null;
        }
        
    • 插入操作(以前插入为例):

      • 创建一个新节点 e ,并将其作为 p 的前驱插入。

      • 创建新节点e,前驱是 p 的原前驱,后继是 e。设 p 的原前驱的后继是 e,p 的新前驱是 e。

      • 实现:

        template <typename T>
        Posi(T) List<T>::insertBefore(Posi(T) p, T const& e){
            _size++;
            return p->insertAsPred(e);	//把e当作p的前驱插入
        }
        
        template <typename T>
        Posi(T) ListNode<T>::insertAsPred(T const& e){
            Posi(T) x = new ListNode(e, pred, this);	//创建新节点e,前驱是p的原前驱,后继是e
            pred->succ = x;		//p的原前驱的后继是e
            pred = x;		    //p的新前驱是e
            return x;	//返回新节点的位置
        }
        
    • 基于复制的构造:

      • 创建一个空列表,然后将传入的自 p 节点开始的 n 项依此作为末节点插入。

      • 实现:

        template <typename T>
        void List<T>::copyNodes(Post(T) p, int n){
            init();		//初始化空列表
            while(n--){
                //insertAsLast(p->data)实际上就是insertBefore(trailer, p->data)
                insertAsLast(p->data);
                p = p->succ;
            }
        }
        
    • 删除操作:

      • 传入所要删除的 p 处的节点。

      • 将 p 的前驱的新后继赋值为 p 的原后继,将 p 的后继的新前驱赋值为 p 的原前驱。

      • 实现:

        template <typename T>
        T List<T>::remove(Posi(T) p){
            T e = p->data;
            p->pred->succ = p->succ;	//将p的前驱的新后继赋值为p的原后继
            p->succ->pred = p->pred;	//将p的后继的新前驱赋值为p的原前驱。
            delete p;
            _size--;
            return e;	//返回被删除的值
        }
        
    • 析构操作:

      • 首先删除所有可见节点,然后删除头尾哨兵。

      • 删除可见节点通过反复删除首节点直到规模为 0 实现。

      • 实现:

        template <typename T>
        List<T>::~List()	//列表析构
        {
            clear();
            delete header;
            delete trailer;
        }
        
        template <typename T>
        int List<T>::clear(){
            int oldSize = _size;
            while(0 < _size){
                remove(header->succ);
            }
            return oldSize;
        }
        
    • 列表的唯一化:

      • 删除无序列表中的重复节点。

      • 从首节点开始,依此寻找其前驱中是否有重复节点。使得向后扩展时,搜索节点的前驱总是无重复的,这样就只需比对该搜索节点在前驱中是否有雷同项。

      • 实现:

        template <typename T>
        int List<T>::dedupicate(){
            if(_size < 2) return 0;
            int oldSize = _size;
            Posi(T) o = first();	//从首节点开始
            Rank r = 1;
            while(trailer != (p = p->succ)){
                Posi(T) q = find(p->data, r, p);	//在p的r个真前驱中,有没有雷同的
                q ? remove(q) : r++;	//如果有雷同的就移除前驱中雷同的项,否则将不雷同的前驱向后扩展一项
            }
            return oldSize - _size;		//返回被删除的元素数
        }
        
    • 有序列表的唯一化:

      • 删除有序列表中的重复节点。

      • 由于有序,如有雷同则必定是紧邻在一个区段中的。

      • p 指向每个区段的第一个节点,删除与之相同的后继。如果出现不相同的后继,则 p 指向这个后继。

      • 实现:

        template <typename T>
        int List<T>::uniquify(){
            if(_size < 2) return 0;
            int oldSize = _size;
            ListNodePosi(T) p = first();	//p初始化为首节点,p为每个区段的首节点
            ListNodePosi(T) q = p->succ;	//q为各个区段中p的后继
            while(trailer != (q->succ)){	//考察紧邻的节点对
                if(p->data != q->data){
                    p = q;
                } else {
                    remove(q);
                }
                q = p->succ;
            }
            return oldSize - _size;
        }
        
      • 时间复杂度为 O(n)。

    • 有序列表查找操作:

      • 与无序向量的查找算法完全相同,在效率上并没有提高,这是由列表的循位置访问的特点决定的。

    3、选择排序与插入排序

    • 选择排序:

      • 选取所有元素中最大的放在列表末,然后再选择剩下的元素中最大的放在末的前驱位置,反复执行。也就是说,每次选择出剩余集合中的最大值。

      • 冒泡排序就是一种选择排序,但是由于每次只进行相邻元素的位置交换,导致效率很低。

      • 改进思路:将序列分为前缀无序子序列 U 和后缀有序子序列 L ,其中 L 中的元素大于等于 U 中的元素。在每次迭代中,选取 U 中最大的元素,并将其移动至 L 的最前端。

      • 实现:

        template <typename T>
        void List<T>::selectionSort(Posi(T) p, int n){
            Posi(T) head = p->pred;
            Posi(T) tail = p;
            for(int i=0; i<n; i++)	
                tail = tail->succ;	//待排序区间为(head,tail)
            //从待排序区间内找出最大者,将其从原位置删除并插入tail的位置,然后将tail前移,,待排序区间宽度-1
            while(1 < n){
                //insertBefore()和remove()虽然可以认为时O(1)时间,但由于使用了动态内存分配,会使效率大大降低。实际上更倾向于进行节点数据域的交换
                insertBefore(tail, remove(selectMax(head->succ, n)));
                tail = tail->pred;
                n--;
            }
        }
        
        template <typename T>	//获取起始于p的n个元素的最大值,画家算法
        Posi(T) List<T>::selectMax(Posi(T) p, int n){
            Posi(T) max = p;	//初始化p为最大
            for(Posi(T) cur=p; 1<n; n--){
                //后续节点与max逐一比较,!lt(a,b)比较器表示a不小于(not less than,即>=)b时
                if(!lt((cur=cur->succ)->data, max->data){
                    max = cur;
                }
            }
            return max;
        }
        
      • 时间复杂度总是 Θ(n^2),没有最好和最坏情况。但是与冒泡排序相比元素移动操作大大减少。

    • 插入排序:

      • 将整个序列分为前缀有序序列 [ 0, r ) 和后缀无序序列 [ r, n),然后将第 r 个元素插入到前缀有序序列中的合适位置,使得前缀有序序列仍然有序并且规模加一。

      • 与选择排序的区别:选择排序中无序部分中元素总小于有序部分,而插入排序无此限制;而且因此选择排序的操作对象为无序部分中的最大值,而插入排序的操作对象为无序部分与有序部分交界的元素。

      • 实现:

        template <typename T>
        void List<T>::insertionSort(Posi(T) p, int n){
            for(int r=0; r<n; r++){		//r实际上就是有序前缀序列的长度
                //每次在search()返回的位置插入,然后删除原位置上的节点
                insertAfter(search(p->data, r, p), p->data);
                p = p->succ;
                remove(p->pred);
            }
        }
        
      • 时间复杂度:最好 O(n),最差 O(n^2),平均复杂度为 O(n^2)。

      • 逆序对:两个元素前大后小。

        • 如果 a、b 是一个逆序对( a>b ),我们将 b 所对应的逆序对数加一。也就是某个元素对应逆序对数,记为其作为逆序对中较右、较小的角色的逆序对数量。
        • 对于插入排序,列表的逆序对数就表征了所要进行的比较次数。最好情况下,列表为顺序的,逆序对数为 0 。最差情况下,列表为逆序的,逆序对数为 n^2。时间复杂度为插入时间加比较时间,比较时间最好为 0,最差为 O(n^2);插入时间为 O(n)。则总得复杂度为最好 O(n),最差 O(n^2)。
        • 插入排序的时间复杂对依赖于输入序列的逆序对数,是输入敏感的。

    参考:数据结构与算法(清华大学C++描述):https://www.bilibili.com/video/av49361421


    iwehdio的博客园:https://www.cnblogs.com/iwehdio/

  • 相关阅读:
    vid = two 切开 分开 两个眼睛 还有看的含义 词根
    ACS 构词法 + 44个后缀 记忆方法
    测试打包失败 已解决 分析过程 关键字 Jenkins nexus packagelock.json npm install build
    RBAC 介绍,案例讲解
    为什么Dapr是比SpringCloud和Istio更优雅的微服务框架?
    SmartIDE v0.1.18 已经发布 助力阿里国产IDE OpenSumi 插件安装提速10倍、Dapr和Jupyter支持、CLI k8s支持
    Then Swift 初始化对象语法糖
    javaGUI 最基础的界面 标签、输入框、按钮、事件处理 模板
    2019百度之星第三场 最短路2
    2021百度之星初赛第二场 魔怔
  • 原文地址:https://www.cnblogs.com/iwehdio/p/12290282.html
Copyright © 2020-2023  润新知