C++程序设计:自定义类作为Key的map编程技法与原理

自定义类作为Key的map编程技法与原理

什么类型可以作为map的Key?

想要明白这一点,首先要知道STL中的map是怎样的一种数据结构。map是基于红黑树实现的数据结构,谈到"红黑树"可能会让不少同学望而却步,红黑树不懂,那二叉搜索树可否理解?如果单单要搞懂这篇博客,知道红黑树也是一种搜索树即可啦!

那么,什么是基于"红黑树"实现呢?其实就是说,它的Key是存储在红黑树上面的!好吧,不说红黑树了,咱们下面就用搜索树去替代这个词吧~

众所周知,搜索树的定义就是:"左子树中的最大值小于根节点、根节点小于右子树的最小值"!如此一来,大家看出搜索树需要能够进行什么操作了吗?没错,就是比较操作!所以,自带重载小于(<)运算符的类型,可以作为map的Key!

普通类型作为map的Key

(一)数值型类型

众所周知,数值型是可以直接根据数值的大小来完成比较的,所以数值型类型可以作为map的Key。

举个例子,记录出现最多的数,比如1 2 2 3 3 3 4 5 6 6 6 7,如果答案不唯一就输出较小的那一个,怎么做呢?

cpp 复制代码
#include <iostream>
#include <map>
#include <algorithm>
using namespace std;

int main() {
    int val, maxCnt = 0, ans;
    map<int, int> cnt;
    while (cin >> val) {
        if (cnt.find(val) == cnt.begin())
            cnt[val] = 1;
        else
            ++cnt[val];
    }
    for (map<int, int>::iterator it = cnt.begin(); cnt.end() != it; ++it) {
        if (maxCnt < it->second) {
            maxCnt = it->second;
            ans = it->first;
        }
    }
    cout << ans << endl;
    return 0;
}

测试结果:

(二)其他stl容器类型

map经常用来做一些容器的关系映射,例如字符串,这个很常见!还是给一个例子帮助大家理解:

词频统计问题,需要给出若干个单词,来计算它们分别出现的次数。各个单词出现的次数除以总次数称之为词频,请输出词频最大的单词。

输入:

bash 复制代码
hello
my
father
and
my
mother
hello
my
motherland
and
my
love

输出:

bash 复制代码
and 0.166667
father 0.0833333
hello 0.166667
love 0.0833333
mother 0.0833333
motherland 0.0833333
my 0.333333

参考程序如下:

cpp 复制代码
#include <iostream>
#include <map>
#include <algorithm>
using namespace std;

int main() {
    string word;
    map<string, int> wordCount;
    map<string, double> wordFreq;
    int totalCount = 0;
    while(getline(cin, word)) {
        if (wordCount.find(word) == wordCount.end())
            wordCount[word] = 1;
        else
            ++wordCount[word];
        ++totalCount;
    }
    for (map<string, int>::iterator it = wordCount.begin(); wordCount.end() != it; ++it) {
        wordFreq[it->first] = it->second * 1.0 / totalCount;
    }
    for (map<string, double>::iterator it = wordFreq.begin(); wordFreq.end() != it; ++it) {
        cout << it->first << ' ' << it->second << endl;
    }
    return 0;
}

如何设计自定义的类型,作为map的Key呢?

其实很简单,只需要你的类能够进行比较运算即可!来看看map的源码吧:

从中可以看到,它需要一个比较器!这个比较器的实现方法就非常多了!什么仿函数、函数指针、lambda表达式、重载小于运算符什么的,都是可以的!我一般喜欢用重载运算符来做,个人比较喜欢!

举个例子:

加入有N个学生,学生有两个属性分别是姓名和考试分数,需要对考试分数进行划分,然后输出学生信息。规定大于等于90分的是A等,大于等于60分的是B等,其余是C等。输出的时候,要求分数高的先输出,如果分数相同,则按照名字的字典序输出。

输入:

bash 复制代码
5
zuozy 98
zhangwj 67
lifl 86
jiangzl 48
jiangjy 98

输出:

bash 复制代码
Name : jiangjy, score : 98      rank : A
Name : zuozy, score : 98        rank : A
Name : lifl, score : 86         rank : B
Name : zhangwj, score : 67      rank : B
Name : jiangzl, score : 48      rank : C

参考程序如下:

cpp 复制代码
#include <iostream>
#include <map>
#include <string>
#include <algorithm>
using namespace std;

class Student {
    friend ostream& operator<<(ostream& out, const Student& res);
private:
    string name;
    int score;
public:
    Student(string name_, int score_) : name(name_), score(score_) {

    }

    bool operator<(const Student& obj) const {
        if (score != obj.score)
            return score > obj.score;
        return name < obj.name;
    }
};

ostream& operator<<(ostream& out, const Student& res) {
    out << "Name : " << res.name << ", score : " << res.score;
    return out;
}

char getRank(int score) {
    if (score >= 90)
        return 'A';
    else if (score >= 60)
        return 'B';
    else
        return 'C';
}

int main() {
    map<Student, char> stuRank;
    int n;
    cin >> n;
    for (int i = 0; i < n; ++i) {
        string curName;
        int curScore;
        cin >> curName >> curScore;
        stuRank[Student(curName, curScore)] = getRank(curScore);
    }
    for (auto it : stuRank) {
        cout << it.first << "\trank : " << it.second << endl;
    }
    return 0;
}

方法论:如何理解对小于运算符的重载?

众所周知,在搜索树中,左子树、根节点、右子树是一种升序关系,所以在重载小于运算符的时候,例如上面的程序:

cpp 复制代码
	bool operator<(const Student& obj) const {
        if (score != obj.score)
            return score > obj.score;
        return name < obj.name;
    }

调用这个运算符重载函数的时候,this是左操作数,也就当前对象,而obj是右操作数,也就是和当前对象来比较的对象。既然搜索树是一种升序的关系,那么肯定最终的结果是this排在obj的前面!!!

那么,可以把重载小于运算符理解为:什么样的对象排在前面!比如我们这里,因为已经知道this是排在前面的了,于是我们要求分数不相等时,分数高的排在前面。如果分数相等,则是名字字典序较小的排在前面!而谁满足这个条件,谁就排在前面!你想想,如果当前的this对象不满足条件会怎样?它会插入到搜索树的另一侧! 因此,可以把逻辑抽象成:

bash 复制代码
if (obj1 < root)
{
	obj1 插入到root的左侧
}
else
{
	obj1 插入到root的右侧
}

你可以发现,不管怎样,都是让小于运算符为True的对象,排在前面!这样,我相信你就一定理解了如何设计重载运算符,也明白了map的原理!