Выберите верхние N элементов связанных объектов

У меня есть класс Product для хранения конкретного экземпляра данного продукта. У этого класса есть список связанных продуктов, которые похожи на основной продукт.

class Product
{
    public string Name;
    public double Rating;
    public List<Product> RelatedProducts;
    //...
    public List<Product> GetTopRelatedProducts(int N)
    {
        // How to implement this method
        // What I did so far(Bad code)
        //     1- INFINITE RECURSION
        //     2- Can not remember visited objects
        var myList = new List<Product>();
        foreach(Product prod in RelatedProducts)
        {
             myList.AddRange(prod.GetTopRelatedProducts(N));
        }
        return myList.Distinct().OrderByDescending(x => x.Rating).Take(N).ToList();
    }
}

Я хочу определить метод в классе Product, чтобы получить лучшие N (наилучшие оценки) связанные продукты. Этот метод должен учитывать, что элементы в списке RelatedProducts имеют тип Product, и у них также есть свой собственный список RelatedProducts. Поэтому я должен продолжать навигацию вложенного объекта до тех пор, пока все связанные продукты не будут достигнуты, а после этого возьмите верхний продукт N. Я имею в виду, что решение не было бы просто this.RelatedProducts.OrderByDescending(x => x.Rating).Take(N);

Еще одна вещь, о которой нужно помнить: Два продукта могут быть взаимно связаны. Это означает, что продукт A может принадлежать RelatedProducts списку продукта B и B также может принадлежать RelatedProducts списку продуктов А.

Любое предложение, как решить эту проблему оптимальным образом?

Представьте, у меня есть миллионы продуктов для поддержки. Как можно рекурсивно перемещаться по всем связанным продуктам и распознавать уже посещенные?

Я отметил это как С# и Java, так как одна и та же логика может применяться к обоим языкам

Ответ 1

Представьте, у меня есть миллионы продуктов для поддержки. Как можно рекурсивно перемещаться по всем связанным продуктам и распознавать уже посещенные?

Это не обязательно быть рекурсивным. Явным Stack или Queue может служить навигационная часть. Для сбора результата вместо List можно использовать HashSet. Это будет служить двум целям - позволить вам пропустить уже посещенные элементы, а также устранить необходимость Distinct в конце.

Вот пример реализации Queue:

public List<Product> GetTopRelatedProducts(int N)
{
    var relatedSet = new HashSet<Product>();
    var relatedListQueue = new Queue<List<Product>>();
    if (RelatedProducts != null && RelatedProducts.Count > 0)
        relatedListQueue.Enqueue(RelatedProducts);
    while (relatedListQueue.Count > 0)
    {
        var relatedList = relatedListQueue.Dequeue();
        foreach (var product in relatedList)
        {
            if (product != this && relatedSet.Add(product) && product.RelatedProducts != null && product.RelatedProducts.Count > 0)
                relatedListQueue.Enqueue(product.RelatedProducts);
        }
    }
    return relatedSet.OrderByDescending(x => x.Rating).Take(N).ToList();
}

Обновление:. Для полноты здесь приведены другие возможные реализации связанной части сбора:

С явным Stack:

public List<Product> GetTopRelatedProducts(int N)
{
    if (RelatedProducts == null || RelatedProducts.Count == 0)
        return new List<Product>();
    var relatedSet = new HashSet<Product>();
    var pendingStack = new Stack<List<Product>.Enumerator>();
    var relatedList = RelatedProducts.GetEnumerator(); 
    while (true)
    {
        while (relatedList.MoveNext())
        {
            var product = relatedList.Current;
            if (product != this && relatedSet.Add(product) && product.RelatedProducts != null && product.RelatedProducts.Count > 0)
            {
                pendingStack.Push(relatedList);
                relatedList = product.RelatedProducts.GetEnumerator();
            }
        }
        if (pendingStack.Count == 0) break;
        relatedList = pendingStack.Pop();
    } 
    return relatedSet.OrderByDescending(x => x.Rating).Take(N).ToList();
}

Хотя это немного более подробно, чем явная реализация на основе Queue, этот метод имеет меньше требований к пространству - O (высота), где height - максимальная глубина.

