怎样为std::map的自定义key提供比较操作(一)

2023-11-13

  stl的关联容器(map,set)的key一般要求提供 < 比较操作。假设我们有一个结构SomeKey:

struct SomeKey
{
    int a, b;
};

  要想以SomeKey作为std::map的key,需要为这个结构提供operator < 比较操作,比如:

// 实现1
bool operator < (const SomeKey& left, const SomeKey& right)
{
    if (left.a < right.a) // 主key
    {
        return true;
    }
    else if (left.a == right.a && left.b < right.b) // 次key
    {
        return true;
    }
    else
    {
        return false;
    }
}

  或者:

// 实现2
bool operator < (const SomeKey& left, const SomeKey& right)
{
    if (left.a != right.a) // 主key
    {
        return left.a < right.a;
    }
    
    if (left.b != right.b) // 次key
    {
        return left.b < right.b;
    }
    
    return false;
}

  这两种实现方式是很常见的了,似乎也没什么好聊的。不过在项目中,我一次又一次地遇到错误的operator < 实现,着实让人吃惊!比如:

// 实现3(错误)
bool operator < (const SomeKey& left, const SomeKey& right)
{
    if (left.a < right.a) // 主key
    {
        return true;
    }
    
    if (left.b < right.b) // 次key
    {
        return true;
    }

    return false;
}

  或者

// 实现4(错误)
bool operator < (const SomeKey& left, const SomeKey& right)
{
    return (left.a < right.a || left.b < right.b);
}

  这样的实现将导致荒谬的结果!比如有两个SomeKey对象:x {10, 20} 和 y {5, 30},采用“实现3”或“实现4”来比较 x 和 y 的大小,结果取决于比较的时候谁在前面,也就是说,如果这样比较:x < y,结果为真;而这样:y < x,结果也为真!似乎作者并没有理解 “ < ” 比较操作的含义。这种错误的比较操作,会导致std::map表现出怪异的行为:

std::map<SomeKey, int> m;
m.insert({ { 10, 20 }, 1 });
m.insert({ { 5, 30 }, 2 });
auto it = m.find({ 5, 30 });
if (it == m.end())
{
	std::cout << "找不到{5, 30}" << std::endl;
}
else
{
	std::cout << "找到{5, 30}" << std::endl;
}

  上面的代码片段会输出“找不到{5, 30}”。在向map表insert键值对时,是以被插入值(后称目标值)和红黑树上的节点(后称节点值)比较:
在这里插入图片描述

  而在map表中find时,是以节点值和目标值进行比较:
在这里插入图片描述

  因为{10, 20} < {5, 30},所以在{10, 20}的右子树上查找,自然就找不到了。再向map表中插入一个节点,行为就更怪异了:

m.insert({ { 8, 25 }, 3 });
it = m.find({ 5, 30 });
if (it == m.end())
{
	std::cout << "找不到{5, 30}" << std::endl;
}
else
{
	std::cout << "找到{5, 30}" << std::endl;
}

  这次将输出“找到{5, 30}”。因为{8, 25} < {10, 20},继续{8, 25} < {5, 30},最终{8, 25}插入到map表的最左子节点,破坏了红黑树的平衡,右旋后,整棵树变成:
这里写图片描述

  然后在表中find {5, 30},首先找到第一个不小于{5, 30}的节点,因为第一个节点值{5, 30} 不小于目标值{5, 30},而目标值{5, 30}也不小于该节点值{5, 30},于是就找到了目标值。这种怪异行为导致的bug是比较隐秘的:你在表中找一个值,时而找得到,时而又找不到。一般的产品代码中,map表中插入的对象数量都不在少数,你讨厌对大量数据的插入进行测试,你不会认为你的operator <区区几行代码会有bug,你又不可能怀疑map有什么问题,于是你很可能最终会将之归结于灵异现象。


  在产品代码中,结构体可能会更复杂,类似的operator < 错误实现也不少见。

  本文以一个小学生都能理解的例子来聊聊正确的`operator <`实现应该是怎样的。我们来比较两个数字的大小(为便于讨论,两个数都是两位的十进制数):


  假如有:x = 36,y = 49;则 x < y 为真;
  假如有:x = 36,y = 29;则 x < y 不为真;
  假如有:x = 36,y = 39;则 x < y 为真;
  假如有:x = 36,y = 32;则 x < y 不为真;
  假如有:x = 36,y = 36;则 x < y 不为真;


  我们是怎么正确比较出各种情况下 x 和 y 的大小的?实在是很简单:首先比较高位数(十位数),谁的高位数小,谁就小;谁的高位数大,谁就大;高位数相等的话,那就以相同的方式再比较低位数(个位数)。为什么要先比较高位数,因为高位数的“权”大啊!


  类似的,比较SomeKey的时候,哪个字段先来,哪个字段就是主key,SomeKey中,a是主key,b是次key,即 a 对于SomeKey对象“大小”的贡献占主导地位,如果 a 能决出胜负来,b 就不用比了,只有 a 不能决出胜负时(相等),才继续比较 b,无论怎么比较,结果要是稳定的!


  “实现3”和“实现4”要这样修改一下才行:

bool operator < (const SomeKey& left, const SomeKey& right)
{
    if (left.a < right.a) // 主key
    {
        return true; // 主key小,就小
    }
    
    if (left.a > right.a) // 主key
    {
        return false; // 主key大,就大
    }
    
    return left.b < right.b; // 主key相等,再比较次key
}

  说起来,本文其实不值一提!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

怎样为std::map的自定义key提供比较操作(一) 的相关文章