数据结构与算法——线性表(顺序表篇)

🚀线性表的基本概念

在这里插入图片描述

🚢线性表的定义


线性表(linear list)是最常用且最简单的一种数据结构, 一个线性表是n个数据元素的有限序列


在这里插入图片描述
线性表中的数据元素可以是各种各样的,但同一线性表中的元素必定具有相同的性质(占用相同的空间大小),即属于同一种数据对象,相邻数据元素之间存在着序偶关系(序:就是有序的意思 偶:一对儿. 序偶:一对有序的数),若用L表示线性表的话,可以将线性表记为:

📌 L = ( a 1 , . . . , a i − 1 , a i , a i + 1 , . . . . , a n ) L=(a1,...,ai-1,ai,ai+1,....,an) L=(a1,...,ai1,ai,ai+1,....,an)

则表中 a i − 1 ai-1 ai1领先于 a i ai ai, a i ai ai领先于 a i + 1 ai+1 ai+1,那么,当锁定 a i ai ai这个元素时,我们就可以称作 a i − 1 ai-1 ai1 a i ai ai直接前驱元素,称 a i + 1 ai+1 ai+1 a i ai ai直接后继元素。当 i = 1 , 2 , 3 , . . . , n − 1 i=1,2,3,...,n-1 i=1,2,3,...,n1时, a i ai ai有且仅有一个直接后继,当 i = 2 , 3 , 4 , . . . , n i=2,3,4,...,n i=2,3,4,...,n时, a i ai ai有且仅有一个直接前驱

线性表中元素的个数n(n>=0)定义为线性表的长度n=0时,表示线性表为空,称空表;在非空的线性表中,每一个元素都有一个确定的位置,如 a 1 a1 a1是第一个数据元素, a n an an是最后一个数据元素, a i ai ai是第i个数据元素,称i为数据元素 a i ai ai在线性表中的位序

在稍微复杂的线性结构中,一个数据元素可以由若干个数据项(item)组成,在这种情况下,我们通常把数据元素称为记录,含有大量记录的线性表又称为文件

例如,把一张学生表中的每个学生看为一个记录,记录由学生的学号、姓名、性别、班级组成,那整张学生表,可以看做一个文件

在这里插入图片描述

🚢线性结构的特点


在数据元素的非空有限集中:

  • 📌存在唯一的一个首元素
  • 📌存在唯一的一个尾元素
  • 📌除第一个元素之外,集合中的每个数据元素有且只有一个前驱
  • 📌除最后一个元素外,集合中的每个数据元素有且只有一个后继


🚀线性表的基本操作


  • 📌初始化表。构造一个空的线性表L,分配内存空间
  • 📌销毁操作。销毁线性表,并释放线性表L所占用的内存空间
  • 📌插入操作。在表L中的第i个位置上插入指定的元素
  • 📌删除操作。删除表L中的第i个位置的元素
  • 📌按值查找操作。根据给定的值,在表L中查找到与之对应的元素
  • 📌按位查找操作。获取到表L中的第i个位置元素的值

其他常用操作:

  • 📌求表长。返回一个线性表的长度,也可以理解为表L中的元素个数
  • 📌输出操作。顾名思义,按照先后次序依次打印出表L中的所有元素值
  • 📌判空操作。判断一个线性表L是否为空表


🚀线性表的顺序表示和实现(顺序表)


线性表的顺序表示指的是用一组地址连续的存储单元依次存储线性表的数据元素

🚢什么是顺序表?


用顺序存储的方式实现线性表

线性表的顺序表示称作为线性表的顺序存储结构或者顺序映像(sequential mapping),元素之间的关系由存储单元的邻接关系来体现,称这种存储结构的线性表为顺序表,顺序表的相邻元素在逻辑结构和物理结构(存储结构)位置相邻。

在这里插入图片描述


假设线性表的每个元素需占用l个存储空间,并以所占用的第一个单元的存储地址作为数据元素的存储位置。则线性表中第i+1个数据元素的存储位置 L O C ( a i + 1 ) LOC(ai+1) LOC(ai+1)和第i个数据元素的存储位置 L O C ( a i ) LOC(ai) LOC(ai)之间满足下列关系:

