数据结构 顺序表(2)---顺序表的实现

1.顺序表的实现

为了能够实现顺序表这一个数据结构,小编是分别分为三个文件编写完成的。分别是一个头文件

(.h),一个实现文件(.c),一个测试文件(.c) 。

以下对这三个模块(头文件、顺序表实现文件、测试文件 )的代码,按功能模块、函数逻辑等进

行详细解释,帮助理解动态顺序表的完整实现:

1.1 头文件( SeqList.h )

头文件( SeqList.h )—— 接口定义与类型声明:

#include
#include
#include

// 定义动态顺序表存储的数据类型,这里用 int 举例,可按需修改
typedef int SLDataType;
// 定义顺序表结构体
typedef struct SeqList
{
    SLDataType* arr;  // 指向动态分配内存的指针,用于存储数据元素
    int size;         // 当前顺序表中有效数据元素的个数
    int capacity;     // 顺序表当前已分配的空间容量(能存储的最大元素个数)
}SL;

// 函数声明,对外提供操作顺序表的接口

// 打印顺序表中元素
void SLPrint(SL* ps);
// 初始化顺序表
void SLInit(SL* ps);
// 销毁顺序表(头文件中原本注释了,实际建议保留正确声明 )
void SLDestroy(SL* ps);

// 尾插:在顺序表末尾添加元素
void SLPushBack(SL* ps, SLDataType x);
// 头插:在顺序表开头添加元素
void SLPushFront(SL* ps, SLDataType x);
// 尾删:删除顺序表末尾元素
void SLPopBack(SL* ps);
// 头删:删除顺序表开头元素
void SLPopFront(SL* ps);

// 在指定位置 pos 之前插入数据(pos 从 0 开始计数 )
void SLInsert(SL* ps, int pos, SLDataType x);
// 删除指定位置 pos 的数据(pos 从 0 开始计数 )
void SLErase(SL* ps, int pos);
// 查找指定数据 x 在顺序表中的位置,找到返回下标,没找到返回 -1
int SLFind(SL* ps, SLDataType x);

作用与意义:
 
- 类型与结构定义:通过 typedef  定义了存储数据的类型  SLDataType  和顺序表结构体  SL ,让

代码后续修改数据类型更方便,也清晰描述了顺序表的组成(数据存储指针、有效元素个数、容量

)。

- 接口声明:把操作顺序表的函数以声明形式列出,相当于给其他文件(如测试文件)提供“使用手

册”,其他文件只要包含这个头文件,就能调用这些函数操作顺序表,实现了接口封装,隐藏了内

部实现细节。

1.2 实现文件( SeqList.c )

顺序表实现文件( SeqList.c )—— 接口函数具体实现:

1. 初始化函数 SLInit:

1. 初始化函数  SLInit 
 
void SLInit(SL* ps)
{
    ps->arr = NULL;    // 初始化时,动态数组指针置空,还未分配内存
    ps->size = 0;      // 有效元素个数初始为 0
    ps->capacity = 0;  // 容量初始为 0
}

逻辑:

将顺序表的动态数组指针、有效元素个数、容量都初始化为初始状态,准备好后续使用。

用这个函数后,顺序表就可以开始被操作(插入元素时会触发扩容分配内存 )。

2. 检查容量函数 SLCheckCapacity

void SLCheckCapacity(SL* ps)
{
    // 当有效元素个数等于容量时,说明空间已满,需要扩容
    if (ps->size == ps->capacity)
    {
        // 确定新容量:如果原来容量是 0 (刚初始化还没分配过空间), 就设为 4;否则容量翻倍
        int NewCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
        // 调用realloc扩容,realloc 会尝试在原内存地址后扩展空间,如果不行则重新找一块新内存并拷贝原数据
        // 这里第二个参数是新容量乘以每个元素的大小,单位是字节
        SLDataType* tmp = (SLDataType*)realloc(ps->arr, NewCapacity * sizeof(SLDataType));
        if (tmp == NULL)  // 如果内存分配失败(如内存不足)
        {
            perror("realloc fail!");  // 打印错误信息
            exit(1);  // 直接终止程序,实际项目中可考虑更优雅的错误处理
        }
        ps->arr = tmp;  // 让顺序表的数组指针指向新分配的内存
        ps->capacity = NewCapacity;  // 更新容量为新容量
    }
}

逻辑:

作为顺序表插入操作的“幕后支撑”,在每次插入元素前(或需要时)检查空间是否足够。空

间不足时,通过 realloc 动态扩容,保证顺序表能继续存储新元素,是实现动态顺序表的关键

逻辑。

3. 打印函数 SLPrint

void SLPrint(SL* ps)
{
    for (int i = 0;i < ps->size;i++)
    {
        printf("%d ", ps->arr[i]);  // 逐个打印顺序表中的有效元素
    }
    printf("\n");  // 打印完换行,让输出更整洁
}

逻辑:

