map/set自动排序的陷阱

复数键与STL map
探讨使用复数作为STL map键时遇到的问题及解决方法。解释了编译失败的原因在于复数类未在标准命名空间内正确重载比较运算符,并给出了正确的实现方式。

这里我想用一系列的代码来阐述这个问题.


1:

请看如下代码:

#include <complex>
#include <map>
using namespace std;

int main()
{
    complex<int> a(1, 2);
    map<complex<int>, int> k;         //默认按小于排序
    //k[a] = 5;
    return 0;
}




以上代码运行正常。


2:

继续看如下代码:

#include <complex>
#include <map>
using namespace std;

int main()
{
    complex<int> a(1, 2);
    map<complex<int>, int> k;   //在这里,complex<int>叫做key的类型,int叫做value的类型
    k[a] = 5;                   //加入了元素
    return 0;
}

以上代码出错了。

原因是: 当执行 k[a] = 5 时, 即向 k 加入了一个新的 pair<complex<int>, int> 型的元素,而map是会自动对元素进行排序,排序的方式是按key的大小来排序。但complex<int>却没有定义对复数比较的函数,于是map就不知如何比较complex<int>,最终导致编译错误。

 

3:

自以为对的扩展complex类:

#include <complex>
#include <map>
using namespace std;

bool operator< (complex<int> l, complex<int> r)    //为complex<int>重载小于运算符,因为map会自动排序
{
    if (l.real() < r.real()) return true;
        else return false;
}

int main()
{
    complex<int> a(1, 2), b(3, 4);
    map<complex<int>, int> k;         
    k[a] = 5;
    return 0;
}





以上编译错误。按理只要重载小于运算符(因为map默认按小于排序),就应该可以的啦,但还是编译不过。错误信息和2的一样,还是由于没找到比较complex<int>的函数

.

3-1:

真的没找到?

#include <complex>
#include <map>
using namespace std;

bool operator< (complex<int> l, complex<int> r)    //为complex<int>重载小于运算符,因为map会自动排序
{
    return (l.real() < r.real());
}

int main()
{
    complex<int> a(1, 2), b(3, 4);
    map<complex<int>, int> k;         
//  k[a] = 5;
    a < b;   //事实证明,确实重载了complex<int>的小于比较
    return 0;
}


以上代码运行正常。

以上代码编译通过。至少证明complex的小于运算符是重载成功的。但map又怎么会找不到呢?



3-2:

侧面验证:

#include <map>
using std::map;

template <class T>
class complex
{
public:
    complex(T real){this->treal = real;}
    T real(){return treal;}
private:
	T treal;
};


bool operator< (complex<int> l, complex<int> r)    //为MyClass<int>重载小于运算符,因为map会自动排序
{
    return (l.real() < r.real());
}


int main()
{
    map<complex<int>, int> k;         //默认按小于排序
    complex<int> a(1);
    k[a] = 5;
    return 0;
}


以上编译通过。
我自编了一个极其简陋complex类(其实只是名字一样罢了),其余没变,map似乎又找到了complex<int>的小于运算符了。怪哉!


4:

思考:

重载的complex<int>的小于运算符没有错。那么,请大家比较一下3和3-2,3用的是stl里的complex,3-2用的是我编的 “complex”,两者有什么根本的不同?
答:内容不同咯!
但两者都有complex<int>的小于运算符,但在3中map又为何找不到呢?
所有的证据都指向了-------namespace std


5:
验证:

#include <complex>
#include <map>
using namespace std;

namespace std
{
        bool operator < (complex<int> l, complex<int> r)
        {
                return l.real() < r.real();
        }
}


int main()
{
        map<complex<int> , int> k;
        complex<int> a(1,2);
        k[a] = 5;
        return 0;
}

以上编译成功了~



6:
map/set 的 “陷阱”:
以上所有代码我虽然只用了map来测试,事实上set也一样的。
所谓陷阱,其实也只不过是一些不容易发现或者平时没留意的一些细节:
map的完整参数是这样的——map< class Key,   class T,   class Compare = less<Key>,   class Allocator = allocator< pair<const Key, T> > >
当class  Key的域是在std中, class Compare就会自动到std中去找key的小于函数。
若class Key 的域不在std中, class Compare就会不会到std中去找key的小于函数。
即map/set是根据Key的域来寻找Key的比较函数。



7:

以上推理仅个人意见,如有不当,恳请指出。


































