多个排序列表的分页

时间:2015-10-17 15:30:26

标签: c# .net algorithm list

我有一些未知数量的有序列表,我需要进行分页。 例如,当页面大小为6时,这3个列表的页面应如下所示。

  • 清单1:01,02,03,04,05,06,07,08,09,10
  • List2:11,12,13,14,15
  • List3:16,17,18,19,20,21,22,23,24,25,26,27,28

结果页面:

  • Page1:01,11,16,02,12,17
  • Page2:03,13,18,04,14,19
  • Page3:05,15,20,06,21,07
  • Page4:22,08,23,09,24,10
  • 第5页:25,26,27,28

在给出页码时,从每个列表(起始索引和项目数)中获取哪些项目的最有效方法是什么?

考虑到每个列表可能有几十万个项目,因此迭代所有这些项目效率不高。

3 个答案:

答案 0 :(得分:0)

我认为可以通过两个步骤很好地完成:

  1. 将您的列表展平为单个列表(按您描述的方式排序)。
  2. 从该列表中取出所需页面的项目。
  3. 要完成第1步,我会执行类似于此处建议的内容:Merging multiple lists

    所以,(假设你的页面项是整数,如你的例子中所示),这是一个很好的方法,可以找到你想要的那个:

        static IEnumerable<int> GetPageItems(IEnumerable<List<int>> itemLists, int pageSize, int page)
        {
            var mergedOrderedItems = itemLists.SelectMany(x => x.Select((s, index) => new { s, index }))
                                              .GroupBy(x => x.index)
                                              .SelectMany(x => x.Select(y => y.s));
    
            // assuming that the first page is page 1, not page 0:
            var startingIndex = pageSize * (page - 1);
    
            var pageItems = mergedOrderedItems.Skip(startingIndex)
                                              .Take(pageSize);
            return pageItems;            
        }
    

    注意 - 您不必担心传入的页面超过了总项目数可能存在的页面总数...感谢Linq的魔力,这个方法只会返回一个空IEnumerable的。同样,如果Take(pageSize)结果小于“pageSize”项,它只返回它找到的项目。

答案 1 :(得分:0)

我不能说它是否是最有效的方式,但这里有一个 O(M * Log2(M))时间复杂度的算法,其中 M 列表的编号。它的工作原理如下。输入集按照项Count按升序进行分组和排序,迭代直到有效起始索引适合当前范围,跳过先前的范围。这是可能的,因为在每一步我们都知道它是最小计数,因此所有剩余的列表都包含该范围内的项目。完成后,我们从剩余的列表中发出页面项。

这是功能:

static IEnumerable<T> GetPageItems<T>(List<List<T>> itemLists, int pageSize, int pageIndex)
{
    int start = pageIndex * pageSize;
    var counts = new int[itemLists.Count];
    for (int i = 0; i < counts.Length; i++)
        counts[i] = itemLists[i].Count;
    Array.Sort(counts);
    int listCount = counts.Length;
    int itemIndex = 0;
    for (int i = 0; i < counts.Length; i++)
    {
        int itemCount = counts[i];
        if (itemIndex < itemCount)
        {
            int rangeLength = listCount * (itemCount - itemIndex);
            if (start < rangeLength) break;
            start -= rangeLength;
            itemIndex = itemCount;
        }
        listCount--;
    }
    if (listCount > 0)
    {
        var listQueue = new List<T>[listCount];
        listCount = 0;
        foreach (var list in itemLists)
            if (itemIndex < list.Count) listQueue[listCount++] = list;
        itemIndex += start / listCount;
        int listIndex = 0;
        int skipCount = start % listCount;
        int nextCount = 0;
        int yieldCount = 0;
        while (true)
        {
            var list = listQueue[listIndex];
            if (skipCount > 0)
                skipCount--;
            else
            {
                yield return list[itemIndex];
                if (++yieldCount >= pageSize) break;
            }
            if (itemIndex + 1 < list.Count)
            {
                if (nextCount != listIndex)
                    listQueue[nextCount] = list;
                nextCount++;
            }
            if (++listIndex < listCount) continue;
            if (nextCount == 0) break;
            itemIndex++;
            listIndex = 0;
            listCount = nextCount;
            nextCount = 0;
        }
    }
}

并测试:

static void Main(string[] args)
{
    var data = new List<List<int>>
    {
        new List<int> { 01, 02, 03, 04, 05, 06, 07, 08, 09, 10 },
        new List<int> { 11, 12, 13, 14, 15 },
        new List<int> { 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28 },
    };
    int totalCount = data.Sum(list => list.Count);
    int pageSize = 6;
    int pageCount = 1 + (totalCount - 1) / pageSize;
    for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
        Console.WriteLine("Page #{0}: {1}", pageIndex + 1, string.Join(", ", GetPageItems(data, pageSize, pageIndex)));
    Console.ReadLine();
}

答案 2 :(得分:0)

我将基于Bear.S对我的第一个答案的反馈提交另一个实现。这个非常低级,非常高效。它有两个主要部分:

  1. 找出首先出现在页面上的项目(具体是包含它的列表的索引是什么,以及该列表中项目的索引是什么)。

  2. 根据需要以正确的顺序从所有列表中取出物品(直到我们拥有所需的全部物品或用完物品)。

  3. 此实现不会在步骤1中迭代各个列表。它确实使用List.Count属性,但这是一个O(1)操作。

    由于我们要在这里进行表演,因此代码不一定像我想的那样自我描述,所以我提出了一些评论以帮助解释逻辑:

        static IEnumerable<T> GetPageItems<T>(List<List<T>> itemLists, int pageSize, int page)
        {
            if (page < 1)
            {
                return new List<T>();
            }
    
            // a simple copy so that we don't change the original (the individual Lists inside are untouched):
            var lists = itemLists.ToList();
    
            // Let's find the starting indexes for the first item on this page:
            var currItemIndex = 0;
            var currListIndex = 0;
            var itemsToSkipCount = pageSize * (page - 1); // <-- assuming that the first page is page 1, not page 0
    
            // I'll just break out of this loop manually, because I think this configuration actually makes
            // the logic below a little easier to understand.  Feel free to change it however you see fit :)
            while (true)
            {
                var listsCount = lists.Count;
                if (listsCount == 0)
                {
                    return new List<T>();
                }
    
                // Let's consider a horizontal section of items taken evenly from all lists (based on the length of
                // the shortest list).  We don't need to iterate any items in the lists;  Rather, we'll just count 
                // the total number of items we could get from this horizontal portion, and set our indexes accordingly...
                var shortestListCount = lists.Min(x => x.Count);
                var itemsWeAreConsideringCount = listsCount * (shortestListCount - currItemIndex);
    
                // Does this horizontal section contain at least as many items as we must skip?
    
                if (itemsWeAreConsideringCount >= itemsToSkipCount) 
                {   // Yes: So mathematically find the indexes of the first page item, and we're done.
                    currItemIndex += itemsToSkipCount / listsCount;
                    currListIndex = itemsToSkipCount % listsCount;
                    break; 
                }
                else
                {   // No: So we need to keep going.  Let's increase currItemIndex to the end of this horizontal 
                    // section, remove the shortest list(s), and the loop will continue with the remaining lists:
                    currItemIndex = shortestListCount;
                    lists.RemoveAll(x => x.Count == shortestListCount);
                    itemsToSkipCount -= itemsWeAreConsideringCount;
                }
            }
    
            // Ok, we've got our starting indexes, and the remaining lists that still have items in the index range.
            // Let's get our items from those lists:
            var pageItems = new List<T>();
            var largestListCount = lists.Max(x => x.Count);
    
            // Loop until we have enough items to fill the page, or we run out of items:
            while (pageItems.Count < pageSize && currItemIndex < largestListCount)
            {
                // Taking from one list at a time:
                var currList = lists[currListIndex];
    
                // If the list has an element at this index, get it:
                if (currItemIndex < currList.Count)
                {
                    pageItems.Add(currList[currItemIndex]);                    
                }
                // else... this list has no more elements.
                // We could throw away this list, since it's pointless to iterate over it any more, but that might 
                // change the indices of other lists...  for simplicity, I'm just gonna let it be... since the above 
                // logic simply ignores an empty list.
    
                currListIndex++;
                if (currListIndex == lists.Count)
                {
                    currListIndex = 0;
                    currItemIndex++;
                }
            }
    
            return pageItems;
        }
    

    这是一些测试代码,使用三个列表。我可以在几毫秒内从页面1,000,000中删除6个项目:)

            var list1 = Enumerable.Range(0, 10000000).ToList();
            var list2 = Enumerable.Range(10000000, 10000000).ToList();
            var list3 = Enumerable.Range(20000000, 10000000).ToList();
            var lists = new List<List<int>> { list1, list2, list3 };
    
            var timer = new Stopwatch();            
            timer.Start();
    
            var items = GetPageItems(lists, 6, 1000000).ToList();
            var count = items.Count();
    
            timer.Stop();