使用char *作为unordered_map的键不能识别重复的键

时间:2019-02-01 02:08:17

标签: c++ char key unordered-map

我正在构建一个De Bruijn Assembly的示例,用于通过获取字符串n长度的每个可能单词,然后通过比较每个序列的末端片段来找到正确的阅读路径,从而组装基因组(或任何字符串)节点。它接受序列和每次读取序列的大小作为参数, 它将首先将所有读取收集到一个大小为[kmer_size] [3]的数组中,[3]索引0 =完全读取1 =读取的所有最右字符2 =读取的所有但最左字符。 / p>

组装读段的部分按预期工作,将其分离为一个函数,并且正确读取了这些读段。

然后我使用char *作为键和另一个映射作为值创建一个unordered_map,该映射由char *键控,并由int值。

应该发生的是,应该检查读取部分(不包括最左边的字符)是否与其他读取部分的相同部分匹配(如果匹配),获取匹配读取部​​分的右排除部分并创建一个新条目在内部地图中,您正在测试的读取的左排除部分将其作为键,并将该元素的值增加1。

如果查看输出,将会看到当i在单独的循环中打印嵌套地图的内容时,外部和内部地图中都有重复的条目。具有相同字符串值的char *键不会将项目放入相同的存储桶中,而是创建具有相同名称的新存储桶。 我认为这是因为char *实际上是一个字符串值,但一个地址,并且它们指向不同的地址。

我将如何修改此代码以使我的地图的每个字符串只有1个存储桶

#include<stdio.h>
#include<string.h>
#include<iostream>
#include<bits/stdc++.h> 
#include<unordered_map>

using namespace std;

void extractReads(char* kmers[][3], int num_kmers, int kmer_size, char* seq);

int main(int nargs, char* args[]){
    if(nargs!=3){
        cout<<"INVALID ARGUMENTS"<<endl;
        cout<<"dba <kmer_size> <sequence>"<<endl;
    }
    char* seq = args[2];
    int kmer_size = atoi(args[1]);
    int num_kmers = strlen(seq)-(kmer_size -1);
    char* kmers[num_kmers][3];
    unordered_map<char*, unordered_map<char*, int> > nodes;

    extractReads(kmers, num_kmers, kmer_size, seq);

    for(int i=0; i< num_kmers; i++)
    {
        for(int j=0; j<num_kmers; j++)
        {
            if(strcmp(kmers[i][2], kmers[j][2]) == 0 )
            {
                // cout<<" match"<<endl;
                nodes[kmers[i][2]][kmers[j][1]]++;
            }

        }
    }

    for(auto node: nodes)
    {
        cout<<node.first<<endl;
        for (auto n: node.second)
        {
            cout<<" "<<n.first<<" "<<n.second<<endl;
        }
    }

    return 0;
}



void extractReads(char* kmers[][3], int num_kmers, int kmer_size, char* seq)
{
    cout<<"READS"<<endl<<"==========="<<endl;
    for (int i=0; i<num_kmers; i++){
        kmers[i][0] = (char*) malloc(kmer_size);
        kmers[i][1] = (char*) malloc(kmer_size-1);
        kmers[i][2] = (char*) malloc(kmer_size-1);
        strncpy(kmers[i][0], seq+i, kmer_size);
        strncpy(kmers[i][1], kmers[i][0], kmer_size-1);
        strncpy(kmers[i][2], kmers[i][0]+1, kmer_size-1);
        cout<<kmers[i][0]<<" : "<<kmers[i][1]<<" "<<kmers[i][2]<<endl;
    }    
    cout<<"==========="<<endl;

}

1 个答案:

答案 0 :(得分:2)

您的代码有很多问题(正如对该问题的评论所暗示的那样),由于它们与问题的核心无关,因此我将在答案的末尾列出它们。

您怀疑的问题线是:

unordered_map<char*, unordered_map<char*, int> > nodes

如你所说

  

这是因为char *实际上是一个字符串值,而是一个地址,并且它们指向不同的地址。

换句话说,将您的字符串(kmers)作为指针进行比较。如果为两个char *对象分配了两个不同的malloc调用,则它们具有不同的地址。 unordered_map仅比较地址,不比较位于该地址的字符集。

解决方案是开始使用C ++字符串而不是C零终止字符串:

std::unordered_map<std::string, std::unordered_map<std::string, int> > nodes

这将解决代码中的其他问题:

  1. 您的代码存在内存泄漏。它使用malloc分配内存,并且从不释放内存。使用std::string可解决问题。
  2. kmer往往是较短的字符串(大多数字符串少于12个字母)。 std::string专门针对这种情况进行了优化,并且完全避免了这些字符串的堆内存。通过避免不必要的堆分配,使用std::string可使代码运行更快。

另一个不太理想的选择是提供自己的Hash KeyEqual函数:

class cstr_hash
{
   public:
      std::size_t operator()(const char *cstr) const
      {
          std::size_t hash = 5381;
          for ( ; *cstr != '\0' ; ++cstr)
             hash = (hash * 33) + *cstr;
          return hash;
      }
};
class cstr_eq
{
   public:
     using value_type = const char*;
     bool operator()(const char* a, const char *b) const
     { return strcmp(a, b) == 0; }
};

然后使用地图:

 std::unordered_map<const char *, int, cstr_hash, cstr_eq> nodes;

但是这种方法是不可取的,因为它使避免内存泄漏变得更加困难,并且无法像std::string那样优化短字符串。


您的代码有一些其他不相关的问题:

 char* kmers[num_kmers][3];

这不是C ++。大多数编译器支持VLA(可变长度数组),但它不是标准的一部分。最好使用std::vector<std::string>

内存泄漏。您使用malloc分配字符串,并且永远不会释放它们。最好使用std :: string并传递它,以便在代码中不再使用malloc。

对于少于10,000个元素的容器,

unordered_map的效率通常比std::map低。利用基因组数据,您很有可能遇到std::unordered_map值得的情况,但我会对此进行测试(尤其是对于内部容器)。

另一个问题是使用std::endl,这会使您的代码运行慢2到10倍。您应该使用'\n'而不是endlendl的作用是在行尾刷新输出。在许多情况下,附加的系统调用在性能方面有很大的不同。当然,如果这只是调试代码,那就没关系了。