数据结构--顺序表的插入、删除、查找详解

数据结构

1 线性表
1.1 线性表的定义
  • 通俗一点来说,线性表就是指各个数据元素它们之间的逻辑关系、逻辑结构是这种一条线的结构,就是被穿到一起,数据元素之间有这样的前后关系。

  • 严谨的定义:线性表是具有相同数据类型 的n(n>=0)个数据元素的有限序列,其中n为表长,当n等于0时,线性表是一个空表。若用L命名线性表,则其一般表示为

    ​ L=(a1,a2,...,ai,ai+1,...,an)

  • ai是线性表中的"第i个"元素线性表中的位序,位序从1开始,数组下标从0开始

  • a1是表头元素,an是表尾元素

  • 除第一个元素外,每个元素有且仅有一个直接前驱,除最后一个元素外,每个元素有且仅有一个直接后继。

1.2 线性表的基本操作

InitList(&L):初始化表。构造一个空的线性表L,分配内存空间

DestoryList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间

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

ListDelete(&L,i,&e):删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值

LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素

GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值

其他常用操作:

Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。

PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。

Empty(L):判空操作。若L为空表,则返回true,否则返回false。

注意:

  • 对数据的操作(记忆思路)------创销、增删改查。
  • C语言函数的定义------<返回值类型>函数名(<参数1类型>参数1,<参数2类型>参数2,...)
  • 什么时候要传入引用"&"------对参数的修改结果需要"带回来"。

总结:

1.3 顺序表的定义
  • 顺序表------用顺序存储的方式来实现的线性表
1.4 顺序表的实现
  • 顺序存储------把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中。元素之间的关系由存储单元的邻接关系来体现。
1.4.1 顺序表的实现------静态分配
1.4.2 顺序表的实现------动态分配

为了让顺序表的大小可变,可以利用动态分配的实现方式

  • malloc和free
  1. 动态申请空间用malloc,释放内存空间用free

  2. L.data=(ElemType*) malloc(sizeof(ElemType) *InitSize);

    int*arr=(int *)malloc(sizeof(int) *10)

  3. malloc(size) → 向系统申请 size 字节的连续内存,返回这块内存的起始地址( void* 类型),需要强转为指定类型

  4. 用完必须用free,否则内存泄漏

  5. malloc申请的总空间大小=单个元素的大小*元素的最大数量

  6. malloc、free函数的头文件:#include<stdlib.h>

练习:

C++ 复制代码
#include <stdio.h>   // 加了printf需要的头文件
#include <stdlib.h>  // malloc/free的头文件

#define InitSize 10  // 默认最大长度,初始化顺序表

// 动态顺序表结构体
typedef struct {
    int *data;      // 动态数组指针
    int MaxSize;    // 最大容量
    int length;     // 当前长度
} SeqList;

// ✅ 修正:C语言用指针传参,加了malloc空判断
void InitList(SeqList *L) {
    // 申请初始内存
    L->data = (int *)malloc(InitSize * sizeof(int));
    if (L->data == NULL) { // 必须判断!防止申请失败崩溃
        printf("初始化失败:内存不足!\n");
        exit(1);//程序异常退出
    }
    L->length = 0;
    L->MaxSize = InitSize;
    printf("顺序表初始化完成,初始容量:%d\n", L->MaxSize);
}

// ✅ 修正:指针传参+空判断+安全扩容
void IncreaseSize(SeqList *L, int len) {
    if (len <= 0) { // 防止无效扩容
        printf("扩容长度必须大于0!\n");
        return;
    }

    int *p = L->data; // 保存旧数组地址
    // 申请更大的内存
    int newSize = L->MaxSize + len;
    L->data = (int *)malloc(newSize * sizeof(int));
    
    if (L->data == NULL) { // 申请失败,恢复原指针,不崩溃
        printf("扩容失败:内存不足!\n");
        L->data = p; // 把原指针还回去,不丢数据
        return;
    }

    // 复制旧数据到新数组
    for (int i = 0; i < L->length; i++) {
        L->data[i] = p[i];
    }

    free(p); // 释放旧内存
    L->MaxSize = newSize; // 更新最大容量
    printf("顺序表扩容完成,新容量:%d\n", L->MaxSize);
}

// ✅ 新增:插入元素函数
bool ListInsert(SeqList *L, int pos, int val) {
    // 位置合法性判断(1~length+1)
    if (pos < 1 || pos > L->length + 1) {
        printf("插入位置不合法!\n");
        return false;
    }
    // 容量满了就扩容(这里自动扩5,也可以手动调)
    if (L->length >= L->MaxSize) {
        IncreaseSize(L, 5);
    }
    // 从后往前移动元素,给新元素腾位置
    for (int i = L->length; i >= pos; i--) {
        L->data[i] = L->data[i - 1];
    }
    L->data[pos - 1] = val; // 插入新元素
    L->length++;
    return true;
}

