前言:
本篇承接 C 语言基础语法与工程化能力模块,聚焦笔试手撕代码核心高频考点,系统覆盖排序、查找、字符串三大必考模块,从基础算法原理、标准手撕实现到笔试真题变种全覆盖,所有代码贴合笔试答题规范,兼顾边界条件与代码鲁棒性,是 C 语言岗位求职笔试冲刺的核心必备内容,适合零基础入门算法、校招社招笔试复盘与面试突击复习。
一、排序算法全解
排序是算法与数据结构的基础,也是笔试手撕代码的第一大高频题型,考察重点不仅是能写出来,更要掌握时间 / 空间复杂度、稳定性、适用场景与边界处理。
算法核心指标对比表
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 | 教学演示、小规模近乎有序数据 |
| 选择排序 | O(n²) | O(n²) | O(1) | 不稳定 | 小规模数据、交换次数少的场景 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 | 小规模、近乎有序数据,性能优于冒泡 |
| 快速排序 | O(nlogn) | O(n²) | O(logn) | 不稳定 | 通用场景,绝大多数情况性能最优 |
| 归并排序 | O(nlogn) | O(nlogn) | O(n) | 稳定 | 海量数据、外部排序、要求稳定的场景 |
稳定性定义:两个相等的元素,排序后相对位置保持不变,则为稳定排序。
1. 冒泡排序
核心思想
相邻元素两两比较,大的往后交换,每一轮将当前最大的元素 "冒泡" 到末尾。
手撕实现
void bubbleSort(int* arr, int len) {
if (arr == NULL || len <= 1) return;
for (int i = 0; i < len - 1; i++) {
int flag = 0; // 优化标记:本轮是否发生交换
for (int j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
flag = 1;
}
}
if (flag == 0) break; // 本轮无交换,已有序,提前退出
}
}
考点:优化版 flag 的作用
当某一轮遍历没有发生任何交换,说明数组已经完全有序,直接提前终止,最好时间复杂度可优化到 O (n)。
2. 快速排序(笔试最高频)
核心思想
分治思想:选一个基准值,将数组分为小于基准和大于基准两部分,再递归排序左右两个子区间。
霍尔分区法(左右指针法)
// 分区函数,返回基准值最终位置
int partition(int* arr, int left, int right) {
int key = arr[left]; // 选最左为基准
while (left < right) {
// 右指针往左找小于基准的值
while (left < right && arr[right] >= key) {
right--;
}
arr[left] = arr[right];
// 左指针往右找大于基准的值
while (left < right && arr[left] <= key) {
left++;
}
arr[right] = arr[left];
}
arr[left] = key; // 基准值归位
return left;
}
// 快排主函数
void quickSort(int* arr, int left, int right) {
if (left >= right) return;
int pivot = partition(arr, left, right);
quickSort(arr, left, pivot - 1); // 递归排左区间
quickSort(arr, pivot + 1, right); // 递归排右区间
}
高频考点
- 最坏情况:数组已有序时,每次分区都极不均匀,时间复杂度退化为 O (n²);优化方法:随机选基准、三数取中法选基准
- 空间复杂度:递归调用栈的开销,平均 O (logn),最坏 O (n)
- 不稳定:交换过程会打乱相等元素的相对顺序
- 适用场景:通用排序首选,绝大多数场景性能最优,是面试必背代码
3. 归并排序
核心思想
分治思想:将数组不断二分拆分为子序列,再将有序子序列合并为完整有序数组。
手撕实现
// 合并两个有序区间 [left, mid] 和 [mid+1, right]
void merge(int* arr, int left, int mid, int right, int* tmp) {
int i = left; // 左区间起点
int j = mid + 1; // 右区间起点
int k = left; // 临时数组下标
// 按序合并
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
tmp[k++] = arr[i++];
} else {
tmp[k++] = arr[j++];
}
}
// 拷贝剩余元素
while (i <= mid) tmp[k++] = arr[i++];
while (j <= right) tmp[k++] = arr[j++];
// 拷回原数组
for (i = left; i <= right; i++) {
arr[i] = tmp[i];
}
}
// 归并排序主函数
void mergeSort(int* arr, int left, int right, int* tmp) {
if (left >= right) return;
int mid = left + (right - left) / 2; // 避免溢出
mergeSort(arr, left, mid, tmp);
mergeSort(arr, mid + 1, right, tmp);
merge(arr, left, mid, right, tmp);
}
高频考点
- 优势:最坏时间复杂度依然是 O (nlogn),性能稳定,且是稳定排序
- 缺点:需要 O (n) 额外空间,空间复杂度高
- 适用场景:海量数据排序、外部排序、要求排序稳定的场景
二、查找算法核心
查找是笔试第二大基础题型,其中二分查找是最高频考点,核心考察边界条件处理与变种问题。
1. 基础二分查找
适用场景
有序数组的目标值查找,时间复杂度 O (logn),远快于遍历查找 O (n)。
标准循环实现(闭区间写法)
// 找到返回下标,找不到返回-1
int binarySearch(int* arr, int len, int target) {
if (arr == NULL || len <= 0) return -1;
int left = 0;
int right = len - 1; // 闭区间 [left, right]
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1; // 目标在右半区
} else {
right = mid - 1; // 目标在左半区
}
}
return -1;
}
核心细节
mid = left + (right - left) / 2替代(left+right)/2,避免两数相加溢出- 闭区间对应循环条件
left <= right,边界更新为mid±1,是最不易出错的标准写法
2. 二分查找高频变种
变种 1:查找第一个等于目标值的位置
int findFirst(int* arr, int len, int target) {
int left = 0;
int right = len - 1;
int res = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
res = mid; // 记录位置
right = mid - 1; // 继续往左找更早的
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return res;
}
变种 2:查找最后一个等于目标值的位置
int findLast(int* arr, int len, int target) {
int left = 0;
int right = len - 1;
int res = -1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
res = mid;
left = mid + 1; // 继续往右找更晚的
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return res;
}
二分变种核心思路:找到目标时不立刻返回,继续向目标方向收缩区间,直到锁定边界。
三、字符串高频手撕题
字符串操作是 C 语言笔试的常驻题型,本质考察指针操作与内存边界意识,常考标准库函数的手动实现。
1. 手写 strlen:求字符串长度
size_t myStrlen(const char* str) {
assert(str != NULL);
size_t len = 0;
while (*str != '\0') {
len++;
str++;
}
return len;
}
考点:必须校验空指针;以
'\0'为结束标志
2. 手写 strcpy:字符串拷贝
char* myStrcpy(char* dest, const char* src) {
assert(dest != NULL && src != NULL);
char* ret = dest; // 保存目标起始地址
while ((*dest++ = *src++) != '\0') {
;
}
return ret;
}
考点:源字符串加 const 保护;返回目标地址支持链式调用;会拷贝结束符
3. 手写 strcmp:字符串比较
int myStrcmp(const char* s1, const char* s2) {
assert(s1 != NULL && s2 != NULL);
while (*s1 == *s2 && *s1 != '\0') {
s1++;
s2++;
}
return *s1 - *s2;
// 大于返回正数,等于返回0,小于返回负数
}
4. 字符串反转
void reverseString(char* str) {
assert(str != NULL);
int left = 0;
int right = myStrlen(str) - 1;
while (left < right) {
char tmp = str[left];
str[left] = str[right];
str[right] = tmp;
left++;
right--;
}
}
四、笔试高频真题精选
真题 1:数组原地去重(有序数组)
题目 :给你一个有序数组,原地删除重复出现的元素,返回去重后的新长度。 思路:快慢指针,慢指针指向最后一个不重复元素,快指针遍历,遇到不同元素就更新慢指针。
int removeDuplicates(int* nums, int numsSize) {
if (numsSize <= 1) return numsSize;
int slow = 0;
for (int fast = 1; fast < numsSize; fast++) {
if (nums[fast] != nums[slow]) {
slow++;
nums[slow] = nums[fast];
}
}
return slow + 1;
}
真题 2:两数之和
题目 :给定一个整数数组和一个目标值,找出数组中和为目标值的两个数的下标。 思路:暴力解法双层循环 O (n²),笔试基础写法;进阶可用哈希表 O (n)。
// 暴力解法,笔试基础版
int* twoSum(int* nums, int numsSize, int target, int* returnSize) {
*returnSize = 2;
int* res = (int*)malloc(sizeof(int) * 2);
for (int i = 0; i < numsSize; i++) {
for (int j = i + 1; j < numsSize; j++) {
if (nums[i] + nums[j] == target) {
res[0] = i;
res[1] = j;
return res;
}
}
}
free(res);
*returnSize = 0;
return NULL;
}
真题 3:最长公共前缀
题目:编写一个函数,查找字符串数组中的最长公共前缀。
char* longestCommonPrefix(char** strs, int strsSize) {
if (strsSize == 0) return "";
// 以第一个字符串为基准
for (int i = 0; strs[0][i] != '\0'; i++) {
char c = strs[0][i];
// 逐个对比其他字符串的对应位置
for (int j = 1; j < strsSize; j++) {
if (strs[j][i] != c || strs[j][i] == '\0') {
// 出现不同或字符串结束,截断返回
strs[0][i] = '\0';
return strs[0];
}
}
}
return strs[0];
}
五、面试高频考点与易错坑点
1. 经典面试问答
Q1:快速排序的时间复杂度是多少?最坏情况是什么?怎么优化?
答: 平均时间复杂度 O (nlogn),最坏 O (n²)。 最坏情况出现在数组完全有序或完全逆序时,每次分区极不均匀,退化为冒泡排序。 优化方法:
- 随机选择基准值,避免有序数组最坏情况
- 三数取中法选基准,降低极端情况概率
- 数据量较小时切换为插入排序,减少递归开销
Q2:哪些排序是稳定的?哪些不稳定?
答: 稳定排序:冒泡排序、插入排序、归并排序、基数排序 不稳定排序:选择排序、快速排序、堆排序、希尔排序 判断核心:排序过程中,相等元素的相对顺序是否会被交换打乱。
Q3:二分查找的前提条件是什么?有哪些常见变种?
答: 前提:数组必须有序,且支持随机访问(数组适用,链表不适用)。 常见变种:查找第一个等于目标值的位置、查找最后一个等于目标值的位置、查找第一个大于等于目标值的位置、查找最后一个小于等于目标值的位置。
Q4:手写字符串函数为什么要校验空指针?为什么源指针要加 const?
答: 校验空指针是为了避免解引用空指针导致段错误,提升代码鲁棒性,是工程代码的基本规范。 源指针加 const 是为了保护源字符串不被意外修改,同时可以接收 const 字符串参数,提升函数通用性,符合 const 正确性原则。
Q5:归并排序和快速排序各有什么优缺点,分别适用什么场景?
答: 快排优点:平均性能好、原地排序空间开销小;缺点:不稳定、最坏时间复杂度差。 归并优点:性能稳定、最坏依然 O (nlogn)、稳定排序;缺点:需要额外 O (n) 空间。 适用场景:通用排序优先快排;要求排序稳定、数据量大、外部排序场景用归并。
2. 常见易错坑点
- 快排分区边界处理错误,导致死循环或数组越界
- 二分查找 mid 计算用
(left+right)/2,大数场景下溢出 - 字符串操作忘记
'\0'结束符,拷贝、比较时出现越界 - 排序算法忘记做空数组、单元素的边界校验
- 二分变种找到目标直接返回,不会收缩区间查找边界
- 字符串函数不校验空指针,传入 NULL 直接崩溃
- 归并排序忘记申请临时数组,或临时数组重复申请释放导致性能低下
以上就是 C 语言笔试最核心的经典算法手撕内容,覆盖排序、查找、字符串三大必考模块,建议重点掌握快排、归并、二分查找与字符串函数实现,是绝大多数公司笔试的常驻题型。
制作不易,如果对你有用,希望能点赞收藏支持一下。