考虑一种情况,例如表示一个稀疏矩阵。例如,矩阵可以是1,000,000行x 1,000,000 cols(或其他较大尺寸),在任何特定时间可能有50、100或几千个单元为非零值。
我正在尝试辨别代表这种情况的最佳C ++数据结构。暴力破解和非常糟糕的答案是(为简洁起见,示例仅在1个单元格中添加了一个值,假设填充了数百或数千个单元格):
int numRows = 1000000;
int numCols = 1000000;
std::vector<std::vector<int>> sparseMatrix(numRows, std::vector<int>(numCols, 0));
int currentRow = 12345;
int currentCol = 98765;
sparseMatrix[currentRow][currentCol] = 10;
std::cout << "\n" << "sparseMatrix[currentRow][currentCol] = " << sparseMatrix[currentRow][currentCol] << "\n\n";
显然这是灾难,因为没有使用专用于数据结构的99%以上的内存。
(至少对我而言)下一个直观的选择是:
std::unordered_map<std::pair<int, int>, int> sparseMatrix;
int currentRow = 12345;
int currentCol = 98765;
std::pair<int, int> rowCol = std::make_pair(currentRow, currentCol);
sparseMatrix[rowCol] = 10;
std::cout << "\n" << "sparseMatrix[rowCol] = " << sparseMatrix[rowCol] << "\n\n";
不幸的是,这无法编译并显示以下错误:
attempting to reference a deleted function
在对此主题进行一些搜索之后,似乎unordered_map
并未设置为使用一对作为密钥。
据我所知,还有4种合法选择:
1)使用map
(确实接受一对整数作为键),而不是unordered_map
(例如,它可以编译并运行):>
std::map<std::pair<int, int>, int> sparseMatrix;
int currentRow = 12345;
int currentCol = 98765;
std::pair<int, int> rowCol = std::make_pair(currentRow, currentCol);
sparseMatrix[rowCol] = 10;
std::cout << "\n" << "sparseMatrix[rowCol] = " << sparseMatrix[rowCol] << "\n\n";
2),例如,使用unordered_map
中的unordered_map
个(也可以编译并运行):
std::unordered_map<int, std::unordered_map<int, int>> sparseMatrix;
int currentRow = 12345;
int currentCol = 98765;
sparseMatrix[currentRow][currentCol] = 10;
std::cout << "\n" << "sparseMatrix[currentRow][currentCol] = " << sparseMatrix[currentRow][currentCol] << "\n\n";
3)为行和列整数创建我自己的哈希函数,并将其输入更典型的std::unordered_map<int, int>
中。这似乎是一个非常糟糕的选择,因为如果两个整数对映射到相同的哈希键,将很难处理。
4)使用boost :: hash,我收集的内容类似于:
std::unordered_map<std::pair<int, int>, int, boost::hash<pair<int, int>>> sparseMatrix;
我倾向于不喜欢此选项b / c 1)数据结构看起来很笨拙,2)我不确定如何执行其余的实现,以及3)在某些情况下,boost可能无法实现可用。
为了澄清我的问题,它们是:
1) 上面的哪个选项最适合大多数情况? (如果可能的话,我真的更愿意坚持第一或第二)。
2) 根据我对map
s(红黑树)和unordered_map
s(哈希表)的了解,在#1会是最好的内存但#2会更快的印象下,我的理解在这种情况下正确吗?
3) 如果我正确地认为#1的内存更好,而#2的内存更快,那么我在上面提到的一般情况下是否有明显的赢家( 1,000,000 x 1,000,000矩阵,通常填充约1,000个值)还是差值被洗掉了?
4) 实施#3和#4会有多困难?如果#3和/或#4实施得非常好,那么性能收益是否足以超过#1或#2的编码复杂度成本?
在有人将此帖子标记为重复之前,我已经阅读了该帖子Why can't I compile an unordered_map with a pair as key?,该帖子涉及上面的选项,但未提供我在此处提出的问题的答案。
在有人说“使用内置的引导稀疏矩阵”之前,是的,我知道boost和其他一些库已经提供了稀疏矩阵类。我仍然在问这个问题,因为b / c是一个无序映射,其中的键是2个整数,在某些其他情况下可能会有用,而且有些人可能无法使用boost或可能希望为自己做更具体的实现一定的目的。
答案 0 :(得分:1)
显然这是灾难,因为没有使用专用于数据结构的99%以上的内存。
这根本不清楚。现代OS倾向于为应用程序提供虚拟内存,该虚拟内存在访问时仅通过物理RAM进行备份,因此只有将元素写入其中的内存页面才需要备份RAM。如果阵列中最多有数千个条目,并且每个内存页面都说是4k,那么您将使用数十兆字节的数量级-在典型的现代计算机上几乎没有压力。因此,这是浪费的,但不一定是有问题的浪费。它不是缓存友好的-其性能影响可能会引起更大的关注。
4)使用boost :: hash,我收集的看起来像这样:
std::unordered_map<std::pair<int, int>, int, boost::hash<pair<int, int>>> sparseMatrix;
我倾向于不喜欢此选项b / c 1)数据结构看起来很笨拙,2)我不确定如何执行其余的实现,以及3)在某些情况下,boost可能无法实现可用。
1)看起来很尴尬?来吧... 2)没什么可做的-您可以像使用其他unordered_map
一样使用它。3)您可以基于boost来创建自己的(参见this q):
template <class T>
inline void hash_combine(std::size_t& seed, const T& v)
{
std::hash<T> hasher;
seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}
struct hash_pair
{
std::size_t operator()(const std::pair<int, int>& p) const
{
std::size_t h = 0;
hash_combine(h, p.first);
hash_combine(h, p.second);
return h;
}
};
1)上面的哪个选项最适合大多数情况? (如果可能的话,我真的更愿意坚持第一或第二)。
在大多数情况下,没有一个编号选项是最好的:按照您对依赖boost的担忧,根据boost实现的hash_combine
创建自己的选项是基于标准库容器的最佳常规解决方案。
2)根据我对地图(红黑树)与unordered_maps(哈希表)的了解,我的印象是#1在内存上是最好的,但是#2会更快,这是我的理解正确的这种情况下?
内存使用情况不会有太大变化。 GCC的哈希表使用链接列表来存储值,其中每个值都需要通过链接指针进行动态内存分配,以及用于存储桶的连续数组(每个数组都是列表迭代器;数组的大小将(重新)调整为保持合理的负载)因素,因此不会特别大)。 map
的每个值也使用动态内存分配-但为左/右指针分配了一些额外的空间。很多。
3)如果我对#1在内存上更好而#2在内存上更快是正确的,那么在我上面提到的一般情况下(1,000,000 x 1,000,000矩阵,通常填充了大约1,000个值)是否有明显的赢家?差别大概是洗吗?
如前所述,不应期望内存使用情况比另一个更好(尽管实现可能有所不同)。至于速度更快,当填充的值太少时,只需同时实现它们并测量即可。当填充的元素数量很大时,哈希表的优势将始终如一地占据主导地位。
4)#3和#4的实施会有多困难?如果#3和/或#4实施得非常好,那么性能收益是否足以超过#1或#2的编码复杂度成本?
如前所述,您应该将#1与#4的窃取进行比较。忘记#3-当您意识到自己“非常糟糕的选择,因为如果两个整数对映射到相同的哈希键将很难处理”,则从根本上来说是有缺陷的。
关于编码复杂性-几乎没有。只需复制上面的哈希实现,在实例化unordered_map
时指定哈希策略,然后继续使用它即可。
如果在实施选项时遇到实际问题,请提出一个新问题以寻求帮助。
答案 1 :(得分:1)
它可能解决也可能无法解决您的问题,但是您的一个假设是错误的:
3)为行和列整数以及供稿创建自己的哈希函数 到更典型的std :: unordered_map中。好像 一个非常糟糕的选择,因为如果两个整数对映射到同一哈希 很难处理的密钥。
处理哈希冲突不是您必须要做的,而是unordered_map
为您做的事情。即使所有值的哈希都映射到相同的整数,即使性能会降低,也可以正确地确保将不同的值视为不同的键。
也就是说,假设您只有很少提及的元素,那么地图(map
或unordered_map
)既可以工作,又可以提供合理的性能。