1. 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就是说是连续的一条直线。
但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2. 顺序表
2.1 概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储元素(一般不采用,固定长度也就意味着空间浪费或不足)
- 动态顺序表:使用动态开辟的数组存储
3. 顺序表的代码实现(重点)
静态顺序表只适用于确定知道需要存多少数据的场景。
静态顺序表的定长数组长度固定,空间开多了浪费,开少了不够用。
所以现实中基本都是使用动态顺序表,根据需要来动态的分配空间大小
3.1 创建顺序表(结构体)
#include <stdio.h>
typedef int DataType;
// 顺序表
typedef struct SeqList
{
// 整型数据
DataType* a;
// 数据个数
int size;
// 顺序表空间大小
int capacity;
}SL;
3.2 初始化顺序表 SLInit
void SLInit(SL* psl)
{
assert(psl);
psl->a = NULL;
psl->capacity = 0;
psl->size = 0;
}
3.3 检查容量大小 CheckCapacity
若容量不够则进行扩容,若容量为0则进行初始化
void CheckCapacity(SL* psl)
{
assert(psl);
if (psl->size == psl->capacity)
{
// 进行扩容或者初始化
// 新容量 若capcity为0 则初始化为4 ; 若不为零 扩容到2倍
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
DataType* tmp =(DataType*)realloc(psl->a,newCapacity * sizeof(DataType));
// 判断扩容是否成功
if (tmp == NULL)
{
// 扩容失败
perror("CheckCapaity");
return;
// eixt(-1);
}
// 将扩容后的空间重新由psl->a维护
psl->a = tmp;
// 容量改为新容量
psl->capacity = newCapacity;
}
}
3.4 尾插尾删接口 SLPushBack / SLPopBack
// 尾插
void SLPushBack(SL* psl,DataType x)
{
assert(psl);
CheckCapacity(psl);
// 在下标为size的位置插入元素
psl->a[psl->size] = x;
psl->size++;
}
// 尾删
void SLPopBack(SL* psl)
{
assert(psl);
// 个数直接减减 ,顺序表是通过size来访问元素的
psl->size--;
}
3.5 头插头删接口 SLPushFront / SLPopFront
// 头插
void SLPushFront(SL* psl,DataType x)
{
assert(psl);
int end = 0;
// 将所有元素向后移动一位
for (end = psl->size; end > 0; end--)
{
psl->a[end] = psl->a[end - 1];
}
// 头部插入x
psl->a[0] = x;
// size++
psl->size++;
}
// 头删
void SLPopFront(SL* psl)
{
assert(psl);
int begin = 0;
for (begin = 0; begin <= psl->size - 2; begin++)
{
// 将后面的元素各自往前覆盖即可 从前往后
psl->a[begin] = psl->a[begin + 1];
}
// size 元素个数减1
psl->size--;
}
3.7 测试上述接口 TestSeqList1
在学习数据结构的过程中,要学会边编译边调试,尽早发现问题
上述接口成功实现
3.8 打印元素 SLPrint
// 打印
void SLPrint(SL* psl)
{
assert(psl);
int begin = 0;
// 一个for循环遍历数组即可
for (begin = 0; begin < psl->size; begin++)
{
printf("%d ", psl->a[begin]);
}
printf("\n");
}
3.9 任意位置插入/删除接口 SLInsert / SLErase
// 顺序表在pos位置插入x
void SLInsert(SL* psl, size_t pos, DataType x)
{
assert(psl);
assert(pos <= psl->size);
// 初始化顺序表大小或扩容
CheckCapacity(psl);
int end =psl->size;
// 从pos开始往后移动 从后往前
for (end; end > pos; end--)
{
psl->a[end] = psl->a[end-1];
}
// pos处插入x
psl->a[pos] = x;
psl->size++;
}
// 顺序表删除pos位置的值
void SLErase(SL* psl, size_t pos)
{
assert(psl);
assert(pos < psl->size);
size_t k = pos;
// 从pos开始往前覆盖 从前往后
for (k; k < psl->size; k++)
{
psl->a[k] = psl->a[k + 1];
}
psl->size--;
}
3.10 测试上述接口 TestSeqList2
3.11 一些细节上的问题
3.12 优化 头插/尾插/头删/尾删 接口
在这之前,我们已经实现了在任意位置处实现插入/删除操作
那么头插相当于在下标为0的位置插入、尾插相当于在下标为size位置处插入
头删、尾删也同理
浅浅测试一下:
跟之前测试结果一样!那么到这里,顺序表的代码实现部分就结束啦 (●’◡’●)
4. 顺序表相关OJ题
4.1 移除元素
这时候可以采用双指针遍历同一个数组的策略
int removeElement(int* nums, int numsSize, int val){
int src = 0,dst = 0;
while(src < numsSize)
{
if(nums[src] == val)
{
src++;
}
else
{
nums[dst]=nums[src];
det++;
src++;
}
}
return dst;
}
4.2 删除重复元素
同样采用的也是双指针遍历同一个数组的策略
大体思路一致,只是去重 也就意味着相同往后遍历,不同覆盖
int removeDuplicates(int* nums, int numsSize){
int src = 0,dst = 0;
while(src<numsSize)
{
if(nums[src] == nums[dst])
{
src++;
}
else
{
//在dst+1的位置赋值src
nums[dst+1]=nums[src];
src++;
dst++;
}
}
return dst+1;
}
4.3 合并有序序列
5. 顺序表的缺陷
缺陷:
- 中间/头部的插入/删除(需要移动后面的数据),时间复杂度为O(N)
- 增容可能需要申请新空间,拷贝数据,释放旧空间,会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:如何解决以上问题呢?可以采用链表的形式来解决