如何使用 LINQ 递归获取员工列表及其报告级别

How to get the list of employees and their reporting level recursively using LINQ

我有一个 table 员工及其主管列表。

| UserID | SupervisorUserId |
|--------|------------------|
| 10     | null             |
| 1      | 10               |
| 2      | 1                |
| 3      | 1                |
| 4      | 3                |
| 5      | 10               |
| 6      | 5                |
| 7      | 6                |
| 8      | 6                |
| 9      | 8                |

这是一个可视化图表:

这是我现在拥有的:

public JsonResult GetEmployeesWithReportingLevel()
{
    try
    {

        var getEmployees = GetEmployees().ToList();

        var employeeList = (from e in getEmployees
                            where e.IsDeleted != true
                            select new ReportingLineList
                            {
                                UserId = e.UserId,
                                SupervisorId = e.SupervisorId 
                            }).ToList();

        var reportingLevel = (from emp in employeeList
                                select new 
                                {
                                    UserId = emp.UserId,
                                    ReportingLevel = GetReportingLevel(employeeList, emp.UserId, 0)
                                }).ToList();

        return new JsonResult(reportingLevel);
    }
    catch
    {
        throw;
    }
}
private int GetReportingLevel(IEnumerable<ReportingLineList> employeeList, Guid UserId, int reportingLevel)
{
    try
    {
        var reportingEmployees = (from emp in employeeList
                                        where emp.SupervisorId == UserId
                                        select new 
                                        {
                                            UserId = emp.UserId
                                        }).ToList();

        foreach (var emp in reportingEmployees)
        {
            if (emp.UserId != null)
            {
                return GetReportingLevel(employeeList, (Guid)emp.UserId, reportingLevel + 1);
            }
        }

        return reportingLevel;
    }
    catch
    {
        throw;
    }
}

我的预期结果应该是:

| UserId | Reporting level |
|--------|-----------------|
| 2      | 0               |
| 4      | 0               |
| 7      | 0               |
| 9      | 0               |
| 3      | 1               |
| 8      | 1               |
| 1      | 2               |
| 6      | 3               |
| 5      | 3               |
| 10     | 4               |

根据我目前的结果,它似乎只是与我的代码将获得的第一个员工进行迭代。

这是我得到的结果。

| UserId | Reporting level |
|--------|-----------------|
| 2      | 0               |
| 4      | 0               |
| 7      | 0               |
| 9      | 0               |
| 3      | 1               |
| 8      | 1               |
| 1      | 1               |
| 6      | 1               |
| 5      | 2               |
| 10     | 3               |

所以我也是新手,但如果我没记错的话,你的问题似乎可以通过添加 .OrderBy() 来解决。

这里有一个 link 可以查看:MSDN Enumerable.OrderBy Method

首先,您说您希望 UserId 6 的报告级别为 3,但如果我理解这个问题,那么数据和图表都建议它应该是 2。

如果这是正确的,那么问题出在您的 GetReportingLevel 方法上。您需要跟踪 currentLevel 和 MaximumReportingLevel。每次下降一个级别时,您都需要增加 currentLevel,然后在完成该级别后再次降低它,这需要独立于 MaximumReportingLevel,只有当它发现比以前更深的级别时才应该改变。

例如,当您处理员工 10 时,您递归地在第 1 层处理员工 1,然后在第 2 层处理员工 2,但是您需要返回到第 1 层处理员工 3,同时不会丢失以下事实你的最高等级是 2。

你的代码也不适合我编译,例如UserID 是 int 还是 Guid 等,但我认为以下内容应该有效 (我故意让你的大部分代码保持原样)

private int GetReportingLevel(IEnumerable<ReportingLineList> employeeList, Guid UserId, 
                              int MaxReportingLevel = 0, int CurrentLevel = 0)
{
    try
    {
        var reportingEmployees = (from emp in employeeList
                                  where emp.SupervisorId == UserId
                                  select new
                                  {
                                      UserId = emp.UserId
                                  }).ToList();

        CurrentLevel++;
        foreach (var emp in reportingEmployees)
        {
            if (emp.UserId != null)
            {
                int newLevel = GetReportingLevel(employeeList, (Guid)emp.UserId,
                                                 MaxReportingLevel , CurrentLevel);
            
                MaxReportingLevel = Math.Max(currentLevel, newLevel);
            }
        }
        CurrentLevel--;

        return MaxReportingLevel;
    }
    catch
    {
        throw;
    }
}

我认为你可以简化你的递归方法。不必将报告级别作为输入参数传递。

考虑到您为示例输入和相关图表提供的预期输出,我想说实现逻辑可以用如下文字表示:

  1. 如果没有人向员工X汇报,那么员工X的汇报级别为0
  2. 否则,员工 X 的报告级别 比直接向员工 X[ 直接报告的所有员工的最高报告级别高一个级别=91=]

