文章目录
顺序表
观看这里的uu建议先看时间复杂度与空间复杂度
时间复杂度与空间复杂度点击这里
一、前言
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列等。
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是典型的线性表之一,以数组的形式存储。
二、顺序表
1·顺序表的概念
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表本质上可以视为数组,其不仅在逻辑上是线性结构的,而且其在物理结构上也是连续存储的,顺序表在数组的基础上引入了静态和动态的概念。
2·静态顺序表及其接口函数
静态顺序表就是给定存储数据的大小来存储数据,存储已满就禁止插入数据,静态顺序表很难确定给定的存储大小是多少,如果给定的存储太小,那么就不够存储的需求;如果给定的存储太大,那么就会浪费空间,所以一般不用静态顺序表存储数据。
静态顺序表头文件Seqlist.h的声明如下:
#pragma once //防止头文件被重复包含
#ifndef _Seqlist_H_
#define _Seqlist_H_
#include <cstddef> //使用size_t需包含此头文件
#define N 1000 //静态顺序表固定的存储容量
//重新自定义int标识符,后续存储其它数据类型元素可以方便修改
typedef int SLDataType;
//静态顺序表
typedef struct Seqlist
{
SLDataType a[N]; //存储数据的数组
int size; //表示数组中目前存储数据的个数
}SL;
// 基本增删查改接口函数
//这些接口函数以指针形式接收是因为函数中的形参改变并不会改变实参的数值
//只有传递指针后对地址空间解引用才能实现实参的修改
// 顺序表初始化
void SeqListInit(SL * ps);
// 顺序表销毁
void SeqListDestory(SL* ps);
// 顺序表打印
void SeqListPrint(SL* ps);
// 检查空间,如果存储已满,进行扩容
void CheckCapacity(SL* ps);
// 顺序表尾插
void SeqListPushBack(SL* ps, SL x);
// 顺序表尾删
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);
#endif
静态顺序表的接口函数实现方式与动态顺序表大同小异,这里不做赘述,在动态顺序表中详细说明。
3·动态顺序表及其接口函数
动态顺序表可以根据需求来扩容存储空间,比静态顺序表灵活,因此动态顺序表被广泛使用。
动态顺序表头文件Seqlist.h的声明如下:
#pragma once //防止头文件被重复包含
#ifndef _Seqlist_H_
#define _Seqlist_H_
#include <cstddef> //使用size_t需包含此头文件
#include <stdio.h>
#include <stdlib.h>
//重新自定义int标识符,后续存储其它数据类型元素可以方便修改
typedef int SLDataType;
//动态顺序表
typedef struct Seqlist
{
SLDataType* a; //后续动态开辟空间的指针
int size; //表示当前空间中目前存储数据的个数
int capacity; //空间实际能存储数据容量大小
}SL;
// 基本增删查改等接口函数
/* 这些接口函数以指针形式接收是因为函数中的形参改变并不会改变实参的数值
只有传递指针后对地址空间解引用才能实现实参的修改*/
// 顺序表初始化
void SeqListInit(SL* ps);
// 顺序表销毁
void SeqListDestory(SL* ps);
// 顺序表打印
void SeqListPrint(SL* ps);
// 检查空间,如果存储已满,进行扩容
void CheckCapacity(SL* ps);
// 顺序表尾插
void SeqListPushBack(SL* ps, SLDataType x);
// 顺序表尾删
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);
#endif
三、动态顺序表的实现
1·初始化、打印、扩容与销毁
初始化函数在Seqlist.c中如下:
//顺序表初始化
void SeqListInit(SL* ps)
{
ps->a = NULL;
ps->capacity = ps->size = 0;
}
打印函数在Seqlist.c中如下:
//打印顺序表的内容
void SeqListPrint(SL* ps)
{
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
}
扩容函数在Seqlist.c中如下:
//顺序表扩容
void CheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
//如果是0,那么设置为4;如果不是0,那么一般扩容两倍
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
//动态开辟空间
SLDataType* tmp = (SLDataType*)realloc(ps->a, newcapacity * sizeof(SLDataType));
//如果开辟失败,打印开辟失败且终止程序
if (tmp == NULL)
{
printf("realloc fail");
exit(-1);
}
//更新容量
ps->capacity = newcapacity;
ps->a = tmp;
}
}
说明:在顺序表插入数据过程中,难免会遇到给定的初始存储空间不够用的情况,因此顺序表扩容一般在尾部插入数据、头部插入数据和任意位置插入数据函数中嵌套使用。
销毁函数在Seqlist.c中如下:
//销毁不使用的空间,并且还原数据初始值
void SeqListDestory(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
说明:在动态开辟的空间不使用时,要及时将空间还给操作系统,以免造成内存泄漏。
以上函数的具体实现情况尾部、头部和任意位置操作时大同小异,仅在尾部操作时详细说明。
2·尾部插入数据&尾部删除数据
尾部插入数据函数在Seqlist.c中如下:
//顺序表尾部插入数据
void SeqListPushBack(SL* ps, SLDataType x)
{
//如果顺序表容量不够,就使用扩容函数扩容
CheckCapacity(ps);
//存入数据
ps->a[ps->size] = x;
//数据+1
ps->size++;
}
尾部删除数据函数在Seqlist.c中如下:
//顺序表删除尾部数据
void SeqListPopBack(SL* ps)
{
顺序表中当前数据个数<0是非法访问
//if (ps->size > 0)
//{
直接将顺序表中当前数据个数减1即可,尾部数据将不会被访问到,等价于消失
// ps->size--;
//}
//如果顺序表中当前数据个数<0,就不运行下面的程序,直接终止程序
assert(ps->size > 0);
//直接将顺序表中当前数据个数减1即可,尾部数据将不会被访问到,等价于消失
ps->size--;
}
具体使用方式如下:
#include "Seqlist.h"
#include <stdio.h>
#include <stdlib.h>
void TestSeqlist1()
{
SL s1;
SeqListInit(&s1);
//尾部插入一些数据
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPushBack(&s1, 6);
SeqListPushBack(&s1, 7);
//打印插入的数据
SeqListPrint(&s1);
printf("\n");
//删除尾部数据
SeqListPopBack(&s1);
SeqListPopBack(&s1);
SeqListPopBack(&s1);
SeqListPopBack(&s1);
SeqListPopBack(&s1);
SeqListPopBack(&s1);
SeqListPopBack(&s1);
SeqListPopBack(&s1);
//打印尾部删除后的数据
SeqListPrint(&s1);
//释放不再使用的空间
SeqListDestory(&s1);
}
int main()
{
TestSeqlist1();
return 0;
}
执行调试结果如下:
1·向尾部添加4个数据,并对相应变量进行监测:
说明:开辟了4个整型的空间,添加的数据都存储到了对应空间。
2·当添加的数据个数超出当前容量时进行扩容,并对相应变量进行监测:
说明:当容量不够时,扩容至当前容量的两倍,capacity变量变为8。
3·删除尾部数据,并对相应变量进行监测:
说明:删除了5个数据,这里size=2,意思是当前能够被访问到的数据个数,即被删除的对应数据并不会改变,只是不会再被访问到,等价于被删除。
4·当删除的数据个数超出了当前容量时:
说明:当前只添加了7个数据,却删除了8次,那么就会终止程序并报错。
5·当开辟的空间不再使用时,销毁数据,并对数据进行监测:
说明:这里可以看到指向空间首字节的指针变为空指针,capacity、size都被置为0并且之前保存数据的空间都变为无法读取状态。
3·头部插入数据&头部删除数据
头部插入数据函数在Seqlist.c中如下:
//顺序表头部插入数据
void SeqListPushFront(SL* ps, SLDataType x)
{
//如果顺序表容量不够,就使用扩容函数扩容
CheckCapacity(ps);
//定义一个end,进行尾数据的移动,移动一次end-1,当end=-1时此时所有数据已向后移动一位
for (int end = ps->size - 1; end >= 0; --end)
{
ps->a[end + 1] = ps->a[end];
}
//将数据插入在头部
ps->a[0] = x;
//数据+1
ps->size++;
}
头部删除数据函数在Seqlist.c中如下:
//顺序表头部删除数据
void SeqListPopFront(SL* ps)
{
//定义一个start,进行头数据的移动,移动一次start+1,当start=ps->size时此时所有数据已向前移动一位
for (int start = 0; start <= ps->size - 1; start++)
{
ps->a[start] = ps->a[start + 1];
}
//数据-1
ps->size--;
}
具体使用方式如下:
#include "Seqlist.h"
#include <stdio.h>
#include <stdlib.h>
//头部
void TestSeqlist2()
{
SL s1;
SeqListInit(&s1);
//尾部插入一些数据
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
//打印插入的数据
SeqListPrint(&s1);
printf("\n");
//头部插入一些数据
SeqListPushFront(&s1, 5);
SeqListPushFront(&s1, 6);
SeqListPushFront(&s1, 7);
//打印插入的数据
SeqListPrint(&s1);
//头部删除一些数据
SeqListPopFront(&s1);
SeqListPopFront(&s1);
SeqListPopFront(&s1);
SeqListPopFront(&s1);
SeqListPopFront(&s1);
printf("\n");
//打印删除的数据
SeqListPrint(&s1);
//释放不再使用的空间
SeqListDestory(&s1);
}
int main()
{
TestSeqlist2();
return 0;
}
执行调试结果如下:
1·在尾部添加4个数据情况下,向头部添加3个数据,并对相应变量进行监测:
说明:首先是尾部插入四个数,之后头部插入3个数,capacity变为8,头部添加的数据存储到了对应位置。
2·删除头部数据,并对相应变量进行监测:
说明:在共有7个数情况下删除5个数据,size变为2。
4.任意位置插入数据&任意位置删除数据
任意位置插入数据函数在Seqlist.c中如下:
//顺序表任意位置插入数据
void SeqListInsert(SL* ps, size_t pos, SLDataType x)
{
//if (pos > ps->size || pos < 0)
//{
// printf("pos invalid");
//}
//如果插入数据为非法访问,那么结束程序并报错
assert(pos <= ps->size && pos >= 0);
//等价于头部插入数据
if (pos == 0)
{
SeqListPushFront(ps, x);
}
//等价于尾部插入数据
else if (pos == ps->size)
{
SeqListPushBack(ps, x);
}
//在合法情况下,除尾首部插入数据
else
{
//定义一个end,进行尾数据的移动,移动一次end-1,当end=pos时,pos位后的数据已向后移动一位
CheckCapacity(ps);
for (int end = ps->size; end > pos ; end--)
{
ps->a[end] = ps->a[end -1];
}
//将数据插在任意部位
ps->a[pos] = x;
//数据+1
ps->size++;
}
}
任意位置删除数据函数在Seqlist.c中如下:
//顺序表任意位置删除数据
void SeqListErase(SL* ps, size_t pos)
{
//if (pos > ps->size || pos < 0)
//{
// printf("pos invalid");
//}
//如果插入数据为非法访问,那么结束程序并报错
assert(pos <= ps->size && pos >= 0);
//等价于头部删除数据
if (pos == 0)
{
SeqListPopFront(ps);
}
//等价于尾部删除数据
else if (pos == ps->size)
{
SeqListPopBack(ps);
}
//在合法情况下,除尾首部删除数据
else
{
//定义一个start,进行指定部位数据的移动,移动一次start+1,当start=size时,pos位后的数据已向前移动一位
CheckCapacity(ps);
for (int start = pos-1; start < ps->size; start++)
{
ps->a[start] = ps->a[start + 1];
}
//数据-1
ps->size--;
}
}
具体使用方式如下:
#include "Seqlist.h"
#include <stdio.h>
#include <stdlib.h>
//任意位置
void TestSeqlist3()
{
SL s1;
SeqListInit(&s1);
//尾部插入一些数据
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
//第2个位置插入5
SeqListInsert(&s1, 2, 5);
//打印插入后的数据
SeqListPrint(&s1);
printf("\n");
//第0个(头部)位置插入6
SeqListInsert(&s1, 0, 6);
//打印插入后的数据
SeqListPrint(&s1);
printf("\n");
//第6个(尾部)位置插入7
SeqListInsert(&s1, 6, 7);
//打印插入后的数据
SeqListPrint(&s1);
printf("\n");
//非法访问
//SeqListInsert(&s1, 9, 8);
//打印插入的数据
SeqListPrint(&s1);
printf("\n");
//删除第3个位置的数据
SeqListErase(&s1, 3);
//打印删除第3个位置后的数据
SeqListPrint(&s1);
printf("\n");
//删除头部位置数据
SeqListErase(&s1, 0);
//打印删除头部位置后的数据
SeqListPrint(&s1);
printf("\n");
//删除尾部位置数据
SeqListErase(&s1, 5);
//打印删除尾部位置后的数据
SeqListPrint(&s1);
printf("\n");
//非法访问
SeqListErase(&s1, 9);
//释放不再使用的空间
SeqListDestory(&s1);
}
int main()
{
TestSeqlist3();
return 0;
}
执行调试结果如下:
1·向任意位置添加数据,并对相应变量进行监测:
2·删除任意位置数据,并对相应变量进行监测:
5·查找数据
查找数据函数在Seqlist.c中如下:
//顺序表查找数据
int SeqListFind(SL* ps, SLDataType x)
{
int find = 0;
//遍历数组
for (find = 0; find <= ps->size - 1; ++find)
{
//如果在size-1次循环内找到了该变量,输出相应结果并直接退出函数
if (x == ps->a[find])
{
printf("顺序表中有此数据:a[%d]=%d", find, ps->a[find]);
break;
}
}
//如果在size-1次循环内未找到该变量,此时find=size,输出相应结果
if (find == ps->size)
{
printf("顺序表中无此数据:%d", x);
}
return 0;
}
具体使用方式如下:
#include "Seqlist.h"
#include <stdio.h>
#include <stdlib.h>
//查找数据
void TestSeqlist4()
{
SL s1;
SeqListInit(&s1);
//尾部插入一些数据
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPushBack(&s1, 5);
SeqListPushBack(&s1, 6);
SeqListPushBack(&s1, 7);
//打印插入的数据
SeqListPrint(&s1);
printf("\n");
//查找数据
SeqListFind(&s1, 5);
printf("\n");
SeqListFind(&s1, 7);
printf("\n");
SeqListFind(&s1, 9);
printf("\n");
SeqListFind(&s1, 0);
printf("\n");
//释放不再使用的空间
SeqListDestory(&s1);
}
int main()
{
TestSeqlist4();
return 0;
}
执行调试结果如下:
四、顺序表的缺点
- 当空间不够需要增容时,要使用realloc函数增容,根据realloc函数的机制,当原空间后面有足够空间进行扩容时,直接扩容即可,付出的代价较小;当原空间后无足够空间进行扩容时,系统会重新找到一块满足需求的空间,然后将原空间数据复制到新空间,之后指针指向新空间首字节,最后释放旧空间,这种方式付出的代价很大。在复杂工程中,难免会遇到第二种情况。
- 由于在进行扩容时,扩容量都是原空间的两倍,在复杂工程中,难免会频繁扩容,当扩容次数为n时,空间会以2^n指数增加,会造成空间浪费。
- 顺序表要求数据从起始位置连续存储,当我们在头部和任意位置插入或删除数据时,需要挪动数据,那么当数据足够多时,效率必然不高。
因此针对顺序表的缺陷,创造出了链表方式存储数据。
线性表之单链表点击这里