合并K个排序的关联列表(详细教程)

223 阅读6分钟

我们已经解释了3种不同的算法来合并K个排序的关联列表,这样最终的关联列表也是排序的。

目录:

  1. 介绍/问题陈述
  2. 方法一
  3. 方法2
  4. 方法3
  5. 应用

先决条件:链接列表排序算法

这类似于Leetcode问题23。合并K个排序的列表。让我们开始学习合并K个排序的关联列表。

介绍/问题陈述

在这个问题中,我们得到了 "k "数量的链接列表。所有这些列表都已经被排序了。我们需要将所有给定的链接列表合并为一个单一的列表,并按排序顺序排列。

让第一个、第二个和第三个给定的链接列表分别为1 -> 4 -> 8 -> 102 -> 3 -> 56 -> 7 -> 9 。注意,它们都已经被排序了。这些列表在下面的图片中表示。

row-1
row-2
row-3

所以我们现在需要做的是将上述链接列表合并成一个单一的排序链接列表,就像下面的图片所示。

final-row

有各种方法来解决这个问题。但是在我们继续之前,让我们快速地看一下链表。链接列表是一种数据结构,其值存储在一个列表中,但在不同的内存位置,而不是连续的。一个链接列表是一个节点的集合,一个节点是数据结构的一部分,我们可以同时存储一个值和对下一个值的引用(指针)。
因此,由于链接列表是由节点组成的,并且每个节点都与下一个节点相连,我们需要在我们的方法中小心地跟踪每个节点的每个引用/指针。否则,我们可能会破坏列表的顺序,失去对所有连接节点的跟踪。

现在,让我们来看看我们可以用一些方法来解决这个问题。

方法一

解决这个问题的一个简单方法是,一次合并两个链表,并对它们进行排序。在这个列表中,我们继续合并后面的链接列表。我们这样做,直到所有的链表被合并成一个单一的排序的链表。为了实现这个算法,请遵循下面提到的步骤

  1. 首先,创建一个函数来检查两个列表中的所有节点,并使用它将两个列表合并成一个。要做到这一点,使用两个变量跟踪两个列表的当前第一节点和指针。然后用一个while循环不断检查两个列表的值,直到我们到达一个点,值变成None 。如果发生这种情况,就意味着我们已经到达了列表的末端,我们就可以跳出循环。否则,比较数值并相应地设置指针。这样,我们就创建了一个辅助函数,在下一步中使用。

  2. 现在,创建一个函数,我们检查每个列表的第一个节点,并使用我们在第一步创建的函数对它们进行排序。

  3. 最后,我们创建一个函数,接收一个包含每个链接列表的第一个节点的列表,并输出一个经过排序的合并列表。我们首先将两个列表合并成一个,然后将下一个链接列表逐一合并到这个列表中。

  4. 使用一个辅助打印函数,我们打印出最终合并后列表的节点值。

执行示例

下面的代码执行了上面讨论的算法:

# Python program to merge k sorted linked lists in-place

# Create a class called 'Node' with one part to store value
# and other part to store pointer/reference to next value
class Node:
	def __init__(self, data):
		self.data = data
		self.next = None


# A function to print all values in a linked list
def print_linkedlist(node):

	while (node != None) :
		print( node.data, end =" ")
		node = node.next
	
# Merges two lists
# lst 1 and lst 2 are the first node in each list respectively
# this function changes the pointers in  a sorted order
# so all values get arranged in order
# we finally merge all values together in the next function
def merge_twolists(lst1, lst2):

	# if only one node in first list
	# make first list's node point to second list's first node
	if (lst1.next == None) :
		lst1.next = lst2
		return lst1
	
	# keep track of current first nodes
	# and next pointers of both lists
	
	curr1 = lst1
	next1 = lst1.next
	curr2 = lst2
	next2 = lst2.next

	while (next1 != None and curr2 != None):
	
		# if the first node of list 2
		# contains value greater than first node of list 1
		# but is smaller than second node of list 1
		# change pointers accordingly
		if ((curr2.data) >= (curr1.data) and
			(curr2.data) <= (next1.data)) :
			next2 = curr2.next
			curr1.next = curr2
			curr2.next = next1

			# set curr1 and curr2
			# point to their immediate next pointers
			curr1 = curr2
			curr2 = next2
		
		else :
			# if values are already in order and
			# more nodes exist in first list
            # shift curr 1 to point to next value in list 1
			if (next1.next) :
				next1 = next1.next
				curr1 = curr1.next
			
			# if values are already in order and
			# no nodes further exist in list 1
			# make the last node of first list point to
			# second list's remaining nodes
			else :
				next1.next = curr2
				return lst1

	return lst1

