数据结构:线性表
📌目录
- 🔗 一,线性表的定义与特点
- 📝 二,案例引入
- 📋 三,线性表的类型定义
- 📊 四,线性表的顺序表示与实现
- (一)线性表的顺序存储表示
- (二)顺序表中基本操作的实现
- 🔗 五,线性表的链式表示与实现
- (一)单链表的定义与表示
- (二)单链表基本操作的实现
- (三)循环链表
- ⚖️ 六,顺序表和链表的比较
- (一)空间性能的比较
- (二)时间性能的比较
- 💡 七,线性表的应用
- (一)线性表的合并
- (二)有序表的合并
- 🛠️ 案例分析与实现
🔗 一,线性表的定义与特点
线性表是最基本、最常用的数据结构之一,其定义可概括为:由n(n≥0)个具有相同特性的数据元素组成的有限序列。
核心特点包括:
- 有序性:元素之间存在明确的先后顺序,第一个元素无前驱,最后一个元素无后继,其余元素有且仅有一个前驱和一个后继(如数组
[1,2,3]
中,2的前驱是1,后继是3)。 - 同质性:所有元素属于同一数据类型(如整数列表、字符串列表)。
- 有限性:元素个数n为非负整数,当n=0时称为“空表”。
线性表是现实世界中“序列关系”的抽象,例如学生点名册(按学号排序)、购物清单(按添加顺序)等均符合其特征。
📝 二,案例引入
假设学校教务处需要管理1000名学生的成绩信息,要求支持以下操作:
- 查看第500名学生的成绩;
- 在第300名学生后插入一名转学生的成绩;
- 删除第100名学生的成绩(该生退学);
- 将所有成绩按从高到低排序。
若采用顺序表(数组)存储,查看操作可直接通过下标完成(高效),但插入/删除需移动大量元素(低效);若采用链表存储,插入/删除只需修改指针(高效),但查看第500名学生需从头遍历(低效)。
这个案例揭示了线性表两种存储结构的核心差异——选择合适的实现方式,直接影响程序的效率。
📋 三,线性表的类型定义
线性表的抽象数据类型(ADT)定义如下,包含数据集合及核心操作:
ADT List {数据:线性表L = (a₁, a₂, ..., aₙ),其中aᵢ为数据元素,i为位序(1≤i≤n)操作:1. InitList(&L):初始化空表L2. ListLength(L):返回表L的长度n3. GetElem(L, i, &e):用e返回L中第i个元素4. LocateElem(L, e):返回e在L中首次出现的位序(若不存在返回0)5. ListInsert(&L, i, e):在L的第i个位置插入元素e6. ListDelete(&L, i, &e):删除L的第i个元素,用e返回其值7. PrintList(L):遍历并输出L中所有元素8. DestroyList(&L):销毁表L,释放内存
}
这些操作构成了线性表的基本功能集,具体实现需结合存储结构设计。
📊 四,线性表的顺序表示与实现
(一)线性表的顺序存储表示
顺序表是线性表的顺序存储结构,其核心思想是:用一段连续的内存空间依次存储线性表的元素,类似数组。
-
存储原理:假设每个元素占用
size
字节,第一个元素地址为LOC(a₁)
,则第i个元素的地址为:
LOC(aᵢ) = LOC(a₁) + (i-1)×size
即通过“基地址+偏移量”可直接访问任意元素,称为“随机存取”特性。 -
代码表示(C语言):
#define MAXSIZE 100 // 最大容量 typedef int ElemType; // 元素类型(示例为int) typedef struct {ElemType data[MAXSIZE]; // 存储元素的数组int length; // 当前长度(≤MAXSIZE) } SqList; // 顺序表类型
(二)顺序表中基本操作的实现
-
初始化:
void InitList(SqList *L) {L->length = 0; // 空表长度为0 }
时间复杂度:O(1)
-
插入操作(在第i个位置插入e):
// 步骤:1. 检查i的合法性(1≤i≤length+1);2. 从后往前移动元素;3. 插入e并更新长度 int ListInsert(SqList *L, int i, ElemType e) {if (i < 1 || i > L->length + 1 || L->length == MAXSIZE)return 0; // 插入失败for (int j = L->length; j >= i; j--) {L->data[j] = L->data[j-1]; // 元素后移}L->data[i-1] = e; // 插入元素(数组下标从0开始)L->length++;return 1; // 插入成功 }
时间复杂度:最坏情况(插入到表头)需移动n个元素,故为O(n);平均复杂度O(n)。
-
删除操作(删除第i个元素,用e返回):
// 步骤:1. 检查i的合法性(1≤i≤length);2. 保存被删元素;3. 从前往后移动元素;4. 更新长度 int ListDelete(SqList *L, int i, ElemType *e) {if (i < 1 || i > L->length)return 0; // 删除失败*e = L->data[i-1]; // 保存被删元素for (int j = i; j < L->length; j++) {L->data[j-1] = L->data[j]; // 元素前移}L->length--;return 1; // 删除成功 }
时间复杂度:同插入操作,最坏与平均均为O(n)。
-
按位查找:
ElemType GetElem(SqList L, int i) {return L.data[i-1]; // 直接通过下标访问 }
时间复杂度:O(1)(随机存取的优势)。
🔗 五,线性表的链式表示与实现
(一)单链表的定义与表示
单链表是线性表的链式存储结构,其核心思想是:用离散的内存块(节点)存储元素,节点间通过指针连接形成序列。
-
节点结构:每个节点包含两部分:
- 数据域:存储元素值;
- 指针域:存储下一个节点的地址(或NULL表示末尾)。
-
代码表示(C语言):
typedef int ElemType; typedef struct LNode {ElemType data; // 数据域struct LNode *next; // 指针域(指向后继节点) } LNode, *LinkList; // LinkList为指向LNode的指针类型
-
链表的头指针:通常用
LinkList L
表示单链表,L=NULL
时为空表;为方便操作,常增设头节点(不存储数据,仅作为链表起点),此时空表表示为L->next=NULL
。
(二)单链表基本操作的实现
-
初始化(带头节点):
LinkList InitList() {LinkList L = (LNode*)malloc(sizeof(LNode)); // 分配头节点L->next = NULL; // 头节点指针域为NULLreturn L; }
时间复杂度:O(1)
-
插入操作(在第i个节点前插入e):
// 步骤:1. 找到第i-1个节点(前驱);2. 创建新节点;3. 新节点指针指向i节点;4. 前驱指针指向新节点 int ListInsert(LinkList L, int i, ElemType e) {LNode *p = L;int j = 0;while (p && j < i-1) { // 寻找第i-1个节点p = p->next;j++;}if (!p || j > i-1) return 0; // i不合法LNode *s = (LNode*)malloc(sizeof(LNode)); // 创建新节点s->data = e;s->next = p->next; // 新节点指向i节点p->next = s; // 前驱节点指向新节点return 1; }
时间复杂度:最坏需遍历整个链表,故为O(n)。
-
删除操作(删除第i个节点,用e返回值):
int ListDelete(LinkList L, int i, ElemType *e) {LNode *p = L;int j = 0;while (p->next && j < i-1) { // 寻找第i-1个节点p = p->next;j++;}if (!p->next || j > i-1) return 0; // i不合法LNode *q = p->next; // q指向待删除节点*e = q->data; // 保存数据p->next = q->next; // 前驱节点跳过qfree(q); // 释放内存return 1; }
时间复杂度:O(n)。
-
按位查找:
LNode* GetElem(LinkList L, int i) {LNode *p = L->next;int j = 1;while (p && j < i) { // 从第1个节点开始遍历p = p->next;j++;}return p; // 若i超出范围,返回NULL }
时间复杂度:O(n)(需顺序遍历)。
(三)循环链表
循环链表是单链表的变种,其特点是:最后一个节点的指针域不指向NULL,而是指向头节点,形成环形结构。
-
优势:
- 从任意节点出发可遍历整个链表(适合多端操作场景);
- 判断链表结束的条件是“指针是否回到头节点”(
p->next == L
)。
-
操作示例:合并两个循环链表(A和B):
void MergeList(LinkList A, LinkList B) {LNode *p = A->next; // 保存A的第一个节点LNode *q = B->next; // 保存B的第一个节点A->next = q->next; // A的头节点连接B的第一个节点free(q); // 释放B的头节点B->next = p; // B的尾节点连接A的第一个节点 }
⚖️ 六,顺序表和链表的比较
(一)空间性能的比较
维度 | 顺序表 | 链表 |
---|---|---|
存储空间 | 需预先分配固定大小(可能浪费或溢出) | 动态分配,按需申请(无浪费) |
存储密度 | 1(数据域占100%空间) | <1(需额外存储指针域) |
内存碎片 | 连续空间,碎片少 | 离散分配,可能产生碎片 |
(二)时间性能的比较
操作 | 顺序表 | 链表 | 结论 |
---|---|---|---|
按位查找 | O(1)(随机存取) | O(n)(顺序存取) | 顺序表更适合频繁查找场景 |
插入/删除 | O(n)(需移动元素) | O(1)(仅改指针) | 链表更适合频繁插入/删除场景 |
遍历整个表 | O(n) | O(n) | 效率相同 |
初始化 | O(1) | O(1) | 效率相同 |
💡 七,线性表的应用
(一)线性表的合并
问题:将两个线性表A和B合并为新表C,要求C包含A和B的所有元素(允许重复)。
实现思路:
- 初始化C为空表;
- 将A的所有元素依次插入C;
- 将B的所有元素依次插入C。
顺序表实现(时间复杂度O(n+m),n、m为A、B长度):
void MergeList(SqList A, SqList B, SqList *C) {int i = 0, j = 0, k = 0;while (i < A.length && j < B.length) {C->data[k++] = A.data[i++];C->data[k++] = B.data[j++];}// 处理剩余元素while (i < A.length) C->data[k++] = A.data[i++];while (j < B.length) C->data[k++] = B.data[j++];C->length = k;
}
(二)有序表的合并
问题:将两个非递减有序表A和B合并为一个新的非递减有序表C(去重)。
实现思路:
- 初始化指针i(A的起点)、j(B的起点);
- 比较A[i]和B[j],将较小值插入C(若相等则只插一次);
- 处理剩余元素。
链表实现(时间复杂度O(n+m)):
LinkList MergeSortedList(LinkList A, LinkList B) {LinkList C = InitList(); // 初始化结果表LNode *p = A->next, *q = B->next, *r = C;while (p && q) {if (p->data < q->data) {r->next = p; r = p; p = p->next;} else if (p->data > q->data) {r->next = q; r = q; q = q->next;} else { // 去重:只保留一个r->next = p; r = p; p = p->next;LNode *temp = q; q = q->next; free(temp);}}r->next = p ? p : q; // 拼接剩余元素free(A); free(B); // 释放原表头节点return C;
}
🛠️ 案例分析与实现
问题:设计一个学生成绩管理系统,支持添加、删除、查询、排序功能。
方案选择:
- 若查询频繁(如按学号查成绩),选择顺序表(O(1)查找);
- 若频繁添加/删除(如转学、退学),选择链表(O(1)插入/删除)。
核心代码(链表实现):
// 添加学生(尾插法)
void AddStudent(LinkList L, ElemType id, ElemType score) {LNode *p = L;while (p->next) p = p->next; // 找到尾节点LNode *s = (LNode*)malloc(sizeof(LNode));s->data = score; // 简化:data存储成绩,实际可扩展为结构体s->id = id; // 新增id字段s->next = NULL;p->next = s;
}// 按学号查询成绩
ElemType QueryScore(LinkList L, ElemType id) {LNode *p = L->next;while (p && p->id != id) p = p->next;return p ? p->data : -1; // -1表示未找到
}
总结:线性表的两种实现各有优劣,实际开发中需根据业务场景的“操作频率”选择——查多改少用顺序表,改多查少用链表。