第二讲 深入理解顺序表:从概念到实战
一、线性表的概念及类型
线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,数据元素之间呈线性逻辑关系,是最基本、最常用的线性结构之一。
-
逻辑特征:
- 存在唯一的“第一个”元素和“最后一个”元素;
- 除首尾元素外,每个元素有唯一的前驱和后继;
- 数据元素“一个挨着一个”存储。
-
常见线性表分类:
- 数组:物理地址连续,支持随机访问,增删效率低;
- 顺序表:基于数组实现的线性表,封装了数组的操作;
- 链表:物理地址不连续,通过指针连接,增删效率高;
- 栈:“后进先出”的线性表,常用于函数调用、表达式求值;
- 队列:“先进先出”的线性表,常用于消息队列、广度优先搜索;
- 字符串:由字符组成的特殊线性表,支持字符串匹配、拼接等操作。
二、顺序表的实现
(1)概念与结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,本质是对数组的“封装化”操作,将数组的增删查改逻辑抽象为接口,提升代码的可维护性和可读性。
- 分类:
- 静态顺序表:使用定长数组存储,空间大小编译期确定,无法动态扩容;
- 动态顺序表:使用动态开辟的数组存储,可根据需求扩容,灵活性更强。
(2)静态顺序表(以尾插、遍历为例)
静态顺序表适用于数据量已知且固定的场景,代码实现简洁直观。
SeqListStatic.h(头文件)
#pragma once
#include <stdio.h>
#include <string.h>
#define MAX_STATIC_SIZE 10 // 静态数组最大容量
typedef int SQDataType; // 数据类型重定义,便于后续修改
typedef struct StaticSeqList {
SQDataType data[MAX_STATIC_SIZE]; // 定长数组存储数据
int size; // 有效元素个数
} StaticSL;
// 接口函数声明
void StaticSeqListInit(StaticSL* psl); // 初始化
void StaticSeqListPushBack(StaticSL* psl, SQDataType x); // 尾插
void StaticSeqListPrint(StaticSL* psl); // 遍历打印
SeqListStatic.c(功能实现)
#include "SeqListStatic.h"
void StaticSeqListInit(StaticSL* psl) {
memset(psl->data, 0, sizeof(SQDataType) * MAX_STATIC_SIZE);
psl->size = 0;
}
void StaticSeqListPushBack(StaticSL* psl, SQDataType x) {
if (psl->size >= MAX_STATIC_SIZE) {
printf("StaticSeqList is full! Cannot push back.\n");
return;
}
psl->data[psl->size] = x;
psl->size++;
}
void StaticSeqListPrint(StaticSL* psl) {
for (int i = 0; i < psl->size; i++) {
printf("%d ", psl->data[i]);
}
printf("\n");
}
TestStatic.c(测试用例)
#include "SeqListStatic.h"
void TestStaticSeqList() {
StaticSL s;
StaticSeqListInit(&s);
// 尾插元素
StaticSeqListPushBack(&s, 1);
StaticSeqListPushBack(&s, 2);
StaticSeqListPushBack(&s, 3);
StaticSeqListPushBack(&s, 4);
StaticSeqListPushBack(&s, 5);
StaticSeqListPrint(&s); // 输出:1 2 3 4 5
// 测试满容量插入
for (int i = 6; i <= 11; i++) {
StaticSeqListPushBack(&s, i);
}
StaticSeqListPrint(&s); // 输出:1 2 3 4 5 (因为容量已满,6~11无法插入)
}
int main() {
TestStaticSeqList();
return 0;
}
(3)动态顺序表(完整增删查改实现)
动态顺序表通过**动态内存分配(malloc/realloc)**实现扩容,支持灵活的增删查改操作,是工程中更常用的形式。
SeqListDynamic.h(头文件)
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
typedef int SQDataType; // 数据类型统一管理
typedef struct DynamicSeqList {
SQDataType* data; // 指向动态开辟的数组
int size; // 有效元素个数
int capacity; // 数组容量(已开辟的空间大小)
} DynamicSL;
// 核心接口函数声明
void DynamicSeqListInit(DynamicSL* psl); // 初始化
void DynamicSeqListPrint(DynamicSL* psl); // 遍历打印
void DynamicSeqListDestroy(DynamicSL* psl); // 销毁空间
void DynamicSeqListPushBack(DynamicSL* psl, SQDataType x); // 尾插
void DynamicSeqListPushFront(DynamicSL* psl, SQDataType x); // 头插
void DynamicSeqListPopBack(DynamicSL* psl); // 尾删
void DynamicSeqListPopFront(DynamicSL* psl); // 头删
void DynamicSeqListInsert(DynamicSL* psl, int pos, SQDataType x); // 任意位置插入
void DynamicSeqListErase(DynamicSL* psl, int pos); // 任意位置删除
int DynamicSeqListFind(DynamicSL* psl, SQDataType x); // 查找元素
void DynamicSeqListModify(DynamicSL* psl, int pos, SQDataType x); // 修改元素
SeqListDynamic.c(功能实现)
#include "SeqListDynamic.h"
// 检查容量:若容量不足则扩容(扩容策略:初始4,之后每次翻倍)
static void CheckCapacity(DynamicSL* psl) {
if (psl->size == psl->capacity) {
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SQDataType* newData = (SQDataType*)realloc(psl->data, newCapacity * sizeof(SQDataType));
if (newData == NULL) {
perror("realloc failed");
exit(1); // 内存开辟失败,直接终止程序
}
psl->data = newData;
psl->capacity = newCapacity;
}
}
void DynamicSeqListInit(DynamicSL* psl) {
psl->data = NULL;
psl->size = 0;
psl->capacity = 0;
}
void DynamicSeqListPrint(DynamicSL* psl) {
for (int i = 0; i < psl->size; i++) {
printf("%d ", psl->data[i]);
}
printf("\n");
}
void DynamicSeqListDestroy(DynamicSL* psl) {
free(psl->data);
psl->data = NULL;
psl->size = 0;
psl->capacity = 0;
}
void DynamicSeqListPushBack(DynamicSL* psl, SQDataType x) {
CheckCapacity(psl);
psl->data[psl->size] = x;
psl->size++;
}
void DynamicSeqListPushFront(DynamicSL* psl, SQDataType x) {
CheckCapacity(psl);
// 元素后移:从最后一个元素开始,到第一个元素结束
for (int i = psl->size; i > 0; i--) {
psl->data[i] = psl->data[i - 1];
}
psl->data[0] = x;
psl->size++;
}
void DynamicSeqListPopBack(DynamicSL* psl) {
assert(psl->size > 0); // 断言:顺序表不为空
psl->size--;
// 注:若需显式置空,可加 psl->data[psl->size] = 0; 但一般没必要
}
void DynamicSeqListPopFront(DynamicSL* psl) {
assert(psl->size > 0);
// 元素前移:从第二个元素开始,到最后一个元素结束
for (int i = 1; i < psl->size; i++) {
psl->data[i - 1] = psl->data[i];
}
psl->size--;
}
void DynamicSeqListInsert(DynamicSL* psl, int pos, SQDataType x) {
assert(pos >= 0 && pos <= psl->size); // 断言:插入位置合法
CheckCapacity(psl);
// 元素后移:从pos位置的元素开始,到最后一个元素结束
for (int i = psl->size; i > pos; i--) {
psl->data[i] = psl->data[i - 1];
}
psl->data[pos] = x;
psl->size++;
}
void DynamicSeqListErase(DynamicSL* psl, int pos) {
assert(pos >= 0 && pos < psl->size); // 断言:删除位置合法
// 元素前移:从pos+1位置的元素开始,到最后一个元素结束
for (int i = pos + 1; i < psl->size; i++) {
psl->data[i - 1] = psl->data[i];
}
psl->size--;
}
int DynamicSeqListFind(DynamicSL* psl, SQDataType x) {
for (int i = 0; i < psl->size; i++) {
if (psl->data[i] == x) {
return i; // 找到,返回下标
}
}
return -1; // 未找到,返回-1
}
void DynamicSeqListModify(DynamicSL* psl, int pos, SQDataType x) {
assert(pos >= 0 && pos < psl->size); // 断言:修改位置合法
psl->data[pos] = x;
}
TestDynamic.c(测试用例)
#include "SeqListDynamic.h"
void TestDynamicSeqList() {
DynamicSL s;
DynamicSeqListInit(&s);
// 尾插测试
DynamicSeqListPushBack(&s, 1);
DynamicSeqListPushBack(&s, 2);
DynamicSeqListPushBack(&s, 3);
printf("尾插后:");
DynamicSeqListPrint(&s); // 输出:1 2 3
// 头插测试
DynamicSeqListPushFront(&s, 0);
printf("头插后:");
DynamicSeqListPrint(&s); // 输出:0 1 2 3
// 任意位置插入测试
DynamicSeqListInsert(&s, 2, 100);
printf("在位置2插入100后:");
DynamicSeqListPrint(&s); // 输出:0 1 100 2 3
// 查找测试
int pos = DynamicSeqListFind(&s, 100);
printf("元素100的位置:%d\n", pos); // 输出:2
// 修改测试
DynamicSeqListModify(&s, pos, 200);
printf("修改位置2为200后:");
DynamicSeqListPrint(&s); // 输出:0 1 200 2 3
// 尾删测试
DynamicSeqListPopBack(&s);
printf("尾删后:");
DynamicSeqListPrint(&s); // 输出:0 1 200 2
// 头删测试
DynamicSeqListPopFront(&s);
printf("头删后:");
DynamicSeqListPrint(&s); // 输出:1 200 2
// 任意位置删除测试
DynamicSeqListErase(&s, 1);
printf("删除位置1后:");
DynamicSeqListPrint(&s); // 输出:1 2
// 销毁测试
DynamicSeqListDestroy(&s);
}
int main() {
TestDynamicSeqList();
return 0;
}
三、顺序表核心习题深度解析
(1)练习1:移除元素(LeetCode 27)
题目:给一个数组 nums 和一个值 val,移除所有等于 val 的元素,返回新数组的长度。要求不使用额外数组空间,时间复杂度 O(n)O(n)O(n),空间复杂度 O(1)O(1)O(1)。
双指针思路:定义两个指针 src(遍历原数组)和 dst(指向新数组的末尾)。若 nums[src] != val,则将 nums[src] 赋值给 nums[dst],并同时后移 src 和 dst;若 nums[src] == val,则仅后移 src。最终 dst 即为新数组的长度。
int removeElement(int* nums, int numsSize, int val) {
int src = 0, dst = 0;
while (src < numsSize) {
if (nums[src] != val) {
nums[dst++] = nums[src++];
} else {
src++;
}
}
return dst;
}
// 测试用例
void TestRemoveElement() {
int nums[] = {3, 2, 2, 3};
int val = 3;
int len = removeElement(nums, 4, val);
printf("新数组长度:%d\n", len); // 输出:2
printf("新数组元素:");
for (int i = 0; i < len; i++) {
printf("%d ", nums[i]); // 输出:2 2
}
printf("\n");
}
(2)练习2:合并两个有序数组(LeetCode 88)
题目:给定两个有序整数数组 nums1 和 nums2,将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。已知 nums1 的空间大小足够容纳 m + n 个元素(m 是 nums1 的有效元素个数,n 是 nums2 的元素个数)。
三指针思路(从后往前):定义三个指针 end1(nums1 有效元素的末尾)、end2(nums2 的末尾)、end(nums1 最终数组的末尾)。比较 nums1[end1] 和 nums2[end2],将较大的元素放到 nums1[end] 位置,然后相应指针后移。若 nums2 还有剩余元素,直接拷贝到 nums1 前端。
void merge(int* nums1, int m, int* nums2, int n) {
int end1 = m - 1;
int end2 = n - 1;
int end = m + n - 1;
while (end1 >= 0 && end2 >= 0) {
if (nums1[end1] > nums2[end2]) {
nums1[end--] = nums1[end1--];
} else {
nums1[end--] = nums2[end2--];
}
}
// 若nums2还有剩余元素,直接拷贝到nums1前端
while (end2 >= 0) {
nums1[end--] = nums2[end2--];
}
}
// 测试用例
void TestMerge() {
int nums1[6] = {1, 2, 3, 0, 0, 0};
int nums2[3] = {2, 5, 6};
merge(nums1, 3, nums2, 3);
printf("合并后nums1:");
for (int i = 0; i < 6; i++) {
printf("%d ", nums1[i]); // 输出:1 2 2 3 5 6
}
printf("\n");
}
(3)练习3:删除有序数组中的重复项(LeetCode 26)
题目:给一个有序数组 nums,原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。要求不使用额外数组空间。
双指针思路:定义指针 slow(指向新数组的末尾)和 fast(遍历原数组)。若 nums[fast] != nums[slow],则将 nums[fast] 赋值给 nums[slow+1],并后移 slow;否则仅后移 fast。最终 slow+1 即为新数组的长度。
int removeDuplicates(int* nums, int numsSize) {
if (numsSize == 0) return 0;
int slow = 0, fast = 1;
while (fast < numsSize) {
if (nums[fast] != nums[slow]) {
nums[++slow] = nums[fast];
}
fast++;
}
return slow + 1;
}
// 测试用例
void TestRemoveDuplicates() {
int nums[] = {1, 1, 2, 2, 3, 4, 4, 5};
int len = removeDuplicates(nums, 8);
printf("新数组长度:%d\n", len); // 输出:5
printf("新数组元素:");
for (int i = 0; i < len; i++) {
printf("%d ", nums[i]); // 输出:1 2 3 4 5
}
printf("\n");
}
四、顺序表的优缺点与适用场景
| 维度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 访问效率 | 支持随机访问(时间复杂度 O(1)O(1)O(1)) | - | 需要频繁根据下标查询元素的场景 |
| 存储密度 | 存储密度高(无额外指针开销) | - | 对内存空间利用率要求高的场景 |
| 增删操作 | 尾插、尾删效率高(时间复杂度 O(1)O(1)O(1)) | 头插、头删、中间增删效率低(时间复杂度 O(n)O(n)O(n)) | 增删操作主要在尾部的场景(如栈结构的模拟) |
| 空间灵活性 | 静态顺序表:空间固定,无扩容开销 | 静态顺序表:易溢出;动态顺序表:扩容有开销 | 静态顺序表:数据量固定;动态顺序表:数据量动态变化但增删不频繁 |
五、总结
顺序表是线性表的经典实现,基于数组封装了一套完整的增删查改接口。静态顺序表适用于数据量固定的场景,动态顺序表通过扩容支持灵活的数据操作。通过“移除元素”“合并有序数组”“删除重复项”等习题,我们掌握了双指针、三指针等核心技巧在顺序表中的应用。
理解顺序表的本质(数组的封装)和优缺点,能帮助我们在实际开发中根据场景选择合适的数据结构:若需频繁随机访问,选顺序表;若需频繁增删(尤其是头部/中间),则需考虑链表等结构。

被折叠的 条评论
为什么被折叠?



