6道经典算法题详解:从排序到链表,覆盖面试高频考点
算法面试中,排序算法 、分治思想 和链表操作 是绝对的高频考点,几乎是每家公司的必考题。本文精选了LeetCode中6道经典题目,覆盖「三色排序」「归并排序」「逆序对统计」「链表相加」「链表两两交换」等核心场景,每道题都包含题目解析、思路推导、完整C++代码、复杂度分析,帮你彻底吃透这些高频考点。
文章目录
- 6道经典算法题详解:从排序到链表,覆盖面试高频考点
一、75. 颜色分类(中等)
题目描述
给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0、1 和 2 分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
示例1:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
示例2:
输入:nums = [2,0,1]
输出:[0,1,2]
思路分析(荷兰国旗问题)
这道题是经典的荷兰国旗问题 ,核心是用三指针法实现原地排序,时间复杂度O(n),空间复杂度O(1):
- 定义三个指针:
left:0的右边界,初始为-1,[0, left]区间全为0right:2的左边界,初始为n,[right, n-1]区间全为2i:遍历指针,初始为0,[left+1, i-1]区间全为1
- 遍历数组,直到
i < right:- 如果
nums[i] == 0:和left+1位置交换,left++,i++(交换后i位置是1,直接后移) - 如果
nums[i] == 1:直接i++,1的区间扩大 - 如果
nums[i] == 2:和right-1位置交换,right--,i不后移(交换后i位置是新元素,需要重新判断)
- 如果
- 遍历完成后,数组自然按0、1、2有序。
完整C++代码
cpp
class Solution {
public:
void sortColors(vector<int>& nums) {
int n = nums.size();
int left = -1, right = n, i = 0;
while (i < right) {
if (nums[i] == 0) {
swap(nums[++left], nums[i++]);
} else if (nums[i] == 1) {
i++;
} else {
swap(nums[--right], nums[i]);
}
}
}
};
复杂度分析
- 时间复杂度 : O ( n ) O(n) O(n),仅遍历一次数组,每个元素最多被交换一次。
- 空间复杂度 : O ( 1 ) O(1) O(1),仅用三个指针,原地排序。
二、912. 排序数组(中等)
题目描述
给你一个整数数组 nums,请你将该数组升序排列。
你必须在不使用任何内置函数 的情况下解决问题,时间复杂度为 O ( n log n ) O(n \log n) O(nlogn),并且空间复杂度尽可能小。
示例1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
思路分析(归并排序)
归并排序是稳定的 O ( n log n ) O(n \log n) O(nlogn) 排序算法,核心是分治思想:
- 分:将数组从中间不断二分,直到每个子数组长度为1(天然有序)
- 治:将两个有序子数组合并为一个有序数组,递归回溯
- 合并时用双指针遍历两个子数组,按大小顺序放入临时数组,最后拷贝回原数组。
完整C++代码
cpp
class Solution {
public:
vector<int> sortArray(vector<int>& record) {
merge(record, 0, record.size() - 1);
return record;
}
void merge(vector<int>& record, int left, int right) {
if (left >= right) return;
int mid = (left + right) >> 1;
// 递归分治
merge(record, left, mid);
merge(record, mid + 1, right);
// 合并两个有序子数组
vector<int> tmp(right - left + 1);
int cur1 = left, cur2 = mid + 1, i = 0;
while (cur1 <= mid && cur2 <= right) {
tmp[i++] = record[cur1] <= record[cur2] ? record[cur1++] : record[cur2++];
}
// 拷贝剩余元素
while (cur1 <= mid) tmp[i++] = record[cur1++];
while (cur2 <= right) tmp[i++] = record[cur2++];
// 写回原数组
for (int i = left; i <= right; i++) {
record[i] = tmp[i - left];
}
}
};
复杂度分析
- 时间复杂度 : O ( n log n ) O(n \log n) O(nlogn),二分层数为 log n \log n logn,每层合并时间为 O ( n ) O(n) O(n)。
- 空间复杂度 : O ( n ) O(n) O(n),合并时需要临时数组存储中间结果。
三、LCR 170. 交易逆序对的总数(困难)
题目描述
在股票交易中,如果前一天的股价高于后一天的股价,则可以认为存在一个「交易逆序对」。请设计一个程序,输入一段时间内的股票交易记录 record,返回其中存在的「交易逆序对」总数。
示例1:
输入:record = [9, 7, 5, 4, 6]
输出:8
解释:交易中的逆序对为 (9,7), (9,5), (9,4), (9,6), (7,5), (7,4), (7,6), (5,4)。
思路分析(归并排序求逆序对)
逆序对的统计是归并排序的经典应用,核心是在合并阶段统计逆序对:
- 归并排序的分治过程不变,在合并两个有序子数组
[left, mid]和[mid+1, right]时:- 如果
record[cur1] > record[cur2],说明cur1到mid之间的所有元素都大于record[cur2],逆序对数量 +=mid - cur1 + 1 - 否则正常合并
- 如果
- 递归回溯时累加所有逆序对,最终得到总逆序对数量。
完整C++代码
cpp
class Solution {
public:
int reversePairs(vector<int>& record) {
int res = 0;
merge(record, 0, record.size() - 1, res);
return res;
}
void merge(vector<int>& record, int left, int right, int& res) {
if (left >= right) return;
int mid = (left + right) >> 1;
merge(record, left, mid, res);
merge(record, mid + 1, right, res);
vector<int> tmp(right - left + 1);
int cur1 = left, cur2 = mid + 1, i = 0;
while (cur1 <= mid && cur2 <= right) {
if (record[cur1] > record[cur2]) {
// 统计逆序对:cur1到mid的元素都大于cur2
res += mid - cur1 + 1;
tmp[i++] = record[cur2++];
} else {
tmp[i++] = record[cur1++];
}
}
while (cur1 <= mid) tmp[i++] = record[cur1++];
while (cur2 <= right) tmp[i++] = record[cur2++];
for (int i = left; i <= right; i++) {
record[i] = tmp[i - left];
}
}
};
复杂度分析
- 时间复杂度 : O ( n log n ) O(n \log n) O(nlogn),归并排序的时间复杂度,统计逆序对仅增加O(1)操作。
- 空间复杂度 : O ( n ) O(n) O(n),临时数组的空间。
四、2. 两数相加(中等)
题目描述
给你两个非空 的链表,表示两个非负的整数。它们每位数字都是按照逆序 的方式存储的,并且每个节点只能存储一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807
思路分析(链表模拟加法)
这道题是链表操作的经典入门题,核心是模拟竖式加法:
- 定义两个指针
cur1、cur2分别遍历两个链表,同时维护一个进位变量t(初始为0) - 遍历两个链表,只要有一个链表不为空或进位不为0,就继续计算:
- 累加当前节点的值和进位:
t += cur1 ? cur1->val : 0; t += cur2 ? cur2->val : 0 - 创建新节点存储
t % 10,更新进位t /= 10 - 指针后移,直到两个链表都遍历完且进位为0
- 累加当前节点的值和进位:
- 最后返回虚拟头节点的下一个节点(避免处理头节点为空的情况)。
完整C++代码
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
ListNode* cur1 = l1, *cur2 = l2;
ListNode* newnode = new ListNode(0); // 虚拟头节点
ListNode* prev = newnode;
int t = 0; // 进位
while (cur1 || cur2 || t) {
if (cur1) {
t += cur1->val;
cur1 = cur1->next;
}
if (cur2) {
t += cur2->val;
cur2 = cur2->next;
}
// 创建新节点
prev->next = new ListNode(t % 10);
prev = prev->next;
t /= 10;
}
ListNode* res = newnode->next;
delete newnode; // 释放虚拟头节点
return res;
}
};
复杂度分析
- 时间复杂度 : O ( max ( m , n ) ) O(\max(m, n)) O(max(m,n)), m m m 和 n n n 分别是两个链表的长度,最多遍历较长链表一次。
- 空间复杂度 : O ( max ( m , n ) ) O(\max(m, n)) O(max(m,n)),结果链表的长度最多为 max ( m , n ) + 1 \max(m, n) + 1 max(m,n)+1(进位)。
五、24. 两两交换链表中的节点(中等)
题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例1:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
示例2:
输入:head = []
输出:[]
示例3:
输入:head = [1]
输出:[1]
思路分析(链表指针操作)
这道题考察链表的指针操作,核心是用虚拟头节点简化头节点交换的逻辑:
- 创建虚拟头节点
newhead,指向原链表头节点,避免处理头节点为空的特殊情况 - 定义三个指针:
prev(当前交换组的前一个节点)、cur(交换组的第一个节点)、next(交换组的第二个节点)、nnext(下一个交换组的第一个节点) - 循环交换:
prev->next = next:前一个节点指向交换后的第一个节点next->next = cur:交换两个节点cur->next = nnext:交换后的第二个节点指向下一个交换组- 更新指针:
prev = cur,cur = nnext,继续下一组交换
- 最后返回虚拟头节点的下一个节点,释放虚拟头节点避免内存泄漏。
完整C++代码
cpp
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr || head->next == nullptr) return head;
ListNode* newhead = new ListNode(0);
newhead->next = head;
ListNode* prev = newhead, *cur = prev->next, *next = cur->next, *nnext = next->next;
while (cur && next) {
// 交换节点
prev->next = next;
next->next = cur;
cur->next = nnext;
// 更新指针
prev = cur;
cur = nnext;
if (cur) next = cur->next;
if (next) nnext = next->next;
}
ListNode* res = newhead->next;
delete newhead;
return res;
}
};
复杂度分析
- 时间复杂度 : O ( n ) O(n) O(n),仅遍历一次链表,每个节点最多被访问一次。
- 空间复杂度 : O ( 1 ) O(1) O(1),仅用几个指针,原地操作。
六、算法核心总结
1. 排序算法对比
| 算法 | 时间复杂度 | 空间复杂度 | 稳定性 | 适用场景 |
|---|---|---|---|---|
| 三指针法(荷兰国旗) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) | 稳定 | 仅0/1/2三种元素的排序 |
| 归并排序 | O ( n log n ) O(n \log n) O(nlogn) | O ( n ) O(n) O(n) | 稳定 | 通用排序,适合链表排序 |
| 归并求逆序对 | O ( n log n ) O(n \log n) O(nlogn) | O ( n ) O(n) O(n) | 稳定 | 统计数组逆序对 |
2. 链表操作核心技巧
- 虚拟头节点:统一处理头节点交换、插入、删除等操作,避免特殊情况
- 指针备份:修改指针前先备份后续节点,避免链表断裂
- 边界处理:始终考虑链表为空、长度为1、最后一组交换等边界情况
面试高频追问
- 颜色分类除了三指针,还有其他解法吗?
可以用两次遍历:第一次统计0、1、2的数量,第二次按数量重写数组,时间复杂度O(n),空间复杂度O(1),但三指针法是原地一次遍历,更优。 - 归并排序可以优化空间吗?
可以用「原地归并排序」,但时间复杂度会退化到 O ( n 2 ) O(n^2) O(n2),工程中一般用临时数组的归并排序,空间换时间。 - 两数相加如果链表是正序存储怎么办?
可以先反转链表,再按本题逻辑相加,最后反转结果;或者用栈存储链表元素,再出栈相加。 - 两两交换链表节点可以用递归吗?
可以,递归逻辑:交换前两个节点,递归处理后续链表,时间复杂度O(n),空间复杂度O(n)(递归栈),迭代法空间更优。