顺序表的应用:通讯录项目与经典算法实战

顺序表的应用:通讯录项目与经典算法实战

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

目录


一、基于动态顺序表实现通讯录项目

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");
}

其他功能(添加、查找、修改、展示)逻辑类似,利用顺序表提供的 PushBackFindAt 等接口完成。

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)

题目 :两个非递减顺序数组 nums1nums2,将 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)


三、顺序表的问题及思考

尽管顺序表简单高效,但存在以下缺陷:

  1. 中间/头部插入删除效率低:需要移动大量元素,时间复杂度 O(n)。
  2. 动态扩容消耗大:每次扩容需申请新空间、拷贝数据、释放旧空间,且通常 2 倍增长可能导致空间浪费(例如扩容到 200 后只用了 105 个,浪费 95 个)。
  3. 不适合频繁插入删除的场景

思考:如何解决?

  • 采用链表(下一讲内容):插入删除只需修改指针,无需移动数据。
  • 采用更合理的扩容策略(如 1.5 倍或按需增长),或预分配足够空间。

总结:本文通过通讯录项目展示了动态顺序表的实际应用,掌握了数据持久化、模块化编程等技巧。同时分析了顺序表相关的经典算法题,并指出了顺序表在中间插入删除和空间浪费上的不足。后续将学习链表,解决这些问题。建议读者动手实现完整的通讯录项目,加深对顺序表的理解。

相关推荐
8Qi81 小时前
LeetCode 583. 两个字符串的删除操作
算法·leetcode·职场和发展·动态规划
tigershang1 小时前
卡尔曼滤波:不确定世界中的最优估计
人工智能·算法·机器学习
一个儒雅随和的男子2 小时前
限流算法详细剖析
java·服务器·算法
工业胶粘剂技术3 小时前
单组分高温环氧结构胶 K-EP280 完整技术参数与工程选型分析
算法·制造
小欣加油4 小时前
Leetcode31 下一个排列
数据结构·c++·算法·leetcode·职场和发展
_日拱一卒4 小时前
LeetCode:39组合总和
java·算法·leetcode·职场和发展
无限进步_4 小时前
【Linux】进程状态、僵尸与孤儿、进程调度
linux·运维·服务器·开发语言·数据结构·算法
郝学胜-神的一滴4 小时前
力扣 662 :二叉树最大宽度
java·数据结构·c++·python·算法·leetcode·职场和发展
2301_764441334 小时前
基于Stackelberg博弈的分散式库存模型
python·算法·数学建模