需要强调的是,直接抄答案对学习并无益处,数据结构的核心在于理解思想、掌握算法、锻炼逻辑思维能力,我将提供思路解析、关键代码和示例,旨在引导您独立思考,而不是简单地给出最终答案。

这份资料将覆盖教材中最核心和经典的章节,包括:
- 线性表
- 栈和队列
- 串
- 树与二叉树
- 图
- 查找
第一章 绪论
相对宏观,主要涉及算法的时间复杂度和空间复杂度分析,是后续所有章节的基础。
核心思想:
- 时间复杂度:估算算法执行时间与问题规模
n之间的增长关系,使用大O表示法,如 O(1), O(n), O(n²), O(log n)。 - 空间复杂度:估算算法所需存储空间与问题规模
n之间的增长关系。
典型习题与解析:

习题 1.3 (1): 设 n 为正整数,请给出下列各程序段的时间复杂度。
// (1)
for (i = 1; i <= n; i++)
for (j = 1; j <= i; j++)
for (k = 1; k <= j; k++)
x++;
// (2)
i = 1;
while (i <= n)
i = i * 2;
// (3)
x = 0;
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++)
for (k = 1; k <= j; k++)
x++;
解析:
-
(1) 分析:
- 最内层循环
k从 1 到j,执行次数约为j。 - 中间层循环
j从 1 到i,其执行次数为Σ(j=1 to i) j,即i(i+1)/2,时间复杂度为 O(i²)。 - 最外层循环
i从 1 到n,其执行次数为Σ(i=1 to n) i²。 - 根据数学公式,
1² + 2² + ... + n² = n(n+1)(2n+1)/6,这是一个关于n的三次多项式。 - 时间复杂度为 O(n³)。
- 最内层循环
-
(2) 分析:
- 这是一个
while循环,变量i的值是按指数增长的:1, 2, 4, 8, 16, ... - 循环执行的次数
m满足2^m <= n < 2^(m+1)。 - 对不等式取对数,得到
m <= log₂n < m+1,循环次数m约为log₂n。 - 时间复杂度为 O(log n)。 (通常省略底数)
- 这是一个
-
(3) 分析:
- 这个程序段与 (1) 的区别在于,
j的循环上限是固定的n,而不是i。 - 最内层
k循环的执行次数约为j。 - 中间层
j循环从 1 到n,其执行次数为Σ(j=1 to n) j,即n(n+1)/2,时间复杂度为 O(n²)。 - 最外层
i循环从 1 到n,它执行了n次,每次内部是一个 O(n²) 的操作。 - 总执行次数为
n * O(n²) = O(n³)。 - 时间复杂度为 O(n³)。
- 这个程序段与 (1) 的区别在于,
第二章 线性表
线性表是数据结构的基础,主要掌握顺序存储(数组)和链式存储(链表)两种实现。
核心思想:
- 顺序表:用一段地址连续的存储单元依次存储数据元素,优点是随机访问快,缺点是插入和删除需要移动大量元素。
- 单链表:通过节点中的指针链接在一起,优点是插入和删除灵活,缺点是只能顺序访问。
典型习题与解析:
习题 2.2: 写一算法,从顺序表中删除所有值为 x 的元素。
思路解析:
- 暴力解法(效率低): 遍历顺序表,每找到一个值为
x的元素,就执行删除操作,删除一个元素需要移动其后的所有元素,时间复杂度为 O(n²)。 - 高效解法(双指针法):
- 使用两个指针
i和j,都从顺序表的头开始。 i指针作为“快指针”,负责遍历整个顺序表。j指针作为“慢指针”,指向当前有效序列的末尾。- 遍历过程:
L.data[i]不等于x,说明这是一个有效元素,将它复制到j的位置,j++。L.data[i]等于x,跳过它,i++。
- 遍历结束后,
j的值就是新顺序表的长度。
- 使用两个指针
C语言代码实现:
#define LIST_INIT_SIZE 100
#define LISTINCREMENT 10
typedef struct {
int *elem; // 存储空间基址
int length; // 当前长度
int listsize; // 当前分配的存储容量
} SqList;
// 删除顺序表中所有值为x的元素 (高效解法)
void DeleteAllX(SqList *L, int x) {
int i, j;
j = 0; // j指向新表的末尾
for (i = 0; i < L->length; i++) {
if (L->elem[i] != x) {
// 如果不是x,则保留,将其放到新表的位置
L->elem[j] = L->elem[i];
j++;
}
// 如果是x,则跳过,i自动增加,j不动
}
// 循环结束后,j就是新表的长度
L->length = j;
}
习题 2.9: 有一个单链表 L,其结点结构为 (data, next),设计一个算法,将该链表逆置。
思路解析: 这是链表操作的经典问题,可以采用迭代法或递归法,这里介绍迭代法,更直观且空间复杂度为 O(1)。
- 初始化三个指针:
pre:指向已逆序部分的头节点,初始为NULL。cur:指向当前待处理的节点,初始为链表的头节点L->head。next:用于临时保存cur的下一个节点,防止断链。
- 遍历链表:
- 当
cur不为NULL时,进行循环。 - 保存
cur的下一个节点:next = cur->next;。 - 将
cur的next指针指向pre,实现局部逆序:cur->next = pre;。 - 更新
pre和cur:pre = cur;,cur = next;。
- 当
- 完成:
- 循环结束后,
pre指向了新的头节点。
- 循环结束后,
C语言代码实现:
typedef struct LNode {
int data;
struct LNode *next;
} LNode, *LinkList;
// 逆置单链表
LinkList ReverseList(LinkList L) {
LNode *pre = NULL;
LNode *cur = L->next; // 假设L是带头结点的
LNode *next = NULL;
while (cur != NULL) {
next = cur->next; // 1. 保存下一个节点
cur->next = pre; // 2. 将当前节点指向前一个节点
pre = cur; // 3. pre后移
cur = next; // 4. cur后移
}
// 循环结束后,pre是新的头节点
L->next = pre; // 将头结点的next指向新的头
return L;
}
第三章 栈和队列
栈(后进先出 LIFO)和队列(先进先出 FIFO)是两种特殊的线性表。
核心思想:
- 栈: 只能在表的一端(栈顶)进行插入和删除操作,实现方式有顺序栈(数组)和链栈。
- 队列: 在表的一端(队尾)插入,在另一端(队头)删除,实现方式有循环队列(解决假溢出问题)和链队列。
典型习题与解析:
习题 3.2 (3): 利用栈的基本操作,写一个算法,判断一个字符串是否是回文。
思路解析: 回文是指正读和反读都相同的字符串,如 "level" 或 "madam"。
- 创建一个空栈。
- 遍历字符串的前半部分,将每个字符依次入栈。
- 继续遍历字符串的后半部分,同时将字符出栈,并与当前遍历到的字符进行比较。
- 如果所有字符都相等,则是回文;否则不是。
C语言代码实现:
#include <stdio.h>
#include <string.h>
#include "stack.h" // 假设有一个顺序栈的实现
int IsPalindrome(char *str) {
int len = strlen(str);
SqStack S;
InitStack(&S); // 初始化栈
// 1. 将前一半字符入栈
for (int i = 0; i < len / 2; i++) {
Push(&S, str[i]);
}
// 2. 比较后一半字符
int start = (len % 2 == 0) ? len / 2 : len / 2 + 1;
for (int i = start; i < len; i++) {
char topElem;
Pop(&S, &topElem);
if (topElem != str[i]) {
return 0; // 不匹配,不是回文
}
}
return 1; // 全部匹配,是回文
}
第四章 树与二叉树
树是非线性的重要数据结构,二叉树是其中最常用的一种。
核心思想:
- 二叉树的遍历: 是二叉树操作的基础,有四种方式:
- 前序遍历 (DLR): 根 -> 左 -> 右
- 中序遍历 (LDR): 左 -> 根 -> 右
- 后序遍历 (LRD): 左 -> 右 -> 根
- 层序遍历: 从上到下,从左到右。
- 线索二叉树: 利用空指针域存放前驱和后继信息,提高遍历效率。
- 哈夫曼树: 带权路径长度最短的二叉树,用于编码。
典型习题与解析:
习题 4.3 (5): 编写算法,交换二叉树中所有结点的左右子树。
思路解析: 这是一个典型的递归问题,对任何一个节点,交换其左右子树即可。
- 递归基(终止条件): 如果当前节点为空(
T == NULL),则直接返回,无需操作。 - 递归步骤:
- 交换当前节点
T的左右孩子指针。 - 递归调用函数,处理左子树。
- 递归调用函数,处理右子树。
- 交换当前节点
C语言代码实现:
typedef struct BiTNode {
char data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
// 交换二叉树的左右子树
void SwapChildren(BiTree *T) {
if (*T == NULL) {
return;
}
// 1. 交换当前节点的左右孩子
BiTNode *temp = (*T)->lchild;
(*T)->lchild = (*T)->rchild;
(*T)->rchild = temp;
// 2. 递归交换左子树
SwapChildren(&(*T)->lchild);
// 3. 递归交换右子树
SwapChildren(&(*T)->rchild);
}
第五章 图
图是最复杂的数据结构,由顶点和边组成。
核心思想:
- 存储结构:
- 邻接矩阵: 适合稠密图,易于实现,但空间复杂度高(O(V²))。
- 邻接表: 适合稀疏图,空间复杂度低(O(V+E)),是更常用的表示方法。
- 遍历算法:
- 深度优先搜索: 类似树的先序遍历,使用栈(或递归)实现。
- 广度优先搜索: 类似树的层序遍历,使用队列实现。
- 图的应用: 最短路径(Dijkstra, Floyd)、最小生成树(Prim, Kruskal)、拓扑排序等。
典型习题与解析:
习题 5.3 (4): 试以邻接表为存储结构,实现深度优先遍历算法的非递归过程。
思路解析: DFS的非递归版本需要显式地使用一个栈来模拟递归调用过程。
- 初始化: 创建一个栈
S,并初始化一个访问标记数组visited。 - 起始点: 访问起始顶点
v0,将其标记为已访问,并将其入栈。 - 循环: 当栈
S不为空时:- 取出栈顶元素
v(注意是取出,不是弹出,因为还要用它去找邻接点)。 - 查找
v的第一个未被访问的邻接点w。- 如果找到
w:- 访问
w,标记为已访问。 - 将
w入栈。 - 将
v也重新入栈(因为我们还要继续找v的下一个邻接点)。 - 从
v的邻接点表头开始查找w的下一个邻接点,以便下次循环。
- 访问
- 如果没找到
w:- 说明
v的所有邻接点都已访问,将v从栈中弹出。
- 说明
- 如果找到
- 取出栈顶元素
- 结束: 当栈为空时,遍历结束。
C语言代码实现 (伪代码/关键部分):
// 假设邻接表的结构如下
typedef struct ArcNode {
int adjvex; // 该弧所指向的顶点的位置
struct ArcNode *nextarc; // 指向下一条弧的指针
} ArcNode;
typedef struct VNode {
int data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧
} VNode, AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices;
int vexnum, arcnum;
} ALGraph;
void DFSTraverse_AL(ALGraph G) {
int visited[MAX_VERTEX_NUM];
InitStack(&S); // 初始化栈
// 初始化所有顶点为未访问
for (int i = 0; i < G.vexnum; ++i) {
visited[i] = 0;
}
// 对每个顶点,如果未访问,则进行DFS
for (int i = 0; i < G.vexnum; ++i) {
if (!visited[i]) {
// --- 核心非递归DFS逻辑 ---
Push(&S, i); // 起始顶点入栈
visited[i] = 1;
printf("%d ", G.vertices[i].data); // 访问
while (!StackEmpty(S)) {
int v;
GetTop(&S, &v); // 取栈顶元素v,不出栈
// 找到v的第一个未访问的邻接点w
ArcNode *p = G.vertices[v].firstarc;
int w = -1;
while (p) {
if (!visited[p->adjvex]) {
w = p->adjvex;
break;
}
p = p->nextarc;
}
if (w != -1) { // 找到了w
visited[w] = 1;
printf("%d ", G.vertices[w].data);
Push(&S, w); // w入栈
} else { // 没找到,v的所有邻接点都访问完了,弹出v
Pop(&S, &v);
}
}
}
}
}
第六章 查找
查找是在数据集合中寻找特定元素的过程。
核心思想:
- 静态查找: 数据集合在查找过程中不改变。
- 顺序查找: 简单,效率低 (O(n))。
- 折半查找(二分查找): 要求数据有序,效率高 (O(log n))。
- 动态查找: 查找过程中可能会插入或删除元素。
- 二叉排序树: 左子树所有节点 < 根节点 < 右子树所有节点,查找、插入、删除的平均时间复杂度为 O(log n),最坏为 O(n)(树退化为链表)。
- 平衡二叉树 (AVL树): 通过旋转保持树的平衡,确保查找效率稳定在 O(log n)。
- 哈希表: 通过哈希函数直接计算元素地址,理想情况下查找时间复杂度为 O(1)。
典型习题与解析:
习题 6.3 (3): 试写一算法,判断给定的二叉树是否是二叉排序树。
思路解析: 二叉排序树的定义是:任意节点的左子树所有节点值小于该节点值,右子树所有节点值大于该节点值。
- 错误思路: 只比较当前节点和其左右孩子,这无法保证整棵树的性质,下面的树就不是二叉排序树,但每个节点都满足“左孩子<父节点<右孩子”。
10 / \ 5 15 / \ 6 20 - 正确思路(利用中序遍历):
- 二叉排序树的一个核心特性是:其中序遍历序列是一个严格递增的序列。
- 我们可以对树进行中序遍历,并在遍历过程中检查当前访问的节点值是否大于前一个访问的节点值。
- 如果发现不满足,则不是二叉排序树。
C语言代码实现:
// 全局变量,记录前一个访问的节点的值
int preValue = -99999; // 假设节点值都大于-99999
int isBST(BiTree T) {
if (T == NULL) {
return 1; // 空树是二叉排序树
}
// 递归检查左子树
if (!isBST(T->lchild)) {
return 0;
}
// 检查当前节点
if (T->data < preValue) {
return 0; // 当前节点值小于前一个,不是BST
}
preValue = T->data; // 更新preValue
// 递归检查右子树
if (!isBST(T->rchild)) {
return 0;
}
return 1;
}
// 使用时需要重置preValue
// preValue = -99999;
// int result = isBST(root);
总结与建议
- 动手实践: 理解理论后,一定要亲手敲代码实现,从定义结构体,到实现基本操作(增删改查),再到解决综合问题。
- 调试能力: 学会使用
gdb或 IDE 自带的调试器,单步执行,观察变量变化,是排查逻辑错误的最佳方式。 - 画图辅助: 对于树、图等非线性结构,在纸上画出数据的变化过程,能极大地帮助你理解算法的执行流程。
- 关注复杂度: 每次写完算法后,都习惯性地分析一下它的时间复杂度和空间复杂度,这是衡量算法优劣的关键指标。
- 善用资源: 除了严蔚敏老师的教材,还可以参考其他优秀教材,如《算法(第4版)》(Java)、《大话数据结构》等,从不同角度理解问题,LeetCode、牛客网等平台提供了大量练习题。
希望这份详细的解析和示例能对您的学习有所帮助!祝您学习顺利!
