在算法中,动态数组是一种常见的数据结构,而C++的STL中的Vector容器提供了方便的动态数组功能,用来代替我们之前在c语言中使用malloc()函数申请的动态数组。本文将介绍Vector容器的定义和常用操作,并通过一个实例来说明其使用方法和注意事项。
Vector是STL提供的动态数组容器,能够根据需要在运行时改变数组大小。它以数组形式存储元素,并具有连续的内存空间,因此可以在常数时间内完成索引操作。下面是Vector的几种定义示例:
默认情况下直接声明vector类型的变量,该变量为空
vector<int> a;
需要使得构造函数为被复制的vector类型的对象
vector<int> b(a);
当构造函数的参数只有一个的时候,数量为该参数,vector的变量内的值的默认值为0
vector<int> a(100);
通过向构造函数传入两个参数,届时,第一个参数表示vector类型变量存储的数量,第二个参数表示该vector类型变量内的值的默认值
vector<int> a(100, 6);
通过修改vector的模板类型string,相当于定义string型数组,且每个元素的值都为构造函数中的第二个参数
vector<string> a(10, "null");
规则同上
vector<string> vec(10, "hello");
通过向构造函数中传入vector类型的对象的begin()函数以及end()函数,使得a中的内容传入b
vector<string> b(a.begin(), a.end());
vector作为一个模板类,可以存储任何其他类的对象,记得需要修改模板类的类型为对应类型
例如,可以申请vector
类型用于存储二叉树,便于解决很多问题
struct point{
int x, y;
};
vector<point> a;
除了一维数组,还可以定义多维数组,例如二维数组用于实现图的邻接表存储。
区别主要体现在两个方面,创建的容器类型以及复制方式不同,这里主要说的是复制方式不同:
vector b(a.begin(), a.end());
创建的 b
是一个 vector
类型的容器,而 vector b(a);
创建的 b
是一个 vector
类型的容器。因此,它们可以存储不同类型的元素。vector b(a.begin(), a.end());
使用迭代器范围初始化 b
,从 a
的起始迭代器 begin()
到结束迭代器 end()
,复制了 a
中的所有元素到 b
中。而 vector b(a);
通过拷贝构造函数直接将容器 a
中的元素复制到 b
中。vector
类型的用法vector
可以作为容器来存储多个二叉树对象。每个 BinaryTree
对象代表一个独立的二叉树结构,通过 vector
可以方便地管理和操作这些二叉树。vector
可以轻松进行对多个二叉树的遍历和搜索操作。可以使用循环结构迭代访问每个二叉树,执行先序、中序、后序或层序遍历等操作。同时,也可以在 vector
中进行搜索,查找符合条件的二叉树。vector
可以方便地对多个二叉树进行批量处理和扩展。例如,可以批量插入、删除、修改二叉树中的节点。同时,可以利用 vector
的动态增长特性,方便地动态扩展存储的二叉树数量。vector
可以方便地管理和组织二叉树的集合。可以进行排序、过滤、分组等操作,基于不同的需求将二叉树进行分类和组织。vector
提供了一种方便灵活的数据结构,用于存储和处理多个二叉树,使得对二叉树进行批量操作和管理变得更加方便和高效。Vector提供了一系列常用的操作函数,下面是一些常用操作示例:
a.push_back(100);
在容器尾部添加元素100int size = a.size();
获取容器内元素的个数并储存到size中bool isEmpty = a.empty();
判断Vector是否为空,注意返回值为true则表示空cout << a[0] << endl;
使用普通数组的方式,打印第一个元素a.insert(a.begin() + i, k);
在第i个元素前插入ka.pop_back();
删除末尾元素a.erase(a.begin() + i, a.begin() + j);
删除区间[i, j-1]的元素⭐(注意是j-1
)a.erase(a.begin() + 2);
删除第3个元素,从第0号开始a.resize(n);
调整数组大小为na.clear();
清空Vectorreverse(a.begin(), a.end());
翻转数组sort(a.begin(), a.end());
对数组进行排序(从小到大)end()
函数删除vector中的最后一个元素时,需要注意以下几点:vector
不为空。可以使用 empty()
函数检查 vector
是否为空,以避免在空向量上调用删除操作导致的错误。end()
函数删除最后一个元素时,确保迭代器仍然有效。在进行删除操作之前,可以使用 begin()
向导取得迭代器,并确保迭代器不等于 end()
。erase()
函数将最后一个元素从 vector
中删除。要删除最后一个元素,只需要传递指向最后一个元素的迭代器作为参数,而不能直接传递end()
函数,即应是 erase(end()-1)
。这将从 vector
中移除最后一个元素,同时减小 vector
的大小。⭐(注意是end()-1
)end()
删除 vector
中的最后一个元素之前,要确保 vector
不为空,迭代器有效且不等于 end()
,并使用 erase()
函数进行删除操作。同时要注意处理迭代器的失效问题。end()
函数,用于返回指向容器中最后一个元素之后的位置的迭代器,需要注意:功能:end()
函数用于返回指向容器中最后一个元素之后的位置的迭代器,一般是指向容器尾部的下一个位置。
适用范围:end()
函数适用于几乎所有 C++ 标准库中的容器,如 vector
、list
、deque
、array
、string
等。在这些容器中,end()
函数返回的迭代器表示容器的末尾的下一个位置。
注意事项:
end()
返回的迭代器指向容器中的虚拟位置,是一个不可解引用(dereference)的迭代器。因此,不能在访问该迭代器指向的元素,否则会导致未定义的行为。⭐(注意不要进行解引用操作!!)end()
函数返回的迭代器是一个尾后(past-the-end)迭代器,标志着容器的结束位置。在使用迭代器遍历容器时,通常是以 begin()
迭代器作为起始位置,end()
迭代器作为结束位置。⭐(迭代器结束的位置并不指向任何元素!!)例子:
vector<int> numbers = {1, 2, 3, 4, 5};
auto iter = numbers.begin(); // 迭代器指向容器开始位置
while (iter != numbers.end()) {
cout << *iter << " "; // 输出元素
++iter;
}
cout << endl;
在上述例子中,numbers.end()
表示 numbers 容器中的最后一个元素之后的位置,用于循环遍历输出容器中的所有元素。注意,end()
函数返回的迭代器不参与实际遍历。
总的来说,end()
函数是用于返回容器中最后一个元素之后位置的迭代器。在使用时,需注意该迭代器不能解引用,通常用于循环遍历容器中的元素或作为结束位置标志。
在该例中使用了auto
,这里的类型实际为vector
,使用 auto
关键字声明的迭代器 iter
的类型会被自动推导出来,根据迭代器的初始化值确定。在这里,numbers.begin()
返回的是 vector
类型的迭代器,因此推导结果会是 auto
的类型与 vector
类型相同。在这个情况下,iter
的类型被推导为 vector
,它是一个指向 vector
中元素的迭代器类型。根据所使用的容器类型不同,迭代器的类型也会有所不同。这个我们之后再说。
vector
时,需要特别小心不要在遍历过程中进行元素的插入或删除操作。如果必须需要进行这些操作,可以使用插入迭代器(std::insert_iterator
)或删除/替换迭代器(std::erase_iterator
)来处理。vector
支持使用下标运算符([]
)来访问元素。但应注意,使用下标访问时需要确保索引的有效范围,避免越界访问。vector
的存储空间不足以容纳新的元素时,vector
会自动进行内存重新分配,以扩大存储空间。然而,这个过程可能涉及元素的复制或移动,导致迭代器、引用和指针失效。因此,在进行大量插入或删除操作时,可能会产生较大的性能开销。为了避免频繁的内存重新分配,可以使用 reserve()
函数在插入元素之前预留一定的存储空间。vector
中删除元素时,该元素可能被销毁,如果有其他指向该元素的指针,就会引起悬垂指针问题(dangling pointers)。要避免悬垂指针,可以在删除元素之前更新指向该元素的指针或使用智能指针等管理资源的方式。vector
是标准库中最常用的容器之一,配合使用算法和迭代器可以方便地进行元素的遍历、查找、排序等操作。了解标准库提供的各种算法和迭代器有助于提高效率和简化代码。std::insert_iterator
)和删除/替换迭代器(std::erase_iterator
):他们是 C++ 标准库中提供的迭代器适配器,用于在容器中进行插入、删除或替换操作时保持迭代器的有效性。
std::insert_iterator
):
std::insert_iterator
是一个迭代器适配器,用于在容器中指定位置之前插入元素。需要注意的是std::insert_iterator
是唯一可用于关联式容器的插入迭代器。
使用 std::insert_iterator
需要包含
头文件。
可以通过 std::inserter()
函数创建 std::insert_iterator
对象。该函数接受一个容器和一个迭代器,并返回一个用于插入的迭代器。
例如:
#include
#include
std::vector<int> numbers {1, 2, 3};
std::vector<int> more_numbers {4, 5, 6};
std::copy(more_numbers.begin(), more_numbers.end(),
std::inserter(numbers, numbers.begin())
);
在这个例子中,std::inserter(numbers, numbers.begin())
返回一个使用 numbers.begin()
作为插入位置的 std::insert_iterator
对象。std::copy()
算法将 more_numbers
容器中的元素插入到 numbers
容器中。
这里不可以提供普通的迭代器,std::copy
函数的目标是将 more_numbers
容器中的元素复制到 numbers
容器中。尝试把 std::inserter
替换为普通的迭代器,将输出的元素插入到 numbers
容器中,会发生编译错误。
std::inserter
创建的是插入迭代器,它会在目标容器的给定位置之前插入元素。然而,给 std::copy
函数提供普通的迭代器时,编译器会尝试将输出元素复制到迭代器指向的位置,而不会自动处理插入操作。⭐(注意是之前的位置)
还可以直接使用 std::back_inserter
创建一个插入迭代器,将输出的元素插入到 numbers
容器的末尾,如下所示:
#include
#include
std::vector<int> numbers {1, 2, 3};
std::vector<int> more_numbers {4, 5, 6};
std::copy(more_numbers.begin(), more_numbers.end(),
std::back_inserter(numbers)
);
当然还可以使用std::front_inserter
创建一个插入迭代器,将输出的元素插入到 numbers
容器的前端,注意最后插入的元素会在容器的最前方。
std::copy
函数的用法如下:
template<class InputIterator, class OutputIterator>
OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result);
参数说明:
first
和 last
是输入范围的起始和结束迭代器,它们指定了要复制的元素范围。⭐(注意是输入范围)result
是输出范围的起始迭代器,它指定了复制的元素将被放置的位置。使用 std::copy
函数时,它会将输入范围 [first, last)
中的元素复制到输出范围,该范围从迭代器 result
开始。注意,输出范围必须有足够的空间来接收复制的元素。⭐(不包括last
迭代器,因此可以使用end()
函数)
定义std::insert_iterator
类型迭代器的语法格式如下:
std::insert_iterator<int> insert_it (container,it);
参数 container 表示目标容器,而 it 是一个普通的迭代器,表示新元素的插入位置。C++ STL标准库中还提供了inserter()
函数,可以快速创建 insert_iterator 类型迭代器。
例如:
#include
#include
#include
int main() {
std::vector<int> foo(2, 5);
std::vector<int>::iterator it = ++foo.begin();
// 创建一个 insert_iterator 迭代器
std::insert_iterator<std::vector<int>> insert_it = std::inserter(foo, it);
insert_it = 1;
insert_it = 2;
insert_it = 3;
insert_it = 4;
for (std::vector<int>::iterator it = foo.begin(); it != foo.end(); ++it) {
std::cout << *it << ' ';
}
return 0;
}
程序执行结果为:5 1 2 3 4 5
直接对插入迭代器赋值,也可以实现插入的操作。
需要注意的是,如果 insert_iterator
迭代器的目标容器是关联式容器,由于该类型容器会自动对存储的元素进行排序,因此我们指定的插入位置仅起到一个提示的作用。这个提示会帮助关联式容器从指定位置开始搜索正确的插入位置。然而,如果我们给出的提示位置不正确,可能会导致插入操作的效率变得更差。
C++ STL 官方手册中有std::insert_iterator
类型的迭代器底层实现的代码,这里不再展开。
std::erase_iterator
):
std::erase_iterator
是一个迭代器适配器,用于在容器中删除或替换指定位置的元素。std::erase_iterator
需要包含
头文件。std::remove_if()
算法结合 std::erase_iterator
来删除容器中特定的元素。#include
#include
#include
std::vector<int> numbers {1, 2, 3, 4, 5, 6};
auto iter = std::remove_if(numbers.begin(), numbers.end(),
[](int n){
return n % 2 == 0;
}
);
numbers.erase(iter, numbers.end());
在这个例子中,std::remove_if()
算法结合了一个 lambda 表达式,用于删除偶数元素。std::remove_if()
算法会返回一个指向修改后的范围末尾之后位置的迭代器,通过将这个迭代器和容器的 end()
迭代器传递给 numbers.erase()
函数,可以删除指定范围内的元素。插入迭代器和删除/替换迭代器在进行插入、删除和替换操作时,能够保持迭代器的有效性。这意味着在使用这些迭代器适配器进行操作后,可以继续使用原始的迭代器进行后续操作,而无需担心迭代器失效的问题。有关插入迭代器和删除/替换迭代器的内容较为复杂,应多加练习来掌握。
reserve()
函数用于预留一定的存储空间,以避免频繁的内存重新分配:函数签名如下:
void reserve(size_type new_capacity);
注意事项:
new_capacity
是一个无符号整数类型,表示所需的新容器容量。reserve()
函数通常在向 vector
容器插入大量元素之前调用,以避免在插入操作时可能发生的多次内存重新分配。通过提前分配足够的存储空间,可以改善性能并减少内存重新分配的次数。reserve()
函数并不会影响 vector
的大小或元素个数,仅仅是预留内存空间。new_capacity
小于当前容器的 capacity()
,则 reserve()
不会进行任何操作。因此,建议在需要增加容器容量时才调用 reserve()
。new_capacity
大于当前容器的 capacity()
,则 reserve()
会重新分配足够的内存空间,使容器的容量达到至少 new_capacity
大小。reserve()
后,之前获取的迭代器、引用和指针可能无法继续使用。下面是使用 reserve()
函数的一个例子:
#include
int main() {
std::vector<int> numbers;
numbers.reserve(100); // 预留至少可以容纳 100 个元素的存储空间
// 插入大量元素到 vector 中
for (int i = 0; i < 100; ++i) {
numbers.push_back(i);
}
return 0;
}
在这个例子中,使用 reserve(100)
事先预留了至少可以容纳 100 个元素的存储空间。这样,在插入100个元素时,就可以避免因为频繁的内存重新分配而产生额外的开销。
使用 reserve()
函数可以根据预期的容器大小提前分配合适的存储空间,从而提高性能和效率,并且减少内存重新分配的次数。
reserve()
函数和 resize()
的区别:reserve()
函数用于预留一定的存储空间,但并不会改变容器的大小或元素个数。它只影响容器的容量,确保容器能够容纳指定大小的元素。resize()
函数用于改变容器的大小,可以增加或减少容器的元素个数。它会根据需要自动插入或删除元素。reserve()
函数接受一个无符号整数类型的参数 new_capacity
,表示所需的新容器容量。resize()
函数接受一个无符号整数类型的参数 new_size
,表示所需的容器大小。reserve()
函数只影响容器的容量,不会改变容器的大小。它通常在插入大量元素之前进行调用,以避免频繁的内存重新分配。resize()
函数会根据指定的大小改变容器的元素个数。如果 new_size
大于当前容器的大小,则会插入新元素,如果 new_size
小于当前容器的大小,则会删除多余的元素。reserve()
函数主要是为了提前分配足够的存储空间,防止频繁的内存重新分配。resize()
函数在需要增大容器大小时,会分配额外的内存空间;在需要减小容器大小时,会释放多余的内存空间。所以,reserve()
和 resize()
在功能和使用方式上有明显的区别。reserve()
主要用于预留存储空间,而 resize()
主要用于改变容器的大小,并可以同时插入或删除元素。根据具体的需求,选择合适的函数来操作 vector
容器。
题目来源:hdu 4841 圆桌问题 Hangzhou Dianzi University ACM Team
题目:
Problem Description
圆桌上围坐着2n个人。其中n个人是好人,另外n个人是坏人。如果从第一个人开始数数,数到第m个人,则立即处死该人;然后从被处死的人之后开始数数,再将数到的第m个人处死……依此方法不断处死围坐在圆桌上的人。试问预先应如何安排这些好人与坏人的座位,能使得在处死n个人之后,圆桌上围坐的剩余的n个人全是好人。
Input
多组数据,每组数据输入:好人和坏人的人数n(<=32767)、步长m(<=32767);
Output
对于每一组数据,输出2n个大写字母,‘G’表示好人,‘B’表示坏人,50个字母为一行,不允许出现空白字符。相邻数据间留有一空行。
Sample Input
2 3
2 4
Sample Output
GBBG
BGGB
解题思路:
vector<int> table;
int n, m;
cin >> n >> m;
在这段代码中,使用了流提取运算符 >>
来从标准输入流 cin
中读取数据。而 cin
对象在读取失败时会返回一个具有特殊含义的值。在这种情况下,当输入流无法正确读取到 n 和 m 时,即无法将它们转换为适当的类型(整数),cin
会返回一个 false 的值,此时循环条件将为 false,从而结束循环。vector<int> table;
table.clear();
for (int i = 0; i < 2 * n; i++)
table.push_back(i);
这里使用table.clear()
是确保多次循环,每次都清空Vector,使得容器不含任何元素。int pos = 0;
for (int i = 0; i < n; i++) {
pos = (pos + m - 1) % table.size();
table.erase(table.begin() + pos);
}
这段代码使用了一个 for
循环来进行坏人赶走的操作。for (int i = 0; i < n; i++)
:初始化一个循环变量 i
,并设置循环的起始条件为 i = 0
,循环继续的条件为 i < n
,每次循环完成后对 i
进行递增操作,因此一共会赶走n个人。
(pos + m -1)
使得pos始终表示元素的下标。注意题目所说为(从第一个人开始数数,数到第m个人,而不是间隔m人)。pos = (pos + m - 1) % table.size();
:将当前位置 pos
更新为 (pos + m - 1) % table.size()
。这一行代码用于确定下一个将要赶走的坏人的位置。根据题目要求,每次赶走一个人后,下一个要赶走的人是当前位置加上步数 m
再减去1,并对 table.size()
取余,以实现环形移动。table.erase(table.begin() + pos);
:从 table
容器中删除指定位置的元素。在这里,table.begin() + pos
表示一个指向要删除的元素的迭代器,table.erase()
函数用于删除该位置上的元素。这一行代码实现了将坏人赶走的操作,即从 table
容器中删除指定位置的元素。要注意的是,table.erase()
操作会直接改变容器的大小,并删除指定位置或范围内的元素。因此会直接改变序列,使得容器内存储的元素不再和其下标相等。后续我们可以通过比较下标以及元素来判断是否进行过删除操作,即可以判断是好人还是坏人。int j = 0;
for (int i = 0; i < 2 * n; i++) {
if (!(i % 50) && i)
cout << endl;
if (j < table.size() && i == table[j]) {
j++;
cout << "G";
} else {
cout << "B";
}
}
这段代码用于输出预先安排好的座位序列,其中 “G” 表示好人,“B” 表示坏人:
for (int i = 0; i < 2 * n; i++)
:初始化一个循环变量 i
,并设置循环的起始条件为 i = 0
,循环继续的条件为 i < 2 * n
,每次循环完成后对 i
进行递增操作。if (!(i % 50) && i)
:该条件判断用于判断是否需要在输出的行末添加换行符。当 i
是 50 的倍数(即 i % 50
的结果为 0)且 i
不等于 0 时,执行该条件判断为真,将输出一个换行符。这是为了让每行输出的字符数量限制在 50 个以内。这么操作是因为题目进行了输出格式的要求,即50个字母为一行。if (j < table.size() && i == table[j])
:该条件判断用于确定当前位置是否是好人的位置。j
是记录 table
容器中的人的索引,table[j]
表示当前位置上的人的编号。(在前面的操作中,我们已经使用循环让每一项的值都成为该项的编号了)如果 j < table.size()
(即 j
小于 table
容器的大小)且 i
等于 table[j]
,表示当前位置是好人的位置,执行该条件判断为真。 也就是之前我们删除操作中没有删除的人。j++
⭐(循环内的参数很重要)j++
时,如果当前位置是好人的位置,在输出 “G” 后,将 j
递增,指向下一个好人的位置。cout << "G"
时,输出字符 “G”,表示好人。cout << "B"
时,输出字符 “B”,表示坏人。j++
操作,循环继续进行,i++
,继续判断容器中下标为 j
的元素是否与 i
相等。table.erase()
操作,移除了元素,导致下标与元素的值不一致。元素的值代表之前的坐标 j
。j
在从0开始遍历时找不到对应的下标时,我们就可以判断出这个位置已经被删除,因此这里存放的应该是坏人。 因为我们操作之后容器内存放的全部为好人。 因此,我们在操作的时候只需要继续循环就可以,不用对标记的下标j进行操作。总代码:
#include
using namespace std;
int main() {
vector<int> table;
int n, m;
while (cin >> n >> m) {
table.clear();
for (int i = 0; i < 2 * n; i++)
table.push_back(i);
int pos = 0;
for (int i = 0; i < n; i++) {
pos = (pos + m - 1) % table.size();
table.erase(table.begin() + pos);
}
int j = 0;
for (int i = 0; i < 2 * n; i++) {
if (!(i % 50) && i)
cout << endl;
if (j < table.size() && i == table[j]) {
j++;
cout << "G";
} else {
cout << "B";
}
}
cout << endl << endl;
}
return 0;
}
这就是有关C++ STL库中vector的一些基础内容。
C++ STL插入迭代器适配器(insert_iterator) C语言中文网
算法竞赛入门到进阶 罗勇军
Problem - 4841 Hangzhou Dianzi University Online Judge 3.0