由于篇幅限制,我无法提供所有习题的完整代码和答案,但我将选择其中最具代表性、最核心的章节和习题,提供详细的思路解析、伪代码/核心代码片段和C语言实现,这足以帮助你掌握解题方法,并可以举一反三。

(图片来源网络,侵删)
第一章 绪论
本章主要考察时间复杂度和空间复杂度的分析。
习题示例:计算算法的时间复杂度
** 求以下算法的时间复杂度。
void fun(int n) {
int i = 1, j = 0;
while (i <= n) {
j++;
i = i * 2;
}
}
思路解析:
- 基本操作:
j++是核心操作,我们计算它的执行次数。 - 循环分析: 循环变量
i的增长不是线性的,而是指数级的。i的取值序列为:1, 2, 4, 8, ..., 直到i > n。 - 执行次数: 循环执行的次数取决于
i何时会超过n。i从 1 开始,每次乘以 2,设循环执行了k次,则有2^k > n。 - 求解 k: 对不等式两边取对数,得到
k > log₂n,因为k是整数,所以循环执行的次数约等于log₂n。 - 基本操作
j++执行了约log₂n次,该算法的时间复杂度为 O(log n)。
第二章 线性表
线性表是数据结构的基础,主要实现方式包括顺序表和链表。

(图片来源网络,侵删)
习题示例一:顺序表(插入与删除)
** 实现顺序表的插入和删除操作。
顺序表结构定义
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100 // 顺序表可能达到的最大长度
typedef struct {
int *data; // 动态分配数组指针
int length; // 当前长度
int capacity; // 总容量
} SeqList;
初始化顺序表
void InitList(SeqList *L) {
L->data = (int *)malloc(sizeof(int) * MAXSIZE);
if (!L->data) {
printf("内存分配失败\n");
exit(1);
}
L->length = 0;
L->capacity = MAXSIZE;
}
插入操作 (在第i个位置前插入元素e)

