如何使用 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;
}
}
我认为你可以简化你的递归方法。不必将报告级别作为输入参数传递。
考虑到您为示例输入和相关图表提供的预期输出,我想说实现逻辑可以用如下文字表示:
- 如果没有人向员工
X
汇报,那么员工X
的汇报级别为0
- 否则,员工
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 |
我有一个 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;
}
}
我认为你可以简化你的递归方法。不必将报告级别作为输入参数传递。
考虑到您为示例输入和相关图表提供的预期输出,我想说实现逻辑可以用如下文字表示:
- 如果没有人向员工
X
汇报,那么员工X
的汇报级别为0
- 否则,员工
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 |