Список с несколькими индексами

Учитывая общий список, мне нужен какой-то индекс (в смысле базы данных), который позволит мне быстро найти. Ключи для этого индекса не будут уникальными, поэтому я не могу использовать словарь. Вот что я имею в виду: учитывая класс Foo {P1, P2, P3}, который может иметь такие данные, как

{ "aaa", 111, "yes" }
{ "aaa", 112, "no" }
{ "bbb", 111, "no" }
{ "bbb", 220, "yes" }
{ "bbb", 220, "no" }
{ "ccc", 300, "yes" }

Мне нужно быстро получить доступ ко всем записям, где P1 - "bbb" (3-й, 4-й и 5-й), или все те, где P2 - 111 (1-й и 3-й). Я мог бы использовать отсортированный список, но если мне понадобится более одного способа сортировки/индексирования, я получаю дублированные списки.

Есть ли что-то встроенное в .NET framework или, возможно, в библиотеку ОС, которая бы сделала что-то подобное? Спасибо.

P.S. Я упомянул "отсортированный список" с мыслью, что отсортированный список вернет/найдет элемент намного быстрее. Мне не нужен список, который нужно сортировать; Я просто ищу быстрого поиска/поиска.

Ответ 1

У меня никогда не было возможности использовать его, но вы можете попробовать i4o. Он должен предоставлять индексы для объектов в памяти для использования с Linq. Вы указываете индексы для класса, используя либо атрибуты, либо как часть построения индексатора, тогда вы создаете IndexableCollection.

В этот момент вы просто запрашиваете коллекцию с помощью Linq, а индексы работают за кулисами, чтобы опционировать шаблоны доступа для данных.

Ответ 2

Никогда не забывайте этот принцип: сделайте это правильно, дайте ему понять, сделайте его кратким, сделайте это быстро. В этой последовательности. Итак, сначала кодируйте наивную реализацию:

static IEnumerable<T> GetByIndex<T>(
    List<T> list,
    Func<T, TIndex> func,
    TIndex key
) {
    return list.Where(x => func(x) == key);
}

Использование:

List<Test> tests = new List<Test>() {
            new Test { Name = "aaa", Value = 111, Valid = Valid.Yes },
            new Test { Name = "aaa", Value = 111, Valid = Valid.Yes },
            new Test { Name = "bbb", Value = 112, Valid = Valid.No },
            new Test { Name = "bbb", Value = 111, Valid = Valid.No },
            new Test { Name = "bbb", Value = 220, Valid = Valid.No },
            new Test { Name = "ccc", Value = 220, Valid = Valid.Yes }
};
IEnumerable<Test> lookup = GetByIndex(tests, x => x.Name, "bbb");

Вышеприведенное является правильным, ясным и кратким. Почти наверняка это достаточно быстро для ваших целей.

Итак, насколько это быстро, вы должны сначала измерить:

  • Установите разумный критерий эффективности.
  • Установите тестовый слой данных реального мира.
  • Профилируйте простой подход к тестовому плану реальных данных. Обратите внимание, что профилирование включает в себя вывод о том, является ли эта функциональность узким местом в вашем приложении.

Тогда, если и только если это недостаточно быстро для вас, вы должны попытаться оптимизировать. Было бы не слишком сложно реализовать IndexedList<T> : ICollection<T>, который позволит вам индексировать различные свойства.

Вот наивная реализация, которая могла бы начать:

class IndexedList<T> : IEnumerable<T> {
    List<T> _list;
    Dictionary<string, Dictionary<object, List<T>>> _dictionary;
    Dictionary<string, Func<T, object>> _propertyDictionary;

    public IndexedList(IEnumerable<string> propertyNames) : this(propertyNames, new List<T>()) { }

    public IndexedList(IEnumerable<string> propertyNames, IEnumerable<T> source) {
        _list = new List<T>();
        _dictionary = new Dictionary<string, Dictionary<object, List<T>>>();
        _propertyDictionary = BuildPropertyDictionary(propertyNames);
        foreach (var item in source) {
            Add(item);
        }
    }

    static Dictionary<string, Func<T, object>> BuildPropertyDictionary(IEnumerable<string> keys) {
        var propertyDictionary = new Dictionary<string,Func<T,object>>();
        foreach (string key in keys) {
            ParameterExpression parameter = Expression.Parameter(typeof(T), "parameter");
            Expression property = Expression.Property(parameter, key);
            Expression converted = Expression.Convert(property, typeof(object));
            Func<T, object> func = Expression.Lambda<Func<T, object>>(converted, parameter).Compile();
            propertyDictionary.Add(key, func);
        }
        return propertyDictionary;
    }