(图片来源网络,侵删)
思路解析:
- 合法性检查: 判断表是否已满 (
L->length == L->capacity) 和插入位置i是否合法 (i < 1或i > L->length + 1)。 - 元素后移: 从最后一个元素开始,依次将所有元素向后移动一个位置,为新元素腾出空间。
- 插入元素: 将新元素
e放入位置i-1(因为数组下标从0开始)。 - 表长更新: 表长加1。
int ListInsert(SeqList *L, int i, int e) {
// 1. 检查合法性
if (i < 1 || i > L->length + 1) {
return 0; // 插入位置不合法
}
if (L->length >= L->capacity) {
return 0; // 顺序表已满
}
// 2. 元素后移
for (int j = L->length - 1; j >= i - 1; j--) {
L->data[j + 1] = L->data[j];
}
// 3. 插入元素
L->data[i - 1] = e;
// 4. 更新表长
L->length++;
return 1; // 插入成功
}
删除操作 (删除第i个位置的元素,并用e返回其值)
思路解析:
- 合法性检查: 判断表是否为空和删除位置
i是否合法 (i < 1或i > L->length)。 - 保存被删元素: 将要删除的元素
L->data[i-1]暂存到e中。 - 元素前移: 从第
i个元素开始,依次将所有元素向前移动一个位置,覆盖被删除的元素。 - 表长更新: 表长减1。
int ListDelete(SeqList *L, int i, int *e) {
// 1. 检查合法性
if (i < 1 || i > L->length) {
return 0; // 删除位置不合法
}
if (L->length == 0) {
return 0; // 顺序表为空
}
// 2. 保存被删元素
*e = L->data[i - 1];
// 3. 元素前移
for (int j = i; j < L->length; j++) {
L->data[j - 1] = L->data[j];
}
// 4. 更新表长
L->length--;
return 1; // 删除成功
}
习题示例二:单链表(反转)
** 写一个算法,将一个带头结点的单链表就地逆置。
思路解析:
“就地”意味着不能使用额外的存储空间,我们可以使用三个指针 p, q, r 来遍历和修改链表。
- 初始化:
p指向第一个实际结点(头结点的下一个结点),q和r初始为NULL。 - 遍历链表:
- 用
q暂存p的后继结点。 - 将
p的next指针指向q(即它的前驱结点,初始时为NULL)。 - 移动指针:将
r指向p,p指向之前暂存的q。
- 用
- 循环:直到
p变为NULL,表示所有结点都已反转。 - 完成:将原头结点的
next指向新的第一个结点(即r指向的结点)。
代码实现:
// 定义单链表结点
typedef struct LNode {
int data;
struct LNode *next;
} LNode, *LinkList;
// 反转单链表
void ReverseList(LinkList L) {
LNode *p = L->next; // p 指向第一个结点
LNode *q = NULL;
LNode *r = NULL;
while (p != NULL) {
q = p->next; // q 暂存 p 的后继
p->next = r; // p 的 next 指向前驱 r
r = p; // r 后移
p = q; // p 后移
}
L->next = r; // 头结点指向新的第一个结点
}
第三章 栈和队列
栈是后进先出,队列是先进先出。
习题示例:循环队列
** 实现循环队列,并解决“假溢出”问题。
思路解析:
- 结构定义: 使用数组存储元素,需要
front和rear两个指针。 - 循环实现: 利用取模运算 实现指针的循环移动。
- 判队满/判队空:
- 方法一(牺牲一个空间): 队头指针在队尾指针的下一个位置时,队列为满,即
(rear + 1) % MAXSIZE == front。 - 方法二(增加 size 变量): 单独设置一个
size变量记录队列中元素个数,判队满为size == MAXSIZE,判队空为size == 0,这里我们采用方法一。
- 方法一(牺牲一个空间): 队头指针在队尾指针的下一个位置时,队列为满,即
代码实现:
#define MAXSIZE 10
typedef struct {
int data[MAXSIZE];
int front; // 队头指针
int rear; // 队尾指针
} SqQueue;
// 初始化队列
void InitQueue(SqQueue *Q) {
Q->front = 0;
Q->rear = 0;
}
// 判断队列是否为空
int QueueEmpty(SqQueue Q) {
if (Q.front == Q.rear) {
return 1; // 空
}
return 0; // 非空
}
// 入队
int EnQueue(SqQueue *Q, int e) {
// 判断队满
if ((Q->rear + 1) % MAXSIZE == Q->front) {
return 0; // 入队失败
}
Q->data[Q->rear] = e;
Q->rear = (Q->rear + 1) % MAXSIZE;
return 1; // 入队成功
}
// 出队
int DeQueue(SqQueue *Q, int *e) {
// 判断队空
if (Q->front == Q->rear) {
return 0; // 出队失败
}
*e = Q->data[Q->front];
Q->front = (Q->front + 1) % MAXSIZE;
return 1; // 出队成功
}
第四章 串
串是特殊的线性表,其操作通常以“串”为单位。
习题示例:KMP算法
** 实现KMP算法,用于模式串 pattern 在主串 text 中的匹配。
思路解析:
KMP算法的核心是利用已匹配的信息,当匹配失败时,模式串不需要回溯,而是通过一个next数组来确定模式串应该向右移动多少位。
- 预处理(求
next数组):next[j]的含义是:当模式串的第j个字符与主串失配时,模式串应该跳转到哪个位置继续与主串当前字符比较。next[0] = -1(约定)。next[1] = 0。- 对于
j > 1,pattern[j-1] == pattern[k],则next[j] = k + 1,否则,递归地k = next[k],直到k == -1或匹配成功。
- 匹配过程:
- 使用两个指针
i(主串)和j(模式串)。 text[i] == pattern[j],则i++, j++。- 如果失配:
j == 0,说明模式串的第一个字符就不匹配,i++。- 否则,
j = next[j],模式串向右移动j - next[j]位,然后继续比较text[i]和pattern[j]。
- 当
j等于模式串长度时,匹配成功。
- 使用两个指针
代码实现(核心部分):
// 计算next数组
void get_next(const char *pattern, int *next) {
int j = 0, k = -1;
next[0] = -1;
while (pattern[j] != '\0') {
if (k == -1 || pattern[j] == pattern[k]) {
j++;
k++;
next[j] = k;
} else {
k = next[k];
}
}
}
// KMP匹配算法
int KMP(const char *text, const char *pattern) {
int i = 0, j = 0;
int next[strlen(pattern)];
get_next(pattern, next);
while (text[i] != '\0' && pattern[j] != '\0') {
if (j == -1 || text[i] == pattern[j]) {
i++;
j++;
} else {
j = next[j];
}
}
if (pattern[j] == '\0') {
return i - j; // 返回匹配的起始位置
} else {
return -1; // 匹配失败
}
}
第五章 树与二叉树
二叉树是树结构的基础,遍历是核心操作。
习题示例:二叉树的遍历与构建
** 实现二叉树的前序、中序、后序遍历,以及根据遍历序列重建二叉树。
二叉树结点定义
typedef struct BiTNode {
char data; // 假设数据为字符
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
前序遍历(递归)
void PreOrderTraverse(BiTree T) {
if (T == NULL) {
return;
}
printf("%c ", T->data); // 访问根结点
PreOrderTraverse(T->lchild); // 遍历左子树
PreOrderTraverse(T->rchild); // 遍历右子树
}
中序遍历(递归)
void InOrderTraverse(BiTree T) {
if (T == NULL) {
return;
}
InOrderTraverse(T->lchild); // 遍历左子树
printf("%c ", T->data); // 访问根结点
InOrderTraverse(T->rchild); // 遍历右子树
}
后序遍历(递归)
void PostOrderTraverse(BiTree T) {
if (T == NULL) {
return;
}
PostOrderTraverse(T->lchild); // 遍历左子树
PostOrderTraverse(T->rchild); // 遍历右子树
printf("%c ", T->data); // 访问根结点
}
根据前序和中序序列重建二叉树
思路解析:
- 前序序列的第一个元素是根结点。
- 在中序序列中找到这个根结点的位置。
- 根结点将中序序列分为左子树和右子树两部分。
- 根据左右子树的长度,可以从前序序列中划分出对应左子树和右子树的前序序列。
- 递归地对左子树和右子树执行上述步骤,直到子树为空。
// 在中序序列中查找根结点位置,返回下标
int findIndex(char *inorder, int inStart, int inEnd, char root) {
for (int i = inStart; i <= inEnd; i++) {
if (inorder[i] == root) {
return i;
}
}
return -1;
}
// 重建二叉树的核心函数
BiTree buildTree(char *preorder, int preStart, int preEnd,
char *inorder, int inStart, int inEnd) {
if (preStart > preEnd || inStart > inEnd) {
return NULL;
}
// 1. 前序序列的第一个元素是根
char rootVal = preorder[preStart];
BiTree root = (BiTree)malloc(sizeof(BiTNode));
root->data = rootVal;
root->lchild = root->rchild = NULL;
// 2. 在中序序列中找到根的位置
int rootIndex = findIndex(inorder, inStart, inEnd, rootVal);
// 3. 计算左子树的大小
int leftTreeSize = rootIndex - inStart;
// 4. 递归构建左子树和右子树
root->lchild = buildTree(preorder, preStart + 1, preStart + leftTreeSize,
inorder, inStart, rootIndex - 1);
root->rchild = buildTree(preorder, preStart + leftTreeSize + 1, preEnd,
inorder, rootIndex + 1, inEnd);
return root;
}
第六章 图
图是非线性结构,遍历和最短路径是重点。
习题示例:图的深度优先搜索
** 实现图的邻接表存储和DFS遍历。
图的结构定义(邻接表)
#define MAX_VERTEX_NUM 20
typedef struct ArcNode {
int adjvex; // 该弧指向的顶点的位置
struct ArcNode *nextarc; // 指向下一条弧的指针
} ArcNode;
typedef struct VNode {
char data; // 顶点信息
ArcNode *firstarc; // 指向第一条依附该顶点的弧
} VNode, AdjList[MAX_VERTEX_NUM];
typedef struct {
AdjList vertices; // 邻接表
int vexnum, arcnum; // 图的当前顶点数和弧数
} ALGraph;
DFS遍历(递归)
思路解析:
- 访问顶点
v。 - 找到
v的第一个邻接点w。 w未被访问,则从w开始进行深度优先搜索。- 重复步骤2、3,直到
v的所有邻接点都被访问过。
// 访问标记数组,0表示未访问,1表示已访问
int visited[MAX_VERTEX_NUM];
// 从第v个顶点出发递归地深度优先遍历图
void DFS(ALGraph G, int v) {
// 访问顶点v
printf("%c ", G.vertices[v].data);
visited[v] = 1;
// 递归访问v的未被访问的邻接点
ArcNode *p = G.vertices[v].firstarc;
while (p != NULL) {
int w = p->adjvex;
if (!visited[w]) {
DFS(G, w);
}
p = p->nextarc;
}
}
// 对图G进行深度优先遍历
void DFSTraverse(ALGraph G) {
// 初始化访问标记数组
for (int i = 0; i < G.vexnum; ++i) {
visited[i] = 0;
}
// 对每个顶点调用DFS,处理非连通图
for (int i = 0; i < G.vexnum; ++i) {
if (!visited[i]) {
DFS(G, i);
}
}
}
总结与建议
- 理解优于记忆: 不要死记硬背代码,上面的每个例子都提供了详细的思路解析,请务必先理解算法的原理和设计思想。
- 亲手实现: 看懂了不代表会写了,一定要亲自在编译器上敲一遍代码,运行、调试,加深理解。
- 举一反三: 掌握了单链表反转,就应该能思考双向链表、循环链表的反转,掌握了DFS,就应该能写出BFS。
- 利用资源: 网络上有很多关于《数据结构(C语言版)》严蔚敏的PDF和答案资源,可以作为参考,但一定要批判性地看,自己思考是关键。
希望这份详细的解析能对你的学习有所帮助!如果你有其他具体章节或习题的疑问,可以随时提出。
