题目 1:快速排序实现与优化
题目描述
实现快速排序,并说明常见的优化手段。
解题思路
核心思想:分治 + 划分。选取一个基准值,将数组划分为小于基准和大于基准两部分,然后递归排序左右区间。
常见优化点:
- 随机选基准 / 三数取中,避免有序数组最坏情况
- 小区间改用插入排序,减少递归开销
- 三路划分,优化大量重复元素场景
代码实现(三数取中优化版)
cpp
运行
cpp
int median3(vector<int>& arr, int lo, int hi) {
int mid = lo + (hi - lo) / 2;
if (arr[lo] > arr[mid]) swap(arr[lo], arr[mid]);
if (arr[lo] > arr[hi]) swap(arr[lo], arr[hi]);
if (arr[mid] > arr[hi]) swap(arr[mid], arr[hi]);
swap(arr[mid], arr[hi - 1]);
return arr[hi - 1];
}
void quickSort(vector<int>& arr, int lo, int hi) {
if (hi - lo + 1 <= 16) {
// 小区间用插入排序优化
for (int i = lo + 1; i <= hi; i++) {
int temp = arr[i];
int j = i - 1;
while (j >= lo && arr[j] > temp) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = temp;
}
return;
}
int pivot = median3(arr, lo, hi);
int i = lo, j = hi - 1;
while (true) {
while (arr[++i] < pivot);
while (arr[--j] > pivot);
if (i < j) swap(arr[i], arr[j]);
else break;
}
swap(arr[i], arr[hi - 1]);
quickSort(arr, lo, i - 1);
quickSort(arr, i + 1, hi);
}
复杂度分析
- 平均时间复杂度:O (n log n)
- 最坏时间复杂度:O (n²),优化后概率极低
- 空间复杂度:O (log n),递归栈开销
面试考点
排序算法核心题。面试官常问:快排为什么快?(缓存友好、常数小)快排的稳定性?和归并排序的对比?
题目 2:归并排序与逆序对问题
题目描述
实现归并排序,并求解数组中的逆序对总数。逆序对指数组中满足 i < j 且 nums[i] > nums[j] 的数对。
解题思路
归并排序分「分」和「治」两步:将数组一分为二,递归排序左右子数组,再合并两个有序数组。
求逆序对:在合并两个有序数组时,如果右数组元素小于左数组当前元素,说明左数组剩余所有元素都与该右元素构成逆序对,累加计数即可。
代码实现
cpp
运行
cpp
int merge(vector<int>& nums, vector<int>& temp, int left, int mid, int right) {
int i = left, j = mid + 1, k = left;
int count = 0;
while (i <= mid && j <= right) {
if (nums[i] <= nums[j]) {
temp[k++] = nums[i++];
} else {
temp[k++] = nums[j++];
count += mid - i + 1; // 累加逆序对
}
}
while (i <= mid) temp[k++] = nums[i++];
while (j <= right) temp[k++] = nums[j++];
for (int p = left; p <= right; p++) {
nums[p] = temp[p];
}
return count;
}
int mergeSort(vector<int>& nums, vector<int>& temp, int left, int right) {
if (left >= right) return 0;
int mid = left + (right - left) / 2;
int count = 0;
count += mergeSort(nums, temp, left, mid);
count += mergeSort(nums, temp, mid + 1, right);
count += merge(nums, temp, left, mid, right);
return count;
}
int reversePairs(vector<int>& nums) {
vector<int> temp(nums.size());
return mergeSort(nums, temp, 0, nums.size() - 1);
}
复杂度分析
- 时间复杂度:O (n log n)
- 空间复杂度:O (n),辅助数组开销
面试考点
归并排序的经典应用,考察对归并过程的理解。延伸问:归并排序的稳定性?外排序中的应用?
题目 3:堆排序与 TopK 问题
题目描述
实现堆排序,并说明如何用堆解决 TopK 问题(找出数组中第 k 大的元素)。
解题思路
堆排序两步:
- 建大顶堆
- 不断将堆顶与末尾元素交换,调整堆,直到整个数组有序
TopK 问题: 找第 k 大用小顶堆:维护一个大小为 k 的小顶堆,遍历数组,元素大于堆顶则入堆并弹出堆顶,最终堆顶就是第 k 大元素。
代码实现
cpp
运行
cpp
// 堆排序
void adjustHeap(vector<int>& arr, int i, int len) {
int temp = arr[i];
for (int child = 2 * i + 1; child < len; child = 2 * child + 1) {
if (child + 1 < len && arr[child + 1] > arr[child]) {
child++;
}
if (temp >= arr[child]) break;
arr[i] = arr[child];
i = child;
}
arr[i] = temp;
}
void heapSort(vector<int>& arr) {
int n = arr.size();
// 建堆
for (int i = n / 2 - 1; i >= 0; i--) {
adjustHeap(arr, i, n);
}
// 排序
for (int i = n - 1; i > 0; i--) {
swap(arr[0], arr[i]);
adjustHeap(arr, 0, i);
}
}
// TopK:第k大元素
int findKthLargest(vector<int>& nums, int k) {
priority_queue<int, vector<int>, greater<int>> minHeap; // 小顶堆
for (int num : nums) {
if (minHeap.size() < k) {
minHeap.push(num);
} else if (num > minHeap.top()) {
minHeap.pop();
minHeap.push(num);
}
}
return minHeap.top();
}
复杂度分析
- 堆排序:时间 O (n log n),空间 O (1)
- TopK 小顶堆法:时间 O (n log k),空间 O (k)
面试考点
堆的核心应用。面试官常对比:快排选择法 vs 堆解法,各自的适用场景(海量数据、流数据)。
题目 4:排序算法稳定性与选型
题目描述
什么是排序算法的稳定性?常见排序算法哪些是稳定的?实际开发中如何选型?
详细讲解
1. 稳定性定义
如果排序后,两个相等元素的相对位置保持不变,则该排序算法是稳定的;反之则不稳定。 例如原数组 [2a, 1, 2b](两个 2 位置不同),稳定排序后 2a 仍在 2b 前面,不稳定排序则可能交换。
2. 常见算法稳定性分类
表格
| 算法 | 稳定性 | 原因 |
|---|---|---|
| 冒泡排序 | 稳定 | 相邻交换,相等不交换 |
| 插入排序 | 稳定 | 向前插入,相等不移动 |
| 归并排序 | 稳定 | 合并时相等优先取左半部分 |
| 基数排序 | 稳定 | 按位排序,保留相对顺序 |
| 选择排序 | 不稳定 | 跨元素交换可能打乱顺序 |
| 快速排序 | 不稳定 | 划分时的交换会破坏相对位置 |
| 堆排序 | 不稳定 | 堆调整时交换会打乱顺序 |
| 希尔排序 | 不稳定 | 不同步长分组插入 |
3. 实际选型原则
- 追求平均速度:优先快排,工程中一般用优化版快排(自省排序)
- 要求稳定排序:选归并排序,数据量小时可选插入排序
- 海量数据、内存不足:归并排序(外排序)
- TopK、优先级队列:堆
- 整数、范围有限:计数排序、基数排序(线性时间)
面试考点
排序算法的基础理论题,考察知识体系的完整性。面试官常结合具体场景让你选择排序算法并说明理由。
谢谢