    public void Add(T item) {
        _list.Add(item);
        foreach (var kvp in _propertyDictionary) {
            object key = kvp.Value(item);
            Dictionary<object, List<T>> propertyIndex;
            if (!_dictionary.TryGetValue(kvp.Key, out propertyIndex)) {
                propertyIndex = new Dictionary<object, List<T>>();
                _dictionary.Add(kvp.Key, propertyIndex);
            }
            List<T> list;
            if (!propertyIndex.TryGetValue(key, out list)) {
                list = new List<T>();
                propertyIndex.Add(key, list);
            }
            propertyIndex[key].Add(item);
        }
    }

    public IEnumerable<T> GetByIndex<TIndex>(string propertyName, TIndex index) {
        return _dictionary[propertyName][index];
    }

    public IEnumerator<T> GetEnumerator() {
        return _list.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return GetEnumerator();
    }
}

Использование:

List<Test> tests = new List<Test>() {
            new Test { Name = "aaa", Value = 111, Valid = Valid.Yes },
            new Test { Name = "aaa", Value = 111, Valid = Valid.Yes },
            new Test { Name = "bbb", Value = 112, Valid = Valid.No },
            new Test { Name = "bbb", Value = 111, Valid = Valid.No },
            new Test { Name = "bbb", Value = 220, Valid = Valid.No },
            new Test { Name = "ccc", Value = 220, Valid = Valid.Yes }
};
// build an IndexedList<Text> indexed by Name and Value
IndexedList<Test> indexed = new IndexedList<Test>(new List<string>() { "Name", "Value" }, tests);
// lookup where Name == "bbb"
foreach (var result in indexed.GetByIndex("Name", "bbb")) {
    Console.WriteLine(result.Value);
}

Но посмотрите, причина, по которой вы этого не делаете, если наивная реализация еще не достаточно быстра, связана с дополнительной сложностью, которую вы только что добавили в свою систему. Вы только что добавили новый код для поддержки, новый код для тестирования и, возможно, ничего не выиграли, если это не ускоряется в ваших реальных данных или не является узким местом вашего приложения.

Ответ 3

( Отредактировано для разработки стратегии на основе коллекции)

В .NET нет встроенной структуры для поиска с использованием различных индексов. Вот две хорошие стратегии:

Вариант 1: LINQ, для гибкости и простоты
Для простоты и множества других встроенных параметров создайте Список (или что-то еще, что реализует IEnumerable) настраиваемых типов, и используйте LINQ для выполнения запросов по запросу. Обратите внимание, что вы можете использовать анонимные типы, если это вам удобно. Вы также можете иметь свои данные в структуре XML и все еще делать все это. Вероятно, вы сможете получить свои данные, выполнить поиск и обработать результаты в небольшом количестве чистого кода. В .Net 4.0 вы можете использовать параллельный Ling (PLINQ), чтобы без труда использовать этот процесс для многоядерной обработки.

List<foo> bigFooList = new List<foo>  
{  
     new Foo {"aaa", 111, "yes"},  
     new Foo {"aaa", 112, "no"},  
     new Foo {"bbb", 111, "no"},  
     new Foo {"bbb", 220, "yes"},  
     new Foo {"bbb", 220, "no"},  
     new Foo {"ccc", 300, "yes"}  
};    
var smallFooList = From f In bigFooList Where f.P2 = 220 Select f; 

Вариант 2: несколько коллекций, для индексированной мощности поиска.
Если вы делаете много поисков на большом наборе и нуждаетесь в мощности, вы можете использовать несколько коллекций для более быстрого поиска. Трудная часть - ваше требование, чтобы значения индекса можно было дублировать. Вот несколько стратегий:

  • Отметьте класс поиска. Создайте свой список. Затем для каждого поля, для которого требуется индексированный поиск, создайте объект Lookup. Они не могут быть построены, но получены из вашей коллекции IEnumerable:
    Lookup<string, foo> LookupP1 = (Lookup<string, foo>) fooList.ToLookup(f => f.P1, f => p)
    См. Ссылку для синтаксиса для извлечения ваших элементов. В основном LookupP1 содержит объекты IGrouping для каждого уникального значения P1, с ключом на это значение P1. Вы перебираете этот объект, чтобы получить соответствующие элементы. Ключевым атрибутом объектов Lookup является то, что они неизменяемы; поэтому каждый раз, когда вы добавляете/вычитаете из своего fooList, вам придется переделать все объекты Lookup. Но если вы редко изменяете свой fooList, это путь.
  • Создайте Dictionary<T, List<foo>> для каждого поля, по которому вам нужно будет искать по индексу, где T - тип этого значения. Итак, для вашего примера мы бы создали:
    var FoosByP1 = new Dictionary<String,List<foo>>
    var FoosByP2 = new Dictionary<Int32,List<foo>> и т.д.
    Затем добавьте FoosByP1, используя каждое уникальное значение P1, Список, содержащий все элементы foo, где P1 имеет это значение. (например, "aaa", "Список", содержащий все объекты foo, для которых P1 "aaa".) Повторите для каждого поля Foo. Основываясь на ваших данных, FoosByP1You будет содержать 3 объекта List, содержащие 2, 3 и 1 элементы foo соответственно. С помощью этой схемы вы можете быстро получить ее. (Словарь в основном представляет собой хеш-таблицу). Основной улов заключается в том, что ваши данные будут дублироваться в каждом из этих словарей, что может быть или не быть проблемой. Если Foo имеет 20 полей, и у вас много элементов foo, вы можете сэкономить память, имея центральный словарь с числовым ключом и всеми вашими элементами foo, а отдельные индексированные словари вместо этого будут Dictionary<T, List<Int32>>, где целое число будет индексом пункта Foo в вашем центральном словаре. Это спасло бы память и все еще было бы довольно быстро. Если у вас есть центральный словарь или нет, построение ваших диктонаров займет несколько циклов процессора, но как только вы их получите, вы будете в отличной форме. И используйте Linq для создания ваших словарей!

