对于非常长的字符串列表,啥是合适的搜索/检索方法?

Posted

技术标签:

【中文标题】对于非常长的字符串列表,啥是合适的搜索/检索方法?【英文标题】:What's an appropriate search/retrieval method for a VERY long list of strings?对于非常长的字符串列表,什么是合适的搜索/检索方法? 【发布时间】:2014-04-18 20:30:56 【问题描述】:

这不是一个非常罕见的问题,但我似乎仍然找不到真正解释这个选择的答案。

我有一个非常大的字符串列表(准确地说是SHA-256 哈希的ASCII 表示),我需要查询该列表中是否存在字符串。

此列表中可能有超过 1 亿个条目,我需要多次重复查询是否存在条目。

考虑到大小,我怀疑我是否可以将其全部塞入HashSet<string>。什么是合适的检索系统以最大限度地提高性能?

我可以对列表进行预排序,可以将其放入 SQL 表中,也可以将其放入文本文件中,但我不确定对于我的应用程序来说什么才是最有意义的。

在这些或其他检索方法中,在性能方面是否有明显的赢家?

【问题讨论】:

乍一看,既然需要搜索,首选的方式是存储在一个Sql表中,但是这真的要看这个列表是什么,如果是一次性的,不可变的转换诸如此类的事情,如果需要维护等等…… @Crono,它或多或少是不可变的,如果列表需要更改,那么我们可能只是拆除然后重新建立表。如果使用 SQL,我最好选择带有聚集索引的单列,还是我也可以做其他事情? 使用“trie” - en.wikipedia.org/wiki/Trie. 没有人看到使用 HashSet<string> 存储 stringed 哈希的讽刺意味吗? i> 为什么要使用哈希来存储和查找本身就是哈希的数据? SHA256 是 256 位。您的 100M 条目非常稀少,以至于在同一个存储桶中发生冲突的机会几乎为零。只需从条目中取出 32 位(或其他数字,具体取决于您的 RAM),然后制作一个大型向量数组(包含对字符串的引用)以进行查找。对于碰撞,只需移动到下一个空桶。 【参考方案1】:

你可以试试Suffix Tree,这个question 介绍了如何在 C# 中做到这一点

或者你可以尝试这样的搜索

var matches = list.AsParallel().Where(s => s.Contains(searchTerm)).ToList();

AsParallel 将有助于加快处理速度,因为它会创建查询的并行化。

【讨论】:

这不是需要先在内存中加载完整的字符串列表吗? @datatest,我无法将此记录集完全加载到内存中,它太大了。 更重要的是,如果您将所有字符串加载到内存中,您不妨只使用哈希集。【参考方案2】:

哈希集将您的数据拆分为存储桶(数组)。在 64 位系统上,the size limit for an array is 2 GB,大约 2,000,000,000 字节。

由于字符串是一种引用类型,并且由于一个引用占用 8 个字节(假设是 64 位系统),因此每个存储桶可以容纳大约 250,000,000(2.5 亿)个对字符串的引用。它似乎比您需要的要多。

话虽如此,正如 Tim S. 指出的那样,即使引用适合哈希集,您也不太可能拥有必要的内存来保存字符串本身。数据库会更适合我。

【讨论】:

那么字符串本身的存储是如何考虑的呢?假设字符串大小约为 400 字节,那么 2GB 段中只能容纳大约 4,000,000 个字符串,不是吗? @GrantH。它没有。该数组不存储字符串本身,它存储对字符串的引用。想象数十亿颗星星散布在夜空中,然后想象一排人,每个人都指向一颗星星。这条线不能超过2.5亿人。 (抱歉,看到 Cosmos 回归太激动了)。 一个 SHA256 哈希是 256 字节。 base64 编码(认为这就是“ASCII 表示”的意思)意味着它需要大约 341 个字符。字符串中的每个字符在 .Net 中由两个字节 (UTF-16) 表示,因此约为 682 个字节。 682 字节 * 100,000,000 ~= 63 TB。因此,除非您有 64TB 的内存,否则这方式太多数据无法一次保存在内存中(无论您如何引用它)。 There is no longer a 2GB limit 如果您正确配置您的应用程序。 一个 SHA256 哈希是 256 个,而不是字节。他可以将所有字符串放在 11 或 12 兆字节中。但这是一种非常昂贵的做事方式。一个 32 字节结构的数组将占用 3.2 gig,这似乎很合理。【参考方案3】:

如果集合是常量,那么只需制作一个大的排序哈希列表(原始格式,每个 32 字节)。存储所有散列,使其适合磁盘扇区 (4KB),并且每个扇区的开头也是散列的开头。将每个第 N 个扇区的第一个哈希值保存在一个特殊的索引列表中,这样可以很容易地放入内存中。在这个索引列表上使用二进制搜索来确定哈希应该在的扇区簇的起始扇区,然后在这个扇区簇中使用另一个二进制搜索来找到你的哈希。 N值应根据测试数据确定。

编辑:替代方法是在磁盘上实现您自己的哈希表。该表应该使用open addressing策略,并且尽可能将探测序列限制在同一个磁盘扇区。空槽必须标有特殊值(例如全零),因此在查询是否存在时应特别处理此特殊值。为避免冲突,该表的值不应少于 80%,因此在您的情况下,有 1 亿个大小为 32 字节的条目,这意味着该表应至少具有 100M/80%= 1.25 亿个插槽,并且具有大小125M * 32 = 4 GB。您只需要创建将 2^256 域转换为 125M 的哈希函数,以及一些不错的探测序列。

【讨论】:

【参考方案4】:

这些答案没有将字符串内存考虑到应用程序中。 .NET 中的字符串不是 1 char == 1 字节。 每个字符串对象都需要 20 字节的常量用于对象数据。缓冲区每个字符需要 2 个字节。因此:字符串实例的内存使用估计为 20 + (2 * Length) 字节。

让我们做一些数学运算。

100,000,000 个唯一字符串 SHA256 = 32 字节(256 位) 每个字符串的大小 = 20 + (2 * 32 字节) = 84 字节 所需的总内存:8,400,000,000 字节 = 8.01 GB

可以这样做,但这不会很好地存储在 .NET 内存中。您的目标应该是将所有这些数据加载到可以访问/分页的表单中,而无需一次将其全部保存在内存中。为此,我会使用Lucene.net,它将您的数据存储在磁盘上并智能地搜索它。将每个字符串作为可搜索的形式写入索引,然后在索引中搜索该字符串。现在你有一个可以处理这个问题的可扩展的应用程序;您唯一的限制是磁盘空间(填满 TB 驱动器需要大量字符串)。或者,将这些记录放入数据库并对其进行查询。这就是数据库存在的原因:将事物保存在 RAM 之外。 :)

【讨论】:

一个 SHA256 哈希的长度是 256 位,而不是 256 字节。用十六进制字符表示的 32 个字节是 64 个字符,即 128 个字节。每个字符串大约需要 148 个字节,而不是 532 个字节。他应该能够将所有字符串放入 11 或 12 GB 的大小。顺便说一句,如果哈希是 256 字节长,那么它们每个需要 1024 字节(编码一个字节需要 2 个字符,每个字符乘以 2 个字节)。 如果您要存储字符串(这里没有意义,因为 32 字节二进制结构的表示显然比其十六进制字符串更紧凑),那么您不一定将它们存储为字符串。例如,紧凑的 DAWG 经常会出现某些插入减少总内存大小的情况。 实际上,我敢打赌这可以用前缀特里树来非常有效地表示。事实上,我敢打赌它会非常高效。 实际上,我正在讨论将字符串表示为十六进制字符(仅使用字符 0-9 和 A-F)。 Base64 编码需要 44 个字符(尽管您可以将其缩减为 43,因为您知道在这种情况下最后一个字符无关紧要)来表示 32 个字节。因此,如果哈希表示为 Base64,则字符串将只有 86 个字节,加上分配开销。 @JonHanna 我使用this 制作了大约 30,000 个随机 64 字符 SHA256 哈希字符串的 DAWG。它大约 7 MB - 至少比拼字游戏字典 TWL06 的 DAWG 大 13 倍,后者大约有 180,000 个单词。因此,DAWG 可能不适合这项任务,因为随机性使其无法使用。【参考方案5】:

如果列表随时间发生变化,我会将其放入数据库中。

如果列表没有改变,我会把它放在一个排序的文件中,并对每个查询进行二进制搜索。

