Python中的堆排序

来源:stackabuse.com/heap-sort-i…python

做者:Olivera Popović面试

翻译:老齐算法


介绍

堆排序是高效排序算法的另外一个例子,它的主要优势是,不管输入数据如何,它的最坏状况运行时间都是O(n*logn)。api

顾名思义,堆排序在很大程度上依赖于堆数据结构——优先级队列的常见实现。数组

毫无疑问,堆排序是一种简单的排序算法,并且与其余简单实现相比,堆排序是更有效,也很常见。安全

堆排序

堆排序的工做原理是从堆逐个“移除”元素并将它们添加到已排序的数组里,在进一步解释和从新访问堆数据结构以前,咱们应该了解堆排序自己的一些属性。bash

它是一种原地算法(译者注:in-place algorithm,多数翻译为“原地算法”,少数也翻译为“就地算法”。这种算法是使用小的、固定数量的额外内存空间来转换资料的算法。),意味着它须要恒定数量的内存,即所需内存不取决于初始数组自己的大小,而取决于存储该数组所需的内存。微信

例如,不须要原始数组的副本,也不须要递归和递归调用堆栈。最简单的堆排序实现一般使用第二个数组来存储排序后的值。咱们将使用这种方法,由于它在代码中更直观、更易于实现,但它也是百分百的原地算法。数据结构

堆排序不稳定,意思是相等的值,并不会在一样的相对位次上。对于整数、字符串等这些基本类型,不会出现这类问题,但当咱们对复杂类型的对象排序时,可能会遇到。app

例如,假设咱们有一个自定义类Person带有agename属性,在一个数组中几个此类的实例对象,好比按顺序出现19岁的名叫“Mike”的人和一个19岁的名叫“David”的人。

若是咱们决定按年龄对这些人进行排序,就不能在排序数组中保证“Mike”会出如今“David”以前,即便他们在初始数组中是按这个顺序出现的。“Mike”有可能出如今“David”以前,但不能保证百分之百如此。

堆数据结构

堆是计算机科学中最流行和最经常使用的一种数据结构——更不用说在软件工程面试中很是流行了

咱们将讨论跟踪最小元素(最小堆)的堆,但它们也能够很容易地实现对最大元素(最大堆)的跟踪。

简单地说,最小堆是一种基于树的数据结构,其中每一个节点比其全部子节点都小。一般使用二叉树。堆有三个基本操做——delete_minimum()get_minimum()add()

每次,你只能删除堆中的第一个元素,而后对其进行“从新排序”。在添加或删除元素后,堆对本身会“从新排序”,以便最小的元素始终处于第一个位置。

注意:这毫不意味着堆是排序的数组。每一个节点都小于其子节点这一事实不足以保证整个堆是按升序排列的。

咱们来看一个关于堆的例子:

正如咱们看到的,上面的例子确实符合堆的描述,可是没有排序。咱们不会详细讨论堆实现,由于这不是本文的重点。当在堆排序中使用堆数据结构时,咱们所利用的堆数据结构的关键优点是:下一个最小的元素始终是堆中的第一个元素。

实现

数组排序

译者注: 做者在本文中并无严格区分Python中的列表和数组,而是将列表看作了数组,这对于列表中的元素是同一种类型的元素而言,无可厚非。对于排序,只有是同一种类型的元素,才有意义。

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*):返回一个列表,其中包含由iterable定义的数据集中的n个最大元素。
  • heapq.nsmallest(*n*, *iterable*, *key=None*):返回一个列表,其中包含由iterable定义的数据集中的n个最小元素。

咱们可使用它来简单地获取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
复制代码

与其余排序算法的比较

堆排序被普遍使用,主要缘由是它的可靠性,尽管它常常被运行良好的“快速排序”法所超越(译者注: 本文的微信公众号“老齐教室”以系列文章,介绍各类排序算法,而且用Python语言实现,敬请关注)。

堆排序的主要优势是时间复杂度上的O(n*logn)上限以及安全性。Linux内核开发人员给出了使用堆排序而不是快速排序的如下理由:

堆排序的平均排序时间和最坏排序时间均为O(n*logn),虽然qsort的平均速度快了20%,但不得不容忍O(n*n)的最坏可能情形和额外的内存支出,这使它不太适合在操做系统内核中使用。

此外,快速排序算法法在可预测的状况下表现不佳。而且,若是对内部实现有足够的了解,你可能会意识到它形成的安全风险(主要是DDoS攻击),由于不良的O(n^2)行为很容易被触发。

常常被用来与堆排序比较的另外一种算法是归并排序算法(译者注: 本微信公众号也会刊发相关文章给予介绍,敬请关注),它们具备相同的时间复杂度。

归并排序的优势是稳定、可并行运算,而堆排序二者都作不到。

另外一个注意事项是:即便复杂度相同,堆排序在大多数状况下也比归并排序慢,由于堆排序具备较大的常数因子。

然而,堆排序比归并排序更容易实现,所以当内存比速度更重要时,它是首选。

结论

正如咱们所看到的,堆排序不像其余高效的通用算法那么流行,可是它的可预测行为(而不是不稳定的行为)使它成为一个很好的算法,适用于内存和安全性比稍快的运行速度更重要的场合。

实现和利用Python提供的内置功能是很是直观的,咱们实际上要作的就是将元素放在一个堆中并取出它们,就像对待硬币计数器同样。

关注微信公众号:老齐教室。读深度文章,得精湛技艺,享绚丽人生。

WechatIMG6
相关文章
相关标签/搜索