基础算法实践一
vector
基本是对基础Vector的应用,这里复习一下: 构造vector变量:
//创建一个空的vector
vector<int> vec1;
//初始化列表(C++11)
vector<int> vec2 = {1, 2, 3, 4, 5};
vector<string> vec_str = {"hello", "world"};
//制定空间数量,并给予默认值,这里是5个空间,默认值100
vector<int> vec4(5, 100);
//复制操作
vector<int> vec5 = vec2
一些常用操作:
[]
:不进行边界检查的访问。v.at(index)
:进行边界检查的访问。v.front()
:返回对第一个元素的引用。v.back()
:返回对最后一个元素的引用。v.push_back(value)
:在末尾添加一个元素。insert(iterator pos, value)
:在指定迭代器pos
指向的位置之前插入一个元素value
。
v.insert(v.begin() + 2, 30); // 在索引为 2 的位置 (即 40 之前) 插入 30
v.pop_back()
:删除 vector 的最后一个元素。v.clear()
:删除 vector 中的所有元素,使其大小变为 0。容量通常保持不变。v.size()
:返回 vector 中元素的数量。v.empty()
:如果 vector 为空(即size() == 0
),则返回true
,否则返回false
。resize(new_size, value)
: 改变 vector 的大小为new_size
。如果new_size
大于当前大小,则添加的新元素会被初始化为value
,value
可以省略。
迭代器(index):
begin()
: 返回一个指向 vector 第一个元素的迭代器。end()
: 返回一个指向 vector 最后一个元素之后位置的迭代器 (哨兵)。rbegin()
: 返回一个指向 vector 最后一个元素的反向迭代器。rend()
:返回一个指向 vector 第一个元素之前位置的反向迭代器。cbegin()
,cend()
,crbegin()
,crend()
(C++11 及更高版本):返回常量迭代器,不允许通过它们修改元素。 [[基于范围的 for 循环(C++11)]]
#include <vector>
#include <iostream>
#include <algorithm> // for std::for_each (现在是 for_each)
// 使用 std 命名空间,这样我们就不需要在每个标准库成员前加上 std::
using namespace std;
int main() {
vector<int> v = {10, 20, 30, 40, 50};
// 1. 使用基于范围的 for 循环 (C++11 及更高版本,推荐)
cout << "Using range-based for: ";
for (int x : v) {
cout << x << " ";
}
cout << endl;
// 2. 使用迭代器
cout << "Using iterators: ";
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
// *it = (*it) * 2; // 可以通过迭代器修改元素
}
cout << endl;
// 3. 使用常量迭代器 (如果不想修改)
cout << "Using const_iterators: ";
for (vector<int>::const_iterator cit = v.cbegin(); cit != v.cend(); ++cit) {
cout << *cit << " ";
// *cit = (*cit) * 2; // 编译错误,不能通过 const_iterator 修改
}
cout << endl;
// 4. 使用反向迭代器
cout << "Using reverse_iterators: ";
for (vector<int>::reverse_iterator rit = v.rbegin(); rit != v.rend(); ++rit) {
cout << *rit << " "; // 输出: 50 40 30 20 10
}
cout << endl;
// 5. 使用 auto 关键字简化迭代器声明 (C++11 及更高版本)
cout << "Using auto with iterators: ";
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " ";
}
cout << endl;
// 6. 使用算法库 (例如 for_each)
cout << "Using for_each: "; // 注意:std::for_each 变成了 for_each
for_each(v.begin(), v.end(), [](int x){ cout << x << " "; });
cout << endl;
return 0;
}
其他: v.assign()
:给 vector 赋新内容,替换其所有现有内容,有两种赋值形式。
std::vector<int> v;
v.assign(5, 10); // v 现在是 {10, 10, 10, 10, 10}
std::vector<int> v2 = {1, 2, 3};
v.assign(v2.begin(), v2.end()); // v 现在是 {1, 2, 3}
v.swap(other_vector)
:交换两个向量的内容,因为是更改指针所以时间复杂度很低。
vector的迭代器的不稳定性
vector中,内存的分配变动会导致迭代器失效,包括以下几种方式:
导致内存重分配的操作,因为vector需要一个连续内存保存,所以一旦插入过多的数,会导致内存重选地方,之前的迭代器也就失效。当操作导致 vector
的大小 size()
超过其当前容量 capacity()
时,就会发生内存重分配。
push_back()
/emplace_back()
insert()
/emplace()
resize()
std::vector<int> vec = {1, 2, 3};
vec.reserve(3); // 此时 size() == 3, capacity() == 3
auto it = vec.begin(); // it 指向 1
std::cout << "Before push_back, a[0] is at: " << &(*it) << std::endl;
// 这个操作会导致 size() 变为 4,超过 capacity(),触发内存重分配
vec.push_back(4);
// 此时,之前的迭代器 it 已经失效了!
// 对它进行任何操作(如解引用、自增)都是未定义行为,很可能导致程序崩溃。
// std::cout << "Value from stale iterator: " << *it; // !!! CRASH !!!
auto it_new = vec.begin(); // 必须重新获取迭代器
std::cout << "After push_back, a[0] is at: " << &(*it_new) << std::endl; // 地址会改变
移动元素也会导致一些迭代器失效:
insert()
/emplace()
: 在位置p
插入元素,会导致插入点p
以及之后的所有迭代器、指针和引用全部失效。因为这些元素需要向后移动来腾出空间。erase()
: 删除位置p
的元素(或一个范围的元素),会导致被删除点p
以及之后的所有迭代器、指针和引用全部失效。因为这些元素需要向前移动来填补空缺。
如何安全地在遍历中删除 vector
元素?
这是一个非常经典的错误场景:
// !!! 错误且危险的写法 !!!
for (auto it = vec.begin(); it != vec.end(); ++it) {
if (*it % 2 == 0) { // 假设要删除所有偶数
vec.erase(it); // erase 之后, it 失效了!
} // 下一轮循环的 ++it 就是对失效的迭代器操作,导致未定义行为
}
正确的写法(C++03/传统写法): erase()
函数会返回一个指向被删除元素的下一个元素的有效迭代器。我们必须接收这个返回值来更新我们的迭代器。
for (auto it = vec.begin(); it != vec.end(); /* a blank */) {
if (*it % 2 == 0) {
it = vec.erase(it); // 用 erase 的返回值更新 it
} else {
++it; // 只有不删除时,才手动将 it 后移
}
}
现代 C++ 写法(:
#include <algorithm>
// std::remove_if 把所有要删除的元素“移动”到容器末尾,并返回一个指向第一个被移动元素的迭代器
auto new_end = std::remove_if(vec.begin(), vec.end(),
[](int n){ return n % 2 == 0; });
// 然后,一次性地调用 erase 删除所有这些元素
vec.erase(new_end, vec.end());
约瑟夫问题
在一组人(或事物)围成的圆圈中,从某个人开始按固定步长(k)计数,每数到第 k 个人就将其淘汰出局,然后从下一个人开始重新计数,如此循环,直到剩下最后一个人。你需要找出最后一个幸存者的初始位置。
输入以一个整数 T (T ≤ 200) 开始,表示测试用例的数量。 每个测试用例包含两个正整数 n (1 ≤ n ≤ 200) 和 k (1 ≤ k < 201)。对于每个测试用例,请打印案例编号和最后一个剩下的人的位置。
思路
设 J(i, k)
是当有 i
个人(0-indexed,即编号为 0, 1, ..., i-1
),每次数 k
个人时,幸存者的0-indexed编号。
- 基本情况: 当只有1个人时(
i=1
),这个人就是幸存者,其0-indexed编号为0。所以J(1, k) = 0
。 - 递推关系: 当有
i
个人时,第一个被淘汰的人的0-indexed编号是(k-1) % i
。在他被淘汰后,剩下i-1
个人。问题规模缩小了。关键在于,原来i-1
人问题的解(假设为J(i-1, k)
)是相对于淘汰第一个人之后重新编号的圈子而言的。 如果我们观察人员编号的变化,可以发现从i-1
个人的解到i
个人的解,幸存者的编号会向后移动k
位(并对i
取模)。 因此,0-indexed的递推公式为:J(i, k) = (J(i-1, k) + k) % i
我们可以从 i=2
迭代计算到 i=n
,初始时 J(1, k)
的结果(即0-indexed的幸存者位置)为0。
example: 0 1 2 3 4 5 6, i=7, k=5,进行一次迭代后 2 3 4 5 0 1, (0+5)%7=5,(2+5)%7=0
求斐波那契数
输入N,求第N个斐波那契数。(N<=100)
思路:这个题的主要难度在于斐波那契数列增长太快了,导致long long
型也存不下,所以必须得使用字符串手搓一个字符串型的加法。
#include <iostream>
#include <string>
#include <vector>
#include <algorithm> // for reverse
using namespace std;
// 函数:将两个表示数字的字符串相加 (使用 if-else 结构,更清晰)
string addStrings_easyToRead(string num1, string num2) {
string sum = "";
int i = num1.length() - 1;
int j = num2.length() - 1;
int carry = 0;
// 只要两个数字中任何一个还有位数,或者最后还有进位,循环就继续
while (i >= 0 || j >= 0 || carry) {
long long digit1 = 0; // 先假设当前位的数字是0
// 如果 num1 还有位数 (i 指针没有越界)
if (i >= 0) {
// 就从 num1 中取出当前位的数字
// '8' - '0' 的结果是整数 8
digit1 = num1[i] - '0';
i--; // 将指针向前移动一位
}
long long digit2 = 0; // 同样,先假设当前位的数字是0
// 如果 num2 还有位数 (j 指针没有越界)
if (j >= 0) {
// 就从 num2 中取出当前位的数字
digit2 = num2[j] - '0';
j--; // 将指针向前移动一位
}
// 将两个数字和上一位的进位相加
long long current_sum = digit1 + digit2 + carry;
// 计算新结果的个位数,并添加到 sum 字符串中
int digit_to_add = current_sum % 10;
sum += to_string(digit_to_add);
// 计算新的进位,用于下一次循环
carry = current_sum / 10;
}
// 因为我们是从个位开始加,结果是反的 (例如 123+456 的 sum 是 "975")
// 所以需要将字符串反转过来得到正确顺序
reverse(sum.begin(), sum.end());
// 处理一个特殊情况:如果两个输入都是 "0",那么循环不会执行,sum会是空字符串。
// 此时我们应该返回 "0"。
if (sum.empty()) {
return "0";
} else {
return sum;
}
}
int main() {
vector<string> Line; // 用字符串存储斐波那契数
Line.push_back("1");
Line.push_back("1");
int N;
cin >> N;
if (N <= 0) {
cerr << "错误:N 必须是一个正整数。" << endl;
return 1;
}
if (N == 1) {
cout << Line[0] << endl;
} else if (N == 2) {
cout << Line[1] << endl;
} else {
for (int i = 2; i < N; i++) {
string next_val = addStrings(Line[Line.size() - 1], Line[Line.size() - 2]);
Line.push_back(next_val);
}
cout << Line[N - 1] << endl;
}
return 0;
}
活动安排
设有 n 个活动的集合 $E={1,2,..,n}$,其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动 $i$ 都有一个要求使用该资源的起始时间 $s_i$ 和一个结束时间 $f_i$,且 $s_i<f_i$ 。如果选择了活动 $i$ ,则它在时间区间 $[si,fi)$ 内占用资源。若区间 $[si,fi)$ 与区间 $[sj,fj)$ 不相交,则称活动 $i$ 与活动 $j$ 是相容的。也就是说,当 $f_i≤s_j$ 或 $f_j≤s_i$ 时,活动 $i$ 与活动 $j$ 相容。选择出由互相兼容的活动组成的最大集合。
Input
4
1 3
4 6
2 5
1 7
Output
2
基本思路是贪心算法:
- 排序:将所有活动根据它们的结束时间(fᵢ),从小到大进行排序。
- 选择第一个活动:排序后,列表中的第一个活动自然是结束时间最早的。我们无条件选择它,并将其计入总数。
- 记录结束时间:记下第一个选定活动的结束时间,我们称之为
last_finish_time
。 - 遍历剩余活动:从第二个活动开始,依次检查列表中的每一个活动。
- 判断兼容性:对于当前检查的活动
i
,如果它的开始时间sᵢ
大于或等于last_finish_time
,则说明它与我们上一个选择的活动是兼容的。 - 选择新活动并更新:如果兼容,我们就选择这个活动,将总数加一,并把
last_finish_time
更新为这个新选定活动的结束时间fᵢ
。 - 重复:继续遍历,直到检查完所有活动。最终得到的总数就是互相兼容的最大活动个数。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 定义一个结构体来存储每个活动
struct Activity {
int start;
int finish;
};
// 用于 sort 函数的自定义比较函数
// 根据活动的结束时间进行升序排序
bool compareActivities(const Activity& a, const Activity& b) {
return a.finish < b.finish;
}
int main() {
// 提高 I/O 效率
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int n;
cin >> n;
// 使用 vector 存储所有活动
vector<Activity> activities(n);
for (int i = 0; i < n; ++i) {
cin >> activities[i].start >> activities[i].finish;
}
// 步骤 1: 根据结束时间对活动进行排序
sort(activities.begin(), activities.end(), compareActivities);
if (n == 0) {
cout << 0 << endl;
return 0;
}
// 步骤 2 & 3: 选择第一个活动
int count = 1;
int last_finish_time = activities[0].finish;
// 步骤 4: 遍历剩余的活动
for (int i = 1; i < n; ++i) {
// 步骤 5: 如果当前活动的开始时间晚于或等于上一个活动的结束时间
if (activities[i].start >= last_finish_time) {
// 步骤 6: 选择此活动,并更新状态
count++;
last_finish_time = activities[i].finish;
}
}
// 输出最终结果
cout << count << endl;
return 0;
}