【问题标题】:How to modify list-indexes according to a list which contains only the changed indexes?如何根据仅包含已更改索引的列表修改列表索引?
【发布时间】:2017-03-10 03:52:18
【问题描述】:

更新:这里是完整代码https://dotnetfiddle.net/eAeWp5

这比我想象的要困难得多。 在实际项目中,我需要更新一个数据库表,其中包含一个列Position(用于排序顺序),但所有方法获取的是一个列表,其中仅包含具有新位置的已更改对象。表和类是WatchList

这里是:

public class WatchList : IEquatable<WatchList>
{
    public WatchList(int id)
    {
        Id = id;
    }

    public int Id { get; }

    public string Name { get; set; }

    public int UserId { get; set; }

    public int Position { get; set; }

    public bool Equals(WatchList other)
    {
        if (other == null) return false;
        if (ReferenceEquals(this, other)) return true;
        return this.Id == other.Id;
    }

    public override bool Equals(object obj)
    {
        WatchList other = obj as WatchList;
        return this.Equals(other);
    }

    public override int GetHashCode()
    {
        return this.Id;
    }

    public override string ToString()
    {
        return $"WatchListId:{Id} Name:{Name} UserId:{UserId} Position:{Position}";
    }
}

所以WatchListId 是主键,Position 是我要更新的列。

考虑该表包含以下监视列表:

WatchListId   Position
1             1
2             2
3             3
4             4
5             5

用户想修改订单,拖拽,最后提交给服务器。客户端将调用 UpdateWatchListsSort 并使用仅包含用户移动的 WatchLists 的列表。

考虑用户移动

1   --->   5
3   --->   1
5   --->   4

所以数据库中的新(正确)顺序是:

WatchListId   Position
3             1
2             2
4             3
5             4
1             5

您注意到,即使是其他一些观察列表也必须更新,因为如果它们的位置受到影响,位置需要向上移动 1。这是它变得棘手。所有未移动到位置的项目应保持稳定的顺序(Position)。在这种情况下,ID=2 和 ID=4 应该保持这个顺序。

样本:

class Program
{
    static void Main(string[] args)
    {
        var changedWatchLists = new List<WatchList>
        {
            new WatchList(1) {Position = 5}, new WatchList(3) {Position = 1}, new WatchList(5) {Position = 4}
        };
        WatchList.UpdateWatchListsSort("123", changedWatchLists);
    }
}

我的方法是首先加载完整的List&lt;WatchList&gt;(来自数据库),然后将其与具有新职位的传递列表合并。这样可以在之前验证输入,并且应该使其更简单,因为所有操作都可以在内存中完成。

基本逻辑是将Remove从完整列表中全部更改为WatchLists,然后将Insert放在所需位置。

我只列举了按新职位排序的更改列表以避免副作用。否则List.Insert 可以向上移动已经有目标位置的项目。

但是,最后我仍然有物品在错误的位置,所以我被卡住了。

完整方法UpdateWatchListsSort

public static void UpdateWatchListsSort(string userId, List<WatchList> watchListsWithModifiedPosition)
{
    List<WatchList> allUserWatchLists = GetWatchListsFromDb(userId);
    // mapping WatchListId --> WatchList (from DB)
    Dictionary<int, WatchList> dbWatchListIdLookup = allUserWatchLists.ToDictionary(w => w.Id);

    if (watchListsWithModifiedPosition.Count == allUserWatchLists.Count)
        allUserWatchLists = watchListsWithModifiedPosition;
    else
    {
        // enumerate all modified WatchLists ordered by position ascending (to avoid side affects)
        foreach (WatchList modified in watchListsWithModifiedPosition.OrderBy(w => w.Position))
        {
            WatchList dbWatchList = dbWatchListIdLookup[modified.Id];
            int newIndex = modified.Position - 1;
            int oldIndex = allUserWatchLists.IndexOf(dbWatchList); // might be at a different position meanwhile( != db-position )
            allUserWatchLists.RemoveAt(oldIndex);
            // if moved forwards index is index-1 because the watchlist was already removed at List.RemoveAt, 
            // if moved backwards index isn't affected
            bool movedForwards = newIndex > oldIndex;
            if (movedForwards)
                newIndex--;
            allUserWatchLists.Insert(newIndex, dbWatchList);
        }
    }

    var changeInfos = allUserWatchLists
        .Select((wl, index) => new { WatchList = wl, NewPosition = index + 1 })
        .Where(x => x.WatchList.Position != x.NewPosition)
        .ToList();
    foreach (var change in changeInfos)
    {
        WatchList wl = change.WatchList;
        wl.Position = change.NewPosition;
        // check if the new position is equal to the position given as parameter
        Debug.Assert(wl.Position == watchListsWithModifiedPosition
           .Where(w => w.Id == wl.Id)
           .Select(w => w.Position)
           .DefaultIfEmpty(wl.Position)
           .First());
    }
    // check if allUserWatchLists contains duplicate Positions which is invalid
    Debug.Assert(allUserWatchLists
       .Select(w => w.Position)
       .Distinct().Count() == allUserWatchLists.Count);

    // update changeInfos.Select(x => x.WatchList) via table-valued-parameter in DB (not related) .....
}

