4道经典算法题代码详解:从两数之和到链表两两交换
一、前言
在算法面试与刷题过程中,有几道题堪称「入门必刷、面试高频」的经典题目,它们覆盖了哈希表、链表操作、数组模拟哈希等核心算法思想,是打牢算法基础的关键。
本文将对4道经典题目(两数之和、两数相加、两两交换链表中的节点、判定是否互为字符重排)进行完整的思路拆解、代码详解与优化分析,所有代码均为C++实现,可直接在LeetCode上通过测试,适合算法入门与面试复习。
文章目录
- 4道经典算法题代码详解:从两数之和到链表两两交换
二、题目1:两数之和(LeetCode 1)
题目描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。你可以按任意顺序返回答案。
思路分析
- 暴力解法:两层循环遍历数组,时间复杂度O(n²),空间复杂度O(1),但数据量较大时效率极低,不推荐。
- 哈希表优化 :使用
unordered_map存储「数值→下标」的映射,遍历数组时,计算target - nums[i],若该值已在哈希表中,则直接返回两个下标;否则将当前数值与下标存入哈希表。 - 时间复杂度:O(n),空间复杂度:O(n),是最优解法。
完整代码
cpp
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hash;
for(int i = 0; i < nums.size(); i++)
{
int x = target - nums[i];
if(hash.count(x)) return {hash[x], i};
hash[nums[i]] = i;
}
return {-1, -1};
}
};
代码详解
- 哈希表初始化 :
unordered_map<int, int> hash,用于存储已经遍历过的数值及其下标。 - 遍历数组 :
for(int i = 0; i < nums.size(); i++),逐个处理数组元素。 - 计算目标差值 :
int x = target - nums[i],计算需要匹配的另一个数值。 - 哈希表查询 :
if(hash.count(x)),判断差值是否已存在于哈希表中,若存在则直接返回{hash[x], i}(两个下标)。 - 存入哈希表 :若不存在,则将当前数值
nums[i]与下标i存入哈希表,供后续元素匹配。 - 兜底返回 :
return {-1, -1},题目保证有解,仅为语法兜底。
三、题目2:两数相加(LeetCode 2)
题目描述
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
思路分析
- 链表逆序存储数字,天然适合从低位到高位相加,直接模拟加法运算即可。
- 核心要点:处理进位,即使两个链表都遍历完,若进位不为0,仍需新增节点存储进位。
- 使用虚拟头节点简化链表操作,避免处理头节点为空的特殊情况。
- 时间复杂度:O(max(m,n)),空间复杂度:O(1)(不计入结果链表)。
完整代码
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;
}
prev = newnode->next;
delete newnode;
return prev;
}
};
代码详解
- 指针初始化 :
cur1、cur2分别指向两个链表的头节点,用于遍历。 - 虚拟头节点 :
newnode为虚拟头节点,prev指向当前结果链表的尾节点,用于新增节点。 - 进位变量 :
t存储当前位的和与进位,初始为0。 - 循环相加 :
while(cur1 || cur2 || t),只要有一个链表未遍历完,或进位不为0,就继续循环。- 若
cur1不为空,将cur1->val加入t,并移动cur1指针。 - 若
cur2不为空,将cur2->val加入t,并移动cur2指针。 - 新增节点存储
t % 10(当前位的结果),移动prev指针。 t /= 10,更新进位(仅保留十位,即进位值)。
- 若
- 处理虚拟头节点 :
prev = newnode->next,跳过虚拟头节点,得到结果链表的头节点。 - 内存释放 :
delete newnode,释放虚拟头节点,避免内存泄漏。 - 返回结果 :返回结果链表的头节点
prev。
四、题目3:两两交换链表中的节点(LeetCode 24)
题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路分析
- 核心要求:仅交换节点,不修改节点值,因此需要操作指针,调整节点的指向。
- 使用虚拟头节点简化操作,统一处理头节点交换的特殊情况。
- 遍历链表,每次处理两个节点的交换,更新指针指向,直到链表末尾。
- 时间复杂度:O(n),空间复杂度:O(1)。
完整代码
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;
}
cur = newhead->next;
delete newhead;
return cur;
}
};
代码详解
- 边界判断 :
if(head == nullptr || head->next == nullptr) return head,链表为空或只有一个节点,直接返回。 - 虚拟头节点 :
newhead为虚拟头节点,newhead->next = head,将原链表接入虚拟头节点后。 - 指针初始化 :
prev:当前待交换的两个节点的前驱节点(初始为虚拟头节点)。cur:待交换的第一个节点。next:待交换的第二个节点。nnext:待交换的第二个节点的后继节点(下一轮的cur)。
- 循环交换 :
while(cur && next),只要有两个可交换的节点,就继续循环。prev->next = next:前驱节点指向第二个节点。next->next = cur:第二个节点指向第一个节点,完成两个节点的交换。cur->next = nnext:第一个节点指向后继节点,连接后续链表。- 更新指针 :
prev = cur(下一轮的前驱节点为当前交换后的第一个节点),cur = nnext(下一轮的第一个节点),next = cur->next(下一轮的第二个节点),nnext = next->next(下一轮的后继节点),注意空指针判断。
- 处理虚拟头节点 :
cur = newhead->next,得到交换后的链表头节点。 - 内存释放 :
delete newhead,释放虚拟头节点,避免内存泄漏。 - 返回结果 :返回交换后的链表头节点
cur。
五、题目4:判定是否互为字符重排(面试题 01.02)
题目描述
给定两个由小写字母组成的字符串 s1 和 s2,请编写一个程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。
思路分析
- 核心逻辑:两个字符串互为重排,当且仅当它们的字符种类与数量完全相同。
- 优化方案:由于仅包含小写字母,使用长度为26的数组模拟哈希表,统计
s1的字符出现次数,再遍历s2减去对应次数,若出现负数则直接返回false,最终返回true。 - 时间复杂度:O(n),空间复杂度:O(1)(数组长度固定为26),是最优解法。
完整代码
cpp
class Solution {
public:
bool CheckPermutation(string s1, string s2) {
if(s1.size() != s2.size()) return false;
//使用两个哈希表,数组模拟哈希表,又刷上力扣了,小朋友
int hash[26] = {0};
for(auto ch : s1)
{
hash[ch - 'a']++;
}
for(auto ch : s2)
{
hash[ch - 'a']--;
if(hash[ch - 'a'] < 0) return false;
}
return true;
}
};
代码详解
- 长度判断 :
if(s1.size() != s2.size()) return false,长度不同直接返回false,是最基础的剪枝。 - 数组模拟哈希表 :
int hash[26] = {0},长度为26的数组,对应26个小写字母,初始值为0。 - 统计
s1字符 :for(auto ch : s1),遍历s1,hash[ch - 'a']++,将字符转换为数组下标(a对应0,b对应1,...,z对应25),统计出现次数。 - 校验
s2字符 :for(auto ch : s2),遍历s2,hash[ch - 'a']--,减去对应字符的次数。- 若
hash[ch - 'a'] < 0,说明s2中该字符数量多于s1,直接返回false。
- 若
- 返回结果 :遍历完成后,所有字符次数均为0,返回
true。
六、总结与拓展
核心算法思想总结
| 题目 | 核心思想 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 两数之和 | 哈希表优化 | O(n) | O(n) |
| 两数相加 | 链表模拟加法 | O(max(m,n)) | O(1) |
| 两两交换链表中的节点 | 链表指针操作 | O(n) | O(1) |
| 判定是否互为字符重排 | 数组模拟哈希表 | O(n) | O(1) |
面试高频考点
- 两数之和:哈希表的应用,是面试中最常考的入门题,需掌握暴力解法与哈希表优化的对比。
- 两数相加:链表操作与进位处理,考察对链表的理解与边界情况的处理(如链表长度不同、最终进位)。
- 两两交换链表中的节点:链表指针操作,考察对链表结构的理解,是链表操作的经典题。
- 判定是否互为字符重排:哈希表的优化应用,考察对空间复杂度的优化(数组替代哈希表)。
七、结语
这4道题目是算法入门的「基石题」,覆盖了哈希表、链表、数组等核心数据结构,是面试刷题的必刷内容。本文不仅提供了可直接通过的代码,更详细拆解了思路与代码逻辑,帮助读者真正理解算法本质,而非死记硬背。
如果对你有帮助,欢迎点赞、收藏、关注,后续会持续更新更多算法题解与技术干货!