目录
疫情之后复工复产,工作又开始忙起来了,加上自己在研究数学公式可视化编辑,比较费神费力,最近文章就写比较少了,但是不能一直停下来,既然已经开始,就有始有终,刚好最近使用堆排序算法,于是今天决定把堆排序算法记录下来。
堆排序算法
对于个元素的序列,当且仅当满足下面关系时称其为堆。
或
其中,和应该小于等于。
如果将此序列对应的一维数组,即以一维数组作为序列的存储结构,看成一个完成二叉树,则完全二叉树中所有非根结点的值都不小于(或者大于)其左右子结点的值。因此,在一个堆中,堆顶的元素(就是根结点)必为序列中的最大元素(或者最早元素),并且堆中的任一棵子树也都是堆。若堆顶为最小元素,则称为小跟堆;若堆顶为最大元素,则称为大根堆。
堆排序思想
下面我们以大根堆为例子说明,对于一直等待排序序列,首先按照堆的定义构建成一颗完全二叉树排序序列,这时序列称为初始堆,从而可以输出堆顶的最大值,然后将剩下的序列再次调整成新堆,得到第二个大的值,依此类推,直到序列被排成有序序列为止。
要想建立大根堆,必须先构建初始堆,初始堆的建立方法是:将待排序的序列看成一颗完全二叉树是不具备堆的性质,因为,序列中所有大于的元素都没有子结点,他们可以被看成是没有子树的堆,因此构建初始堆可以从完全二叉树的第结点开始,通过调整之后使得、、、...、、为根的子树满足堆的定义。
上面可能还是一头雾水,结合画图之后,就茅塞顿开了。
构建初始堆
设待排序序列,构建初始堆:
可以看出来,完全二叉树按照根结点为,子结点为和构建,所有大于=4的结点,都在完全二叉树的叶子结点上,即没有子树。再根据、、、...、、进行调整,如下图所示:
当=4时,
====>
当 -1=3时,
====>
当 -2=2时,
====>
当 -3=1时,
====>
调整之后,发现如上图的两个红色结点不满足堆的定义,也就是说,在对为根的子树构建堆的过程中,可能会不断交换、和三个值,最终的结果是,以 或者为根的子树不再满足堆的定义,之后还得继续以 或者为根进行调整,直到所有非叶子结点为根的子树都满足堆的定义为止。最后继续调整如下图所示:
====>
这样,初始堆就构建完成了,堆顶80即为序列中最大值。
调整新堆
建立初始堆已经得到根结点80是最大值,此时将堆顶根序列最后一个值做交换(本例子按照递增排序),得到序列,再次将序列的前个元素调整成新堆,如下图所示:
====>
上面右图就表示了前个元素构建的完全二叉树,但还不一定满足堆的定义,得继续调整满足堆的定义。
当 =4、 -1=3、 -2=2时, 对为根的子树已经满足了堆定义,不需要调整;
当-3=1时,
====>
此时,K2、K4、K5不满足堆定义,需要调整,最后得到以75为根结点的大根堆:
====>
如此一层一层将构建和调整满足大根堆,直到剩下最后一个元素为止,这也就得到了最后的排序序列。
代码设计
var arr = [55, 60, 40, 10, 80, 65, 15, 5, 75];
console.log("待排序序列:", arr);
// 初始堆
function HeapInit(list, cur, len) {
var tmp = 0;
var i = 0;
tmp = list[cur];
for (i = 2 * cur + 1; i < len; i = 2 * i + 1) {
if (i < len && list[i] < list[i + 1]) {// 左右子结点比较
i++;
}
if (tmp >= list[i]) break; // 如果已经满足了堆定义直接返回
list[cur] = list[i];
cur = i;
}
list[cur] = tmp;
}
// 对排序
function HeapSort(list, len) {
var i = 0;
var tmp = 0;
for (i = parseInt(len / 2) - 1; i >= 0; i--) {
HeapInit(list, i, len - 1);
}
for (i = len - 1; i > 0; i--) {
tmp = list[0];
list[0] = list[i];
list[i] = tmp; // 交换最后一个元素和第一个元素
HeapInit(list, 0, i - 1); // 继续调整堆
}
console.log('堆排序结果:', list);
}
HeapSort(arr, arr.length);
结果如下:
根据上面代码可以看出,堆排序的时间复杂度为。
总结
对于序列较少的情况下,堆排序优势其实并不是很明显,但对于大量数据排序,堆排序是非常有效的,堆排序主要是的费时点在于构建初始堆和不断调整满足堆定义两部分上面。堆排序是一种不稳定的排序算法。