顺序表的插入和删除

前言

相信通过上一篇文章(顺序表的定义)大家已经能动手定义一个顺序表,并且知道顺序表如何进行初始化的工作。当完成一个顺序表的建立和初始化后,我们得到的会是一个空的顺序表(空表),所以这篇文章我们来学习顺序表的两个基本操作——插入和删除。

顺序表的基本操作——插入

ListInsert(&L,i,e):插入操作。在表L中的第i个位置(位序)上插入指定元素e

  • 位序的意思就是按照我们正常生活中的理解,第1位就是第1个,而不是程序语言中的第0位代表第1个

现假设我们用静态分配的方式实现了一个顺序表:

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

那么在内存中我们看到的应该是这样。

image-20220705093627056

假设现在我们已经存入了5个元素,其在顺序表中存放应该是这样。

image-20220705093721667

如果这时候我们要进行插入操作——在第3个位置插入一个元素c。

image-20220705093849561

这时候c就应该成为b的后继节点,d的前驱节点。

image-20220705093946228

由于顺序表是通过物理地址的相邻来表示逻辑相邻的元素。

image-20220705094143579

所以这时候就需要将原先处于第3个位置及第3个位置以后的所有元素都后移1位,再将c插入到第3个位置。

image-20220705094111179

广义化

从上述的例子中我们可以得出一个普遍的结论:

  • 如果想在第i个位置插入n个元素,那么就要将第i个元素及第i个元素以后的所有元素,向后移n位,然后再以此插入这n个元素

代码实现

注意:我们本篇文章主要以静态分配的方式来实现,当然动态分配也大同小异

在这里我们只书写了部分代码

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

void ListInsert(SqList &L,int i,int e){
    for(int j=L.length;j>=i;j--)        // 将第i个元素及之后的元素后移
        L.data[j] = L.data[j-1];
    L.data[i-1] = e;                    // 在位置i处插入e元素
    L.length++;                         // 长度+1
}

int main(){
    SqList L;              // 声明顺序表
    InitList(L);            // 初始化顺序表
    // ...省略往顺序表插入元素的过程(你可以假装它满了)
    ListInsert(L,3,3)
    return 0;
}

代码分析

声明顺序表和初始化顺序表在上篇文章中已经做了详细的介绍,这里就不再过多介绍,我们直接开始介绍ListInsert()插入函数的操作

  1. 开头的for循环是顺序表的最后端(最后一个元素)开始将元素后移
    • 令j = 顺序表的最后一个元素,每一次移动后j都-1,并且在j<i之后停止循环
      • 这里我们用上面的假设:顺序表里已经有5个元素,要在第3个位置插入一个3(ListInsert(L,3,3):在表L中的第3个位置插入一个元素3)
      • j = 5(但是我们知道在程序语言中,下标是5相当于表示的是第6个位置)
      • L.data[j] = L.data[j-1]:将顺序表中第5个位置的元素赋值给第6个元素(就相当于把第5个位置后移至第6个位置)
      • 以此类推,实现顺序表元素后移的操作。当j=i时,这里就是j=3时,L.data[j] = L.data[j-1]:将第3个位置的元素赋值给第4个位置。到此第3个位置的元素以及第3个位置往后的元素已经全部后移一位
  2. 经过上面的循环后移操作后,第3个位置已经空出,我们直接在第3个位置上插入我们要插入的元素e(赋值为3),上面我们说过由于在程序语言中,是从0开始,所以i=3时表示的是数组中第4个位置,所以要i-1
    • 注意这里的虽然第3个位置经过循环后移操作已经空出,但是不代表里面没有数据,这里只是将前一位赋值到后一位,但是前一位的数值仍然存在。当你进行了赋值操作之后,覆盖了原来的数据,才算真正的数据消失(这里听不懂没关系,可能我说的比较抽象,只是为了更加的严谨)
  3. 由于插入了一个新元素,所以顺序表的长度要+1

问题

这时候已经可以让别人调用自己写的函数实现顺序表的插入功能了,但是我们的代码还是比较的薄弱,仍然还有一些问题,就是可能会存在一些不合法的操作。

image-20220705104014313

如果此时顺序表中只有6个元素,而你的猪队友想在第9个位置插入元素呢?这显然时不合法的,因为我们知道顺序表是通过物理地址的相邻来表示逻辑位置的相邻,第9个位置插入元素,但是第7和第8的位置上都还没有元素。

这时我们就需要进行插入操作的范围限定。如果我们已经有了6个元素,那么我们可以操作的范围应该是:[1,7](注意这里是使用位序表达, 即1代表第1个位置),也就是[1,length+1]。如果传入的i超出了这个范围,那么我们就不能继续进行下面的操作,所以可以加入一个判断,来判断i是否处于可操作的范围内。

当然,如果当你的顺序表已经插满时,我们也不能在进行插入,所以我们还应该加入一段代码用来检测顺序表是否已经存满,如果已经从存满则不进行下面插入的操作。

反馈

虽然说上面我们考虑到了一些不合法的操作,并且插入了代码检测并且限制。但是不管是哪种不合法的情况,我们都应该要给使用者一些反馈,这样才能让他们知道他们到底是什么操作导致了插入不成功。

所以说我们在写代码的时候除了要保障代码的逻辑正确,此外还要让别人在使用我们代码的时候比较的舒服。比如我们可以在他们可能产生不合法行为的地方返回一些东西,合法行为的地方也返回一些东西。

bool ListInsert(SqList &L,int i,int e){
    if(i<1 || i>L.length+1)			// 判断i的范围是否有效
        return false;
    if(L.length>=MaxSize)			// 当前存储空间已满,不能插入
        return false;
    for(int j=L.length;j>=i;j--)	//	将第i个元素及以后的元素后移
        L.data[j] = L.data[j-1];
    L.data[i-1] = e;				// 在第i个位置插入e
    L.length++;						// 长度+1
    return true;
}

从以上的代码我们若检测到用户的不合法操作,则返回一个false的布尔值,以告诉使用者你的操作不合法。而在合法执行操作后返回一个true的布尔值来告诉使用者,你的操作合法。按照上述这样的代码才具有”健壮性“

插入操作的时间复杂度

在计算时间复杂度的时候,我们应该是关注最深层循环的执行次数与问题规模n的关系。在这里最深层的循环时将元素后移的语句

for(for(int j=L.length;j>=i;j--))
    L.data[j] = L.data[j-1];

我们在之前的时间复杂度中说到过如果碰到问题规模相同会有(最好、最坏、平均)的情况:

  • 最好情况:新元素插入到表尾,不需要移动元素
    • i = n+1 ,循环次数0,最好时间复杂度 = O(1)
  • 最坏情况:新元素插入表头,需要将原有的n个元素全部向后移动
    • i = 1,循环次数n+1次,最坏时间复杂度 = O(n)
  • 平均情况:假设新元素插入到任何一个位置的概率相同,所以 i = 1,2,3,…,length+1的概率 p = 1 n + 1 p=\frac{1}{n+1} p=
  • 18
    点赞
  • 87
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值