C程序题案例分析
大纲要求
- 掌握C程序设计语言
- 掌握常用数据结构及其操作
- 掌握排序算法、查找算法
- 灵活应用算法设计策略
目录
一、算法设计的基础知识
1. 指针
2. 常用数据结构
1)线性表
2)队列
3)栈
4)堆
5)数组
6)树
7)Huffman树
8)图
二、常用算法设计技术
1)迭代法
2)穷举搜索法
3)递推法
4)递归法
5)回溯法
6)贪心法
7)分治法
8)动态规划法
三、例题
一、算法设计的基础知识
1. 指针
-
本质与类型
- 本质:存储内存地址的变量
- 类型意义:
- 决定指针运算的步长(如
int*
步长为4字节) - 指定操作的内存单元大小(如
char*
操作1字节)
- 决定指针运算的步长(如
-
数组名与指针的关系
- 数组名是常指针(首地址不可修改)
- 示例:
int arr[3] = {1, 2, 3}; int *p = arr; // arr等价于&arr[0]
-
指针作为函数参数
- 值传递原则:
- 修改指针指向的内容:直接传递指针(如
void func(int *p)
) - 修改指针本身:传递二级指针(如
void alloc(char **p)
)
- 修改指针指向的内容:直接传递指针(如
- 值传递原则:
#include <stdio.h>
int main() {int array3[6] = {100,101,102,103,110,111};printf("%x\n", array3); // 输出数组首地址(十六进制)int *p = array3; // 数组名作为指针while (p <= &array3[5]) { printf("%8d", *(p++)); // 指针遍历数组}return 0;
}
- 数组名是指向首元素的常指针。
- 指针自增操作
*(p++)
按类型大小移动(如int
移动4字节)。
2. 常用数据结构
1)线性表
概念
- 由相同数据类型的节点组成的有限序列,存在唯一首节点(无前驱)和尾节点(无后继)。
- 基本操作:初始化(
Initiate
)、插入(Insert
)、删除(Delete
)、查找(Locate
)等。
存储结构对比
类型 | 优点 | 缺点 |
---|---|---|
顺序存储 | 随机访问快(O(1)) | 插入/删除需移动元素 |
链式存储 | 动态扩展灵活 | 访问需遍历(O(n)) |
链式变种
- 单链表:节点含数据域 + 后继指针。
- 循环链表:尾节点指向头节点,形成环。
- 双向链表:节点含前驱 + 后继指针,支持双向遍历。
typedef struct Node {int data;struct Node *next;
} Node;// 插入操作示例(未完整,需补充)
void Insert(Node **head, int value) {Node *newNode = (Node*)malloc(sizeof(Node));newNode->data = value;newNode->next = *head;*head = newNode;
}
- 链式存储:单链表
- 链表的节点通过
malloc
动态分配内存。 - 插入操作需处理指针指向的修改。
2)队列
特性
- 先进先出(FIFO),队尾插入,队首删除。
实现方式
- 顺序队列:数组实现,需处理假溢出(循环队列)。
- 链队列:链表实现,无空间限制。
- 优先级队列:按优先级出队,常用堆实现。
// 链队列节点结构
typedef struct QNode {int data;struct QNode *next;
} QNode;// 入队操作(需维护队尾指针rear)
void EnQueue(QNode **rear, int value) {QNode *newNode = (QNode*)malloc(sizeof(QNode));newNode->data = value;newNode->next = NULL;(*rear)->next = newNode;*rear = newNode;
}
3)栈
特性
- 后进先出(LIFO),仅在栈顶(表尾)插入/删除。
实现方式
- 顺序栈:数组实现,需预设大小。
- 链栈:链表实现,无容量限制。
应用场景
- 函数调用栈、表达式求值、括号匹配。
// 链栈节点结构(文档中用于二叉树非递归遍历)
typedef struct SNode {BiTree elem; // 存储二叉树节点指针struct SNode *next;
} SNode;// 压栈操作
void Push(SNode **top, BiTree node) {SNode *newNode = (SNode*)malloc(sizeof(SNode));newNode->elem = node;newNode->next = *top;*top = newNode;
}
4)堆
定义
- 完全二叉树,满足堆性质:
- 大顶堆:父节点 ≥ 子节点
- 小顶堆:父节点 ≤ 子节点
典型应用
- 堆排序(时间复杂度 O(nlogn))
- 优先级队列
// 调整堆的代码未完整给出
void HeapAdjust(int a[], int s, int m) {/* 将序列a[s..m]调整为一个大顶堆 */// 具体实现
}// 初始化堆的步骤:
for (int i = n/2; i > 0; i--) HeapAdjust(a, i, n);
5)数组
类型
- 静态数组:编译时分配固定大小(如
int a[10]
)。 - 动态数组:运行时分配(如
malloc
)。
特殊矩阵存储
- 对称矩阵:仅存储上/下三角。
- 稀疏矩阵:三元组(行、列、值)压缩存储。
// 动态数组分配(文档中指针部分示例)
void GetMemory(char **p) {*p = (char*)malloc(100 * sizeof(char));
}int main() {char *arr = NULL;GetMemory(&arr); // 动态分配数组空间strcpy(arr, "Dynamic Array");free(arr);
}
6)树
二叉树性质
- 第
i
层最多有2^(i-1)
个节点。 - 叶子节点数
n0 = n2 + 1
(n2
为度为2的节点数)。
遍历方式
- 递归实现:代码简洁,可能栈溢出。
- 非递归实现:借助栈(中序)或队列(层序)。
特殊二叉树
类型 | 特点 |
---|---|
满二叉树 | 每一层节点数达到最大值。 |
完全二叉树 | 除最后一层外节点数满,最后一层左对齐。 |
AVL树 | 左右子树高度差 ≤ 1,平衡二叉查找树。 |
二叉查找树 | 左子树值 < 根 < 右子树值。 |
// 二叉树节点结构
typedef struct BiTNode {int data;struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;// 中序遍历递归实现
void InOrder(BiTree root) {if (root) {InOrder(root->lchild);printf("%d ", root->data);InOrder(root->rchild);}
}
7)Huffman树
定义
- 带权路径长度最短的二叉树(最优二叉树)。
构造算法
- 选权值最小的两个节点合并为新树。
- 新树根节点权值为子节点权值和。
- 重复直到只剩一棵树。
应用
- Huffman编码:高频字符用短编码,低频用长编码。
// 描述构造步骤(伪代码):
while (节点数 > 1) {1. 选择权值最小的两个节点;2. 合并为新节点(权值为两者之和);3. 将新节点加入集合;
}
8)图
表示方法
类型 | 适用场景 | 空间复杂度 |
---|---|---|
邻接矩阵 | 稠密图 | O(V²) |
邻接表 | 稀疏图 | O(V+E) |
遍历算法
- DFS(深度优先):递归/栈实现。(6.1)搜索-CSDN博客
- BFS(广度优先):队列实现。
二、常用算法设计技术
1)迭代法
- 核心思想:通过重复逼近求解数值近似解。
- 步骤:
- 确定迭代公式(如 x=g(x))。
- 选择初始近似值 x0。
- 循环计算 xn+1=g(xn)。
- 终止条件:∣xn+1−xn∣<ϵ(误差阈值)。
- 适用场景:方程求根(如 x=cos(x))。
- 注意事项:
- 需验证迭代公式的收敛性。
- 可能陷入无限循环(需设置最大迭代次数)。
// 求方程x = cos(x)的近似解(示例)
#include <math.h>
void iteration() {float x0 = 0.5, x1 = cos(x0); // 初始近似值while (fabs(x1 - x0) > 1e-6) { // 误差控制x0 = x1;x1 = cos(x0); // 迭代公式}printf("近似解: %f\n", x1);
}
2)穷举搜索法
- 核心思想:遍历所有可能的候选解,检查是否满足条件。
- 适用场景:
- 解空间有限且无更优算法时(如百钱买百鸡问题)。
- 密码破解、组合优化问题。
- 特点:
- 保证找到解,但效率低(时间复杂度可能为指数级)。
- 需合理剪枝以减少计算量。
- 适用场景:解空间有限且无更优算法时
- 特点:
- 按顺序检查所有候选解
- 保证找到解但效率低(如百钱买百鸡问题)
// 百钱买百鸡问题(补充经典示例)
void exhaustive() {for (int x = 0; x <= 20; x++) // 公鸡数量for (int y = 0; y <= 33; y++) { // 母鸡数量int z = 100 - x - y; // 小鸡数量if (5*x + 3*y + z/3 == 100 && z%3 == 0)printf("公鸡%d, 母鸡%d, 小鸡%d\n", x, y, z);}
}
3)递推法
- 核心思想:从已知小规模解推导大规模解(自底向上)。
- 典型应用:
- Fibonacci数列:F(n)=F(n−1)+F(n−2)。
- 组合数学问题(如杨辉三角)。
- 与递归的区别:
- 递推显式使用循环,无函数调用开销。
- 递归通过函数自调用实现(可能栈溢出)。
// 递推实现
int fibonacci(int n) {int f0 = 0, f1 = 1, f;for (int i = 2; i <= n; i++) {f = f0 + f1; // 递推公式f0 = f1; // 状态传递f1 = f;}return f;
}
4)递归法
- 核心思想:将问题分解为同类子问题,直到达到最小规模(递归出口)。
- 关键要素:
- 递归出口:最小问题的直接解(如 n=1)。
- 分解策略:如分治法(Hanoi塔)、回溯法(n皇后)。
- 典型应用:
- Fibonacci数列、背包问题、树的遍历。
- 缺点:
- 重复计算(可通过记忆化优化)。
- 栈空间消耗大(深度过大时溢出)。
// 背包问题递归框架
void try(int i, int tw, int tv) {if (包含物品i是可接受的) {将物品i加入当前方案;if (i < n-1) try(i+1, tw + w[i], tv);else if (当前方案更优) 保存方案;回溯:移除物品i;}if (不包含物品i仍可能更优) {if (i < n-1) try(i+1, tw, tv - v[i]);else if (当前方案更优) 保存方案;}
}
5)回溯法
- 核心思想:试探性搜索解空间,失败时回溯(DFS + 剪枝)。
- 步骤:
- 选择候选解并标记。
- 递归进入下一层。
- 若失败则撤销选择(回溯)。
- 典型应用:
- n皇后问题:逐行放置皇后,检查冲突后回溯。
- 数独求解、全排列生成。
- 优化:剪枝(提前终止无效分支)。
// n皇后问题框架
int is_valid(int *queens, int row, int col) {for (int i = 0; i < row; i++)if (queens[i] == col || abs(queens[i]-col) == abs(i-row))return 0;return 1;
}void backtrack(int *queens, int row, int n) {if (row == n) { 打印解; return; }for (int col = 0; col < n; col++) {if (is_valid(queens, row, col)) {queens[row] = col;backtrack(queens, row+1, n); // 试探下一行}}
}
6)贪心法
(5.1)基本算法-CSDN博客
- 核心思想:每一步选择局部最优解,不回溯。
- 适用条件:
- 问题具有贪心选择性质(局部最优导致全局最优)。
- 如Dijkstra算法(最短路径)、Huffman编码。
- 典型应用:
- 装箱问题:优先装体积大的物品。
- 活动选择问题:选择最早结束的活动。
- 局限性:不一定得到全局最优解(如部分背包问题)。
// 装箱问题伪代码
void greedy_packing() {sort(items, items+n, descending); // 按体积降序排序for (int i = 0; i < n; i++) {int j = find_first_available_box(); // 找到第一个能放的箱子if (j == -1) 启用新箱子;else 放入箱子j;}
}
7)分治法
- 核心思想:将问题分解为独立子问题,合并子问题的解(自顶向下)。
- 步骤:
- 分解:将问题划分为多个子问题。
- 解决:递归求解子问题。
- 合并:将子问题的解组合为最终解。
- 典型应用:
- 归并排序、快速排序。
- Hanoi塔问题:移动n个盘子转化为移动n-1个盘子。
- 效率分析:通常用Master定理计算时间复杂度(如归并排序为
)。
// Hanoi塔完整实现
void hanoi(int n, char A, char B, char C) {if (n == 1) printf("Move disk 1 from %c to %c\n", A, C);else {hanoi(n-1, A, C, B); // 分解为子问题printf("Move disk %d from %c to %c\n", n, A, C);hanoi(n-1, B, A, C); // 合并子问题解}
}
8)动态规划法
- 核心思想:存储子问题的解以避免重复计算(自底向上)。
- 适用条件:
- 重叠子问题:子问题被多次重复计算。
- 最优子结构:全局最优解包含局部最优解。
- 典型应用:
- Fibonacci数列(记忆化)、背包问题。
- Floyd算法(多源最短路径)、编辑距离。
- 实现方式:
- 表格法(如填二维数组)。
- 状态转移方程(如 dp[i]=dp[i−1]+dp[i−2])。
// 斐波那契数列的DP实现(对比递推法)
int dp_fib(int n) {int dp[n+1];dp[0] = 0; dp[1] = 1;for (int i = 2; i <= n; i++)dp[i] = dp[i-1] + dp[i-2]; // 复用子问题解return dp[n];
}
算法 | 核心思想 | 适用场景 | 是否回溯 | 时间复杂度 |
---|---|---|---|---|
迭代法 | 逐步逼近解 | 数值计算(如方程求根) | 否 | 依赖收敛速度 |
穷举法 | 遍历所有可能解 | 小规模解空间 | 否 | 指数级(O(2n)) |
递推法 | 从已知解推导 | 线性问题(如Fibonacci) | 否 | O(n) |
递归法 | 分治 + 递归出口 | 树形结构、分治问题 | 否 | 可能指数级 |
回溯法 | 试探 + 回溯 + 剪枝 | 组合优化(如n皇后) | 是 | 指数级(剪枝优化) |
贪心法 | 局部最优选择 | 最短路径、Huffman编码 | 否 | O(nlogn) |
分治法 | 分解→解决→合并 | 排序、Hanoi塔 | 否 | O(nlogn) |
动态规划 | 存储子问题解 | 最优化问题(如背包) | 否 | 多项式级(如O(n^2)) |
三、例题
- 题目背景:设有m台完全相同的机器运行n个独立的任务,运行任务i所需时间为ti,需确定一个调度方案,使完成所有任务所需时间最短。
- 算法策略:假设任务已按运行时间从大到小排序,基于最长运行时间作业优先的策略,先把每个任务分配到一台机器上,然后将剩余任务依次放入最先空闲的机器。
- C代码相关说明
- 常量和变量说明:
- m:机器数;
- n:任务数;
- t[]:输入数组,长度为n,每个元素表示任务的运行时间;
- s[][]:二维数组,长度为m×n,元素s[i][j]表示机器i运行的任务j的编号;
- d[]:数组,长度为m,元素d[i]表示机器i的运行时间;
- count[]:数组,长度为m,元素count[i]表示机器i运行的任务数;
- i、j、k:循环变量或临时变量;
- max:完成所有任务的时间;
- min:临时变量。
- 函数schedule:函数用于实现上述任务调度算法,代码中先对机器相关信息(运行时间、任务编号等)进行初始化,后续有分配前m个任务等操作。
- 问题设置:要求阅读上述说明和C代码,回答问题1 - 问题3,并将解答写在答题纸的对应栏内。
void schedule() {int i, j, k, max = 0;for (i = 0; i < m; i++) {d[i] = 0;for (j = 0; j < n; j++) {s[i][j] = 0;}}for (i = 0; i < m; i++) { //分配前m个任务s[i][0] = i;__ (1) __count[i] = 1;}for (__ (2) __; i < n; i++) { //分配后n - m个任务int min = d[0];k = 0;for (j = 1; j < m; j++) { //确定空闲机器if (min > d[j]) {min = d[j];k = j; //机器k空闲}}__ (3) __;count[k] = count[k] + 1;d[k] = d[k] + t[i];}for (i = 0; i < m; i++) { //确定完成所有任务所需要的时间if (__ (4) __) {max = d[i];}} }
- 问题1(8分):依据给定说明和C代码,填充代码中的空(1)-(4)。
- 问题2(2分):根据说明和C代码,判断该问题采用的算法设计策略,并给出时间复杂度(用O符号表示)。
- 问题3(5分):针对实例m = 3(机器编号0 - 2),n = 7(任务编号0 - 6),各任务运行时间为{16, 14, 6, 5, 4, 3, 2},确定机器0、1和2上运行的任务(给出任务编号),以及从任务开始运行到完成所需要的时间。
- 解析:本题考查贪心算法,该算法不追求最优解,以当前情况为基础做出最优选择,无需回溯。
- 答案
- 问题1:(1)
d[i] = d[i] + t[i]
;(2)i = m
;(3)s[k][0] = i
;(4)Max < d[i]
。 - 问题2:采用贪心算法策略,时间复杂度为O(2m×n + 2m)。
- 问题3:机器0运行任务0;机器1运行任务1、5;机器2运行任务2、3、4、6;运行的最长时间为17。
- 问题1:(1)