在这两种情况下,我都会使用Bloom filter 来最小化 I/O。而且我会停止使用字符串并使用带有四个 ulong 的二进制表示(以避免对象引用成本)。

如果您有超过 16 GB(2*64*4/3*100M,假设Base64 编码)的空闲空间,一个选项是创建一个 Set&ltstring> 并感到高兴。当然,如果您使用二进制表示,它会小于 7 GB。

David Haney 的回答告诉我们,内存成本并不是那么容易计算的。

【讨论】:

使用 Bloom 过滤器是个好主意,但仅当值不在集合中的可能性为中到高时才使用它。它只能为以下问题提供“肯定不是”或“可能是”的答案:“这个值在集合中吗?”。如果答案是“可能它在集合中”,那么您仍然需要查看它以确保它不是误报。【参考方案6】:

为了获得最大速度,请将它们保存在 RAM 中。它只有约 3GB 的数据价值,加上您的数据结构所需的任何开销。 HashSet&lt;byte[]&gt; 应该可以正常工作。如果您想降低开销和 GC 压力,请打开 <gcAllowVeryLargeObjects>,使用单个 byte[],并使用带有自定义比较器的 HashSet&lt;int&gt; 对其进行索引。

为了提高速度和降低内存使用率,请将它们存储在基于磁盘的哈希表中。 为简单起见,请将它们存储在数据库中。

无论您做什么,都应该将它们存储为纯二进制数据,而不是字符串。

【讨论】:

HashSet&lt;byte[]&gt; 相当昂贵。分配一个数组需要大约 50 字节的开销。所以你的开销大于数据。最好创建一个包含 4 个 ulong 值的 struct。×评论只能编辑 5 分钟×评论只能编辑 5 分钟×评论只能编辑 5 分钟【参考方案7】:

使用&lt;gcAllowVeryLargeObjects&gt;,您可以拥有更大的数组。为什么不将这些 256 位哈希码的 ASCII 表示转换为实现 IComparable&lt;T&gt; 的自定义结构?它看起来像这样:

struct MyHashCode: IComparable<MyHashCode>

    // make these readonly and provide a constructor
    ulong h1, h2, h3, h4;

    public int CompareTo(MyHashCode other)
    
        var rslt = h1.CompareTo(other.h1);
        if (rslt != 0) return rslt;
        rslt = h2.CompareTo(other.h2);
        if (rslt != 0) return rslt;
        rslt = h3.CompareTo(other.h3);
        if (rslt != 0) return rslt;
        return h4.CompareTo(other.h4);
    

然后您可以创建一个包含这些的数组,该数组将占用大约 3.2 GB。您可以使用Array.BinarySearch 轻松搜索。

当然,您需要将用户的输入从 ASCII 转换为其中一种哈希码结构,但这很容易。

就性能而言,它不会像哈希表那样快,但肯定会比数据库查找或文件操作快。

想想看,你可以创建一个HashSet&lt;MyHashCode&gt;。您必须覆盖MyHashCode 上的Equals 方法,但这真的很容易。我记得,HashSet 每个条目的成本大约为 24 个字节,并且您将获得更大结构的额外成本。如果您要使用HashSet,总共需要五到六 GB。更多内存,但仍然可行,并且您得到 O(1) 查找。

【讨论】:

【参考方案8】:

可能需要一段时间 (1) 来转储(聚集索引)表中的所有记录(最好使用它们的值,而不是它们的字符串表示形式 (2))并让 SQL 进行搜索。它会为你处理二进制搜索,它会为你处理缓存,如果你需要对列表进行更改,它可能是最容易使用的东西。而且我很确定查询内容将与构建自己的内容一样快(或更快)。

(1):要加载数据,请查看 SqlBulkCopy 对象,ADO.NET 或 Entity Framework 之类的东西会太慢,因为它们会逐行加载数据。

(2):SHA-256 = 256 位,所以二进制 (32) 就可以了;这只是您现在使用的 64 个字符的一半。 (如果您使用的是Unicode numbers =P,则为四分之一)再一次,如果您当前有纯文本文件中的信息,您仍然可以采用 char(64) 方式并将数据转储到使用 bcp.exe 的表。数据库会更大,查询会稍微慢一些(因为需要更多的 I/O + 缓存只保存相同数量 RAM 的一半信息)等等......但这很简单,如果你'对结果不满意,您仍然可以编写自己的数据库加载器。

