【数据结构】——2.顺序表
目录
一、线性表概念
线性表(linear list):具有相同特征的数据元素的有限序列。
线性表在逻辑结构上时线性结构、是一条连续的线,但是物理结构上不一定是连续的。
常见的线性表:顺序表、链表、栈、队列、字符串等
二、顺序表概念及结构
1. 顺序表概念
顺序表(sequence list):用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般使用数组存储。在数组上完成增删查改
顺序表一般分为静态顺序表和动态顺序表
2. 顺序表结构
1.1 静态顺序表
使用定长数组存储元素,容量不够时提醒用户不能存储
#define NUMBER 1000
typedef int SLDataType;
typedef struct SeqList
{
SLDataType data[NUMBER]; //定长数组
size_t size; //有效数据个数
} SeqList;
1.2 动态顺序表
使用动态开辟数组的存储,容量不够时可以增容
typedef int SLDataType;
typedef struct SeqList
{
SLDataType *data; //指向动态开辟的数组
size_t size; //有效数据个数
size_t capacity; //容量空间的大小
}
三、顺序表的实现
1. 结构定义
现实中基本使用动态顺序表,所以我们对动态顺序表进行操作
typedef int SLDataType;
typedef struct SeqList
{
SLDataType *data; //指向动态开辟的数组
size_t size; //有效数据个数
size_t capacity; //容量空间的大小
}
2. 初始化
- 初始化时可以不分配空间,在插入时检查并分配空间,不够了就重新申请
void SeqListInit(SeqList* psl)
{
assert(psl != NULL);
psl->data = NULL;
psl->size = 0;
psl->capacity = 0;
}
3. 检查容量
- 在插入操作之前(头插、尾插、下标插入),都要进行容量检查,所以单独成为一个函数
- 检查顺序表是否满了,判断有效数据个数和最大容量是否相等
- 判断有效数据个数和最大容量相等时,还要判断空间是为0(未使用)时给空间一个初始值,不为0时让容量按指定值增长(这里是*2了)
- 使用
realloc
函数时,要注意返回值的判断和赋值
void CheckCapacity(SeqList* psl)
{
assert(psl != NULL);
if (psl->size == psl->capacity)
{
//初始值判断,从4开始,不够就*2
int newCapacity = psl->capacity == 0 ? 4 : 2 * psl->capacity;
//申请空间
SLDataType* temp = (SLDataType*)realloc(psl->data, newCapacity * sizeof(SLDataType));
if (temp == NULL)
{
perror("realloc fail\n");
exit(-1);
}
//将重新申请的空间赋值给data,并改变最大容量
psl->data = temp;
psl->capacity = newCapacity;
}
}
4. 插入数据
4.1 尾插
尾插:在线性表的末尾插入数据
- 插入之前检查容量
- 插入时直接将size位置的data赋值就行,size加一
void SeqListPushBack(SeqList* psl, SLDataType x)
{
assert(psl != NULL);
CheckCapacity(psl);
psl->data[psl->size] = x;
psl->size++;
}
4.2 头插
头插:在线性表头部插入数据
- 插入之前检查容量
- 插入前要移动数据,从后面开始移动
- 插入时将0位置的data赋值,size加一
void SeqListPushFront(SeqList* psl, SLDataType x)
{
assert(psl != NULL);
CheckCapacity(psl);
int end = psl->size - 1;
while (end >= 0)
{
psl->data[end + 1] = psl->data[end];
--end;
}
psl->data[0] = x;
psl->size++;
}
4.3 插入
在指定下标的位置插入数据
- 插入数据的下标位置一般声明为无符号整型
- 从后往前移动数字,将指定位置空出来,插入指定数据
- 检查下标不用检查负数,使用的是无符号整型,下标可以是size,插入到末尾
- 错误的移动方式:当从
size-1
开始移动时,每次使data[end+1] = data[end]
来完成移动,会出现错误
如果
pos
为0,则end>=0
时执行循环体,end=0
时执行循环体end--
,end
为无符号整型值,减一后为整型最大值,无限循环
若是将end
声明为int
型,end
与无符号pos
比较依然按照无符号来,所以是死循环
size_t end = psl->size - 1; //end从size-1开始移动
while (end >= pos) //end>=pos执行循环体
{
psl->data[end + 1] = psl->data[end];
--end;
}
- 正确的移动方式:使移动从
size
开始,每次使data[end] = data[end-1]
来完成移动,就不会出现错误
此时
pos
为0时,则end>0
时执行循环体,end--
后为0,循环停止,不会陷入死循环
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
assert(psl != NULL);
assert(pos <= (size_t)psl->size); //检查pos,可以等于size,就是尾插
CheckCapacity(psl);
size_t end = psl->size; //从size开始,end>pos时执行,pos为0时就不会出现死循环了
while (end > pos)
{
psl->data[end] = psl->data[end-1];
--end;
}
psl->data[pos] = x;
psl->size++;
}
5. 删除数据
5.1 尾删
尾删:删除末尾数据
- 删除之前要先判断size是否已经为0,若为0,则终止程序或不做处理(下面是不做处理的,终止程序可以用断言)
- 删除数据直接将size减一即可
void SeqListPopBack(SeqList* psl)
{
assert(psl != NULL);
//不做处理,直接return
if (psl->size == 0)
{
return;
}
//assert(psl->size != 0); 断言处理,终止程序
psl->size--;
}
5.2 头删
头删:删除第一个数据
- 删除之前要先判断size是否已经为0,若为0,则终止程序或不做处理(下面是不做处理的,终止程序可以用断言)
- 从下标为1的数据从前往后向前移动一位即可,再size减一
void SeqListPopFront(SeqList* psl)
{
assert(psl != NULL);
if (psl->size == 0)
{
return;
}
int begin = 0;
while (begin < psl->size-1)
{
psl->data[begin] = psl->data[begin + 1];
++begin;
}
psl->size--;
}
5.3 删除
删除指定下标位置的数据
- 从指定下标开始,从前往后将数据向前移动一格
- 下标不能为size,必须是有效的数据
void SeqListErase(SeqList* psl, size_t pos)
{
assert(psl != NULL);
assert(pos < (size_t)psl->size);
size_t begin = pos;
while (begin < psl->size - 1)
{
psl->data[begin] = psl->data[begin + 1];
begin++;
}
psl->size--;
}
6. 查找数据
- 顺序查找指定值,返回下标,若是没有就返回-1
- 不建议使用二分查找,排序会影响效率
int SeqListFind(SeqList* psl, SLDataType x)
{
assert(psl != NULL);
int i = 0;
for (i = 0; i < psl->size; i++)
{
if (psl->data[i] == x)
{
return i;
}
}
return -1;
}
7. 修改数据
- 修改就直接将值赋值给该位置即可
//修改 Modify
void SeqListModify(SeqList* psl, size_t pos, SLDataType x)
{
assert(psl != NULL);
assert(pos < (size_t)psl->size);
psl->data[pos] = x;
}
8. 销毁顺序表
- 顺序表销毁的是data申请的空间,断言时只需断言顺序表是否存在即可,NULL被free时不报错
- 释放完空间时,记得将data置为NULL,容量和有效个数置为0
void SeqListDestory(SeqList* psl)
{
assert(psl != NULL);
free(psl->data);
psl->data = NULL;
psl->size = 0;
psl->capacity = 0;
}
四、顺序表的相关问题
1. 效率问题
- 中间、头部、的插入时间复杂度为 O ( n ) O(n) O(n),效率低
- 增容需要申请空间,拷贝数据,释放空间。消耗很大
- 若是数据少,申请的空间多,造成浪费
2. 缩容问题
顺序表删除数据时不建议缩容
realloc
函数可以实现缩容,所以不缩容与函数无关- 缩容造成的时间浪费非常大,若是在扩容边界上反复插入、删除,就会导致空间反复扩容缩容,严重影响效率
- 不缩容是以空间换取时间,以现在的科技水平,空间成本低,空间上的要求并没有时间上的要求高
3. 插入和删除问题
3.1 头插和尾插
头插法和尾插法可以调用插入函数实现
- 头插法就是下标为0的插入
//头插法
void SeqListPushFront(SeqList* psl, SLDataType x)
{
SeqListInsert(psl, 0, x);
}
- 尾插法就是下标为size的插入,尾插要传参
size
,所以要检查psl
//尾插法
void SeqListPushBack(SeqList* psl, SLDataType x)
{
assert(psl != NULL);
SeqListInsert(psl, psl->size, x);
}
3.2 头删和尾删
头删和尾删可以调用删除函数实现
- 头删就是下标为0的删除
//头删
void SeqListPopFront(SeqList* psl)
{
SeqListErase(psl, 0);
}
- 尾删就是下标为
size-1
的删除,尾删要传参size
,所以要检查psl
//尾删
void SeqListPopBack(SeqList* psl)
{
assert(psl != NULL);
SeqListErase(psl, psl->size-1);
}
五、顺序表的特点
1. 顺序表的优点
- 尾插尾删的效率很高
- 随机访问,用下标访问
- CPU高速缓存命中率更高
CPU访问数据:
- CPU不直接访问内存,而是先访问高速缓存
- 没有命中时,先加载到缓存,局部性原理(加载数据时会连带周围的数据)
2. 顺序表的缺点
- 头部和中部插入删除数据的效率低
- 扩容时的性能消耗和空间浪费
六、完整代码
代码保存在gitee中:点击完整代码参考