private static List<WatchList> GetWatchListsFromDb(string userId)
{
    var allDbWatchLists = new List<WatchList>
    {
        new WatchList(1) {Position = 1}, new WatchList(2) {Position = 2}, new WatchList(3) {Position = 3},
        new WatchList(4) {Position = 4}, new WatchList(5) {Position = 5}
    };
    return allDbWatchLists;
}

如果您执行此示例,此 Debug.Assert 将失败:

// check if the new position is equal to the position given as parameter
Debug.Assert(wl.Position == watchListsWithModifiedPosition
    .Where(w => w.Id == wl.Id)
    .Select(w => w.Position)
    .DefaultIfEmpty(wl.Position)
    .First());

所以算法是错误的,因为 WatchList 新的 Position 不是所需的(作为参数给出)。

我希望你理解这个要求,看看我做错了什么。我怀疑这部分但不知道如何修复它:

 // if moved forwards index is index-1 because the watchlist was already removed at List.RemoveAt, 
// if moved backwards index isn't affected
bool movedForwards = newIndex > oldIndex;
if (movedForwards)
    newIndex--;

也许你有更好的方法,可读性很重要。

【问题讨论】:

  • 为了清楚一点:你得到用户实际执行的操作列表,按照他执行的顺序?例如,如果表中有 ID [1000,2000],并且得到 [2000->1, 1000->1],那么表应该保持不变吗?
  • @MattTimmermans:我得到了WatchList 的列表,其中仅包含位置已更改但不包含受影响的位置(Fe WatchList 从 2->1 更改,因此 WatchList 1 必须向上移动到 2,在这种情况下,我只得到 WatchList ID=2 Position=1)。这是为了减少网络流量。否则,如果将最后一个移到第一个位置,则必须传递所有 WatchList(可能是 10000 个)。

标签: c# algorithm list sorting


【解决方案1】:

我建议使用插入排序算法原理。该算法的步骤是:

  1. 获取原始对象列表 (original) 并输入对象 (input)
  2. 丢弃originalinput 中的所有对象。通过Position 字段订购其余部分。致电此新列表ordered
  3. 对于输入中的每个对象,找到将其放入 ordered 的位置并将其放置在那里

最后你会得到一个正确排序的对象列表,但是位置已经过时了。但是位置现在对应于ordered列表中对象的索引,所以这很容易解决。

代码来说明我的意思。我做了一些简化的定义,很简短:

class WatchList
{
    public int WatchListId;
    public int Position;
}

List<WatchList> original = new List<WatchList>
{
    new WatchList{WatchListId=1, Position=1},
    new WatchList{WatchListId=2, Position=2},
    new WatchList{WatchListId=3, Position=3},
    new WatchList{WatchListId=4, Position=4},
    new WatchList{WatchListId=5, Position=5}
};

List<WatchList> input = new List<WatchList>
{
    new WatchList{WatchListId=1, Position=5},
    new WatchList{WatchListId=3, Position=1},
    new WatchList{WatchListId=5, Position=4}
};

现在算法是这样的:

List<WatchList> ordered = original.Where(w => !input.Any(iw => iw.WatchListId == w.WatchListId)).OrderBy(w => w.Position).ToList();
foreach (var inputWatchlist in input)
{
    int indexToInsert = 0;
    while (indexToInsert < ordered.Count)
    {
        if (ordered[indexToInsert].Position <= inputWatchlist.Position)
        {
            indexToInsert++;
        } 
        else
        {
            break;
        }
    }

    ordered.Insert(indexToInsert, inputWatchlist);
}