【讨论】:

【参考方案9】:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;

namespace HashsetTest

    abstract class HashLookupBase
    
        protected const int BucketCount = 16;

        private readonly HashAlgorithm _hasher;

        protected HashLookupBase()
        
            _hasher = SHA256.Create();
        

        public abstract void AddHash(byte[] data);
        public abstract bool Contains(byte[] data);

        private byte[] ComputeHash(byte[] data)
        
            return _hasher.ComputeHash(data);
        

        protected Data256Bit GetHashObject(byte[] data)
        
            var hash = ComputeHash(data);
            return Data256Bit.FromBytes(hash);
        

        public virtual void CompleteAdding()  
    

    class HashsetHashLookup : HashLookupBase
    
        private readonly HashSet<Data256Bit>[] _hashSets;

        public HashsetHashLookup()
        
            _hashSets = new HashSet<Data256Bit>[BucketCount];

            for(int i = 0; i < _hashSets.Length; i++)
                _hashSets[i] = new HashSet<Data256Bit>();
        

        public override void AddHash(byte[] data)
        
            var item = GetHashObject(data);
            var offset = item.GetHashCode() & 0xF;
            _hashSets[offset].Add(item);
        

        public override bool Contains(byte[] data)
        
            var target = GetHashObject(data);
            var offset = target.GetHashCode() & 0xF;
            return _hashSets[offset].Contains(target);
        
    

    class ArrayHashLookup : HashLookupBase
    
        private Data256Bit[][] _objects;
        private int[] _offsets;
        private int _bucketCounter;

        public ArrayHashLookup(int size)
        
            size /= BucketCount;
            _objects = new Data256Bit[BucketCount][];
            _offsets = new int[BucketCount];

            for(var i = 0; i < BucketCount; i++) _objects[i] = new Data256Bit[size + 1];

            _bucketCounter = 0;
        

        public override void CompleteAdding()
        
            for(int i = 0; i < BucketCount; i++) Array.Sort(_objects[i]);
        

        public override void AddHash(byte[] data)
        
            var hashObject = GetHashObject(data);
            _objects[_bucketCounter][_offsets[_bucketCounter]++] = hashObject;
            _bucketCounter++;
            _bucketCounter %= BucketCount;
        

        public override bool Contains(byte[] data)
        
            var hashObject = GetHashObject(data);
            return _objects.Any(o => Array.BinarySearch(o, hashObject) >= 0);
        
    

    struct Data256Bit : IEquatable<Data256Bit>, IComparable<Data256Bit>
    
        public bool Equals(Data256Bit other)
        
            return _u1 == other._u1 && _u2 == other._u2 && _u3 == other._u3 && _u4 == other._u4;
        

        public int CompareTo(Data256Bit other)
        
            var rslt = _u1.CompareTo(other._u1);    if (rslt != 0) return rslt;
            rslt = _u2.CompareTo(other._u2);        if (rslt != 0) return rslt;
            rslt = _u3.CompareTo(other._u3);        if (rslt != 0) return rslt;

            return _u4.CompareTo(other._u4);
        

        public override bool Equals(object obj)
        
            if (ReferenceEquals(null, obj))
                return false;
            return obj is Data256Bit && Equals((Data256Bit) obj);
        

        public override int GetHashCode()
        
            unchecked
            
                var hashCode = _u1.GetHashCode();
                hashCode = (hashCode * 397) ^ _u2.GetHashCode();
                hashCode = (hashCode * 397) ^ _u3.GetHashCode();
                hashCode = (hashCode * 397) ^ _u4.GetHashCode();
                return hashCode;
            
        

        public static bool operator ==(Data256Bit left, Data256Bit right)
        
            return left.Equals(right);
        

        public static bool operator !=(Data256Bit left, Data256Bit right)
        
            return !left.Equals(right);
        

        private readonly long _u1;
        private readonly long _u2;
        private readonly long _u3;
        private readonly long _u4;

        private Data256Bit(long u1, long u2, long u3, long u4)
        
            _u1 = u1;
            _u2 = u2;
            _u3 = u3;
            _u4 = u4;
        

        public static Data256Bit FromBytes(byte[] data)
        
            return new Data256Bit(
                BitConverter.ToInt64(data, 0),
                BitConverter.ToInt64(data, 8),
                BitConverter.ToInt64(data, 16),
                BitConverter.ToInt64(data, 24)
            );
        
    

    class Program
    
        private const int TestSize = 150000000;

        static void Main(string[] args)
        
            GC.Collect(3);
            GC.WaitForPendingFinalizers();

            
                var arrayHashLookup = new ArrayHashLookup(TestSize);
                PerformBenchmark(arrayHashLookup, TestSize);
            

            GC.Collect(3);
            GC.WaitForPendingFinalizers();

            
                var hashsetHashLookup = new HashsetHashLookup();
                PerformBenchmark(hashsetHashLookup, TestSize);
            

            Console.ReadLine();
        

        private static void PerformBenchmark(HashLookupBase hashClass, int size)
        
            var sw = Stopwatch.StartNew();

            for (int i = 0; i < size; i++)
                hashClass.AddHash(BitConverter.GetBytes(i * 2));

            Console.WriteLine("Hashing and addition took " + sw.ElapsedMilliseconds + "ms");

            sw.Restart();
            hashClass.CompleteAdding();
            Console.WriteLine("Hash cleanup (sorting, usually) took " + sw.ElapsedMilliseconds + "ms");

            sw.Restart();
            var found = 0;

            for (int i = 0; i < size * 2; i += 10)
            
                found += hashClass.Contains(BitConverter.GetBytes(i)) ? 1 : 0;
            

            Console.WriteLine("Found " + found + " elements (expected " + (size / 5) + ") in " + sw.ElapsedMilliseconds + "ms");
        
    

