• 欢迎光临~

堆排序

开发技术 开发技术 2022-08-05 次浏览

堆可以理解成一个完全二叉树,树上的每个节点都对应着一个元素。
存储堆的数组A通常包括两个属性:
A.length 给出数组元素的个数
A.heap-size 存储在数组中的堆元素的数量
也就是说,虽然[1, A.length]中可能都存有数据,但实际上只有[1, A.heap-size]中存放的是有效元素。
使树的根节点是A[1], 这样给出一个节点的下标i,我们可以很快计算出它的父节点,左子节点和右子节点的下标:

PARENT(i)
    return ⌊i/2⌋

LEFT(i)
    return 2*i

RIGHT(i)
    return 2*i+1

二叉堆可以分为两种形式:最大堆和最小堆。在这两种堆中,结点的值都要满足堆的性质,但一些细节定义则有所差异。
在最大堆中,最大堆性质是指除了根节点以外的所有节点i都要满足:A[PARENT(i)] >= A[i]
也就是说,某个节点的值至多与其父节点一样大。因此,队中的最大元素存放在根节点中。
而最小堆与最大堆正好相反。
如果把堆看成是一棵树,我们定义一个堆中的结点的高度就为该节点到叶子节点最长简单路径上边的数目;进而我们可以把堆的高度定义为根节点的高度。既然一个包含n个元素的队可以看作一棵完全二叉树,那么堆的高度是lgn。那么堆结构上的一些操作最多也就是O(lgn)的时间复杂度。

MAX-HEAPIFY过程:其时间复杂度为O(lgn),它是维护最大堆性质的关键。
BUILD-MAX-HEAP过程:具有线性的时间复杂度,功能是从无序的输入数组中构造一个最大堆。
HEAPSORT过程:其时间复杂度为O(nlgn),功能是对一个数组进行原址排序。
MAX-HEAP-INSERT, HEAP-EXTRACT-MAX, HEAP-MAXIMUM过程:时间复杂度O(lgn),功能是利用堆实现一个优先队列。

维护堆的性质

MAX-HEAPIFY是用于维护最大堆性质的重要过程,它的输入是一个数组A和一个下标i。
在调用MAX-HEAPIFY的时候,我们假定根节点为LEFT(i)和RIGHT(i)的二叉树都是最大堆,但这时A[i]有可能小于其孩子,就违反了最大堆的性质。MAX-HEAPIFY通过让A[i]的值在最大堆中“逐级下降”,从而使得下标i为根节点的子树遵循最大堆的性值。

MAX-HEAPIFY(A, i)
    l = LEFT(i)
    r = RIGHT(i)
    if l <= A.heap-size and A[l] > A[i]
	largest = l
    else largest = i
    if r <= A.heap-size and A[r] > A[largest]
	largest = r
    if laegest ≠ i
	exchange A[i] with A[largest]
	MAX-HEAPIFY(A, largest)

解释一下上面伪代码的过程:
首先我们要在A[i]和它的子节点中找出最大值,若A[i]不是最大值,那么根据最大堆的性质要将最大值位置的元素与A[i]进行交换,但交换后有可能破坏对应子树的最大堆结构,所以要再次将该子树维护一下最大堆性质,直到到叶子节点。显而易见,这个过程的时间复杂度为O(h),h为树高。

建堆

我们可以用自底向上的方法利用过程MAX-HEAPIFY把一个数组转化为最大堆。

BUILD-MAX-HEAP(A)
    A.heap-size = A.length
    for i = ⌊A.length/2⌋ downto 1
	MAX-HEAPIFY(A, i)

可以看到,对于每个节点,我们都进行了一次维护最大堆性质的操作。由于我们是自底向上进行维护的,而叶子节点没有子节点,一定满足最大堆性质,所以显而易见这个思路是正确的。

堆排序算法

初始时候,堆排序算法利用BUILD-MAX-HEAP将数组建成最大堆,然后我们将A[1]与A[n]交换位置,再使A.heap-size - 1,相当于从堆中移除了这个值,然后再将A[1]进行MAX-HEAPIFY操作放到对应的位置维护最大堆,不断重复这个过程,我们就可以得到一个从小到大排序的数组。

HEAPSORT(A)
    BUILD-MAX-HEAP(A)
    for i = A.length downto 2
	exchange A[1] with A[i]
	A.heap-size = A.heap-size - 1
	MAX-HEAPIFY(A, 1)

代码实现

我们先将上面的伪代码换成好理解的文字来看一下各个区域的作用:

//首先使用宏或者内联函数对i的左子节点和右子节点以及父节点的坐标进行定义

void MaxHeapify(int* A, int i, int heapSize){
    //定义好左右子节点的坐标
    //找到左右子节点中的最大值以及判断是否还在有效区间内,定义变量记录
    //与节点的值进行比较,若大于节点值则进行交换,并维护对应位置的子树
}

void BuildMaxHeap(int* A, int length){
    //初始有效区间为数组A内的元素数量
    //自底向上维护最大堆的性质
}

void HeapSort(int* A, int length){
    //首先建堆
    //将堆建好后依次将最大值与末尾元素交换,然后再维护最大堆
    //将有效区间范围减少
}

然后再照着上面的思路进行代码实现:

inline int left(int i){
    return 2 * i;
}
inline int right(int i){
    return 2 * i + 1;
}
inline int parent(int i){
    return i / 2;
}

void MaxHeapify(int* A, int i, int heapSize){
    int l = left(i);
    int r = right(i);
    int largest = 0;
    if(l <= heapSize && A[l] > A[i]) largest = l;
    else largest = i;
    if(r <= heapSize && A[r] > A[largest]) largest = r;
    if(largest != i){
	swap(A[i], A[largest]);
	MaxHeapify(A, largest, heapSize);
    }
}

void BuildMaxHeap(int* A, int length){
    for(int i = length/2; i >= 1; i--){
	MaxHeapify(A, i, length);
    }
}

void HeapSort(int* A, int length){
    BuildMaxHeap(A, length);
    for(int i = length; i >= 2; i--){
	swap(A[i], A[1]);
	length--;
	MaxHeapify(A, 1, length);
    }
}
程序员灯塔
转载请注明原文链接:堆排序
喜欢 (0)