大家还记得在C语言中,如果我们想要一口气存放很多数据,那我们就必须要依靠数组。数组是一种基础且常见的数据结构,那今天我们要介绍的是数组的plus版本--顺序表!!
目录
一、顺序表
1. 顺序表的概念及结构
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
而线性表是顺序表的一种,顺序表逻辑结构是线性的,物理结构上是连续的。
2. 顺序表分类
顺序表与数组的区别:顺序表的底层结构是数组,对数组的封装,实现了常用的增删改查等接口。
2.1 静态顺序表
静态顺序表是通过定长数组存储元素。
//静态顺序表
#define N 100
typedef int SLDataType;
struct SeqList
{
SLDataType a[N];//数组大小固定
//N是100,是指开辟了100个空间,而不是已经有100个有效数据
int size;//有效数据个数
};
静态顺序表缺陷:空间给少了不够用,给多了造成空间浪费。
2.2 动态顺序表
//动态顺序表
typedef int SLDataType;
struct SeqList
{
SLDataType* arr;//存储数据的底层结构
int capacity;//记录顺序表的空间大小
int size;//有效数据个数
};
当给定的空间全部用完,我们可以进行扩容。
3. 动态顺序表的实现
先在头文件中定义我们想要实现的各种功能接口:
//SeqList.h
#pragma once
//动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* arr;//存储数据的底层结构
int capacity;//记录顺序表的空间大小
int size;//有效数据个数
}SL;
//顺序表的初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
//打印顺序表
void SLPrint(SL* ps);//都传地址是为了保持接口一致性
//顺序表的尾插和头插
void SLPushFront(SL* ps, SLDataType x);
void SLPushBack(SL* ps, SLDataType x);
//顺序表头部/尾部的删除
void SLPopBack(SL* ps);
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.1 顺序表的初始化及销毁
//初始化和销毁
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
void SLDestroy(SL* ps)
{
assert(ps);
//先判断数组是否为空,不为空才可销毁
if (ps->arr)
{
free(ps->arr);
}
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
3.2 顺序表的打印
//打印顺序表
void SLPrint(SL* ps)
{
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
3.3 尾插/头插数据
涉及到插入数据的时候,最重要的是判断现有的空间是否足够去插入新的数据,因此我们可以单独封装一个函数,用于判断现有空间是否足够插入数据,不够的话就进行扩容。
//一次只扩容一个空间,不会造成空间浪费,但执行效率低下
//一次扩容固定大小的空间,少了需要频繁扩容,大了造成空间浪费
//扩容原则:成倍数增加(1.5倍/2倍),数据插入的越多,扩容的大小变大
void SLCheckCapacity(SL* ps)
{
if (ps->size == ps->capacity)
{
//注意:初始化时,capacity=0
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;//capacity是所占的比特数
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
if (tmp == NULL)
{
perror("realloc fail");
return 1;
}
//扩容成功
//free(ps->arr);//realloc会自动把旧空间释放
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
实现扩容之后,我们便可以进行数据的插入操作。
//数据的尾插
void SLPushBack(SL* ps, SLDataType x)
{
//ps不能为空,需要判断
//断言--粗暴的判断方式
assert(ps);
//if--温柔的判断方式(不推荐)
/*if (ps == NULL)
{
return;
}*/
//空间不够,就要扩容
SLCheckCapacity(ps);
//空间足够时,直接插入,arr[size]=x
ps->arr[ps->size++] = x;
}
//头插
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
//空间不够,就要扩容
SLCheckCapacity(ps);
//空间足够时,需要先把当前顺序表中已有的数据向后移一位
//把下标为0的位置空出来,然后再插入
for (int i = ps->size; i > 0; i--)
{
ps->arr[i] = ps->arr[i - 1];//把下标为i-1的数据给i
}
ps->arr[0] = x;
ps->size++;
}
3.4 顺序表的尾删/头删
//尾删
void SLPopBack(SL* ps)
{
assert(ps);
//顺序表为空,不能执行删除
assert(ps->size);
//顺序表不为空,直接删除,size--
ps->size--;//下标为size本身就不会被打印,只打印到size-1
//假设要把最后的100删除,只需要size--
//后续想在最后插入200,只需要把100的位置改成200,size++即可
}
//头删
void SLPopFront(SL* ps)
{
assert(ps);
//顺序表为空,不能执行删除
assert(ps->size);
//顺序表不为空,需要把后面所有有效数据前移
for (int i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];//把下标为i+1的数据前移到下标为i的位置
}
ps->size--;
}
3.5 在指定位置之前插入数据
//指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x) {
assert(ps);
//需要确保pos是一个有效的位置
assert(pos >= 0 && pos <= ps->size);
//插入数据的时候要确保空间足够,否则需要扩容
SLCheckCapacity(ps);
//把原本pos及之后的数据往后挪动一位,把下标为pos的位置空出来
for (int i = ps->size; i > pos; i--)
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;//有效数据+1
}
3.6 删除指定位置的数据
//删除指定位置数据
void SLErase(SL* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);
//pos以后的数据往前挪动一位
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;//有效数据-1
}
3.7 查找指定数据的位置
//查找指定数据所在的位置
int SLFind(SL* ps, SLDataType x)
{
//加上断言对代码的健壮性更好
assert(ps);
//直接对顺序表进行遍历
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x) {
return i;//i为x在顺序表中所在位置的数组下标
}
}
return -1;
}
二、顺序表实现通讯录项目
1. 功能要求
- 至少能够存储100个人的通讯信息
- 能够保存用户信息:名字、性别、年龄、电话、地址等
- 增加联系人信息
- 删除指定联系人
- 查找制定联系人
- 修改指定联系人
- 显示联系人信息
2. 代码实现
2.1 头文件定义相关接口
//Contact.h
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h> //暂时加上
//#include"SeqList.h"//SeqList.h中已经包含了Contact.h,就会造成头文件嵌套问题
//解决方法:前置声明
#define NAME_MAX 100
#define GENDER_MAX 10
#define TEL_MAX 12
#define ADDR_MAX 100
//通讯录数据类型
typedef struct PersonInfo
{
char name[NAME_MAX];
int age;
char gender[GENDER_MAX];
char tel[TEL_MAX];
char addr[ADDR_MAX];
}Info;
struct SeqList;//使用顺序表的前置声明
typedef struct SeqList Contact;
//通讯里提供的操作
//通讯录的初始化和销毁
void ContactInit(Contact* pcon);//实际初始化的还是顺序表
void ContactDestroy(Contact* pcon);//针对通讯录项目起贴切的名字
//增加、删除、修改、查找、查看通讯录
void ContactAdd(Contact* pcon);
void ContactDel(Contact* pcon);
void ContactModify(Contact* pcon);
void ContactFind(Contact* pcon);
void ContactShow(Contact* pcon);
2.2 接口的具体实现
//Contact.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Contact.h"
#include"SeqList.h"
#include<string.h>
//通讯录的初始化和销毁
void ContactInit(Contact* pcon) {
SLInit(pcon);
}
void ContactDestroy(Contact* pcon) {
SLDestroy(pcon);
}
//增加、删除、修改、查找、查看通讯录
void ContactAdd(Contact* pcon) {
//创建联系人结构体变量
Info info;
printf("请输入联系人姓名:\n");
scanf("%s", info.name);
printf("请输入联系人年龄:\n");
scanf("%d", &info.age);
printf("请输入联系人性别:\n");
scanf("%s", info.gender);
printf("请输入联系人电话:\n");
scanf("%s", info.tel);
printf("请输入联系人住址:\n");
scanf("%s", info.addr);
SLPushBack(pcon, info);//保存数据到通讯录(顺序表)
}
//从通讯录中查找想要操作的姓名是否存在
int FindByName(Contact* pcon, char name[])
{
for (int i = 0; i < pcon->size; i++)
{
//通过strcmp比较两个字符串是否相等
if (strcmp(pcon->arr[i].name, name) == 0)
{
return i;
}
}
return -1;
}
void ContactDel(Contact* pcon) {
//删除之前一定要先查找
//找到了,可以删除
//找不到,不能执行删除
printf("请输入要删除的联系人姓名:\n");
char name[NAME_MAX];
scanf("%s", name);
int findIndex = FindByName(pcon, name);
if (findIndex < 0)
{
printf("要删除的联系人不存在!\n");
return;
}
//执行删除操作
SLErase(pcon, findIndex);
printf("联系人删除成功!\n");
}
void ContactModify(Contact* pcon) {
//修改之前要先查找
//找到了,执行修改操作
//没有找到,不能执行修改操作
char name[NAME_MAX];
printf("请输入要修改的联系人姓名:\n");
scanf("%s", name);
int findIndex = FindByName(pcon, name);
if (findIndex < 0)
{
printf("要修改的联系人不存在!\n");
return;
}
//找到了,执行修改操作
printf("请输入姓名:\n");
scanf("%s", pcon->arr[findIndex].name);
printf("请输入年龄:\n");
scanf("%d", &pcon->arr[findIndex].age);
printf("请输入性别:\n");
scanf("%s", pcon->arr[findIndex].gender);
printf("请输入电话:\n");
scanf("%s", pcon->arr[findIndex].tel);
printf("请输入地址:\n");
scanf("%s", pcon->arr[findIndex].addr);
printf("联系人修改成功!\n");
}
void ContactShow(Contact* pcon)
{
printf("%s %s %s %s %s\n", "姓名", "性别", "年龄", "电话", "住址");
for (int i = 0; i < pcon->size; i++)
{
printf("%s %s %d %s %s\n",
pcon->arr[i].name,
pcon->arr[i].gender,
pcon->arr[i].age,
pcon->arr[i].tel,
pcon->arr[i].addr
);
}
}
void ContactFind(Contact* pcon) {
char name[NAME_MAX];
printf("请输入要查找的用户姓名:\n");
scanf("%s", name);
int findIndex = FindByName(pcon, name);
if (findIndex < 0)
{
printf("该联系人不存在!\n");
return;
}
//找到了就打印信息
printf("%s %s %s %s %s\n", "姓名", "性别", "年龄", "电话", "住址");
printf("%s %s %d %s %s\n",
pcon->arr[findIndex].name,
pcon->arr[findIndex].gender,
pcon->arr[findIndex].age,
pcon->arr[findIndex].tel,
pcon->arr[findIndex].addr
);
}
2.3 整体代码测试
#define _CRT_SECURE_NO_WARNINGS 1
//#include"Contact.h" //在SeqList.h文件中已经包了Contact.h
#include"SeqList.h"
//通讯录菜单
void menu()
{
printf("***************通讯录***************\n");
printf("*****1.添加联系人 2.删除联系人*****\n");
printf("*****3.修改联系人 4.查找联系人*****\n");
printf("*****5.查看通讯录 0. 退 出 ******\n");
printf("************************************\n");
}
int main()
{
int op = -1;
//创建通讯录结构对象
Contact con;
ContactInit(&con);
do
{
menu();
printf("请选择您的操作:\n");
scanf("%d", &op);
switch (op)
{
case 1:
//添加联系人
ContactAdd(&con);
break;
case 2:
//删除联系人
ContactDel(&con);
break;
case 3:
//修改联系人
ContactModify(&con);
break;
case 4:
//查找联系人
ContactFind(&con);
break;
case 5:
//查看通讯录
ContactShow(&con);
break;
case 0:
//退出通讯录
printf("通讯录已退出\n");
break;
default:
break;
}
} while (op != 0);
//销毁通讯录
ContactDestroy(&con);
return 0;
}
三、顺序表经典算法
1. 移除数组元素
题目链接:移除元素 - 力扣(LeetCode)
int removeElement(int* nums, int numsSize, int val) {
//思路:双指针
//两个指针初始时分别位于数组的首尾,向中间移动遍历该序列
int left = 0;
int right = numsSize-1;
while (left <= right)
{
if (nums[left] == val)
{
//左边等于val,就用右边的数据覆盖这个左边的数据
nums[left] = nums[right];
right--;
}
else
left++;
}
return left;
}
2. 合并两个有序数组
//思路1:先把nums2放到nums1里,再对nums1排序
int cmp(int* a, int* b){
return *a - *b;
}
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
for(int i = 0; i != n; i++){
nums1[m + i]=nums2[i];
}
qsort(nums1,nums1Size,sizeof(int),cmp);//qsort快排函数(不推荐,投机取巧)
}
//思路2:逆向双指针
//从两个数组的最后一个有效数据从后往前比较
//几个大的数据从大到小把nums1里末尾的0从后往前覆盖掉
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
int p1 = m - 1;
int p2 = n - 1;
int p3 = m + n - 1;
int cur;
while (p1 >= 0 && p2 >= 0)
{
//从后往前比较
if(nums1[p1]>nums2[p2])
nums1[p3--]=nums1[p1--];
else
nums1[p3--]=nums2[p2--];
}
//p2>=0说明nums2还有元素没放到nums1里
while(p2>=0)
nums1[p3--]=nums2[p2--];
}
四、顺序表的问题及思考
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。
为了解决以上问题,我们引入了“链表”这个概念。那我们下一篇就开始介绍单链表及其具体实现吧!!