线性表
2.1 线性表的定义
线性表是具有相同数据类型的 n(n ≥ 0)个数据元素的有限序列。记为 L = (a₁, a₂, ..., aₙ),其中 a₁ 是第一个元素(表头),aₙ 是最后一个元素(表尾)。
特点:
- 元素个数有限。
- 元素具有相同数据类型。
- 元素具有逻辑上的顺序性,除首尾元素外每个元素有且仅有一个直接前驱和一个直接后继。
基本操作:
InitList(&L):初始化线性表ListInsert(&L, i, e):在第 i 个位置插入元素 eListDelete(&L, i, &e):删除第 i 个元素,返回被删元素LocateElem(L, e):查找元素 e,返回位置GetElem(L, i):获取第 i 个位置的元素Length(L):返回线性表长度PrintList(L):输出线性表DestroyList(&L):销毁线性表
💡 记忆技巧:线性表就像"排成一队的人",每个人的位置是固定的,可以报数、插入、离队。
2.2 顺序表
顺序表是用顺序存储方式实现的线性表,即用连续的存储单元依次存储元素。
静态分配
#define MaxSize 50
typedef struct {
ElemType data[MaxSize];
int length;
} SqList;
长度固定,不可改变。如果数组满了,无法插入新元素。
动态分配
#define InitSize 100
typedef struct {
ElemType *data;
int maxSize; // 最大容量
int length; // 当前长度
} SqList;
通过 malloc 和 realloc 动态分配空间,可在运行时扩充容量。
顺序表的基本操作
插入操作
bool ListInsert(SqList &L, int i, ElemType e) {
if (i < 1 || i > L.length + 1) return false;
if (L.length >= MaxSize) return false;
for (int j = L.length; j >= i; j--)
L.data[j] = L.data[j-1];
L.data[i-1] = e;
L.length++;
return true;
}
时间复杂度:最好 O(1)(表尾插入),最坏 O(n)(表头插入),平均 O(n)。
删除操作
bool ListDelete(SqList &L, int i, ElemType &e) {
if (i < 1 || i > L.length) return false;
e = L.data[i-1];
for (int j = i; j < L.length; j++)
L.data[j-1] = L.data[j];
L.length--;
return true;
}
时间复杂度:最好 O(1)(删除表尾),最坏 O(n)(删除表头),平均 O(n)。
按值查找
int LocateElem(SqList L, ElemType e) {
for (int i = 0; i < L.length; i++)
if (L.data[i] == e) return i + 1;
return 0;
}
时间复杂度:最好 O(1),最坏 O(n),平均 O(n)。
📌 408考点提示:顺序表插入、删除时元素移动次数的计算是常考题型。平均移动次数 = n/2(插入)或 (n-1)/2(删除)。
2.3 链表
链表是用链式存储方式实现的线性表,通过指针将各结点连接。
单链表
typedef struct LNode {
ElemType data;
struct LNode *next;
} LNode, *LinkList;
头插法建立单链表
LinkList List_HeadInsert(LinkList &L) {
L = (LNode*)malloc(sizeof(LNode));
L->next = NULL;
int x;
scanf("%d", &x);
while (x != 9999) {
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = x;
s->next = L->next;
L->next = s;
scanf("%d", &x);
}
return L;
}
头插法得到的链表顺序与输入顺序相反,常用于链表逆置。
尾插法建立单链表
LinkList List_TailInsert(LinkList &L) {
L = (LNode*)malloc(sizeof(LNode));
LNode *r = L; // 尾指针
int x;
scanf("%d", &x);
while (x != 9999) {
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = x;
r->next = s;
r = s;
scanf("%d", &x);
}
r->next = NULL;
return L;
}
尾插法得到的链表顺序与输入顺序一致,需要维护一个尾指针。
按序号查找
LNode *GetElem(LinkList L, int i) {
if (i < 0) return NULL;
LNode *p = L;
int j = 0;
while (p && j < i) {
p = p->next;
j++;
}
return p;
}
插入操作(在第 i 个位置插入 e)
bool Insert(LinkList &L, int i, ElemType e) {
LNode *p = GetElem(L, i-1);
if (!p) return false;
LNode *s = (LNode*)malloc(sizeof(LNode));
s->data = e;
s->next = p->next;
p->next = s;
return true;
}
时间复杂度 O(n)(主要耗在查找上)。
删除操作(删除第 i 个结点)
bool Delete(LinkList &L, int i, ElemType &e) {
LNode *p = GetElem(L, i-1);
if (!p || !p->next) return false;
LNode *q = p->next;
e = q->data;
p->next = q->next;
free(q);
return true;
}
时间复杂度 O(n)。
双链表
typedef struct DNode {
ElemType data;
struct DNode *prior, *next;
} DNode, *DLinkList;
双链表既有前驱指针又有后继指针,可以双向遍历。
双链表的插入(在 p 之后插入 s)
s->next = p->next;
s->prior = p;
if (p->next) p->next->prior = s;
p->next = s;
双链表的删除(删除 p 的后继结点 q)
p->next = q->next;
if (q->next) q->next->prior = p;
free(q);
循环链表
- 循环单链表:最后一个结点的 next 指向头结点(或第一个结点)。判空条件:
L->next == L。 - 循环双链表:头结点的 prior 指向尾结点,尾结点的 next 指向头结点。判空条件:
L->next == L && L->prior == L。
静态链表
用数组实现的链表结构,通过"游标"(数组下标)代替指针:
#define MaxSize 50
typedef struct {
ElemType data;
int next; // 游标
} SLinkList[MaxSize];
静态链表适用于不支持指针的高级语言,如早期的 BASIC。
2.4 顺序表与链表的比较
| 比较维度 | 顺序表 | 链表 |
|---|---|---|
| 存取方式 | 随机存取(O(1)) | 顺序存取(O(n)) |
| 插入/删除 | 需移动大量元素,O(n) | 只需修改指针,O(1) |
| 存储密度 | 高(=1,无额外空间) | 低(<1,需要存指针) |
| 空间分配 | 静态分配或动态分配,可能造成浪费 | 动态分配,灵活 |
| 适用场景 | 频繁按位查找、对存储密度要求高 | 频繁插入删除、长度变化大 |
💡 记忆技巧:"顺序表找得快,链表改得快"——这是选择时的根本原则。
2.5 408高频考点
链表逆置
将单链表就地逆置(头插法思路):
LinkList Reverse(LinkList L) {
LNode *p = L->next;
LNode *q;
L->next = NULL;
while (p) {
q = p->next;
p->next = L->next;
L->next = p;
p = q;
}
return L;
}
时间复杂度 O(n),空间复杂度 O(1)。
链表合并
合并两个递增有序单链表为一个递减有序链表(头插法):
LinkList Merge(LinkList &A, LinkList &B) {
LNode *p = A->next, *q = B->next;
LinkList C = A;
C->next = NULL;
free(B);
while (p && q) {
if (p->data <= q->data) {
LNode *r = p->next;
p->next = C->next;
C->next = p;
p = r;
} else {
LNode *r = q->next;
q->next = C->next;
C->next = q;
q = r;
}
}
while (p) {
LNode *r = p->next;
p->next = C->next;
C->next = p;
p = r;
}
while (q) {
LNode *r = q->next;
q->next = C->next;
C->next = q;
q = r;
}
return C;
}
找链表中间节点
使用快慢指针法(慢指针每次走一步,快指针每次走两步):
LNode *FindMiddle(LinkList L) {
LNode *slow = L->next, *fast = L->next;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow; // slow 即为中间结点
}
⚠️ 易错点:
- 链表操作中容易丢失指针,修改指针时务必先保存后继结点地址。
- 头结点和头指针的区别:头结点是第一个结点之前的辅助结点,头指针指向链表的第一个结点(可以是头结点)。
- 顺序表插入删除时注意边界条件,i 的合法范围是 [1, length+1](插入)和 [1, length](删除)。
2.6 题型示例
例题1:已知长度为 n 的顺序表 L,编写算法删除所有值为 x 的元素。 解:用 k 记录不等于 x 的元素个数(同时也是新表的下标位置),遍历一次即可。
void DeleteX(SqList &L, ElemType x) {
int k = 0;
for (int i = 0; i < L.length; i++) {
if (L.data[i] != x) {
L.data[k] = L.data[i];
k++;
}
}
L.length = k;
}
时间复杂度 O(n),空间复杂度 O(1)。
例题2:给定两个单链表 A 和 B,编写算法找出它们的公共后缀起始结点。 解:先遍历两个链表求长度 lenA 和 lenB,让较长的链表先走 |lenA-lenB| 步,然后同时遍历,第一个相同的结点即为公共后缀起始结点。
LNode *FindCommon(LinkList A, LinkList B) {
int lenA = Length(A), lenB = Length(B);
LNode *p = A->next, *q = B->next;
int diff = abs(lenA - lenB);
if (lenA > lenB) {
for (int i = 0; i < diff; i++) p = p->next;
} else {
for (int i = 0; i < diff; i++) q = q->next;
}
while (p && q && p != q) {
p = p->next;
q = q->next;
}
return p;
}
例题3:设计一个算法,将带头结点的单链表就地逆置。 解:见上文"链表逆置"部分,使用头插法思想重新建链。
本章总结
线性表是数据结构中最基础的结构,顺序表和链表各有优劣,需要根据场景选择。408考试中,链表操作题常出现在综合应用题中,务必熟练掌握链表的各种操作,特别是指针修改的顺序。