结果非常有希望。它们运行单线程。在 7.9GB RAM 使用情况下,hashset 版本每秒可以进行超过 100 万次查找。基于阵列的版本使用更少的 RAM (4.6GB)。两者之间的启动时间几乎相同(388 秒对 391 秒)。 hashset 用 RAM 换取查找性能。由于内存分配限制,两者都必须分桶。

阵列性能:

散列和加法耗时 307408 毫秒

哈希清理(通常是排序)花费了 81892 毫秒

在 562585 毫秒 [每秒 53k 次搜索] 内找到 30000000 个元素(预计为 30000000 个)

========================================

哈希集性能:

散列和加法耗时 391105 毫秒

哈希清理(通常是排序)耗时 0ms

在 74864 毫秒 [每秒 40 万次搜索] 内找到 30000000 个元素(预计为 30000000 个)

【讨论】:

所以,我昨晚试了一下,结果就像做梦一样!将所有数据加载到内存中大约需要 20 分钟(可以将其并行化,但担心为此所需的缓冲可能会让我处于边缘),但是一旦它在那里,查询速度非常快。内存使用率相当高(~9gb),但我的 64 位机器有 16 gigs 的 ram 并不介意。 使用多个哈希集的目的是什么?此外,因为他正在搜索 SHA 哈希,所以哈希的每个部分都应该足够随机以显着简化 GetHashCode() 多个哈希集是因为一个哈希集OOMs在93m记录。通过使用散列数据来确定将散列放入哪个桶,可以对类进行改进。这可能会产生更不均匀的存储分布,但查找将直接转到有问题的哈希,而不是尝试所有这些。所有相等部分都是 R# 自动生成的。 在您的 app.config 中设置 <gcAllowVeryLargeObjects> 并没有让您创建更大的哈希集? @insta,每秒一百万次查找。哇,这绝对是这个问题的明确答案。感谢您提供如此完整的答案。【参考方案10】:

在这种情况下您需要小心,因为大多数语言中的大多数集合都没有真正针对这种规模进行设计或优化。正如您已经确定内存使用也是一个问题。

这里明显的赢家是使用某种形式的数据库。一个 SQL 数据库或多个 NoSQL 数据库都合适。

SQL 服务器已经过设计和优化,可用于跟踪大量数据、为其编制索引以及在这些索引中进行搜索和查询。它旨在完全按照您的意愿去做,这确实是最好的方法。

为了提高性能,您可以考虑使用嵌入式数据库,该数据库将在您的进程中运行并节省由此产生的通信开销。对于 Java,我可以为此推荐一个 Derby 数据库,我不知道 C# 等效项足以在那里提出建议,但我想存在合适的数据库。