这个输出

foreach (var w in ordered)
{
    Console.WriteLine("Id: " + w.WatchListId + " P: " + w.Position);
}

Id: 3 P: 1
Id: 2 P: 2
Id: 4 P: 4
Id: 5 P: 4
Id: 1 P: 5

小提琴示例链接:https://dotnetfiddle.net/7MtjVZ

如您所见,对象按预期排序,位置不合适。然而,现在更新位置是微不足道的。

【讨论】:

  • 这错过了主要的困难,新的位置会影响原始列表的位置。因此,如果我将一个项目从 3 移动到 1,则项目 1 也会受到影响,因为它必须移动到 2(如果该位置尚未被另一个想要移动到 2 的位置占据)。极端的例子:你将最后一个项目移动到第一个位置。输入列表仅包含这一项,但所有其他项也会受到影响,因为它们必须向上移动。这种上移操作也会影响已经在所需位置的物品,这就是我首先按位置订购的原因。
  • @TimSchmelter,我认为它不会错过这一点。请注意,我们实际上并不太担心Position 字段中的内容,因为在ordered 列表中,项目的索引是它的位置。即一旦ordered 完成item ordered[i] 的位置是i。因此,如果所有项目都必须向上移动 - 它们会向上移动,最后我们会重新计算它们的位置。
  • @TimSchmelter,至于按位置排序-公平点,我在代码中确实有这个但忘记在描述中提及。更新
  • 谢谢安德烈。首先从完整列表中删除所有更改的监视列表确实要简单得多,以避免您必须在循环中删除它们而导致其他人的位置发生变化。
  • 不过,我目前使用的是Matt Timmermans approach的第二版
【解决方案2】:

您的算法几乎可以工作,但您需要先删除所有旧的监视列表,然后然后将它们重新插入到新位置。

按照目前的编写方式,在位置 2 插入一个新的 dbWatchList 后,您可以从位置 1 移除一个 dbWatchList,这将改变插入的观察列表的位置。

修正后的函数如下所示:

public static void UpdateWatchListsSort(string userId, List<WatchList> watchListsWithModifiedPosition)
{
    var modifiedIds = new HashSet<int>(watchListsWithModifiedPosition.Select( w=>w.Id ));

    List<WatchList> allUserWatchLists = GetWatchListsFromDb(userId);

    var modifiedWatchLists = allUserWatchLists.FindAll(w => modifiedIds.Contains(w.Id)).ToDictionary(w => w.Id);

    allUserWatchLists.RemoveAll( w => modifiedIds.Contains(w.Id));

    foreach (WatchList modified in watchListsWithModifiedPosition.OrderBy(w => w.Position))
    {
        int newIndex = modified.Position - 1;
        allUserWatchLists.Insert(newIndex, modifiedWatchLists[modified.Id]);
    }

    //... Your testing and Position fix-up code ...
}

请注意,以上是 O(N^2) 算法,因为它插入到列表的中间。像这样创建一个新列表实际上要快得多:

public static void UpdateWatchListsSort(string userId, List<WatchList> watchListsWithModifiedPosition)
{
    var modifiedIds = new HashSet<int>(watchListsWithModifiedPosition.Select( w=>w.Id ));

    List<WatchList> allUserWatchLists = GetWatchListsFromDb(userId);

    var modifiedWatchLists = allUserWatchLists.FindAll(w => modifiedIds.Contains(w.Id)).ToDictionary(w => w.Id);

    var newList = new List<WatchList>();
    var unmodifiedIter = allUserWatchLists.FindAll(w => !modifiedIds.Contains(w.Id)).GetEnumerator();

    foreach (WatchList modified in watchListsWithModifiedPosition.OrderBy(w => w.Position))
    {
        int newIndex = modified.Position - 1;
        while(newList.Count < newIndex && unmodifiedIter.MoveNext())
            newList.Add(unmodifiedIter.Current);

        newList.Add(modifiedWatchLists[modified.Id]);
    }
    while(unmodifiedIter.MoveNext())
        newList.Add(unmodifiedIter.Current);

    allUserWatchLists = newList;

    //... Your testing and Position fix-up code ...
}

