📅  最后修改于: 2020-08-27 07:44:28             🧑  作者: Mango
介绍
堆排序是高效排序算法的另一个示例。它的主要优点是,无论输入数据如何,它都具有最坏的O(n * logn)运行时。
顾名思义,堆排序在很大程度上依赖于堆数据结构- 优先级队列的常见实现。
毫无疑问,堆排序是最容易实现的排序算法之一,而且与其他简单实现相比,它是一种相当高效的算法,这是一个常见的问题。
堆排序
堆排序的工作方式是“一对一”地从数组的堆部分“删除”元素,并将它们添加到数组的排序部分。在进一步解释并重新访问堆数据结构之前,我们应提及堆排序本身的一些属性。
这是一个就地算法,这意味着它需要恒定数量的附加内存,即所需的内存不取决于初始数组本身的大小,而不是存储该数组所需的内存。
例如,不需要原始数组的副本,也没有递归和递归调用堆栈。堆排序的最简单实现通常使用第二个数组来存储排序后的值。我们将使用这种方法,因为它在代码中更加直观和易于遵循,但是可以完全就地实现。
堆排序是不稳定的,这意味着它不会保持具有相等值的元素的相对顺序。这不是原始类型(例如整数和字符…)的问题,但是当我们对诸如对象之类的复杂类型进行排序时,这可能是一个问题。
例如,假设我们有一个Person
带有age
和name
字段的自定义类,并且该类的几个对象以阵列的形式出现,其中包括一个叫19岁的“迈克”的人和也叫19岁的“大卫”的人-依次出现。
如果我们决定按年龄对那一组人进行排序,则即使在初始数组中按顺序出现,也不能保证“迈克”会出现在“大卫”之前。它可以发生,但它不能保证。
有趣的事实:堆排序是Linux内核中选择的排序算法
堆数据结构
堆是计算机科学中最流行和使用最广泛的数据结构之一,更不用说在软件工程采访中非常流行的了。
我们将讨论堆来跟踪最小元素(最小堆),但是可以很容易地实现它们来跟踪最大元素(最大堆)。
简而言之,最小堆是基于树的数据结构,其中每个节点都小于其所有子节点。最常见的是使用二叉树。堆有三个支持的操作- delete_minimum()
,get_minimum()
和add()
。
您只能删除堆中的第一个元素,然后对其进行“重新排序”。在添加或删除元素之后,堆会自己对其进行“重新排序”,以便最小的元素始终位于第一位置。
注意:这绝不意味着堆是排序的数组。每个节点小于其子节点的事实不足以保证整个堆按升序排列。
让我们看一个堆的例子:
如我们所见,上面的示例确实适合于堆的描述,但是没有排序。我们不会讨论堆实现的细节,因为这不是本文的重点。我们在堆排序中使用堆数据结构的关键优势在于,下一个最小的元素始终是堆中的第一个元素。
注意:由于删除元素后对元素进行堆排序的方式,下一个最小元素移到第一个位置的复杂性(同时使数组保持堆不变)花费O(logn)时间,这是一种高效的操作。
实作
排序数组
Python提供了用于创建和使用堆的方法,因此我们不必自己实现它们:
heappush(list, item)
:将元素添加到堆中,然后对其进行重新排序,以使其保留为堆。可以用于空列表。
heappop(list)
:弹出(删除)第一个(最小)元素并返回该元素。在执行此操作后,该堆仍为堆,因此我们不必调用heapify()
。
heapify(list)
:将给定列表转换为堆。值得注意的是,即使我们不使用此方法,也存在该方法,因为我们不想更改原始数组。
现在我们知道了,堆排序的实现相当简单:
from heapq import heappop, heappush
def heap_sort(array):
heap = []
for element in array:
heappush(heap, element)
ordered = []
# While we have elements left in the heap
while heap:
ordered.append(heappop(heap))
return ordered
array = [13, 21, 15, 5, 26, 4, 17, 18, 24, 2]
print(heap_sort(array))
输出:
[2, 4, 5, 13, 15, 17, 18, 21, 24, 26]
正如我们所看到的,繁重的工作是通过堆数据结构完成的,我们要做的就是添加我们需要的所有元素,并将它们逐个删除。几乎就像一台硬币计数机,它按照输入的硬币的价值对它们进行分类,然后我们可以将它们取出。
排序自定义对象
使用自定义类时,事情变得有些复杂。通常,我们建议不要在类中重写比较运算符,以便为它们使用排序算法,而建议重写该算法,使其改用lambda函数比较器。
但是,由于我们的实现依赖于内置的堆方法,因此我们不能在此处这样做。
Python确实提供了以下方法:
heapq.nlargest(*n*, *iterable*, *key=None*)
:返回由定义的数据集中n个最大元素的列表iterable
。
heapq.nsmallest(*n*, *iterable*, *key=None*)
:返回由定义的数据集中n个最小元素的列表iterable
。
我们可以使用它简单地获取n = len(array)
最大/最小元素,但方法本身不使用堆排序,本质上等效于仅调用sorted()
方法。
我们留给自定义类的唯一解决方案是实际上重写比较运算符。可悲的是,这限制了我们每个班级只能进行一种比较。在我们的示例中,它限制了我们只能Movie
按年份对对象进行排序。
但是,它确实让我们演示了如何在自定义类上使用堆排序。让我们继续定义Movie
类:
from heapq import heappop, heappush
class Movie:
def __init__(self, title, year):
self.title = title
self.year = year
def __str__(self):
return str.format("Title: {}, Year: {}", self.title, self.year)
def __lt__(self, other):
return self.year < other.year
def __gt__(self, other):
return other.__lt__(self)
def __eq__(self, other):
return self.year == other.year
def __ne__(self, other):
return not self.__eq__(other)
现在,让我们稍微修改一下我们的heap_sort()
函数:
def heap_sort(array):
heap = []
for element in array:
heappush(heap, element)
ordered = []
while heap:
ordered.append(heappop(heap))
return ordered
最后,让我们实例化一些电影,将它们放入数组中,然后对其进行排序:
movie1 = Movie("Citizen Kane", 1941)
movie2 = Movie("Back to the Future", 1985)
movie3 = Movie("Forrest Gump", 1994)
movie4 = Movie("The Silence of the Lambs", 1991);
movie5 = Movie("Gia", 1998)
array = [movie1, movie2, movie3, movie4, movie5]
for movie in heap_sort(array):
print(movie)
输出:
Title: Citizen Kane, Year: 1941
Title: Back to the Future, Year: 1985
Title: The Silence of the Lambs, Year: 1991
Title: Forrest Gump, Year: 1994
Title: Gia, Year: 1998
与其他排序算法的比较
堆排序仍然被经常使用的主要原因之一是它的可靠性,尽管它经常被实施良好的快速排序所超越。
就时间复杂度和安全性而言,堆排序的主要优点是O(n * logn)上限。Linux内核开发人员对使用堆排序而不是快速排序给出了以下理由:
平均和最坏情况下,堆排序的排序时间均为O(n * logn)。尽管qsort平均快20%,但它遭受O(n * n)可利用的最坏情况的行为以及额外的内存需求,从而使其不太适合内核使用。
此外,快速排序在可预测的情况下表现不佳,并且如果对内部实现有足够的了解,则可能会引发安全风险(主要是DDoS攻击),因为很容易触发不良的O(n 2)行为。
堆排序经常被比较的另一种算法是合并排序,它具有相同的时间复杂度。
合并排序的优点是稳定且直观可并行化,而堆排序都不是。
另一个要注意的是,即使堆排序具有相同的复杂度,大多数情况下堆排序也比合并排序慢,因为堆排序的常数因子更大。
但是,堆排序可以比合并排序更轻松地就地实现,因此当内存比速度更重要时,它是首选。
结论
正如我们所看到的,堆排序不像其他高效的通用算法那样流行,但是其可预测的行为(不是不稳定的)使其成为在内存和安全性比运行时间略微重要的地方使用的出色算法。
实现和利用Python随附的内置功能非常直观,我们要做的基本上就是将物品放进堆中并取出-类似于硬币计数器。