c回顾 01
1 C 语言本身的可移植性很强
为什么说 C 语言可移植性强?
- 有统一的标准:C 语言有明确的官方标准,比如 ANSI C(1989 年)、C99、C11 等。只要代码严格遵循这些标准,不依赖平台专属特性,就能在不同系统上通用。
- 依赖编译器而非系统:C 语言代码需要通过编译器翻译成机器码才能运行。不同平台(Windows、Linux、macOS)都有对应的 C 编译器(如 GCC、Clang、MSVC),只需用目标平台的编译器重新编译代码,就能生成适配该平台的可执行文件,无需修改源代码。
- 底层无关性:标准 C 语言不直接操作特定硬件或系统内核,而是通过标准库(如
stdio.h
、stdlib.h
)提供通用功能,这些标准库会由编译器根据不同平台进行适配,保证代码的跨平台能力。
容易混淆的 “可移植性差” 场景
我们常说的 “C 语言代码可移植性差”,其实不是语言本身的问题,而是代码编写时的问题:
- 使用了平台专属库或语法:比如 Windows 下的
windows.h
、Linux 下的pthread.h
,这些库只在特定系统可用,会导致代码无法跨平台。 - 依赖硬件底层操作:比如直接访问特定内存地址、操作硬件寄存器,这类代码只能在对应的硬件架构(如 x86、ARM)上运行。
- 没有遵循C 语言标准:比如使用编译器的非标准扩展语法,换个编译器就可能报错。
2 C 语言中所有运算符的优先级(从高到低排序)
优先级 | 运算符 | 含义说明 | 结合性 |
---|---|---|---|
1 | () [] -> . ++ -- (后缀) | 括号、数组、指针成员、结构体成员、后缀自增 / 减 | 从左到右 |
2 | ! ~ ++ -- (前缀) + (正号) - (负号) (类型) * (指针) & (取地址) sizeof | 逻辑非、按位非、前缀自增 / 减、正负号、强制类型转换、指针解引用、取地址、求大小 | 从右到左 |
3 | * (乘) / (除) % (取余) | 乘法、除法、取模 | 从左到右 |
4 | + (加) - (减) | 加法、减法 | 从左到右 |
5 | << (左移) >> (右移) | 位左移、位右移 | 从左到右 |
6 | < <= > >= | 小于、小于等于、大于、大于等于 | 从左到右 |
7 | == (等于) != (不等于) | 等于、不等于 | 从左到右 |
8 | & (按位与) | 位与 | 从左到右 |
9 | ^ (按位异或) | 位异或 | 从左到右 |
10 | | (按位或) | 位或 | 从左到右 |
11 | && (逻辑与) | 逻辑与 | 从左到右 |
12 | || (逻辑或) | 逻辑或 | 从左到右 |
13 | ?: (条件运算符) | 三目条件运算 | 从右到左 |
14 | = += -= *= /= %= <<= >>= &= ^= |= | 赋值及复合赋值 | 从右到左 |
15 | , (逗号运算符) | 逗号分隔表达式 | 从左到右 |
关键说明:
- 优先级越高,越先执行:例如
a + b * c
中,*
优先级高于+
,先算乘法。 - 结合性决定同优先级运算顺序:
- 从左到右:如
a + b + c
等价于(a + b) + c
- 从右到左:如
a = b = c
等价于a = (b = c)
- 从左到右:如
- 避免过度依赖优先级:复杂表达式建议用
()
明确运算顺序,例如(a + b) * c
比a + b * c
更清晰。
3 几个算法的时间复杂度
设顺序表的长度为n。下列算法中,最坏情况下比较次数小于n的是()
A 寻找最大项
B 堆排序
C 快速排序
D 顺序查找法正确答案:A
实现代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>// 记录比较次数
int compare_count = 0;// 重置比较计数器
void reset_count() {compare_count = 0;
}// A. 寻找最大项
int find_max(int arr[], int n) {reset_count();if (n <= 0) return -1; // 无效输入int max = arr[0];for (int i = 1; i < n; i++) {compare_count++; // 记录比较次数if (arr[i] > max) {max = arr[i];}}return max;
}// 交换两个元素
void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp;
}// 堆排序辅助函数 - 维护堆特性
void heapify(int arr[], int n, int i) {int largest = i;int left = 2*i + 1;int right = 2*i + 2;if (left < n) {compare_count++;if (arr[left] > arr[largest])largest = left;}if (right < n) {compare_count++;if (arr[right] > arr[largest])largest = right;}if (largest != i) {swap(&arr[i], &arr[largest]);heapify(arr, n, largest);}
}// B. 堆排序
void heap_sort(int arr[], int n) {reset_count();// 构建堆for (int i = n / 2 - 1; i >= 0; i--)heapify(arr, n, i);// 提取元素for (int i = n-1; i >= 0; i--) {swap(&arr[0], &arr[i]);heapify(arr, i, 0);}
}// C. 快速排序
int partition(int arr[], int low, int high) {int pivot = arr[high];int i = (low - 1);for (int j = low; j <= high-1; j++) {compare_count++;if (arr[j] <= pivot) {i++;swap(&arr[i], &arr[j]);}}swap(&arr[i+1], &arr[high]);return (i+1);
}void quick_sort(int arr[], int low, int high) {if (low < high) {int pi = partition(arr, low, high);quick_sort(arr, low, pi-1);quick_sort(arr, pi+1, high);}
}// D. 顺序查找法
int sequential_search(int arr[], int n, int target) {reset_count();for (int i = 0; i < n; i++) {compare_count++;if (arr[i] == target) {return i; // 找到目标,返回索引}}return -1; // 未找到
}// 打印数组
void print_array(int arr[], int size) {for (int i = 0; i < size; i++)printf("%d ", arr[i]);printf("\n");
}int main() {int n = 10;int arr[n];// 生成随机数组srand(time(0));for (int i = 0; i < n; i++) {arr[i] = rand() % 100;}printf("原始数组: ");print_array(arr, n);// 测试寻找最大项int max = find_max(arr, n);printf("A. 寻找最大项: 最大值=%d, 比较次数=%d\n", max, compare_count);// 测试堆排序int heap_arr[n];for (int i = 0; i < n; i++) heap_arr[i] = arr[i];heap_sort(heap_arr, n);printf("B. 堆排序: 比较次数=%d\n", compare_count);// 测试快速排序int quick_arr[n];for (int i = 0; i < n; i++) quick_arr[i] = arr[i];quick_sort(quick_arr, 0, n-1);printf("C. 快速排序: 比较次数=%d\n", compare_count);// 测试顺序查找(查找一个不存在的元素,最坏情况)sequential_search(arr, n, -1); // -1肯定不存在printf("D. 顺序查找: 比较次数=%d\n", compare_count);return 0;
}
题目分析
题目问的是 "最坏情况下比较次数小于 n" 的算法,其中 n 是顺序表的长度。
各选项分析:
- A. 寻找最大项:只需要遍历一次数组,比较 n-1 次就能找到最大值,最坏情况也只需 n-1 次比较
- B. 堆排序:最坏情况比较次数为 O (n log n),远大于 n
- C. 快速排序:最坏情况(数组已排序)比较次数为 O (n²),远大于 n
- D. 顺序查找法:最坏情况需要比较 n 次(要找的元素在最后或不存在)
所以正确答案是 A,因为只有寻找最大项的最坏情况比较次数是 n-1,小于 n。
算法解析
寻找最大项
- 原理:遍历数组一次,记录遇到的最大值
- 比较次数:n-1 次(无论数据如何分布)
- 时间复杂度:O (n)
堆排序
- 原理:构建最大堆,然后反复提取最大值并调整堆
- 比较次数:约 n log n 次
- 时间复杂度:O (n log n)(最好、最坏和平均情况相同)
快速排序
- 原理:选择一个基准元素,将数组分为两部分,递归排序
- 比较次数:平均情况 O (n log n),最坏情况 O (n²)(当数组已排序时)
- 时间复杂度:平均 O (n log n),最坏 O (n²)
顺序查找法
- 原理:从数组开头逐个查找目标元素
- 比较次数:平均 n/2 次,最坏 n 次(目标在最后或不存在)
- 时间复杂度:O (n)
通过运行代码,你会发现只有寻找最大项的比较次数在任何情况下都是 n-1,严格小于 n,这就是为什么正确答案是 A。
4 堆排序
堆排序是一种基于堆数据结构的高效排序算法,平均、最好、最坏时间复杂度均为 O(n log n),且是不稳定排序。
核心原理:先建堆,再排序
堆排序的过程分为两大步骤,核心是利用堆 “父节点值大于 / 小于子节点值” 的特性,反复提取最大值 / 最小值并调整堆结构。
构建初始堆:将无序数组转化为大根堆(升序排序用)或小根堆(降序排序用)。
- 大根堆:每个父节点的值 大于等于 其左右子节点的值,堆顶是整个数组的最大值。
- 小根堆:每个父节点的值 小于等于 其左右子节点的值,堆顶是整个数组的最小值。
逐步提取堆顶并调整:
- 第一步:将堆顶元素(最大值 / 最小值)与堆的最后一个元素交换,此时数组末尾已确定一个有序元素。
- 第二步:将堆的规模缩小 1(排除已排序的末尾元素),对新堆顶执行 “堆调整” 操作,重新恢复堆的特性。
- 第三步:重复前两步,直到堆的规模为 1,排序完成。
关键操作:堆调整(以大根堆为例)
堆调整(Heapify)是维护堆特性的核心步骤,当某个节点的值破坏堆结构时,通过 “向下渗透” 使其归位。
操作步骤:
- 假设当前节点为
i
,其左子节点为2i+1
,右子节点为2i+2
。 - 找出
i
、左子节点、右子节点中值最大的节点,记为max_index
。 - 若
max_index
不等于i
(说明当前节点不是最大值),则交换i
和max_index
的值。 - 交换后,
max_index
位置的新值可能破坏子堆结构,需递归对max_index
执行堆调整,直到满足堆特性。
算法示例(升序排序)
以无序数组 [4, 6, 8, 5, 9]
为例,演示堆排序过程:
步骤 | 操作 | 数组状态(堆结构) |
---|---|---|
1 | 构建初始大根堆 | [9, 6, 8, 5, 4] |
2 | 堆顶 9 与末尾 4 交换,缩小堆规模,调整堆 | [8, 6, 4, 5, 9](末尾 9 已排序) |
3 | 堆顶 8 与末尾 5 交换,缩小堆规模,调整堆 | [6, 5, 4, 8, 9](末尾 8、9 已排序) |
4 | 堆顶 6 与末尾 4 交换,缩小堆规模,调整堆 | [5, 4, 6, 8, 9](末尾 6、8、9 已排序) |
5 | 堆顶 5 与末尾 4 交换,堆规模为 1,排序完成 | [4, 5, 6, 8, 9](全部有序) |
优缺点分析
优点 | 缺点 |
---|---|
时间复杂度稳定为 O (n log n),效率高 | 不稳定排序(相等元素的相对位置可能变化) |
空间复杂度低,仅需 O (1) 辅助空间(原地排序) | 对小规模数据,性能不如插入排序等简单算法 |
适合处理海量数据,无需额外内存存储 | 实现逻辑较复杂,需理解堆结构和调整过程 |
5 快速排序
快速排序是一种高效的排序算法,采用分治法策略,平均时间复杂度为 O(n log n),最坏情况下为 O(n²)(可通过合理选择基准值优化),是不稳定排序。
核心原理:分而治之
快速排序的核心思想是通过分区操作将数组分为两部分,再递归处理子数组:
- 选择基准值(pivot):从数组中选择一个元素作为基准(通常选第一个、最后一个或中间元素)。
- 分区(partition):将数组重新排列,所有比基准值小的元素放基准左侧,比基准值大的放右侧(相等元素可放任意一侧)。
- 递归排序:对基准值左右两个子数组重复上述过程,直到子数组长度为 0 或 1(天然有序)。
关键操作:分区(以最后一个元素为基准)
分区是快速排序的核心步骤,目标是确定基准值的最终位置:
操作步骤:
- 选数组最后一个元素为基准值
pivot
。 - 初始化一个指针
i
(指向小于基准区域的边界,初始为-1
)。 - 遍历数组(0 到 n-2),对每个元素
arr[j]
:- 若
arr[j] < pivot
,则i++
,交换arr[i]
和arr[j]
(扩展小于基准的区域)。
- 若
- 遍历结束后,交换
arr[i+1]
和arr[n-1]
,将基准值放到最终位置i+1
。 - 返回基准值位置,用于分割子数组。
算法示例
以数组 [8, 1, 5, 3, 9, 4, 7, 6, 2]
为例:
- 选最后一个元素
2
为基准,分区后数组变为[1, 2, 5, 3, 9, 4, 7, 6, 8]
,基准2
已就位。 - 递归处理左子数组
[1]
(已有序)和右子数组[5, 3, 9, 4, 7, 6, 8]
。 - 对右子数组选
8
为基准,分区后为[5, 3, 6, 4, 7, 8, 9]
,基准8
就位。 - 继续递归处理剩余子数组,直至全部有序。
最终排序结果:[1, 2, 3, 4, 5, 6, 7, 8, 9]
优缺点分析
优点 | 缺点 |
---|---|
平均性能优秀,实际应用中通常比同为 O (n log n) 的归并排序快 | 最坏情况时间复杂度为 O (n²)(如对已排序数组选择两端元素为基准) |
原地排序,空间复杂度为 O (log n)(递归栈开销) | 不稳定排序(相等元素相对位置可能改变) |
缓存友好,局部性好 | 对小规模数据,性能不如插入排序 |
快速排序的c实现
#include <stdio.h>// 交换两个元素的值
void swap(int* a, int* b) {int temp = *a;*a = *b;*b = temp;
}/** 分区操作* 参数:* arr[] - 待分区的数组* low - 分区的起始索引* high - 分区的结束索引(基准值初始位置)* 返回值:* 基准值的最终位置索引*/
int partition(int arr[], int low, int high) {// 选择最右侧元素作为基准值int pivot = arr[high];// i指向小于基准值区域的最后一个元素int i = (low - 1);// 遍历数组,将小于基准值的元素移到左侧for (int j = low; j <= high - 1; j++) {if (arr[j] <= pivot) {i++; // 扩展小于基准值的区域swap(&arr[i], &arr[j]);}}// 将基准值放到最终位置(i+1)swap(&arr[i + 1], &arr[high]);return (i + 1);
}/** 快速排序主函数* 参数:* arr[] - 待排序的数组* low - 当前排序区间的起始索引* high - 当前排序区间的结束索引*/
void quickSort(int arr[], int low, int high) {if (low < high) {// 获取基准值的位置,将数组分为两部分int pi = partition(arr, low, high);// 递归排序基准值左侧子数组quickSort(arr, low, pi - 1);// 递归排序基准值右侧子数组quickSort(arr, pi + 1, high);}
}// 打印数组元素
void printArray(int arr[], int size) {for (int i = 0; i < size; i++) {printf("%d ", arr[i]);}printf("\n");
}// 示例用法
int main() {int arr[] = {8, 1, 5, 3, 9, 4, 7, 6, 2};int n = sizeof(arr) / sizeof(arr[0]);printf("排序前: ");printArray(arr, n);quickSort(arr, 0, n - 1);printf("排序后: ");printArray(arr, n);return 0;
}
优化建议
- 基准值选择:可采用 “三数取中” 法(首、尾、中间元素的中值)避免最坏情况。
- 小规模数组优化:当子数组长度小于一定阈值(如 10)时,改用插入排序。
- 尾递归优化:对较大的子数组采用循环处理,减少递归栈深度。