# Compare first nodes in each list
# and call merge_twolists() to merge lists
# Lists are are finally merged in place
def merge(lst1, lst2):

	if (lst1 == None):
		return lst2
	if (lst2 == None):
		return lst1

	# first use the linked list
	# whose first node's data is the smallest
	if (lst1.data < lst2.data):
		return merge_twolists(lst1, lst2)
	else:
		return merge_twolists(lst2, lst1)

# function to merge all lists into a single linked list
# two lists are merged together at a time
# n is the number of linked lists to be merged
# lst is the main list containing all linked lists to merge
def  mergeKlists (lst,n):
        if n == 1:
                return lst[0]
        if n == 0:
                return None
        # first merge first two linked lists
        mergedlist = merge(lst[0],lst[1])

        # then keep on merging additional linked list
        # into the linked list created in the previous step
        for i in range(2,n):
                mergedlist = merge(mergedlist,lst[i])
        return mergedlist

# example to test above programs
# create a total of 4 linked list
k = 4
mlist = [i for i in range(k)]

# LinkedList 1

mlist[0] = Node(0)
mlist[0].next = Node(2)
mlist[0].next.next = Node(4)

# LinkedList 2

mlist[1] = Node(1)
mlist[1].next = Node(3)
mlist[1].next.next = Node(5)

# LinkedList 3

mlist[2] = Node(6)
mlist[2].next = Node(7)
mlist[2].next.next = Node(9)
  
# LinkedList 4

mlist[3] = Node(8)
mlist[3].next = Node(10)

# final merged linked list 
mergedlists = mergeKlists(mlist, k)

# print final linked list
print_linkedlist(mergedlists)

上述程序产生的输出如下

0 1 2 3 4 5 6 7 8 9 10 

复杂度

  • 时间:在最坏的情况下,需要的运行时间为O(n(k^2)),其中n是每个链表的节点数,k是链表的数量。这是因为在每个步骤中,我们都要经过被合并的各个链表中的所有节点,我们重复这个步骤,直到所有链表都被合并在一起。
  • 空间:在最坏的情况下,空间要求是O(N),其中N是合并所有链表的节点总数。

方法二

解决这个问题的另一种方法是使用迷你堆数据结构。迷你堆是一个二进制树,其中每个节点的值都大于或等于根/父节点的值。父节点/根节点包含最小/最低值。这在下图中有所描述。

min-heap-binary-tree

要使用min-heap解决这个问题,请遵循以下步骤--

  1. 在创建类Node ,修改类结构中的__lt()__ 特别方法。我们这样做是因为这个方法允许我们调用和使用小于 (<) 操作符。由于我们在解决方案中实现了min-heap,我们需要检查最小值。这样,我们可以这样做,并使用我们的Node 结构来处理min-heap。

  2. 在导入heapq 模块和heappop,heappush 方法后,我们直接创建一个函数,将所有链接列表合并成一个。为此,我们首先创建一个包含每个链表的第一个节点的列表(m_heap ),并将这个列表转换成一个堆结构。然后我们创建两个变量来跟踪最终输出列表的第一个和最后一个节点。最初,我们将它们设置为None

  3. 然后使用一个while循环,我们查看堆结构中的所有元素(m_heap)。我们使用heappop 方法从堆中获得最小的值,并将这个值设置为最终输出列表中的第一个和最后一个节点,以防第一个节点不包含任何值。否则,我们只设置最后一个节点来指向这个值。

  4. 之后,我们检查我们包含在最终列表中的最小值是否也有一个指向另一个值的指针,从同一个列表中取出最小的值。如果这个指针不是None ,我们就把这个值插入到堆中。我们一直这样做,直到我们清空堆。最后,我们返回列表的第一个头部节点。

  5. 使用一个辅助函数,我们打印我们从步骤4得到的最终合并列表中的所有节点值。

实施示例

下面的代码实现了上面讨论的算法:

# import heapq module to use the heap data structure
# in a min-heap structure  every node' value is greater than
# or equal to the parent node's value
# therefore the parent/root node contains the smallest/minimum value
import heapq
from heapq import heappop, heappush
 
 
# A Linked List Node

class Node:
    def __init__(self, data):
        self.data = data
        self.next = None
 
    # we update the `__lt__()` special method within the `Node` class
    def __lt__(self, other):
        return self.data < other.data
 
 
# a function to print values of a linked list
def print_list(node):
    while node:
        print(node.data, end=' ')
        node = node.next
 
 
 
# We use this function to merge all `k` sorted linked lists.
# As input, this function takes a list of lists
# and then returns the final sorted merged list
def mergeKLists(lists):
 
    # using the first node of every list
    # we create a min-heap structure
    m_heap = [i for i in lists]
    heapq.heapify(m_heap)
 
    # create two pointer variables
    # variable 'first' points to first node of the final output list
    # 'last' variable points to the last node of the final output list
    first = last = None
 
    # loop through all elements in min-heap till it gets empty
    while m_heap:
 
        # get the smallest valued node from the min-heap
        min = heappop(m_heap)
 
        # put the smallest value node in the final output list
        if first is None:
            first = min
            last = min
        else:
            last.next = min
            last = min
 
        # move to the next node
        # from the same list we took the smallest valued node
        # insert it into min-heap
        if min.next:
            heappush(m_heap, min.next)
 
    # return the first node of the final merged list
    return first
 
 
# an EXAMPLE to test above program 

# total number of linked lists
k = 3

# a list to store the first nodes of all linked lists
mlists = [i for i in range(k)]

# linked list 1
mlists[0] = Node(1)
mlists[0].next = Node(5)
mlists[0].next.next = Node(7)

# linked list 2

mlists[1] = Node(2)
mlists[1].next = Node(3)
mlists[1].next.next = Node(6)
mlists[1].next.next.next = Node(9)

# linked list 3

mlists[2] = Node(4)
mlists[2].next = Node(8)
mlists[2].next.next = Node(10)

# Merge all lists into a single merged sorted list
# Print final merged list
final_list = mergeKLists(mlists)
print_list(final_list)

上述程序产生以下输出。

1 2 3 4 5 6 7 8 9 10 

