当前位置: 首页 > news >正文

解剖线性表

文章目录

  • 1.线性表
  • 2.顺序表
    • 2.1 概念与结构
    • 2.2 分类
      • 2.2.1 静态顺序表
      • 2.2.2 动态顺序表
      • 3.动态顺序表的实现
    • 3.1初始化
    • 3.2尾插
    • 3.3头插
    • 3.4尾删
    • 3.5头删
    • 3.6查找数据
    • 3.7指定位置前插入数据
    • 3.8指定位置删除数据
    • 3.9销毁
  • 4两道算法
    • 4.1移除元素
    • 4.2删除有序数组中的重复项

1.线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是⼀种在实际中广泛使 用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…线性表在逻辑上是线性结构,也就说是连续的⼀条直线。但是在物理结构上并不⼀定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。

2.顺序表

2.1 概念与结构

概念:顺序表是用⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采用数组存储。

在这里插入图片描述

顺序表和数组的区别?

顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口

在这里插入图片描述

我们在学习后续的数据结构的时候,需要大家具备的C语言知识包含(下面是我之前总结的博客链接,大家可以参考复习)

  • 一级指针、二级指针
  • 结构体指针
  • 动态内存管理

2.2 分类

2.2.1 静态顺序表

概念:使用定长数组存储元素

静态顺序表缺陷:空间给少了不够用,给多了造成空间浪费

  • Seq:是 “Sequence” 的缩写,意为 “序列、顺序”
  • List:意为 “列表”

我们以后就用SeqList来为顺序表命名。

我们后面实现顺序表的时候,会创建3个文件

  • SeqList.h用来声明头文件
  • SeqList.c用来实现顺序表操作函数
  • test.c用来测试这些函数

2.2.2 动态顺序表

typedef struct SeqList {SLDataType* arr;int size;//有效数据个数int capacity;//空间容量
}SL;

​ 动态顺序表的逻辑是,我们先创建一个指针变量a,后面我们使用动态内存分配函数(例如malloc)向操作系统申请一块空间,然后将动态内存分配函数返回的这块空间首元素的地址赋值给这个指针变量a,此时a就可以像数组名一样使用,我们就创建了一个动态数组,后面还可以通过例如realloc函数等,对这个数组进行增容操作。上面对顺序表类型的创建都是在SeqList.h文件中的。

3.动态顺序表的实现

3.1初始化

这时我们将来到test.c文件中,创建主函数入口以及测试函数。

#include"SeqList.h"
void SLTest()
{SL s1;}
int main()
{SLTest();return 0;
}

同时还要注意包含头文件SeqList.h,否则编译器识别不了SL,接下来我们创建顺序表S1
再在SeqList.c文件中实现初始化函数SLInit()

我们马上就会遇到一个问题,初始化时我们是传入顺序表的变量名、还是顺序表的地址?

大家记住:是否选择传址调用(或传引用调用),核心就在于是否需要让函数内部对参数的修改影响到函数外部的实参
很明显,我们初始化就是要直接改变实参的值,所以初始化函数要使用传址调用

后面我们会使用malloc还有断言等,所以要记得在SeqList.h中包含头文件

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

接下来在SeqList.c实现

#include"SeqList.h"
void SLInity(SL* ps)
{ps->arr = NULL;ps->size = ps->capacity = 0;
}

最后在test.c中传参

#include"SeqList.h"
void SLtest()
{SL s1;SLInity(&s1);
}
int main()
{SLtest();return 0;
}

3.2尾插

void SLPushBack();

接下来我们在SeqList.c实现尾插函数,并且要在头文件中声明这个函数方便后续使用。

首先,我们这个尾插函数一定会改变顺序表(传址调用),所以第一个参数就是SL类型的指针,另外我们还需要尾插的数据作为第二个参数,所以:

void SLPushBack(SL* pa ,SLDatatype x)

进入函数之后,后面我们将会多次使用顺序表里面的参数,会不断对这个顺序表的地址进行解引用,所以我们要防止pa是空指针

assert(pa);

接下来,我们要判断这个顺序表是否有空间给我们进行尾插,如果没有我们要进行扩容

if (pa->size == pa->capacity)
{}

这是会出现一个问题,扩容的话,我们应该增加多少容量呢?扩容少了我们就需要频繁扩容,扩容多了可能会造成空间的浪费