📌 L O C ( a i + 1 ) = L O C ( a i ) + l LOC(ai+1) = LOC(ai)+l LOC(ai+1)=LOC(ai)+l

在这里插入图片描述

则通过上述这个关系,又可以得出第i个元素第m个元素之间的关系为:

📌 L O C ( a m ) = L O C ( a i ) + ( m − i ) ∗ l LOC(am) = LOC(ai)+(m-i)*l LOC(am)=LOC(ai)+(mi)l

在这里插入图片描述

只要确定了存储线性表的起始位置,线性表中任一数据元素都可以随机存取,所以线性表的顺序存储结构是一种随机存取的存储结构

🚢顺序表的特点

  • 📌随机访问,即可以在 O ( 1 ) O(1) O(1)时间内找到第i个元素
  • 📌存储密度高,每个节点只存储数据元素
  • 📌拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
  • 📌插入、删除操作不方便,需要移动大量元素


🚀顺序表的基本操作


🚢静态分配(初始化表)

# define MaxSize 10         //定义最大长度
typedef struct{
    int data[MaxSize];      //用静态数组存放数据元素
    int length;             //顺序表的当前长度
}SqList;                    //顺序表名称

void InitializeList(SqList &L){
    for (int i = 0; i < MaxSize; i++)
    {
        L.data[i] = 0;      //将所有数据元素初始化为0
    }
    L.length = 0;
}

int main(){
    SqList L;               //声明一个顺序表
    InitializeList(L)       //初始化顺序表
    ......
}

在这里插入图片描述


🚢动态分配(初始化表)


在C语言中,动态分配需要借助两个函数:

  • void *malloc(size_t size) 分配所需的内存空间,并返回一个指向它的指针。指针指向已分配大小的内存
  • void free(void *ptr) 释放之前调用 calloc、malloc 或 realloc 所分配的内存空间。指针指向一个要释放内存的内存块
#define InitSize 10         //默认的最大长度
typedef struct{
    int *data;              //指向动态分配数组的指针
    int MaxSize;            //顺序表的最大容量
    int length;             //顺序表的当前长度
}SeqList;

void InitializeList(SeqList &L){
    //用malloc函数申请一片连续的存储空间
    L.data = (int *)malloc(InitSize*Sizeof(int));       //返回一个指针变量
    L.length = 0;
    L.MaxSize = InitSize;
}

//动态增加数组的长度
void IncreaseSize(SeqList &L, int len){
    int *p = L.data;
    L.data = (int *)malloc((L.MaxSize+len)*sizeof(int));
    for(int i=0; i<L.length; i++){      //将原来的数据赋值到新区域
        L.data[i] = p[i];
    }
    L.MaxSize = L.MaxSize + len;        //增加顺序表的最大长度    
    free(p);                            //释放原来的内存空间
}


int main(){
    SeqList L;              //声明一个顺序表
    InitializeList(L);      //初始化顺序表
    ......
    ......
    IncreaseSize(L, 5);     //往原来顺序表中再添加5个元素
}

内存分析和上面静态分配一样,不同的是,在动态分配中,生成连续的存储空间后,需要通过指针指向这片连续的空间

在这里插入图片描述

动态增加顺序表的长度理解起来其实也很简单,实际上就是再次使用malloc()函数再次开辟一个更大的空间,然后把原来的顺序表复制到新开辟的空间里,再回收原来的空间

在这里插入图片描述


🚢插入元素

插入操作:在表L中的第i个位置上插入指定元素

#define MaxSize 10         //定义的最大长度
typedef struct{
    int MaxSize;            //顺序表的最大容量
    int length;             //顺序表的当前长度
}SeqList;

bool ListInsert(SeqList &L, int i, int j){
    if(i<1 || i>L.length+1){                //判断i的范围是否有效 
        return false;
    }
    if(L.length >= MaxSize){                //判断存储空间是否已经满了
        return false;
    }
    for(int k=L.length; K>=i; K--){         //将第i个元素及其以后的元素后移
        L.data[k] = L.data[k-1];
    }      
    L.data[i-1] = e;        //在位置i处放入新增元素j
    L.length++;             //长度加1
}