【讨论】:

  • 我不确定我是否理解。 foreach 确实 在将 WatchList 插入目标索引之前先删除它:allUserWatchLists.RemoveAt(oldIndex);。请注意,我不添加新的监视列表。作为参数传递的List&lt;WatchList&gt;allUserWatchLists 的子集。我在.net fiddle 处添加了代码(没有断言,但有console.output)
  • @TimSchmelter 在执行任何插入操作之前,您必须从旧位置删除所有监视列表。 然后您可以将它们插入到它们的新位置,确信它们不会被移动。
  • @TimSchmelter 我在答案中添加了更正的函数。
  • 谢谢。我还没有检查它,但我认为它正在工作。它和 Andreis 的算法一样,不是吗?没错,我最初的尝试失败了,因为枚举期间的删除也会更改列表。
  • 再次感谢,无论如何我都会接受 Andreis,因为他是第一个提到我应该先删除所有已更改的监视列表,然后再将它们插入正确位置的人。但我会使用你的第二种方法:)
【解决方案3】:

这个问题确实很难——我最初的尝试完全错误。

这是我的第二次尝试 - IMO 一种非常有效的算法,基于两个有序序列的修改合并(在代码中注释):

public static void UpdateWatchListsSort(string userId, List<WatchList> watchListsWithModifiedPosition)
{
    // Get the original ordered sequence
    var oldSeq = GetWatchListsFromDb(userId);
    // Create sequence with elements to be modified (ordered by the new position)
    var modifiedSeq = watchListsWithModifiedPosition.OrderBy(e => e.Position);
    // Extract ordered sequence with the remaining elements (ordered by the original position) 
    var otherSeq = oldSeq.Except(watchListsWithModifiedPosition);
    // Build the new ordered sequence by merging the two 
    var newSeq = new List<WatchList>(oldSeq.Count);
    using (var modifiedIt = modifiedSeq.GetEnumerator())
    using (var otherIt = otherSeq.GetEnumerator())
    {
        var modified = modifiedIt.MoveNext() ? modifiedIt.Current : null;
        var other = otherIt.MoveNext() ? otherIt.Current : null;
        while (modified != null || other != null)
        {
            if (modified != null && modified.Position == newSeq.Count + 1)
            {
                newSeq.Add(modified);
                modified = modifiedIt.MoveNext() ? modifiedIt.Current : null;
            }
            else
            {
                newSeq.Add(other);
                other = otherIt.MoveNext() ? otherIt.Current : null;
            }
        }
    }
    // Here the new sequence elements are in the correct order
    // Update the Position field and populate a list 
    // with the items that need db update
    var updateList = new List<WatchList>();
    for (int i = 0; i < newSeq.Count; i++)
    {
        var item = newSeq[i];
        if (item.Id == oldSeq[i].Id) continue;
        item.Position = i + 1;
        updateList.Add(item);
    }
}

或更紧凑的版本使用 LINQ Zip:

public static void UpdateWatchListsSort(string userId, List<WatchList> watchListsWithModifiedPosition)
{
    // Get the original ordered sequence
    var oldSeq = GetWatchListsFromDb(userId);
    // Build the new ordered sequence
    var newSeq = new WatchList[oldSeq.Count];
    // Place the modified elements in their new position
    foreach (var item in watchListsWithModifiedPosition)
        newSeq[item.Position - 1] = item;
    // Place the remaining elements in the free slots, keeping the original order
    var remainingSeq = Enumerable.Range(0, newSeq.Length)
        .Where(index => newSeq[index] == null)
        .Zip(oldSeq.Except(watchListsWithModifiedPosition), (index, item) => new { index, item });
    foreach (var entry in remainingSeq)
        newSeq[entry.index] = entry.item;
    // Update the Position field and populate a list with the items that need db update
    var updateList = new List<WatchList>();
    for (int i = 0; i < newSeq.Length; i++)
    {
        var item = newSeq[i];
        if (item.Id == oldSeq[i].Id) continue;
        item.Position = i + 1;
        updateList.Add(item);
    }
}

最后,我得到了一个简单的 LINQ:

public static void UpdateWatchListsSortB(string userId, List<WatchList> modifiedList)
{
    var originalList = GetWatchListsFromDb(userId);
    var updateList = modifiedList
        .Concat(Enumerable.Range(1, originalList.Count).Except(modifiedList.Select(e => e.Position))
        .Zip(originalList.Except(modifiedList), (pos, e) => e.Position == pos ? e : new WatchList(e.Id) { Position = pos }))
        .Where(e => e.Id != originalList[e.Position - 1].Id)
        .ToList();
}

