简体   繁体   English

在Linq中通过单链接实体列表自定义订单

[英]Custom OrderBy in Linq with singly linked entity list

I'm trying to come up with an efficient solution to be able to query the entity list and order it correctly. 我正在尝试提出一种有效的解决方案,以便能够查询实体列表并正确地对其进行排序。 I have created a singly linked list type structure in SQL DB schema. 我在SQL DB模式中创建了一个单链列表类型结构。 I am using GUID as my IDs but for simplicity, I'll use int here. 我使用GUID作为我的ID,但为简单起见,我将在这里使用int。 I could solve this problem easily by having a SortOrder column on the DB but because of other requirements, this is how I have to implement this table. 我可以通过在数据库上有一个SortOrder列来轻松解决此问题,但是由于其他要求,这就是我必须实现此表的方式。

I have a table structure that looks like the following entity model: 我有一个表结构,看起来像下面的实体模型:

public class Record
{
    public int ID;
    public string Name;
    public int? ChildID;  //References the next record
}

My initial thought is to create a partial class like the following: 我最初的想法是创建一个如下的局部类:

public partial class Record
{
    public int SortOrder
    {
        get
        {
           //query table and loop through the entire list building it from
           //the bottom and keeping count of the integer sort order and return 
           //the one specific to this record
        }
    }
}

However, this seems very inefficient to have to query the entire list every time and iterate through to find the SortOrder. 但是,这似乎效率很低,每次必须查询整个列表并进行遍历才能找到SortOrder。 Is there anything else I can leverage like a custom OrderBy function or anything? 还有什么可以像自定义OrderBy函数那样利用? I'm trying sort by the order that would be created when iterating a building the list. 我正在尝试按迭代构建列表时创建的顺序进行排序。 For instance, the record with ChildID = null, is the last one in the list, since it does not have a child. 例如,ChildID = null的记录是列表中的最后一条,因为它没有子记录。 I'll start with that record, then get the next one above it that references the previous as its ChildID and go until there is no more in the list that has a reference to ID, which should be when the list is complete and ordered correctly. 我将从该记录开始,然后获取其上方的下一个记录,该记录将前一个记录作为其ChildID,然后进行操作,直到列表中没有引用ID的列表为止,该列表应该是完整且正确排序的。 No two records have the same ChildID. 没有两个记录具有相同的ChildID。

If I had the following 3 records in a list, 如果我在列表中有以下3条记录,

ID = 3,  Name = "Apple",  ChildID = 6,
ID = 54, Name = "Orange", ChildID = 3,
ID = 6,  Name = "Banana", ChildID = null

Then I would expect to get Orange, Apple, Banana, in that order. 然后,我期望依次获得Orange,Apple,Banana。

One way to do it would be to write a method that will return a list in sorted order. 一种实现方法是编写一个将以排序顺序返回列表的方法。 You would first find the record with ChildId == null , add it to the results list, and then continue to search for items where item.ChildId == previousItem.Id , and then insert them at the beginning of the list: 您将首先找到带有ChildId == null的记录,将其添加到结果列表中,然后继续搜索item.ChildId == previousItem.Id项目,然后将其插入列表的开头:

private static IEnumerable<Record> OrderRecords(IReadOnlyCollection<Record> records)
{
    // "Exit fast" checks
    if (records == null) return null;
    if (records.Count < 2) return records.ToList();

    // Get last record and add it to our results
    Record currentRecord = records.Single(r => r.ChildID == null);
    var results = new List<Record> {currentRecord};

    // Keep getting the parent reference to the previous record 
    // and insert it at the beginning of the results list
    while ((currentRecord = records.SingleOrDefault(r => 
        r.ChildID == currentRecord.ID)) != null)
    {
        results.Insert(0, currentRecord);
    }

    return results;
}

In use, this would look something like: 在使用中,这看起来像:

private static void Main()
{
    var records = new List<Record>
    {
        new Record {ID = 3, Name = "Apple", ChildID = 6},
        new Record {ID = 54, Name = "Orange", ChildID = 3},
        new Record {ID = 6, Name = "Banana", ChildID = null}
    };

    var sortedRecords = OrderRecords(records);

    Console.WriteLine(string.Join(", ", sortedRecords.Select(r => r.Name)));

    Console.Write("\nPress any key to exit...");
    Console.ReadKey();
}

Output 输出量

