数据结构 - 线性表第二篇:动态顺序表进阶接口实现

前言

接上一篇动态顺序表基础实现,上次完成了初始化、销毁、自动扩容、头尾插入、头尾删除、通用打印核心基础操作,算是搭好了动态顺序表的骨架。

本篇继续完善顺序表进阶功能,手写实现四个高频核心接口:指定位置插入、指定位置删除、按值查找元素、按位置修改元素

初学数据结构一定要慢下来,每一个函数理解底层下标移动逻辑,每一处边界条件自己琢磨为什么要这么限制,比赶进度抄代码收获大得多。

一、上篇简单回顾

上一篇我们实现了顺序表最基础操作:

函数 功能
SLInit 初始化空动态顺序表
Destroy 释放内存、销毁顺序表
SLExp 检测容量,满了自动二倍扩容
PushBack 尾部插入数据
PushFront 头部插入数据
SLDeleteBack 头部删除数据
SLDeleteBehind 尾部删除数据
Print 通用格式化打印函数

本篇新增 4 个进阶接口,补齐顺序表所有基础能力:

新增函数 功能 时间复杂度
SLInsert 在指定下标位置插入元素 O(n)
SLErase 删除指定下标位置的元素 O(n)
SLFind 按数值查找,返回对应下标 O(n)
SLModify 修改指定下标位置的元素值 O(1)

二、进阶接口核心原理

2.1 指定位置插入 SLInsert

  1. 先校验下标合法性:pos >= 0 && pos <= size
  2. 检测容量,不够则自动扩容
  3. 从顺序表末尾开始,把 pos 及往后的元素整体后移一位
  4. 空出 pos 下标,放入新元素
  5. 有效数据个数 size++

2.2 指定位置删除 SLErase

  1. 校验下标合法性:pos >=0 && pos < size
  2. pos 后面的元素整体向前覆盖一位
  3. 有效数据个数 size--
  4. 只做逻辑删除,不手动清空内存,顺序表特性使然

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

===== 顺序表销毁完成 =====

六、学习总结

  1. 顺序表所有增删改查基础接口至此全部完成,从初始化、扩容、头尾操作到指定位置操作,形成完整闭环;
  2. assert 断言是新手必须养成的习惯,防护空指针、非法下标,程序崩溃直接定位问题;
  3. 插入删除核心就是元素下标移动,牢记:插入从后往前移,删除从前往后移;
  4. 坚持多文件拆分:头文件声明、源文件实现、单独测试文件,养成工程化编码思维;
  5. 手写踩坑比看视频记知识点更牢固,每一个 Bug 都是帮自己补齐 C 语言指针、数组、下标短板。

七、后续更新预告

下一篇正式进入实战项目 :基于完整版动态顺序表,手写实现简易通讯录,支持联系人增删改查、查找、修改、保存等功能,把顺序表知识真正落地应用。

初学数据结构的小伙伴可以跟着一起手写,有问题评论区交流,一起稳扎稳打打好基础!

相关推荐
哥本哈士奇2 小时前
LangChain DeepAgents 学习笔记
笔记·学习·langchain
承渊政道2 小时前
【贪心算法】(经典实战应用解析(五):单调递增的数字、坏了的计算器、合并区间、⽆重叠区间、⽤最少数量的箭引爆⽓球)
数据结构·c++·leetcode·贪心算法·排序算法·动态规划·哈希算法
weixin_404679312 小时前
虚幻5 学习笔记
笔记·学习·ue5
念恒123063 小时前
Python(while循环)
数据结构·python·算法
bqq198610263 小时前
Redis持久化
数据结构·数据库·redis·缓存
在坚持一下我可没意见3 小时前
Python 修仙修炼录 08:字典秘境,参悟键值玄机
开发语言·笔记·python·入门·字典
凌波粒3 小时前
深度学习入门(鱼书)第1章笔记——Python 基础
笔记·python·深度学习
一只机电自动化菜鸟3 小时前
一建机电备考笔记(38) 焊接技术—焊接质量检验(含考频+题型)
笔记·学习·职场和发展·生活·学习方法
AI科技星3 小时前
全域粒子质量几何曲率统一公式体系(通俗易懂版)
c语言·开发语言·网络·量子计算·agi