关于set实现结构体自动去重原理的推论

转自本人博客,原文链接

先说结论

在每个操作均为log复杂度的前提下,set无法在判断顺序和重复关键字不同时完成对结构体元素的去重。

首先我们先看这段结构体定义,目的是先按num相等进行去重,再按key降序排列。

cpp 复制代码
struct node{
    int num;
    int key;
    bool operator < (const node &b) const{
        if(num == b.num)
            return false;
        else{
            if(key != b.key)
                return key > b.key;
            return num > b.num;
        }
    }
};

然后我们测试以下四组输入数据




从第一组数据来看,我们的去重要求确实得到了满足,第二个插入的(3, 7)并没有进入set。但我们看到第二组数据,我们后插入(3, 7)set却没有正确的执行我们想象中的去重操作。首先我们要知道,set容器在判定已有元素a和新插入元素b是否相等时,是这么做的:

1)将a作为左操作数,b作为有操作数,调用比较函数,并返回比较值

2)将b作为左操作数,a作为有操作数,再调用一次比较函数,并返回比较值。

如果1、2两步的返回值都是false,则认为a、b是相等的,则b不会被插入set容器中

如果1、2两步的返回值都是true,则可能发生未知行为

按照我们结构体定义的优先级,(3, 7)没有被正确去重,只能说明在插入时它并没有与(3, 1)进行比较,而是与其他元素比较后直接确定了大小顺序就插入了。结合第三组数据,难道set插入时是按顺序从头比较吗?显然不是的,因为第四组数据我们把(2, 4)换成(3, 4)就能成功去重了。所以(2, 4)的插入应当影响了set中数据存储的结构。

而我们又知道set底层使用红黑树实现的。所以我们做出如下猜想验证三,四组数据的结果。

对于第三组数据,首先插入(3, 1)为根节点,后插入(3, 7)与根节点比较发现重复,不插入。再插入(1, 2)由于我们按key的大小排序(1, 2) > (3, 1)因此放到右子节点。(2, 4)同理,放到(1, 2)的右子节点。此时,树的平衡性丧失,所以要进行左旋使树重新平衡。左旋后根节点为(1, 2),左子节点(3, 1), 右子节点(2, 4)。再插入(3, 7)时,先与根节点比较,发现大于,后(3, 7)进入右子树,与(2, 4)比较后就直接确定顺序了,不会与(3, 1)比较,这就导致了无法正确去重。

而对于第四组数据,我们可以发现,在插入(1, 2)后,树的平衡性并没有丧失,所以根节点还是(3, 1)所以后插入的元素还是会和根节点进行比较,从而能够正确的去重。

为了进一步验证,我们可以将后插入的(3, 7)换为(3, 0)这样它在与左旋后的根节点(1, 2)比较后就会进入左子树与(3, 1)比较,从而能够被正确去重。实验数据如下:

发现(3, 0)确实没有被插入,因此我们的猜想的到了验证。


其实出现这种情况的本质原因是我们判断重复和排序的关键字不同。导致num相同的元素可能因为与根节点key的大小关系不同而被分到两个完全不同的子树中去。而如果我们如果将重复和排序的关键字换成如下相同的样子:

cpp 复制代码
struct node{
    int num;
    int key;
    bool operator < (const node &b) const{
        if(num == b.num)
            return false;
        return num > a.num;
        return key > a.key;
    }
};

则不会出现无法去重的情况。因为在后者的情况下,只要num相同,与其他元素的大小关系就会确定,就会被存储在红黑树中的相同位置。

从另一个角度说,我们选择set,是因为它有每个操作log级别的优秀复杂度。而log级别的插入操作不可能做到和所有元素都进行比较。