文章目录
一、线性表
1. 什么是线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列; 线性表是一种在实际中广泛使用的数据结构,常见的线性表有:顺序表、链表、栈、队列、字符串…
2. 线性表的结构
线性表在逻辑上是线性结构,也就是说是连续的一条直线;但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储,下面的顺序表就是以数组结构存储。
二、顺序表
1. 什么是顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删查改。
简单来说,顺序表就是数组,只是要求数组里面的元素必须连续存储而已。
2. 顺序表的分类
顺序表一般分为两类:静态顺序表和动态顺序表。
静态顺序表:采用定长数组来存储元素。
typedef int SSeqList;
//静态顺序表
typedef struct SSeqList
{
SSLDataType data[100];
int size;//实际存储元素个数
}SSeqList;
动态顺序表:使用动态开辟的数组来存储元素。
typedef int DSeqList;
//动态顺序表
typedef struct DSeqList
{
SSLDataType* data;
int size;
int capacity; //容量
}DSeqList;
两种顺序表的对比:相较于动态顺序表,静态顺序表存在很大的缺陷,那就是空间问题:当我们数据量很大时给定的空间可能不够用,但我们数据量比较小时,给定的空间又可能过大,造成空间浪费,即静态顺序表只适用于确定需要存多少数据的场景;所以现实中基本都是使用动态顺序表,根据需要动态地分配空间大小,而静态顺序表很少使用,下面我们用C语言来模拟实现一个动态的顺序表。
三、动态顺序表的实现
1. 结构定义
typedef int DSLDataType; //定义数据类型
#define INIT_LEN 4 //动态数组初始长度
//动态顺序表
typedef struct DSeqList
{
SSLDataType* data;
int size;
int capacity; //容量
}DSeqList;
2. 初始化顺序表
void InitDSeqList(DSeqList* ps)
{
assert(ps);
ps->data = (DSLDataType*)malloc(sizeof(DSLDataType) * INIT_LEN);
if (ps->data == NULL)
{
perror("InitDSeqList");
return;
}
ps->size = 0;
ps->capacity = INIT_LEN;
}
在初始化函数中,我们把size和capacity都置为相应大小,并且为data指针动态开辟一块空间,用于存储数据。
3. 检查容量,并扩容
在检查容量的函数中,当我们结构体中的size和capacity相等时,我们就扩容,在扩容时我们要注意不要直接用data指针来接收realloc函数的返回值,避免扩容失败导致data指针找不到之前管理的空间,从而造成内存泄漏。
关于初始容量和每次扩增倍数的问题,这里把初始容量设定为4,然后把扩增倍数设定为2,即我们的顺序表每次扩容两倍。
void CheckCapacity(DSeqList* ps)
{
if (ps->size == ps->capacity)
{
DSLDataType* temp = (DSLDataType*)realloc(ps->data, sizeof(DSLDataType) * ps->capacity * 2);
if (temp == NULL)
{
perror("realloc");
return;
}
ps->data = temp;
ps->capacity *= 2;
}
}
4. 头插
在头部插入数据时,我们需要先将顺序表中的数据整体向后挪动一位,然后在顺序表的开头插入;在插入完成后记得要让size++。
void DSeqListPushFront(DSeqList* ps, DSLDataType x)
{
assert(ps);
CheckCapacity(ps);
int end = ps->size;
while (end)
{
ps->data[end] = ps->data[end - 1];
end--;
}
ps->data[0] = x;
ps->size++;
}
5. 尾插
void DSeqListPushBack(DSeqList* ps, DSLDataType x)
{
assert(ps);
CheckCapacity(ps);
ps->data[ps->size] = x;
ps->size++;
}
6. 在指定下标插入数据
在此函数中,我们需要先将pos及其之后的元素整体向后挪动一位,然后再在pos处插入数据。
void DSeqListInsert(DSeqList* ps, int pos, DSLDataType x)
{
assert(ps);
CheckCapacity(ps);
if (pos >= 0 && pos <= ps->size)
{
int end = ps->size;
while (end > pos)
{
ps->data[end] = ps->data[end - 1];
end--;
}
ps->data[pos] = x;
ps->size++;
}
else
{
printf("下标错误, 插入失败!\n");
exit(-1);
}
}
7. 头删
在头部删除数据,我们只需要将顺序表中的数据整体向前挪动一位,然后将size–即可。
void DSeqListPopFront(DSeqList* ps)
{
assert(ps);
if (ps->size == 0)
{
printf("顺序表为空,删除失败!\n");
return;
}
for (int i = 0; i < ps->size - 1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}
8. 尾删
删除尾部的数据很简单,我们只需要将size–即可,并不需要对其进行改动,因为我们下一次插入数据时会直接将原来空间中的数据覆盖掉。
void DSeqListPopBack(DSeqList* ps)
{
assert(ps);
if (ps->size == 0)
{
printf("顺序表为空,删除失败!\n");
return;
}
ps->size--;
}
9. 删除指定下标的数据
删除指定位置数据,我们需要将pos后面的数据整体向前挪动一位,然后让size–。
void DSeqListErase(DSeqList* ps, int pos)
{
assert(ps);
if (pos >= 0 && pos < ps->size)
{
for (int i = pos; i < ps->size - 1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}
else
{
printf("下标错误,删除失败!\n");
exit(-1);
}
}
10. 按值查找下标
当我们找到该元素时,我们返回元素的下标;当该元素不存在时,我们返回一个无意义的值。(如-1)
int DSeqListFind(DSeqList* ps, DSLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->data[i] == x)
{
return i;
}
}
return -1;
}
11. 按值删除
void DSeqListDelete(DSeqList* ps, DSLDataType x)
{
assert(ps);
int pos = DSeqListFind(ps, x);
if (pos != -1)
{
DSeqListErase(ps, pos);
}
else
{
printf("值不存在!\n");
}
}
12. 打印顺序表
void PrintDSeqList(DSeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->data[i]);
}
printf("\n");
}
13. 销毁顺序表
在销毁顺序表的时候我们一定要记得将前面动态开辟的空间释放掉,防止内存泄漏。
void DestroyDSeqList(DSeqList* ps)
{
assert(ps);
free(ps->data);
ps->data = NULL;
ps->capacity = 0;
ps->size = 0;
}
四、完整代码
1. SeqList.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int SSLDataType; //定义数据类型
typedef int DSLDataType; //定义数据类型
#define INIT_LEN 4 //动态数组初始长度
//静态顺序表
typedef struct SSeqList
{
SSLDataType data[100];
int size;//实际存储元素个数
}SSeqList;
//动态顺序表
typedef struct DSeqList
{
SSLDataType* data;
int size;
int capacity; //容量
}DSeqList;
//初始化顺序表
void InitDSeqList(DSeqList* ps);
//销毁顺序表
void DestroyDSeqList(DSeqList* ps);
//打印顺序表
void PrintDSeqList(DSeqList* ps);
//头插、头删、尾插、尾删
void DSeqListPushFront(DSeqList* ps, DSLDataType x);
void DSeqListPopFront(DSeqList* ps);
void DSeqListPushBack(DSeqList* ps, DSLDataType x);
void DSeqListPopBack(DSeqList* ps);
//按值查找, 返回下标
int DSeqListFind(DSeqList* ps, DSLDataType x);
//按位插入值x
void DSeqListInsert(DSeqList* ps, int pos, DSLDataType x);
//按位删除
void DSeqListErase(DSeqList* ps, int pos);
//按值删除
void DSeqListDelete(DSeqList* ps, DSLDataType x);
2. SeqList.c
#include"SeqList.h"
void InitDSeqList(DSeqList* ps)
{
assert(ps);
ps->data = (DSLDataType*)malloc(sizeof(DSLDataType) * INIT_LEN);
if (ps->data == NULL)
{
perror("InitDSeqList");
return;
}
ps->size = 0;
ps->capacity = INIT_LEN;
}
void DestroyDSeqList(DSeqList* ps)
{
assert(ps);
free(ps->data);
ps->data = NULL;
ps->capacity = 0;
ps->size = 0;
}
void PrintDSeqList(DSeqList* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->data[i]);
}
printf("\n");
}
//检查容量,如果满了就扩容
void CheckCapacity(DSeqList* ps)
{
if (ps->size == ps->capacity)
{
DSLDataType* temp = (DSLDataType*)realloc(ps->data, sizeof(DSLDataType) * ps->capacity * 2);
if (temp == NULL)
{
perror("realloc");
return;
}
ps->data = temp;
ps->capacity *= 2;
}
}
void DSeqListPushFront(DSeqList* ps, DSLDataType x)
{
assert(ps);
CheckCapacity(ps);
int end = ps->size;
while (end)
{
ps->data[end] = ps->data[end - 1];
end--;
}
ps->data[0] = x;
ps->size++;
}
void DSeqListPopFront(DSeqList* ps)
{
assert(ps);
if (ps->size == 0)
{
printf("顺序表为空,删除失败!\n");
return;
}
for (int i = 0; i < ps->size - 1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}
void DSeqListPushBack(DSeqList* ps, DSLDataType x)
{
assert(ps);
CheckCapacity(ps);
ps->data[ps->size] = x;
ps->size++;
}
void DSeqListPopBack(DSeqList* ps)
{
assert(ps);
if (ps->size == 0)
{
printf("顺序表为空,删除失败!\n");
return;
}
ps->size--;
}
int DSeqListFind(DSeqList* ps, DSLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->data[i] == x)
{
return i;
}
}
return -1;
}
void DSeqListInsert(DSeqList* ps, int pos, DSLDataType x)
{
assert(ps);
CheckCapacity(ps);
if (pos >= 0 && pos <= ps->size)
{
int end = ps->size;
while (end > pos)
{
ps->data[end] = ps->data[end - 1];
end--;
}
ps->data[pos] = x;
ps->size++;
}
else
{
printf("下标错误, 插入失败!\n");
exit(-1);
}
}
void DSeqListErase(DSeqList* ps, int pos)
{
assert(ps);
if (pos >= 0 && pos < ps->size)
{
for (int i = pos; i < ps->size - 1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}
else
{
printf("下标错误,删除失败!\n");
exit(-1);
}
}
void DSeqListDelete(DSeqList* ps, DSLDataType x)
{
assert(ps);
int pos = DSeqListFind(ps, x);
if (pos != -1)
{
DSeqListErase(ps, pos);
}
else
{
printf("值不存在!\n");
}
}
3. test.c
#include"SeqList.h"
void TestDSeqList1()
{
DSeqList S; //创建顺序表变量
InitDSeqList(&S);
DSeqListPushFront(&S, 1);
DSeqListPushFront(&S, 2);
DSeqListPushFront(&S, 3);
DSeqListPushFront(&S, 4);
DSeqListPushFront(&S, 5);
PrintDSeqList(&S);
DSeqListPopFront(&S);
DSeqListPopFront(&S);
DSeqListPopFront(&S);
PrintDSeqList(&S);
DSeqListPushBack(&S, 10);
DSeqListPushBack(&S, 20);
DSeqListPushBack(&S, 30);
PrintDSeqList(&S);
DSeqListPopBack(&S);
DSeqListPopBack(&S);
PrintDSeqList(&S);
DestroyDSeqList(&S);
}
void TestDSeqList2()
{
DSeqList S; //创建顺序表变量
InitDSeqList(&S);
DSeqListPushFront(&S, 1);
DSeqListPushFront(&S, 2);
DSeqListPushFront(&S, 3);
DSeqListPushFront(&S, 4);
DSeqListPushFront(&S, 5);
PrintDSeqList(&S);
DSeqListDelete(&S, 2);
DSeqListDelete(&S, 4);
PrintDSeqList(&S);
DSeqListInsert(&S, 1, 20);
DSeqListInsert(&S, 3, 40);
PrintDSeqList(&S);
DSeqListErase(&S, 0);
DSeqListErase(&S, 1);
DSeqListErase(&S, 2);
PrintDSeqList(&S);
DestroyDSeqList(&S);
}
int main()
{
//TestDSeqList1();
//TestDSeqList2();
return 0;
}
五、顺序表的缺陷
顺序表存在如下缺陷:
- 在头部/中间的插入与删除需要挪动数据,时间复杂度为O(N),效率低;
- 增容需要申请新空间,可能会拷贝数据,释放旧空间,会有不小的时间消耗;
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,如果我们再继续插入了5个数据,后面没有数据插入了,那么会浪费95个数据空间;
针对顺序表存在的这些缺陷,我们设计出了链表。