从概率学上讲,一次扩容两倍是最合适的,另外如果扩容的空间足够,realloc就直接返回原来空间首地址,如果空间不够,realloc会找到新空间,拷贝旧数据,释放旧空间,返回新空间首地址。

同时也要考虑到特殊情况,如果说这个顺序表容量是0,那对它*2之后还是0,所以在capacity是0的情况下,我们直接将容量的初始值改为4。

if (pa->size == pa->capacity)//判断是否存在尾插空间
{int newcapacity = pa->capacity == 0 ? 4 : 2 * pa->capacity;SLDatatype* tmp = (SLDatatype*)realloc(pa->arr, newcapacity * sizeof(SLDatatype));if (tmp == NULL){perror("realloc fail");exit(-1);}pa->arr = tmp;pa->capacity = newcapacity;
}

还要判断一下,realloc是否申请空间成功,最后将申请好的空间地址赋值给顺序表里边的arr和新的容量赋值给capacity

最后一步,将需要尾插的数据插入有效数据size的最后一个

pa->arr[pa->size++] = x;

最后给出尾插完整代码

void SLPushBack(SL* pa ,SLDatatype x)
{//防止后面对空指针解引用等操作,会产生未知行为assert(pa);if (pa->size == pa->capacity)//判断是否存在尾插空间{int newcapacity = pa->capacity == 0 ? 4 : 2 * pa->capacity;SLDatatype* tmp = (SLDatatype*)realloc(pa->arr, newcapacity * sizeof(SLDatatype));if (tmp == NULL){perror("realloc fail");exit(-1);}pa->arr = tmp;pa->capacity = newcapacity;}//走到这里说明有空间来尾插pa->arr[pa->size++] = x;
}

经过我们的测试,尾插成功。
<img src="blog_image/image-20250920101426107.png"

3.3头插

头插也需要空间足够才能插入数据,所以我们遇到的第一个问题数组可能就是容量不够的问题,我们在刚才的尾插中同样遇到了这个问题。

void SLCheckCapacity(SL* pa)
{if (pa->size == pa->capacity)//判断是否存在尾插空间{int newcapacity = pa->capacity == 0 ? 4 : 2 * pa->capacity;SLDatatype* tmp = (SLDatatype*)realloc(pa->arr, newcapacity * sizeof(SLDatatype));if (tmp == NULL){perror("realloc fail");exit(-1);}pa->arr = tmp;pa->capacity = newcapacity;}
}//这个函数包含了完整的容量检查以及扩容操作

后续对顺序表进行增删改查操作的时候,都有可能涉及到容量不够的问题,所以我们将这部分代码封装成函数,后续使用直接调用避免代码冗余。

头插的逻辑是将原有的数据向后移动一位,然后再给首元素插入你想要的数据。

void SLPushFront(SL* ps,SLDatatype x)
{assert(ps);SLCheckCapacity(ps);for (int i = ps->size; i > 0; i--){ps->arr[i] = ps->arr[i - 1];}ps->arr[0] = x;ps->size++;
}

别忘了要将有效数据size增加一位。

可以看到,头插我们使用了n次循环,所以头插的时间复杂度是O(n),之前尾插的时间复杂度是O(1)。

3.4尾删

尾删的操作非常简单

void PopBack(SL* ps)
{assert(ps && ps->size);ps->size--;
}
  • 首先断言要加上有效数据size不能让它为0,如果去掉 assert( ps->size); 这行代码,当 size0 时,执行 --ps->size; 会将 size 变为 -1 。这会导致后续与 size 相关的操作(比如访问 ps->arr[ps->size] 这样的元素)出现越界访问的问题 。
  • 其次,你可能会疑问仅仅只是把size--,那数据不还是没有删掉吗?当我们下次再使用顺序表时,新增的数据会将原来的尾部数据覆盖掉,还有我们后面使用打印函数SLPrint的时候,像打印函数 SLPrint 这类遍历顺序表的操作,是以 size 作为循环结束的条件(比如 for (int i = 0; i < ps->size; i++))。当 size 被减 1 后,SLPrint 会 “自然跳过原尾部元素”,只打印 [0, size - 1] 范围内的有效元素。
  • 顺序表的 “元素有效范围” 是 [0, size - 1],将 size 减 1 后,原本的 “最后一个元素” 会被 “逻辑上忽略”(后续新增元素会覆盖它,无需手动清空内存)。

3.5头删