遍历顺序表的有效元素(依据  size  确定遍历范围 ),逐个打印,方便查看顺序表当前存储

的数据内容。

4. 尾插函数 SLPushBack

void SLPushBack(SL* ps, SLDataType x)
{
    assert(ps != NULL);  // 断言检查,确保传入的顺序表指针不为空,否则程序崩溃提示
    SLCheckCapacity(ps);  // 先检查空间,不够就扩容
    ps->arr[ps->size] = x;  // 把新元素放到当前有效元素的末尾位置(下标为 size 处 )
    ps->size++;  // 有效元素个数加 1
}

逻辑:

在顺序表末尾添加元素,先通过 SLCheckCapacity 确保有空间,然后把元素放到合适位置,

更新有效元素个数,是最基础的插入操作之一。

5. 头插函数 SLPushFront

void SLPushFront(SL* ps, SLDataType x)
{
    assert(ps != NULL);  // 检查顺序表指针有效性
    SLCheckCapacity(ps);  // 检查空间
    // 从后往前,把原有元素依次向后挪动一位,为新元素腾出开头位置
    for (int i = ps->size;i > 0;i--)
    {
        ps->arr[i] = ps->arr[i - 1];
    }
    ps->arr[0] = x;  // 把新元素放到开头位置
    ps->size++;  // 有效元素个数加 1
}

逻辑:

要在顺序表开头插入元素,需要先把原有元素整体向后“挪位”,空出第一个位置放新元素,

操作过程中要注意循环的方向(从后往前挪,避免元素被覆盖 ),这也导致头插的时间复杂

度是 O(n) ( n  是当前有效元素个数 )。

6. 尾删函数 SLPopBack

void SLPopBack(SL* ps)
{
    // 断言检查:顺序表指针不为空,且有效元素个数大于 0(否则没法删 )
    assert(ps && ps->size);  
    ps->size--;  // 直接把有效元素个数减 1,逻辑上“删除”最后一个元素(后续插入会覆盖 )
}

逻辑:

实现简单的尾删,通过减少 size ,  让后续操作不再访问最后一个元素,达到逻辑删除的效

果,时间复杂度是 O(1),因为不需要移动其他元素。

7. 头删函数  SLPopFront 
 

void SLPopFront(SL* ps)
{
    assert(ps && ps->size);  // 检查有效性
    // 把从第二个元素开始的所有元素,依次向前挪动一位,覆盖第一个元素,达到删除开头的效果
    for (int i = 0;i < ps->size - 1;i++)
    {
        ps->arr[i] = ps->arr[i + 1];
    }
    ps->size--;  // 有效元素个数减 1
}

逻辑:

删除开头元素,需要把后面的元素依次向前移动,填补被删除的位置,时间复杂度是 O(n)

( n  是有效元素个数 ),因为要移动多个元素。

8. 指定位置pos插入函数  SLInsert 
 

void SLInsert(SL* ps, int pos, SLDataType x)
{
    assert(ps);  // 检查顺序表指针
    // 检查插入位置合法性:pos 要在 0 到 size 之间(可以插入到最后一个元素之后,相当于尾插 )
    assert(pos >= 0 && pos <= ps->size);  

    SLCheckCapacity(ps);  // 检查空间
    // 从 pos 位置开始,把后面的元素依次向后挪动一位,为新元素腾出位置
    for(int i = ps->size;i > pos;i--)
    {
        ps->arr[i] = ps->arr[i - 1];
    }
    ps->arr[pos] = x;  // 在 pos 位置放入新元素
    ps->size++;  // 有效元素个数加 1
}

逻辑:


是比较通用的插入操作,可在任意合法位置(包括开头、中间、末尾 )插入元素。核心是通

过循环移动元素腾出位置,时间复杂度取决于移动元素的数量,平均为 O(n) 。

9. 指定位置删除函数  SLErase 
 

void SLErase(SL* ps, int pos)
{
    assert(ps);  // 检查顺序表指针
    // 检查删除位置合法性:pos 要在 0 到 size - 1 之间(只能删除已有元素位置 )
    assert(pos >= 0 && pos < ps->size);  

    // 从 pos 位置开始,把后面的元素依次向前挪动一位,覆盖 pos 位置的元素,达到删除效果
    for (int i = pos;i < ps->size - 1;i++)
    {
        ps->arr[i] = ps->arr[i + 1];
    }
    ps->size--;  // 有效元素个数减 1
}

逻辑:


删除指定位置的元素,通过移动后面的元素覆盖要删除的位置,时间复杂度同样取决于移动

元素的数量,平均为 O(n) 。

10. 查找函数  SLFind 
 

int SLFind(SL* ps, SLDataType x)
{
    for (int i = 0;i < ps->size;i++)
    {
        if (ps->arr[i] == x) 
        {
            return i;  // 找到目标元素,返回其下标
        }
    }
    return -1;  // 遍历完没找到,返回 -1 标识
}

