目录
1.前言
2.线性表
3.顺序表
3.1 概念及结构
3.2 接口实现
1.顺序表初始化
2.顺序表销毁
3.检查空间并扩容
4.顺序表尾插
5.顺序表头插
6.顺序表尾删
7.顺序表头删
8.顺序表查找
9.顺序表在pos位置插入x
10.顺序表删除pos位置的值
11.顺序表打印
4.数组相关面试题
1.移除元素
2.删除有序数组中的重复项
什么是数据结构?
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
在我们以后的项目中,数据结构是必不可少的,因为我们就是在跟数据打交道。而数据结构有许多种,没有一种数据结构能够满足各种场景,所以我们需要学习很多数据结构,彼此都有很多差异。然后当我们在开发中,有什么场景适合哪个数据结构,我们就从我们学习的数据机构中选一个出来用今天,我们将来学习一个数据结构:顺序表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,
线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表看上去和数组好像有些相似,但是数组当我们定义之后便无法动态改变其长度,而顺序表却能实现动态改变。但是顺序表要求我们要依次紧挨着存储元素,不能像数组一样跳跃间隔存放。
✏️顺序表结构设计
在开始之前,我们可以先创建 SeqList.h的头文件用于存放各个接口(函数)的声明以及各种全局需要的代码,再创建SeqList.c的源文件用于存放各个接口的具体实现,最后再创建一个test.c源文件用于写主函数(测试函数)
这样做能使得代码更加清晰易读
顺序表能够存放各种数据类型的数据,所以我们需要利用结构体
而我们可以在结构体中定义一个变量用于标记存储数据的个数
1.静态顺序表:使用定长数组存储元素
#define N 10 struct SeqList { int arr[N]; int size; };
顺序表我们是想存储各种类型的数据,像如上的写法直接定义int 这不就写死了嘛,所以我们可以利用重定义(便于修改)进行优化
#define N 10 typedef int SLDataType; struct SeqList { SLDataType arr[N]; int size;//存储数据的个数 };
2.动态顺序表:使用动态开辟内存存储
因为动态内存开辟是通过指针接收的,所以结构体中的数据结构定义为指针就能通过realloc函数灵活地动态开辟内存,改变长度
而因为动态改变,所以我们可以再定义一个变量来标记存储空间的大小
typedef int SLDataType; struct SeqList { SLDataType* a; int size; int capacity;//存储空间的大小 };
顺序表的结构基本实现后,当我们通过函数调用的时候,会发现结构体的名字太长了
比如作为参数调用函数时:void seqListInit(struct SeqList s1);
那么我们可以再次利用重定义简化
typedef int SLDataType; typedef struct SeqList { SLDataType* a; int size; //存储数据的个数 int capacity; //存储空间的大小 }SL;
以上就是我们顺序表的结构了,接下来我们开始实现顺序表的各种接口
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表
以上便是头文件中的部分代码,是顺序表具备的各种接口的声明,接下来我们便来一一实现
void SLInit(SL* psl)
{
assert(psl);
psl->a = NULL;
psl->capacity = psl->size = 0;
}
void SLDestory(SL* psl)
{
free(psl->a);
psl->a = NULL;
psl->capacity = psl->size = 0;
}
void CheckCapacity(SL* psl)
{
if (psl->capacity == psl->size)
{
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(psl->a, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
psl->a = tmp;
psl->capacity = newCapacity;
}
}
这个接口的实现有一些小技巧✔️
首先是利用capacity和size两个变量的大小作为判断条件 因为我们的容量每一次扩容后肯定是有空间可供添加,而我们的size正好是用于统计顺序表中的数据个数,每添加一个数据我们的size就+1,所以当size和capacity一样大也就意味着此时顺序表的空间满了
其次关于扩容大小的选择,从我们学过的动态内存开辟的知识可知,开辟内存也是需要付出一些代价,比如内存碎片化、影响运行效率...所以每次需要扩容时我们也应该选个合适的大小,而一般这种扩容在这建议是选择扩容2倍,因为比较合适
最后当我们选择每次扩容时,容量翻倍的话我们会发现一个问题:那在使用之前,容量是初始化为0的时候怎么办?所以这里利用了三目运算符来解决了这个问题。若容量为0(还未使用)--->先赋予大小为4的容量,若容量不为0(已使用过,需要扩容)--->扩容(*2)
尾插,也即是从顺序表的后边紧挨着添加数据
void SLPushback(SL* psl, SLDataType x)
{
CheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;
}
void SLPushFront(SL* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
//挪动数据
int end = psl->size - 1;
while (end >= 0)
{
psl->a[end + 1] = psl->a[end];
end--;
}
psl->a[0] = x;
psl->size++;
}
✨这里有一个小细节就是,当我们一串连续的数据需要往后挪动时,必须从后边(从后往前)的数据开始挪,否则会使得数据被覆盖(数据往前挪反之)
void SLPopBack(SL* psl)
{
assert(psl);
if (psl->size == 0)
return;
psl->size--;
}
顺序表尾删其实并不复杂,只需简单地将size-1即可,因为我们最终使用顺序表的数据或是打印,是直接遍历到size位置,所以这样这个被我们抛弃的数据也等同于被删除了。至于有的兄弟会想说那这个空间不就浪费掉了嘛,那顺便释放呗。这就又涉及到我们动态内存开辟的知识,利用动态内存开辟函数开辟的内存,你开辟多少之后只能整块释放,不能部分释放噢。
void SLPopFront(SL* psl)
{
int beg = 0;
while (beg < psl->size-1)
{
psl->a[beg] = psl->a[beg + 1];
beg++;
}
psl->size--;
}
int SLFind(SL* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
return i+1;
}
return -1;
}
遍历顺序表查找数据,若有则返回数据所在位置,若查找不到则返回-1
void SLInsert(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
size_t end = psl->size - 1;
//检查想插入的位置是否在已有的范围内
assert(pos <= psl->size);
CheckCapacity(psl);
while (end>=pos)
{
psl->a[end + 1] = psl->a[end];
end--;
}
psl->a[pos] = x;
psl->size++;
}
❓❓上面的代码是一个有bug的代码,有兄弟能看出来嘛
这段代码运行之后会陷入死循环,其实是无符号整型 size_t 的错
到这是不是会感觉似曾相识的bug呢✏️那你就应该想到和数据类型相关的知识点吧(若遗忘可翻找之前的博客复习一下)
size_t是无符号整型,而当我们选择pos为0,也即是在顺序表的头部插入的话,0再减1就会变成整型的最大值,然后就会一直循环下去✈️
✏️那?把end的数据类型改成int?这样仍然会有bug,因为pos是size_t的,所以当end和pos作比较的时候,由于两个数据的类型不同所以会发生隐式转换,仍然会将end转换为无符号数
✈️✈️✈️✈️✈️✈️
那干嘛还要这么麻烦?直接全改为int不就好了??
其实这样也行,但也不全行,因为在C语言中这些接口的参数若一定不为负,一般都定义为size_t(无符号)所以我们自己实现 的时候也尽量遵循约定✔️
而解决方法其实也很简单:强制类型转换
void SLInsert(SL* psl, size_t pos, SLDataType x)
{
assert(psl);
int end = psl->size - 1;
//检查想插入的位置是否在已有的范围内
assert(pos <= psl->size);
CheckCapacity(psl);
while (end>=(int)pos)//强制类型转换
{
psl->a[end + 1] = psl->a[end];
end--;
}
psl->a[pos] = x;
psl->size++;
}
void SLErase(SL* psl, size_t pos)
{
assert(psl);
assert(pos <= psl->size);
for (pos; (int)pos < psl->size - 1; pos++)
{
psl->a[pos] = psl->a[pos + 1];
}
psl->size--;
}
void SLPrint(SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
✏️✏️下面为大家分享分析2道LeetCode上面的题目
题目:
方法一
许多兄弟一拿起这道题就能想到思路:遍历数组,若遇到val则挪动元素,这也是博主第一时间想到的思路 ,但是这个算法的时间复杂度太高了
int removeElement(int* nums, int numsSize, int val) {
int beg = 0;
for (beg; beg < numsSize; beg++)
{
if (nums[beg] == val)
{
for (int i = beg; i < numsSize - 1; i++)
{
nums[i] = nums[i + 1];
}
numsSize--;
beg--;
}
}
return numsSize;
}
在这里注意每轮挪动删除之后beg记得-1,这样才能避免因为刚好val连续2个紧挨在一起,如果beg不-1,则会漏掉一个
方法二
我们可以创建一个临时数组,遍历nums,若元素和val不相同则放入临时数组中,到最后再将临时数组拷贝回去✔️
int removeElement(int* nums, int numsSize, int val) {
int tmp[100] = {0};
int src = 0;
int dst = 0;
for(int src;src
✏️其实程序优化到方法二已经算是挺好的了,但是还有没有更优化的方法呢?也就是时间复杂度和空间复杂度能再少一些?
有✨我们可以在方法二的基础上再加优化。方法二的思路就是:不是val就保留,是val就丢弃(不放入临时数组中) 那我们能否不另外开辟空间(空间复杂度为0) 在原数组上操作呢?可以
✈️✈️✈️✈️✈️✈️
方法三
方法三的核心就是利用双指针✔️✔️两个指针(src、dst)同时向后移动,若src不等于val 则在dst的位置放入src元素后再同时向后移一位,若src等于val 则dst不后移,src继续向后移
int removeElement(int* nums, int numsSize, int val) {
int dst = 0;
int src = 0;
while(src< numsSize)
{
if(nums[src] != val)
{
nums[dst] = nums[src];
dst++;
src++;
}
if(nums[src] == val)
{
src++;
}
}
return dst;
}
遍历挪动覆盖删除(beg记得-1)
int removeDuplicates(int* nums, int numsSize){
int beg = 1;
for(int beg = 1;beg
✨✨✨✨✨✨
方法二
双指针遍历
int removeDuplicates(int* nums, int numsSize){
int src = 0,dst = 0;
while(src