当前位置: 首页 > news >正文

《手搓动态顺序表:从数组到自动扩容的华丽转身》

#新星杯·14天创作挑战营·第15期#

🔥个人主页:月夜的风吹雨 的主页
🎥作者简介: C++研发方向学习者
📖学习专栏:《C语言知识点详解》《VS2022编译器的使用》《数据结构》《算法》
🌄人生格言: 任何一个伟大的思想,都有一个微不足道的开始。
在这里插入图片描述


文章目录

    • 🚀 欢迎来到《C语言数据结构实战》!
    • 一、为什么不用原生数组?
    • 二、结构体定义:搭好地基
    • 三、写骨架:核心函数声明
    • 四、初始化 & 销毁
      • 4.1 初始化:SLInit
      • 4.2 销毁:SLDestroy
    • 五、扩容函数
      • SLCheckCapacity
    • 六、增删改查:核心四件套
      • 6.1 尾插:SLPushBack
      • 6.2 尾删:SLPopBack
      • 6.3 头插:SLPushFront
      • 6.4 任意位置插入:SLInsert
      • 6.5 查找:SLFind
    • 七、SeqList.h、SeqList.c、test.c 全代码
    • 八、实战任务


🚀 欢迎来到《C语言数据结构实战》!

这里是从“数组恐惧症”到“顺序表自由人”的修炼场,也是你面试前必刷的硬核副本。

🔍 专栏特色:

  • 图解+实战:拒绝纯理论,每行代码都配“人话”解析。
  • 从零到一:手把手带你实现动态顺序表。
  • 避坑指南:那些让你熬夜Debug的坑,我提前给你标好雷区。
  • 真实项目衔接:下一篇直接拿它写通讯录,学完就能用!

💡 学习建议:
1️⃣ 先照着敲一遍(搞崩了也没关系,C语言的浪漫就是Segmentation fault)
2️⃣ 对照注释理解每一行
3️⃣ 用文末【实战任务】巩固肌肉记忆

📌 C语言圈内名言:

“顺序表不是背出来的,是在一次次‘数组越界’中调试出来的!”


一、为什么不用原生数组?

问得好!

原生数组就像你家的老式衣柜:

  • 容量固定:买件大衣?没地方放!
  • 插入麻烦:想在第二件衣服前加一件?后面所有衣服都得挪!
  • 删除尴尬:抽走一件,空位还得手动补!

动态顺序表 = 智能衣柜
✅ 自动扩容
✅ 插入删除一键搞定
✅ size/capacity 自动记账

一句话总结:

会写动态顺序表,是判断你能不能搞定“真实项目”的第一块试金石。


二、结构体定义:搭好地基

先看核心结构:

#define INIT_CAPACITY 4  // 初始容量,你说了算typedef int SLDataType;  // 存int,想存结构体?直接改这行!typedef struct SeqList
{SLDataType* a;     // 指向动态数组的指针int size;          // 当前有效数据个数int capacity;      // 当前总容量
} SL;

📌 关键点:

  • a 是真正在上开辟的数组(堆上开辟的动态内存的空间不随函数的销毁而返回操作系统,而是通过free释放)。
  • sizecapacity 是两个“记账本”,缺一不可。
    在这里插入图片描述

🔍这里我们可以看出size刚好在最后一个有效位置之后便于我们以后的插入和修改


三、写骨架:核心函数声明

接下来,我们把所有核心函数的“空壳子”先声明出来。你只需要把这些代码声明到你的 .h 头文件里,然后在 .c 文件中去实现它们的具体逻辑。
在这里插入图片描述
✍️废话不多说,接下来我们来实现SeqList.h头文件的函数声明

// ================= 初始化 & 销毁 =================
// 初始化顺序表
void SLInit(SL* ps);   // 销毁顺序表,释放内存
void SLDestroy(SL* ps);// 打印所有数据(调试用)
void SLPrint(SL* ps);// ================= 核心:自动扩容 =================
// 检查容量,如果满了就自动扩容
// 这是顺序表的灵魂!所有插入操作前都要调用它
void SLCheckCapacity(SL* ps);// ================= 尾部操作 =================
// 在末尾插入数据
void SLPushBack(SL* ps, SLDataType x);// 删除末尾的数据
void SLPopBack(SL* ps);// ================= 头部操作 =================
// 在头部插入数据(所有元素后移)
void SLPushFront(SL* ps, SLDataType x);// 删除头部的数据(所有元素前移)
void SLPopFront(SL* ps);// ================= 随机位置操作 =================
// 在指定位置 pos 插入数据
void SLInsert(SL* ps, int pos, SLDataType x);// 删除指定位置 pos 的数据
void SLErase(SL* ps, int pos);// 查找数据 x,返回其下标,没找到返回 -1
int SLFind(SL* ps, SLDataType x);

