【本节目标】
1、线性表
- 定义:线性表(liner list)是n个具有相同特性的数据元素的有限序列。
- 线性表是一种在实际中被广泛使用的数据结构,讨论的是数据的存储方式。当我们存储了数据之后,我们要对数据进行管理,而主要的管理操作是增删查改。
- 常见的线性表有:顺序表、链表、栈、队列、字符串……本节重点介绍顺序表和链表。
2、顺序表
2.1 顺序表的概念及结构
- 顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构。
- 一般采用数组存储,在数组上完成数据的增删查改。
2.2 顺序表的分类
顺序表一般可分为:
- 静态顺序表:使用定长数组存储数据元素。
- 缺点:开少了不够用,开多了浪费!
- 适用于:确定知道要存多少数据的场景。
- 动态顺序表:使用动态开辟的数组存储数据元素。
- 特点:按需申请!
2.3 顺序表的接口实现
2.3.1 顺序表的类型定义
typedef int SLDataType;
#define INIT_CAPACITY 4
//顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; //指向动态开辟的数组
int size; //有效数据的个数
int capacity; //当前数组的容量大小
}SL;
2.3.2 基本增删查改接口
提前说明:
涉及到指针的时候,我们最好养成先进行assert断言的好习惯,这样做的好处是当指针为NULL或者assert处出现错误的时候,assert断言会定位到哪一行,方便我们快速检查出错误。
因此,在下面的操作接口中,我们都先进行assert断言。
- 顺序表的初始化
//顺序表的初始化 void SLInit(SL* ps) { assert(ps); ps->array = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY); ps->size = 0; ps->capacity = INIT_CAPACITY; }
这段代码看似没什么问题,但是不够严谨!!!也是我们很容易忽略的一个点,那就是要对malloc的返回值进行检查!
malloc:如果开辟成功,则返回一个指向开辟好空间的指针,
如果开辟失败,则返回一个NULL指针,
因此,malloc的返回值一定要做检查!
完善后的代码:
//顺序表的初始化 void SLInit(SL* ps) { assert(ps); ps->array = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY); if (ps->array == NULL) { perror("malloc fail"); return 1; } ps->size = 0; ps->capacity = INIT_CAPACITY; }
-
顺序表的扩容:检查空间,如果满了,进行扩容
当size = capacity的时候,就要进行扩容。
这里有两个值得我们注意的地方:
- 记住realloc的参数:第二个的参数是要扩充的字节大小,而不是个数!
- 最不用原指针接收好,万一开辟失败,就把原指针ps搞为空指针了。
- 合适的做法是:先将realloc函数的返回值放在一个临时指针中,并且判断返回值的有效性,这里同样要进行返回值检查,如果不为NULL,再把临时指针赋给原指针。
完善后的代码:
//顺序表的扩容 void SLCheckCapacity(SL* ps) { assert(ps); SLDataType* tmp = (SLDataType*)realloc(ps->array, sizeof(SLDataType)*ps->capacity*2); if(tmp == NULL) { perror("realloc fail"); return 1; } ps->array = tmp; ps->capacity *= 2; }
-
顺序表的销毁
//顺序表的销毁 void SLDetroy(SL* ps) { assert(ps); free(ps->array); ps->array = NULL; ps->size = 0; ps->capacity = 0; }
- 顺序表的尾插
插入都要记得检查:表是否已满!
//顺序表的尾插 void SLPushBack(SL *ps, SLDataType x) { assert(ps); SLCheckCapacity(ps); //ps->array[ps->size] = x; //ps->size++; ps->array[ps->size++] = x; }
- 顺序表的尾删
删除都要记得检查:表是否为空!
//顺序表的尾删 void SLPopBack(SL* ps) { assert(ps); //暴力检查 assert(ps->size > 0); //温柔的检查 //if (ps->size == 0) //return 1; ps->size--; }
尾插/尾删N个数据的时间复杂度:O(N)
- 顺序表的头插
//顺序表的头插 void SLPushFront(SL* ps, SLDataType x) { assert(ps); SLCheckCapacity(ps); int end = ps->size-1; while (end >= 0) { ps->array[end + 1] = ps->array[end]; end--; } ps->array[0] = x; ps->size++; }
- 顺序表的头删
//顺序表的头删 void SLPopFront(SL* ps) { assert(ps); int begin = 1; while (begin < ps->size) { ps->array[begin - 1] = ps->array[begin]; begin++; } ps->size--; }
这样写忽略了一个问题,那就是当数组一个元素也没有的时候,ps->size再减减就成了-1。
所以,当数组为空的时候就无需再删。
完善后的代码:
//顺序表的头删 void SLPopFront(SL* ps) { assert(ps); assert(ps->size > 0); int begin = 1; while (begin < ps->size) { ps->array[begin - 1] = ps->array[begin]; begin++; } ps->size--; }
头插/头删N个数据的时间复杂度:O(N^2)
- 顺序表的查找
//顺序表的查找 int SLFind(SL* ps, SLDataType x) { assert(ps); for (int i = 0; i < ps->size; i++) { if (ps->array[i] == x) { return i; } } return -1;//表示没找到 }
- 顺序表在pos位置插入x
注意检查:
- 位置pos是否在下标为0 - size之间,在0处插入相当于头插,在size处插入相当于尾插。
- 表空间是否已满。
//顺序表在pos位置插入x void SLInsert(SL* ps, int pos, SLDataType x) { assert(ps); assert(pos >= 0 && pos <= ps->size); SLCheckCapacity(ps); int end = ps->size-1; while (end >= pos) { ps->array[end + 1] = ps->array[end]; end--; } ps->array[pos] = x; ps->size++; }
- 顺序表删除pos位置的值
和插入一样,要检查pos的合理性,但与插入不同的是,删除位置不能为size处,而插入可以,因为下标为size处为空。
//顺序表删除pos位置的值 void SLErase(SL* ps, int pos) { assert(ps); assert(pos >= 0 && pos < ps->size); int begin = pos + 1; while (begin < ps->size) { ps->array[begin - 1] = ps->array[begin]; begin++; } ps->size--; }
- 顺序表的打印
//顺序表的打印 void SLPrint(SL* ps) { assert(ps); for (int i = 0; i < ps->size; i++) { printf("%d ", ps->array[i]); } printf("\n"); }
2.4 顺序表的不足
- 若在顺序表的插入中间或头部插入、删除数据,需要挪动大量的数据,时间复杂度为O(N),比较费时。
- 增容需要申请空间,如果是异地扩容,需要拷贝数据到新空间,释放旧空间,这就会有不少消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如:当前容量大小为100,满了以后增容到200,而我们只需要再插入5个数据,那么就浪费了95个数据。
思考:为了解决上面这些问题,有没有更好的方法来存储数据呢?
且听下回分解!
写到最后的话:小编也是进修阶段,如果你发现有错误或对你造成困扰的地方,欢迎指正和交流讨论,我们一起共同进步!如果你觉得还不错或对你有帮助的话,别忘了给小编一个鼓励的小心心哟~~~