int main(){
    SeqList L;              //声明一个顺序表
    InitializeList(L);      //初始化一个顺序表
    ......
    ......
    ListInsert(L, 3, 3);    //在顺序表L第三个元素的位置插入元素3
}

插入操作时,我们要在执行插入操作的具体实现中分析最深层循环语句的执行次数与问题规模n的关系,上述代码问题规模n=L.length

分析上述代码,可以得出,假如新元素插入在表尾,那么就不需要移动元素,那时间复杂度就为 O ( 1 ) 。 O(1)。 O(1)假如新元素插入在表头,那么所有元素都要向后移动,时间复杂度就为 O ( n ) O(n) O(n)

我们一般都是考虑最坏时间复杂度,所以,可得出在顺序表中插入一个元素的时间复杂度为:

📌 T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)


🚢删除元素

删除操作:删除表L中的第i个位置的元素

#define MaxSize 10         //定义的最大长度
typedef struct{
    int MaxSize;            //顺序表的最大容量
    int length;             //顺序表的当前长度
}SeqList;

bool ListDelete(SeqList &L, int i, int &j){
    if(i<1 || i>L.length+1){                //判断i的范围是否有效 
        return false;
    }
    j = L.data[i-1];            //将被删除的元素赋值给j
    for(int k=i; k<L.length; k++){          //将第i个位置后的元素前移
        L.data[k-1] = L.data[k];
    }
    L.length--;                 //顺序表长度减1
    return true;
}

int main(){
    SeqList L;              //声明一个顺序表
    InitializeList(L);      //初始化一个顺序表
    ......
    ......
    int j = -1;
    ListDelete(L, 3, j);    //删除顺序表L中第三个元素
}

在删除操作中,我们定义了一个变量j,它会在内存中开辟一小个空间,用来保存被删除的元素。

接下来我们分析顺序表删除操作的时间复杂度,其实顺序表插入和删除操作的时间复杂度是一样的,假如删除元素是表的最后一个元素,就不需要移动其他元素,时间复杂度就为 O ( 1 ) O(1) O(1)。假如删除元素为第一个元素,那么就要将2~最后一个元素前移,时间复杂度为 O ( n ) O(n) O(n)

所以综合考虑,删除操作时间复杂度为:

📌 T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)


🚢按位查找

按位查找:获取表L中第i个位置的元素的值

ElemType GetElem(SeqList L, int i){
    return L.data[i-1];
}

时间复杂度:

📌 T ( n ) = O ( 1 ) T(n) = O(1) T(n)=O(1)

因为顺序表的各个数据元素在内存中连续存放,因此可以根据下标立即找到第i个元素,同时也印证了顺序表随机存取的特性


🚢按值查找

按值查找:根据给定元素,获取表L中与之对应的元素

int LocateElem(SeqList l, ElemType e){
    for(int i=0; i<L.length; i++){
        if(L.data[i] == e){
            return i+1;     //返回元素在顺序表中的位置
        }
        return 0;            //退出循环,说明没有找到元素
    }
}

在按值查找操作中,通过给定的元素与顺序表内各个元素依次进行对比,所以可以分析得出,当给定元素在表头时,则只需要对比一次,时间复杂度就为 O ( 1 ) O(1) O(1)。如果给定元素是最后一个元素,则就要对比所有数据,时间复杂度为 O ( n ) O(n) O(n)

综上考虑,按位查找时间复杂度为:

📌 T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)



💻总结

以上就对线性结构概念以及线性结构中顺序表概念的一些概述,以及顺序表基本操作的讲解和分析,其实在顺序表操作中,只要理解了顺序表的创建,后面的增删改查其实都很容易理解。在学习数据结构与算法的过程中,我们更多的是要去思考代码如何实现,操作的整个流程是怎样的,这样有助于我们牢记重点和知识点,并且学起来也不会很枯燥。同样,我也会持续更新数据结构与算法的内容,在自我提升的同时,也能有好的内容分享给大家


🎨觉得不错的话记得点赞收藏呀!!🎨

😀别忘了给我关注~~😀

  • 94
    点赞
  • 208
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 89
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 89
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小田是个程序员

各位看官,觉得不错就多支持哟!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值