逻辑:


遍历顺序表的有效元素,逐个比较,找到目标元素就返回下标,没找到返回 -1 ,是基础的查

找逻辑,时间复杂度为 O(n) ( n  是有效元素个数 )。
 

11. 销毁函数  SLDestroy 
 

void SLDestroy(SL* ps)
{
    assert(ps);  // 检查顺序表指针
    if (ps->arr != NULL)  // 如果动态数组指针不为空(说明分配过内存 )
    {
        free(ps->arr);  // 释放动态分配的内存,避免内存泄漏
    }
    ps->arr = NULL;  // 指针置空,避免野指针
    ps->size = ps->capacity = 0;  // 重置有效元素个数和容量
}

逻辑:


使用完顺序表后,释放动态分配的内存,把顺序表相关成员重置,防止内存泄漏和野指针问

题,是动态内存管理必不可少的操作。

1.3 测试文件(test.c  )

测试文件( test.c  ,代码里包含了测试逻辑 )—— 验证接口功能:

#include"SeqList.h"
// 注意:实际项目中不要这样包含 .c 文件!这里只是示例演示,正常应该通过头文件 + 编译链接的方式使用
#include"SeqList.c"   

void test01()
{
    SL sl;  // 定义一个顺序表变量
    SLInit(&sl);  // 初始化顺序表

    // 尾插测试
    SLPushBack(&sl, 1);
    SLPushBack(&sl, 2);
    SLPushBack(&sl, 3);
    SLPushBack(&sl, 4);
    SLPrint(&sl);  // 打印:1 2 3 4 

    // 以下是被注释的其他测试代码,可按需取消注释测试对应功能
    // SLPushBack(&sl, 5); 
    // SLPushFront(&sl, 1);
    // SLPushFront(&sl, 2);
    // SLPushFront(&sl, 3);
    // SLPushFront(&sl, 4);//4 3 2 1
    // SLPushFront(NULL, 1); // 触发断言失败,因为传了 NULL

    // SLPopBack(&sl);
    // SLPrint(&sl);
    // SLPopBack(&sl);
    // SLPrint(&sl);
    // SLPopBack(&sl);
    // SLPrint(&sl);
    // SLPopBack(&sl);
    // SLPrint(&sl);
    // SLPopBack(&sl);

    // SLPopFront(&sl);
    // SLPrint(&sl);
    // SLPopFront(&sl);
    // SLPrint(&sl);
    // SLPopFront(&sl);
    // SLPrint(&sl);
    // SLPopFront(&sl);
    // SLPrint(&sl);
    // SLPopFront(&sl);
    // SLPrint(&sl);

    // SLInsert(&sl, 4, 100);
    // SLPrint(&sl);

    // SLErase(&sl, 3);
    // SLPrint(&sl);
    // int find = SLFind(&sl, 22222);
    // if (find != -1)
    // {
    // 	printf("  \n");
    // }
    // else {
    // 	printf("    \n");
    // }

    SLDestroy(&sl);  // 销毁顺序表,释放内存
}

int main()
{
    test01();  // 调用测试函数
    return 0;
}

- test01  函数:定义了一系列对顺序表的操作,包括初始化、尾插、打印、其他注释掉的操作(头

插、尾删、头删、指定位置插入删除、查找等 ),最后销毁顺序表。通过这些操作,可以验证顺

序表各个接口函数是否能正常工作,是测试代码常用的“用例驱动”写法。

1.4 总结:

总结来说,这三个模块分工明确:

- 头文件  SeqList.h  负责定义类型和接口,是“规范”;
- 实现文件  SeqList.c  负责实现接口逻辑,是“具体干活的”;
- 测试文件负责验证接口功能,是“质检员” 。

通过这样的分层设计,代码的可读性、可维护性、可扩展性都会更好,也符合软件工程中“高内

聚、低耦合”的思想。

这个测试文件部分一定要注意。在上面的两个文件中都是分别按照顺序所写的函数。但在这个测试

文件中,这些函数的顺序可能是混用的。我可能在尾插函数使用后再使用打印函数,也可能在头插

函数之后使用打印函数,也可能在使用删除某个位置的函数后使用打印函数。根据不同的场景有不

同的用法,所以一定要理解在头文件和实现文件中的代码内容。只有将这些函数熟练掌握,才能够

在测试文件中灵活运用。

以上三个文件就是关于小编在实现顺序表时所写的全部代码。小编所用的是VS2022。还有小编想

要强调的是小编写的代码也只是小编对于这个顺序表的理解,当然每个人有每个人的理解和代码写

法。小编也只是将小编所写的分享出来,能够给大家提供参考,希望对大家能有帮助。再次感谢大

家的观看。接下来小编还会分享更多的数据结构内容,喜欢的可以给小编留个关注,谢谢大家!

你可能感兴趣的:(数据结构 顺序表(2)---顺序表的实现)