一.顺序表的概念:
概念:顺序表是⽤⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采⽤数组存储
顺序表和数组的区别?
顺序表的底层结构是数组,对数组的封装,实现了常⽤的增删改查等接
二.顺序表的分类
2.1静态顺序表
概念:使⽤定⻓数组存储元素,大小在编译时就确定了。
代码如下:
#define N 10 typedef int SLDataType; typedef struct SeqList { SLDataType array[N]; // 定长数组 size_t size; // 有效数据个数 } SeqList;这里的
N是 10,最多就只能存 10 个数据。缺点挺明显的:把 N 开大了,数据少的时候浪费空间;把 N 开小了,数据多的时候又不够用。只适合那种事先确切知道数据量的场景,平时写项目基本用不上。
2.2动态顺序表
动态顺序表用malloc或realloc动态申请空间,数据多了就扩容。
typedef int SLDataType; typedef struct SeqList { SLDataType* a; // 指向动态开辟的数组 size_t size; // 有效数据个数 size_t capacity; // 容量大小 } SeqList;这里的
a指向一块堆空间,size记录当前存了多少个数据,capacity记录总共能存多少个。当size达到capacity时,就调用realloc扩容。
SLDataType是int的别名,这样做的好处是如果后面想把 int 改成 char 或者其他类型,只需要在这一行改就行了,不用到处找。
size_t是无符号整数类型,专门用来表示长度和大小。
2.3 动态顺序表接口的实现
1.创建三个文件:
2.头文件SeqList.h代码如下
// 文件名:SeqList.h // 功能:动态顺序表(动态数组)头文件 #pragma once // 头文件包含 #include <assert.h> // assert() 断言 #include <stdlib.h> // realloc(), free() 等内存管理函数 #include<stdio.h> // 类型定义 // 顺序表存储的数据类型(便于后续修改) typedef int SLDataType; // 动态顺序表结构体 typedef struct SeqList { SLDataType* a; // 指向动态开辟的数组(存储数据) size_t size; // 有效数据个数(当前已存储的元素数量) size_t capacity; // 容量空间大小(当前最多能存储的元素数量) } SeqList; // 基本增删查改接口 // 初始化和销毁 void SeqListInit(SeqList* ps); // 顺序表初始化 void SeqListDestroy(SeqList* ps); // 顺序表销毁(释放内存) // 容量管理 void CheckCapacity(SeqList* ps); // 检查空间,如果满了则进行增容 // 尾部操作 void SeqListPushBack(SeqList* ps, SLDataType x); // 尾插 void SeqListPopBack(SeqList* ps); // 尾删 // 头部操作 void SeqListPushFront(SeqList* ps, SLDataType x); // 头插 void SeqListPopFront(SeqList* ps); // 头删 // 指定位置操作 void SeqListInsert(SeqList* ps, size_t pos, SLDataType x); // 在 pos 位置插入 x void SeqListErase(SeqList* ps, size_t pos); // 删除 pos 位置的值 // 查找与访问 int SeqListFind(SeqList* ps, SLDataType x); // 查找数据,返回下标(-1表示未找到) void SeqListAt(SeqList* ps, size_t pos, SLDataType x); // 修改指定下标位置的数据 // 工具函数 void SeqListPrint(SeqList* ps); // 打印顺序表所有元素 size_t SeqListSize(const SeqList* ps); // 查看有效数据个数(只读)3.在SeqList.c实现各个接口函数
1.初始化顺序表
//初始化顺序表 void SeqListInit(SeqList* ps); { assert(ps); // 断言 -- 防止传进来的指针为空 ps->a = NULL; // 初始化顺序表为空 ps->size = 0; // 初始数据个数为0 ps->capacity = 0; // 初始空间容量为0 }2. 销毁顺序表
void SeqListDestroy(SeqList* ps) { assert(ps); // 断言 -- 防止传进来的指针为空 free(ps->a); // 释放动态开辟的空间 ps->a = NULL; // 置空 ps->size = 0; // 数据个数置为0 ps->capacity = 0; // 空间容量大小置为0 }3. 容量管理( 检查空间,如果满了进行增容)
//检查容量,满了则增容 // 扩容策略:容量为0时扩容到4,否则扩容为原来的2倍 // 为什么是2倍?—— 避免频繁扩容(代价高),同时避免空间浪费(3倍/4倍浪费太多) void SLCheckCapacity(SeqList* ps) { assert(ps); // 防御性检查 if (ps->size == ps->capacity) // 空间已满,需要增容 { // 计算新容量:原来为0则给4,否则翻倍 size_t newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; // 使用 realloc 扩容 SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType) * newCapacity); // 检查 realloc 是否成功 if (tmp == NULL) { perror("SLCheckCapacity::realloc failed"); return; } ps->a = tmp; // 更新指针 ps->capacity = newCapacity; // 更新容量 } }realloc 的三种情况
| 情况 | realloc行为 | 示例 |
|---|---|---|
| 传入 NULL | 等价于malloc(size),开辟新空间 | realloc(NULL, 40)→ 开辟 40 字节 |
| 传入有效指针,且空间足够 | 原地扩容,返回原地址 | realloc(ptr, 80)→ 在原地址后追加空间 |
| 传入有效指针,但空间不足 | 找新地址,拷贝数据,释放旧空间,返回新地址 | realloc(ptr, 200)→ 迁移到新地址 |
4. 顺序表尾插
// 顺序表尾插 void SeqListPushBack(SeqList* ps, SLDateType x) { // 方法一:直接尾插 /* CheckCapacity(ps); // 检查容量,不够则扩容 ps->a[ps->size] = x; // 在末尾位置插入数据 ps->size++; // 有效数据个数+1 */ // 方法二:复用 SeqListInsert,在 size 位置插入 SeqListInsert(ps, ps->size, x); }
作用:在顺序表末尾插入一个元素。
方法一:先检查容量(
CheckCapacity),不足则扩容;然后在ps->size位置赋值,最后size++。方法二:复用
SeqListInsert函数,在ps->size位置插入,也就是末尾插入,代码更简洁。时间复杂度:O(1)(不考虑扩容的情况)。
5. 顺序表尾删
// 顺序表尾删 void SeqListPopBack(SeqList* ps) { assert(ps); // 断言指针不为空 // 方法一:暴力检查(推荐) /* assert(ps->size > 0); // 顺序表不能为空 ps->size--; // 有效数据个数-1,逻辑上删除最后一个元素 */ // 方法二:复用 SeqListErase,删除最后一个元素 SeqListErase(ps, ps->size - 1); }
作用:删除顺序表最后一个元素。
核心思想:只需将
size减 1,最后一个元素在逻辑上就不存在了。注意:不需要把最后一个元素置为 0,因为:
数据类型不确定(可能是结构体、浮点数等)
即使置 0 也没意义,浪费性能
错误检查方式:
温柔检查:
if (ps->size > 0) { ps->size--; }(不推荐,错误被掩盖)暴力检查(推荐):
assert(ps->size > 0);出错直接报错,方便调试时间复杂度:O(1)。
温柔检查(不推荐)
void SeqListPopBack(SeqList* ps) { assert(ps); if (ps->size > 0) { // 有数据才删 ps->size--; } }问题:顺序表为空时,什么都不做就返回了,调用者不知道删除失败,bug被隐藏。
暴力检查(推荐)
void SeqListPopBack(SeqList* ps) { assert(ps); assert(ps->size > 0); // 为空直接报错终止 ps->size--; }优点:出错立即报错并显示位置,方便调试。
6. 顺序表头插
// 顺序表头插 第一种: void SeqListPushFront(SeqList* ps, SLDateType x) { assert(ps); // 断言指针不为空 CheckCapacity(ps); // 检查容量,不够则扩容 // 将现有元素从后往前依次向后移动一位 for (int i = ps->size; i > 0; i--) { ps->a[i] = ps->a[i - 1]; } ps->a[0] = x; // 在头部插入新元素 ps->size++; // 有效数据个数+1 } 第二种: void SeqListPushFront(SeqList* ps, SLDateType x) { SeqListInsert(ps, 0, x); // 在下标0插入 }
作用:在顺序表头部插入一个元素。
实现步骤:
检查容量是否充足
将所有已有元素从后往前向后移动一位(腾出第一个位置)
在
a[0]位置插入新元素
size++为什么从后往前移动?如果从前往后移动,后面的数据会被覆盖。
时间复杂度:O(n),因为需要移动所有元素。
7.顺序表头删
// 顺序表头删 void SeqListPopFront(SeqList* ps) { 方法一: assert(ps); // 断言指针不为空 assert(ps->size > 0); // 顺序表不能为空 // 将后面的元素从前往后依次前移一位 for (int i = 0; i < ps->size - 1; i++) { ps->a[i] = ps->a[i + 1]; } ps->size--; // 有效数据个数-1 // 方法二: SeqListErase(ps, 0); }
作用:删除顺序表第一个元素。
实现步骤:
检查顺序表是否为空
将后续元素从前往后依次前移一位,覆盖第一个元素
size--时间复杂度:O(n)。
8.打印顺序表
void SeqListPrint(SeqList* ps) { assert(ps); // 防止传空指针进来 for (int i = 0; i < ps->size; i++) { printf("%d ", ps->a[i]); // 一个一个打印出来 } printf("\n"); // 打印完换行 }就是遍历数组,把每个元素打印出来。
ps->size记录着有几个有效数据,所以循环到size就行。最后加个换行,好看一点。
9.查找指定值
int SeqListFind(SeqList* ps, SLDateType x) { assert(ps); for (int i = 0; i < ps->size; i++) { if (ps->a[i] == x) { return i; // 找到了,返回下标 } } return -1; // 找遍了都没有,返回-1 }从头到尾遍历,找到了就直接返回下标。如果循环结束了还没找到,说明没有这个值,返回 -1(因为下标从0开始,-1肯定不可能是合法位置)。外面调用的时候判断一下返回值是不是 -1 就知道找没找到了。
10.在 pos 位置插入 x
void SeqListInsert(SeqList* ps, size_t pos, SLDateType x) { assert(ps); // 防止传空指针 assert(pos >= 0 && pos <= ps->size); // 检查位置合不合法 CheckCapacity(ps); // 空间不够就扩容 //第一种写法:while 循环 size_t end = ps->size; while (end > pos) { ps->a[end] = ps->a[end - 1]; // 从后往前依次往后挪 end--; // 继续挪下一个 } // 第二种写法:for 循环 for (size_t i = ps->size; i > pos; i--) { ps->a[i] = ps->a[i - 1]; // 从后往前依次往后挪 } ps->a[pos] = x; // 把新数据放进去 ps->size++; // 有效数据+1 }插入的核心就是挪数据。比如要在下标2的位置插入,那原来的2、3、4...都要往后挪,给新元素腾地方。
注意:
pos可以是size,就是在末尾插入,相当于尾插挪的时候必须从后往前挪,如果从前往后挪会把后面的数据覆盖掉
举个例子: [1,2,3,4],在 pos=1 插入 99 从后往前挪:4-->3位置,3-->2位置,2-->1位置 然后 a[1]=99 结果:[1,99,2,3,4]在这里我踩过一些坑:
一开始我是这么写的:
//错误写法 for (int i = ps->size - 1; i >= pos; i--) { ps->a[i + 1] = ps->a[i]; }看着好像没问题,但是当
pos = 0的时候,i会一直减到-1,然后i >= 0这个条件,-1 >= 0应该是假的,循环就该停了。但是如果i是int类型,pos是size_t(无符号),比较的时候i会被转成无符号数,-1就变成了一个超级大的数,肯定大于等于pos,循环就停不下来了,直接越界访问崩溃。
那应该怎么改呢?
// 正确写法 for (size_t i = ps->size; i > pos; i--) { ps->a[i] = ps->a[i - 1]; }
i从size开始,不是size-1;循环条件是
i > pos,当i减到0时,如果pos = 0,条件0 > 0是假的,循环结束;永远不会出现
i = -1的情况.
11. 删除 pos 位置的值
// 顺序表删除pos位置的值 void SeqListErase(SeqList* psl, size_t pos) { assert(psl); // 断言 assert(psl->size > 0); // 顺序表不能为空 assert(pos >= 0 && pos < psl->size); // 检查pos下标的合法性 // 第一种写法: /* size_t start = pos; while (start < psl->size-1) { psl->a[start] = psl->a[start + 1]; start++; } */ //第二种写法: size_t i = 0; for (i = pos + 1; i < psl->size; i++) // 将pos位置后面的数据依次向前挪动一位 { psl->a[i - 1] = psl->a[i]; } psl->size--; // 有效数据个数-1 }两种写法其实一样
一个用while,一个用for,做的事情完全相同:
while版本:先定义end = size,每次挪完end--,直到end不大于pos
for版本:i从size开始,每次i--,直到i不大于pos我一般用
for,写起来短一点,看起来也舒服。
12.查看有效数据个数
// 查看顺序表中有多少个有效数据 size_t SeqListSize(const SeqList* ps) { assert(ps); return ps->size; }13.修改指定位置的数
// 修改指定下标位置的数据 void SeqListAt(SeqList* ps, size_t pos, SLDateType x) { assert(ps); assert(ps->size > 0); // 表不能是空的 assert(pos >= 0 && pos < ps->size);// 位置要在范围内 ps->a[pos] = x; // 直接改掉 }那为啥不直接访问?
刚学的时候我也纳闷,明明sl.size直接就能拿到,为啥非要写个函数去取?
原因如下:
1. 安全
万一哪天改代码,把size这个变量名换了,或者结构体变了,所有直接访问的地方都得改,麻烦死了。用函数的话只改这一个地方就行。2. 好加检查
比如修改数据的时候,直接在函数里加上assert判断位置对不对,你要是直接访问数组赋值,位置错了可能都不知道。3. 规范
不让外面随便动内部数据,都通过函数接口来操作,不容易出乱子。
补充:关于数组越界
数组越界不是每次都会报错
越界读:
printf("%d", ps->a[ps->size]); // 多读了一个这种一般不会崩,因为只是看看,没改什么东西,系统不太管你。但读出来的是垃圾数据,可能会影响程序逻辑。
越界写:
ps->a[ps->size] = 10; // 在size位置写数据(没扩容)这个就比较危险了,相当于在别人的地盘乱写东西。如果改到了系统用来标记内存边界的数据,程序可能直接就崩了。
重点:系统检查越界有点像"抽查",不是每次都管,所以不能指望系统帮你发现错误,自己写代码的时候要小心。
三.代码整合:
SeqList.h(头文件)
#pragma once #include <stdio.h> #include <assert.h> #include <stdlib.h> // 类型定义 typedef int SLDateType; // 顺序表结构体 typedef struct SeqList { SLDateType* a; // 指向动态数组 int size; // 有效数据个数 int capacity; // 容量大小 } SeqList; // 函数声明 // 1. 初始化 void SeqListInit(SeqList* ps); // 2. 销毁 void SeqListDestroy(SeqList* ps); // 3. 检查容量 void CheckCapacity(SeqList* ps); // 4. 打印 void SeqListPrint(SeqList* ps); // 5. 尾插 void SeqListPushBack(SeqList* ps, SLDateType x); // 6. 尾删 void SeqListPopBack(SeqList* ps); // 7. 头插 void SeqListPushFront(SeqList* ps, SLDateType x); // 8. 头删 void SeqListPopFront(SeqList* ps); // 9. 查找 int SeqListFind(SeqList* ps, SLDateType x); // 10. 指定位置插入 void SeqListInsert(SeqList* ps, size_t pos, SLDateType x); // 11. 指定位置删除 void SeqListErase(SeqList* ps, size_t pos); // 12. 获取有效数据个数 size_t SeqListSize(const SeqList* ps); // 13. 修改指定位置的数据 void SeqListAt(SeqList* ps, size_t pos, SLDateType x);SeqList.c(具体实现)
#include "SeqList.h" //1. 初始化 void SeqListInit(SeqList* ps) { assert(ps); ps->a = NULL; ps->size = 0; ps->capacity = 0; } // 2. 销毁 void SeqListDestroy(SeqList* ps) { assert(ps); free(ps->a); ps->a = NULL; ps->size = 0; ps->capacity = 0; } // 3. 检查容量 void CheckCapacity(SeqList* ps) { assert(ps); if (ps->size == ps->capacity) { int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; SLDateType* tmp = (SLDateType*)realloc(ps->a, newCapacity * sizeof(SLDateType)); if (tmp == NULL) { printf("扩容失败\n"); exit(-1); } ps->a = tmp; ps->capacity = newCapacity; } } // 4. 打印 void SeqListPrint(SeqList* ps) { assert(ps); for (int i = 0; i < ps->size; i++) { printf("%d ", ps->a[i]); } printf("\n"); } // 5. 尾插 void SeqListPushBack(SeqList* ps, SLDateType x) { SeqListInsert(ps, ps->size, x); // 在size位置插入,就是尾插 } // 6. 尾删 void SeqListPopBack(SeqList* ps) { assert(ps); assert(ps->size > 0); // 不能删空的 SeqListErase(ps, ps->size - 1); // 删最后一个 } // 7. 头插 void SeqListPushFront(SeqList* ps, SLDateType x) { SeqListInsert(ps, 0, x); // 在0位置插入,就是头插 } // 8. 头删 void SeqListPopFront(SeqList* ps) { assert(ps); assert(ps->size > 0); SeqListErase(ps, 0); // 删第0个 } // 9. 查找 int SeqListFind(SeqList* ps, SLDateType x) { assert(ps); for (int i = 0; i < ps->size; i++) { if (ps->a[i] == x) { return i; // 找到了返回下标 } } return -1; // 没找到返回-1 } // 10. 指定位置插入 void SeqListInsert(SeqList* ps, size_t pos, SLDateType x) { assert(ps); assert(pos >= 0 && pos <= ps->size); // pos要在0到size之间 CheckCapacity(ps); // 空间不够就扩容 // 从后往前挪,给新元素腾位置 for (size_t i = ps->size; i > pos; i--) { ps->a[i] = ps->a[i - 1]; } ps->a[pos] = x; ps->size++; } // 11. 指定位置删除 void SeqListErase(SeqList* ps, size_t pos) { assert(ps); assert(pos >= 0 && pos < ps->size); // pos要在0到size-1之间 // 从前往后挪,覆盖掉要删的那个 for (size_t i = pos; i < ps->size - 1; i++) { ps->a[i] = ps->a[i + 1]; } ps->size--; } // 12. 获取有效数据个数 size_t SeqListSize(const SeqList* ps) { assert(ps); return ps->size; } // 13. 修改指定位置的数据 void SeqListAt(SeqList* ps, size_t pos, SLDateType x) { assert(ps); assert(ps->size > 0); // 表不能是空的 assert(pos >= 0 && pos < ps->size);// 位置要在范围内 ps->a[pos] = x; }test.c(测试文件)
#include "SeqList.h" // 测试1:初始化 void TestSeqList1() { printf("测试初始化\n"); SeqList sl; SeqListInit(&sl); printf("size = %d, capacity = %d\n", sl.size, sl.capacity); SeqListDestroy(&sl); } // 测试2:销毁 void TestSeqList2() { printf("测试销毁\n"); SeqList sl; SeqListInit(&sl); SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); printf("销毁前 size = %d\n", sl.size); SeqListDestroy(&sl); printf("销毁后 size = %d\n", sl.size); } // 测试3:打印 void TestSeqList3() { printf("测试打印\n"); SeqList sl; SeqListInit(&sl); SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); printf("打印数据:"); SeqListPrint(&sl); SeqListDestroy(&sl); } // 测试4:头插头删 void TestSeqList4() { SeqList sl; SeqListInit(&sl); SeqListPushFront(&sl, 1); SeqListPushFront(&sl, 2); SeqListPushFront(&sl, 3); SeqListPrint(&sl); SeqListPopFront(&sl); SeqListPrint(&sl); SeqListDestroy(&sl); } // 测试5:尾插尾删 void TestSeqList5() { SeqList sl; SeqListInit(&sl); SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPrint(&sl); SeqListPopBack(&sl); SeqListPrint(&sl); SeqListDestroy(&sl); } // 测试6:混着来 void TestSeqList6() { SeqList sl; SeqListInit(&sl); SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushFront(&sl, 0); SeqListPushBack(&sl, 3); SeqListPrint(&sl); SeqListPopFront(&sl); SeqListPopBack(&sl); SeqListPrint(&sl); SeqListDestroy(&sl); } // 测试7:指定位置插入 void TestSeqList7() { SeqList sl; SeqListInit(&sl); SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 4); SeqListPushBack(&sl, 5); SeqListPrint(&sl); SeqListInsert(&sl, 2, 3); SeqListPrint(&sl); SeqListDestroy(&sl); } // 测试8:指定位置删除 void TestSeqList8() { SeqList sl; SeqListInit(&sl); for (int i = 1; i <= 5; i++) { SeqListPushBack(&sl, i); } SeqListPrint(&sl); SeqListErase(&sl, 2); SeqListPrint(&sl); SeqListDestroy(&sl); } // 测试9:查找 void TestSeqList9() { SeqList sl; SeqListInit(&sl); SeqListPushBack(&sl, 10); SeqListPushBack(&sl, 20); SeqListPushBack(&sl, 30); int idx = SeqListFind(&sl, 20); printf("20的位置:%d\n", idx); idx = SeqListFind(&sl, 100); printf("100的位置:%d\n", idx); SeqListDestroy(&sl); } // 测试10:修改数据 void TestSeqList10() { SeqList sl; SeqListInit(&sl); SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); SeqListPushBack(&sl, 3); SeqListPrint(&sl); SeqListAt(&sl, 1, 99); SeqListPrint(&sl); SeqListDestroy(&sl); } // 测试11:有效数据个数 void TestSeqList11() { SeqList sl; SeqListInit(&sl); printf("空表大小:%d\n", SeqListSize(&sl)); SeqListPushBack(&sl, 1); SeqListPushBack(&sl, 2); printf("插了两个后:%d\n", SeqListSize(&sl)); SeqListPopBack(&sl); printf("删了一个后:%d\n", SeqListSize(&sl)); SeqListDestroy(&sl); } int main() { TestSeqList1(); TestSeqList2(); TestSeqList3(); TestSeqList4(); TestSeqList5(); TestSeqList6(); TestSeqList7(); TestSeqList8(); TestSeqList9(); TestSeqList10(); TestSeqList11(); return 0; }