数据结构实战:顺序表全解析 - 从零实现到性能分析
1.2 线性表
基础概念与结构特性
定义:由 n ( n >= 0 )个数据特性相同的元素构成的有限序列,称为线性表。(线性表是n个元素的有限序列,其中 n 个数据是相同数据类型的。)
分类:顺序表和链表
核心属性:
- 长度: 线性表中实际包含的数据元素个数 n 称为线性表的长度。
- 空表: 当长度 n=0 时,该线性表称为空表。
非空线性表的结构特征:
对于任意非空线性表,其元素序列需满足以下四个结构特性:
- 首元素唯一性: 必存在一个唯一的元素被称作“第一个”元素(也称首元素)。
- 尾元素唯一性: 必存在一个唯一的元素被称作“最后一个”元素(也称尾元素)。
- 前驱性: 除第一个元素外,其余所有元素都只有一个前驱(紧邻其前的元素)。
- 后继性: 除最后一个元素外,其余所有元素都只有一个后继(紧邻其后的元素)。
定义:用一组连续的内存单元依次存储线性表的各个元素,也就是说,逻辑上相邻的元素,实际的物理存储空间也是连续的。(这和数组类似,在数组里,'A 后面是 B' (这是逻辑上的前后关系),就意味着 A 的储存单元紧挨着 B 的储存单元(这是物理上的连续排列)。
一.顺序表 - 定义和初始化以及遍历
(1)顺序表定义
//宏定义与类型定义
#define MAXSIZE 100
typedef int ElemType; //定义 int 的别名为 ElemType (方便后续修改数据类型)//结构体定义(SeqList)
typedef struct{ElemType data[MAXSIZE]; //声明了一个长度为MAXSIZE(100)的整数类型的数组int length; //用于记录当前顺序表中实际存储了多少个元素
}SeqList;
(2)顺序表初始化
//初始化函数 (initList)
void initList(SeqList *L)
{L->length = 0;
}
通过传入结构体的指针L,将其中存储当前元素数量的 length
成员设置为 0
。这使得当我们声明一个新的顺序表时,它就是空的。
//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)遍历函数
//遍历
void listElem(SeqList *L)
{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->data 数组,L->length 数组的下标L->length++;return 1;
}
L->data
表示:“找到指针L
所指向的顺序表,并访问它的数据数组部分。”L->length
表示:“找到指针L
所指向的顺序表的长度变量。”
在main函数里调用appendElem函数
//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;
}
结果如下:
三.顺序表 - 中间插入元素
//插入元素的核心代码
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函数
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-- )就可以了
//删除元素的核心逻辑
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函数中调用
//mian函数
int main(int argc, char const* argv[])
{//声明变量delDataElemType 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;
}
结果如下:
五.顺序表 - 查找
//查找的核心逻辑
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中调用
//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;
}
结果如下:
六.顺序表 - 动态分配内存地址初始化
//动态分配内存地址初始化
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)
完整代码实例:
#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)。