顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删查改
顺序表分为:静态顺序表、动态顺序表
一.静态顺序表
cpp
#define N 7
typedef int SLDataType;
typedef struct Seqlist
{
SLDataType array[N]; //定长数组
size_t size; //有效元素个数
}Seqlist;
静态顺序表突出的特点是:少了不够、多了浪费。
二.动态顺序表
cpp
typedef int SLDataType;
typedef struct Seqlist
{
SLDataType* array; //指针指向动态开辟的数组
size_t size; //有效数据个数
size_t capacity; //容量空间大小
}Seqlist;
动态顺序表突出的特点是:按需申请。
三.实现增删查改
基于顺序表的核心思想,参考之前的通讯录程序,我们来用顺序表实现增删查改。
1.准备工作:
欲实现增删查改,首先我们需要分装函数:
|--------|-----------|-----------|
| test.c | SeqList.c | SeqList.h |
| 测试模块 | 函数模块 | 声明模块 |
2.声明模块(SeqList.h):
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
#define INIT_CAPACITY 4
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);
这段代码定义了一个动态顺序表(动态数组)的接口:
一、结构定义解析
cpp
typedef int SLDataType;
#define INIT_CAPACITY 4
typedef struct SeqList
{
SLDataType* a; // 动态数组指针
int size; // 当前元素个数
int capacity; // 当前容量
} SL;
关键成员
a
:- 指向动态分配的数组,存储顺序表元素。
size
:- 当前有效元素个数(范围:
0 ≤ size ≤ capacity
)。
- 当前有效元素个数(范围:
capacity
:- 数组当前的最大容量,扩容时会动态调整。
二、接口功能分析
1. 生命周期管理
SLInit(SL* ps)
:初始化顺序表。SLDestroy(SL* ps)
:释放内存,销毁顺序表。
2. 元素操作
- 尾部操作 :
SLPushBack
(尾插)、SLPopBack
(尾删)。 - 头部操作 :
SLPushFront
(头插)、SLPopFront
(头删)。 - 任意位置操作 :
SLInsert
(指定位置插入)、SLErase
(指定位置删除)。
3. 辅助功能
SLPrint
:打印顺序表元素。SLCheckCapacity
:检查容量并在需要时扩容。SLFind
:查找元素并返回位置。
三、性能分析
操作 | 时间复杂度 | 说明 |
---|---|---|
尾插 PushBack |
O(1) | 均摊复杂度(扩容时 O (n)) |
头插 PushFront |
O(n) | 需要移动所有元素 |
指定位置插入 SLInsert |
O(n) | 平均移动 n/2 个元素 |
尾删 PopBack |
O(1) | 直接减少 size,无需移动元素 |
头删 PopFront |
O(n) | 需要移动所有元素 |
指定位置删除 SLErase |
O(n) | 平均移动 n/2 个元素 |
查找 Find |
O(n) | 需遍历数组 |
3.函数模块(SeqList.c):
确定了具体函数以后,接下来的任务就是如何将这些函数在.c文件中逐个实现:
<1>.初始化函数:
cpp
void SLInit(SL* ps)
{
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
一、功能概述
SLInit
函数的核心目的是:
- 为顺序表分配初始内存空间(容量为
INIT_CAPACITY
,通常为 4)。 - 初始化顺序表的状态(
size
和capacity
)。 - 处理内存分配失败的情况。
二、代码逻辑拆解
cpp
void SLInit(SL* ps)
{
assert(ps); // 检查指针有效性
// 分配初始内存
ps->a = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);
// 处理内存分配失败
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
// 初始化状态
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
三、关键技术细节
① 指针有效性检查
cpp
assert(ps);
- 使用
assert
确保传入的指针非空。 - 注意 :
assert
在调试模式下生效,发布版本可能被忽略。建议结合运行时检查(如if (ps == NULL)
)增强健壮性。
② 内存分配
cpp
ps->a = (SLDataType*)malloc(sizeof(SLDataType)* INIT_CAPACITY);
- 分配
INIT_CAPACITY
个元素的空间。 - 强制类型转换 :在 C 语言中,
malloc
返回void*
,可隐式转换为其他指针类型,因此(SLDataType*)
可省略。但在 C++ 中必须显式转换。
③ 错误处理
cpp
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
- 当内存分配失败时:
- 使用
perror
输出错误信息(包含系统错误描述)。 - 函数直接返回,此时
ps->a
为NULL
,后续操作需检查指针有效性。
- 使用
- 潜在风险 :调用者可能未检查
a
是否为NULL
,导致后续操作崩溃。
④ 状态初始化
cpp
ps->size = 0;
ps->capacity = INIT_CAPACITY;
size
初始化为 0,表示顺序表为空。capacity
记录当前分配的空间大小。
<2>.尾插函数:
cpp
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//扩容:
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
ps->a[ps->size++] = x;
}
一、功能概述
SLPushBack
函数的核心目的是:
- 将元素
x
添加到顺序表的尾部。 - 若顺序表已满(
size == capacity
),则自动扩容(容量翻倍)。 - 处理内存分配失败的情况。
二、代码逻辑拆解
cpp
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps); // 检查指针有效性
// 扩容逻辑
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
// 插入元素
ps->a[ps->size++] = x;
}
三、关键技术细节
① 指针有效性检查
cpp
assert(ps);
- 使用
assert
确保传入的指针非空。 - 注意 :
assert
在调试模式下生效,发布版本可能被忽略。建议结合运行时检查(如if (ps == NULL)
)增强健壮性。
② 动态扩容机制
cpp
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
// ...
}
- 扩容条件 :当
size
等于capacity
时触发。 - 扩容策略 :容量翻倍(
ps->capacity * 2
),均摊时间复杂度为 O (1)。 - 安全使用
realloc
:- 使用临时指针
tmp
接收realloc
的返回值,避免内存泄漏(若realloc
失败,原指针ps->a
保持不变)。 - 强制类型转换:在 C 语言中可省略,但 C++ 中必须显式转换。
- 使用临时指针
③ 错误处理
cpp
if (tmp == NULL)
{
perror("realloc fail");
return;
}
- 当内存分配失败时:
- 使用
perror
输出错误信息。 - 函数直接返回,此时原指针
ps->a
未被修改,但插入操作失败。
- 使用
- 潜在风险:调用者可能未检查操作是否成功,误认为元素已插入。
④ 元素插入
cpp
ps->a[ps->size++] = x;
-
将元素
x
放入当前size
位置,然后size
自增。 -
等价于 :
cppps->a[ps->size] = x; ps->size++;
<3>.头插函数:
cpp
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[0] = x;
ps->size++;
}
一、功能概述
SLPushFront
函数的核心目的是:
- 将元素
x
添加到顺序表的头部。 - 若顺序表已满(
size == capacity
),则自动扩容(容量翻倍)。 - 通过元素后移实现头部插入。
二、代码逻辑拆解
cpp
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps); // 检查指针有效性
// 扩容逻辑(同SLPushBack)
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
// 元素后移
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
// 插入元素
ps->a[0] = x;
ps->size++;
}
三、关键技术细节
① 扩容机制
与 SLPushBack
相同,当容量不足时进行翻倍扩容,确保有空间插入新元素。
② 元素后移
cpp
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
- 移动方向:从最后一个元素开始,依次向后移动一位,直到所有元素后移完毕。
- 时间复杂度 :O (n),需移动全部
size
个元素。
③ 插入操作
cpp
ps->a[0] = x;
ps->size++;
- 将新元素放入数组头部(索引 0),并更新
size
。
<4>.扩容函数:
观察发现,插入函数需要扩容,为减轻冗长代码,我们可以将扩容代码另外分装:
cpp
void SLCheckCapacity(SL* ps)
{
assert(ps);
//扩容:
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
}
<5>.尾删函数:
cpp
void SLPopBack(SL* ps)
{
assert(ps); // 检查指针有效性
// 处理空表情况
if (ps->size == 0)
return;
// 逻辑删除:减小size,无需清除元素
ps->size--;
}
尾删函数较为简单,不加以赘述......
<6>.头删函数:
cpp
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
一、功能概述
SLPopFront
函数的核心目的是:
- 删除顺序表的第一个元素。
- 通过元素前移覆盖原头部元素。
- 减小
size
以标记有效元素数量减少。
二、代码逻辑拆解
cpp
void SLPopFront(SL* ps)
{
assert(ps); // 检查指针有效性
assert(ps->size > 0); // 确保表非空
// 元素前移覆盖头部
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
// 减小size
ps->size--;
}
三、关键技术细节
① 断言检查
cpp
assert(ps);
assert(ps->size > 0);
- 确保传入指针非空且表非空。
- 注意 :
assert
仅在调试模式生效,发布版本可能忽略,需依赖调用者检查。
② 元素前移
cpp
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
- 移动方向:从第二个元素(索引 1)开始,依次向前覆盖前一个元素。
- 时间复杂度 :O (n),需移动全部
size-1
个元素。
③ 逻辑删除
cpp
ps->size--;
- 通过减小
size
标记有效元素减少,无需物理清除最后一个元素。
<7>.指定位置插入函数:
cpp
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
一、功能概述
SLInsert
函数的核心目的是:
- 将元素
x
插入到顺序表的指定位置pos
。 - 若顺序表已满,自动扩容。
- 通过元素后移为新元素腾出空间。
二、代码逻辑拆解
cpp
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps); // 检查指针有效性
assert(pos >= 0 && pos <= ps->size); // 确保pos合法
SLCheckCapacity(ps); // 检查并扩容(若需要)
// 元素后移
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
// 插入元素
ps->a[pos] = x;
ps->size++;
}
三、关键技术细节
① 合法性检查
cpp
assert(pos >= 0 && pos <= ps->size);
- 合法范围 :
0 ≤ pos ≤ size
pos == size
时,等价于尾部插入。- 若
pos > size
,会导致元素间出现空洞(如[1, 2, pos=5插入3] → [1, 2, ?, ?, ?, 3]
)。
② 动态扩容
cpp
SLCheckCapacity(ps);
- 调用扩容函数,确保有足够空间插入新元素。
- 扩容策略:通常为容量翻倍(如从 4→8→16),均摊时间复杂度 O (1)。
③ 元素后移
cpp
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
- 移动方向 :从最后一个元素开始,逐个后移直到
pos
位置。 - 时间复杂度:O (n)(最坏情况:插入头部需移动所有元素)。
④ 插入操作
cpp
ps->a[pos] = x;
ps->size++;
- 将元素放入指定位置
pos
,更新size
。
<8>.指定位置删除函数:
cpp
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
一、功能概述
SLErase
函数的核心目的是:
- 删除顺序表中指定位置
pos
的元素。 - 通过元素前移覆盖被删除元素。
- 减小
size
以标记有效元素数量减少。
二、代码逻辑拆解
cpp
void SLErase(SL* ps, int pos)
{
assert(ps); // 检查指针有效性
assert(pos >= 0 && pos < ps->size); // 确保pos合法
// 元素前移覆盖
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
// 减小size
ps->size--;
}
三、关键技术细节
① 合法性检查
cpp
assert(pos >= 0 && pos < ps->size);
- 合法范围 :
0 ≤ pos < size
- 若
pos == size
,会导致访问越界(如size=3
时,pos=3
访问a[4]
)。
- 若
② 元素前移
cpp
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
- 移动方向 :从
pos
的下一个位置开始,依次向前覆盖前一个元素。 - 时间复杂度:O (n)(最坏情况:删除头部需移动所有元素)。
③ 逻辑删除
cpp
ps->size--;
- 通过减小
size
标记有效元素减少,无需物理清除最后一个元素。
基于以上的两个指定位置函数,我们可以进一步简化代码,将头插、尾插、头删、尾删函数均用任意位置函数代替,代码量将大大减少。后续总体展示时可以看出。
<9>.查找函数:
cpp
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;
}
代码较为简单,不加以赘述.....
综上,整个程序基本完成,接下来将总体展示,包括三个模块以及一些代码的简化:
4.整体展示:
<1>.测试模块(test.c):
cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include"SeqList.h"
void TestSeqList1()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPushBack(&s, 6);
SLPrint(&s);
SLPopBack(&s);
SLPopBack(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLPopFront(&s);
SLPrint(&s);
SLInsert(&s, 2, 1);
SLPrint(&s);
void* ptr1 = malloc(10);
printf("%p\n", ptr1);
void* ptr2 = realloc(ptr1,20);
printf("%p\n", ptr2); //两次开辟地址不同,所以是异地扩容
SLDestroy(&s);
}
int main()
{
TestSeqList1();
return 0;
}
当然,若想要设置菜单,和通讯录一样也是可以的......
<2>.函数模块(SeqList.c):
cpp
#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 fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
void SLDestroy(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 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)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity *= 2;
}
}
void SLPushBack(SL* ps, SLDataType x)
{
//assert(ps);
扩容:
//SLCheckCapacity(ps);
//ps->a[ps->size++] = x;
SLInsert(ps, ps->size, x);
}
void SLPopBack(SL* ps)
{
/*assert(ps);
if (ps->size == 0)
return;
ps->size--;*/
SLErase(ps, ps->size - 1);
}
void SLPushFront(SL* ps, SLDataType x)
{
/*assert(ps);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[0] = x;
ps->size++;*/
SLInsert(ps, 0, x);
}
void SLPopFront(SL* ps)
{
/*assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;*/
SLErase(ps, 0);
}
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
SLCheckCapacity(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
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;
}
<3>.声明模块(SeqList.h):
cpp
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLDataType;
#define INIT_CAPACITY 4
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);
至此,代码全部结束,欢迎指出错误与不当之处,谢谢!!!