// ✅ 新增:打印顺序表函数(看运行结果)
void PrintList(SeqList *L) {
    printf("当前顺序表元素:");
    for (int i = 0; i < L->length; i++) {
        printf("%d ", L->data[i]);
    }
    printf("\n当前长度:%d,最大容量:%d\n", L->length, L->MaxSize);
}

int main() {
    SeqList L;          // 声明顺序表
    InitList(&L);       // 初始化(传指针)

    // 插入元素(补全原代码的注释)
    ListInsert(&L, 1, 10);
    ListInsert(&L, 2, 20);
    ListInsert(&L, 3, 30);
    PrintList(&L);

    // 手动扩容5
    IncreaseSize(&L, 5);
    PrintList(&L);

    // 继续插入,触发自动扩容
    for (int i = 4; i <= 15; i++) {
        ListInsert(&L, i, i * 10);
    }
    PrintList(&L);

    // ✅ 规范:手动释放内存
    free(L.data);
    L.data = NULL;
    return 0;
}

效果图:

1.5 顺序表的特点
  • 随机访问,即可以在O(1)时间内找到第i个元素,顺序表当中的各个元素的存放位置是连续存放的,只要知道第一个元素的存放地址,那么后面这些元素的存放地址就可以马上算出来。比如数组,只要给一个数组的下标就可以直接找到后面的元素
  • 存储密度高,每个节点只存储数据元素本身,如果是链式存储,除了本身还要耗费一定的空间存储指针
  • 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高,数据要复制到新区域)
  • 插入、删除操作不方便,需要移动大量元素

总结:

1.6 顺序表的插入和删除
1.6.1 插入操作

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

注意点:

  • 代码中的int j=L.length。类似于int i等于零,一个从0往后加,一个从length往前减,逐步把前面的数移到后一个位置
1.6.2 插入操作的时间复杂度
1.6.3 删除操作
  • 我们在进行删除的操作时,把元素依次往前移动一位,先移动前面的元素,再移动后面的元素,而当我们进行插入操作时,需要把元素依次往后移动一位,先移动后面的再去移动前面的
  • int e=-1是为了给e一个无效初始值,方便判断删除是否成功,这是一个不可能出现在正常顺序表元素里的「标记值」(假设顺序表存的都是正整数),如果删除失败,函数不会修改 e , e 就还是 -1 ,你一眼就能看出来,如果删除成功,函数会把被删元素的值赋给e,就变成了被删的那个数(比如 3 )
  • 元素前移逻辑:从第i个位置后的元素依次向前覆盖,填补被删元素的空位
1.6.4 删除操作的时间复杂度

总结:

1.7 顺序表的查找

GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值

LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素

1.7.1 静态分配数组的按位查找

注意:

  • 第i个元素所对应的数组下标为i-1,位序从1开始,而数组下标从0开始

  • 如果想让代码健壮性更强,可以加一个判断i的值是否合法

    if (i < 1 || i > L->length + 1) {
    printf("插入位置不合法!\n");
    return false;
    }

1.7.2 动态分配数组的按位查找

注意:

  • data这个变量是个指针,指向顺序表中的第一个数据元素,也就是malloc函数申请的一整片的连续内存空间的起始地址
1.7.3 按位查找的时间复杂度
  • 时间复杂度O(1)的意思是:不管数据量n有多大,执行这个操作需要的时间都是固定的,不会随着n变大而变长。

  • 按位查找的时候:

    • 不需要从头遍历数组(那是O(n)的做法)

    • 只需要做一次简单的地址计算,然后直接访问对应内存位置

    • 不管顺序表里有10个元素还是10000个元素,这个步骤都只做1次,时间完全一样

1.7.4 动态分配数组的按值查找
  • 如果是结构类型如何查找,比较是否相等?
1.7.5 按值查找的时间复杂度

总结:

相关推荐
Jasmine_llq2 小时前
《B3954 [GESP202403 二级] 乘法问题》
算法·顺序输入输出算法·布尔标记算法·累乘算法·循环迭代算法·阈值判断算法·条件分支输出算法
Halo_tjn2 小时前
Java 抽象类 知识点
java·开发语言·算法
念恒123062 小时前
Linux权限
linux·c语言
我要成为嵌入式大佬2 小时前
学习linux的部分疑惑与解答(非专业)
学习
say_fall2 小时前
滑动窗口算法
数据结构·c++·算法
蚊子码农2 小时前
每日一题--关于转向的思考
c语言
落羽的落羽2 小时前
【算法札记】练习 | Week1
linux·服务器·c++·人工智能·python·算法·机器学习
踏着七彩祥云的小丑2 小时前
Python——排序
开发语言·python
qq_454245032 小时前
图数据标准化与智能去重框架:设计与实现解析
数据结构·架构·c#·图论