在堆结构中写到了将无序数组利用大根堆进行排序。其时间复杂度为 O ( N ∗ l o g N ) O(N*logN) O(N∗logN),但还有一种更为简便的时间复杂度为 O ( N ) O(N) O(N)的构建堆的方式。那就是从下到上的构建。
从上到下的方式:从无序数组0位置开始遍历,数组中元素每一次加入到大根堆后,都会heapInsert寻找父节点进行比较,直到达到父节点或小于父节点终止。
从下到上的方式:从无序数组尾端开始遍历,数组中元素每一次加入到大根堆后,heapify下沉,看是否有左右子节点是比自己大的,如果有则交换。
假设一共为N个节点的话,那最下层叶节点(没有子节点)的节点数量就为 N / 2个,这些节点在heapify时,因为是最末端的节点,所以在left = i * 2 + 1寻找左子节点时,会发现找不到,直接就不用进行比较,最多看一个数,所以是 N / 2 * 1,到底第二层节点数量为 N / 4,它们在heapify时,最多往下沉1个单位,最多看两个数,所以是 N / 4 * 2,第三层节点数量为 N / 8,它们在heapify时,最多下沉2个单位,最多看3个数,所以是 N / 8 * 3。
那么所有的时间复杂度就是 N / 2 * 1 + N / 4 * 2 + N / 8 * 3…
T(N) = N / 2 * 1 + N / 4 * 2 + N / 8 * 3 左右两边同时 * 2。
2T(N) = N + N / 2 * 2 + N / 4 * 3…
用下面减上面(错位相减) : T(N) = N + N / 2 + N / 4 + N / 8 ,等比数列。转化后得到 O(N)。
本质区别:
从上往下建堆时,因为二叉树的层高是logN,所以从每次加入新节点都需要 ( i - 1 ) / 2的方式来寻找父节点,最差的情况就是每次都需要进行比较和交换。每一次承担的复杂度是log i,i为此时高度。所以整体的时间内复杂度为O(N * logN)。
从下往上建堆时,必须要从数组尾端开始遍历,因为每次都会left = i * 2 + 1寻找左右子节点,如果从0开始遍历,可能就不是堆结构了,而先构建的最末端的叶子节点的个数为 N / 2,此时虽然heapify进行比较,但是根本不用交换,在根据上面的公式推导,所以时间复杂度是O(N)
代码实现:
//从下构建
for (int i = arr.length - 1; i <= 0; i--) {
heapify(arr,i,arr.length);
}
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;
while (left > heapSize) {
//找出左右节点中较小的一个数
int least = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
//当前数和左右子节点中较小的数作比较
least = arr[index] > arr[least] ? index : least;
//如果当前数最小 break;
if (least == index) {
break;
}
//走到这说明当前数小,交换
swap(arr, least, index);
//交换完之后,当前数来到了左右子节点位置,需再次计算左右子节点作比较。
index = least;
//重新计算左子节点位置
left = index * 2 + 1;
}
}
//从上构建
for (int i = 0; i <= k; i++) {
heapInsert(arr, i);
}
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}