`std::map` 和 `std::set` 是 C++ STL 中两个非常重要的**关联容器(Associative Containers)**,它们基于**有序二叉搜索树**(通常是红黑树)实现,能够自动维护元素的排序,并提供高效的查找、插入和删除操作(时间复杂度为 O(log n))。掌握它们对于编写高效、清晰的数据处理代码至关重要。 --- ## 一、`std::set`:集合容器 ### 1. 基本概念 - 存储**唯一键值**(不允许重复)。 - 自动按升序排序(默认使用 `less<Key>`)。 - 常用于去重、快速查找是否存在某个元素。 ### 2. 基本用法示例 ```cpp #include <iostream> #include <set> int main() { std::set<int> s; // 插入元素 s.insert(5); s.insert(1); s.insert(3); s.insert(5); // 重复元素,插入失败,不会报错 // 遍历(自动有序) for (const auto& val : s) { std::cout << val << " "; // 输出: 1 3 5 } std::cout << std::endl; // 查找 if (s.find(3) != s.end()) { std::cout << "Found 3\n"; } // 删除 s.erase(1); return 0; } ``` ### 3. 其他常用操作 | 操作 | 说明 | |------|------| | `s.find(x)` | 返回指向 x 的迭代器,若未找到则返回 `s.end()` | | `s.count(x)` | 返回 0 或 1(因为 set 不允许重复) | | `s.lower_bound(x)` | 第一个 ≥x 的元素 | | `s.upper_bound(x)` | 第一个 >x 的元素 | | `s.size()` / `s.empty()` | 大小和是否为空 | --- ## 二、`std::map`:键值对映射容器 ### 1. 基本概念 - 存储 `key -> value` 映射关系。 - 键(key)唯一且自动排序。 - 常用于字典、计数、缓存等场景。 ### 2. 基本用法示例 ```cpp #include <iostream> #include <map> int main() { std::map<std::string, int> ageMap; // 插入方式1:insert ageMap.insert({"Alice", 25}); ageMap.insert(std::make_pair("Bob", 30)); // 插入方式2:下标访问(推荐但注意副作用) ageMap["Charlie"] = 35; // 修改已存在键 ageMap["Alice"] = 26; // 遍历 for (const auto& pair : ageMap) { std::cout << pair.first << ": " << pair.second << std::endl; } // 查找 auto it = ageMap.find("Bob"); if (it != ageMap.end()) { std::cout << it->first << "'s age is " << it->second << std::endl; } return 0; } ``` ### 3. 注意事项:`operator[]` 的陷阱 - 使用 `map[key]` 如果 key 不存在,会**自动创建该 key 并用默认构造函数初始化 value**。 - 这可能导致意外插入或性能问题。建议在只读查找时使用 `find()`。 ```cpp if (ageMap.find("David") == ageMap.end()) { std::cout << "David not found\n"; } // 而不是: // if (ageMap["David"] == 0) { ... } // 错误!会插入 David ``` --- ## 三、自定义排序规则 默认按键升序排列,可通过自定义比较函数改变顺序。 ### 示例:降序 map ```cpp #include <map> #include <string> struct greater_cmp { bool operator()(const std::string& a, const std::string& b) const { return a > b; // 降序 } }; std::map<std::string, int, greater_cmp> descendingMap; descendingMap["apple"] = 1; descendingMap["banana"] = 2; // 遍历时将从 z 到 a 排列 ``` --- ## 四、`multiset` 和 `multimap` 当需要允许**重复键**时使用: | 容器 | 特点 | |------|------| | `std::multiset` | 允许重复元素的有序集合 | | `std::multimap` | 允许重复 key 的键值对容器 | ```cpp std::multiset<int> ms; ms.insert(1); ms.insert(1); // OK std::multimap<int, std::string> mm; mm.insert({1, "apple"}); mm.insert({1, "orange"}); // OK ``` --- ## 五、实际应用场景举例 ### 1. 统计词频(map + insert or []) ```cpp #include <map> #include <string> #include <sstream> std::map<std::string, int> wordCount; std::string text = "hello world hello cpp world"; std::istringstream iss(text); std::string word; while (iss >> word) { wordCount[word]++; } for (const auto& p : wordCount) { std::cout << p.first << ": " << p.second << "\n"; } ``` ### 2. 判断是否有重复元素(set) ```cpp bool hasDuplicate(const std::vector<int>& nums) { std::set<int> seen; for (int x : nums) { if (seen.find(x) != seen.end()) return true; seen.insert(x); } return false; } ``` ### 3. 找出两个数组交集(set) ```cpp #include <set> #include <vector> #include <algorithm> std::vector<int> intersection(const std::vector<int>& a, const std::vector<int>& b) { std::set<int> setA(a.begin(), a.end()); std::set<int> result; for (int x : b) { if (setA.count(x)) { result.insert(x); } } return std::vector<int>(result.begin(), result.end()); } ``` --- ## 六、性能与选择建议 | 容器 | 查找 | 插入/删除 | 是否有序 | 是否允许重复 | |------|------|------------|-----------|----------------| | `std::set` | O(log n) | O(log n) | ✅ | ❌ | | `std::multiset` | O(log n) | O(log n) | ✅ | ✅ | | `std::map` | O(log n) | O(log n) | ✅ | ❌(key) | | `std::multimap` | O(log n) | O(log n) | ✅ | ✅(key) | | `std::unordered_set/map` | O(1) 平均 | O(1) 平均 | ❌ | 取决于类型 | > 提示:如果不需要排序,优先考虑 `unordered_set` / `unordered_map`(哈希表),速度更快。 --- ## 七、常见错误与最佳实践 1. **避免滥用 `operator[]` 导致意外插入** 2. **遍历时不要修改 key(会破坏排序)** 3. **尽量使用 `const auto&` 避免拷贝** 4. **删除元素时使用 `erase(it++)` 防止迭代器失效** ```cpp // 安全删除满足条件的元素 for (auto it = m.begin(); it != m.end(); ) { if (it->second < 10) { it = m.erase(it); // erase 返回下一个有效迭代器 } else { ++it; } } ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值