一. 线性表
1.什么是线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结
构,常见的线性表:顺序表、链表、栈、队列、字符串...
2.线性表的结构
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物
理上存储时,通常以数组和链式结构的形式存储。
二. 顺序表
1.什么是顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改
2.顺序表的分类
顺序表一般分为两类,静态顺序表和动态顺序表。
2.1 静态顺序表
静态顺序表:使用定长数组存储元素
- 缺点:小了不够用,大了浪费
#define N 7
typedef int SLDataType; // 重定义类型
//静态顺序表
typedef struct SeqList
{
SLDataType a[N];// 定长数组
int size;//存储元素个数
}SL;
2.2动态顺序表
动态顺序表:使用动态开辟的数组存储。
- 特点:可以根据自己的需要调整大小
// 动态顺序表
typedef struct SeqList
{
SLDataType* a; // 指向动态开辟的数组
int size;// 有效数据个数
int capacity; // 当前容量
}SL;
三.动态顺序表的实现
3.1. 动态顺序表的定义
typedef int SLDataType; // 重定义类型
typedef struct SeqList // 重定义顺序表
{
SLDataType* a; // 指向动态开辟数组
int size;// 有效的数据个数
int capacity; // 数组的容量
}SL;
- 数据类型重命名为
SLDataType
,这样重命名的好处是:我们用顺序表管理别的类型数据类型时,只需要更改重定义部分即可。 - 对比静态顺序表,动态顺序表中多了一个
capacity
变量,我们用该变量来记录数组的容量大小,当size
大小等于capacity
时,我们就要对顺序表的空间进行扩容。
3.2 顺序表的初始化
void SeqListInit(SL* ps)
{
assert(psl); // 断言:防止ps为空指针
ps->a = NULL;
ps->size = ps->capacity = 0;
}
- 顺序表刚开始为空,所以我们把
size
和capacity
置为0,并且把指向动态开辟的数组的指针置为NULL
,防止野指针。
3.3 检查空间容量(扩容)
void SeqListCheckCapacity(SL* ps)
{
// 如果没有空间或者空间不足就扩容
if (ps->size == ps->capacity)
{
int newcapcity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapcity * sizeof(SLDataType));
if (NULL == tmp)
{
printf("realloc fail\n");
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapcity;
}
}
- 当
size
等于capacity
时,就扩容。这里需要注意的是,第一次扩容时,顺序表中的容量为0,0*2 = 0, 我们无法对第一次扩容直接翻倍,需要单独给,所以我们采用三目操作符
来实现。 对第一次扩容赋值为4(我们在实现过程中,扩容倍数和初始容量都不是唯一的,具体情况具体分析。采用倍数为2,是因为两倍是一个比较合理的倍数),一次如果增容过多,容易造成空间浪费,一次增容太少又容易导致增容频繁,造成效率低下。 - 我们不能直接用指针a来接收
realloc
函数的返回值,如果开辟内存失败,返回NULL
,导致指针a找不到原来开辟的空间—偷鸡不成蚀把米,导致内存泄漏。所以我们应该创建一个临时变量tmp
,来接收realloc
函数的返回值,并且做判空处理。然后再把开辟成功空间的地址传递给指针a。 realloc
函数单独使用能实现malloc
函数的效果。当realloc
第一个参数为NULL
时,效果等同于malloc
.所以无论是第一次开辟空间还是增容时,realloc
函数都适用。
3.4 顺序表尾插
void SeqListPushback(SL* ps, SLDataType x) // 尾插
{
assert(ps);
// 如果没有空间或者空间不足就扩容
// 方法一:
/* SeqListCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;*/
//方法二: 直接调用任意位置插入函数(本文后面介绍)
SeqListInsert(ps, ps->size, x);
}
- 顺序表尾插比较容易,直接插入,但是再尾插时,注意空间的容量,是否需要扩容(直接调用3.3中的扩容函数)
3.5 顺序表尾删
void SeqListPopBack(SL* ps) // 尾删
{
assert(ps);
// 温柔方式:
//if (ps->size > 0)
//{
// ps->size--;
//}
// 严格方式:
assert(ps->size > 0); // 判断是否大于0
// 方法一:
ps->size--;
// 方法二:直接调用任意位置删除函数(本文后面介绍)
SeqListErase(ps, ps->size-1); // 尾删
}
- 顺序表尾删很简单,直接
size
–即可。不需要缩容或者改动 - 我们在尾删过程中,必须保证
size
>0(即有数据)。我们有两种判断方式: 温柔方式为采用一个if
语句,满足条件再进行-- ; 严格方式: 采用断言(assert)方式,当不满足条件时,会直接报错并且告诉你错误在哪里。
3.6顺序表头插
void SeqListPushFront(SL* ps, SLDataType x) // 头插
{
//方法1:
assert(ps);
// 检查增容
SeqListCheckCapacity(ps);
// 挪动数据
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
//方法2: 直接调用任意位置插入函数
SeqListInsert(ps, 0, x);
}
- 先检查容量,是否需要扩容,然后把数据全部先后挪动一位,然后把要插入的数据放入第一个位置,然后
size
++。
3.7 顺序表头删
void SeqListPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//方法1:
//挪动数据
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
//方法2:
/* int begin = 0;
while (begin < psl->size-1)
{
psl->arr[begin] = psl->arr[begin+1];
begin++;
}
ps->size--;*/
//方法3:
SeqListErase(ps, 0); // 头删
}
- 在头删时,我们需要先将顺序表中的数据整体向前挪动一位,然后size–即可。
3.8 顺序表查找
int SeqListFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
- 遍历数组,若找到该元素,则返回该元素的下标,如果该元素不存在时,返回-1。
3.9 顺序表任意位置插入
//顺序表在指定位置(pos)插入指定的值(x)
void SeqListInsert(SL* ps, int pos, SLDataType x)
{
// 温柔方式:
if (pos > ps->size || pos < 0)
{
printf("pos invalid\n");
return;
}
// 严格方式: assert(pos >= 0 && pos <= ps->size);
// 判断是否需要扩容
SeqListCheckCapacity(ps);
int end = ps->size - 1;
// 挪动数据
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
- 插入数据一定涉及挪动数据,在每次挪动数据,都需要对容量进行检查,判断是否需要扩容。
- 在
pos
位置插入数据,所以我们要把pos
及其之后的数据全部向后移动一位,然后再在pos
位置插入数据
我们发现头插和尾插也可以调用SeqListInsert函数实现,我们可以进行改造,以此简化代码:
3.9.1 头插
void SeqListPushFront(SL* ps, SLDataType x)
{
assert(ps);
SeqListInsert(ps, 0, x);
}
3.9.2尾插
void SeqListPushBack(SL* ps, SLDataType x)
{
assert(ps);
SeqListInsert(ps, ps->size, x);
}
3.10 顺序表任意位置删除
void SeqListErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
- 删除指定位置数据,我们需要将pos后面数据整体向前挪动一位,然后让size–。
同上,头删和尾删也可以调用SeqListErase函数实现,我们可以进行改造,以此简化代码:
3.10.1头删
void SeqListPopFront(SL* ps)
{
assert(ps);
SeqListErase(ps, 0);
}
3.10.2尾删
void SeqListPopBack(SL* ps)
{
assert(ps);
SeqListErase(ps, psl->size-1);
}
3.11 顺序表的销毁
void SeqListDestory(SL* ps) // 结束释放空间
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
- 在销毁顺序表之前,一定要将前面动态开辟的内存空间释放掉,防止内存泄漏。
3.12 顺序表打印
void SeqListPrint(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
3. 13 顺序表修改
void SeqListModify(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(psl->size > pos);
ps->arr[pos] = x;
}
- 找到指定下标的元素,并修改为指定的值。但是注意检查size的值一定要大于pos,利用断言(assert)
四.完整代码
在完成各个接口函数实现过程,中我们创建了三个文件。
SeqList.h文件,用于函数声明
SeqList.c文件,用于函数的定义
Test.c文件,用于测试函数
4.1 SeqList.h
#define _CRT_SECURE_NO_WARNINGS 1
//#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
// 动态顺序表
typedef struct SeqList
{
SLDataType* a; // 指向动态开辟的数组
int size;// 有效数据个数
int capacity; // 当前容量
}SL;
//对数据的管理:基本增删查改接口
// 顺序表初始化
void SeqListInit(SL* psl);
// 检查空间容量,如果满了,进行增容
void CheckCapacity(SL* psl);
// 顺序表尾插
void SeqListPushBack(SL* psl, SLDataType x);
// 顺序表尾删
void SeqListPopBack(SL* psl);
// 顺序表头插
void SeqListPushFront(SL* psl, SLDataType x);
// 顺序表头删
void SeqListPopFront(SL* psl);
// 顺序表查找
int SeqListFind(SL* psl, SLDataType x);
// 顺序表在pos位置插入x
void SeqListInsert(SL* psl, size_t pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErase(SL* psl, size_t pos);
// 顺序表销毁
void SeqListDestory(SL* psl);
// 顺序表打印
void SeqListPrint(SL* psl);
// 顺序表修改
void SeqListModify(SL* psl, size_t pos, SLDataType x);
4.2 SeqList.c
#include"SeqList.h"
// 顺序表的初始化
void SeqListInit(SL* ps)
{
assert(psl); // 断言:防止ps为空指针
ps->a = NULL;
ps->size = ps->capacity = 0;
}
// 检查扩容
void SeqListCheckCapacity(SL* ps)
{
// 如果没有空间或者空间不足就扩容
if (ps->size == ps->capacity)
{
int newcapcity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapcity * sizeof(SLDataType));
if (NULL == tmp)
{
printf("realloc fail\n");
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapcity;
}
}
// 尾插
void SeqListPushback(SL* ps, SLDataType x)
{
assert(ps);
// 如果没有空间或者空间不足就扩容
SeqListCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
// 尾删
void SeqListPopBack(SL* ps)
{
assert(ps);
// 温柔方式:
//if (ps->size > 0)
//{
// ps->size--;
//}
// 严格方式:
assert(ps->size > 0); // 判断是否大于0
ps->size--;
}
//头插
void SeqListPushFront(SL* ps, SLDataType x) // 头插
{
//方法1:
assert(ps);
// 检查增容
SeqListCheckCapacity(ps);
// 挪动数据
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
//方法2: 直接调用任意位置插入函数
//SeqListInsert(ps, 0, x);
}
// 头删
void SeqListPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//方法1:
//挪动数据
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
//方法2:
/* int begin = 0;
while (begin < psl->size-1)
{
psl->arr[begin] = psl->arr[begin+1];
begin++;
}
ps->size--;*/
//方法3:
//SeqListErase(ps, 0); // 头删
}
// 查找
int SeqListFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
//顺序表在指定位置(pos)插入指定的值(x)
void SeqListInsert(SL* ps, int pos, SLDataType x)
{
// 温柔方式:
if (pos > ps->size || pos < 0)
{
printf("pos invalid\n");
return;
}
// 严格方式: assert(pos >= 0 && pos <= ps->size);
// 判断是否需要扩容
SeqListCheckCapacity(ps);
int end = ps->size - 1;
// 挪动数据
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
// 指定位置删除
void SeqListErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
//顺序表的销毁
void SeqListDestory(SL* ps) // 结束释放空间
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = ps->capacity = 0;
}
// 打印
void SeqListPrint(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
//修改
void SeqListModify(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(ps->size > pos);
ps->a[pos] = x;
}
4.3 Test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void TestSeqList1()
{
SL s;//初始化
SeqListInit(&s);
//尾插
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPrint(&s);//打印
//头插
SeqListPushFront(&s, 10);
SeqListPushFront(&s, 20);
SeqListPushFront(&s, 30);
SeqListPrint(&s);
//尾删
SeqListPopBack(&s);
SeqListPopBack(&s);
SeqListPrint(&s);
//头删
SeqListPopFront(&s);
SeqListPopFront(&s);
SeqListPrint(&s);
//销毁
SeqListDestory(&s);
}
void TestSeqList2()
{
SL s;
SeqListInit(&s);
SeqListPushBack(&s, 1);
SeqListPushBack(&s, 2);
SeqListPushBack(&s, 3);
SeqListPushBack(&s, 4);
SeqListPrint(&s);
//在指定位置插入指定元素
SeqListInsert(&s, 1, 10);
SeqListInsert(&s, 3, 20);
SeqListInsert(&s, 5, 30);
SeqListInsert(&s, 7, 40);
SeqListPrint(&s);
//在指定位置删除元素
//注意元素越来越少
SeqListErase(&s, 0);
SeqListErase(&s, 1);
SeqListErase(&s, 2);
SeqListErase(&s, 3);
SeqListPrint(&s);
// 销毁
SeqListDestory(&s);
}
void TestSeqList3()
{
SL s;
SeqListInit(&s);
SeqListPushFront(&s, 30);
SeqListPushFront(&s, 20);
SeqListPushFront(&s, 10);
SeqListPushback(&s, 4);
SeqListPushback(&s, 5);
SeqListPushback(&s, 6);
SeqListPrint(&s);
int x = 0;
int y = 0;
printf("请输入要查找的元素:>");
scanf("%d", &x);//要查找的元素
printf("请输入要修改的值:>");
scanf("%d", &y);//要修改的值
//查找该元素是否存在
int pos = SeqListFind(&s, x);
if (pos != -1)
{
printf("要查找的元素下标存在,并且为:%d\n", pos);
//存在就修改
SeqListModify(&s, pos, y);
}
printf("修改后的顺序表元素为:\n");
SeqListPrint(&s);
// 销毁
SeqListDestory(&s);
}
void Menu()
{
printf("***************************\n");
printf("请选择你的操作:>\n");
printf("1.头插 2.头删\n");
printf("3.尾插 4.尾删\n");
printf("5.打印 6.修改\n");
printf(" -1.退出\n");
printf("***************************\n");
}
void MenuTest()
{
SL s1;
SeqListInit(&s1); // 初始化
int input = 0;
int x;
int pos;
while (input != -1)
{
Menu();
scanf("%d", &input);
switch (input)
{
case 0:
break;
case 1:
printf("请输入你要头插的数据,以-1结束:");
scanf("%d", &x);
while (x != -1)
{
SeqListPushFront(&s1, x);
scanf("%d", &x);
}
break;
case 2:
SeqListPopFront(&s1);
break;
case 3:
printf("请输入你要尾插的数据,以-1结束:");
scanf("%d", &x);
while (x != -1)
{
SeqListPushback(&s1, x);
scanf("%d", &x);
}
break;
case 4:
SeqListPopBack(&s1);
break;
case 5:
SeqListPrint(&s1);
break;
case 6:
printf("请输入你要修改的下标:>");
scanf("%d", &pos);
printf("请输入要修改为的值:>");
scanf("%d", &x);
SeqListModify(&s1, pos, x);
break;
dafault:
printf("输入错误,请重新输入!\n");
}
}
// 释放
SeqListDestory(&s1);
}
int main()
{
//TestSeqList1();
//TestSeqList2();
//TestSeqList3();
//建议先写函数模块,测试完后没有问题再写菜单是最合适的
MenuTest();
return 0;
}
五.顺序表的问题及思考
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,
我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
为了解决这些问题,我们设计产生了链表! 消化消化顺序表,让我们继续学习链表。
🔑注:本人也是刚刚开始学习数据结构,文章有表达错误或者代码错误,欢迎各位大佬前辈们指正!希望我们共同学习进步。