【讨论】:

【参考方案11】:

首先,我真的建议您使用数据压缩以最大程度地减少资源消耗。高速缓存和内存带宽通常是现代计算机中最有限的资源。无论你如何实现,最大的瓶颈都是等待数据。

我还建议使用现有的数据库引擎。其中许多具有内置压缩功能,并且任何数据库都会利用您可用的 RAM。如果您有一个不错的操作系统,系统缓存将尽可能多地存储文件。但大多数数据库都有自己的缓存子系统。

我无法确定哪种数据库引擎最适合您,您必须尝试一下。就我个人而言,我经常使用性能不错的 H2,它既可以用作内存数据库,也可以用作基于文件的数据库,并且内置了透明压缩。

我看到有些人表示将数据导入数据库并构建搜索索引可能比某些自定义解决方案需要更长的时间。这可能是真的,但进口通常很少见。我假设您对快速搜索更感兴趣,因为它们可能是最常见的操作。

还有为什么 SQL 数据库既可靠又非常快速,您可能需要考虑 NoSQL 数据库。尝试一些替代方案。要知道哪种解决方案能给您带来最佳性能,唯一的方法就是对它们进行基准测试。

您还应该考虑将列表存储为文本是否有意义。也许您应该将列表转换为数值。这将使用更少的空间,从而为您提供更快的查询。数据库导入可能会明显变慢,但查询可能会变得更快。

【讨论】:

你真的可以压缩 SHA 哈希,它实际上是随机字符串吗? 好吧,您可以将它们转换为大小为 (256/8) = 32 的 int 数组。即使您的哈希使用 Base64 编码,您仍然有 33% 的开销,因为每个 8 位字符只编码一个6 位哈希 上面的评论有一个错字:如果hash表示为int数组,那么里面有8个整数 如果您使用有意义的字符串编码,它将仅使用所有可用字符的子集,以便可打印和可读。您真的不想在这样的字符串中使用退格或箭头字符。此外,您不压缩字符串,而是压缩包含许多字符串的存储数据块。压缩到少量数据几乎总是失败。【参考方案12】:

如果您想要非常快,并且元素或多或少是不可变的并且需要完全匹配,您可以构建像病毒扫描程序一样运行的东西:使用任何与之相关的算法设置范围以收集最小数量的潜在元素您的条目和搜索条件,然后遍历这些项目,使用 RtlCompareMemory 对搜索项目进行测试。如果它们相当连续,您可以从磁盘中提取项目并使用以下内容进行比较:

    private Boolean CompareRegions(IntPtr hFile, long nPosition, IntPtr pCompare, UInt32 pSize)
    
        IntPtr pBuffer = IntPtr.Zero;
        UInt32 iRead = 0;

        try
        
            pBuffer = VirtualAlloc(IntPtr.Zero, pSize, MEM_COMMIT, PAGE_READWRITE);

            SetFilePointerEx(hFile, nPosition, IntPtr.Zero, FILE_BEGIN);
            if (ReadFile(hFile, pBuffer, pSize, ref iRead, IntPtr.Zero) == 0)
                return false;

            if (RtlCompareMemory(pCompare, pBuffer, pSize) == pSize)
                return true; // equal

            return false;
        
        finally
        
            if (pBuffer != IntPtr.Zero)
                VirtualFree(pBuffer, pSize, MEM_RELEASE);
        
    

我将修改此示例以获取一个充满条目的大缓冲区,然后循环遍历这些条目。但是托管代码可能不是要走的路。最快总是更接近执行实际工作的调用,因此基于直接 C 构建的具有内核模式访问权限的驱动程序会快得多。..

【讨论】:

【参考方案13】:
    将您的哈希存储为 UInt32[8]

2a。使用排序列表。要比较两个哈希,首先比较它们的第一个元素;如果它们相等,则比较第二个,依此类推。

2b。使用前缀树

【讨论】:

【参考方案14】:

首先,您说字符串实际上是 SHA256 哈希值。观察100 million * 256 bits = 3.2 gigabytes,因此可以将整个列表放入内存中,假设您使用内存高效的数据结构。

如果你原谅偶尔的误报,你实际上可以使用更少的内存。查看布隆过滤器http://billmill.org/bloomfilter-tutorial/

