顺序表
一、前言
初次接触数据结构,我们最先遇到的、且最简单的是线性表中的顺序表,所谓顺序表,就是与1234567…依次排序一样。
线性表的顺序存储是指用一组地址连续的存储单元依次存储线性表中的各个元素、使得线性表中在逻辑结构上相邻的数据元素存储在相邻的物理存储单元中,即通过数据元素物理存储的相邻关系来反映数据元素之间逻辑上的相邻关系,采用顺序存储结构的线性表通常称为顺序表。
二、两种顺序表
顺序表有两种实现方法:一是静态顺序表,二是动态顺序表。
2.1静态的顺序表
我们先来看看静态的顺序表:
我们最最常见的数组就是一个静态的顺序表,我们存在数组里的数据,在内存中是连续存储的。
静态顺序表虽然很方便,但是有一个弊端:
当我已经在a[15]数组中存放了15个元素了,依然想在后面再存储16、17…时,此时就数组越界了,即使再创建一个数组来存放16、17…这两个数组的内存也不一定是连续的,此时就不能称之为一个顺序表了,而变成了两个独立的顺序表。
此时就需要使用动态的顺序表了。
2.2动态的顺序表
使用malloc、celloc、realloc等动态内存分配函数来动态管理内存。
如下图,首先用malloc创建15*sizeof(int)个字节大小的空间,当这60个字节的内存已经存满时,就使用realloc函数在刚刚创建的60个字节空间后面再扩容8个字节的大小空间,以便存入16和17两个数据。
以下将分为三个模块代码展示动态的顺序表的基本逻辑。
三、动态顺序表的实现
三个模块是指在同一个项目中创建三个文件包括:
1个头文件:SeqList.h
2个源文件:SeqList.c 、 Test.c
3.1 SeqList.h
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<memory.h>
typedef int SLDataType;
#define INIT_CAPACITY 4//顺序表的初始大小
typedef struct SeqList
{
SLDataType* a;
int size; // 有效数据的个数
int capacity; // 空间容量
}SL;
//增删查改
void SLInit(SL* ps); //初始化
void SLDestory(SL* ps); //销毁
void SLPrint(SL* ps); //打印
void SLCheckCapacity(SL* ps); //扩容
void SLPushBack(SL* ps, SLDataType x); //尾插
void SLPopBack(SL* ps); //尾删
void SLPushFront(SL* ps, SLDataType x); //头插
void SLPopFront(SL* ps); //头删
void SLInsert(SL* ps, int pos, SLDataType x); //在某个位置插入
void SLErase(SL* ps, int pos); //删除某个位置的数据
int SLFind(SL* ps, SLDataType x); //查找某个数据的位置
3.2 SeqList.c
#include"SeqList.h"
void SLInit(SL* ps)
{
//暴力检查
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->capacity = INIT_CAPACITY;
ps->size = 0;
}
void SLDestory(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
void SLCheckCapacity(SL* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDataType* pf = NULL;
pf = (SLDataType*)realloc(ps->a,
sizeof(SLDataType) * ps->capacity * 2);
if (pf == NULL)
{
perror("realloc fail");
return;
}
ps->a = pf;
ps->capacity *= 2;
}
}
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//扩容
SLCheckCapacity(ps);
ps->a[ps->size++] = x;
}
void SLPopBack(SL* ps)
{
assert(ps);
//两种判断size是否为0
//1.size已经为0的时候,断言的方式来暴力提醒非法访问了
assert(ps->size > 0);
//2.不提醒,直接返回
//if (ps->size == 0)
//{
// return;
//}
ps->size--;
}
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
//扩容
SLCheckCapacity(ps);
for (int i = ps->size; i > 0; --i)
{
ps->a[i] = ps->a[i - 1];
}
//增加到第一个
ps->a[0] = x;
ps->size++;
}
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
//往前挪动数据
for (int i = 0; i < ps->size; ++i)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
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->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
//在这里已经间接检查了size是否为0,所以assert(ps->size)就没有必要了
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
因为头插、尾插、头删、尾删可以用SeqInsert函数和SeqErase函数来实现,所以原本这四个函数的实现代码就可以不用了,可以直接使用SeqInsert函数和SeqErase函数,即函数的复用。多使用可复用的代码,让程序可维护性更好,更便于调试、测试。
SeqList.c中部分函数复用的版本(简化)
#include"SeqList.h"
void SLInit(SL* ps)
{
//暴力检查
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->capacity = INIT_CAPACITY;
ps->size = 0;
}
void SLDestory(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
void SLCheckCapacity(SL* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDataType* pf = NULL;
pf = (SLDataType*)realloc(ps->a,
sizeof(SLDataType) * ps->capacity * 2);
if (pf == NULL)
{
perror("realloc fail");
return;
}
ps->a = pf;
ps->capacity *= 2;
}
}
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//扩容
SLCheckCapacity(ps);
SLInsert(ps, ps->size, x);
}
void SLPopBack(SL* ps)
{
assert(ps);
//两种判断size是否为0
//1.size已经为0的时候,断言的方式来暴力提醒非法访问了
assert(ps->size > 0);
//2.不提醒,直接返回
//if (ps->size == 0)
//{
// return;
//}
SLErase(ps, ps->size - 1);
}
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
//扩容
SLCheckCapacity(ps);
SLInsert(ps, 0, x);
}
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
SLErase(ps, 0);
}
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->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
//在这里已经间接检查了size是否为0,所以assert(ps->size)就没有必要了
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
3.3 Test.c
#include"SeqList.h"
void TestSeqList1()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushFront(&s, 0);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLDestory(&s);
}
int main()
{
TestSeqList1();
return 0;
}
注意:
- 头插、头删N个数据的时间复杂度是O(N)
- 尾插、尾删N个数据的时间复杂度是O(1)
- 有了SLInsert,就不用再编写头插、尾插函数了,可以复用了,SLErase和头删、尾删同理。(复用见SeqList.c代码
- 对于顺序表,尾插、尾删是很高效的,但是头插和头删就太低效了,所以头插、头删、中间插入、中间删除等就要使用链表等等了
拓展:
关于realloc
当realloc修改的内存大小比原本malloc(或celloc)创建的大小还要小的时候,将相差的那部分内存的使用权限还给操作系统,理论上来说算作是缩容。但是我们顺序表中一般不用缩容。
原因如下:
- 缩容本身是要消耗性能的。
- 一般情况下,我们扩容(只要要扩容的空间不是特别大)都是在原来的内存后边再申请空间。缩容后,若再要开辟空间,此时如果后面的空间已经被别的数据给占用了,那么就要重新找一块大的空间来给realloc开辟,这就很消耗性能了。
- 对于现在的计算机而言,内存一般用不着节省,没必要缩容来节省那一小块空间。