1. 数组与Vector:C++中的动态容器实战
在C++编程中,数组是最基础的数据结构之一,但传统静态数组存在固定大小的限制。标准库提供的vector容器完美解决了这个问题,它就像个智能伸缩的收纳盒,能根据物品数量自动调整容量。我们先看传统数组声明:
cpp复制int arr[5] = {1, 2, 3, 4, 5}; // 静态数组大小固定为5
而vector的灵活之处在于:
cpp复制#include <vector>
vector<int> dynamicArr = {1, 2, 3}; // 初始3个元素
dynamicArr.push_back(4); // 现在包含[1,2,3,4]
关键区别:vector会在内存中预留额外空间(capacity),当size超过capacity时会自动扩容(通常是2倍增长),这个特性在算法题中处理未知数据量时特别有用。
1.1 vector核心操作详解
初始化方式多样化的背后是不同应用场景的考量:
cpp复制vector<int> v1(10); // 10个0(默认构造)
vector<int> v2(10, -1); // 10个-1(填充构造)
vector<int> v3 = {1,2,3}; // 初始化列表(C++11)
常用方法的时间复杂度:
- push_back/pop_back: O(1) 平均
- insert/erase: O(n)
- operator[]: O(1)
- at(): O(1) 但会做边界检查
避坑指南:在循环中频繁push_back可能导致多次扩容,影响性能。已知数据量时可先用reserve()预分配空间:
cpp复制vector<int> nums;
nums.reserve(1000); // 预分配1000个元素空间
for(int i=0; i<1000; ++i) {
nums.push_back(i); // 不会触发扩容
}
2. 算法实战:数组倒序与隔位输出
2.1 倒序输出的三种实现方式
假设需要倒序输出vector内容,以下是不同实现方式的对比:
方法一:反向迭代器
cpp复制for(auto it = vec.rbegin(); it != vec.rend(); ++it) {
cout << *it << " ";
}
方法二:下标倒序遍历
cpp复制for(int i = vec.size()-1; i >=0; --i) {
cout << vec[i] << " ";
}
方法三:STL算法
cpp复制reverse(vec.begin(), vec.end());
for(int num : vec) {
cout << num << " ";
}
性能提示:方法三会修改原数组,如果只是输出不需要修改,优先选择方法一。
2.2 隔位输出技巧
隔位输出(如奇数位元素)的几种写法:
cpp复制// 方案1:步长为2
for(int i=0; i<vec.size(); i+=2) {
cout << vec[i] << " ";
}
// 方案2:判断索引奇偶
for(int i=0; i<vec.size(); ++i) {
if(i%2 == 0) cout << vec[i] << " ";
}
// 方案3:使用位运算(效率最高)
for(int i=0; i<vec.size(); ++i) {
if(!(i&1)) cout << vec[i] << " ";
}
实测发现,在1000万次循环中,位运算版本比取模快约15%。但在现代编译器优化下,简单场景差异不大。
3. 字符串处理全解析
3.1 string与char[]的抉择
C++中字符串有两种主要表示形式:
cpp复制char str1[] = "C-style"; // 字符数组
string str2 = "C++ style"; // STL字符串
关键区别对比表:
| 特性 | char数组 | string |
|---|---|---|
| 内存管理 | 手动分配 | 自动管理 |
| 长度获取 | strlen() | .size() |
| 拼接操作 | strcat() | + 或 append() |
| 安全性 | 易缓冲区溢出 | 边界检查 |
经验法则:除非有特殊性能要求或兼容C接口,否则优先使用string。
3.2 输入输出的那些坑
常见输入问题及解决方案:
问题场景:混合使用cin和getline时出现跳过输入
cpp复制int age;
string name;
cin >> age; // 输入后回车符留在缓冲区
getline(cin, name); // 直接读取到空行
解决方案:
cpp复制cin >> age;
cin.ignore(); // 清除缓冲区中的回车符
getline(cin, name);
读取整行文本的最佳实践:
cpp复制string line;
while(getline(cin, line)) {
// 处理每一行
if(line.empty()) break; // 空行退出
}
4. 经典算法题精讲
4.1 摆平积木问题
题目要求:将不同高度的积木堆调整为相同高度,求最小移动次数。
算法步骤:
- 计算所有堆的总积木数total
- 求平均高度avg = total / 堆数
- 累加所有高于avg的堆的超额部分
cpp复制int minMoves(vector<int>& blocks) {
int sum = accumulate(blocks.begin(), blocks.end(), 0);
if(sum % blocks.size() != 0) return -1; // 无法平分
int avg = sum / blocks.size();
int moves = 0;
for(int h : blocks) {
if(h > avg) moves += h - avg;
}
return moves;
}
易错点:没有检查总积木数是否能被堆数整除,导致出现小数块的情况。
4.2 打印空心正方形
两种实现方式的对比:
传统分段式写法:
cpp复制void printSquare(int n) {
// 上边
for(int i=0; i<n; ++i) cout << "*";
cout << endl;
// 中间
for(int i=1; i<n-1; ++i) {
cout << "*";
for(int j=1; j<n-1; ++j) cout << " ";
cout << "*" << endl;
}
// 下边
if(n > 1) { // n=1的特殊情况处理
for(int i=0; i<n; ++i) cout << "*";
}
}
统一判断式写法(更简洁):
cpp复制void printSquare(int n) {
for(int i=0; i<n; ++i) {
for(int j=0; j<n; ++j) {
if(i==0 || i==n-1 || j==0 || j==n-1)
cout << "*";
else
cout << " ";
}
cout << endl;
}
}
5. 浮点数处理与类型转换
5.1 平均绩点计算要点
计算浮点平均数时的关键细节:
cpp复制vector<float> grades = {3.5, 4.0, 3.7};
float sum = 0.0f; // 必须用浮点累加
for(float g : grades) {
sum += g;
}
float gpa = sum / grades.size(); // 自动浮点除法
常见错误对比:
cpp复制int sum = 0; // 错误:整数累加
sum / grades.size(); // 整数除法截断
float sum = 0; // 正确但不够明确
sum / grades.size(); // 至少一方是float即可
最佳实践:在涉及浮点运算的表达式中,至少保证一个操作数是浮点类型(如3.0f),或者使用static_cast
()进行显式转换。
5.2 字符串与数值转换
C++11引入的便捷转换方法:
cpp复制#include <string>
// 字符串转数值
float f = stof("3.14");
int i = stoi("42");
// 数值转字符串
string s1 = to_string(3.14159);
string s2 = to_string(42);
传统C风格方法(仍有用武之地):
cpp复制char buffer[50];
sprintf(buffer, "%.2f", 3.14159); // 精确到两位小数
string s(buffer);
在处理算法题输入时,经常需要组合使用这些技巧。比如从包含字母和数字的字符串中提取数值:
cpp复制string input = "Grade:3.5";
size_t pos = input.find(':');
if(pos != string::npos) {
float grade = stof(input.substr(pos+1));
// 使用grade...
}
我在实际项目中发现,理解这些基础数据结构的底层原理和特性,能帮助我们在面对复杂问题时快速选择最优解决方案。比如vector的扩容机制解释了为什么有时候reserve()能显著提升性能,而string的内部实现则说明了为什么某些拼接操作效率低下。掌握这些细节,才是从"会用"到"用好"的关键跃迁。