1. Two Sum - leecode题库
思路
使用哈希表(哈希表可以在O(1)的时间内找到是否存在一个数),我们遍历当前下标元素x
的时候,只需要判断这个元素之前是否存在等于target - x
。
从前向后扫描,每扫描一个数就将他放到哈希表里面,当扫描到s[i]
的时候,就看前面是否存在一个数字等于target - s[i]
,每个数字进行插入和查询哈希表一次,时间复杂度为O(n)
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int>heap;
for(int i = 0; i < nums.size(); i ++){
int r = target - nums[i];
if(heap.count(r)){ // count方法返回r出现的次数,已经找到两个数的和是target
return {heap[r], i};
}else{ // 将当前数字插入到哈希表中
heap[nums[i]] = i;
}
}
return {}; // 避免警告,最好加上,虽然不会执行
}
};
2.两数相加
思路
从个位开始计算,就是从链表的第一个数(最右边),计算第一个链表的数值+第二位连表内的数值+进位,三个数字的和%10 得到当前位置的和,用一个变量记录进位
虚拟头结点的使用:
虚拟头结点(dummy node)在链表操作中常用于简化边界条件的处理。以下是它的主要作用和好处:
简化逻辑:在处理链表时,尤其是在插入或删除节点时,常常需要处理头节点的特殊情况。使用虚拟头结点,可以避免这些特殊情况,因为你总是可以在虚拟头结点之后插入新节点。
避免空指针异常:当你开始构建结果链表时,虚拟头结点的存在可以让你在未创建第一个有效节点之前就能安全地进行指针操作,避免空指针的错误。
易于返回结果:因为虚拟头结点的下一个节点就是结果链表的头节点,所以在函数返回时,只需返回 dummy->next,而无需再检查链表是否为空或处理头节点的特殊情况。
简单来说,虚拟头结点的使用使得链表操作更加简洁和安全。你觉得这样解释清楚了吗?
/**
* 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) {
//凡是需要特判第一个点的,则创建一个虚拟头结点
auto dummy = new ListNode(-1);
auto cur = dummy;// 表示尾结点
int t = 0;// 表示进位
while(l1 || l2 || t){
if(l1){ // 第一个数的链表没有循环完则执行,注意这里存储是逆序的,所以越靠前的数字对应的位权越低
t += l1->val;
l1 = l1->next;
}
if(l2){ // 第二个数的链表没有循环完
t += l2 -> val;
l2 = l2 -> next;
}
cur -> next = new ListNode(t % 10);
cur = cur->next;
t /= 10;
}
return dummy->next;// 返回实际的链表
}
};
3.无重复字符的最长子串
思路
对于从第一个数字开始,总共又n-1个字串,对于从第二个数开始,总共又n-2个字串…依此类推,所以总共的情况大约为
(
n
2
)
/
2
(n^2)/2
(n2)/2种,那么需要从这总共的情况中,找到最大的那个。思路就转换成:如何枚举所有情况,并取最大值。
这里采用双指针算法,i
,j
两个指针,每当i向后移动一格子i'
,j必然要么为当前位置,要么向后移动成为j'
(可以用反证法证明),这样就只需要从左往右走一遍。使用Hash表维护当前序列元素出现的次数,当i向后移动一格,如果不满足元素不重复,那必然是s[i + 1]
大于1,此时让j
向后移动,直到s[i + 1] = 1
class Solution {
public:
int lengthOfLongestSubstring(string s) {
unordered_map<char, int> heap;
int res = 0;// 记录最大值
for(int i = 0, j = 0; i < s.size(); i ++){
heap[s[i]] ++;
while(heap[s[i]] > 1){
heap[s[j ++]] --;//j向后移动,当前位置哈希表删除
}
res = max(res, i - j + 1);
}
return res;
}
};
补充C++知识点
unordered_map
在 C++ 中,unordered_map
是标准模板库(STL)中的一个关联容器,它基于哈希表实现。unordered_map
允许你存储键值对(key-value pairs),并且可以非常快速地进行查找、插入和删除操作。
以下是一些关于 unordered_map
的基本用法:
-
包含头文件:
要使用unordered_map
,你需要包含头文件<unordered_map>
。#include <unordered_map>
-
声明:
声明一个unordered_map
时,你需要指定键(key)和值(value)的类型。std::unordered_map<KeyType, ValueType> myMap;
-
插入元素:
使用insert
方法或方括号[]
操作符来插入元素。// 使用 insert 方法 myMap.insert(std::make_pair("key1", "value1")); // 使用方括号操作符 myMap["key2"] = "value2";
-
访问元素:
使用方括号[]
操作符来访问元素。std::string value = myMap["key1"];
-
查找元素:
使用find
方法来查找元素。auto it = myMap.find("key1"); if (it != myMap.end()) { std::cout << "Found: " << it->second << std::endl; } else { std::cout << "Not found" << std::endl; }
-
删除元素:
使用erase
方法来删除元素。// 删除键为 "key1" 的元素 myMap.erase("key1"); // 删除迭代器指向的元素 myMap.erase(it);
-
遍历:
使用迭代器遍历unordered_map
。for (auto it = myMap.begin(); it != myMap.end(); ++it) { std::cout << it->first << " : " << it->second << std::endl; }
-
大小:
使用size
方法来获取unordered_map
中元素的数量。std::size_t size = myMap.size();
-
清空:
使用clear
方法来清空unordered_map
。myMap.clear();
unordered_map
的性能通常比 map
更好,因为 unordered_map
使用哈希表,而 map
使用红黑树。但是,unordered_map
的元素顺序是不确定的,而 map
则按照键的顺序存储元素。
请注意,unordered_map
的键必须是可哈希的,并且必须能够与等号操作符一起使用以比较键值。
c++ auto介绍
auto 关键字在 C++ 中用于自动推断变量的类型。编译器根据初始化表达式的类型来确定变量的类型。使用 auto 的优点包括:
- 简化代码:减少类型声明的冗长性,特别是对于复杂类型(如迭代器、lambda 表达式等)。
- 类型安全:依然保持类型安全,编译器会检查推断的类型是否符合使用要求。
- 方便维护:如果类型发生变化,使用 auto 的地方不需要修改,减少了维护成本。
举一个例子:
auto x = 5; // x 的类型为 int
auto y = 3.14; // y 的类型为 double
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin(); // it 的类型为 std::vector<int>::iterator