02.【数据结构-C语言】顺序表(线性表概念、顺序表实现:增删查、前向声明、顺序表实现通讯录项目:增删改查、通讯录数据导入及保存到本地文件)

目录

[1. 线性表](#1. 线性表)

[2. 顺序表概念及分类](#2. 顺序表概念及分类)

[2.1 顺序表的概念](#2.1 顺序表的概念)

[2.2 顺序表分类](#2.2 顺序表分类)

[2.3 动静态顺序表对比](#2.3 动静态顺序表对比)

[3. 顺序表的实现(附完整版代码)](#3. 顺序表的实现(附完整版代码))

[3.1 顺序表结构体声明](#3.1 顺序表结构体声明)

[3.2 初始化&销毁](#3.2 初始化&销毁)

[3.3 插入(尾插、头插、指定位置之前插入)](#3.3 插入(尾插、头插、指定位置之前插入))

[3.4 扩容](#3.4 扩容)

[3.5 删除(尾删、头删、指定位置删除)](#3.5 删除(尾删、头删、指定位置删除))

[3.6 查找(查找数据下标)](#3.6 查找(查找数据下标))

[4. 顺序表实现通讯录(附完整版代码)](#4. 顺序表实现通讯录(附完整版代码))

[4.1 前向声明](#4.1 前向声明)

[4.2 通讯录结构体声明&顺序表结构体修改](#4.2 通讯录结构体声明&顺序表结构体修改)

[4.3 初始化&销毁(直接调用顺序表)](#4.3 初始化&销毁(直接调用顺序表))

[4.4 添加联系人(直接调用顺序表)](#4.4 添加联系人(直接调用顺序表))

[4.5 删除联系人](#4.5 删除联系人)

[4.6 修改联系人信息](#4.6 修改联系人信息)

[4.7 查找联系人](#4.7 查找联系人)

[5. 通讯录保存到文件](#5. 通讯录保存到文件)

[5.1 保存函数和加载函数](#5.1 保存函数和加载函数)

[5.2 通讯录初始化和销毁函数修改](#5.2 通讯录初始化和销毁函数修改)


1. 线性表

线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...

线性表在逻辑上是线性结构 ,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。(物理结构就是物理上的存储结构)

案例:蔬菜分为绿叶类、瓜类、菌菇类。线性表指的是具有部分相同特性的一类数据结构的集合如何理解逻辑结构和物理结构?

2. 顺序表概念及分类

2.1 顺序表的概念

顺序表和数组的区别:顺序表 的底层结构是数组,对数组的封装,实现了常用的增删改查等接口。

顺序表的特性:逻辑结构是连续的,物理结构是连续的(顺序表底层是数组,数组是连续的)。

2.2 顺序表分类

顺序表分为静态顺序表动态顺序表

静态顺序表:使用定长数组存储元素

cpp 复制代码
struct SeqList
{
    int arr[100];// 定长数组
    int size;    // 顺序表当前有效的数据个数
};

动态顺序表:动态内存开启,数组大小可以动态调整。

cpp 复制代码
struct SeqList
{
    int* arr;    // 指向动态内存开辟的空间
    int size;    // 顺序表当前有效的数据个数
    int capacity;// 动态申请的空间大小
};

2.3 动静态顺序表对比

|-------------------------|-----------|
| 静态顺序表 | 动态顺序表 |
| 数组给小了,空间不够用;数组给大了,空间浪费。 | 动态增容 |

动态顺序表相比于静态顺序表有很大优势。

3. 顺序表的实现(附完整版代码)

完整版顺序表实现代码:【免费】顺序表-C语言实现代码资源-CSDN下载

3.1 顺序表结构体声明

顺序表的结构体一般有三个成员变量:存储的数据的类型的指针(重定义以下,方便以后对其他类型使用)、顺序表当前有效数据的个数、动态申请的空间的大小

同时可以给顺序表结构体重定义,方便以后使用。

cpp 复制代码
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int SLDataType;		// 类型重定义,方便以后修改为其他类型的数据

typedef struct SeqList
{
	SLDataType* arr;// 指向动态内存开辟的空间
	int size;		// 顺序表当前有效的数据个数
	int capacity;	// 动态申请的空间大小
}SL;			// 重定义结构体类型名,方便以后使用

3.2 初始化&销毁

cpp 复制代码
// 顺序表初始化
void SLInit(SL* ps)
{
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

// 顺序表销毁
void SLDestory(SL* ps)	
{
	if (ps->arr)
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

3.3 插入(尾插、头插、指定位置之前插入)

顺序表插入分为:尾插、头插、指定位置之前插入

每次插入之前要判断现有空间是否足够,不够时要进行扩容,见3.4节。

实现了SLInsert指定位置之前插入之后,可以直接复用到尾插、头插中。

注意:

  1. 插入时要对传入的指针判断,避免传入指针为空指针,可以用assert断言。

  2. 指定位置之前插入的形参pos要进行判断,插入的位置是否在顺序表内部。如果不在内部执行插入时可能会越界访问导致程序出错。

cpp 复制代码
//顺序表尾插
void SLPushBack(SL* ps, SLDataType x)	
{
	assert(ps);		// 避免这种情况的发生SLPushBack(NULL, 2);
	SLCheckCapacity(ps);
	ps->arr[ps->size++] = x;
}

//顺序表头插
void SLPushFront(SL* ps, SLDataType x)	
{
	assert(ps);		// 避免这种情况的发生SLPushBack(NULL, 2);
	SLCheckCapacity(ps);
	// 顺序表中的内容整体往后挪动一位
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[0] = x;
	ps->size++;
	//SLInsert(ps, 0, x);
}

//指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);		// 避免这种情况的发生SLPushBack(NULL, 2);
	assert(pos >= 0 && pos <= ps->size);	// 确保pos在ps->arr的里面
	SLCheckCapacity(ps);
	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

3.4 扩容

在顺序表空间不够时需要扩容,每一种插入都要判断扩容,所以将扩容单独写成一个函数。

扩容的规则:动态增容**,一般以2倍或3倍的形式增加。**

cpp 复制代码
// 扩容
void SLCheckCapacity(SL* ps)	
{
	if (ps->size == ps->capacity)
	{	// 申请空间
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		int* tmp = (SLDataType*)realloc(ps->arr, sizeof(SLDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc");
			exit(1);	// 直接退出程序,不在继续执行
		}
		ps->arr = tmp;
		ps->capacity = newCapacity;
	}
}

3.5 删除(尾删、头删、指定位置删除)

顺序表的删除分为:尾删、头删、指定位置删除。

注意:

  1. 头删、尾删要保证顺序表不为空。

  2. 指定位置删除,要保证pos在顺序表内部,否则可能出现越界访问导致程序运行出错。

cpp 复制代码
// 尾删
void SLPopBack(SL* ps)	
{
	assert(ps);
	assert(ps->size);//保证顺序表不为空
	ps->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];
	}
	ps->size--;
}

//指定位置删除数据
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >=0 && pos < ps->size);
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

3.6 查找(查找数据下标)

没查找到时,确保返回值不在ps->size内部。

cpp 复制代码
// 查找数据的位置
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
			return i;
	}
	return -1;
}

4. 顺序表实现通讯录(附完整版代码)

完整通讯录实现代码:【免费】通讯录程序实现-C语言顺序表实现资源-CSDN下载

4.1 前向声明

4.1.1 前向声明是什么?

定义:在类型完整定义出现之前,先声明其存在的机制

本质:告诉编译器"这个类型存在,具体细节稍后提供"

前向声明 是C/C++解决类型声明与定义分离的核心机制,通过允许"先声明后定义":

  1. 解决了头文件循环依赖问题

  2. 实现了接口与实现的分离

  3. 提高了编译效率和代码可维护性

cpp 复制代码
C 语言允许 只声明一个结构体类型而不提供完整定义(称为"不完全类型"或"前向声明")。

typedef struct SeqList Contact; 只是告诉编译器:

struct SeqList 是一个合法的类型(但暂时不知道它的具体内容)。

Contact 是它的别名。

编译器不需要知道 struct SeqList 的完整定义,只要知道它是一个有效的类型名即可。

4.1.2 前向声明的关键特性

|------------|----------------------|-------------------------|
| 特性 | 说明 | 示例 |
| 不完整类型 | 编译器知道类型存在,但不知道其大小和结构 | struct SeqList; |
| 延迟定义 | 具体定义可以稍后提供 | Contact.h声明→SeqList.h定义 |
| 指针友好 | 可以创建指向该类型的指针 | Contact* ptr; |
| 成员不可访问 | 不能访问结构体成员 | c.size = 10; // 会报错 |

4.1.3 为什么这种设计有效?

单向依赖:

SeqList.h → 需要 Contact.h(因使用 peoInfo)

Contact.h → 不需要 SeqList.h(只需声明 struct SeqList 存在)

编译过程:

编译 Contact.h 时:知道 struct SeqList 是合法类型名

编译 SeqList.h 时:已获得 peoInfo 的完整定义

最终使用者(如 main.c)同时包含两者,获得完整信息

4.1.4 前向声明使用规则

1. 允许的操作

✅ 仅需类型名(不访问成员)

✅ 定义类型别名:typedef struct S T;

✅ 函数参数/返回值:void func(struct S*);

✅ 声明指针/引用:struct S* p;

2. 禁止的操作

❌ 实例化对象:struct S obj;

❌ 访问成员:p->member = 0;

❌ 计算大小:sizeof(struct S)

❌ 非指针类型成员:struct X { struct S s; };

3. 核心原则

只声明类型存在,不提供结构细节

必须在使用前提供完整定义

主要用于打破头文件循环依赖

4. 典型用途

解决相互依赖:A.h 和 B.h 互相引用时

隐藏实现:接口声明指针,实现文件定义结构体

减少编译依赖:避免不必要头文件包含

4.1.5 Contach.h 和 SeqList.h 头文件包含问题

✅ Contact.h 不需要包含 SeqList.h

因为 typedef struct SeqList Contact; 只是声明,不涉及 struct SeqList 的成员访问。

✅ SeqList.h 必须包含 Contact.h

因为它用到了 peoInfo 的具体定义(typedef peoInfo SLDataType;)。

4.2 通讯录结构体声明&顺序表结构体修改

1. 顺序表头文件修改(arr数组修改为存入数据的类型即可)

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include"Contact.h"

//typedef int SLDataType;		// 类型重定义,方便以后修改为其他类型的数据
typedef peoInfo SLDataType;
// 动态顺序表
typedef struct SeqList
{
	SLDataType* arr;// 指向动态内存开辟的空间
	int size;		// 顺序表当前有效的数据个数
	int capacity;	// 动态申请的空间大小
}SL;			// 重定义结构体类型名,方便以后使用

2.通讯录结构体声明(注意前向声明,见4.1详解)

cpp 复制代码
// 宏定义通讯录成员数组的最大长度
#define NAME_MAX 20
#define GENDER_MAX 10
#define TEL_MAX 20
#define ADDR_MAX 100

// 通讯录结构体声明
typedef struct personInfo
{
	char name[NAME_MAX];
	char gender[GENDER_MAX];
	int age;
	char tel[TEL_MAX];
	char addr[ADDR_MAX];
}peoInfo;

// 要用到顺序表相关的方法,对通讯录的实际操作就是对顺序表进行操作
// 给顺序表起个名字,叫做通讯录
// 前向声明
typedef struct SeqList Contact;

4.3 初始化&销毁(直接调用顺序表)

cpp 复制代码
//初始化通讯录
void ContactInit(Contact* con)
{
	// 实际上要进行的是顺序表的初始化
	// 顺序表的初始化已经实现好了,直接调用即可
	SLInit(con);
}

//销毁通讯录数据
void ContactDestroy(Contact* con)
{
	SLDestory(con);
}

4.4 添加联系人(直接调用顺序表)

cpp 复制代码
//添加通讯录数据
void ContactAdd(Contact* con)
{
	peoInfo info;
	printf("请输入要添加的联系人姓名:");
	scanf("%s", info.name);

	printf("请输入要添加的联系人性别:");
	scanf("%s", info.gender);

	printf("请输入要添加的联系人年龄:");
	scanf("%d", &info.age);

	printf("请输入要添加的联系人电话:");
	scanf("%s", info.tel);

	printf("请输入要添加的联系人地址:");
	scanf("%s", info.addr);

	// 往通讯录中添加数据
	SLPushBack(con, info);
	printf("添加成功\n\n");
}

4.5 删除联系人

需要确定:按照什么方式查找要删除的数据(此处按照姓名查找,字符串比较要用strcmp,调用FindByName函数找到对应姓名的下标)

找到下标后,调用顺序表的指定位置删除函数。

cpp 复制代码
int FindByName(Contact* con, char* name)
{
	for (int i = 0; i < con->size; i++)
	{
		if (strcmp(con->arr[i].name, name) == 0)
			return i;
	}
	return -1;
}

//删除通讯录数据
void ContactDel(Contact* con)
{
	// 要删除的数据必须存在才可以执行删除操作
	char name[NAME_MAX];
	printf("请输入要删除的数据的姓名:");
	scanf("%s", name);
	int find = FindByName(con, name);

	if (find == -1)
	{
		printf("要删除的数据的姓名不存在!!\n\n");
		return;
	}
	SLErase(con, find);
	printf("删除成功!\n\n");
}

4.6 修改联系人信息

通过FindByName()查找到要修改的联系人姓名的下标之后,直接scanf修改。

cpp 复制代码
//修改通讯录数据
void ContactModify(Contact* con)
{
	char name[NAME_MAX];
	printf("请输入要修改的数据的姓名:");
	scanf("%s", name);

	int find = FindByName(con, name);
	if (find == -1)
	{
		printf("要修改的数据的姓名不存在!!\n\n");
		return;
	}
	printf("请输入新的姓名:");
	scanf("%s", con->arr[find].name);

	printf("请输入新的性别:");
	scanf("%s", con->arr[find].gender);

	printf("请输入新的年龄:");
	scanf("%d", &con->arr[find].age);

	printf("请输入新的电话:");
	scanf("%s", con->arr[find].tel);

	printf("请输入新的住址:");
	scanf("%s", con->arr[find].addr);

	printf("修改成功!\n\n");
}

4.7 查找联系人

调用FindByName函数,找到下标后打印对应的联系人信息。

cpp 复制代码
//查找通讯录数据
void ContactFind(Contact* con)
{
	char name[NAME_MAX];
	printf("请输入要查找的数据的姓名:");
	scanf("%s", name);

	int find = FindByName(con, name);
	if (find == -1)
	{
		printf("要查找的数据的姓名不存在!!\n\n");
		return;
	}

	// 姓名  性别  年龄  电话  地址
	printf("找到了!!\n");
	printf("%5s %5s %5s %5s %5s\n", "姓名", "性别", "年龄", "电话", "地址");
	printf("%4s %5s %5d %5s %5s\n\n",
		con->arr[find].name,
		con->arr[find].gender,
		con->arr[find].age,
		con->arr[find].tel,
		con->arr[find].addr);
}

5. 通讯录保存到文件

5.1 保存函数和加载函数

注意,加载函数中的添加通讯录数据是用的 SLPushBack() 函数。

cpp 复制代码
//保存通讯录
void ContactSave(Contact* con)
{
	FILE* pf = fopen("contact.txt", "wb");
	if (pf == NULL)
	{
		perror("fopen");
		return;
	}
	// 将通讯录数据写入文件
	for (int i = 0; i < con->size; i++)
	{
		// 此处用二进制写入
		fwrite(con->arr + i, sizeof(peoInfo), 1, pf);
		printf("写入第%d个数据\n", i + 1);
	}
	printf("通讯录数据保存成功!\n");
}

//加载通讯录数据
void ContactLoad(Contact* con)
{
	FILE* pf = fopen("contact.txt", "rb");
	if (pf == NULL)
	{
		perror("fopen");
		return;
	}
	peoInfo info;
	while (fread(&info, sizeof(peoInfo), 1, pf))
	{
		SLPushBack(con, info);
	}
	printf("历史数据导入成功\n");
}

5.2 通讯录初始化和销毁函数修改

初始化需要读取文件中的数据,销毁前需要保存修改后的通讯录数据。

cpp 复制代码
//初始化通讯录
void ContactInit(Contact* con)
{
	// 实际上要进行的是顺序表的初始化
	// 顺序表的初始化已经实现好了,直接调用即可
	SLInit(con);
	ContactLoad(con);  // 读取文件中的通讯录数据
}

//销毁通讯录数据
void ContactDestroy(Contact* con)
{
	ContactSave(con);  // 保存修改后的通讯录数据到文件中
	SLDestory(con);
}