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

时间:2012-06-27 11:50:29

标签: c++ containers std replace

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

每次在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);
}

第一次没问题,第二次在ClashingName0中更改名称,第三次将名称首次变为ClashingName0,而不是发现与第二个实例的冲突并将名称设置为ClashingName1 ...你得到了这个想法,它是指数级的,当它在该循环结束时,通过了不可接受的时间量。

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

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

3 个答案:

答案 0 :(得分:2)

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

的最后一个索引

答案 1 :(得分:1)

mapunordered_map将执行此任务,使用count函数来测试名称是否在地图中,以及find函数或{ {1}}访问它。

operator[]整体上可能会快一点。

unordered_map可以更好地处理您讨厌的map示例,因为您可以使用"ClashingName"lower_bound在单个查找中查找最后一个冲突名称,而不是查找upper_bound依次提升每个人。

请注意,ClashingName0默认按字典顺序排序,因此ClashingName9999位于map之前。如果某人为您提供了包含数字的名称,尤其是最后的名称,那么也会出现问题。

使用Nim的建议修复此问题 - 使用一对ClashingName10作为地图密钥,并根据需要从该对构造名称。同样,当有人为您提供以数字结尾的名称时,您必须做一些特别的事情。确保名称ClashingName9不能出现两次,一次为string, int,另一次为"Clash10"。一个简单的选择是禁止提供名称中的一个字符,并将其用作分隔符。

答案 2 :(得分: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];
    }
}

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