✨接下来就是这些核心函数的实现了。


四、初始化 & 销毁

4.1 初始化:SLInit

void SLInit(SL* ps)
{assert(ps); // 防御式编程第一步ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);if (ps->a == NULL) {perror("malloc failed");exit(-1);}ps->size = 0;ps->capacity = INIT_CAPACITY;
}

❗ 注意事项:

  • assert(ps):防止传入空指针,程序秒崩。
  • malloc 必须检查返回值,一但开辟失败你不进行检查所有数据将会丢失
  • sizecapacity 必须初始化,不然就是随机值。
  • INIT_CAPACITY是在头文件进行宏定义,代表初始化开辟的空间

4.2 销毁:SLDestroy

void SLDestroy(SL* ps)
{assert(ps);free(ps->a);      // 释放堆内存ps->a = NULL;     // 防野指针ps->size = 0;ps->capacity = 0;
}

❗ 注意事项:

  • free 之后必须 = NULL,否则你的ps->a变为野指针。
  • 即使 ps->aNULLfree(NULL) 也是安全的,但置空操作不能省。

五、扩容函数

SLCheckCapacity

void SLCheckCapacity(SL* ps)
{assert(ps);if (ps->size == ps->capacity) {// 扩容为原来的2倍int newCapacity = ps->capacity * 2;SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * newCapacity);if (tmp == NULL) {perror("realloc failed");exit(-1);}ps->a = tmp;        // 更新指针ps->capacity = newCapacity; // 更新容量}
}

❗ 注意事项:

  • 所有插入函数第一行必须调用它!
  • 必须用临时指针 tmp,防止 realloc 失败导致原指针丢失,内存泄漏。
  • 扩容策略:通常选“倍增”(一般扩容2~3倍),时间与空间的完美平衡。

六、增删改查:核心四件套

6.1 尾插:SLPushBack

void SLPushBack(SL* ps, SLDataType x)
{assert(ps);SLCheckCapacity(ps); // 第一步!永远的第一步!ps->a[ps->size] = x;ps->size++;
}

❗ 注意事项:

  • 顺序不能错:先扩容,再赋值,最后 size++

6.2 尾删:SLPopBack

void SLPopBack(SL* ps)
{assert(ps);assert(ps->size > 0); // 防止对空表删除ps->size--; // 逻辑删除,物理上数据还在,但被“隐藏”了
}

❗ 注意事项:

  • 不需要移动任何数据,效率 O(1)最安全的删除方式
    • 因为有效数据只会读到size下标的前一个
      在这里插入图片描述

6.3 头插:SLPushFront

void SLPushFront(SL* ps, SLDataType x)
{assert(ps);SLCheckCapacity(ps);// 从最后一个元素开始,逐个后移for (int i = ps->size; i > 0; i--) {ps->a[i] = ps->a[i - 1];}ps->a[0] = x;ps->size++;
}

❗ 注意事项:

  • 移动数据必须从后往前,避免数据覆盖。
  • 时间复杂度 O(n),数据量大时慎用。
    在这里插入图片描述

6.4 任意位置插入:SLInsert

void SLInsert(SL* ps, int pos, SLDataType x)
{assert(ps);assert(pos >= 0 && pos <= ps->size); // pos合法性检查SLCheckCapacity(ps);// 从最后一个元素开始,逐个后移for (int i = ps->size; i > pos; i--) {ps->a[i] = ps->a[i - 1];}ps->a[pos] = x;ps->size++;
}

❗ 注意事项:

  • pos 的合法范围是 [0, size]pos == size 等价于尾插。
  • 同样,移动数据要从后往前

在这里插入图片描述


6.5 查找:SLFind

int SLFind(SL* ps, SLDataType x)
{assert(ps);for (int i = 0; i < ps->size; i++) {if (ps->a[i] == x) {return i; // 找到,返回下标}}return -1; // 未找到
}

❗ 注意事项:

  • 别忘了 return -1!因为我们需要返回找不到的情况。
  • 时间复杂度 O(n),大数据量考虑哈希表优化。

七、SeqList.h、SeqList.c、test.c 全代码

SeqList.h

#pragma once
#define INIT_CAPACITY 4
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>// 1. 定义你要存的数据类型
// 比如存整数,就用 int;存通讯录联系人,可以改成 struct Contact
typedef int SLDataType;// 2. 定义顺序表结构体
typedef struct SeqList
{SLDataType* a;     // 指向动态数组的指针(这就是我们的“盒子”本体)int size;          // 当前盒子里有多少个有效数据int capacity;      // 当前盒子总共能装多少个数据(容量)
} SL; // 起个短点的别名,后面用着方便// ================= 初始化 & 销毁 =================
// 初始化顺序表
void SLInit(SL* ps);   // 销毁顺序表,释放内存
void SLDestroy(SL* ps);// 打印所有数据(调试用)
void SLPrint(SL* ps);// ================= 核心:自动扩容 =================
// 检查容量,如果满了就自动扩容
// 这是顺序表的灵魂!所有插入操作前都要调用它
void SLCheckCapacity(SL* ps);// ================= 尾部操作 =================
// 在末尾插入数据
void SLPushBack(SL* ps, SLDataType x);// 删除末尾的数据
void SLPopBack(SL* ps);// ================= 头部操作 =================
// 在头部插入数据(所有元素后移)
void SLPushFront(SL* ps, SLDataType x);// 删除头部的数据(所有元素前移)
void SLPopFront(SL* ps);// ================= 随机位置操作 =================
// 在指定位置 pos 插入数据
void SLInsert(SL* ps, int pos, SLDataType x);// 删除指定位置 pos 的数据
void SLErase(SL* ps, int pos);// 查找数据 x,返回其下标,没找到返回 -1
int SLFind(SL* ps, SLDataType x);

SeqList.c

// ============= 动态顺序表实现 =============
#define  _CRT_SECURE_NO_WARNINGS  1
#include "SeqList.h"// 初始化
void SLInit(SL* ps)
{assert(ps);ps->a = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);if (ps->a == NULL){perror("malloc failed");exit(-1);}ps->size = 0;ps->capacity = INIT_CAPACITY;
}// 销毁
void SLDestroy(SL* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->size = 0;ps->capacity = 0;
}// 打印
void SLPrint(SL* ps)
{assert(ps);for (int i = 0; i < ps->size; i++){printf("%d ", ps->a[i]);}printf("\n");
}// 检查容量并扩容
void SLCheckCapacity(SL* ps)
{assert(ps);if (ps->size == ps->capacity){int newCapacity = ps->capacity * 2;SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * newCapacity);if (tmp == NULL){perror("realloc failed");exit(-1);}ps->a = tmp;ps->capacity = newCapacity;}
}// 尾部插入
void SLPushBack(SL* ps, SLDataType x)
{assert(ps);SLCheckCapacity(ps);ps->a[ps->size] = x;ps->size++;
}// 尾部删除
void SLPopBack(SL* ps)
{assert(ps);assert(ps->size > 0);ps->size--;
}// 头部插入
void SLPushFront(SL* ps, SLDataType x)
{assert(ps);SLCheckCapacity(ps);// 从后往前挪动数据for (int i = ps->size; i > 0; i--){ps->a[i] = ps->a[i - 1];}ps->a[0] = x;ps->size++;
}// 头部删除
void SLPopFront(SL* ps)
{assert(ps);assert(ps->size > 0);// 从前往后挪动数据for (int i = 0; i < ps->size - 1; i++){ps->a[i] = ps->a[i + 1];}ps->size--;
}// 在指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x)
{assert(ps);assert(pos >= 0 && pos <= ps->size); // pos == size 是尾插SLCheckCapacity(ps);// 从最后一个元素开始,逐个后移for (int i = ps->size; i > pos; i--){ps->a[i] = ps->a[i - 1];}ps->a[pos] = x;ps->size++;
}// 删除指定位置的元素
void SLErase(SL* ps, int pos)
{assert(ps);assert(pos >= 0 && pos < ps->size); // pos 必须小于 size// 从 pos 位置开始,逐个前移for (int i = pos; i < ps->size - 1; i++){ps->a[i] = ps->a[i + 1];}ps->size--;
}// 查找元素,返回下标,未找到返回 -1
int SLFind(SL* ps, SLDataType x)
{assert(ps);for (int i = 0; i < ps->size; i++){if (ps->a[i] == x){return i;}}return -1;
}

test.c

#define  _CRT_SECURE_NO_WARNINGS  1
#include "SeqList.h"// ============= 测试代码 =============
void TestSeqList()
{SL sl;SLInit(&sl);// 测试尾插SLPushBack(&sl, 1);SLPushBack(&sl, 2);SLPushBack(&sl, 3);SLPrint(&sl); // 预期输出: 1 2 3// 测试头插SLPushFront(&sl, 0);SLPrint(&sl); // 预期输出: 0 1 2 3// 测试任意位置插入SLInsert(&sl, 2, 99);SLPrint(&sl); // 预期输出: 0 1 99 2 3// 测试查找int pos = SLFind(&sl, 99);printf("99 is at position: %d\n", pos); // 预期输出: 2// 测试删除SLErase(&sl, 2);SLPrint(&sl); // 预期输出: 0 1 2 3// 测试头删和尾删SLPopFront(&sl);SLPopBack(&sl);SLPrint(&sl); // 预期输出: 1 2SLDestroy(&sl);
}int main()
{TestSeqList();return 0;
}

✅ 代码全在这儿了,一个字符都不差。
⌨️ 别光看,打开你的编译器,照着——敲——一——遍!

知道你心里在想:
💬 “复制粘贴多快啊,何必自己敲?”

但——信我一次!
✋ 亲手敲代码 和 Ctrl+C / Ctrl+V
是两种完全不同的世界!

你会在敲的过程中:
🐞 发现括号没配对
🐞 发现分号漏了
🐞 发现变量名拼错了
🐞 甚至手滑把 == 敲成 =
……

这些看似“浪费时间”的 Debug 过程,
💥 恰恰是你肌肉记忆形成的关键!
💥 是你真正内化知识的必经之路!

✨ 敲完、编译、运行、跑通的那一刻——
那种从指尖传到心里的成就感,
🏆 比刷十篇教程、看一百个视频都爽!

求求你了


八、实战任务

🎯 任务1: 把上面所有代码整合到一个项目里,实现一个能存100个整数的顺序表。

🎯 任务2: 在 main 函数里,测试所有操作:初始化、尾插10个数、头插一个数、中间插入一个数、查找、删除、打印、销毁。

🎯 任务3(进阶): 把 SLDataType 改成 struct Student,实现一个学生信息管理系统! (储存学生姓名就行了


把这个顺序表吃透,它就是你C语言进阶路上的第一块基石。
在这里插入图片描述

觉得有用的大佬,点个赞👍支持一下!
关注我 @月夜的风吹雨,下一篇我们用这个顺序表,手把手撸一个【通讯录系统】!

http://www.dtcms.com/a/389401.html

相关文章:

  • 【Day 60】Linux-LVS负载均衡
  • bash zsh sh与shell 有什么关系
  • AI时代格局重构:2025 GEO服务公司Top3
  • GEO 优化重构数字营销格局 孟庆涛技术创新引领行业突破
  • 思迈特 Agent BI 发布,重构数据应用新范式
  • 重构组态软件边界:紫金桥如何实现原生跨平台?
  • 工作中的好奇心:Perplexity CEO的AI时代洞察
  • JsonCpp:高效序列化与反序列化指南
  • (树)Leetcode98二叉搜索树
  • 【笔记】Agent产品案例深度拆解
  • 嵌入式ARM SOC开发中文专题分享二:ARM SOC外围引脚功能说明
  • Python快速入门专业版(三十四):函数实战1:计算器程序(支持加减乘除与括号优先级)
  • datawhale玩转通义四大新模型 202509 第2次作业
  • 【项目实战】知识库——基础理论1
  • ego(7)---为Astar路径点计算交点,避障基准点与排斥方向
  • Oracle体系结构-警报日志文件 (Alert Log File / Alert SID.log)
  • 解锁 TiDB:供应链场景下分布式分库分表的案例
  • 【IEEE出版 | 早鸟优惠开启】人工智能驱动图像处理与计算机视觉技术国际学术研讨会 (AIPCVT 2025)
  • Ubuntu之旅-02 Redis
  • 基于RK3588+FPGA的无人机飞控系统,支持AI算力和FPGA实时性,强大的图像处理能力,支持全国产化
  • 杂七杂八之基于ApiPost的Jar包调用
  • 华为HCCL集合通信库AllGather算子全流程解析
  • uv管理的python项目怎么打包成docker部署发布上线
  • unity之uv编辑
  • 华为电源研发的IPD实践:从概念到生命周期的结构化管控
  • MySQL中什么是回表查询,如何避免和优化?
  • MySql01
  • 2025版基于springboot的旅游门票预定系统
  • 3. Linux 计划任务管理
  • EasyCVR在智慧城市中场景中的核心应用与实践方案