1 std::map 应用于自定义数据结构
std::map 在 C++ 中可以方便地应用于自定义数据结构,使得开发者能够将自定义类型的对象作为键或值进行存储和检索。当使用自定义类型作为键时,需要为该类型定义比较函数对象(通常是一个仿函数或重载 operator<),以便 std::map 能够确定键之间的顺序。
以下是一个示例,展示了如何使用 std::map 来存储自定义数据结构的键值对:
首先,定义一个自定义的数据结构(例如,一个简单的 Person 类):
#include <iostream>
#include <map>
#include <string>
// 自定义数据结构:Person
class Person {
public:
std::string name;
int age;
Person(const std::string& name, int age) : name(name), age(age) {}
// 重载 < 运算符,用于在 std::map 中排序
bool operator<(const Person& other) const {
if (name == other.name) {
return age < other.age;
}
return name < other.name;
}
};
int main()
{
// 使用自定义数据结构 Person 作为键的 std::map
std::map<Person, std::string> peopleInfo;
// 向 map 中添加元素
peopleInfo[Person("Alice", 25)] = "Software Engineer";
peopleInfo[Person("Bob", 30)] = "Data Scientist";
peopleInfo[Person("Alice", 25)] = "UI Designer"; // 注意:同名的 Alice 会覆盖 Alice,因为 age 相同且 name 也相同
// 遍历 map 并输出信息
for (const auto& kv : peopleInfo) {
std::cout << "Name: " << kv.first.name << ", Age: " << kv.first.age
<< ", Occupation: " << kv.second << std::endl;
}
return 0;
}
上面代码的输出为:
Name: Alice, Age: 25, Occupation: UI Designer
Name: Bob, Age: 30, Occupation: Data Scientist
在这个例子中,定义了一个 Person 类,它包含 name 和 age 两个成员变量。为了让 Person 对象能够用作 std::map 的键,重载了 Person 类型的 < 运算符来定义比较逻辑。在这个比较逻辑中,首先比较 name,如果 name 相同,则比较 age。
然后,创建了一个 std::map,其键类型为 Person,值类型为 std::string(表示职业信息)。接下来向 map 中添加了几个 Person 对象作为键,并关联了相应的职业信息。
最后,遍历 map 并输出每个人的姓名、年龄和职业信息。
注意:当使用自定义类型作为 std::map 的键时,务必确保比较逻辑是严格弱序的,即满足以下条件:
- 不可反身性:对于任何非空集合中的任意元素 a,a < a 为假。
- 对称性:对于任何非空集合中的任意元素 a 和 b,如果 a < b 为真,那么 b < a 为假。
- 传递性:对于任何非空集合中的任意元素 a、b 和 c,如果 a < b 为真且 b < c 为真,那么 a < c 也为真。
- 可比较性:对于任何非空集合中的任意元素 a 和 b,a < b、a == b 或 b < a 之中至少有一个为真。
如果比较逻辑不满足这些条件,std::map 的行为将是未定义的。
2 std::map 的主要应用场景
std::map是C++ STL库中的一个关联容器,它提供了一种将键映射到值的方式,其中键和值可以是任何类型。std::map的主要应用场景包括但不限于以下几个方面:
(1)字典或哈希表功能: std::map非常适合实现类似于字典或哈希表的功能。它允许用户存储键值对,并通过键快速查找和访问对应的值。由于std::map中的元素是按照键的顺序进行排序的,因此在需要有序访问键值对的情况下,它也是一个很好的选择。
(2)计数器: std::map可以用作计数器,通过键来记录每个元素出现的次数。例如,可以创建一个std::map<std::string, int>,其中键是字符串,值是该字符串在文本中出现的次数。
(3)索引: std::map还可以用作索引,将某种类型的键映射到另一种类型的值。这在许多数据结构和算法中都非常有用,特别是当需要快速根据键查找值时。
(4)映射任意类型: std::map的一个强大之处在于,它可以将任何基本类型(包括 STL 容器)映射到任何基本类型(包括 STL 容器)。这使得 std::map 在需要建立复杂数据关系或映射的场景中非常有用。
(5)判断数据是否存在: 在某些情况下,可以将 std::map 当作布尔型数组使用,来判断大整数或其他类型数据是否存在。由于 std::map 中的每个键只能出现一次,因此可以通过检查键是否存在来判断相应的数据是否存在。
总的来说,std::map 是一个功能强大的容器,其应用场景非常广泛,可以用于各种需要将键映射到值的场景。需要注意的是,std::map 的底层实现基于红黑树,因此在平均情况下,查找、插入和删除操作的时间复杂度为 O(log n)。这使得它在处理大规模数据时可能不是最优选择,但在许多常见应用中,其性能表现通常是足够好的。
2.1 std::map 应用于字典或哈希表功能
std::map 在 C++ 中可以被用作一种字典或映射结构,它按照键的顺序存储元素,并通过键快速查找和访问对应的值。虽然 std::map 的实现并不是基于哈希表,而是基于平衡搜索树(通常是红黑树),但它提供了类似字典的键值对存储和查找功能。
以下是一个简单的示例,展示如何使用 std::map 来实现字典或映射功能:
#include <iostream>
#include <map>
#include <string>
int main()
{
// 创建一个 std::map,键是 std::string 类型,值是 int 类型
std::map<std::string, int> wordCount;
// 向字典中添加元素(键值对)
wordCount["apple"] = 3;
wordCount["banana"] = 2;
wordCount["cherry"] = 5;
// 遍历字典并输出键值对
for (const auto& kv : wordCount) {
std::cout << kv.first << ": " << kv.second << std::endl;
}
// 通过键查找值
std::string keyToFind = "banana";
if (wordCount.find(keyToFind) != wordCount.end()) {
std::cout << "Found " << keyToFind << ": " << wordCount[keyToFind] << std::endl;
}
else {
std::cout << "Not found: " << keyToFind << std::endl;
}
// 修改现有键的值
wordCount["apple"] = 4;
// 再次输出修改后的 apple 的值
std::cout << "Updated apple count: " << wordCount["apple"] << std::endl;
return 0;
}
上面代码的输出为:
apple: 3
banana: 2
cherry: 5
Found banana: 2
Updated apple count: 4
在这个示例中,创建了一个 std::map<std::string, int> 来存储单词及其出现的次数。然后使用 [] 运算符向 map 中添加键值对,并通过迭代器遍历 map 来输出所有的键值对。另外还展示了如何使用 find 成员函数来查找一个键是否存在,并如何修改现有键的值。
需要注意的是,虽然 std::map 提供了类似于字典的功能,但它并不是基于哈希表的实现。如果需要基于哈希表的实现,C++11 及以后的版本提供了 std::unordered_map,它使用哈希表来存储键值对,从而在平均情况下提供更快的查找速度。然而,std::unordered_map 中的元素不是有序的,而 std::map 中的元素是按照键的顺序进行排序的。
2.2 std::map 应用于计数器
当使用 std::map 作为计数器时,通常将需要计数的项作为键(key),并将计数值作为值(value)。每当遇到一个新的项时,我们在 std::map 中增加或更新对应的计数。以下是一个使用 std::map 实现计数器的示例:
#include <iostream>
#include <map>
#include <string>
int main()
{
// 创建一个 std::map,键是 std::string 类型,值是 int 类型,用于计数
std::map<std::string, int> wordCount;
// 假设有一个字符串数组,需要统计每个字符串出现的次数
std::string words[] = { "apple", "banana", "apple", "cherry", "banana", "banana" };
int numWords = sizeof(words) / sizeof(words[0]);
// 遍历字符串数组,并使用 map 进行计数
for (int i = 0; i < numWords; ++i) {
// 如果单词已经在 map 中,增加其计数
if (wordCount.find(words[i]) != wordCount.end()) {
wordCount[words[i]]++;
}
else {
// 如果单词不在 map 中,将其添加到 map 并初始化为 1
wordCount[words[i]] = 1;
}
}
// 输出每个单词及其出现的次数
for (const auto& kv : wordCount) {
std::cout << kv.first << ": " << kv.second << std::endl;
}
return 0;
}
上面代码的输出为:
apple: 2
banana: 3
cherry: 1
在这个示例中,定义了一个 std::map<std::string, int> 来存储每个单词及其出现的次数。然后遍历一个包含多个单词的数组,并使用 std::map 来记录每个单词的出现次数。如果单词已经在 map 中,则增加其对应的计数值;如果单词不在 map 中,则将其添加到 map 中并将计数值初始化为 1。
最后,遍历 std::map 并输出每个单词及其对应的计数。由于 std::map 是按键的顺序存储的,输出的结果也将是有序的。
2.3 std::map 应用于索引
std::map 在 C++ 中常用于作为索引结构,特别是在需要按照键的顺序快速检索和存储键值对的情况下。以下是一个简单的示例,展示了如何使用 std::map 来创建一个索引,其中键是唯一的标识符(例如整数或字符串),值是与这些键相关联的数据。
假设有一个学生列表,每个学生都有一个唯一的 ID,需要创建一个索引来按 ID 快速查找学生的信息。
#include <iostream>
#include <map>
#include <string>
// 学生信息结构
struct Student {
std::string name;
int age;
std::string major;
};
int main()
{
// 创建一个 std::map,键是 int 类型(学生 ID),值是 Student 类型的对象
std::map<int, Student> studentIndex;
// 添加一些学生到索引中
studentIndex[1] = { "Alice", 20, "Computer Science" };
studentIndex[2] = { "Bob", 22, "Mathematics" };
studentIndex[3] = { "Charlie", 21, "Physics" };
// 假设有一个学生 ID,需要查找该学生的信息
int idToFind = 2; // 假设需要查找 ID 为 2 的学生
// 在 map 中查找学生 ID
auto it = studentIndex.find(idToFind);
if (it != studentIndex.end()) {
// 如果找到了学生,输出其信息
const Student& student = it->second;
std::cout << "Found student with ID: " << idToFind << std::endl;
std::cout << "Name: " << student.name << ", Age: " << student.age << ", Major: " << student.major << std::endl;
}
else {
// 如果没有找到学生,输出一条消息
std::cout << "No student found with ID: " << idToFind << std::endl;
}
// 遍历整个索引并输出所有学生的信息
for (const auto& kv : studentIndex) {
const Student& student = kv.second;
std::cout << "ID: " << kv.first << ", Name: " << student.name << ", Age: " << student.age << ", Major: " << student.major << std::endl;
}
return 0;
}
上面代码的输出为:
Found student with ID: 2
Name: Bob, Age: 22, Major: Mathematics
ID: 1, Name: Alice, Age: 20, Major: Computer Science
ID: 2, Name: Bob, Age: 22, Major: Mathematics
ID: 3, Name: Charlie, Age: 21, Major: Physics
在这个示例中,首先定义了一个 Student 结构体来存储学生的信息,并创建了一个 std::map<int, Student> 来作为索引。然后使用学生的 ID 作为键,并将每个学生的 Student 对象作为值存储在 map 中。
通过 std::map 的 find 方法,可以根据给定的 ID 快速查找学生的信息。如果找到了对应的键,find 方法返回一个迭代器,指向包含该键的键值对;如果没找到,它返回 end() 迭代器。
最后,遍历了整个 map,输出了所有学生的信息,展示了 map 中元素的顺序性。
这个示例展示了 std::map 如何作为一个高效的索引结构,用于存储和检索按键排序的数据。由于 std::map 内部实现了红黑树,因此其插入、删除和查找操作的时间复杂度都是对数级别的,这在很多情况下都是非常高效的。
3 实现一个简单的 std::map 容器
实现一个完整的 std::map 容器是一个相当复杂的任务,因为需要考虑到许多细节,比如红黑树的实现、迭代器、异常安全性等。但是,为了展示基本的思路,可以简化这个过程,使用红黑树作为底层数据结构,实现元素的插入与打印。
注意红黑树具有以下五个关键性质:
(1)每个节点要么是红色,要么是黑色。
(2)根节点是黑色。
(3)所有叶子节点(NIL或空节点)是黑色。
(4)如果一个节点是红色的,则它的两个子节点都是黑色。
(5)对于每个节点,从该节点到其所有后代叶子节点的简单路径上,均包含相同数目的黑色节点。
这些性质确保了红黑树在插入、删除节点后仍然能够保持相对平衡,从而保证了树的查找、插入和删除操作的时间复杂度为O(log n)。
如下是使用红黑树作为底层数据结构的 std::map 容器实现:
#include <iostream>
enum Color { RED, BLACK };
template <typename K,typename V>
struct Node {
K key;
V value;
Color color;
Node* left;
Node* right;
Node* parent;
Node(K key_, V value_, Color c = RED, Node* p = nullptr, Node* l = nullptr, Node* r = nullptr)
: key(key_), value(value_), color(c), parent(p), left(l), right(r) {}
};
template <typename K, typename V>
class SimpleMap
{
public:
SimpleMap() : root(nullptr) {}
// 插入元素
void insert(const K& key, const V& value) {
root = insert(root, key, value);
}
// 打印元素
void print() {
inorderTraversal(root);
std::cout << std::endl;
}
// 析构函数,用于释放内存
~SimpleMap() {
// 递归删除所有节点
deleteTree(root);
}
private:
// 递归删除树
void deleteTree(Node<K, V>* node) {
if (node != nullptr) {
deleteTree(node->left);
deleteTree(node->right);
delete node;
}
}
// 辅助函数:检查节点是否是红色或黑色
bool isRed(Node<K, V>* node) const {
if (node == nullptr) return false;
return node->color == RED;
}
bool isBlack(Node<K, V>* node) const {
if (node == nullptr) return true;
return node->color == BLACK;
}
// 左旋
void leftRotate(Node<K, V>*& x) {
Node<K, V>* y = x->right;
x->right = y->left;
if (y->left) y->left->parent = x;
y->parent = x->parent;
if (!x->parent) root = y;
else if (x == x->parent->left) x->parent->left = y;
else x->parent->right = y;
y->left = x;
x->parent = y;
}
// 右旋
void rightRotate(Node<K, V>*& x) {
Node<K, V>* y = x->left;
x->left = y->right;
if (y->right) y->right->parent = x;
y->parent = x->parent;
if (!x->parent) root = y;
else if (x == x->parent->right) x->parent->right = y;
else x->parent->left = y;
y->right = x;
x->parent = y;
}
// 插入修复
void fixInsert(Node<K, V>*& k) {
while (k != root && isRed(k->parent)) {
if (isRed(k->parent->parent->left) == (k == k->parent->right)) {
Node<K, V>* u = k->parent->parent->left;
if (u) u->color = RED;
k->parent->color = BLACK;
k->parent->parent->color = RED;
k = k->parent->parent;
}
else {
if (k == k->parent->left) {
k = k->parent;
rightRotate(k);
}
k->parent->color = BLACK;
k->parent->parent->color = RED;
leftRotate(k->parent->parent);
}
}
root->color = BLACK;
}
// 插入节点
Node<K, V>* insert(Node<K, V>*& node, const K& key, const V& value) {
if (node == nullptr) {
return new Node<K, V>(key, value);
}
if (key < node->key) {
node->left = insert(node->left, key, value);
node->left->parent = node;
}
else if (key > node->key) {
node->right = insert(node->right, key, value);
node->right->parent = node;
}
else {
// Duplicate values not allowed
return node;
}
// 修复红黑树性质
if (isRed(node->right) && isRed(node->left)) {
node->color = RED;
node->left->color = BLACK;
node->right->color = BLACK;
}
else if (isRed(node->parent) && isRed(node->parent->parent) &&
(node == node->parent->right ||
(node->parent == node->parent->parent->left &&node == node->parent->left))) {
node->parent->color = BLACK;
node->parent->parent->color = RED;
if (node == node->parent->left) {
rightRotate(node->parent->parent);
}
else {
leftRotate(node->parent->parent);
}
}
if (node->parent == nullptr) {
root = node;
}
fixInsert(node);
return node;
}
// 中序遍历打印,用于验证
void inorderTraversal(Node<K, V>* node) {
if (node != nullptr) {
inorderTraversal(node->left);
std::cout << node->key << ":" << node->value << std::endl;
inorderTraversal(node->right);
}
}
private:
Node<K, V>* root;
};
int main()
{
SimpleMap<int, int> valMap;
valMap.insert(5, 2);
valMap.insert(3, 3);
valMap.insert(8, 2);
valMap.insert(1, 2);
valMap.insert(4, 6);
valMap.insert(7, 9);
valMap.insert(6, 1);
valMap.insert(9, 12);
valMap.print(); // 打印集合
return 0;
}
上面代码的输出为:
1 3 4 5 6 7 8 9
注意,上面的代码只是一个框架,并且重点放在了红黑树的实现上,并不是 std::map 容器的全部实现。实际的 std::map 容器实现会涉及更多的方法、逻辑细节和边界情况处理。