1.线性表介绍
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储
2.顺序表
顺序表其实非常的简单,顺序表其实就是数组
那么就引出来一个问题,有了数组以后为什么还需要顺序表这一个概念?
因为C语言的数组是有一些缺陷的,最大的缺陷在于没有支持C99之前数组是定长的,比如说一开始就要确定要存多少个数据,但是哪怕是变长数组也会面临一个问题,如果自身也不知道要存多少数据呢?
比如通讯录或者图书管理系统,不知道通讯录中到底会存多少个联系人或者图书管理系统会存多少的书籍信息,那这时候数据结构就把它单独再对其进行管理,这时候起了名字就叫顺序表, 简单来说就是管理数组存储数据的结构就叫做顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
但是顺序表有个要求: 存储的数据从0开始,依次存储
顺序表一般可以分为两种:
1. 静态顺序表:使用定长数组存储元素。(静态顺序表的缺陷就在于,开小了不够用,开大了浪费空间)
2. 动态顺序表:使用动态开辟的数组存储。
了解了顺序表的性质以及存储性质后,开始对顺序表进行实际操作深一步了解
顺序表的接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。
静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。
所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面实现动态顺序表。
typedef int SLDataType;
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; 指向动态开辟的数组
size_t size ; 有效数据个数
size_t capicity ; 容量空间的大小
}SeqList;
基本增删查改接口
顺序表初始化
void SeqListInit(SeqList* psl, size_t capacity);
检查空间,如果满了,进行增容
void CheckCapacity(SeqList* psl);
顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x);
顺序表尾删
void SeqListPopBack(SeqList* psl);
顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x);
顺序表头删
void SeqListPopFront(SeqList* psl);
顺序表查找
int SeqListFind(SeqList* psl, SLDataType x);
顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x);
顺序表删除pos位置的值
void SeqListErase(SeqList* psl, size_t pos);
顺序表销毁
void SeqListDestory(SeqList* psl);
顺序表打印
void SeqListPrint(SeqList* psl);
1.顺序表初始化以及检查空间是否需要扩容功能以及打印功能的实现
初始化
void SeqListInit(SeqList* psl, size_t capacity)
{
assert(psl);
psl->array = NULL;
psl->size = 0;
psl->capicity = 0;
将所有数值置为空指针和0
}
扩容
void CheckCapacity(SeqList* psl)
{
assert(psl);
if (psl->size == psl->capicity)检查空间是否足够,不够就扩容
{
创建一个新的变量,用于判定当前总容量是否是0,如果不是0则是初识过的
如果为0,则用4个该变量类型的空间,如果不为0则开辟原总容量的2倍
size_t newcapicity = psl->capicity == 0 ? 4 : (psl->capicity * 2);
SLDataType* tmp = realloc(psl->array, sizeof(SLDataType) * newcapicity);
if (tmp == NULL)
{
printf("CheckCapacity has been an error \n");如果开辟失败报错
exit(-1);开辟失败就是内存不够了,直接结束程序 没必要return 都开辟失败了程序就没必要走往下继续走了
}
else
{
psl->array = tmp;如果开辟成功则将指针置为开辟好的地址
psl->capicity = newcapicity;并且将目前容纳最大总量更新
}
}
}
顺序表打印
void SeqListPrint(const SeqList* psl)
{
int i = 0;
for (i = 0; i < psl->size; i++)
{
printf("%d ", psl->array[i]);
}
printf("\n");
}
2. 顺序表尾插与顺序表尾删
在一个顺序表中进行尾插和尾删是非常简单的,并且它们的时间复杂度都是O(1)
只需要注意 指向空间大小是否足够容纳一个新的数据?
void SeqListPushBack(SeqList* psl, SLDataType x)//尾插
{
思想:
1.尾插之前要检查空间是否足够,不够就扩容
2.尾插的位置下标就是size
assert(psl);
CheckCapacity(psl);
//进行尾插
psl->array[psl->size] = x;
psl->size++;
}
// 顺序表尾删
void SeqListPopBack(SeqList* psl)
{
思想:直接把size--掉就好,不要把数据给置成个人想得默认值然后再-- 因为某一个默认值可能该尾部位置就是那个值呢?
psl->size--;
}
3.顺序表头插与头删
相对比与尾插头插则会考虑的多一些
1.头插时指向空间大小是否足够容纳一个新的数据?
2.头插时数据应该依次向后移动
3.数据移动时应从后向前依次移动,否则会出现数据覆盖的情况
头插错误情况: 如果数据是从前向后依次移动则会出现这种情况
正确运行:如果数据是从后向前依次移动则会是这样的情况
如果是头删则适用于从前向后依次移动的顺序
以下是实现代码
顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x)
{
思想:
1.检查空间是否足够
2.插入一个数据其它数据需要往后移动
3.往后移动的数据应从后往前开始移动 否则会产生数据覆盖
assert(psl);
CheckCapacity(psl);
//建立一个下标,指向下标为psl->size的位置
SLDataType end = psl->size;
while (end > 0)
{
psl->array[end] = psl->array[end-1];
end--;
}
psl->array[0] = x;
psl->size++;
}
顺序表头删
void SeqListPopFront(SeqList* psl)
{
思想:
1.这个就无须检查空间是否足够,因为是删除
2.删除一个数据需要后面往前覆盖并且size--就可以
assert(psl);
SLDataType end = 0;
if (psl->size > 0)//如果当前数量大于0的话才进行删除 否则不进行
{
while (end < psl->size)
{
psl->array[end] = psl->array[end + 1];
end++;
}
psl->size--;
}
}
4.顺序表在pos位置插入x与顺序表删除pos位置的值功能实现
这个思想则和头插头删的思想差不多
1.插入时指向空间大小是否足够容纳一个新的数据?
2.插入时数据应该依次向后移动
3.数据移动时应从后向前依次移动,否则会出现数据覆盖的情况
在pos位置插入x数值正确的运行方式
如果是在pos位置删除X数值,则和头删一样,从前向后依次移动覆盖
以下为是实现代码
顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
思想:
1.检查空间是否需要扩容
2.插入位置后面的数据要往后挪
assert(psl);
CheckCapacity(psl);
size_t end = psl->size;从末尾开始移动
while (end > pos)
{
这里有个坑要注意,size_t是无符号整形
如果设置成end>=pos 极端情况下 pos=0时,当end=0进入一次循环再-1后会变为极大的数字
解决方法:
1.一开始把pos和end设定为int类型 或者强转
2.size_t类型 end一开始就设定为size位置
方法二的思想:
比如一个数组有5个元素,那么最后一个元素下标为4,程序中我们设置size为最后一个元素的后一位
也就是size=5
那么end从下标5开始,把下标4元素移动到5来,再-1,这样只要把循环条件设置为end>pos;
当end=1时,就可以把下标0的元素移动到当前位置,再次-1时,end=0 循环条件不达成
这样可以很好避免极端情况pos=0的出现;
psl->array[end] = psl->array[end - 1];
end--;
}
psl->array[pos] = x;
psl->size++;
同时这个插入也可以用于头插尾插
}
顺序表删除pos位置的值
void SeqListErase(SeqList* psl, size_t pos)
{
思想:
1.删除后后面的值要往前挪
2.防止size为0的时候越界
assert(psl);
if (psl->size > 0)
{
while (pos < psl->size-1)
{
psl->array[pos] = psl->array[pos + 1];
pos++;
}
psl->size--;
return;
}
else
return;
}
同样的,这个功能同样适用于头插头删,尾插尾删,那么试一下把头插头删以及尾插尾删用这个功能实现,这样可以减少代码长度,使得这个功能有更多的复用性
顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x)
{
SeqListInsert(psl, 0, x);
}
顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x)
{
SeqListInsert(psl, psl->size, x);
}
顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
思想:
1.检查空间是否需要扩容
2.插入位置后面的数据要往后挪
assert(psl);
CheckCapacity(psl);
size_t end = psl->size;从末尾开始移动
while (end > pos)
{
这里注意一点是,unsgined int 是没有负数概念,所以如果设置为end>=pos
end和pos都=0时,循环依然可以进入,end再-1 就会变成无限大的数,程序则会陷入死循环
解决方法有两种:
1.强转或者一开始设定成int类型
2.end从size空位开始,比如数组6个成员,第六个元素下标为5,此时end从下标6开始把前面元素
往自身空位上拿,再依次减减,等它到下标1时,就可以拿到下标0元素移过来,当end=0时,end>pos
条件不成立,循环终止,pos=0,就在下标0的位置进行插入
psl->array[end] = psl->array[end - 1];
end--;
}
psl->array[pos] = x;
psl->size++;
}
剩下就是顺序表查找/销毁/修改数据
就直接贴代码了哈~
顺序表查找
int SeqListFind(SeqList* psl, SLDataType x)
{
思想:
找到这个数并且返回下标 没找到返回-1
时间复杂度为O(N)
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (psl->array[i] == x)
{
return i;
}
}
return -1;
}
顺序表销毁
void SeqListDestory(SeqList* psl)
{
free(psl->array);
psl->array = NULL;
psl->capicity = psl->size = 0;
}
修改数据
void SeqListModify(SeqList* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos < psl->size);
psl->array[pos] = x;
}