1、什么是堆

(1)   堆是具有以下性质的完全二叉树(那么,什么是完全二叉树呢?完全二叉树是一种除了最后一层之外的其他每一层都被完全填充,并且所有结点都保持向左对齐的树):每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:

 

 

同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子

 

 

该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:

大顶堆:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2] 

小顶堆:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2] 

 

比如20 10 15 这三个数的关系,i表示坐标,i=3时,对应的arr值为20,i=3*2+1=7,arr值为10,i=3*2+2=8,arr值为15,所以2*i+1和2*i+2是这么来的。

 

(2)   堆的起始坐标从1开始算:

比如说当前节点在list中的坐标为i

那么他的左节点坐标:2i 

那么他的右节点坐标:2i + 1

找到它的父节点:i//2

 

(3)   堆的坐标从0开始算:

比如说当前节点在list中的坐标为i

那么他的左节点坐标:2i+1 

那么他的右节点坐标:2i + 2

找到它的父节点:(i-1)//2

 

 

(4)   堆也叫做优先队列

(5)   上滤:新插入元素和父节点比对,发生交换,当发生了新插入结点和父结点没有交换的情况,那么上滤过程结束

(6)   下滤:左右节点比对后发生交换,将大的元素上调,不断重复,直到不需要调整或者调整到堆底,如下图

 

 

 

 

2、什么是二叉树及二叉树的一些概念

(1)   结点的度:结点拥有的子树的数目

(2)   叶子:没有子节点,度为零

(3)   森林:多个不相交的树,多个树

(4)   满二叉树:每个结点下都挂了2个子节点

(5)   完全二叉树:叶子结点只能出现在最下层和次下层,最下层的叶子结点集中在树的左部。满二叉树一定是完全二叉树,一个完全二叉树不一定是满二叉树。

(6)   二叉搜索树(又叫二叉查找树):左边的结点都小于根结点,右边的结点都大于根结点

 

3、堆排序介绍

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(n*logn),它也是不稳定排序。

 

4、堆排序的核心算法

(1)   建堆:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点

(2)   把堆顶的元素和最下最右一个元素交换,此时,最大的元素就排到了最后面

(3)   对于新的root节点,继续建堆,这样会得到n个元素的次小值

(4)   重复上面的1到3步,便能得到一个有序序列了

 

5、堆排序基本思想及步骤

步骤一 构造初始堆(从堆最下层开始构造,并且需要进行上虑操作)。将给定无序序列构造成一个大顶堆(一般升序采用大顶堆,降序采用小顶堆)。

假设给定无序序列结构如下:

 

 

此时我们从最后一个非叶子结点开始(叶结点自然不用调整,最后一个非叶子结点 arr.length/2-1=5/2-1=1,也就是下面的6结点),从后往前,从下往上的第一个非叶子节点进行调整

 

 

找到第二个非叶节点4,由于[4,9,8]中9元素最大,4和9交换。

 

 

这时,交换导致了子根[4,5,6]结构混乱,继续调整,[4,5,6]中6最大,交换4和6。

 

 

此时,我们就将一个无需序列构造成了一个大顶堆。

 

步骤二 将堆顶元素与末尾元素进行交换,也就是与堆的最下最右的元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

 

将堆顶元素9和末尾元素4进行交换

 

 

重新调整结构,使其继续满足堆定义

 

 

再将堆顶元素8与末尾元素5进行交换,得到第二大元素8

 

 

后续过程,继续进行调整,交换,如此反复进行,最终使得整个序列有序

 

 

总结:对于堆排序的核心算法就是堆结点的调整

  • 度数为2的结点A(有两个孩子结点),如果它的左右孩子结点的最大值比它大的,将这个最大值和该结点交换
  • 度数为1的结点A(有左孩子结点),如果它的左孩子的值大于它,则交换
  • 如果结点A被交换到新的位置,还需要和其孩子结点重复上面的过程堆排序的代码

 

6、堆排序代码

# encoding=utf-8

#左叶子、右叶子和父节点,三个元素,找到最大的一个

def maxHeap(heap,heapSize,i):

    #i为某个节点

    #它的左节点坐标:2i

    #那么它的右节点坐标:2i + 1

    #它的父节点:i/2

 

    left = 2*i +1

    right = 2*i +2

    larger = i

 

    #通过2次if的比较,将left、right和lager三者的最大值找到,这里假设当前是一个已经构建好的堆

    if left < heapSize and heap[larger] < heap[left]:

        larger = left

    if right < heapSize and heap[larger] < heap[right]:

        larger = right

    #如果lager的值不是i,说明i的值需要和最大值进行交换

    #因为i的坐标是最大堆的堆顶,所以必须是最大值

    if larger != i:

        heap[i],heap[larger] = heap[larger],heap[i]

        maxHeap(heap,heapSize,larger)

    #以上步骤完成,堆顶坐标为i坐标的最大子堆建立好了

 

def buildMaxHeap(heap): 

#heap参数是未排序,未建堆的list

    heapSize = len(heap)

    #堆的长度//2可以找到堆里面的

    #最后一个带有子节点的节点

    #循环可以实现从堆的最下层节点开始建堆

    #每次建立的堆都是一个最大堆

    #简单来说把所有字段都建成最大堆

    #然后组成了最终的最大堆

    # (heapSize-1)//2当坐标从0开始算时,算出了当前堆中最后一个含有子节点的结点坐标

    #这个循环要理解一下:这个循环调用,表示从最下层的子树,开始实现最大堆,这个就是我刚才说的从最下层开始建立最大堆的过程

    for i in range((heapSize-1)//2,-1,-1):

        maxHeap(heap,heapSize,i)

 

def heapSort(heap):

    #先把所有元素先建立一个最大堆

    buildMaxHeap(heap)

 

    #将堆中所有的元素都遍历一遍

    #让每个元素都做一次堆顶

    #然后将堆顶的每个元素都换到堆的最后一个节点

    for i in range(len(heap)-1,-1,-1):

        heap[0],heap[i] = heap[i],heap[0]

        #maxheap中的i是列表的长度,这样可以防止追加到

        #堆后面的元素重新被当做最大堆元素进行建队

        maxHeap(heap,i,0)

    return heap

#第一次循环时,把最大值放到了列表最后面

#把最后一个值(肯定不是最大值)放到了堆顶

#把不包含最后一个元素的剩余元素,重新进行最大堆

#第二次循环的时候,把堆顶(次大值)放到了列表倒数的第二位置

#然后把不包含最后2个元素的剩余元素,重新进行建立最大堆

………

#循环结束,那么列表的数据就排好了

 

if __name__ == '__main__':

    heap1 = [3,4,5,6,23,4,1,1,23,45,6678]

    print (heap1)

    heapSort(heap1)

    print (heap1)

7、堆排序的时间复杂度

堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(N*logN)级。