根据我对 GetReportingLevel() 的当前实现方式的解释,我相信您正在处理第一条语句和第二条语句的大部分内容,但缺少 最高的 部分在第二份声明中,高于直接向员工 X 报告的所有员工的最高 报告级别。

在伪代码中,我会在递归方法中将两条语句的逻辑体现如下:

int GetReportingLevel(<all employees>, <employee / user ID>)
{
    // Find all the employees in <all employees> that report directly to <employee>

    // If no employees report directly to <employee>:
    // return 0

    // Otherwise:
    // return 1
    //    + the maximum reporting level of all employees that report directly to <employee>
}

我将所有直接向报告的员工称为下属。

此伪代码的 straight-forward 实现是:

private static int GetReportingLevel(IEnumerable<ReportingLineList> employeeList, int userId)
{
    var subordinates = employeeList
        .Where(emp => emp.SupervisorUserId == userId);
    
    if (!subordinates.Any())
    {
        return 0;
    }
    
    return 1 + subordinates.Max(sub => GetReportingLevel(employeeList, sub.UserId));
}

straight-forward 方法的示例 fiddle here


不过,在 straight-forward 实现中,您将基本上遍历员工的整个子树并计算 所有员工后代 的报告级别,以便能够计算 one 员工的报告级别,即使您在计算 another[=90] 之前已经遍历了部分子树(甚至整个子树!) =] 员工的报告级别。

举个例子,借用你的图:

如果从计算 Emp 10 的报告级别开始,您将遍历 整个 树只是为了计算 Emp 10 的报告级别(蓝色区域),因为报告级别是从叶节点向上计算的。当您随后继续计算 Emp 1 的报告级别时,您将重做 计算 [=24] 的报告级别时刚刚进行的 所有递归调用的子集=](橙色区域)。

只要有机会,我们就希望尽量减少递归调用的数量。我建议使用字典来跟踪我们已经计算出的报告级别,并在需要时从字典中简单地获取报告级别,并且已经计算过了。

这种实现的伪代码可以是:

// dictionary with key <user ID> and value <reporting level>

int GetReportingLevel(<all employees>, <employee / user ID>)
{
    // If my dictionary already has an entry for <employee>:
    // return the reporting level from the dictionary

    // Otherwise: Find all the employees in <all employees>
    //    that report directly to <employee>

    // Calculate the reporting level for <employee>
    //    (using the same logic as in the straight-forward implementation)
    //    and add <employee's user ID> and their reporting level to the dictionary

    // return the reporting level that was just calculated for <employee>
}

实现可能如下所示:

private static Dictionary<int, int> _reportingLevelForUserId = new();

private static int GetReportingLevel(IEnumerable<ReportingLineList> employeeList, int userId)
{
    if (_reportingLevelForUserId.TryGetValue(userId, out var reportingLevel))
    {
        return reportingLevel;
    }
    
    var subordinates = employeeList
        .Where(emp => emp.SupervisorUserId == userId);
    
    _reportingLevelForUserId[userId] = subordinates.Any()
        ? 1 + subordinates.Max(sub => GetReportingLevel(employeeList, sub.UserId))
        : 0;
    
    return _reportingLevelForUserId[userId];
}

示例 fiddle 更优化的方法 here


对于您提供的示例输入,straight-forward 方法导致对 GetReportingLevel() 的 31 次调用,而 dictionary-based 方法导致对 GetReportingLevel() 的 19 次调用。对于这么小的一棵树,可能没什么区别;但是树越大,这种优化就越重要。

GetReportingLevel() 的两个实现中,我假定 ReportingLineList class 的以下结构:

public class ReportingLineList
{
    public int UserId { get; set; }
    public int? SupervisorUserId { get; set; }
}

如果输出计算如下:

var employeesWithReportingLevel = employeeList
    .Select(employee => new
        {
            UserId = employee.UserId,
            ReportingLevel = GetReportingLevel(employeeList, employee.UserId)
        })
    .OrderBy(reportingEmployee => reportingEmployee.ReportingLevel)
    .ToList();

,该方法的任一实现都会为给定的输入生成以下输出:

           Input                               Output
=============================        ===========================
                             
| UserID | SupervisorUserId |        | UserId | ReportingLevel |
|--------|------------------|        |--------|----------------|
|     10 |             null |        |      2 |              0 |
|      1 |               10 |        |      4 |              0 |
|      2 |                1 |        |      7 |              0 |
|      3 |                1 |        |      9 |              0 |
|      4 |                3 |        |      3 |              1 |
|      5 |               10 |        |      8 |              1 |
|      6 |                5 |        |      1 |              2 |
|      7 |                6 |        |      6 |              2 |
|      8 |                6 |        |      5 |              3 |
|      9 |                8 |        |     10 |              4 |