6道经典算法题详解:从排序到链表,覆盖面试高频考点

6道经典算法题详解:从排序到链表,覆盖面试高频考点

算法面试中,排序算法分治思想链表操作 是绝对的高频考点,几乎是每家公司的必考题。本文精选了LeetCode中6道经典题目,覆盖「三色排序」「归并排序」「逆序对统计」「链表相加」「链表两两交换」等核心场景,每道题都包含题目解析、思路推导、完整C++代码、复杂度分析,帮你彻底吃透这些高频考点。


文章目录

一、75. 颜色分类(中等)

题目描述

给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

我们使用整数 012 分别表示红色、白色和蓝色。

必须在不使用库内置的 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):

  1. 定义三个指针:
    • left:0的右边界,初始为-1,[0, left] 区间全为0
    • right:2的左边界,初始为n,[right, n-1] 区间全为2
    • i:遍历指针,初始为0,[left+1, i-1] 区间全为1
  2. 遍历数组,直到 i < right
    • 如果 nums[i] == 0:和 left+1 位置交换,left++i++(交换后i位置是1,直接后移)
    • 如果 nums[i] == 1:直接 i++,1的区间扩大
    • 如果 nums[i] == 2:和 right-1 位置交换,right--i不后移(交换后i位置是新元素,需要重新判断)
  3. 遍历完成后,数组自然按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. :将数组从中间不断二分,直到每个子数组长度为1(天然有序)
  2. :将两个有序子数组合并为一个有序数组,递归回溯
  3. 合并时用双指针遍历两个子数组,按大小顺序放入临时数组,最后拷贝回原数组。

完整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)。

思路分析(归并排序求逆序对)

逆序对的统计是归并排序的经典应用,核心是在合并阶段统计逆序对

  1. 归并排序的分治过程不变,在合并两个有序子数组 [left, mid][mid+1, right] 时:
    • 如果 record[cur1] > record[cur2],说明 cur1mid 之间的所有元素都大于 record[cur2],逆序对数量 += mid - cur1 + 1
    • 否则正常合并
  2. 递归回溯时累加所有逆序对,最终得到总逆序对数量。

完整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

思路分析(链表模拟加法)

这道题是链表操作的经典入门题,核心是模拟竖式加法

  1. 定义两个指针 cur1cur2 分别遍历两个链表,同时维护一个进位变量 t(初始为0)
  2. 遍历两个链表,只要有一个链表不为空或进位不为0,就继续计算:
    • 累加当前节点的值和进位:t += cur1 ? cur1->val : 0; t += cur2 ? cur2->val : 0
    • 创建新节点存储 t % 10,更新进位 t /= 10
    • 指针后移,直到两个链表都遍历完且进位为0
  3. 最后返回虚拟头节点的下一个节点(避免处理头节点为空的情况)。

完整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]

思路分析(链表指针操作)

这道题考察链表的指针操作,核心是用虚拟头节点简化头节点交换的逻辑:

  1. 创建虚拟头节点 newhead,指向原链表头节点,避免处理头节点为空的特殊情况
  2. 定义三个指针:prev(当前交换组的前一个节点)、cur(交换组的第一个节点)、next(交换组的第二个节点)、nnext(下一个交换组的第一个节点)
  3. 循环交换:
    • prev->next = next:前一个节点指向交换后的第一个节点
    • next->next = cur:交换两个节点
    • cur->next = nnext:交换后的第二个节点指向下一个交换组
    • 更新指针:prev = curcur = nnext,继续下一组交换
  4. 最后返回虚拟头节点的下一个节点,释放虚拟头节点避免内存泄漏。

完整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、最后一组交换等边界情况

面试高频追问

  1. 颜色分类除了三指针,还有其他解法吗?
    可以用两次遍历:第一次统计0、1、2的数量,第二次按数量重写数组,时间复杂度O(n),空间复杂度O(1),但三指针法是原地一次遍历,更优。
  2. 归并排序可以优化空间吗?
    可以用「原地归并排序」,但时间复杂度会退化到 O ( n 2 ) O(n^2) O(n2),工程中一般用临时数组的归并排序,空间换时间。
  3. 两数相加如果链表是正序存储怎么办?
    可以先反转链表,再按本题逻辑相加,最后反转结果;或者用栈存储链表元素,再出栈相加。
  4. 两两交换链表节点可以用递归吗?
    可以,递归逻辑:交换前两个节点,递归处理后续链表,时间复杂度O(n),空间复杂度O(n)(递归栈),迭代法空间更优。
相关推荐
wfbcg2 小时前
每日算法练习:LeetCode 3. 无重复字符的最长子串 ✅
算法·leetcode·职场和发展
何陋轩2 小时前
AI时代,程序员何去何从?别慌,看完这篇你就明白了
后端·面试
keqistarry2 小时前
java-python快速转语言
面试
_日拱一卒2 小时前
LeetCode:矩阵置零
java·数据结构·线性代数·算法·leetcode·职场和发展·矩阵
穿条秋裤到处跑2 小时前
每日一道leetcode(2026.04.10):三个相等元素之间的最小距离 I
算法·leetcode
nlpming2 小时前
OpenClaw 代码解析
算法
学习永无止境@2 小时前
MATLAB中矩阵转置
算法·matlab·fpga开发·矩阵
Wect2 小时前
JS手撕:函数进阶 & 设计模式解析
前端·javascript·面试
七颗糖很甜2 小时前
雨滴谱数据深度解析——从原始变量到科学产品的Python实现【下篇】
python·算法·pandas