Список с несколькими индексами - PullRequest
13 голосов
/ 27 января 2010

Учитывая общий список, мне понадобится какой-то индекс (в смысле базы данных), который позволил бы мне быстрый поиск. Ключи для этого индекса не будут уникальными, поэтому я не могу использовать словарь. Вот что я имею в виду: учитывая класс 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. Я упомянул «отсортированный список» с идеей, что отсортированный список вернет / найдет элемент намного быстрее. Мне не нужно, чтобы список был обязательно отсортирован; Я просто ищу быстрый поиск / поиск.

Ответы [ 8 ]

13 голосов
/ 27 января 2010

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

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");

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

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

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

Тогда, если и только если это не достаточно быстро для вас, вы должны попытаться оптимизировать. Было бы не сложно реализовать 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);
}

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

12 голосов
/ 27 января 2010

( Отредактировано для уточнения стратегии на основе коллекций)

В .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 в вашем центральном словаре. Это сэкономит память и все еще будет довольно быстрым.
    Независимо от того, есть ли у вас центральный словарь или нет, создание ваших Dictonaries займет несколько циклов ЦП, но как только вы их получите, вы будете в отличной форме. И используйте Linq для создания своих словарей!
2 голосов
/ 27 января 2010

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

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

2 голосов
/ 27 января 2010

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

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

0 голосов
/ 08 января 2015

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

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"];
0 голосов
/ 27 января 2010

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

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

{ "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} );

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

0 голосов
/ 27 января 2010

Почему бы не использовать 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;
0 голосов
/ 27 января 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...