前言
接上一篇动态顺序表基础实现,上次完成了初始化、销毁、自动扩容、头尾插入、头尾删除、通用打印核心基础操作,算是搭好了动态顺序表的骨架。
本篇继续完善顺序表进阶功能,手写实现四个高频核心接口:指定位置插入、指定位置删除、按值查找元素、按位置修改元素
初学数据结构一定要慢下来,每一个函数理解底层下标移动逻辑,每一处边界条件自己琢磨为什么要这么限制,比赶进度抄代码收获大得多。
一、上篇简单回顾
上一篇我们实现了顺序表最基础操作:
| 函数 | 功能 |
|---|---|
| SLInit | 初始化空动态顺序表 |
| Destroy | 释放内存、销毁顺序表 |
| SLExp | 检测容量,满了自动二倍扩容 |
| PushBack | 尾部插入数据 |
| PushFront | 头部插入数据 |
| SLDeleteBack | 头部删除数据 |
| SLDeleteBehind | 尾部删除数据 |
| 通用格式化打印函数 |
本篇新增 4 个进阶接口,补齐顺序表所有基础能力:
| 新增函数 | 功能 | 时间复杂度 |
|---|---|---|
| SLInsert | 在指定下标位置插入元素 | O(n) |
| SLErase | 删除指定下标位置的元素 | O(n) |
| SLFind | 按数值查找,返回对应下标 | O(n) |
| SLModify | 修改指定下标位置的元素值 | O(1) |
二、进阶接口核心原理
2.1 指定位置插入 SLInsert
- 先校验下标合法性:
pos >= 0 && pos <= size - 检测容量,不够则自动扩容
- 从顺序表末尾开始,把
pos及往后的元素整体后移一位 - 空出
pos下标,放入新元素 - 有效数据个数
size++
2.2 指定位置删除 SLErase
- 校验下标合法性:
pos >=0 && pos < size - 把
pos后面的元素整体向前覆盖一位 - 有效数据个数
size-- - 只做逻辑删除,不手动清空内存,顺序表特性使然
2.3 按值查找 SLFind
遍历顺序表数组,匹配到目标值直接返回下标;遍历结束没找到返回 -1,交给调用者判断处理。
2.4 按位置修改 SLModify
校验下标合法后,直接赋值覆盖对应位置元素即可,随机访问特性,时间复杂度 O (1)。
三、手写代码常见 5 个致命 Bug(新手必看)
Bug1:指定插入 pos 断言范围写错
错误写法:
cpp
assert(pos >= 0 && pos < ps->size);
错误原因:插入位置可以是末尾下标 size (相当于尾插),写成 < size 会导致无法在最后位置插入。正确写法:
cpp
assert(pos >= 0 && pos <= ps->size);
Bug2:指定删除 pos 断言越界
错误写法:
cpp
assert(pos >= 0 && pos <= ps->size);
错误原因:删除必须操作已有有效元素 ,最大下标只能是 size-1,等于 size 就是非法越界。正确写法:
cpp
assert(pos >= 0 && pos < ps->size);
Bug3:元素移动循环方向写反
插入时如果从前往后移元素 ,会覆盖还没移动的数据,造成数据丢失。必须从后往前倒着移动元素。
Bug4:查找函数内部强行打印
新手容易把 printf 写死在查找函数里,导致只想获取下标、不想打印时无法复用。规范写法:查找只返回下标,打印交给测试层。
Bug5:修改函数缺少指针和下标校验
不写 assert 防护,传入空指针、非法下标,直接野指针崩溃,健壮性极差。
四、完整多文件最终代码
4.1 SeqList.h 头文件(新增接口声明)
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
// 通用打印格式宏,切换类型只改这一处
#define SLTYPE_PRINT_FMT "%d"
// 顺序表存储类型
typedef int SLType;
// 动态顺序表结构体
typedef struct SeqList
{
SLType* a;
int size; // 有效数据个数
int capacity; // 储存空间总容量
}SL;
// 初始化和销毁
void SLInit(SL* ps);
void Destroy(SL* ps);
void Print(SL* ps);
// 扩容检测
void SLExp(SL* ps);
// 头部操作
void PushFront(SL* ps, SLType x); // 头部插入
void SLPopFront(SL* ps); // 头部删除
// 尾部操作
void PushBack(SL* ps, SLType x); // 尾部插入
void SLPopBack(SL* ps); // 尾部删除
// 进阶接口:新增
int SLFind(SL* ps, SLType x); // 按值查找
void SLInsert(SL* ps, int pos, SLType x); // 指定位置插入
void SLErase(SL* ps, int pos); // 指定位置删除
void SLModify(SL* ps, int pos, SLType x); // 按位置修改
4.2 SeqList.c 功能实现文件(补全进阶函数)
cpp
#include"SeqList.h"
// 初始化顺序表
void SLInit(SL* ps)
{
assert(ps);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
// 销毁顺序表,释放动态内存
void Destroy(SL* ps)
{
assert(ps);
if (ps->a)
{
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
// 检测并自动扩容
void SLExp(SL* ps)
{
assert(ps);
if (ps->capacity == ps->size)
{
int Newspace = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLType* Newps = (SLType*)realloc(ps->a, Newspace * sizeof(SLType));
if (Newps == NULL)
{
perror("Failed to apply for space");
exit(1);
}
ps->a = Newps;
ps->capacity = Newspace;
}
}
// 尾部插入
void PushBack(SL* ps, SLType x)
{
assert(ps);
SLExp(ps);
ps->a[ps->size++] = x;
}
// 头部插入
void PushFront(SL* ps, SLType x)
{
assert(ps);
SLExp(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);
// 元素前移覆盖第一个元素
for (int i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
// 尾部删除
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size);
// 逻辑删除:有效数据个数减1
ps->size--;
}
// 通用打印函数
void Print(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf(SLTYPE_PRINT_FMT " ", ps->a[i]);
}
printf("\n");
}
// 按值查找:返回下标,没找到返回-1
int SLFind(SL* ps, SLType x)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
// 指定位置插入元素
void SLInsert(SL* ps, int pos, SLType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLExp(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);
assert(ps->size);
// 元素从前往后前移覆盖
for (int i = pos; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
// 修改指定位置元素
void SLModify(SL* ps, int pos, SLType x)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
ps->a[pos] = x;
}
4.3 test.c 测试文件(全覆盖测试所有接口)
cpp
#include "SeqList.h"
int main()
{
// 定义顺序表变量
SL sl;
// 初始化
SLInit(&sl);
printf("===== 顺序表初始化完成 =====\n\n");
// 测试尾插
printf("----- 测试尾插 -----\n");
PushBack(&sl, 10);
PushBack(&sl, 20);
PushBack(&sl, 30);
PushBack(&sl, 40);
printf("尾插 10 20 30 40 后:");
Print(&sl);
printf("\n");
// 测试头插
printf("----- 测试头插 -----\n");
PushFront(&sl, 5);
PushFront(&sl, 1);
printf("头插 5 1 后:");
Print(&sl);
printf("\n");
// 测试尾删
printf("----- 测试尾删 -----\n");
SLPopBack(&sl);
SLPopBack(&sl);
printf("尾删2次后:");
Print(&sl);
printf("\n");
// 测试头删
printf("----- 测试头删 -----\n");
SLPopFront(&sl);
printf("头删1次后:");
Print(&sl);
printf("\n");
// 测试指定位置插入
printf("----- 测试指定位置插入 -----\n");
SLInsert(&sl, 1, 99);
printf("在下标1插入99后:");
Print(&sl);
printf("\n");
// 测试按值查找
printf("----- 测试按值查找 -----\n");
int ret = SLFind(&sl, 99);
if (ret != -1)
printf("找到元素99,下标为:%d\n", ret);
else
printf("未找到该元素\n");
printf("\n");
// 测试按位置修改
printf("----- 测试按位置修改 -----\n");
SLModify(&sl, 1, 88);
printf("修改下标1为88后:");
Print(&sl);
printf("\n");
// 测试指定位置删除
printf("----- 测试指定位置删除 -----\n");
SLErase(&sl, 1);
printf("删除下标1元素后:");
Print(&sl);
printf("\n");
// 释放内存
printf("===== 顺序表销毁完成 =====\n");
Destroy(&sl);
return 0;
}
五、编译运行与输出结果
编译命令:
bash
gcc test.c SeqList.c -o SeqList
运行输出:
cpp
===== 顺序表初始化完成 =====
----- 测试尾插 -----
尾插 10 20 30 40 后:10 20 30 40
----- 测试头插 -----
头插 5 1 后:1 5 10 20 30 40
----- 测试尾删 -----
尾删2次后:1 5 10 20
----- 测试头删 -----
头删1次后:5 10 20
----- 测试指定位置插入 -----
在下标1插入99后:5 99 10 20
----- 测试按值查找 -----
找到元素99,下标为:1
----- 测试按位置修改 -----
修改下标1为88后:5 88 10 20
----- 测试指定位置删除 -----
删除下标1元素后:5 10 20
===== 顺序表销毁完成 =====
六、学习总结
- 顺序表所有增删改查基础接口至此全部完成,从初始化、扩容、头尾操作到指定位置操作,形成完整闭环;
assert断言是新手必须养成的习惯,防护空指针、非法下标,程序崩溃直接定位问题;- 插入删除核心就是元素下标移动,牢记:插入从后往前移,删除从前往后移;
- 坚持多文件拆分:头文件声明、源文件实现、单独测试文件,养成工程化编码思维;
- 手写踩坑比看视频记知识点更牢固,每一个 Bug 都是帮自己补齐 C 语言指针、数组、下标短板。
七、后续更新预告
下一篇正式进入实战项目 :基于完整版动态顺序表,手写实现简易通讯录,支持联系人增删改查、查找、修改、保存等功能,把顺序表知识真正落地应用。
初学数据结构的小伙伴可以跟着一起手写,有问题评论区交流,一起稳扎稳打打好基础!