2.2顺序表及实现

1.顺序表

​ 1.由于线性表中的每个数据元素的类型相同,所以一般选择一维数组来实现顺序表,又因为数组和线性表都有”序号”这种概念,所以让线性表的序号和一维数组的下标一一对应是一个非常不错的选择,可以方便调用。但是请注意,数组的下标是从0开始计数的嗷,和线性表是刚好错开的哈,也就是说,线性表的第i个元素对应的是数组的第i-1个位置(当然,如果实在有强迫症或者害怕搞混的,也可以将数组的下标定义为从1开始或者把线性表的序号定义为从0开始)。

​ //学过C++的数组后都知道,一维数组在定义时需要定义数组的长度,有两种方法,一种是直接在定义时表明数组长度,例如:

int arr[100];

​ //或者在文件开头就定义一个常数MaxSize来方便多次调用和统一修改,例如:

const int MaxSize = 100;
int arr[MaxSize];

Tip:C++数组

​ 2.那么把这个思路带回顺序表,既然是用数组来实现顺序表,那么自然也要遵守顺序表的规则。不仅如此,因为在线性表中可以进行插♂入操作,所以数组的长度要大于当前线性表的长度不然被插满了就插不进去了。前面提到用MaxSize表示数组的长度,这里用length表示线性表的长度(其实我上篇文章就提到了哒)。

二者关系:MaxSize >> length

​ 3.顺序表是一种随机存取的储存结构。

只要确定了储存顺序表的起始地址(即基地址),计算任意一个元素的存储地址的时间相等的,具有这一特点的存储 结构称为随机存取结构。

​ 4.顺序表的优缺点

  • 优点:
    • 通过下标来直接存储,可以快速地存取表中的任一位置的元素。
    • 无需为了表示表中元素之间的逻辑关系而增加额外的存储空间。
  • 缺点:
    • 插入和删除较慢。插入或者删除一个元素时,整个表需要遍历移动元素来重新排一次顺序。
    • 表的容量难以确定,表的空间难以扩充。当需要存取的元素个数可能多于顺序表的元素个数时,会出现"溢出"问题;当元素个数远少于预先分配的空间时,空间浪费巨大

​ 5.顺序表的时间性能:查找 O(1),插入和删除O(n)。

Tip:时间复杂度与空间复杂度

2.顺序表的实现

​ 由于无法确定线性表的数据元素类型,在实现线性表时会使用C++的模板机制。

Tip:关于C++的模板机制可以参考这篇文章:【带你吃透C++】模板详解

​ 下面来看一下一个标准顺序表的具体写法:

const int MaxSize = 100;		//100只是举个栗子,按实际情况来设置MaxSize
template<class T>				//定义模板类SeqList,以下的代码都属于这个步骤
//这里的T没有什么特殊含义,只是习惯而已,书上的是DataType,换成T只是为了图个方便,换成其他字母也是一样的效果
class SeqList
{
public:
    SeqList()					//这个是无参构造函数,用于建立一个空的顺序表
    {
        length = 0;					//这一步就是设定顺序表初始状态为空
    }
    SeqList(T a[], int n);		//这个是有参构造函数,这里的意思是建立一个长度为n的顺序表
    ~SeqList(){}				//析构函数一般为空
    int Length()				//这里就是上一篇文章提到的Length函数,链接放在代码块下面
    {
        return length;				//返回线性表的长度
    }
    T Get(int i);				//按位查找,在线性表中查找第i个元素
    int Locate(T x);			//按值查找,在线性表中查找值为x的元素序号
    void Insert(int i, T x);	//插♂入操作,在线性表中第i个位置插♂入值为x的元素
    T Delete(int i);			//删除操作,删除线性表的第i个元素
    void PrintList();			//遍历操作,按序号依次输出各元素
	//以上的各种操作我都在上一篇文章中大概分析过原理,链接放在代码块下面了,欢迎前去指正~
private:
    T data[MaxSize];			//存放数据元素的数组
    int length;					//线性表的长度
};

​ 需要说明一下,这里只是写出了程序的框架,即只写出了需要用到的函数及其定义,没有写具体实现。在实际操作中,上面的函数基本上都是单独写在后面的,方便修改和排除故障。(其实就是类与对象里面的写法)

Tip:回顾一下上一篇文章的内容吧~线性表的逻辑结构

1>构造函数

  • 无参构造函数:创建空表,将length初始化为0。
  • 有参构造函数:创建长度为n的顺序表,需要传入给定的数组元素(包括数据本体和数据元素的个数)。

有参构造的实际操作(角色演示[确信]):以SeqList函数为例

template<class T>
SeqList<T>::SeqList(T a[], int n)
{
    if(n>MaxSize)throw "参数非法(你犯法了.jpg)";
    for(i=0; i<n; i++)
        data[i] = a[i];
    length=n;
}

2>求线性表的长度

​ 求线性表的长度只需返回成员变量length的值(因为上一步就已经在构造函数里面输入过length的值啦)。

