选择相关对象的前N个元素

11
我有一个类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 可以属于产品BRelatedProducts列表,而B 也可以属于产品 ARelatedProducts列表。

有什么好的建议可以以最优化的方式解决这个问题吗?假设我有数百万个产品需要维护,如何递归地遍历所有相关产品并识别已访问过的产品?

我将此标记为C#和Java,因为相同的逻辑可以应用于这两种语言。


1
尝试实现某些可行的解决方案;如果速度不够快,那么您可以考虑优化它。但到目前为止,您还没有展示出解决此问题的任何努力。 - Scott Hunter
1
你在“递归”中是否只深入了一个固定的层级,假设相关产品的相关产品可能与原始产品无关。如果是这样,我认为你走对了路,只需将不同深度的内容合并即可。 - CF5
@ScottHunter,我已经添加了我目前所做的内容。但这不是一个解决方案,因为它最终会导致无限递归。 - Mhd
你能依赖于引用相等性来比较 Product 实例吗? - Ivan Stoev
为了代码方便,我们可以依赖引用相等性。 - Mhd
2
正如Ivan Stoev已经问过的那样,您对赏金有什么期望?已经提出的答案有什么问题吗? - SergGr
4个回答

16
想象一下,我需要维护数百万个产品。如何递归地导航所有相关产品并识别已经访问过的产品?
不必使用递归。显式的StackQueue可以用于导航部分。为了收集结果,可以使用HashSet而不是List。它将有两个目的-允许您跳过已经访问的元素,并消除最后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)其中height是最大深度。
迭代实现的好处是它们当然可以处理比递归解决方案更大的深度,后者可能导致StackOverflowExpection。但是,如果不希望深度太大并且更喜欢递归,则以下是一些递归实现(它们只需要访问relatedSetthis):
使用经典的私有递归方法:
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);
}

使用递归lambda:

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();
}

最后但并非最不重要的,使用最新的C# 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);
    }
}

所有这些方法(在我看来)都最优地处理了收集部分。当然,前N部分不是最优的 - O(N*log(N)),可以像@Amit Kumar的答案中提到的那样进行优化,但这需要实现一个缺失的标准数据结构,这超出了SO答案的范围。

1
@Mhd 你为什么认为深度是2?它是无限的。可能你错过了内部循环,将正在添加的相关产品的RelatedProducts入队。 - Ivan Stoev
1
@Mhd:他使用队列代替递归。关键是使用HashSet来防止重新访问产品。你也可以用递归解决同样的问题。 - Jim Mischel
1
@Mhd:是的,这两种方法都可以。不过我认为提供的队列解决方案更清晰明了。 - Jim Mischel
1
@Mhd:你没有维护一个显式队列,但是你有一个可能非常深的调用堆栈。我认为递归解决方案不太可能具有更好的性能。 - Jim Mischel
1
@Mhd 有什么问题吗?不确定你需要更多的关注。收集部分非常简单和标准。这里没有太多的实现选项 - 要么是递归,要么是迭代。递归可能会导致堆栈溢出,并且性能应该几乎相同,因此我更喜欢迭代。我也可以使用显式的 Stack 和一些递归方式,但除非你有特殊的原因,否则我看不到任何意义,我很想知道。 - Ivan Stoev
显示剩余3条评论

4
我建议使用一个固定大小为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进行迭代。

2

我的解决方案:

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();
}

我尽力使它尽可能简单;)

1
你只需要使用LINQ。首先根据所有条件获取所有数据,然后最后只需使用.Take(N)即可解决您的问题。 :)

3
不,这根本没有解决问题。“先获取所有数据”只是对他指出的具体问题进行了概括。 - Jim Mischel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接