void SLPopFront(SL* ps)
{assert(ps && ps->size);for (int i = 0; i < ps->size - 1; i++){ps->arr[i] = ps->arr[i + 1];}
}

在这里插入图片描述

为了方便我们后面的观察,我们将数组打印函数也写上SLPrint

void SLPrint(SL* ps)
{for (int i = 0; i < ps->size; i++){printf("%d\n", ps->arr[i]);}
}

给大家分享一下写代码过程中的两个小技巧

  • 你选中一行代码,按下 Ctrl + D,这行代码就会被复制,并且粘贴到当前行的下方
  • 函数前缀+Tab,可以快速补全这个函数

3.6查找数据

在指定位置之前插入数据,我们首先需要知道这个位置对应的下标所以我们要写一个SLFind函数,用来找到指定位置,并返回它的下标

void SLFind(SL* ps, SLDatatype x)
{for (int i = 0; i <= ps->size; i++){if (ps->arr[i] == x){return i;}}//返回无效下标return -1;
}

后续在使用的时候记得先进行判断返回的是不是有效下标。

3.7指定位置前插入数据

void SLInsert(SL* ps,int pos,SLDatatype x)
{assert(ps);assert(pos >= 0 && pos <= ps->size);SLCheckCapacity(ps);for (int i = ps->size; i > pos; i--){ps->arr[i] = ps->arr[i-1];}ps->arr[pos] = x;ps->size++;
}

我们使用的第二个assert,是为了确保pos在有效范围之内。

核心逻辑还是,将pos之后的数据整体向后挪动一位,再在pos处插入你想要的数据。

这样插入的时间复杂度是O(N).

3.8指定位置删除数据

void SLErease(SL* ps, int pos)
{assert(ps);assert(pos >= 0 && pos <= ps->size);for (int i = pos; i < ps->size; i++){ps->arr[i] = ps->arr[i + 1];}ps->size--;
}

将pos之后的数据整体向前挪动一位

3.9销毁

void SLDesTroy(SL* ps)
{assert(ps);free(ps->arr);ps->arr = NULL;ps->size = ps->capacity = 0;
}

4两道算法

4.1移除元素

27. 移除元素 - 力扣(LeetCode)

在这里插入图片描述

在这里插入图片描述

4.2删除有序数组中的重复项

26. 删除有序数组中的重复项 - 力扣(LeetCode)

在这里插入图片描述
在这里插入图片描述

http://www.dtcms.com/a/392911.html

相关文章:

  • 计算数学研究方向有哪些细分领域?
  • [xboard]08-Makefile逐行分析2
  • Clash 中 REJECT 的技术原理与解决方案 —— 以哔哩哔哩延迟问题为例
  • 庖丁解牛与专家思维:道家的“心手合一”训练法
  • matlab通过GUI实现点云的读取、自定义显示和保存
  • 工业现场实战:如何利用智能网关实现西门子PLC与库卡机器人的无缝连接
  • 【开题答辩全过程】以 Java程序设计课程作业数据分析为例,包含答辩的问题和答案
  • ubuntu配置cuda与torch
  • C语言:输出水仙花数
  • 进程的创建
  • 如何用Anaconda Navigator和命令行管理Python库?
  • 28 种 LLM 越狱攻击全景拆解(2025.9 版)从“AIM”到“Generation Exploitation”,一张防御地图看懂所有套路
  • 第14章 智能床位
  • 总结一下MySQL数据库服务器性能优化的几个维度
  • IP 打造财富新机遇
  • linux系统如何查看文件位置在数据盘还是系统盘
  • C#关键字 unchecked与checked
  • EasyClick JavaScript 字符串进阶
  • 小明打砖块-算法
  • 【Open3D】在Conda环境下安装Open3D | Anaconda | VSCode
  • AWS 的存储方案全对比:EBS、S3、EFS 用在哪?
  • 【实证分析】上市公司债务违约风险KMV模型及违约距离(2000-2023年)
  • 【牛客网】dd爱科学 最长非递减子序列 二分查找
  • vmware安装uos v20无法识别网卡
  • 力扣hot100 | 动态规划1 | 70. 爬楼梯、118. 杨辉三角、198. 打家劫舍、279. 完全平方数、322. 零钱兑换
  • 每天五分钟深度学习:softmax回归的交叉熵损失的前向传播
  • leetcode算法刷题的第四十天
  • 算法基础篇(3)高精度
  • Java Log
  • 最常见的MCP服务