Hôm nay tôi sẽ thực hiện một phương pháp để đi qua một đồ thị sâu tùy ý và làm phẳng nó thành một số đếm đơn. Thay vào đó, tôi đã làm một chút tìm kiếm đầu tiên và thấy điều này:Duyệt đồ thị hiệu quả với LINQ - loại bỏ đệ quy
public static IEnumerable<T> Traverse<T>(this IEnumerable<T> enumerable, Func<T, IEnumerable<T>> recursivePropertySelector)
{
foreach (T item in enumerable)
{
yield return item;
IEnumerable<T> seqRecurse = recursivePropertySelector(item);
if (seqRecurse == null) continue;
foreach (T itemRecurse in Traverse(seqRecurse, recursivePropertySelector))
{
yield return itemRecurse;
}
}
}
Về lý thuyết này có vẻ tốt, nhưng trong thực tế tôi đã tìm thấy nó thực hiện đáng kể kém hơn sử dụng mã viết tay tương đương (như tình hình phát sinh) để đi qua một đồ thị và làm bất cứ điều gì cần phải được thực hiện. Tôi nghi ngờ điều này là bởi vì trong phương pháp này, cho mỗi mục nó trả về, ngăn xếp phải thư giãn đến một mức độ tùy ý sâu.
Tôi cũng nghi ngờ rằng phương pháp này sẽ chạy hiệu quả hơn nhiều nếu đệ quy bị loại bỏ. Tôi cũng tình cờ không giỏi loại bỏ đệ quy.
Có ai biết cách viết lại phương pháp này để loại bỏ đệ quy không?
Cảm ơn bạn đã được trợ giúp.
EDIT: Cảm ơn rất nhiều vì tất cả các câu trả lời chi tiết. Tôi đã thử benchmarking giải pháp ban đầu so với giải pháp của Eric vs không sử dụng một phương pháp đếm và thay vì đệ quy đi qua với một lambda và kỳ quặc, đệ quy lambda nhanh hơn đáng kể so với một trong hai phương pháp kia.
class Node
{
public List<Node> ChildNodes { get; set; }
public Node()
{
ChildNodes = new List<Node>();
}
}
class Foo
{
public static void Main(String[] args)
{
var nodes = new List<Node>();
for(int i = 0; i < 100; i++)
{
var nodeA = new Node();
nodes.Add(nodeA);
for (int j = 0; j < 100; j++)
{
var nodeB = new Node();
nodeA.ChildNodes.Add(nodeB);
for (int k = 0; k < 100; k++)
{
var nodeC = new Node();
nodeB.ChildNodes.Add(nodeC);
for(int l = 0; l < 12; l++)
{
var nodeD = new Node();
nodeC.ChildNodes.Add(nodeD);
}
}
}
}
nodes.TraverseOld(node => node.ChildNodes).ToList();
nodes.TraverseNew(node => node.ChildNodes).ToList();
var watch = Stopwatch.StartNew();
nodes.TraverseOld(node => node.ChildNodes).ToList();
watch.Stop();
var recursiveTraversalTime = watch.ElapsedMilliseconds;
watch.Restart();
nodes.TraverseNew(node => node.ChildNodes).ToList();
watch.Stop();
var noRecursionTraversalTime = watch.ElapsedMilliseconds;
Action<Node> visitNode = null;
visitNode = node =>
{
foreach (var child in node.ChildNodes)
visitNode(child);
};
watch.Restart();
foreach(var node in nodes)
visitNode(node);
watch.Stop();
var lambdaRecursionTime = watch.ElapsedMilliseconds;
}
}
Trường hợp TraverseOld là phương thức gốc, TraverseNew là phương thức của Eric và rõ ràng lambda là lambda.
Trên máy của tôi, TraverseOld mất 10127 ms, TraverseNew mất 3038 ms, đệ trình lambda mất 1181 ms.
Đây có phải là điển hình mà phương pháp liệt kê (với lợi tức lợi nhuận) có thể mất 3X miễn là trái ngược với thực hiện ngay lập tức không? Hoặc là một cái gì đó khác đang xảy ra ở đây?
Thoạt nhìn, nó trông giống như phương pháp này được recursing nhân ở mọi cấp độ, tức là, giống như một định dạng ngây thơ 'f (x) = f (x - 1) + f (x - 2) '. – mellamokb
Phiên bản cuối cùng không hoạt động ở bất kỳ nơi nào gần cùng một lượng công việc như hai phiên bản đầu tiên; đặc biệt, nó không phân bổ bất kỳ danh sách nào, sao chép các mục từ mảng sang mảng, v.v. Hãy tưởng tượng bạn là người điều tra dân số; nếu bạn quyết định chỉ đơn giản là đi bộ từ nhà này sang nhà khác và không bận tâm, bạn biết đấy, hãy viết ra bất kỳ thông tin nào, thì công việc của bạn sẽ nhanh hơn rất nhiều. –
Tôi có thể tìm TraverseOld và TraverseNew 3 năm sau ở đâu? – user1756694