zoukankan      html  css  js  c++  java
  • 第二篇 线性数据结构的编程实验 第5章 应用顺序存取类线性表编程

     约瑟夫问题:

           故事1:

    5.1 顺序表应用的实验范例

          5.1.1 小孩报数问题

          5.1.2 The Dole Queue

    5.2 栈应用的实验范例

            更为常用的两类数据结构————栈与队列。与此前介绍的向量和列表一样,它们也属于线性序列结构,故其中存放的数据对象之间也具有线性次序。

           在信息处理领域,栈与队列的身影随处可见。许多程序语言本身就是建立于栈结构之上,无论PostScript或者Java,其实时运行环境都是基于栈结构的虚拟机。再如,网络浏览器多会将用户最近访问过的地址组织为一个栈。这样,用户每访问一个新页面,其地址就会被存放至栈顶; 而用户每按下一次“后退”按钮,即可沿相反的次序返回此前刚访问过的页面。类似地,主流的文本编辑器也大都支持编辑操作的历史记录功能,用户的编辑操作被依次记录在一个栈中。一旦出现误操作,用户只需按下“撤销”按钮,即可取消最近一次操作并回到此前的编辑状态。

           相对于向量和列表,栈与队列的外部接口更为简化和紧凑,故亦可视作向量与列表的特例, 因此C++的继承与封装机制在此可以大显身手。

           在栈的应用方面,本章将在1.4节的基础上,结合函数调用栈的机制介绍一般函数调用的实现方式与过程,并将其推广至递归调用。然后以降低空间复杂度的目标为线索,介绍通过显式地维护栈结构解决应用问题的典型方法和基本技巧。着重介绍如何利用栈结构,实现基于试探回溯策略的高效搜索算法。在队列的应用方面,本章将介绍如何实现基于轮值策略的通用循环分配器,并以银行窗口服务为例实现基本的调度算法。

    §4.1 栈

    4.1.1  ADT接口 

         入栈与出栈

             栈(stack)是存放数据对象的一种特殊容器,其中的数据元素按线性的逻辑次序排列,故也可定义首、末元素。不过,尽管栈结构也支持对象的插入和删除操作,但其操作的范围仅限于栈的某一特定端。也就是说,若约定新的元素只能从某一端插入其中,则反过来也只能从这一端删除已有的元素。禁止操作的另一端,称作盲端。

            如图4.1所示,数把椅子叠成一摞即可视作一个栈。 为维持这一放置形式,对该栈可行的操作只能在其顶部实施:新的椅子只能叠放到最顶端;反过来,只有最顶端的椅子才能被取走。因此比照这类实例,栈中可操作的一端更多地称作栈顶(stack top),而另一无法直接操作的盲端则更多地称作栈底(stack bottom)。

           作为抽象数据类型,栈所支持的操作接口可归纳为表4.1。其中除了引用栈顶的top()等操作外,如图4.2所示,最常用的插入与删除操作分别称作入栈(push) 和出栈(pop)。

       后进先出

          由以上关于栈操作位置的约定和限制不难看出,栈中元素接受操作的次序必然始终遵循所谓 “后进先出”(last-in-first-out, LIFO)的规律:从栈结构的整个生命期来看,更晚(早) 出栈的元素,应为更早(晚)入栈者;反之,更晚(早)入栈者应更早(晚)出栈。

    4.1.2  操作实例

         表4.2给出了一个存放整数的栈从被创建开始,按以上接口实施一系列操作的过程。

    4.1.3  Stack模板类

            既然栈可视作序列的特例,故只要将栈作为向量的派生类,即可利用C++的继承机制,基于 2.2.3节定义的向量模板类实现栈结构。当然,这里需要按照栈的习惯,对各接口重新命名。

           按照表4.1所列的ADT接口,可描述并实现Stack模板类如代码4.1所示。

    2.1.2  向量
            按照面向对象思想中的数据抽象原则,可对以上的数组结构做一般性推广,使得其以上特性更具普遍性。向量(vector)就是线性数组的一种抽象与泛化,它也是由具有线性次序的一组元素构成的集合V = { v0, v1, ..., vn-1 },其中的元素分别由秩相互区分。

            各元素的秩(rank)互异,且均为[0, n)内的整数。具体地,若元素e的前驱元素共计r个, 则其秩就是r。以此前介绍的线性递归为例,运行过程中所出现过的所有递归实例,按照相互调用的关系可构成一个线性序列。在此序列中,各递归实例的秩反映了它们各自被创建的时间先后, 每一递归实例的秩等于早于它出现的实例总数。反过来,通过r亦可唯一确定e = vr。这是向量特有的元素访问方式,称作“循秩访问”(call-by-rank)。

            经如此抽象之后,我们不再限定同一向量中的各元素都属于同一基本类型,它们本身可以是来自于更具一般性的某一类的对象。另外,各元素也不见得同时具有某一数值属性,故而并不保证它们之间能够相互比较大小。

            以下首先从向量最基本的接口出发,设计并实现与之对应的向量模板类。然后在元素之间具有大小可比性的假设前提下,通过引入通用比较器或重载对应的操作符明确定义元素之间的大小判断依据,并强制要求它们按此次序排列,从而得到所谓有序向量,并介绍和分析此类向量的相关算法及其针对不同要求的各种实现版本。

    §2.2 接口
    2.2.1  ADT接口

         作为一种抽象数据类型,向量对象应支持如下操作接口。
                                                        表2.1 向量ADT支持的操作接口

            以上向量操作接口,可能有多种具体的实现方式,计算复杂度也不尽相同。而在引入秩的概念并将外部接口与内部实现分离之后,无论采用何种具体的方式,符合统一外部接口规范的任一实现均可直接地相互调用和集成。

    2.2.2  操作实例

            按照表2.1定义的ADT接口,表2.2给出了一个整数向量从被创建开始,通过ADT接口依次实施一系列操作的过程。请留意观察,向量内部各元素秩的逐步变化过程。

                                                  表2.2 向量操作实例

    2.2.3  Vector模板类

         按照表2.1确定的向量ADT接口,可定义Vector模板类如代码2.1所示。

    #include "pch.h"
    typedef int Rank;//
    #define DEFAULT_CAPACITY 3 //默认的初始容量(实际应用中可设置为更大)
    
    template <typename T> class Vector {//向量模板类
    protected:
    Rank _size; int _capacity; T* _elem;//规模、容量、数据区 void copyFrom(T const* A,Rank lo,Rank hi);//复制数组区间A[lo,hi) void expand();//空间不足时扩容 void shrink();//装填因子过小时压缩 bool bubble(Rank lo,Rank hi);//扫描交换 void bubbleSort(Rank lo,Rank hi);//气泡排序算法 Rank max(Rank lo,Rank hi);//选取最大元素 void selectionSort(Rank lo,Rank hi);//选择排序算法 void merge(Rank lo, Rank mi, Rank hi);//归并算法 void mergeSort(Rank lo,Rank hi);//归并排序算法 Rank partition(Rank lo, Rank hi);//轴点构造算法 void quickSort(Rank lo,Rank hi);//快速排序算法 void heapSort(Rank lo,Rank hi);//堆排序(稍后结合完全堆讲解) public: // 构造函数 Vector(int c = DEFAULT_CAPACITY,int s = 0,T v = 0)//容量为c、规模为s、所有元素初始为v { _elem = new T[_capacity = c];for (_size = 0;_size < s;_elem[_size++] = v); }//s <= c Vector(T const* A, Rank lo, Rank hi) { copyFrom(A, lo, hi); }//数组区间复制 Vector(T const* A,Rank n) { copyFrom(A,0,n); }//数组整体复制 Vector(Vector<T> const& V, Rank lo, Rank hi) { copyFrom(V._elem,lo,hi); } //向量区间复制 Vector(Vector<T> const& V) { copyFrom(V._elem,0,V._size); } //向量整体复制 //析构函数 ~Vector() { delete[]_elem; }//释放内部空间 //只读访问接口 Rank size() const { return _size; }//规模 bool empty() const { return !_size; } //判空 int disordered() const; //向量是否已排序 Rank find(T const& e) const { return find(e,0,(Rank)_size); } //无序向量整体查找 Rank find(T const& e, Rank lo, Rank hi) const;//无序向量区间查找 Rank search(T const& e) const //有序向量整体查找 { return (0 >= _size) ? -1 : search(e,(Rank)0,(Rank)_size); } Rank search(T const& e, Rank lo, Rank hi) const;//有序向量区间查找 // 可写访问接口 T& operator[](Rank r) const;//重载下标操作符,可以类似于数组形式引用各元素 Vector<T> & operator=(Vector<T> const&); //重载赋值操作符,以便直接克隆向量 T remove(Rank r); //删除秩为r的元素 int remove(Rank lo, Rank hi);//删除秩在区间[lo,hi)之内的元素 Rank insert(Rank r,T const& e); //插入元素 Rank insert(T const& e) { return insert(_size,e); } //默认作为末元素插入 void sort(Rank lo,Rank hi);//对[lo,hi)排序 void sort() { sort(0,_size); }//整体排序 void unsort(Rank lo,Rank hi);//对[lo,hi)置乱 void unsort() { unsort(0,_size); }//整体置乱 int deduplicate(); //无序去重 int uniquify(); //有序去重 // 遍历 void traverse(void (*)(T&)); //遍历 ( 使用函数指针,只读或局部修改 ) template <typename VST> void traverse(VST&); //遍历(使用函数对象,可全局性修改) }; //Vector /******************************************************************/ /*代码2.1 向量模板类Vector*/
    #pragma once
    typedef int Rank;//
    #define DEFAULT_CAPACITY 3 //默认的初始容量(实际应用中可设置为更大)
    
    template <typename T> class Vector {//向量模板类
    protected:
    Rank _size; int _capacity; T* _elem;//规模、容量、数据区 void copyFrom(T const* A, Rank lo, Rank hi);//复制数组区间A[lo,hi) void expand();//空间不足时扩容 void shrink();//装填因子过小时压缩 bool bubble(Rank lo, Rank hi);//扫描交换 void bubbleSort(Rank lo, Rank hi);//气泡排序算法 Rank max(Rank lo, Rank hi);//选取最大元素 void selectionSort(Rank lo, Rank hi);//选择排序算法 void merge(Rank lo, Rank mi, Rank hi);//归并算法 void mergeSort(Rank lo, Rank hi);//归并排序算法 Rank partition(Rank lo, Rank hi);//轴点构造算法 void quickSort(Rank lo, Rank hi);//快速排序算法 void heapSort(Rank lo, Rank hi);//堆排序(稍后结合完全堆讲解) public: // 构造函数 Vector(int c = DEFAULT_CAPACITY, int s = 0, T v = 0)//容量为c、规模为s、所有元素初始为v { _elem = new T[_capacity = c];for (_size = 0;_size < s;_elem[_size++] = v); }//s <= c Vector(T const* A, Rank lo, Rank hi) { copyFrom(A, lo, hi); }//数组区间复制 Vector(T const* A, Rank n) { copyFrom(A, 0, n); }//数组整体复制 Vector(Vector<T> const& V, Rank lo, Rank hi) { copyFrom(V._elem, lo, hi); } //向量区间复制 Vector(Vector<T> const& V) { copyFrom(V._elem, 0, V._size); } //向量整体复制 //析构函数 ~Vector() { delete[]_elem; }//释放内部空间 //只读访问接口 Rank size() const { return _size; }//规模 bool empty() const { return !_size; } //判空 int disordered() const; //向量是否已排序 Rank find(T const& e) const { return find(e, 0, (Rank)_size); } //无序向量整体查找 Rank find(T const& e, Rank lo, Rank hi) const;//无序向量区间查找 Rank search(T const& e) const //有序向量整体查找 { return (0 >= _size) ? -1 : search(e, (Rank)0, (Rank)_size); } Rank search(T const& e, Rank lo, Rank hi) const;//有序向量区间查找 // 可写访问接口 T& operator[](Rank r) const;//重载下标操作符,可以类似于数组形式引用各元素 Vector<T> & operator=(Vector<T> const&); //重载赋值操作符,以便直接克隆向量 T remove(Rank r); //删除秩为r的元素 int remove(Rank lo, Rank hi);//删除秩在区间[lo,hi)之内的元素 Rank insert(Rank r, T const& e); //插入元素 Rank insert(T const& e) { return insert(_size, e); } //默认作为末元素插入 void sort(Rank lo, Rank hi);//对[lo,hi)排序 void sort() { sort(0, _size); }//整体排序 void unsort(Rank lo, Rank hi);//对[lo,hi)置乱 void unsort() { unsort(0, _size); }//整体置乱 int deduplicate(); //无序去重 int uniquify(); //有序去重 // 遍历 void traverse(void(*)(T&)); //遍历 ( 使用函数指针,只读或局部修改 ) template <typename VST> void traverse(VST&); //遍历(使用函数对象,可全局性修改) }; //Vector /******************************************************************/ /*代码2.1 向量模板类Vector*/

            这里通过模板参数T,指定向量元素的类型。于是,以Vector<int>或Vector<float>之类的形式,可便捷地引入存放整数或浮点数的向量;而以Vector<Vector<char>>之类的形式,则可直接定义存放字符的二维向量等。这一技巧有利于提高数据结构选用的灵活性和运行效率,并减少出错,因此将在本书中频繁使用。

           在表2.1所列基本操作接口的基础上,这里还扩充了一些接口。比如,基于size()直接实现的判空接口empty(),以及区间删除接口remove(lo, hi)、区间查找接口find(e, lo, hi)等。 它们多为上述基本接口的扩展或变型,可使代码更为简洁易读,并提高计算效率。

           这里还提供了sort()接口,以将向量转化为有序向量。为此可有多种排序算法供选用,本章及后续章节,将陆续介绍它们的原理、实现并分析其效率。排序之后,向量的很多操作都可更加高效地完成,其中最基本和最常用的莫过于查找。因此,这里还针对有序向量提供了search() 接口,并将详细介绍若干种相关的算法。为便于对sort()算法的测试,这里还设有一个unsort() 接口,以将向量随机置乱。在讨论这些接口之前,我们首先介绍基本接口的实现。

    §2.3 构造与析构

           由代码2.1可见,向量结构在内部维护一个元素类型为T的私有数组_elem[]:其容量由私有变量_capacity指示;有效元素的数量(即向量当前的实际规模),则由_size指示。此外还进一步地约定,在向量元素的秩、数组单元的逻辑编号以及物理地址之间,具有如下对应关系:

              向量中秩为r的元素,对应于内部数组中的_elem[r],其物理地址为_elem + r

          因此 ,向量对象的构造与析构,将主要围绕这些私有变量和数据区的初始化与销毁展开。

    2.3.1 

             默认构造方法与所有的对象一样,向量在使用之前也需首先被系统创建————借助构造函数(constructor)做初始化(initialization)。由代码2.1可见,这里为向量重载了多个构造函数。

            其中默认的构造方法是,首先根据创建者指定的初始容量,向系统申请空间,以创建内部私有数组_elem[];若容量未明确指定,则使用默认值DEFAULT_CAPACITY。接下来,鉴于初生的向量尚不包含任何元素,故将指示规模的变量_size初始化为0。

          整个过程顺序进行,没有任何迭代,故若忽略用于分配数组空间的时间,共需常数时间。

    2.3.2  基于复制的构造方法
            向量的另一典型创建方式,是以某个已有的向量或数组为蓝本,进行(局部或整体的)克隆。代码2.1中虽为此功能重载了多个接口,但无论是已封装的向量或未封装的数组,无论是整体还是区间,在入口参数合法的前提下,都可归于如代码2.2所示的统一的copyFrom()方法:

     
    #pragma once
    #include "Vector.h"
    template <typename T> //元素类型
    void Vector<T>::copyFrom(T const* A, Rank lo, Rank hi) {//以数组区间A[lo,hi)为蓝本复制向量
        _elem = new T[_capacity = 2*(hi-lo)];_size = 0; //分配空间,规模清零
        while (lo < hi) { //A[lo,hi)内的元素逐一
            _elem[_size++] = A[lo++]; //复制至_elem[0,hi-lo)
        }
    }
    /******************************************************************************************/
                            /*代码2.2 基于复制的向量构造器*/

             copyFrom()首先根据待复制区间的边界,换算出新向量的初始规模;再以双倍的容量,为内部数组_elem[]申请空间。最后通过一趟迭代,完成区间A[lo, hi)内各元素的顺次复制。

            若忽略开辟新空间所需的时间,运行时间应正比于区间宽度,即O(hi - lo) = O(_size)。

            需强调的是,由于向量内部含有动态分配的空间,默认的运算符"="不足以支持向量之间的直接赋值。例如,6.3节将以二维向量形式实现图邻接表,其主向量中的每一元素本身都是一维向量,故通过默认赋值运算符,并不能复制向量内部的数据区。

           为适应此类赋值操作的需求,可如代码2.3所示,重载向量的赋值运算符。

          5.2.1 Rails

          5.2.2 Boolean Expressions

    5.3 队列应用的实验范例

         5.3.1 A Stack or A Queue?

         5.3.2 Team Queue

         5.3.3 Printer Queue

    5.4 相关题库

         5.4 Roman Roulette

  • 相关阅读:
    项目部署
    nginx
    IDEA中Lombok插件的安装与使用
    Git常用命令总结
    CentOS 7 NAT模式上网配置
    一名3年工作经验的java程序员应该具备的技能
    maven 项目加载本地JAR
    linux压缩(解压缩)命令详解
    jdk7与jdk8环境共存与切换
    linux服务器卸载本机默认安装的jdk
  • 原文地址:https://www.cnblogs.com/ZHONGZHENHUA/p/10414616.html
Copyright © 2011-2022 走看看