Ответ 4

Одним из путей было бы просто использовать встроенную реляционную базу данных a la SQLite (здесь есть привязка ADO.NET: http://sqlite.phxsoftware.com/)

Большинство структур данных не будут соответствовать вашим требованиям, если вы не захотите повторно сортировать список/независимо от каждого раза, так как вам нужен другой порядок.

Ответ 5

Возможно, вы захотите рассмотреть нечто вроде Lucene.Net, библиотеку индексирования и поиска. Я не знаю, может ли это быть более сложным решением, чем вы искали, но это определенно соответствовало бы вашим потребностям в производительности.

Ответ 6

Я знаю, что вы сказали, что не можете использовать словарь, но будет ли следующая работа?

Для вашего примера набора данных:

{ "aaa", 111, "yes" }
{ "aaa", 112, "no"  }
{ "bbb", 111, "no"  }
{ "bbb", 220, "yes" }
{ "bbb", 220, "no"  }
{ "ccc", 300, "yes" }

Вы можете использовать следующее:

var p1Lookup = new Dictionary<string,int []>();
p1Lookup.Add( "aaa", new int [] {0, 1} );
p1Lookup.Add( "bbb", new int [] {2, 3, 4} );
p1Lookup.Add( "ccc", new int [] {5} );

var p2Lookup = new Dictionary<int,int []>();
p1Lookup.Add( 111, new int [] {0, 2} );
p1Lookup.Add( 112, new int [] {1} );
p1Lookup.Add( 220, new int [] {3, 4} );
p1Lookup.Add( 300, new int [] {5} );

var p3Lookup = new Dictionary<int,int []>();
p1Lookup.Add( "yes", new int [] {0, 3, 5} );
p1Lookup.Add(  "no", new int [] {1, 2, 4} );

В зависимости от использования вы можете создавать поисковые словари только один раз

Ответ 7

Если вам нужно только перебирать список один раз, но искать его много раз и менять его очень мало (лучше всего использовать индексы БД). Словарь будет очень быстрым после его создания. Мой метод не создает дубликатов.

var indexDict = new Dictionary<string, List<int>>();

for(int ct = 0; ct < pList.length; ct++)
{
    var item = pList[ct];

    if (!indexDict.ContainsKey(item.toIndexBy))
    {
        indexDict.Add(item.toIndexBy, new List<int> { ct };
    }
    else
    {
        indexDict[item.toIndexBy].add(ct);
    }
}

Теперь у вас есть супер быстрый поиск индексов.

Итак, если вам нужны индексы "bbb", вы можете сделать:

int bbbIndexes = indexDict["bbb"];

Ответ 8

Почему бы не использовать HashSet для хранения различных экземпляров объекта Foo (который будет уникальным), а затем использовать запрос LINQ для извлечения тех, которые соответствуют заданным критериям?

Что-то вроде:

var hash = new HashSet<Foo>
{
new Foo { P1 = "aaa", P2 = 111, P3 = "yes"},
new Foo { P1 = "aaa", P2 = 112, P3 = "no"},
new Foo { P1 = "bbb", P2 = 111, P3 = "no"},
new Foo { P1 = "bbb", P2 = 220, P3 = "yes"},
new Foo { P1 = "bbb", P2 = 220, P3 = "no"},
new Foo { P1 = "ccc", P2 = 300, P3 = "yes"},
};

var results = from match in hash
where match.P1 == "aaa"
select match;