顺序表的代码实现
- 1.认识什么是顺序表
- 1.1顺序表的优缺点
- 2.实现顺序表代码准备
- 3.顺序表的代码实现
- 3.1 顺序表结构体的定义
- 3.2 顺序表的初始化
- 3.3 顺序表的销毁
- 3.4 顺序表的输出打印
- 3.5顺序表的扩容
- 3.6 顺序表的头部插入(头插)
- 3.7 顺序表的头部删除(头删)
- 3.8 顺序表的尾部插入(尾插 )
- 3.9 顺序表的尾部删除(尾删)
- 3.10 在指定位置之前插入数据
- 3.11 删除指定位置的数据
- 3.12 查找数据
- 4.完整代码
1.认识什么是顺序表
- 顺序表是一种基本的数据结构,属于线性表的一种具体实现形式。在顺序表中,数据元素被存储在计算机内存中的一系列连续的存储位置上,这些位置通常是由数组提供的。这意味着顺序表中的每个元素都占据一个固定的存储空间,并且可以通过其索引(或位置)直接访问。
总的来说:顺序表的底层是数组,是对数组的封装,实现了常用的增删改查等接口。因为底层是数组,所以在逻辑结构上一定是连续的,物理结构上不一定连续。
1.1顺序表的优缺点
2.实现顺序表代码准备
为了保持代码的清晰和模块化,在实现代码前我们需要定义三个文件:
SeqList.h
:(函数的声明)
1.这是一个头文件,它定义了顺序表的结构和对外提供的函数接口。
2.其他程序可以通过包含这个头文件来使用顺序表的功能。
3.头文件提供了一个清晰的接口文档,说明了如何使用顺序表,同时也隐藏了内部实现细节。
SeqList.c
:(函数的实现)
2.这是源文件,包含了顺序表的实际实现代码。
3.它实现了头文件中声明的所有函数。
Test.c
:(测试函数的功能)
1.这是一个测试文件,用来验证顺序表的正确性。
2.在这个文件中,你会写一些测试代码,调用顺序表的函数并检查结果是否符合预期。
3.顺序表的代码实现
在实现顺序表时需要用到的头文件:
<stdio.h>
,<stdlib.h>
,<assert.h>
<stdio.h>
-
用途:会使用 printf() 和 scanf() 等函数,用于控制台输出和输入。
-
应用场景:
- 在调试阶段,可以使用 printf() 来输出顺序表的状态,例如在插入、删除操作前后打印顺序表的内容。
- 使用 scanf() 或者其他输入函数来从用户那里获取数据,例如插入新元素的值。
<stdlib.h>
- 用途:提供内存管理相关的函数,如 malloc(), calloc(), realloc(), 和 free()。
- 应用场景:
- 当需要扩展顺序表的容量时,可以使用 realloc() 来调整已分配内存的大小。
- 当销毁顺序表时,使用 free() 来释放分配给 data 的内存。
<assert.h>
- 用途:在编译时检查条件是否为真,如果条件不满足,则会终止程序执行,并给出错误信息。
- 应用场景:
- 在函数入口处检查参数的有效性,例如在 SLInsert() 和 SLDelete() 函数中检查索引是否有效。
3.1 顺序表结构体的定义
- 在
SeqList.h
中定义(动态)顺序表的基本数据结构
#include<stdio.h>
#include<stdlib.h>//开辟空间要用
#include<assert.h>//断言用
typedef int SLDataType;//重复定义int,便于修改顺序表中的顺序类型
typedef struct SeqList
{SLDataType* arr;//指向一块连续的空间int size;//顺序表的有效数据个数int capacity;//顺序表的可以容纳的数据容量个数
}SL://将结构体命名为SL,更加简洁,便于后期的创建
typedef int SLDataType;
:这一行定义了一个类型别名SLDataType,它实际上就是int类型。这里使用类型别名是为了让数据结构更加通用和灵活,如果将来需要改变顺序表中元素的数据类型(比如从整型改为浮点型或者其他类型),只需要在这里修改SLDataType的定义即可,而不需要去修改整个数据结构的实现代码。
SLDataType* arr;
:这是一个指向SLDataType类型的指针,代表了顺序表中存储数据的数组。arr指向的是一块连续的内存区域,这块区域可以存储多个SLDataType类型的元素。
int size;
:定义一个整型变量,表示顺序表当前实际包含的元素数量。size的值不会超过capacity,并且在插入和删除元素时会相应地增减。
int capacity;
:一个整型变量,表示顺序表当前的容量,即arr所指向的数组能容纳的最大元素数量。capacity反映了顺序表当前分配的内存大小,当size接近capacity时,以便重新分配更大的内存空间以避免溢出。
3.2 顺序表的初始化
- 先在
SeqList.h
中声明初始化函数SLInit
,然后在SeqList.c
中实现对顺序表的初始化,定义一个初始化函数SLInit
,将data
初始化为空NULL
,将有效数据个数size
和顺序表大小capacity
初始化为0
;
.h
文件中声明:
// 声明初始化顺序表的函数
void SLInit(SeqList *list);
.c
文件中实现:
//顺序表的初始化
void SLInit(SL* ps)
{ps->a = NULL;ps->size = ps->capacity = 0;
}
test
文件中测试
#include "SeqList.h"
void SLTest01()
{SL sl;SLInit(&sl);//初始化
}
int main()
{SLTest01();return 0;
}
3.3 顺序表的销毁
- 先在
.h
文件中声明销毁函数,然后在.c
文件中实现,销毁通常是释放分配给顺序表的内存,如将data
指针指向的内存释放,并将其置为空。
.h
中声明:
// 声明销毁顺序表的函数
void SLDestroy(SeqList *ps);
.c
中实现:
//销毁
void SLDestroy(SL* ps)if (ps->a)//判断顺序表(数组)是否为空;等价于(ps->a != NULL){free(ps->a);//若不为空,则释放内存}ps->a = NULL;//释放内存后置空ps->size = ps->capacity = 0;
}
test
中测试
#include "SeqList.h"
void SLTest01()
{SL sl;SLInit(&sl);//初始化SLDestroy(&sl);//销毁
}int main()
{SLTest01();return 0;
}
3.4 顺序表的输出打印
- 遍历顺序表,打印顺序表数据:
//顺序表的打印
void SLPrint(SL ps)
{for (int i = 0; i < ps.size; i++){printf("%d ",ps.a[i]);}printf("\n");
}
3.5顺序表的扩容
- 在插入数据时,要保证顺序表的空间足够,否则就要对顺序表进行扩大,以保证插入操作的有效性,所以定义
SLCheckCapacity
函数对顺序表的大小进行检查,以便及时对空间大小进行调整。
.h
中声明:
//扩容
void SLCheckCapacity(SL* ps);
.c
中实现:
void SLCheckCapacity(SL* ps)//检查空间是否足够
{if (ps->capacity == ps->size)//若不够,则动态开辟内存{int newCapacity = ps->capacity == 0 ? INIT_CAPACITY : 2 * ps->capacity;//创建临时变量newCapacity来开辟内存,保证a不会因为realloc开辟失败返回空,导致a的数据丢失SLDataType* temp = (SLDataType*)realloc(ps->a, newCapacity * sizeof(SLDataType));if (temp == 0){perror("realloc error!");//内存开辟失败的提示return 1;//退出程序}//开辟内存成功ps->a = temp;//确保内存开辟成功后赋值给aps->capacity = newCapacity;//更新空间大小}
}
3.6 顺序表的头部插入(头插)
- 函数首先检查传入的指针
ps
是否为 NULL。如果是NULL
,则触发断言失败,程序终止。防止对无效指针进行操作。 - 然后,调用
SLCheckCapacity
函数来确保顺序表有足够的空间来容纳新插入的元素。如果空间不足,则调整数组的大小。 - 接着,使用一个循环从最后一个元素开始,将每个元素向后移动一个位置。循环条件是从
ps->size
(有效数据个数)到1
。每次循环时,ps->a[i] 的值被赋值给ps->a[i + 1]
,将所有元素都向后移动了一位,腾出ps->a[0]
这个位置。 - 将新元素
x
放在ps->a[0]
的位置上,进行头插。 - 插入元素后,更新元素总数,数量增加
1
,即ps->size
的值加1
。
.h中声明:
//头插
void SLPushFront(SL* ps, SLDataType x);
.c中实现:
//头插
void SLPushFront(SL* ps, SLDataType x)//头插
{assert(ps);SLCheckCapacity(ps);//判断数组(顺序表)大小是否足够//让顺序表中原来的数据整体向后移动一位for (int i = ps->size - 1; i >= 0; i--){ps->a[i + 1] = ps->a[i];//ps->a[1] = ps->a[0]}ps->a[0] = x;ps->size++;//头插后,顺序表中数据个数有所变化,所以需要++更新
}
3.7 顺序表的头部删除(头删)
- 参数检查:
- 检查传入的指针
ps
是否为NULL
。如果是NULL
,断言失败,防止对无效指针进行操作。 - 接着检查
ps->size
是否大于0
,即顺序表中是否有元素可以删除。如果没有元素,则断言失败,防止从空顺序表中删除元素。
- 检查传入的指针
- 元素移动:
- 使用一个循环从第一个元素开始,将每个元素向前移动一个位置。循环条件是从
0
到
ps->size - 2
。每次循环时,ps->a[i + 1]
的值被赋值给ps->a[i]
。这样就能将所有元素都向前移动了一位,覆盖了原本位于ps->a[0]
的元素,实现了头删操作。
- 使用一个循环从第一个元素开始,将每个元素向前移动一个位置。循环条件是从
- 更新大小:
- 删除元素后,顺序表中的元素总数减少
1
,因此ps->size
的值减1
。
- 删除元素后,顺序表中的元素总数减少
.h中声明:
void SLPopFront(SL* ps);//头删
.c中实现:
//头删
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];//arr[size - 2] = arr[size - 1]}ps->size--;//删除后对有效数据进行更新
}
test中测试:
3.8 顺序表的尾部插入(尾插 )
- 参数检查:
- assert(ps) 确保传入的指针 ps 不为 NULL。如果 ps 为 NULL,则断言失败,程序会终止。
- 容量检查:
- 调用 SLCheckCapacity(ps) 函数来确保顺序表有足够的空间来容纳新插入的元素。如果空间不足,SLCheckCapacity 会调整数组的大小。
- 插入新元素:
- 使用 ps->a[ps->size++] = x;,将新元素 x 插入到数组 ps->a 的当前有效大小的位置,也就是数组的最后一个位置。最后,ps->size 的值加 1,更新顺序表中元素数量的变化。
.h中声明:
//尾插
void SLPushBack(SL* ps, SLDataType x);
.c中是实现:
//尾插
void SLPushBack(SL* ps, SLDataType x)
{assert(ps);SLCheckCapacity(ps);//判断数组(顺序表)大小是否足够ps->a[ps->size++] = x;//尾插数据,并更新有效数据的个数//ps->a[ps->size] = x;//ps->size++;
}
3.9 顺序表的尾部删除(尾删)
- 参数检查:
- 使用 assert(ps) 确保传入的指针 ps 不为 NULL。如果 ps 为 NULL,则断言失败,导致程序终止。
- 使用 assert(ps->size) 确保顺序表中至少有一个元素。如果 ps->size 为 0,说明顺序表为空,无法删除元素,也会断言失败。
- 删除元素:
- 由于顺序表是通过数组实现的,删除尾部元素的操作非常简单,只需将 ps->size 减少 1 即可。
- 在 C 语言中,数组的索引是从 0 开始的,因此 ps->a[ps->size - 1] 表示顺序表中的最后一个元素。
- 删除最后一个元素后,ps->size 应该减 1,更新顺序表中有效的数据个数。
- 清理尾部元素(理解即可):
- 代码注释中将最后一个元素置为 -1,但这不是必需的。因为 ps->size 已经减少了 1,所以 ps->a[ps->size](即原本的 ps->a[ps->size - 1])不再是有效索引,即使保留原有的值也不会影响后续的操作。
- 因此,代码中并没有执行 ps->a[ps->size - 1] = -1; 这一行,而是直接减少 ps->size 的值。
.h中声明:
void SLPopBack(SL* ps);//尾删
.c中实现:
//尾删
void SLPopBack(SL* ps)
{assert(ps);assert(ps->size);//assert断言,确保顺序表不能为空(顺序表不为空,顺序表中的有效数据size也不为空)//ps->a[size - 1] = -1;//尾删的数据不论是否置为-1,都不影响增删查改ps->size--;
}
test中测试:
3.10 在指定位置之前插入数据
- 参数检查:
- 使用 assert(ps) 确保传入的指针 ps 不为 NULL。如果 ps 为 NULL,则断言失败,导致程序终止。
- 使用 assert(pos >= 0 && pos <= ps->size) 确保指定的插入位置 pos 是有效的。位置 pos 必须是非负的,并且不能超过当前顺序表的有效大小 ps->size。
- 容量检查:
- 调用 SLCheckCapacity(ps) 函数来确保顺序表有足够的空间来容纳新插入的元素。如果空间不足,SLCheckCapacity 会调整数组的大小以满足插入需求。
- 元素移动:
- 使用一个循环从最后一个有效元素开始,将每个元素向后移动一个位置,直到达到目标位置 pos。
- 循环条件是从 ps->size 到 pos,即 for (int i = ps->size; i > pos; i–)。将所有在 pos 之后的元素都向后移动一位,为新元素腾出空间。
- 插入新元素:
- 将新元素 x 插入到 ps->a[pos] 的位置,即指定的位置。
- 更新大小:
- 插入元素后,顺序表中的元素总数增加 1,更新ps->size 的值加 1。
.h中声明:
//指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x);
.c中实现:
//指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{assert(ps);assert(pos >= 0 && pos <= ps->size);//插入数据,判断空间够不够SLCheckCapacity(ps);//让数据整体向后挪一位for (int i = ps->size;i >pos;i--){ps->a[i] = ps->a[i - 1];//a[pos + 1] = a[pos];}ps->a[pos] = x;ps->size++;
}
test中测试:
3.11 删除指定位置的数据
- 参数检查:
- 使用 assert(ps) 确保传入的指针 ps 不为 NULL。如果 ps 为 NULL,则断言失败,导致程序终止。
- 使用 assert(pos >= 0 && pos < ps->size) 确保指定的删除位置 pos 是有效的。位置 pos 必须是非负的,并且不能超过当前顺序表的有效大小 ps->size。
- 元素移动:
- 使用一个循环从指定位置 pos 开始,将每个元素向前移动一个位置,直到达到顺序表的倒数第二个元素。
- 循环条件是从 pos 到 ps->size - 1,即 for (int i = pos; i < ps->size - 1; i++)。将所有在 pos 之后的元素都向前移动一位,覆盖被删除元素的位置。
- 更新大小:
删除元素后,顺序表中的元素总数减少 1,更新ps->size 的值减 1。
.h中声明:
void SLErase(SL* ps, int pos);//指定位置删除
.c中实现:
//指定位置删除数据
void SLErase(SL* ps, int pos)
{assert(ps);assert(pos >= 0 && pos < ps->size);for (int i = pos; i < ps-> size - 1;i++){//删除后后面的数据向前挪一位ps->a[i] = ps->a[i + 1];//a[size - 2] = a[size - 1];}ps->size--;
}
test中测试:
3.12 查找数据
- 参数检查:
- 使用 assert(ps) 确保传入的指针 ps 不为 NULL。如果 ps 为 NULL,则断言失败,导致程序终止。
- 遍历顺序表:
- 使用一个循环从顺序表的第一个元素开始遍历整个顺序表。
- 循环条件是从 0 到 ps->size - 1,即 for (int i = 0; i < ps->size; i++)。因为 ps->size 表示顺序表中有效元素的数量,所以有效元素的索引范围是从 0 到 ps->size - 1。
- 比较数据:
- 在循环体内,使用 if (ps->a[i] == x) 来检查当前元素 ps->a[i] 是否等于要查找的数据 x。
- 如果找到匹配的元素,则返回该元素的下标 i。
- 未找到数据:
- 如果循环结束后都没有找到匹配的数据,函数返回 -1,表示数据 x 不在顺序表中。
.h中声明:
int SLFind(SL* ps, SLDataType x);//指定位置查找
.c中实现:
//查找数据
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;//返回一个无效下标,习惯上返回-1;
}
test中测试:
4.完整代码
SeqList.h文件:
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>#define INIT_CAPACITY 4typedef int SLDataType;
// 动态顺序表 -- 按需申请
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);//头删//指定位置之前插入/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);//指定位置插入
void SLErase(SL* ps, int pos);//指定位置删除
int SLFind(SL* ps, SLDataType x);//指定位置查找
SeqList.c文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"void SLInit(SL* ps)//顺序表的初始化
{ps->a = NULL;ps->size = ps->capacity = 0;
}void SLCheckCapacity(SL* ps)//检查空间是否足够
{if (ps->capacity == ps->size)//若不够,则动态开辟内存{int newCapacity = ps->capacity == 0 ? INIT_CAPACITY : 2 * ps->capacity;//创建临时变量newCapacity来开辟内存,保证a不会因为realloc开辟失败返回空,导致a的数据丢失SLDataType* temp = (SLDataType*)realloc(ps->a, newCapacity * sizeof(SLDataType));if (temp == 0){perror("realloc error!");//内存开辟失败的提示return 1;//退出程序}//开辟内存成功ps->a = temp;//确保内存开辟成功后赋值给aps->capacity = newCapacity;//更新空间大小}
}//尾插
void SLPushBack(SL* ps, SLDataType x)
{assert(ps);SLCheckCapacity(ps);//判断数组(顺序表)大小是否足够ps->a[ps->size++] = x;//尾插数据,并更新有效数据的个数//ps->a[ps->size] = x;//ps->size++;
}//头插
void SLPushFront(SL* ps, SLDataType x)//头插
{assert(ps);SLCheckCapacity(ps);//判断数组(顺序表)大小是否足够//让顺序表中原来的数据整体向后移动一位for (int i = ps->size - 1; i >= 0; i--){ps->a[i + 1] = ps->a[i];//ps->a[1] = ps->a[0]}ps->a[0] = x;ps->size++;//头插后,顺序表中数据个数有所变化,所以需要++更新
}//尾删
void SLPopBack(SL* ps)
{assert(ps);assert(ps->size);//assert断言,确保顺序表不能为空(顺序表不为空,顺序表中的有效数据size也不为空)//ps->a[size - 1] = -1;//尾删的数据不论是否置为-1,都不影响增删查改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];//arr[size - 2] = arr[size - 1]}ps->size--;//删除后对有效数据进行更新
}//顺序表的打印
void SLPrint(SL ps)
{for (int i = 0; i < ps.size; i++){printf("%d ",ps.a[i]);}printf("\n");
}void SLDestroy(SL* ps)//销毁
{if (ps->a)//判断顺序表(数组)是否为空;等价于(ps->a != NULL){free(ps->a);//若不为空,则释放内存}ps->a = NULL;//释放内存后置空ps->size = ps->capacity = 0;
}//指定位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{assert(ps);assert(pos >= 0 && pos <= ps->size);//插入数据,判断空间够不够SLCheckCapacity(ps);//让数据整体向后挪一位for (int i = ps->size;i >pos;i--){ps->a[i] = ps->a[i - 1];//a[pos + 1] = a[pos];}ps->a[pos] = x;ps->size++;
}//指定位置删除数据
void SLErase(SL* ps, int pos)
{assert(ps);assert(pos >= 0 && pos < ps->size);for (int i = pos; i < ps-> size - 1;i++){//删除后后面的数据向前挪一位ps->a[i] = ps->a[i + 1];//a[size - 2] = a[size - 1];}ps->size--;
}//查找数据
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;//返回一个无效下标,习惯上返回-1;
}
Test.c文件:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void SLTest01()
{SL sl;SLInit(&sl);//初始化//增删查改操作头插测试//SLPushFront(&sl, 0);//SLPrint(sl);//SLPushFront(&sl, 2);//SLPrint(sl);//SLPushFront(&sl, 5);//SLPrint(sl);//头删测试//SLPopFront(&sl);//SLPrint(sl);//SLPopFront(&sl);//SLPrint(sl);//SLPopFront(&sl);//SLPrint(sl);//尾插测试SLPushBack(&sl, 1);SLPrint(sl);SLPushBack(&sl, 3);SLPrint(sl);SLPushBack(&sl, 1);SLPrint(sl);SLPushBack(&sl, 4); SLPrint(sl);//尾删测试//SLPopBack(&sl);//SLPrint(sl);//SLPopBack(&sl);//SLPrint(sl);//SLPopBack(&sl);//SLPrint(sl);//SLPopBack(&sl);//SLPrint(sl);printf("\n");//指定位置插入测试SLInsert(&sl, 0, 520);SLPrint(sl);SLInsert(&sl, 5, 20);SLPrint(sl);SLInsert(&sl, 2, 1314);SLPrint(sl);//指定位置删除测试SLErase(&sl, 0);SLPrint(sl);SLErase(&sl, 2);SLPrint(sl);SLErase(&sl, 1);SLPrint(sl);//顺序表查找数据int find = SLFind(&sl, 40);if (find >= 0){printf("找到了!下表为%d\n",find);}else{printf("没找到!");}SLDestroy(&sl);//销毁
}int main()
{SLTest01();return 0;
}