Python算法之『 神秘的堆排序』

一、为什么说堆排序很“神秘”?

堆排序相较于其他常见的线性排序(冒泡、选择、插入、快排等),它比较不一样(我们不一样,不一样)。堆排序使用“堆”结构进行非线性排序,这里的堆就是数据结构里的二叉树,确切的说应该是【完全二叉树】。说到树结构,可能会想到二分法的“分治”思想,难道跟快排有些许关系?不!快排是使用了二分法的思想,但是没有使用二叉树的组织结构,数据还是线性列表的;而堆排序则不同。

另外,在我们刚开始接触算法的时候,可能大部分都是从前面说的几种简单的排序算法开始的,好理解,写起来又简单。堆排序可能就被冷落了一些。并且堆排序的逻辑比那几种算法稍微复杂一点,从下面的代码量就可以看出,比前面说的几种算法多很多代码。

 

二、堆排序的特性?

堆排序的时间复杂度:O(n) = n*log(n)

堆排序的稳定性:不稳定(假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!)

 

三、堆排序的原理是怎样的?

首先,我们需要理解什么大根堆小根堆、父节点和子节点之间的索引关系

1、大根堆(大顶堆):在完全二叉树的条件下,任意父节点的值都大于等于其左右子节点。

2、小根堆(小顶堆):在完全二叉树的条件下,任意父节点的值都小于等于其左右子节点。

3、索引关系:

    若某一节点A的索引为x时:

        A的左子节点索引为:2*x+1

        A的右子节点索引为:2*x+2

        A的父节点的索引为:(x-1)//2  ('//'为除法向下取整符号)

下图是是数列到堆的映射关系:

 

本程序示例中使用的是大根堆方法,基本逻辑是:

1、把数列初始化成一个大根堆(或者小根堆)。

2、取出最大值(根节点值)放到数列最后。

3、把剩余的数列重新调整为一个大根堆。

4、重复第2、3步骤。

import numpy

def heap_sort_asc(arr):
    """
    堆排序(升序)。
    时间复杂度:n*log(n)
    稳定性:不稳定(假设在数列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;并且排序之后,a[i]仍然在a[j]前面。则这个排序算法是稳定的!)
    此处使用大根堆(即:对于任意节点,其孩子节点都不大于该节点)
    
    :param arr: 待排序的数列
    :return: 排好序的数列(升序)
    """
    arr_size = len(arr)
    # 把数列排列成大根堆形式
    set_big_heap(arr, arr_size)
    for i in range(arr_size): # 循环n次
        heap_size = arr_size - i
        # 交换。[0]处的数值是最大值,交换放到数列最后
        arr[0], arr[heap_size - 1] = arr[heap_size - 1], arr[0]
        # 把剩余的数列元素重新调整成大根堆,最多循环log(n)次,即二叉树的深度
        adjust_to_big_heap(arr, heap_size - 1)
    return arr
    

def set_big_heap(arr, size):
    """
    把一个数列处理成一个大根堆
    注意:若当前节点索引为x,则:其父节点索引为:(x-1)//2
    
    :param arr: 数列
    :param size: 要处理的数列元素数量
    :return:
    """
    # 遍历每一个数据
    for i in range(size):
        this_index = i
        parent_index = (this_index - 1) // 2 # 父节点索引
        if parent_index < 0: # 没有父节点了
            continue
        # 循环查找,直到当前节点不大于父节点
        while arr[this_index] > arr[parent_index]:
            # 交换当前节点和父节点
            arr[this_index], arr[parent_index] = arr[parent_index], arr[this_index]
            this_index = parent_index # 当前节点指向交换之前的父节点位置
            parent_index = (this_index - 1) // 2 # 计算父节点
            if parent_index < 0: # 找不到父节点,则退出循环
                break
    return arr

    
def adjust_to_big_heap(arr, size):
    """
    调整为大根堆.
    该函数处理的堆有一个“异常值”,那就是根节点上的值,它是由堆的最大值(原根节点)和堆的最后一个节点值交换而来。
    所以这个函数的任务就是给这个“异常值”,寻找一个合适的家(位置)。
    注意:若当前节点索引为x,则:
        其左孩子索引为:2*x+1
        其右孩子索引为:2*x+2
    
    :param arr: 是一个被替换掉跟节点的大根堆(此时,除了根节点,其他及节点都满足大根堆的要求)。正因为如此,所以需要重新调整为大根堆
    :param size: 堆的大小,即在数组中的索引范围[0, size)
    :return: 调整后的原数组
    """
    current_index = 0 # 最大的值的索引(因为是大根堆,所以索引0的值最大)
    while current_index * 2 + 1 < size: # 有左孩子
        left_node_index = current_index*2+1
        
        # 寻找最大孩子节点
        max_child_index = left_node_index
        if left_node_index+1 < size: # 有右孩子
            if arr[left_node_index] < arr[left_node_index+1]:
                max_child_index = left_node_index+1
        # 孩子节点大于父节点(当前节点),则交换
        if arr[max_child_index] > arr[current_index]:
            arr[max_child_index], arr[current_index] = arr[current_index], arr[max_child_index]
            # 更新当前索引指向(当前索引一直指向最初那个根节点上的值,因为只有这个值是“异常值”,需要给它寻找合适的位置)
            current_index = max_child_index
        else:
            break

    
if __name__ == '__main__':

    array = list(numpy.random.randint(0, 100, 20))
    print(array)
    a = heap_sort_asc(array)
    print(a)
排序前:[28, 93, 13, 51, 9, 97, 42, 8, 1, 57, 89, 10, 24, 73, 97, 24, 13, 41, 11, 1]
排序后:[1, 1, 8, 9, 10, 11, 13, 13, 24, 24, 28, 41, 42, 51, 57, 73, 89, 93, 97, 97]

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章