1.2 线性表
基础概念与结构特性
定义 :由 n ( n >= 0 )个数据特性相同的元素构成的有限序列,称为线性表。(线性表是n个元素的有限序列,其中 n 个数据是相同数据类型的。)
分类:顺序表和链表
核心属性:
- 长度: 线性表中实际包含的数据元素个数 n 称为线性表的长度。
- 空表: 当长度 n=0 时,该线性表称为空表。
非空线性表的结构特征:
对于任意非空线性表,其元素序列需满足以下四个结构特性:
- 首元素唯一性: 必存在一个唯一的元素被称作"第一个"元素(也称首元素)。
- 尾元素唯一性: 必存在一个唯一的元素被称作"最后一个"元素(也称尾元素)。
- 前驱性: 除第一个元素外,其余所有元素都只有一个前驱(紧邻其前的元素)。
- 后继性: 除最后一个元素外,其余所有元素都只有一个后继(紧邻其后的元素)。
定义 :用一组连续的内存单元依次存储线性表的各个元素,也就是说,逻辑上 相邻的元素,实际的物理存储空间 也是连续的 。(这和数组类似,在数组里**,'A 后面是 B' (这是逻辑上的前后关系),就意味着 A 的储存单元紧挨着 B 的储存单元(这是物理上的连续排列)。**
一.顺序表 - 定义和初始化以及遍历
(1)顺序表定义
cs
//宏定义与类型定义
#define MAXSIZE 100
typedef int ElemType; //定义 int 的别名为 ElemType (方便后续修改数据类型)
//结构体定义(SeqList)
typedef struct{
ElemType data[MAXSIZE]; //声明了一个长度为MAXSIZE(100)的整数类型的数组
int length; //用于记录当前顺序表中实际存储了多少个元素
}SeqList;
(2)顺序表初始化
cs
//初始化函数 (initList)
void initList(SeqList *L)
{
L->length = 0;
}
通过传入结构体的指针L,将其中存储当前元素数量的 length
成员设置为 0
。这使得当我们声明一个新的顺序表时,它就是空的。
cs
//mian函数
int main(int argc,char const *argv[])
{
//声明一个顺序表并初始化
SeqList list;
initList(&list);
printf("初始化成功,目前长度占用%d\n",list.length);
printf("目前占用内存%zu字节\n",sizeof(list.data)); //打印数组部分占用的字节数
return 0;
}
(3)遍历函数
cs
//遍历
void listElem(SeqList *L)
{
for (int i = 0; i < L->length; i++)
{
printf("%d",L->data[i]);
}
printf("\n");
}
二.顺序表 - 尾部插入元素
cs
//添加元素的核心代码
int appendElem(SeqList *L,ElemType e) //传入顺序表的指针,想要添加的元素
{
if(L->length>=MAXSIZE)
{
printf("顺序表已满\n");
return 0;
}
// 核心逻辑:添加元素
L->data[L->length] = e; //L->data 数组,L->length 数组的下标
L->length++;
return 1;
}
L->data
表示:"找到指针L
所指向的顺序表,并访问它的数据数组部分。"L->length
表示:"找到指针L
所指向的顺序表的长度变量。"
在main函数里调用appendElem函数
cs
//mian函数
int main(int argc,char const *argv[])
{
//声明一个顺序表并初始化
SeqList list;
initList(&list);
printf("初始化成功,目前长度占用%d\n",list.length);
printf("目前占用内存%zu字节\n",sizeof(list.data)); //打印数组部分占用的字节数
//调用函数
appendElem(&list,88);
appendElem(&list,45);
appendElem(&list,43);
appendElem(&list,17);
listElem(&list);
return 0;
}
结果如下:

三.顺序表 - 中间插入元素
cs
//插入元素的核心代码
int insertElem(SeqList *L,int pos,ElemType e)
{
if(L->length >= MAXSIZE)
{
printf("表已经满了\n");
return 0;
}
if(pos < 1 || pos > L->length)
{
printf("插入位置错误\n");
return 0;
}
//核心逻辑
if (pos <= L->length)
{
for (int i = L->length-1; i >= pos-1; i--)
{
L->data[i+1] = L->data[i];
}
L->data[pos-1] = e;
L->length++;
}
return 1;
}
在main函数里调用appendElem函数
cs
int main(int argc, char const* argv[])
{
//声明一个顺序表并初始化
SeqList list;
initList(&list);
printf("初始化成功,目前长度占用%d\n", list.length);
printf("目前占用内存%zu字节\n", sizeof(list.data)); //打印数组部分占用的字节数
appendElem(&list, 88);
appendElem(&list, 45);
appendElem(&list, 43);
appendElem(&list, 17);
listElem(&list);
//调用函数
insertElem(&list,2,18);
listElem(&list);
return 0;
}
结果如下:

四.顺序表 - 删除元素
虽然说是删除,但本质上是覆盖的过程。
34覆盖56,43覆盖34,45覆盖43,12覆盖45,后面就没有元素覆盖12了,所以我们可以看最后顺序表中有两个12,那么我们怎么处理呢?
答案是:长度-1( L->length-- )就可以了
cs
//删除元素的核心逻辑
int deleteElem(SeqList* L, int pos, ElemType* e)
{
*e = L->data[pos - 1]; //*e用来储存被删掉数据的位置(L->data[pos-1])
if (pos < L->length)
{
for (int i = pos; i < L->length; i++)
{
L->data[i - 1] = L->data[i];
}
}
L->length--;
return 1;
}
mian函数中调用
cs
//mian函数
int main(int argc, char const* argv[])
{
//声明变量delData
ElemType delData;
//声明一个顺序表并初始化
SeqList list;
initList(&list);
printf("初始化成功,目前长度占用%d\n", list.length);
printf("目前占用内存%zu字节\n", sizeof(list.data)); //打印数组部分占用的字节数
appendElem(&list, 88);
appendElem(&list, 45);
appendElem(&list, 43);
appendElem(&list, 17);
listElem(&list);
insertElem(&list,2,18);
listElem(&list);
//调用函数
deleteElem(&list, 2, &delData);
printf("被删除的数据为:%d\n", delData);
listElem(&list);
return 0;
}
结果如下:

五.顺序表 - 查找
cs
//查找的核心逻辑
int findElem(SeqList* L,ElemType e)
{
for (int i = 0; i < L->length; i++)
{
if (L->data[i] == e)
{
return i + 1;
}
}
return 0;
}
mian中调用
cs
//mian函数
int main(int argc, char const* argv[])
{
ElemType delData;
//声明一个顺序表并初始化
SeqList list;
initList(&list);
printf("初始化成功,目前长度占用%d\n", list.length);
printf("目前占用内存%zu字节\n", sizeof(list.data)); //打印数组部分占用的字节数
appendElem(&list, 88);
appendElem(&list, 45);
appendElem(&list, 43);
appendElem(&list, 17);
listElem(&list);
insertElem(&list,2,18);
listElem(&list);
deleteElem(&list, 2, &delData);
printf("被删除的数据为:%d\n", delData);
listElem(&list);
//调用
printf("%d\n", findElem(&list, 43));
return 0;
}
结果如下:

六.顺序表 - 动态分配内存地址初始化
cs
//动态分配内存地址初始化
typedef struct {
ElemType* data;// 存储元素数据的指针,指向一块动态分配的内存
int length; // 当前顺序表中元素的数量
} SeqList;
SeqList* initList(SeqList* L)
{
//在堆内存中开辟SeqListd结构体空间
SeqList* L = (SeqList*)malloc(sizeof(SeqList));
//在堆内存中开辟顺序表储存数据的连续空间(相当于上面的data(100))
L->data = (ElemType*)malloc(sizeof(ElemType) * MAXSIZE);
//初始化长度
L->length = 0;
//返回指向新创建的顺序表的指针
return L;
}
将数据从栈内存中转移到堆内存中开辟空间
分析:
SeqList* L = (SeqList*)malloc(sizeof(SeqList));
malloc函数表示在内存当中开辟一片内存空间,默认返回值是void*(通用类型数据的指针),所以要做类型的强制转换(SeqList*),sizeof(SeqList)表示SeqList需要的空间,用SeqList指针接返回的值(SeqList* L)
完整代码实例:
cs
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100
typedef int ElemType;
// 动态分配顺序表结构体
typedef struct {
ElemType* data; // 存储元素数据的指针,指向一块动态分配的内存
int length; // 当前顺序表中元素的数量
} SeqList;
// 动态分配内存地址初始化
SeqList* initList() // 移除参数,直接返回指针
{
// 在堆内存中开辟SeqList结构体空间
SeqList* L = (SeqList*)malloc(sizeof(SeqList));
if (L == NULL) {
printf("结构体内存分配失败\n");
return NULL;
}
// 在堆内存中开辟顺序表储存数据的连续空间
L->data = (ElemType*)malloc(sizeof(ElemType) * MAXSIZE);
if (L->data == NULL) {
printf("数据内存分配失败\n");
free(L); // 释放之前分配的结构体
return NULL;
}
// 初始化长度
L->length = 0;
// 返回指向新创建的顺序表的指针
return L;
}
// 销毁顺序表
void destroyList(SeqList* L) {
if (L != NULL) {
if (L->data != NULL) {
free(L->data); // 先释放数据数组
}
free(L); // 再释放结构体
}
}
// 遍历函数
void listElem(SeqList* L) {
if (L->length == 0) {
printf("顺序表为空\n");
return;
}
printf("顺序表元素:");
for (int i = 0; i < L->length; i++) {
printf("%d ", L->data[i]);
}
printf("\n");
}
// 尾部插入元素
int appendElem(SeqList* L, ElemType e) {
if (L->length >= MAXSIZE) {
printf("顺序表已满\n");
return 0;
}
L->data[L->length] = e;
L->length++;
return 1;
}
// 主函数测试
int main() {
// 创建顺序表
SeqList* list = initList();
if (list == NULL) {
printf("顺序表初始化失败\n");
return -1;
}
printf("顺序表初始化成功,当前长度:%d\n", list->length);
// 添加元素
appendElem(list, 10);
appendElem(list, 20);
appendElem(list, 30);
appendElem(list, 40);
// 遍历显示
listElem(list);
printf("当前长度:%d\n", list->length);
// 销毁,避免内存泄漏
destroyList(list);
return 0;
}
顺序表的核心概念总结
基于数组 (Array-based) :数据存储在一个固定大小的数组中,这保证了物理存储是连续的。
随机访问 (Random Access) :正因为物理地址连续,我们可以通过索引(L->data[i]
)高效地访问任何一个元素,时间复杂度为 O(1)。
插入和删除效率低 :appendElem
(即在表尾添加)是高效的 O(1)操作(只要没满),但如果在表头或中间 插入/删除元素,需要将后面所有元素整体移动,时间复杂度是 O(n)。
但缺点是:
容量固定 :一旦定义了 MAXSIZE
,此表的最大容量就是固定的。当 length == MAXSIZE
时,表满。
插入/删除效率低:平均需要移动半个表,时间复杂度O(n)。
-
插入时的"多米诺效应":在位置i插入新元素,需要将i之后的所有元素都向后移动一位
-
删除时的"补位操作":删除位置i的元素,需要将i之后的所有元素都向前移动一位
那么,有没有一种数据结构,既能保持线性表"逻辑上相邻"的特性,又能在物理存储上摆脱连续性的束缚,从而解决插入删除的效率问题呢?
答案是肯定的------这就是我们接下来要重点学习的链表(Linked List)。