NzN的数据结构--顺序表及实现

        大家还记得在C语言中,如果我们想要一口气存放很多数据,那我们就必须要依靠数组。数组是一种基础且常见的数据结构,那今天我们要介绍的是数组的plus版本--顺序表!!

目录

一、顺序表

1. 顺序表的概念及结构

2. 顺序表分类

2.1 静态顺序表

2.2 动态顺序表

3. 动态顺序表的实现

 3.1 顺序表的初始化及销毁

3.2 顺序表的打印

3.3 尾插/头插数据

3.4 顺序表的尾删/头删

3.5 在指定位置之前插入数据

3.6 删除指定位置的数据

3.7 查找指定数据的位置

二、顺序表实现通讯录项目

1. 功能要求

2. 代码实现

2.1 头文件定义相关接口

2.2 接口的具体实现

2.3 整体代码测试

三、顺序表经典算法

1. 移除数组元素

2. 合并两个有序数组

四、顺序表的问题及思考


一、顺序表

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. 合并两个有序数组

题目链接:合并两个有序数组 - 力扣(LeetCode)

//思路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倍的增长,势必会有一定的空间浪费。

        为了解决以上问题,我们引入了链表这个概念。那我们下一篇就开始介绍单链表及其具体实现吧!!

  • 39
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值