7.28学习日志
题目:
1. 两数之和 - 力扣(LeetCode)
1768. 交替合并字符串 - 力扣(LeetCode)
1769. 移动所有球到每个盒子所需的最小操作数 - 力扣(LeetCode)
49. 字母异位词分组 - 力扣(LeetCode)
for (int pos : ballPositions)
:
这句代码 for (int pos : ballPositions)
使用的是 C++11 引入的范围 for 循环(range-based for loop),专门用于遍历容器(如 vector
、数组等)中的所有元素,语法简洁直观。
它的作用等价于传统的 for 循环:
for (int j = 0; j < ballPositions.size(); j++) {int pos = ballPositions[j];// 循环体内容
}
各部分含义:
int pos
:声明一个变量pos
,用于接收容器中每个元素的值(这里是小球的位置索引,类型为int
)。ballPositions
:要遍历的容器(这里是存储小球位置的vector
)。- 循环过程:自动从容器的第一个元素开始,依次将每个元素的值赋给
pos
,直到遍历完所有元素。
优势:
- 代码更简洁,不需要手动控制索引(如
j
的初始值、终止条件、自增),减少了出错的可能。 - 可读性更强,一眼就能看出是要遍历
ballPositions
中的所有元素。
在这段代码中,这个循环的目的是:依次取出每个小球的位置 pos
,计算它到当前目标位置 i
的距离(abs(i - pos)
),并累加到总操作数中。
vector<int> answer(n, 0):
vector<int> answer(n, 0);
这行代码的作用是创建一个初始化为特定值的向量容器,具体含义如下:
vector<int>
:声明这是一个存储int
类型元素的向量(动态数组)。answer
:向量的名称,后续用于存储计算结果。(n, 0)
:这是向量的初始化参数:- 第一个参数
n
表示向量的初始大小(即包含n
个元素),这里的n
是之前通过boxes.size()
获取的字符串长度。 - 第二个参数
0
表示向量中所有元素的初始值,即每个位置都会被默认赋值为0
。
- 第一个参数
举例说明:
如果 n = 3
,那么这行代码会创建一个包含 3 个元素的向量 answer
,初始状态为 [0, 0, 0]
。
作用:
提前为结果分配好空间并初始化,避免后续动态扩容的性能开销,同时确保每个位置都有一个初始值(0),后续只需根据计算结果更新对应位置的值即可。
在这段代码中,answer
向量最终会存储每个盒子位置对应的最小操作数,因此初始大小与字符串 boxes
的长度相同(每个位置对应一个结果)。
Vector 的用法
Vector 是 C++ 标准模板库(STL)中的一个动态数组容器,它提供了动态大小调整、快速随机访问和高效的尾部插入/删除操作。以下是 vector 的主要用法:
基本操作
1. 包含头文件和声明
#include <vector>
using namespace std;vector<int> v1; // 空vector
vector<int> v2(10); // 包含10个元素,默认值为0
vector<int> v3(10, 5); // 包含10个元素,每个初始化为5
vector<int> v4 = {1, 2, 3}; // 初始化列表
vector<int> v5(v4); // 拷贝构造
2. 添加元素
v1.push_back(10); // 在末尾添加元素10
v1.emplace_back(20); // C++11, 更高效的添加方式
v1.insert(v1.begin(), 5); // 在开头插入5
3. 访问元素
int a = v1[0]; // 通过下标访问,不检查边界
int b = v1.at(1); // 通过at访问,会检查边界
int c = v1.front(); // 第一个元素
int d = v1.back(); // 最后一个元素
4. 删除元素
v1.pop_back(); // 删除最后一个元素
v1.erase(v1.begin()); // 删除第一个元素
v1.erase(v1.begin(), v1.begin()+2); // 删除前两个元素
v1.clear(); // 清空vector
5. 容量和大小
int size = v1.size(); // 当前元素数量
bool empty = v1.empty(); // 是否为空
int cap = v1.capacity(); // 当前分配的存储容量
v1.reserve(100); // 预分配空间
v1.shrink_to_fit(); // 减少容量以匹配大小(C++11)
常用算法
#include <algorithm>sort(v1.begin(), v1.end()); // 排序
reverse(v1.begin(), v1.end()); // 反转
auto it = find(v1.begin(), v1.end(), 5); // 查找元素5
int cnt = count(v1.begin(), v1.end(), 5); // 计数
遍历vector
// 1. 使用下标
for(size_t i = 0; i < v1.size(); ++i) {cout << v1[i] << " ";
}// 2. 使用迭代器
for(auto it = v1.begin(); it != v1.end(); ++it) {cout << *it << " ";
}// 3. 范围for循环(C++11)
for(int num : v1) {cout << num << " ";
}
多维vector
vector<vector<int>> matrix(3, vector<int>(4)); // 3行4列矩阵
matrix[0][1] = 5; // 访问元素
性能特点
-
随机访问:O(1)
-
尾部插入/删除:O(1) 平摊
-
中间/头部插入/删除:O(n)
-
当容量不足时,重新分配内存通常是原大小的2倍(实现相关)
Vector 是 C++ 中最常用的容器之一,适用于需要频繁随机访问但主要在尾部添加/删除元素的场景。
Vector 的拷贝构造详解
vector<int> v5(v4);
这行代码使用了 vector 的拷贝构造函数,它创建了一个新的 vector v5
,其内容是 v4
的完整副本。
拷贝构造的详细说明
-
基本概念:
-
拷贝构造函数会创建一个新 vector,其元素是原 vector 的精确拷贝
-
新 vector 的大小和容量与原 vector 相同
-
元素会逐个拷贝(对于基本类型如 int 是值拷贝,对于对象会调用拷贝构造函数)
-
-
内存分配:
-
新 vector 会分配自己的内存空间
-
不会与原 vector 共享内存
-
修改其中一个不会影响另一个
-
-
示例代码:
#include <iostream>
#include <vector>
using namespace std;int main() {vector<int> v4 = {1, 2, 3, 4, 5};// 使用拷贝构造创建v5vector<int> v5(v4);// 输出两个vector的内容cout << "v4: ";for(int num : v4) cout << num << " ";cout << "\nv5: ";for(int num : v5) cout << num << " ";// 修改v5不会影响v4v5[0] = 100;cout << "\nAfter modification:\n";cout << "v4: ";for(int num : v4) cout << num << " ";cout << "\nv5: ";for(int num : v5) cout << num << " ";return 0;
}
输出结果:
v4: 1 2 3 4 5
v5: 1 2 3 4 5
After modification:
v4: 1 2 3 4 5
v5: 100 2 3 4 5
其他拷贝方式
-
赋值运算符:
vector<int> v6; v6 = v4; // 使用赋值运算符拷贝
-
范围构造:
vector<int> v7(v4.begin(), v4.end()); // 使用迭代器范围拷贝
-
C++11 移动构造(非拷贝,转移所有权):
vector<int> v8(std::move(v4)); // v4的内容被"移动"到v8,v4变为空
性能考虑
-
拷贝构造的时间复杂度是 O(n),因为需要拷贝所有元素
-
对于大型 vector,拷贝可能很昂贵
-
如果不需要保留原 vector,考虑使用移动语义(C++11)
拷贝构造是创建 vector 副本的最直接方式,适用于需要独立修改副本而不影响原 vector 的场景。
Vector 元素访问方式比较:operator[] 和 at()
在 C++ 的 vector 中,有两种主要方式访问元素:使用 operator[]
和使用 at()
成员函数。它们有相似的功能但有重要区别。
1. 使用 operator[] 访问
int a = v1[0]; // 通过下标访问,不检查边界
特点:
-
不进行边界检查:如果索引越界,行为是未定义的(UB)
-
性能更高:因为没有额外的边界检查开销
-
适用于:已知索引安全的情况,或对性能要求极高的代码
示例:
vector<int> v = {10, 20, 30};
cout << v[1]; // 输出20 - 安全访问
cout << v[5]; // 未定义行为 - 可能崩溃或输出垃圾值
2. 使用 at() 访问
int b = v1.at(1); // 通过at访问,会检查边界
特点:
-
进行边界检查:如果索引越界,抛出
std::out_of_range
异常 -
安全性更高:适合不确定索引是否有效的情况
-
性能稍低:因为有额外的边界检查开销
-
适用于:用户输入或不确定索引是否有效的情况
示例:
vector<int> v = {10, 20, 30};
try {cout << v.at(1); // 输出20 - 安全访问cout << v.at(5); // 抛出std::out_of_range异常
} catch (const out_of_range& e) {cerr << "错误: " << e.what() << endl;
}
对比表格
特性 | operator[] | at() |
---|---|---|
边界检查 | 不检查 | 检查 |
越界行为 | 未定义行为 | 抛出异常 |
性能 | 更高 | 稍低 |
安全性 | 较低 | 较高 |
适用场景 | 已知索引安全的情况 | 不确定索引是否有效 |
最佳实践建议
-
确定索引安全时使用
operator[]
(如循环中的迭代变量)for(size_t i = 0; i < v.size(); ++i) {cout << v[i] << " "; // 安全使用operator[] }
-
不确定索引安全时使用
at()
(如处理用户输入)size_t index; cin >> index; try {cout << "值: " << v.at(index); } catch (const out_of_range&) {cout << "索引超出范围!"; }
-
C++17 后可考虑使用
std::optional
或if
检查结合operator[]
if(index < v.size()) {cout << v[index]; } else {cout << "索引无效"; }
记住:在调试阶段,使用 at()
可以帮助快速发现越界访问的问题,而在发布版本中,对已知安全的访问可以使用 operator[]
来提高性能。
C++ 中 map
实现:
-
std::map
(基于红黑树,有序) -
std::unordered_map
(基于哈希表,无序)
1. std::map
(有序映射)
-
底层结构:红黑树(自平衡二叉搜索树)
-
特点:
-
键(
key
)按升序自动排序(默认<
比较,可自定义排序规则) -
查找、插入、删除的时间复杂度:O(log n)
-
适用于需要有序遍历的情况
-
基本用法
#include <iostream>
#include <map>int main() {std::map<std::string, int> ageMap;// 插入数据ageMap["Alice"] = 25;ageMap["Bob"] = 30;ageMap.insert({"Charlie", 35});// 访问数据std::cout << "Bob's age: " << ageMap["Bob"] << std::endl;// 遍历(按 key 升序)for (const auto& pair : ageMap) {std::cout << pair.first << ": " << pair.second << std::endl;}// 查找if (ageMap.find("Alice") != ageMap.end()) {std::cout << "Alice found!" << std::endl;}// 删除ageMap.erase("Bob");return 0;
}
输出:
Bob's age: 30
Alice: 25
Bob: 30
Charlie: 35
Alice found!
2. std::unordered_map
(无序映射)
-
底层结构:哈希表(Hash Table)
-
特点:
-
键无序(但访问更快)
-
平均时间复杂度:O(1)(最坏情况 O(n))
-
适用于需要快速查找但不需要排序的情况
-
基本用法
#include <iostream>
#include <unordered_map>int main() {std::unordered_map<std::string, int> ageMap;// 插入数据ageMap["Alice"] = 25;ageMap["Bob"] = 30;ageMap.insert({"Charlie", 35});// 访问数据std::cout << "Bob's age: " << ageMap["Bob"] << std::endl;// 遍历(顺序不确定)for (const auto& pair : ageMap) {std::cout << pair.first << ": " << pair.second << std::endl;}// 查找if (ageMap.find("Alice") != ageMap.end()) {std::cout << "Alice found!" << std::endl;}// 删除ageMap.erase("Bob");return 0;
}
输出(顺序可能不同):
Bob's age: 30
Charlie: 35
Alice: 25
Bob: 30
Alice found!
map
vs unordered_map
特性 | std::map | std::unordered_map |
---|---|---|
底层结构 | 红黑树(有序) | 哈希表(无序) |
查找时间 | O(log n) | 平均 O(1),最坏 O(n) |
插入/删除 | O(log n) | 平均 O(1),最坏 O(n) |
内存占用 | 较低(树结构) | 较高(哈希表可能有冲突) |
是否有序 | 是(按 key 排序) | 否(顺序不确定) |
适用场景 | 需要有序遍历 | 需要快速查找,不关心顺序 |
常见操作
1. 插入数据
std::map<std::string, int> m;
m["key1"] = 10; // 方式1
m.insert({"key2", 20}); // 方式2
m.emplace("key3", 30); // 方式3(更高效)
2. 访问数据
int val1 = m["key1"]; // 如果 key 不存在,会插入默认值(如 0)
int val2 = m.at("key2"); // 如果 key 不存在,抛出异常
3. 查找数据
auto it = m.find("key1");//m.find("key1")指在map/unordered_map 中查找键 "key1"
if (it != m.end()) {//说明键存在std::cout << "Found: " << it->second << std::endl;
}
//it->first 是键(key)
//it->second 是值(value)
4. 删除数据
m.erase("key1"); // 删除指定 key
m.erase(m.begin()); // 删除迭代器指向的元素
m.clear(); // 清空整个 map
5. 遍历
for (const auto& pair : m) {std::cout << pair.first << ": " << pair.second << std::endl;
}
6.降序
// 在 std::map 的模板参数中指定了比较函数 std::greater<std::string>,按 key 降序排序
std::map<std::string, int, std::greater<std::string>> ageMap = {{"Bob", 30},{"Alice", 25},{"Charlie", 35}
};
for (const auto& pair : ageMap) {std::cout << pair.first << ": " << pair.second << std::endl;
}//std::less<Key> 会让树保持 左小右大(升序)。
//std::greater<Key> 会让树保持 左大右小(降序)。
总结
-
std::map
:有序,适用于需要排序的场景(如按 key 遍历)。 -
std::unordered_map
:更快,适用于高频查找但不需要排序的场景。 -
选择依据:
-
需要排序?→
std::map
-
需要最快查找?→
std::unordered_map
-
题解
两数之和
class Solution {
public:string mergeAlternately(string word1, string word2) {string result;int i = 0;while(i<word1.size()&&i<word2.size()){result += word1[i];result += word2[i];i++;}while(i<word1.size()){result += word1[i];i++;}while(i<word2.size()){result += word2[i];i++;}return result;}
};
移动所有球到每个盒子所需的最小操作数
class Solution {
public:vector<int> minOperations(string boxes) {int n=boxes.size();vector<int> ballPositions;//记录所有小球1的位置for(int i = 0;i<n;++i){if(boxes[i]=='1'){//如果字符为1 将索引i存入ballPositionsballPositions.push_back(i);}} vector<int> answer(n,0);//对于每个盒子位置i,计算所有小修移动到i的操作数之和for(int i = 0;i<n;++i){int operations = 0;//操作数for(int pos : ballPositions){//范围for循环,遍历ballPositions中的每个位置operations += abs(i-pos);//累加当前小球到位置i的距离(操作数)}answer[i] = operations;} return answer;}
};
字母异位词分组
class Solution {
public:vector<vector<string>> groupAnagrams(vector<string>& strs) {unordered_map<string,vector<string>> map;//创建了一个无序哈希表map,键是字符串类型,值是字符串向量类型。这个哈希表用于存储 "排序后的字符串" 与 "原始字符串列表" 的映射关系。键(key)是排序后的字符串(用于标识字母异位词组),值(value)是 vector<string> 类型,存储了该组所有的原始字母异位词for(string& s: strs){//遍历输入的字符串向量strs中的每个字符串sstring key = s;//对每个字符串s,先创建一个副本keysort(key.begin(),key.end());//对key进行排序(按字符顺序),这样所有字母异位词排序后会得到相同的keymap[key].push_back(s);//将原始字符串s添加到哈希表中对应key的向量里} vector<vector<string>> result;//建结果向量result,用于存储最终的分组结果(返回的是二维的)for(auto& pair: map){ //循环遍历哈希表中的每一对键值对(pair):result.push_back(pair.second);//我们将每个分组的字符串向量添加到结果向量中}return result;}
};
交替合并字符串
class Solution {
public:string mergeAlternately(string word1, string word2) {string result;int i = 0;while(i<word1.size()&&i<word2.size()){result += word1[i];result += word2[i];i++;}while(i<word1.size()){result += word1[i];i++;}while(i<word2.size()){result += word2[i];i++;}return result;}
};