如何根据仅包含已更改索引的列表修改列表索引?

时间:2016-10-27 12:48:40

标签: c# algorithm list sorting

更新:以下是完整代码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列。

请考虑此表包含以下WatchLists:

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

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

考虑移动用户

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

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

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

您注意到即使其他一些WatchLists也必须更新,因为如果他们的位置受到影响,这些位置需要向上移动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<WatchList>(来自数据库),然后将其与传递的列表合并到新的位置。这样可以在之前验证输入,并且应该使其更简单,因为所有这些都可以在内存中完成。

基本逻辑是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());

因此算法错误,因为WatchListPosition不是所需的{(1)作为参数。

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

 // 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--;

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

5 个答案:

答案 0 :(得分:1)

您的算法几乎可以使用,但您需要首先删除所有旧观察列表,然后然后将它们重新插入新位置。

当前编写的方式,您可以在在位置2插入新的位置后从位置1 中删除dbWatchList,这将更改插入的监视列表的位置。 / p>

更正的功能如下所示:

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 ...
}

答案 1 :(得分:1)

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

  1. 获取原始对象列表(original)和输入的对象(input
  2. 丢弃original中位于input的所有对象。通过Position字段订购其余部分。请拨打此新列表ordered
  3. 对于输入中的每个对象,找到将其放在ordered中的位置并将其放在那里
  4. 最后,您将获得正确订购的对象列表,但位置已过期。但是,position现在对应于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

    正如您所见,对象按预期排序,位置不合适。然而,更新职位现在变得微不足道了。

答案 2 :(得分:1)

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

这是我的第二次尝试 - 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();
}

答案 3 :(得分:0)

我在大约一周左右的时候遇到过与网格视图中的优先级相关的类似挑战。最终为我工作的算法如下:

      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和优先级来获得所需的结果。

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

答案 4 :(得分:0)

根据您所说的内容,由于您在该方法中使用的信息有限,您需要执行级联的提取集。首先获取现有的头寸持有者,这样当您换出时,您可以将原始持有者分配到新头寸。

然后你必须获取下一组受影响的持仓者,并重复这个过程。从本质上讲,这将成为一种泡沫排序的东西。遗憾的是,由于后端需要进行所有往返,因此效率不高。

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