使用优先队列合并K个已排序列表

3
我在算法课中被要求制作一个K路归并算法,其时间复杂度为O(nlogk)。经过搜索,我发现可以通过制作一个长度为k的优先队列,并将每个列表的第一个元素入队来实现。提取最小值,将其附加到结果中,并从已提取元素的列表中入队。
以下是我的困惑:
1. 当一个特定的列表耗尽时,它如何知道?比如说一个列表的元素都比其他列表中的元素都小。
2. 如果没有使用结构来定义,它如何知道元素属于哪个列表?
3. 时间复杂度如何是O(nlogk)?
编辑:
如果有人能够逐步编写算法,会更有帮助,因为我读到的所有内容都是句子,很难理解。如果有人能够编写算法,那么可能更有帮助。

1
i) 你的代码可以检查列表是否耗尽,也可以尝试删除元素并失败。 ii) 元素来自哪里并不重要。如果你在将其插入堆时关心,可以用一些列表标识符包装每个元素。 iii) 因为有 n 个元素,并且每个元素都被插入/从 k 大小的堆中删除。算法中的所有其他工作都被这些步骤“吸收”了。 - rliu
1
您也可以在没有堆的情况下进行K路合并。我在这里解释了:https://dev59.com/0XfZa4cB1Zd3GeqPOjOV#18984961。我还没有进行性能测试来确定哪种方法(使用堆或不使用)更快。两种方法都是O(n log k),但其中一种可能比另一种多做一些工作。 - Jim Mischel
7个回答

7

以下是一些 Python 2 代码,用于合并。

import heapq

def addtoheap(h, i, it):
    try:
        heapq.heappush(h, (next(it), i))
    except StopIteration:
        pass

def mergek(*lists):
    its = map(iter, lists)
    h = []
    for i, it in enumerate(its):
        addtoheap(h, i, it)
    while h:
        v, i = heapq.heappop(h)
        addtoheap(h, i, its[i])
        yield v

for x in mergek([1, 3, 5], [2, 4, 6], [7, 8, 9], [10]):
    print x

为什么时间复杂度是O(n log k)?因为对于每个被移除的值,都需要执行一次堆弹出和可能的堆插入操作(这两个操作的时间复杂度都是O(log k))。由于我们要移除n个元素,所以总时间复杂度是O(n log k)。


你可以简化它:你不需要索引i,你可以使用heappush(h, (next(it), it)) - jfs

2

与其仅将每个列表的第一个元素存储在优先队列中,不如将其包装在这样的结构中:

struct wrapper
{
    int list_number;
    int element;
}

然后,当您将元素推入优先级队列时,只需添加其来自的列表编号。这样,当最小元素被弹出时,通过检查popped_element.list_number,您将知道应从哪个列表推送下一个要推送到队列上的元素。
为了确定列表是否为空,您应该向其中添加一个名为empty的函数,如果列表没有更多元素,则返回true,否则返回false。实现该函数非常简单。只需检查大小是否为零,然后列表为空,否则它具有一个或多个元素。
根据您的问题,我假设二叉堆用于实现优先级队列。在二叉堆中,插入操作需要O(lg k)时间,而提取最小值操作也需要O(lg k)时间,其中k是堆的大小(在您的情况下为列表数)。现在,如果您拥有的总元素数为n,则处理所有元素的总时间将为O(n lg k)。

2
几年前,我写了一系列关于如何排序大型文本文件的文章。其核心思想是将你放在堆中的项不仅包含值,还包括该值来源的列表。或者,你也可以只将列表引用放在堆中,并让比较函数与特定列表中的第一个项目进行比较。
请参见http://www.informit.com/guides/content.aspx?g=dotnet&seqNum=676http://www.informit.com/guides/content.aspx?g=dotnet&seqNum=677以了解使用顺序列表替代堆的基本算法的说明。请参见http://www.informit.com/guides/content.aspx?g=dotnet&seqNum=680以了解使用堆的改进版本。
正如我在评论中所说的,您也可以不使用堆进行合并。请参阅https://dev59.com/0XfZa4cB1Zd3GeqPOjOV#18984961了解详细信息。

0

这是我的使用C++ STL的代码

#include<iostream>
#include<vector>
#include<climits>
#include<queue>
#define ROWS 4
#define COLS 8
using namespace std;

struct node
{
    int ele;
    int arr_no;
    int next_index; 
};

void printVector(vector<int> v)
{
    for(unsigned int i=0;i<v.size();i++)
        cout<<v[i]<<" ";
}

// THIS IS THE BASIS ON WHICH THE ELEMENTS OF A PRIORITY QUEUE ARE SORTED AND 
// KEPT IN THE QUEUE, HERE THE CRITERIA IS THAT THE NODE WITH SMALLER ELEMENT SHOULD
// COME ABOVE THE ONE WITH LARGER ELEMENT

