突破编程_C++_STL教程( map 的实战应用)

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 容器实现会涉及更多的方法、逻辑细节和边界情况处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值