顺序表的应用:通讯录项目与经典算法实战
顺序表是线性表的一种基础实现。本文将带你基于动态顺序表完成一个完整的通讯录项目(支持增删改查、文件持久化),并分析顺序表的经典算法题(移除元素、合并有序数组),最后探讨顺序表的优缺点及改进方向。
目录
一、基于动态顺序表实现通讯录项目

1. 功能要求
- 至少存储100人的通讯信息(实际动态扩容,不限100)
- 保存用户信息:姓名、性别、年龄、电话、地址
- 支持:增加、删除、查找、修改、显示所有联系人
- 程序结束后数据不丢失(文件持久化)
2. 架构设计
项目分为三个模块:
SeqList.h/c:动态顺序表的通用实现(数据存储与操作)contact.h/c:通讯录业务逻辑(联系人的增删改查、文件读写)test.c:主菜单与程序入口
关键设计 :顺序表存储的数据类型为 PersonInfo 结构体,实现了代码复用。
3. 核心代码解析
3.1 顺序表头文件 SeqList.h
c
#pragma once
#include <stdio.h>
#include <assert.h>
#include "contact.h"
// 将顺序表的数据类型定义为 PersonInfo
typedef struct PersonInfo SQDataType;
// 动态顺序表结构
typedef struct SeqList {
SQDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
} SLT;
// 接口声明
void SeqListInit(SLT* ps);
void SeqListDesTroy(SLT* ps);
void SeqListPrint(SLT s);
void CheckCapacity(SLT* ps);
void SeqListPushBack(SLT* ps, SQDataType x);
void SeqListPushFront(SLT* ps, SQDataType x);
void SeqListPopBack(SLT* ps);
void SeqListPopFront(SLT* ps);
int SeqListFind(SLT* ps, SQDataType x);
void SeqListInsert(SLT* ps, size_t pos, SQDataType x);
void SeqListErase(SLT* ps, size_t pos);
size_t SeqListSize(SLT* ps);
void SeqListAt(SLT* ps, size_t pos, SQDataType x);
3.2 通讯录业务 contact.c
数据结构定义:
c
#define NAME_MAX 100
#define SEX_MAX 4
#define TEL_MAX 11
#define ADDR_MAX 100
typedef struct PersonInfo {
char name[NAME_MAX];
char sex[SEX_MAX];
int age;
char tel[TEL_MAX];
char addr[ADDR_MAX];
} PeoInfo;
文件持久化 :程序启动时从 contact.txt 加载历史数据,退出时自动保存。
c
// 加载历史数据
void LoadContact(SLT* con) {
FILE* pf = fopen("contact.txt", "rb");
if (pf == NULL) return;
PeoInfo info;
while (fread(&info, sizeof(PeoInfo), 1, pf)) {
SeqListPushBack(con, info);
}
fclose(pf);
printf("历史数据加载成功!\n");
}
// 保存数据到文件
void SaveContact(SLT* con) {
FILE* pf = fopen("contact.txt", "wb");
if (pf == NULL) {
perror("fopen error");
return;
}
for (int i = 0; i < con->size; i++) {
fwrite(con->a + i, sizeof(PeoInfo), 1, pf);
}
fclose(pf);
printf("通讯录数据保存成功!\n");
}
注意 :使用二进制读写("rb"/"wb")可以一次性保存整个结构体,简单高效。
增删改查实现(以删除为例):
c
int FindByName(SLT* con, char name[]) {
for (int i = 0; i < con->size; i++) {
if (strcmp(con->a[i].name, name) == 0)
return i;
}
return -1;
}
void DelContact(SLT* con) {
char name[NAME_MAX];
printf("请输入要删除的姓名:");
scanf("%s", name);
int pos = FindByName(con, name);
if (pos < 0) {
printf("用户不存在,删除失败!\n");
return;
}
SeqListErase(con, pos);
printf("删除成功!\n");
}
其他功能(添加、查找、修改、展示)逻辑类似,利用顺序表提供的 PushBack、Find、At 等接口完成。
3.3 主菜单 test.c
c
int main() {
SLT con;
InitContact(&con); // 内部调用 SeqListInit 和 LoadContact
int op = -1;
do {
// 打印菜单...
scanf("%d", &op);
switch (op) {
case 1: AddContact(&con); break;
case 2: DelContact(&con); break;
case 3: FindContact(&con); break;
case 4: ModifyContact(&con); break;
case 5: ShowContact(&con); break;
default: break;
}
} while (op != 0);
DestroyContact(&con); // 内部调用 SaveContact 和 SeqListDestroy
return 0;
}
二、顺序表经典算法
1. 移除元素(LeetCode 27)
题目 :给你一个数组 nums 和一个值 val,原地 移除所有数值等于 val 的元素,返回新长度。
思路 :双指针法。一个指针 src 遍历数组,另一个指针 dst 指向待插入位置。当 nums[src] != val 时,将其复制到 nums[dst++]。
c
int removeElement(int* nums, int numsSize, int val) {
int src = 0, dst = 0;
while (src < numsSize) {
if (nums[src] != val) {
nums[dst++] = nums[src];
}
src++;
}
return dst;
}
时间复杂度 O(n),空间复杂度 O(1)
2. 合并两个有序数组(LeetCode 88)
题目 :两个非递减顺序数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使结果有序。nums1 有足够空间(大小为 m+n)。
思路 :从后向前比较,将较大的元素放到 nums1 的末尾。
c
void merge(int* nums1, int m, int* nums2, int n) {
int i = m - 1, j = n - 1, k = m + n - 1;
while (i >= 0 && j >= 0) {
if (nums1[i] > nums2[j])
nums1[k--] = nums1[i--];
else
nums1[k--] = nums2[j--];
}
while (j >= 0) {
nums1[k--] = nums2[j--];
}
}
从后向前可以避免覆盖未处理的元素,时间复杂度 O(m+n)
三、顺序表的问题及思考
尽管顺序表简单高效,但存在以下缺陷:
- 中间/头部插入删除效率低:需要移动大量元素,时间复杂度 O(n)。
- 动态扩容消耗大:每次扩容需申请新空间、拷贝数据、释放旧空间,且通常 2 倍增长可能导致空间浪费(例如扩容到 200 后只用了 105 个,浪费 95 个)。
- 不适合频繁插入删除的场景。
思考:如何解决?
- 采用链表(下一讲内容):插入删除只需修改指针,无需移动数据。
- 采用更合理的扩容策略(如 1.5 倍或按需增长),或预分配足够空间。
总结:本文通过通讯录项目展示了动态顺序表的实际应用,掌握了数据持久化、模块化编程等技巧。同时分析了顺序表相关的经典算法题,并指出了顺序表在中间插入删除和空间浪费上的不足。后续将学习链表,解决这些问题。建议读者动手实现完整的通讯录项目,加深对顺序表的理解。