class compare
{
    public:
        bool operator()(node& n1, node& n2)
        {
           if (n1.ele > n2.ele) 
                return true;
           else
                return false;
        }
};

vector<int> mergeKArrays(vector< vector<int> > v)
{
    int k = v.size();       // NUMBER OF LISTS
    int n = v.at(0).size(); //SIZE OF EACH LIST

    vector<int> result;
    //result.resize( n*k );

    priority_queue<node, vector<node>, compare> minHeap;
    for (int i = 0; i < k; i++)
    {
        node temp;
        temp.ele = v[i][0]; //STORE THE FIRST ELEMENT
        temp.arr_no = i;        //INDEX OF ARRAY
        temp.next_index = 1;    //INDEX OF NEXT ELEMENT TO BE STORED FROM ARRAY
        minHeap.push(temp);
    }

    // NOW ONE BY ONE GET THE MINIMUM ELEMENT FROM MIN
    // HEAP AND REPLACE IT WITH NEXT ELEMENT OF ITS ARRAY
    for (int count = 0; count < n*k; count++)
    {
        // GET THE MINIMUM ELEMENT AND STORE IT IN OUTPUT
        node min_ele_node = minHeap.top();
        minHeap.pop();      
        result.push_back(min_ele_node.ele);

        // FIND THE NEXT ELELEMENT THAT WILL REPLACE CURRENT
        // ROOT OF HEAP. THE NEXT ELEMENT BELONGS TO SAME
        // ARRAY AS THE CURRENT ROOT.
        node new_node;
        new_node.arr_no = min_ele_node.arr_no;
        if (min_ele_node.next_index < n)
        {
            new_node.ele = v.at(min_ele_node.arr_no)[min_ele_node.next_index];
            new_node.next_index = min_ele_node.next_index + 1;
        }
        // IF ROOT WAS THE LAST ELEMENT OF ITS ARRAY
        else 
        {
            new_node.ele =  INT_MAX; //INT_MAX IS FOR INFINITE
        }

        // REPLACE ROOT WITH NEXT ELEMENT OF ARRAY
        minHeap.push(new_node);
    }
    return result;
}


int main()
{
    int arr[ROWS][COLS] = 
                    { 
                        {10, 20, 30, 40, 50, 60, 71, 86},
                        {15, 25, 35, 45, 60, 69, 77, 78},
                        {27, 29, 37, 48, 50, 65, 75, 78},
                        {32, 33, 39, 50, 80, 133, 139, 150},
                    }; 

    vector< vector<int> > matrix ;

    for( int i=0 ; i < ROWS; i++)
    {
        vector<int> vec;
        for(int j=0; j < COLS; j++)
            vec.push_back(arr[i][j]);
        matrix.push_back(vec);
    }

    vector<int> result = mergeKArrays(matrix);
    printVector(result);
    return 0;
}

0

Paul Hankin的解决方案是正确的,但阅读起来有点困难,特别是你想在c++或java中实现。我的解决方案与Paul的类似。如果你用c++或java编写,可能需要一个额外的数据结构来存储元素的值、k-th数组中元素的索引和列表中数组的索引。

Element{
    int value;
    int idInArray,
    int idInList
}

但在Python中,我只是将其存储在一个元组中(值,数组中的ID,列表中的ID)。
def mergeKArray(*lists):
    # implemented by min heap
    h = []
    r = []
    for k, arr in enumerate(lists):
        heapq.heappush(h, (arr[0], 0, k))
    while h:
        # min is the minimum element
        # i is the index of the min in the k-th array
        # k is the index of array in the list
        min, i, k = heapq.heappop(h)
        r.append(min)
        if i < len(lists[k]) - 1:
            i += 1
            heapq.heappush(h, (lists[k][i], i, k))
    return r

因为我只需要维护一个包含k个元素的最小堆,所以弹出或插入堆的时间复杂度为O(log k)。我还需要扫描所有n个元素,每个元素插入和弹出堆的时间复杂度为2*log(k)。因此,总的时间复杂度为O(n*log k)。


0
你应该注意到这里的“n”是所有列表中节点的总数,而不仅仅是一个列表。在这种情况下,解决方案的时间复杂度为O(n logk)。如果我们的意思是每个列表上平均有n个节点(总共k个列表),那么时间复杂度将为O(nk logk)。 这里有一个深入的解释和一些代码。

0

1 当列表没有更多元素时,它被耗尽了。

2 你需要跟踪最小元素来自哪个列表

3 对于每个元素,将其放入大小为k的最小堆中,这需要logk的时间,因此您需要n次logk。


是的,这就是我想问的。当数组的最后一个元素被提取时,我们如何检查是否没有更多的元素。我们如何追踪最小元素来自哪里。您能详细说明第三点吗? 如果我理解正确的话,从k大小堆中提取所有元素的时间复杂度是log k,而我们重复推入n个元素(来自所有列表),所以它需要n*logk的时间复杂度? - Shaurya Chaudhuri

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接