否则,使用排序的数据结构来实现快速查询(时间复杂度O(log n))。


如果您确实想将数据存储在内存中(因为您经常查询并且需要快速的结果),请尝试使用 Redis。 http://redis.io/

Redis 是一个开源、BSD 许可的高级键值存储。它通常被称为数据结构服务器,因为键可以包含字符串、散列、列表、集合和排序集合。

它有一个固定的数据类型http://redis.io/topics/data-types#sets

Redis 集合是字符串的无序集合。可以在 O(1) 中添加、删除和测试成员的存在(无论 Set 中包含的元素数量如何,时间都是恒定的)。


否则,请使用将数据保存在磁盘上的数据库。

【讨论】:

【参考方案15】:

普通的二叉搜索树将在大型列表上提供出色的查找性能。但是,如果您真的不需要存储字符串并且您想知道简单的成员资格,那么布隆过滤器可能是一个很好的解决方案。布隆过滤器是一种紧凑的数据结构,您可以使用所有字符串进行训练。一旦受过训练,它可以很快告诉你它以前是否见过一根绳子。它很少报告假阳性,但从不报告假阴性。根据应用程序的不同,它们可以用相对较少的内存快速产生惊人的结果。

【讨论】:

也许您可以通过一些示例和/或代码片段来支持您的答案,并解释它如何比 OP 正在考虑的 HashSet 表现更好。【参考方案16】:

我开发了一个类似于Insta's 方法的解决方案,但有一些区别。实际上,它看起来很像他的分块数组解决方案。但是,我的方法不是简单地拆分数据,而是构建块索引并将搜索定向到适当的块。

索引的构建方式与哈希表非常相似,每个桶都是一个排序数组,可以使用二进制搜索进行搜索。但是,我认为计算 SHA256 散列的散列没有什么意义,所以我只取值的前缀。

这项技术的有趣之处在于,您可以通过扩展索引键的长度来调整它。更长的键意味着更大的索引和更小的桶。我的 8 位测试用例可能偏小; 10-12 位可能会更有效。

我试图对这种方法进行基准测试,但它很快就耗尽了内存,因此我看不到任何有趣的性能。

我还写了一个 C 实现。 C 实现也无法处理指定大小的数据集(测试机器只有 4GB 的 RAM),但它确实管理得更多。 (在这种情况下,目标数据集实际上并不是什么大问题,它是填满 RAM 的测试数据。)我无法找到一种快速向其抛出数据的好方法查看其性能测试。

虽然我喜欢写这篇文章,但总的来说,我想说的是,它主要提供了支持你不应该尝试在内存中使用 C# 执行此操作的论点。

public interface IKeyed

    int ExtractKey();


struct Sha256_Long : IComparable<Sha256_Long>, IKeyed

    private UInt64 _piece1;
    private UInt64 _piece2;
    private UInt64 _piece3;
    private UInt64 _piece4;

    public Sha256_Long(string hex)
    
        if (hex.Length != 64)
        
            throw new ArgumentException("Hex string must contain exactly 64 digits.");
        
        UInt64[] pieces = new UInt64[4];
        for (int i = 0; i < 4; i++)
        
            pieces[i] = UInt64.Parse(hex.Substring(i * 8, 1), NumberStyles.HexNumber);
        
        _piece1 = pieces[0];
        _piece2 = pieces[1];
        _piece3 = pieces[2];
        _piece4 = pieces[3];
    

    public Sha256_Long(byte[] bytes)
    
        if (bytes.Length != 32)
        
            throw new ArgumentException("Sha256 values must be exactly 32 bytes.");
        
        _piece1 = BitConverter.ToUInt64(bytes, 0);
        _piece2 = BitConverter.ToUInt64(bytes, 8);
        _piece3 = BitConverter.ToUInt64(bytes, 16);
        _piece4 = BitConverter.ToUInt64(bytes, 24);
    

    public override string ToString()
    
        return String.Format("0:X0:X0:X0:X", _piece1, _piece2, _piece3, _piece4);
    

    public int CompareTo(Sha256_Long other)
    
        if (this._piece1 < other._piece1) return -1;
        if (this._piece1 > other._piece1) return 1;
        if (this._piece2 < other._piece2) return -1;
        if (this._piece2 > other._piece2) return 1;
        if (this._piece3 < other._piece3) return -1;
        if (this._piece3 > other._piece3) return 1;
        if (this._piece4 < other._piece4) return -1;
        if (this._piece4 > other._piece4) return 1;
        return 0;
    

    //-------------------------------------------------------------------
    // Implementation of key extraction

    public const int KeyBits = 8;
    private static UInt64 _keyMask;
    private static int _shiftBits;

    static Sha256_Long()
    
        _keyMask = 0;
        for (int i = 0; i < KeyBits; i++)
        
            _keyMask |= (UInt64)1 << i;
        
        _shiftBits = 64 - KeyBits;
    

    public int ExtractKey()
    
        UInt64 keyRaw = _piece1 & _keyMask;
        return (int)(keyRaw >> _shiftBits);
    