复杂度

  1. 时间:这个算法的总运行时间在最坏情况下为O(nk(log(k)),其中k是链接列表的数量,n是每个链接列表中的节点数量。这是因为在堆结构中插入和删除需要O(log(k))的运行时间,而且我们对每个链表的所有节点都这样做,直到所有链表被合并。

  2. 空间:在最好的情况下,空间要求是O(1),在最坏的情况下,要求是O(k),其中k代表链表的数量。

方法三

我们也可以用我们在合并排序中应用的直觉来解决这个问题。我们可以通过对两个列表进行配对,然后合并所有的列表。让链接的lsits总数为'k'。在最初的迭代中,我们会有k/2个列表,在下一次迭代中,我们会有k/4个列表,这将继续下去,直到我们将所有的链接列表合并成一个单一的列表,我们将只剩下一个列表。为了实现这个算法,我们执行以下步骤

  1. 我们首先创建一个函数,将两个列表分类并合并成一个列表。我们检查每个列表的第一个节点是否是None ,如果是,我们返回另一个列表。然后我们检查列表1的第一个节点是否小于或等于列表2的第一个节点。我们可以对列表1或列表2这样做。然后我们设置最终列表的第一个节点(result 变量)等于列表1的第一个节点,并递归调用同一个函数来获得指针变量。我们这样做,直到我们到达两个列表的末尾,指针变量变成None 。最后,我们返回最终列表的第一个节点。

  2. 然后我们创建一个单独的函数,将所有的链接列表合并成一个单一的列表。这个函数接收每个链表的第一个节点的列表。参数'k'代表链接列表的数量。

  3. 使用一个叫做last 的变量,我们跟踪链接列表的数量。然后使用 while 循环,我们遍历所有的链接列表,直到last 变量变成一个。我们创建两个变量ij 来跟踪每一对链接列表。我们将i 设为零,将j 设为last 变量。然后使用另一个 while 循环,我们合并所有的链接列表,直到ij 不相等,并且i 保持比j 大。否则,这将意味着一对链接列表的重叠,我们将合并错误的列表。我们继续递增i 并递减j 。如果i 大于或等于j ,我们就把last 变量设为 j。这样,最后当j 变成 0 时,我们就脱离了这个循环。然后我们返回第一个列表,我们将所有的链接列表合并成这个列表。

  4. 使用一个辅助函数,我们打印上一步得到的最终合并列表中的所有节点值。

执行示例

下面的程序执行了上面解释的算法:

# A program to merge k sorted lists 

# create a Node class
class Node:
	
	def __init__(self,data):
		
		self.data = data
		self.next = None

# Function to print all node values in a linked list
def printlist(node):

	while (node != None):
		print(node.data, end = ' ')
		node = node.next
	

# a function to create a new sorted linked list
# this function takes in two linked lists to create a new merged one
# lst1 and lst2 are the first head node of the two lists respectively
def Sort_Merge(lst1, lst2):


	# Base cases
	if (lst1 == None):
		return(lst2)
	elif (lst2 == None):
		return(lst1)

	# Choose either lst1 or lst2
	# we use recursion to get the smallest values in sorted order
	if (lst1.data <= lst2.data):
		result = lst1
		result.next = Sort_Merge(lst1.next, lst2)
	else:
		result = lst2
		result.next = Sort_Merge(lst1, lst2.next)
	return result

# a function to create a single sorted linked list
# combining all linked lists into a single one
def mergeKLists(lst, k):

	# merge and sort til only one single list exists
	last = k-1
	while (last != 0):
		i = 0
		j = last

		# i and j must be unequal
		# otherwise we'd overlap lists when merging and sorting
		# leading to faulty final merged list output
		while (i < j):
			
			# merge two lists at a time
			# and assign the merged list into a single list
			lst[i] = Sort_Merge(lst[i], lst[j])

			# increment i and decrease j
			# this way to merge and sort the correct pairs of lists
			i += 1
			j -= 1
			# In case all correct pairs are merged, 
            # we update last variable
			# i >= j implies overlap of already merged lists
			# and we should stop before that happens
			# setting last = j would stop the while loop
			# as in the end j would become less than i or 0
			# so we would break out of the loop
			if (i >= j):
				last = j

        # in the end, all lists get merged in the first linked list
        # that's why we return lst[0]
	return lst[0]



# an example to test the above program


# k denotes the number of linked lists
k = 3

# a list to store the first nodes of all linked lists
mlist = [i for i in range(k)]

mlist[0] = Node(1)
mlist[0].next = Node(3)
mlist[0].next.next = Node(5)
mlist[0].next.next.next = Node(7)

mlist[1] = Node(2)
mlist[1].next = Node(4)
mlist[1].next.next = Node(6)
mlist[1].next.next.next = Node(8)

mlist[2] = Node(0)
mlist[2].next = Node(9)
mlist[2].next.next = Node(10)
mlist[2].next.next.next = Node(11)


# call mergeKLists to merge all lists into a single sorted one
final_list = mergeKLists(mlist, k)

# print final merged linked list
printlist(final_list)

上述程序产生以下输出:

0 1 2 3 4 5 6 7 8 9 10 11 

复杂性

  • 时间:我们用来将链接列表合并成一个单一的列表的while循环的运行时间为O(log(k)),其中k是链接列表的数量。我们使用 while 循环遍历每个链接列表中的所有节点,直到每个链接列表都被合并到最终的链接列表中,所以总的运行时间和复杂性在最坏的情况下为 O(nk(log(k)),其中 n 是每个链接列表中的节点数。
  • 空间:这个程序的空间需求在最坏的情况下变成了O(N)(因为我们对每个节点都使用递归),其中N是合并所有链表的节点总数。

应用

  • 链接列表从根本上说是用来以一种有组织的方式存储数据的,比如以堆栈和队列的形式,数据以一种顺序存储,以后可以很容易地进行编辑/添加。这是计算机编程和计算机组织的一个重要部分。

  • 一般来说,链接列表有助于将数据作为一个集合进行跟踪,并告诉数据的各个部分是如何相互连接的。因此,它们被用于各种应用,如电影播放器、音乐播放器、浏览器、用户界面、图像查看器、日历系统、游戏等。

通过OpenGenus的这篇文章,你一定对如何合并K排序的关联列表有了完整的认识。