3>查找操作

  • 按位查找(Get):时间复杂度为O(1),算法如下:

    template<class T>
    T SeqList<T>::Get(int i)
    {
        if(i<1 && i>length)throw "查找位置非法(输入的数据不在范围内)";
        //数据不在范围内就返回异常
        else return data[i-1];
        //数据正常就从data数组返回对应的值
        //注意:这里我默认是下标从0开始的数组和序号从1开始的线性表,请务必结合自身情况确定是否需要修改!
    }
    

    (从零开始的异世界生活(bushi))

  • 按值查找(Locate):需要和顺序表中的元素依次比较,时间复杂度是O(n),算法如下:

    template<class T>
    int SeqList<T>::Locate(T x)
    {
        for(i=0;i<length;i++)
        //经典遍历2333...
            if(data[i]==x)return i+1;
        	//如果找到了相同的值,就返回元素的序号(注意嗷!!!这里是序号,不是数组下标)
        return 0;//没找到就返回0——表示查找失败
        //注意:这里我默认是下标从0开始的数组和序号从1开始的线性表,请务必结合自身情况确定是否需要修改!
    }
    

4>插入操作

​ 举个栗子,在表 L 中的第 i 个位置上插入指定元素 e 时,我们需要先找到 L 中的第 i 个元素,然后将这第 i 个元素以及后面的所有元素都向后挪一个位置,给将要被插♂进来的新元素让一个位置,但是,在做这些之前,我们需要先考虑两个问题:

  1. 插♂进来的新元素是否合法?如果不合法的话我们需要给他丢回去并反手给他一个大逼兜(就你小子天天违法是吧?)
  2. 顺序表是否足够长?如果已经满了,我们同样需要给他丢回去(已经被塞满力(悲))。

本算法的平均时间复杂度为O(n),具体代码如下:

Tip:这里说平均时间复杂度是因为Insert算法有两种极端(用T(n)表示时间复杂度):

  • 最好情况:新元素插入到表尾,不用移动元素,T(n)= O(1);
  • 最坏情况:新元素插入到表头,所有元素往后挪,T(n)= O(n);
template<class T>
void SeqList<T>::Insert(int i, T x)
{
    if(length>=MaxSize)throw "上溢(意思就是顺序表满了,元素溢出)";
    if(i<l || i>length+1)throw "位置(意思是没有这个位置)";
    for(j=length; j>=i; j--)
        data[j]=data[j-1];	//遍历,一个个往后面挪
    data[i-1]=x;	//真正的插入在这里!
    length++;		//最后别忘了更新一下顺序表的长度
}

在这里做一个小小的区分,顺序表的MaxSize值并不代表顺序表的长度length,在前面提到过:MaxSize >> length

5>删除操作

​ 基本上可以理解为和上面的插入操作反着来,要删除表 L 中的第 i 个元素时,我们需要先找到 L 中的第 i 个元素,对它进行删除操作,然后将第 i+1 个元素以及后面的所有元素都向前挪一个位置,将删除产生的空洞堵上,但是,在做这些之前,我们同样需要先考虑两个问题:

  1. 删除的位置是否合法?如果不合法就返回删除异常并飞起给他一脚(你tm天天卡bug是吧?)
  2. 在删除之前,顺序表是否为空?如果是空的,就返回下溢异常(一个元素都没有你删个der)

Tip:上溢和下溢不同,上溢通常是程序中的错误,程序应当捕获并处理;而下溢通常作为条件用来控制程序的走向

​ (其实上溢和下溢还有其他更详细的解释,只不过目前只需要了解这些即可)

本算法的平均时间复杂度也为O(n),具体代码如下:

Tip:这里说平均时间复杂度同样是因为Delete算法有两种极端(用T(n)表示时间复杂度):

  • 最好情况:删除表尾元素,不用移动其他元素,T(n)= O(1);
  • 最坏情况:删除表头元素,所有元素往前挪,T(n)= O(n);(和插入操作一个模板2333)
template<class T>
T SeqList<T>::Delete(int i)
{
    if(length==0)throw "下溢(表是空的)";
    if(i<1 || i>length)throw "位置(你在尝试删除一个不存在的位置doge)";
    x=data[i-1];
    for(j=1; j<length; j++)
        data[j-1]=data[j];	//又又又是遍历
    length--;		//更新顺序表的长度
    return x;		//返回被删的元素值
}

6>遍历操作

​ 嘛~这个就很简单啦,就是按照下标依次输出呗,上代码:

template<class T>
void SeqList<T>::PrintList()
{
    for(i=0; i<length; i++)
        cout<<data[i];	//依次输出各元素值
}

3.小结

​ 顺序表的结构很简单,基本上了解数组的原理就能明白顺序表,但同样,在简单的同时必定有缺陷,顺序表的能力十分有限,比如在无法确定数据量的时候,顺序表就很难定义MaxSize。因此,下一节——链表有能力解决这个问题。

posted @ 2023-04-16 18:20  杨与S8  阅读(13)  评论(0编辑  收藏  举报