class IndexedSet<T> where T : IComparable<T>, IKeyed

    private T[][] _keyedSets;

    public IndexedSet(IEnumerable<T> source, int keyBits)
    
        // Arrange elements into groups by key
        var keyedSetsInit = new Dictionary<int, List<T>>();
        foreach (T item in source)
        
            int key = item.ExtractKey();
            List<T> vals;
            if (!keyedSetsInit.TryGetValue(key, out vals))
            
                vals = new List<T>();
                keyedSetsInit.Add(key, vals);
            
            vals.Add(item);
        

        // Transform the above structure into a more efficient array-based structure
        int nKeys = 1 << keyBits;
        _keyedSets = new T[nKeys][];
        for (int key = 0; key < nKeys; key++)
        
            List<T> vals;
            if (keyedSetsInit.TryGetValue(key, out vals))
            
                _keyedSets[key] = vals.OrderBy(x => x).ToArray();
            
        
    

    public bool Contains(T item)
    
        int key = item.ExtractKey();
        if (_keyedSets[key] == null)
        
            return false;
        
        else
        
            return Search(item, _keyedSets[key]);
        
    

    private bool Search(T item, T[] set)
    
        int first = 0;
        int last = set.Length - 1;

        while (first <= last)
        
            int midpoint = (first + last) / 2;
            int cmp = item.CompareTo(set[midpoint]);
            if (cmp == 0)
            
                return true;
            
            else if (cmp < 0)
            
                last = midpoint - 1;
            
            else
            
                first = midpoint + 1;
            
        
        return false;
    


class Program

    //private const int NTestItems = 100 * 1000 * 1000;
    private const int NTestItems = 1 * 1000 * 1000;

    private static Sha256_Long RandomHash(Random rand)
    
        var bytes = new byte[32];
        rand.NextBytes(bytes);
        return new Sha256_Long(bytes);
    

    static IEnumerable<Sha256_Long> GenerateRandomHashes(
        Random rand, int nToGenerate)
    
        for (int i = 0; i < nToGenerate; i++)
        
            yield return RandomHash(rand);
        
    

    static void Main(string[] args)
    
        Console.WriteLine("Generating test set.");

        var rand = new Random();

        IndexedSet<Sha256_Long> set =
            new IndexedSet<Sha256_Long>(
                GenerateRandomHashes(rand, NTestItems),
                Sha256_Long.KeyBits);

        Console.WriteLine("Testing with random input.");

        int nFound = 0;
        int nItems = NTestItems;
        int waypointDistance = 100000;
        int waypoint = 0;
        for (int i = 0; i < nItems; i++)
        
            if (++waypoint == waypointDistance)
            
                Console.WriteLine("Test lookups complete: " + (i + 1));
                waypoint = 0;
            
            var item = RandomHash(rand);
            nFound += set.Contains(item) ? 1 : 0;
        

        Console.WriteLine("Testing complete.");
        Console.WriteLine(String.Format("Found: 0 / 0", nFound, nItems));
        Console.ReadKey();
    

【讨论】:

以上是关于对于非常长的字符串列表,啥是合适的搜索/检索方法?的主要内容,如果未能解决你的问题,请参考以下文章

iOS SceneKit 啥是 inversedTransform? (对于假人)

Python Fabric:如何检索目录的文件列表

对于相对较新的计算机,啥是 ol' C++ Beep() 的好替代品?

高效的海量字符串搜索问题

python中啥是序列,列表,元组,字符串,索引,区别是啥?

Lucene的不同搜索类型及其作用