1. 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表,链表,栈,队列,字符串……
线性表在逻辑上是线性结构,也就是说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
所以,线性表指的是具有部分相同特性的一类数据结构的集合。例如:水果分为苹果,香蕉,葡萄,西瓜等。
2. 顺序表的概念及结构
顺序表是用一段物理地址连续的存储单元一次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可分为:
- 静态顺序表:使用定长数组存储元素。(数组长度不可改变)
数组长度不可改变,所以我们只能在创建时就确定数组的长度,但是我们并不清楚将会使用多大的空间。因此,空间给少了就不够用,空间给多了会造成空间浪费。 - 动态顺序表:使用动态开辟的数组存储。(数组长度可改变)
使用动态顺序表,可以避免空间不够的问题,且能减少空间的浪费。
3. 顺序表的接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
- 新建三个文件
建立三个文件,使代码模块化,有助于理解。
// 创建三个文件
// SeqList.h 放顺序表的结构,声明顺序表的接口
// SeqList.c 实现顺序表的接口
// test.c 测试文件
- 声明顺序表的结构及接口
顺序表的基本接口:增删查改
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
// 按需申请空间
typedef int SLDataType;
typedef struct Seqlist
{
SLDataType* array; // 动态开辟的数组,可改变长度
size_t size; // 有效数据个数
size_t capacity; // 数组空间容量
}Seqlist;
// 基本增删查改接口
//
// 顺序表初始化
void SeqlistInit(Seqlist* psl);
// 检查空间,如果满了,进行增容
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);
// 顺序表修改
void SeqlistModify(Seqlist* psl, size_t pos, SLDataType x);
-
实现顺序表的接口
- 初始化
void SeqlistInit(Seqlist* psl)
{
assert(psl); // 判断psl是否为空,防止后面对空指针进行访问
psl->array = NULL;
psl->capacity = psl->size = 0;
}
2. 销毁
void SeqlistDestory(Seqlist* psl)
{
assert(psl); // 防止psl为空
if (psl->array)
free(psl->array); // 如果array不为空,则里面有内容,将其释放,为空则跳过
psl->array = NULL; // 防止产生野指针
psl->capacity = psl->size = 0;
}
3. 打印
void SeqlistPrint(Seqlist* psl)
{
assert(psl);
assert(psl->size); // 数组中没有有效数据则不打印
for (size_t i = 0; i < psl->size; i++)
{
printf("%d ", psl->array[i]); // 通过循环,遍历整个数组中所有元素
}
printf("\n");
}
顺序表简单部分的初始化,销毁和打印写完,接下来就要正式实现顺序表的增删查改的功能,要开始加速了
5. 检查空间
我们要将数据插入到顺序表中,那么顺序表的空间一定不能是满的,所以我们每次插入数据时都要判断空间是否满了,如果满了就要扩容,由于我们之后在多个地方会用到检查空间,那么我们就将其封装成一个函数,方便后续使用。
所以我们可以这样写:
void CheckCapacity(Seqlist* psl)
{
assert(psl); // 防止传空指针,使得后面对空指针进行解引用
if (psl->capacity == psl->size) // 有效数据个数和空间大小相等,说明空间满了,要扩容
{
// 使用三目操作符,如果capacity为0,则用4赋值,否则让capacity * 2,空间翻倍
int Newcapacity = (0 == psl->capacity) ? 4 : 2 * psl->capacity;
// 创建一个临时指针,获得扩容的空间的首地址
// 为什么要创建新的指针,不能直接让 array 接收扩容的空间的首地址呢?
// 如果扩容成功,那么直接用 array 接收没有问题,但是扩容失败就会有严重后果
// 扩容失败,realloc会返回NULL,array 指向的地址就变为了NULL,之前的数据就无法找到
// 这样就会导致数据丢失,而让一个临时指针接收,就能避免数据丢失
SLDataType* tmp = (SLDataType*)realloc(psl->array, sizeof(int) * Newcapacity);
// 判断是否扩容成功,失败则结束程序,并警告
if (NULL == tmp)
{
perror("realloc fail");
exit(-1);
}
// 到了这里就扩容成功了,让array指向扩容的空间
psl->array = tmp;
psl->capacity = Newcapacity; // 扩容成功后,更改空间大小
}
}
6. 尾插
尾插比头插更简单,我们先了解尾插。
void SeqlistPushBack(Seqlist* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl); // 判断空间是否满了
psl->array[psl->size] = x; // 直接将 x 插入到顺序表的最后面
psl->size++; // 然后让size++,有效数据增加
//也可以将这两行代码合并
// psl->array[psl->size++] = x;
}
7. 头插
void SeqlistPushFront(Seqlist* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
// 让顺序表中的数据全部往后移,将第一个位置空出来
for (size_t i = psl->size; i > 0; --i)
{
psl->array[i] = psl->array[i - 1];
}
psl->array[0] = x; // 让第一个位置赋值为x
psl->size++;
}
接下来我们可以测试一下我们的头插和尾插:
void Test()
{
Seqlist sl;
SeqlistInit(&sl);
// 先尾插4个数据
SeqlistPushBack(&sl, 1);
SeqlistPushBack(&sl, 2);
SeqlistPushBack(&sl, 3);
SeqlistPushBack(&sl, 4);
SeqlistPrint(&sl); // 将这四个数据打印出来
// 再头插2个数据
SeqlistPushFront(&sl, 5);
SeqlistPushFront(&sl, 6);
SeqlistPrint(&sl); // 将全部数据打印出来
SeqlistDestory(&sl);
}
输出结果:
8. 尾删
void SeqlistPopBack(Seqlist* psl)
{
assert(psl);
assert(psl->size); // 判断是否有有效数据,有才能删除
psl->size--; // 只需要这样就行,简单吧
}
9. 头删
void SeqlistPopFront(Seqlist* psl)
{
assert(psl);
assert(psl->size);
// 将除了第一个数据以外的数据往前移动一位即可
for (size_t i = 1; i < psl->size; i++)
psl->array[i - 1] = psl->array[i];
psl->size--;
}
我们来测试一下删除代码:
void Test()
{
Seqlist sl;
SeqlistInit(&sl);
// 先尾插4个数据
SeqlistPushBack(&sl, 1);
SeqlistPushBack(&sl, 2);
SeqlistPushBack(&sl, 3);
SeqlistPushBack(&sl, 4);
SeqlistPrint(&sl); // 将这四个数据打印出来
// 再头插2个数据
SeqlistPushFront(&sl, 5);
SeqlistPushFront(&sl, 6);
SeqlistPrint(&sl); // 将全部数据打印出来
// 尾删2个数据
SeqlistPopBack(&sl);
SeqlistPopBack(&sl);
SeqlistPrint(&sl); // 打印
// 头删2个数据
SeqlistPopFront(&sl);
SeqlistPopFront(&sl);
SeqlistPrint(&sl); // 打印
SeqlistDestory(&sl);
}
输出结果:
10. 查找
int SeqlistFind(Seqlist* psl, SLDataType x)
{
assert(psl);
for (size_t i = 0; i < psl->size; i++)
{
if (psl->array[i] == x)
return i; // 如果相等,则返回对应的下标位置
}
return -1; // 执行到这里表明没有找到,返回-1
}
11. 修改
修改需要在查找的基础上进行,要先输入要修改的数据的下标位置
void SeqlistModify(Seqlist* psl, size_t pos, SLDataType x)
{
assert(psl);
// 保证pos是指向有效空间
assert(pos >= 0 && pos < psl->size);
psl->array[pos] = x;
}
我们可以对插入和删除进行改进----在pos位置插入和删除
12. 在pos位置插入x
void SeqlistInsert(Seqlist* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos >= 0 && pos < psl->size);
CheckCapacity(psl);
// 将pos处及后面的所有数据都往后移动一位,将pos这个位置空出来
// 与头插很接近,就是将0改为了pos,如果pos是0,就是头插
for (size_t i = psl->size; i > pos; i--)
{
psl->array[i] = psl->array[i - 1];
}
psl->array[pos] = x;
psl->size++;
}
13. 删除pos位置的值
void SeqlistErase(Seqlist* psl, size_t pos)
{
assert(psl);
assert(pos >= 0 && pos < psl->size);
// 将pos后面的数据往前移动一位
// 同样与头删很相近,pos为0就是头删
for (size_t i = pos + 1; i < psl->size; i++)
psl->array[i - 1] = psl->array[i];
psl->size--;
}
我们测试一下这几个功能:
void Test()
{
Seqlist sl;
SeqlistInit(&sl);
for (int i = 0; i < 10; i++)
SeqlistPushBack(&sl, i);
SeqlistPrint(&sl);
int pos = SeqlistFind(&sl, 6);
SeqlistInsert(&sl, pos, 12); // 在pos处插入12
SeqlistPrint(&sl);
pos = SeqlistFind(&sl, 3);
SeqlistErase(&sl, pos); // 删除pos处数据
SeqlistPrint(&sl);
pos = SeqlistFind(&sl, 8);
SeqlistModify(&sl, pos, 16); // 修改pos处的数据
SeqlistPrint(&sl);
SeqlistDestory(&sl);
}
输出结果:
#4. 顺序表完整代码
SeqList.h
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
// 按需申请空间
typedef int SLDataType;
typedef struct Seqlist
{
SLDataType* array; // 动态开辟的数组,可改变长度
size_t size; // 有效数据个数
size_t capacity; // 数组空间容量
}Seqlist;
// 基本增删查改接口
//
// 顺序表初始化
void SeqlistInit(Seqlist* psl);
// 检查空间,如果满了,进行增容
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);
// 顺序表修改
void SeqlistModify(Seqlist* psl, size_t pos, SLDataType x);
SeqList.c
#include "SeqList.h"
#define _CRT_SECURE_NO_WARNINGS 1
void SeqlistInit(Seqlist* psl)
{
assert(psl); // 判断psl是否为空,防止后面对空指针进行访问
psl->array = NULL;
psl->capacity = psl->size = 0;
}
void CheckCapacity(Seqlist* psl)
{
assert(psl); // 防止传空指针,使得后面对空指针进行解引用
if (psl->capacity == psl->size) // 有效数据个数和空间大小相等,说明空间满了,要扩容
{
// 使用三目操作符,如果capacity为0,则用4赋值,否则让capacity * 2,空间翻倍
int Newcapacity = (0 == psl->capacity) ? 4 : 2 * psl->capacity;
// 创建一个临时指针,获得扩容的空间的首地址
// 为什么要创建新的指针,不能直接让 array 接收扩容的空间的首地址呢?
// 如果扩容成功,那么直接用 array 接收没有问题,但是扩容失败就会有严重后果
// 扩容失败,realloc会返回NULL,array 指向的地址就变为了NULL,之前的数据就无法找到
// 这样就会导致数据丢失,而让一个临时指针接收,就能避免数据丢失
SLDataType* tmp = (SLDataType*)realloc(psl->array, sizeof(int) * Newcapacity);
// 判断是否扩容成功,失败则结束程序,并警告
if (NULL == tmp)
{
perror("realloc fail");
exit(-1);
}
// 到了这里就扩容成功了,让array指向扩容的空间
psl->array = tmp;
psl->capacity = Newcapacity; // 扩容成功后,更改空间大小
}
}
void SeqlistPushBack(Seqlist* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl); // 判断空间是否满了
psl->array[psl->size] = x; // 直接将 x 插入到顺序表的最后面
psl->size++; // 然后让size++,有效数据增加
//也可以将这两行代码合并
// psl->array[psl->size++] = x;
}
void SeqlistPopBack(Seqlist* psl)
{
assert(psl);
assert(psl->size); // 判断是否有有效数据,有才能删除
psl->size--; // 只需要这样就行,简单吧
}
void SeqlistPushFront(Seqlist* psl, SLDataType x)
{
assert(psl);
CheckCapacity(psl);
// 让顺序表中的数据全部往后移,将第一个位置空出来
for (size_t i = psl->size; i > 0; --i)
{
psl->array[i] = psl->array[i - 1];
}
psl->array[0] = x; // 让第一个位置赋值为x
psl->size++;
}
void SeqlistPopFront(Seqlist* psl)
{
assert(psl);
assert(psl->size);
// 将除了第一个数据以外的数据往前移动一位即可
for (size_t i = 1; i < psl->size; i++)
psl->array[i - 1] = psl->array[i];
psl->size--;
}
int SeqlistFind(Seqlist* psl, SLDataType x)
{
assert(psl);
for (size_t i = 0; i < psl->size; i++)
{
if (psl->array[i] == x)
return i; // 如果相等,则返回对应的下标位置
}
return -1; // 执行到这里表明没有找到,返回-1
}
void SeqlistInsert(Seqlist* psl, size_t pos, SLDataType x)
{
assert(psl);
assert(pos >= 0 && pos < psl->size);
CheckCapacity(psl);
// 将pos处及后面的所有数据都往后移动一位,将pos这个位置空出来
// 与头插很接近,就是将0改为了pos,如果pos是0,就是头插
for (size_t i = psl->size; i > pos; i--)
{
psl->array[i] = psl->array[i - 1];
}
psl->array[pos] = x;
psl->size++;
}
void SeqlistErase(Seqlist* psl, size_t pos)
{
assert(psl);
assert(pos >= 0 && pos < psl->size);
// 将pos后面的数据往前移动一位
// 同样与头删很相近,pos为0就是头删
for (size_t i = pos + 1; i < psl->size; i++)
psl->array[i - 1] = psl->array[i];
psl->size--;
}
void SeqlistDestory(Seqlist* psl)
{
assert(psl); // 防止psl为空
if (psl->array)
free(psl->array); // 如果array不为空,则里面有内容,将其释放,为空则跳过
psl->array = NULL; // 防止产生野指针
psl->capacity = psl->size = 0;
}
void SeqlistPrint(Seqlist* psl)
{
assert(psl);
assert(psl->size); // 数组中没有有效数据则不打印
for (size_t i = 0; i < psl->size; i++)
{
printf("%d ", psl->array[i]); // 通过循环,遍历整个数组中所有元素
}
printf("\n");
}
void SeqlistModify(Seqlist* psl, size_t pos, SLDataType x)
{
assert(psl);
// 保证pos是指向有效空间
assert(pos >= 0 && pos < psl->size);
psl->array[pos] = x;
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SeqList.h"
void Test()
{
Seqlist sl;
SeqlistInit(&sl);
for (int i = 0; i < 10; i++)
SeqlistPushBack(&sl, i);
SeqlistPrint(&sl);
int pos = SeqlistFind(&sl, 6);
SeqlistInsert(&sl, pos, 12); // 在pos处插入12
SeqlistPrint(&sl);
pos = SeqlistFind(&sl, 3);
SeqlistErase(&sl, pos); // 删除pos处数据
SeqlistPrint(&sl);
pos = SeqlistFind(&sl, 8);
SeqlistModify(&sl, pos, 16); // 修改pos处的数据
SeqlistPrint(&sl);
SeqlistDestory(&sl);
}
int main()
{
Test();
return 0;
}
5. 顺序表的问题及思考
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间,会有不小消耗
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:如何解决以上问题呢?
6. 总结
顺序表是一种线性表,在逻辑结构上是线性的,在物理结构上同样也是线性的,也就是说顺序表会使用一块连续的空间进行数据的存储,基本上以数组形式表示。
顺序表中数据是连续存储,我们就可以使用下标来进行访问,访问元素更迅速。
在顺序表功能实现中,头插头删,pos位置插入删除比较复杂,会对其他数据产生影响,需要关注操作细节。
顺序表优点是访问便捷,但是缺点有很多,就在那几个问题中,这些缺点导致需要频繁中间插入删除数据时效率较慢。
感谢观看,希望能给予支持!!!