认识数据结构
数据结构是计算机存储、组织数据的方式
- 能够存储数据(如顺序表、链表等结构)
- 存储的数据能够⽅便查找
- 通过数据结构,能够有效将数据组织和管理在⼀起。按照我们的⽅式任意对数据进⾏增删改查等操作。 最基础的数据结构:数组。
线性表
线性表是n个具有相同特性的数据元素的有限序列。常⻅的线性表:顺序表、链表、栈、队列、字符串...线性表在逻辑上是线性结构,但是在物理结构上并不⼀定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储
顺序表
概念: 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
底层结构是数组,对数组的封装,实现了常⽤的增删改查等接⼝
顺序表一般可以分为:
1.静态顺序表
给定的数组长度
缺点:
- 若空间不够,会导致后续的数据保存失败(数据丢失是严重的技术事故!)
- 若空间太大,会导致空间大大的浪费
2.动态顺序表
接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。
所以现实中基本都是使用动态顺序表,根据需要动态的分配空间 大小,所以下面我们实现动态顺序表。
什么是接口函数?
接口函数是在数据进行操作时进行调用的函数,通过调用数据结构的接口帮助你完成一系列操作
基本增删查改接口
void SLInit(SL* ps);//名字简写 初始化
void SLPushBack(SL* ps, SLDatatype x); //尾插
void SLPopBack(SL* ps); //尾删
void SLPushFront(SL* ps, SLDatatype x); //头插
void SLPopFront(SL* ps); //头删
void SLPrit(SL* ps); //打印
void SLDestory(SL* ps); //释放
void SLCheckCapacity(SL* ps); //检查容量-扩容
void SLInsert(SL* ps, int pos, SLDatatype x);//任意位置插入
void SLErase(SL* ps, int pos); //任意位置删除
int SLFind(SL* ps, SLDatatype x); //查找
void SLModify(SL* ps,int pos, SLDatatype x); //修改
1、创建![](https://i-blog.csdnimg.cn/blog_migrate/ce7faf20493957b5a11196f9356e733c.png)
#pragma once
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
//#define N 200
typedef int SLDatatype;
//静态顺序表
//struct SeqList
//{
// SLDatatype a[N];//静态顺序表,无法改变,N太小,不够用,太大,浪费空间
// int size; //有序数组的个数
//};
//改成动态开辟
typedef struct SeqList
{
SLDatatype* a;//指向动态数组指针
int size; //数据个数
int capacity; //容量-空间大小
}SL;
为了方便后续修改数据类型,我们可以使用 typedef 定义一个新的数据类型,这里我们把它取名为 SLDataType(顺序表数据类型)。
我们为了让定义的结构体使用时更方便,我们同样可以使用 typedef 将其定义为 SL
(此时 SL = struct SeqList,后续在使用时可以更加方便)。
2、初始化
首先引入我们自己创建的头文件 #include "SeqList.h" ,我们就可以开始动手实现顺序表初始化函数了。
首先通过 psl 指向 array,将数组为空。因为是初始化,所以将有效数据个数和数组时即能存数据的空间容量一并置为0。
#include "SeqList.h"
//初始化函数
void SLInit(SL* ps)
{
ps->a= NULL;
ps->size = ps->capacity= 0;
}
3、扩容 - 检查
void SLCheckCapacity(SL* ps1)
{
assert(ps1);
if (ps1->size == ps1->capacity)
{
SLDatatype* tmp = (SLDatatype*)realloc(ps1->a, sizeof(SLDatatype) * ps1->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps1->a = tmp;
ps1->capacity *= 2;
}
}
4、尾插
尾插就是在最后一个元素的后面插入一个元素
尾插有三种情况:
第一种情况是顺序表压根就没有空间。
第二种情况就是我们创建的 capacity 空间满了。
第三种情况是空间足够,直接插入数据即可。
void SLPushBack(SL* ps1, SLDatatype x)//尾插
{
assert(ps1);
SLCheckCapacity(ps1);
ps1->a[ps1->size] = x;
ps1->size++;
//ps1->a[ps1->size++] = x;
}
5、打印
void SLPrint(SL* ps1)
{
assert(ps1);
for(int i = 0;i < ps1->size;i++)
{
printf("%d ", ps1->a[i]);
}
printf("\n");
}
6、销毁
因为是动态开辟的,所以如果空间不用我们就需要销毁掉。如果不销毁会存在内存泄漏的风险,所以与之对应的我们写一个销毁的接口函数。
void SLDestroy(SL* ps1)//释放
{
assert(ps1);
free(ps1->a);
ps1->a = NULL;//局部变量 看后面有没有访问 有访问就要 有些情况不用置空,但是这个要
//这里的a不置空就是野指针
ps1->capacity = 0;
ps1->size = 0;
}
7、尾删
即删除最后一个元素,大部分人所想也许是把最后一个元素置为0或者-1,这是可行的,但如果最后一个数就是0呢?
其实我们这里只需要元素数量减去一个就好了,即size--,这样我们就无法访问最后一个元素了,它便是无效的数据。
void SLPopBack(SL* ps1)//尾删
{
assert(ps1);
//assert(ps1->size > 0);
ps1->a[ps1->size - 1] = 0;
//ps1->size--;
SLErase(ps1,ps1->size-1 );
}
这里有可能会出现如果内存中一个元素都没有了,size有可能减到-1的位置上,这便是越界了,但size是我们用来统计元素数量的,不可能小于0的,所以这里我们需要断言一下。
越界:
越界是不一定报错的,系统对越界的检查是一种设岗抽查。
如同你酒驾了,但没有被交警抓住,但它依然是错误的,所以我们应该避免这种情况。
8、头插
顺序表要求数据是连续存储的,且必须是从头开始存储。所以,对于顺序表而言如果要实现头插,就需要把数据往后挪动。不能从前往后挪,如果从前往后挪就挪就会把后面的数据覆盖掉。
思路:首先创建一个 end 变量用来指向要移动的数据,因为指向的是数据的下标,所以是 size 要减 1 。随后进入 while 循环,如果 end >= 0 说明还没有移动完,就会进入循环。循环体内利用下标,进行向后移动操作,移动结束后再 end-- ,进行下一个数据的向后移动。挪动数据成功后,就可以插入了。此时顺序表第一个位置就被腾出来了,就可以在下标0位置插入欲插入的数据 x 了。最后记得 size++ 。
void SLPushFront(SL* ps1, SLDatatype x)//头插
{
assert(ps1);
//SLCheckCapacity(ps1);
//
挪动数据
//int end = ps1->size - 1;
//while (end >= 0)
//{
// ps1->a[end + 1] = ps1->a[end];
// end--;
//}
//ps1->a[0] = x;
//ps1->size++;
SLInsert(ps1, 0, x);
}
9、头删
思路和头插类似,依次向前挪动,然后数量-1即size--
void SLPopFront(SL* ps1)//头删
{
assert(ps1);
//assert(ps1->size > 0);
int start = 0;
while (start < ps1->size - 1)
{
ps1->a[start] = ps1->a[start + 1];
start++;
}
//
//int start = 1;
//while (start < ps1->size)
//{
// ps1->a[start - 1] = ps1->a[start];
// start++;
//}
//ps1->size--;
SLErase(ps1, 0);
}
10、插入任意位置
顺序表要求数据从第一个开始放并且数据是连续存储的,所以我们就要注意下指定的下标位置 pos 的位置了!有些位置并不可以,所以我们需要进行一些检查。
代码思路:
首先添加 pos 位置的限定条件,限定 pos >= 0 并且 pos <= psl->size 从而保证 pos 合法。然后,因为是插入所以免不了要检查增容,直接调用之前写好的检查增容的函数即可。检查完后就可以开始移动了,和头插差不多,我们创建一个变量 end 记录最后一个的下标(psl->size-1),并通过它来指向要移动的数据。最后进入 while 循环,以 end >= pos 作为条件。移动完后,x 的位置就腾出来了,再把 x 插入进去,最后再 size++,就完成了。
11、删除任意位置
删除指定位置的数据,我们仍然要限制 pos 的位置。限制条件部分和 SeqListInsert 不同的是,因为 psl->size 这个位置没有效数据,所以删除的位置不能是 psl->size!
void SLErase(SL* ps1, int pos)//移除
{
assert(ps1);
assert(0 <= pos && pos < ps1->size);
int start = pos + 1;
while (start < ps1->size)
{
ps1->a[start - 1] = ps1->a[start];
++start;
}
ps1->size--;
}
任意位置删除的函数可以替代头删和尾删
12、查找
int SLFind(SL* ps1, SLDatatype x)//查找//找到下标,没有找到返回-1
{
assert(ps1);
for (int i = 0;i < ps1->size;i++)
{
if (ps1->a[i] == x)
{
return i;//返回位置
}
}
return -1;
}
13、修改
void SLModify(SL* ps1, int pos, SLDatatype x)//修改
{
assert(ps1);
assert(0 <= pos && pos <= ps1->size);
ps1->a[pos] = x;
}