Преимущество обеих итерационных реализаций заключается в том, что они могут обрабатывать гораздо большую глубину, чем рекурсивные решения, которые могут привести к StackOverflowExpection. Но если глубина не будет настолько большой, и вы предпочтете рекурсию, то вот пара рекурсивных реализаций (все они должны иметь доступ к relatedSet и this):

С классическим частным рекурсивным методом:

public List<Product> GetTopRelatedProducts(int N)
{
    var relatedSet = new HashSet<Product>();
    GetRelatedProducts(this, relatedSet);
    return relatedSet.OrderByDescending(x => x.Rating).Take(N).ToList();
}

private void GetRelatedProducts(Product product, HashSet<Product> relatedSet)
{
    if (product.RelatedProducts == null) return;
    foreach (var item in product.RelatedProducts)
        if (item != this && relatedSet.Add(item))
            GetRelatedProducts(item, relatedSet);
}

С рекурсивной лямбдой:

public List<Product> GetTopRelatedProductsD(int N)
{
    var relatedSet = new HashSet<Product>();
    Action<Product> GetRelatedProducts = null;
    GetRelatedProducts = product =>
    {
        if (product.RelatedProducts == null) return;
        foreach (var item in product.RelatedProducts)
            if (item != this && relatedSet.Add(item))
                GetRelatedProducts(item);
    };
    GetRelatedProducts(this);
    return relatedSet.OrderByDescending(x => x.Rating).Take(N).ToList();
}

Последнее, но не менее важное: с последней версией С# 7.0 - рекурсивной локальной функцией:

public List<Product> GetTopRelatedProducts(int N)
{
    var relatedSet = new HashSet<Product>();
    GetRelatedProducts(this);
    return relatedSet.OrderByDescending(x => x.Rating).Take(N).ToList();

    void GetRelatedProducts(Product product)
    {
        if (product.RelatedProducts == null) return;
        foreach (var item in product.RelatedProducts)
            if (item != this && relatedSet.Add(item))
                GetRelatedProducts(item);
    }
}

Все эти методы обрабатывают (IMO) оптимально собирающую часть. Верхняя часть N не является оптимальной - O (N * log (N)) и может быть оптимизирована, как указано в ответе @Amit Kumar, но для этого потребуется внедрить отсутствующую стандартную структуру данных, которая выходит за рамки ответа SO.

Ответ 2

Я бы рекомендовал использовать приоритетную очередь (минимальную кучу) фиксированного размера N. Создайте очередь приоритетов при создании списка, поэтому после первоначальной операции сборки очередь приоритетов будет иметь наивысшие продукты с рейтингом N. Последующее добавление/удаление может быть выполнено путем проверки проверки верхнего элемента в очереди приоритетов в O(log(N)).

Псевдокод: новый элемент для добавления E

while PQ.size < N
     PQ.enqueue(E)
if PQ.size == N
   Etop = PQ.top() < Min heap element >
   if E.rating > Etop.rating 
      PQ.dequeu()
      PQ.enqueue(E)

Чтобы получить верхние N элементов, просто перейдите через PQ.

Ответ 3

Мое решение:

public List<Product> GetTopRelatedProducts(int N)
{
     List<Product> visitedProducts = new List<Product>();
     Queue<Product> ProductsQueue = new Queue<Product>();
     visitedProducts.add(this);
     foreach (product prod in relatedProducts)
         if(prod != this) //if a product can't be related to itself then remove this if statement
             ProductsQueue.Enqueue(prod); 

     //for now visitedproducts contains only our main product and ProductsQueue contains the product related to it.


     while (ProductsQueue.count > 0)
     {
          Product p = ProductsQueue.Dequeue();
          visitedProducts.add(p);
          foreach (product prod in p.relatedProducts)
          {
              if( ! visitedProduct.contains(prod) && !ProductsQueue.contains(prod))//if we haven't visited the product already or if it is not in the queue so we are going to visit it.
                  ProductsQueue.Enqueue(prod);
          }

     }
     //now visitedProducts contains all the products that are related (somehow :P) to your first product

     visitedProducts.remove(this);// to remove the main product from the results
     //all what is left to do is to take the top N products.
     return visitedProducts.OrderByDescending(x => x.Rating).Take(N).ToList();
}

Я попытался сделать это как можно проще;)

Ответ 4

Вам просто нужно LINQ. Сначала выберите все данные со всеми вашими условиями, а затем в конце используйте .Take(N). Ваша проблема будет решена.:)