在此处输入图片说明

Given that the record ID order is random, and assuming that the List you are ordering is complete, or that you won't run out of memory/time if you have to scan the entire table to order the list, I think the best you can do is compute the depth for a Record and cache the results: 鉴于记录ID的顺序是随机的,并且假设您要排序的List是完整的,或者如果您必须扫描整个表来对列表进行排序,则不会耗尽内存/时间,我认为最好可以做的是计算Record的深度并缓存结果:

I am using the List as the table, but you could use the table instead if the list you want to order is incomplete: 我使用List作为表,但是如果要订购的列表不完整,则可以使用表:

public partial class Record {
    static Dictionary<int, int> depth = new Dictionary<int, int>();
    public int Depth(List<Record> dbTable) {
        int ans = 0;

        var working = new Queue<int>();
        var cur = this;
        do {
            if (depth.TryGetValue(cur.ID, out var curDepth)) {
                ans += curDepth;
                break;
            }
            else {
                working.Enqueue(cur.ID);
                cur = dbTable.FirstOrDefault(r => r.ChildID == cur.ID);
                if (cur != null)
                    ++ans;
            }
        } while (cur != null);

        var workAns = ans;
        while (working.Count > 0) {
            var workingID = working.Dequeue();
            depth.Add(workingID, workAns);
            --workAns;
        }

        return ans;
    }
}

Update: I re-wrote the code to use a specific queue; 更新:我重新编写了代码以使用特定的队列; my first version was recursive and that was straightforward but risked overflowing the stack and my second version didn't cache the intermediate results when following the linked list which wasn't very efficient. 我的第一个版本是递归的,很简单,但是有堆栈溢出的风险,而我的第二个版本在遵循效率不高的链表时没有缓存中间结果。 Using a queue of the intermediate IDs ensures I only follow a particular chain depth once. 使用中间ID的队列可确保我只跟踪一次特定的链深度。

Now that you have a Depth method, sorting is easy: 现在有了Depth方法,排序很容易了:

var ans = work.OrderBy(w => w.Depth(work));

The best algorithm for this task is to prepare a fast lookup data structure (like Dictionary ) of Record by ChildID . 这项任务的最佳算法是通过ChildID准备Record的快速查找数据结构(如Dictionary )。 Then the ordered result can be produced backwards starting with ChildID = null and using the record ID to find the previous record. 然后可以从ChildID = null并使用记录ID查找先前的记录来向后产生排序结果。

Since the hash lookup time complexity is O(1), the time complexity of the algorithm is linear O(N) - the fastest possible. 由于哈希查找时间复杂度为O(1),因此算法的时间复杂度为线性O(N)-可能最快。

Here is the implementation: 这是实现:

static Record[] Ordered(IEnumerable<Record> records)
{
    var recordByNextId = records.ToDictionary(e => e.ChildID.Wrap());
    var result = new Record[recordByNextId.Count];
    int? nextId = null;
    for (int i = result.Length - 1; i >=0; i--)
        nextId = (result[i] = recordByNextId[nextId]).ID;
    return result;
}

The explanation of e.ChildID.Wrap() custom extension method. e.ChildID.Wrap()自定义扩展方法的说明。 I wish I can use simply e.ChildID , but the BCL Dictionary class throws annoying exception for null key. 我希望我可以简单地使用e.ChildID ,但是BCL Dictionary类会为null键抛出恼人的异常。 To overcome that limitation in general, I use a simple wrapper struct and "fluent" helper: 通常,要克服该限制,我使用一个简单的包装器struct和“流畅的”帮助器:

public struct ValueWrapper<T> : IEquatable<ValueWrapper<T>>
{
    public readonly T Value;
    public ValueWrapper(T value) => Value = value;
    public bool Equals(ValueWrapper<T> other) => EqualityComparer<T>.Default.Equals(Value, other.Value);
    public override bool Equals(object obj) => obj is ValueWrapper<T> other && Equals(other);
    public override int GetHashCode() => EqualityComparer<T>.Default.GetHashCode(Value);
    public static implicit operator ValueWrapper<T>(T x) => new ValueWrapper<T>(x);
    public static implicit operator T(ValueWrapper<T> x) => x.Value;
}

public static class ValueWrapper
{
    public static ValueWrapper<T> Wrap<T>(this T value) => new ValueWrapper<T>(value);
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM