在 c++ 中存储对象的最佳策略是什么,确保名称唯一并能够在以后有效地检索它们

What is the best strategy to store objects ensuring unique name and being able to efficiently retrieve them later, in c++?

本文关键字:唯一 检索 有效地 对象 存储 c++ 最佳 是什么 策略 确保      更新时间:2023-10-16

我正在编写一个复合类(类似于Qt的QObject),目前我将子级存储在std::vector中。每个复合实例都有一个名称,并且此名称在作为该实例的同级的所有其他实例之间必须是唯一的,或者更好地说,在共享同一父实例的实例之间必须是唯一的。

每次将一个新实例推送到vector内时,我都必须查看它的名称是否已被vector中已有的实例之一使用,如果是,我必须更改其名称并添加一个数字。

我想出的代码非常愚蠢,当孩子的数量变得一致时,速度非常慢。

这是类:

class Composite
{
public:
    Composite(const std::string &name, Composite *ancestor=NULL);
    ~Composite();
private:
    std::string name;
    Composite *ancestor;
    std::vector<Composite*> descendants;
public:
    void setName(const std::string &name);
};

这是构造函数和setName实现:

Composite::Composite(const std::string &name, Composite *ancestor)
{
    this->ancestor = ancestor;
setName(name);
if (ancestor!=NULL)
    ancestor->descendants.push_back(this);
}

.

void Composite::setName(const std::string &name)
{
    this->name = name;
    if (ancestor!=NULL)
    {
        CompositeList::iterator dIt;
        for( dIt=ancestor->descendants.begin(); dIt!=ancestor->descendants.end(); dIt++)
        {
            if ((*dIt)==this)
            {
                continue;
            }
            else if (this->name == (*dIt)->getName())
            {
                 int trailingNumber = stringToInt(getTrailingNumber(this->name));
                 std::string cleanName = removeTrailingNumber(this->name);
                 this->name = cleanName+intToString(trailingNumber+1);
            }
        }
    }
}

对于极少数孩子来说,这可能很好,但是当他们变成几百个孩子时,setName功能真的变得很慢。想象一下这种情况:

Composite *parent = new Composite("pippo");
for (int i=0; i<10000; i++)
{
    Composite("ClashingName", parent);
}

第一次没问题,第二次在冲突名称0中更改名称,第三次名称首先被卡入冲突名称0,然后找到与第二个实例的冲突并将名称设置为冲突名称1...你明白了,它是指数级的,当它到达那个循环结束时,会经过一个不可接受的时间。

所以这里真正的问题是如何有效地找到名称冲突并有效地分配一个尚未使用的新名称?任何 std 容器对我来说都很好,我的编译器支持 C++11,但我不能/不想使用 Boost,因为我正在处理的项目非常小(基本上是这个类)

我不是一个经验丰富的C++用户,我想使用mapunordered_map但我真的很渴望专家的建议。

IMO,您需要更改对象的存储方式。如下所示:

std::map<std::string, std::vector<Composite> >

映射的关键是前缀,向量中的索引是第 n 个Composite对象。您需要提供一个自定义查找函数来拆分传入的名称。例如

std::string query = "pipo_10";

在您的查找函数中,

Composite* lookup(std::string const& key)
{
  // split the key to get the prefix and the index
  // lookup in the map using the prefix
  // return the index
}

编辑1:要保存所有字符串操作,您可以定义自己的自定义键,它只是一对(例如,std::pair<std::string, int>是前缀和索引),您的查找将仅使用此中的值。

编辑2:多考虑一下,最好有一张地图,例如

std::map<std::string, std::map<int, Composite> >
现在,索引

不再是向量中的索引,而是第二个映射中的查找。这样可以更好地处理删除,键将是我之前所说的组合键(对)。

归功于@Steves的建议..

std::map<std::pair<std::string, int>, Composite>

使用lower_bound()技巧查找给定Composite的最后一个索引

mapunordered_map将完成这项工作,使用 count 函数测试名称是否在地图中,并使用find函数或operator[]来访问它。

unordered_map总体上可能会更快一些。

map可以更好地处理您讨厌的"ClashingName"示例,因为您可以使用lower_boundupper_bound在单个查找中查找最后一个冲突的名称,而不是查找ClashingName0中的每个名称...... 反过来ClashingName9999

请注意,默认情况下map按字典顺序排序,因此ClashingName10排在ClashingName9之前。当有人为您提供包含数字的名称时会发生什么,尤其是在最后,也存在问题。

使用

Nim 的建议解决此问题 - 使用一对string, int作为映射键,并根据需要从该对构造名称。同样,当有人为您提供以数字结尾的名称时,您必须做一些特别的事情。确保名称"Clash10"不能出现两次,一次是("Clash", 10),一次是("Clash1", 0)。一个简单的选项是禁止提供的名称中的一个字符,并将其用作分隔符。

如果您不介意为每个对象添加额外的映射,则可以执行以下操作:

// inside Composite definiton
std::map<std::string, int> names_of_descendants;

然后简单地:

void Composite::setName(const std::string &name)
{
    if (ancestor)
    {
        // for the first usage of certain name, map's operator[]
        // will insert default constructed int (0) in the map
        this->name = name + ancestor->names_of_descendants[name];
        // increment the value for this name, for the next call of setName
        ++names_of_descendants[name];
    }
}

您可以保留用于存储后代的向量。