【讨论】:

  • 谢谢。由于我的头已经在旋转,我稍后或明天会看看它。不需要初始排序,因为它已经通过ORDER BY 按位置排序。 Andrei 的方法似乎已经奏效了。
  • @TimSchmelter 我的头已经晕了 - 不可能,我不敢相信 :)
  • 但是,它需要一些返工,因为使用不同样本的快速测试返回了 itemList 中的错误位置。需要看看它。我认为如果watchListsWithModifiedPosition 没有按位置排序(这是正常情况),则会出现问题。首次测试:new WatchList(4) {Position = 5}, new WatchList(2) {Position = 1}, new WatchList(1) {Position = 4}
  • 感谢伊万的努力。我会接受 Andrei 的观点,他首先提到了简单地从完整列表中删除所有已更改的内容的方法。然后插入会容易得多,因为如果您在循环中删除它们,您可以避免更改位置的问题。 Matt Timmermann 使用了类似的方法。
  • 不客气@Tim,这是一个有趣的挑战。接受 Andrei 的答案没有问题,但我建议您使用我的第二种方法,即 O(N) 而 Andrei 的方法是 O(N*M),并且当 M 相对较大时(可能不是典型情况)确实执行缓慢。干杯。
【解决方案4】:

大约一周前,我遇到了与网格视图中的优先级相关的类似挑战。最终对我有用的算法如下:

      foreach (GridViewRow gvr in gvSerials.Rows)
            {
                //Moved record up
                if (Priority < e.RowIndex + 1)
                {
                    //Greater than priority but less than index - Increase Prioirty
                    if (gvr.RowIndex + 1 >= Priority && gvr.RowIndex < e.RowIndex)
                        DAL.UpdatePriority(gvr.RowIndex + 2, int.Parse(gvSerials.DataKeys[gvr.RowIndex]["SerialID"].ToString()));
                }
                else if (Priority > e.RowIndex + 1)
                {
                    if (gvr.RowIndex > e.RowIndex)
                    {
                        if (gvr.RowIndex + 1 <= Priority)
                            DAL.UpdatePriority(gvr.RowIndex, int.Parse(gvSerials.DataKeys[gvr.RowIndex]["SerialID"].ToString()));
                    }
                }
            }

我决定让用户移动优先顺序,然后在提交后对数据库进行更改,而不是尝试维护更改列表,并且只在最后提交整个列表。

我使用了行的rowindex和优先级来得到想要的结果。

我不相信我的做法是正确或最有效的方法,但也许它会让你想到一些你还没有想到的东西。

【讨论】:

  • 谢谢。但我不认为它可以帮助我,因为这是一个不同的要求。我有一个仅包含新位置的列表(例如,用户将项目从 3 拖放到 1)。但是这个动作会影响更多的项目(在这个例子中,1 到 3 之间的所有项目都必须向上移动 1)。这些变化不在列表中,必须在不破坏作为参数给出的位置的情况下计算。因此,目标列表仅包含更改,但包含受影响项目的完整列表。
【解决方案5】:

根据您所说的,由于您在该方法中使用的信息有限,您需要执行一组级联的提取。首先 fetch 获取该仓位的现有持有者,这样当你换出时,你可以将原来的持有者分配给新的仓位。

然后您必须获取下一组受影响的持仓者,并重复此过程。从本质上讲,这将成为一种泡沫式的事情。不幸的是,性能不是很好,因为后端需要所有往返。

另一种方法是将所有位置保存在内存中,并跳过往返。您仍然需要遍历所有受影响的位置,但由于整个列表已在内存中展开,您可以跳过往返。恕我直言,仍然受限于冒泡式计算。

【讨论】:

  • 如果您可以尝试修改sample code 以提供示例,那就太好了。我的方法已经将所有内容加载到内存中(不是问题并且需要验证输入(上面省略)。但是仍然很难找到所有受影响的项目并使用正确的新位置更新它们。请注意,位置基本上是列表- index +1。因此,如果列表正确排序,则所有 WatchLists 都在正确的位置,并且我有正确的结果。
猜你喜欢
  • 2017-04-18
  • 2017-12-12
  • 1970-01-01
  • 2022-08-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-30
  • 2017-09-05
相关资源
最近更新 更多