zoukankan      html  css  js  c++  java
  • 数据结构与算法(三)——列表

    数据结构与算法(三)——列表
      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/

  • 相关阅读:
    两个页面相同js方法兼容
    两个页面js方法兼容
    jQuery Chosen 使用
    我写的一个抓取基金净值的工具
    自己做的一个固定大小对象内存池,效率大概为原始的new/delete的2倍
    用libevent实现的echo服务器及telnet客户端
    共享一个防止脚本重复启动的shell脚本
    神煞排盘软件
    一个在字符串中查找多个关键字的函数strstrs(三种不同算法实现及效率分析)
    写了一个时间处理的类,能将人类时间转换成距离公元零年一月一日秒数(时间戳),同时支持时间戳转换成日期时间
  • 原文地址:https://www.cnblogs.com/iwehdio/p/12290282.html
Copyright © 2011-2022 走看看