概念及结构
概念:
顺序表是一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下,采用数组存储。
结构:
-
静态顺序表:使用定长数组存储元素。
静态顺序表只适用确定需要多少数据的场景。
-
动态顺序表:使用动态开辟的数组存储。
动态顺序表根据需要动态的分配空间大小开辟空间。(常用)
接口实现 (顺序表的动态存储)
我们把函数的声明、实现和测试分别写在三个文件。
分别是:SeqList.h / SeqList.c / test.c。
函数的声明
SeqList.h:
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
//定义初始容量
#define INIT_CAPACITY 4
//重定义类型名称,方便修改
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* a; //指向动态开辟的数组
size_t size; //有效数据个数
size_t capacity; //数组容量
}SL;
//增删查改
//顺序表初始化
void SeqListInit(SL* ps);
//顺序表销毁
void SeqListDestroy(SL* ps);
//顺序表尾插
void SeqListPushBack(SL* ps, SLDataType x);
//顺序表打印
void SeqListPrint(SL* ps);
//扩容
void CheckCapacity(SL* ps);
//顺序表尾删
void SeqListPopBack(SL* ps);
//顺序表头插
void SeqListPushFront(SL* ps, SLDataType x);
//顺序表头删
void SeqListPopFront(SL* ps);
//顺序表查找
int SeqListFind(SL* ps, SLDataType x);
//顺序表在pos位置插入x
void SeqListInsert(SL* ps, size_t pos, SLDataType x);
//顺序表删除pos位置
void SeqListErase(SL* ps, size_t pos);
函数的实现
SeqList.c:
#include "SeqList.h"
//顺序表初始化
void SeqListInit(SL* ps)
{
//断言:ps如果为NULL,程序直接报错
assert(ps);
//给数组开辟初始空间
ps->a = (SLDataType*)malloc(sizeof(ps->capacity) * INIT_CAPACITY);
if (NULL == ps->a) //判断是否开辟成功
{
perror("Intit::malloc"); //开辟失败,在屏幕输出错误,下一步直接结束程序
return;
}
//开辟成功,把容量调整为初始状态,有效数据调整为0
ps->capacity = INIT_CAPACITY;
ps->size = 0;
}
//顺序表销毁
void SeqListDestroy(SL* ps)
{
assert(ps);
//释放所开辟的空间,保持好习惯,把指针置空
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
//扩容
void CheckCapacity(SL* ps)
{
assert(ps);
//判断容量是否足够,如果容量和有效数据一样,则需要再开辟空间
if (ps->capacity == ps->size)
{
//正常情况,新开辟的空间是原来开辟空间的2倍
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (NULL == tmp)
{
perror("CheckCapacoty::realloc");
return;
}
//将新开辟的空间的指针交给ps->a这个指针来维护
ps->a = tmp;
//容量也需要乘2
ps->capacity *= 2;
}
}
//顺序表尾插
void SeqListPushBack(SL* ps, SLDataType x)
{
assert(ps);
//检查是否扩容
CheckCapacity(ps);
//直接在尾部插入数据
ps->a[ps->size] = x;
ps->size++;
}
void SeqListPrint(SL* ps)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
void SeqListPopBack(SL* ps)
{
assert(ps);
//判断顺序表是否为空,如果为空返回。当然在判断的时候也可以使用断言,如果顺序表为空直接指出错误
if (ps->size == 0)
{
return;
}
//尾删是直接把尾部要删除的数据空间还给操作系统
ps->size--;
}
//顺序表头插(效率低)
void SeqListPushFront(SL* ps, SLDataType x)
{
assert(ps);
//第一步还是检查数组容量够不够
CheckCapacity(ps);
//把目前所有数据向后移动一位。
int i = 0;
for (i = ps->size - 1; i >= 0; i--)
{
ps->a[i + 1] = ps->a[i];
}
//完成上一步,直接在在头部插入数据
ps->a[0] = x;
ps->size++;
}
//顺序表头删
void SeqListPopFront(SL* ps)
{
assert(ps);
//检查顺序表是否为空
//暴力检查
assert(ps->size);
//温柔检查
//if (ps->size == 0)
//{
// return;
//}
//所有数据向前覆盖一步
int i = 0;
for (i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
//顺序表查找
//在使用查找函数时,需要对-1进行判断
int SeqListFind(SL* ps, SLDataType x)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
//查找到,直接返回下标
return i;
}
}
//查找不到返回-1
return -1;
}
//顺序表在pos位置插入x
void SeqListInsert(SL* ps, size_t pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
CheckCapacity(ps);
int i = 0;
for (i = ps->size - 1; i >= pos; i--)
{
ps->a[i + 1] = ps->a[i];
}
ps->a[pos] = x;
ps->size++;
}
//顺序表删除pos
void SeqListErase(SL* ps, size_t pos)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
int i = 0;
for (i = pos ; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
注意:我们在写代码的同时,一定要经常性的测试,容易发现一些错误。并且要多写几组测试,因为我们在写接口的时候,大多只会注意它的主体。就如同头删……,我们需要检查顺序表是否为空,有时还要检查是不是有野指针等情况。
测试
test.c:
#include "SeqList.h"
void TestSeqList()
{
SL s;
//顺序表初始化。
SeqListInit(&s);
//尾插+打印
SeqListPushBack(&s, 1);
SeqListPrint(&s);
SeqListPushBack(&s, 2);
SeqListPrint(&s);
SeqListPushBack(&s, 3);
SeqListPrint(&s);
SeqListPushBack(&s, 4);
SeqListPrint(&s);
//尾删+打印
SeqListPopBack(&s);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
//头插+打印
SeqListPushFront(&s, 6);
SeqListPrint(&s);
SeqListPushFront(&s, 7);
SeqListPrint(&s);
SeqListPushFront(&s, 8);
SeqListPrint(&s);
//头删+打印
SeqListPopFront(&s);
SeqListPrint(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
//在pos位置插入9+打印
SeqListInsert(&s, 1, 9);
SeqListPrint(&s);
SeqListInsert(&s, 1, 10);
SeqListPrint(&s);
//在pos位置删除+打印
SeqListErase(&s, 1);
SeqListPrint(&s);
//顺序表销毁
SeqListDestroy(&s);
}
int main()
{
TestSeqList();
return 0;
}
注意:
顺序表单独写初始化函数的原因:给定一个初值,防止后期使用出错。顺序表初始化不是一两句代码能完成的,所以要写一个单独的函数
题目练习
题目
- 移除数组元素,要求时间复杂度为O(N),空间复杂度O(1)。OJ 移除元素
- OJ 删除有序数组中的重复项
- OJ 合并两个有序数组
解答
int removeElement(int* nums, int numsSize, int val)
{
//给数组定义两个下标
int src = 0;
int dest = 0;
//查找
while (src < numsSize)
{
if(nums[src] == val)
{
src++;
}
else
{
nums[dest] = nums[src];
dest++;
src++;
}
}
return dest;
}
int removeDuplicates(int* nums, int numsSize)
{
int src = 0;
int dest = 0;
while (src < numsSize)
{
if (nums[src] == nums[dest])
{
src++;
}
else
{
//第一个元素直接留下
nums[++dest] = nums[src];
src++;
}
}
//元素个数:下标+1
return dest + 1;
}
本题思路,定义三个下标,从后向前执行
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
int end1 = m - 1;
int end2 = n - 1;
int dest = m + n - 1;
while (end1 >= 0 && end2 >= 0)
{
if (nums1[end1] > nums2[end2])
{
nums1[dest] = nums1[end1];
dest--;
end1--;
}
else
{
nums1[dest] = nums2[end2];
dest--;
end2--;
}
}
while (end2 >= 0)
{
nums1[dest--] = nums2[end2--];
}
}
顺序表劣势总结
- 中间/头部的插入删除,时间复杂度过大
- 增容需要申请新空间,拷贝数据,释放旧空间,消耗大。
- 增容一般是呈2倍的增长,存在空间浪费