堆排序

[scode type=”yellow”]堆排序思路[/scode]

  1. 将传入的数组看作是一个没有完成的堆
  2. 将堆整理排序成一个大顶堆
  3. 将大顶堆最大的元素,也就是堆顶,与这个堆最后的元素进行交换
  4. 然后视这个除刚刚交换的那个元素外的数组为一个堆,对它进行大顶堆标准检查,并将其整理成一个大顶堆
  5. 有一点需要注意的是每次交换之后接下来需要接着排序的堆的大小需要减一!
  6. 为了减小空间的占用,可以视交换到末尾的元素为已经出堆的元素,仅仅对这些元素之前的数组进行大顶堆检查。

[scode type=”yellow”]大顶堆概念[/scode]

​ 大顶堆是一个被完全填满的二叉树,除了堆底部的叶子节点以外.其父节点必定要大于它的子节点,由这个特性,我们可以构造大顶堆。

[scode type=”yellow”]堆的数列表示[/scode]

​ 设其父节点的下标为i,那么它的左儿子的下标就是2i+1,其右儿子的下标就是左儿子加一也就是2i+2

​ 由上面这个下标的规律我们可以直接从整个堆数组的中部开始遍历整个堆,因为观察堆的结构可知,length/2 - 1下标的节点是必定存在有左儿子的,所以我们不用担心下标越界的问题。

[scode type=”yellow”]堆的遍历与调整[/scode]

​[scode type=”blue”]遍历的本质就是将每一个元素都访问到,这里可以用递归,也可以用循环,递归对大数据并不友好,所以应该视情况而定!

​ 调整的话主要是实现父节点与子节点的比较,如果父节点小于子节点的较大者,那么就下沉。这里小于较大者的原因是:因为小的元素要下沉就必须要跟两个子树中较大的那个进行交换,否则如果跟较小的节点进行交换的话可能还需要交换两次!。[/scode]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/**
* MyHeapSort
*/
public class MyHeapSort {
public static void main(String[] args) {
int[] arr = { 50, 10, 90, 30, 70, 40, 80, 60, 20 };
System.out.println("排序之前:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}

// 堆排序
heapSort(arr);

System.out.println();
System.out.println("排序之后:");
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
public static void heapSort(int[] arr) {
// Create a big top heap
for (int i = arr.length/2; i >=0; i--) {
heapAdjust(arr, i, arr.length);
}
// 将堆顶最大的那个元素和后面的元素进行交换,然后再生成大顶堆
for (int i = arr.length-1; i >= 0; i--) {
// 交换堆顶和队列中最后一个元素的值
exchange(arr, 0, i);
// 判断这个队列是否能构成大顶堆不能的话就调整
// 这里调整的实质是:只有堆顶一个元素需要调整,因此只需要将堆顶的元素下沉到相应的位置就好了不需要调整太多元素!!
heapAdjust(arr, 0, i);
}
}
public static void heapAdjust(int[] arr,int root,int length) {
// 以 root节点为根节点,遍历这个长度为length的子树,判断其是否满足大顶堆的要求
int child; // 子节点的值
int father; // 父节点的值
for(father = arr[root];2*root+1 < length;root = child){
child = 2*root+1; // 以root节点为根节点的左子树的下标

// 如果这个根节点的左子树不是这个堆的最后一个元素并且左子树小于右子树。那么就把下标指向右子树
// 因为小的元素要下沉就必须要跟两个子树中较大的那个进行交换,否则如果跟较小的节点进行交换的话可能还需要交换两次!
if (child != length-1 && arr[child] < arr[child+1]) {
child++;
}
// 如果此时的父节点小于子树中的那个较大者,就与之交换!
if (father < arr[child]) {
arr[root] = arr[child]; // 父节点的原有值已经记录在father这个变量内了!
}else{
// 说明该节点符合大顶堆的标准,退出for循环
break;
}
}
// 由于当前下表为root的节点值可能是废弃的,即已经被交换过,所以这个节点位置的应有值是该移动节点的值,即最初始的父节点
arr[root] = father;
}
public static void exchange(int[] arr,int began,int end) {
int temp = arr[began];
arr[began] = arr[end];
arr[end] = temp;
}

// 下面是我自己探索出来的堆排序,使用了过多的递归,导致空间爆炸,效率很低


/**
* 堆排序
*
* @param A
*/
public static void heapSort(int[] A, int index) {
// A 是要排序的数组
int[] temp = createHeap(A,index);
int temp1 = temp[0];
temp[0] = temp[temp.length-1-index];
temp[temp.length-1-index] = temp1;
if (index == A.length-1) {
return;
}
heapSort(temp, ++index);

}
/**
* 构造小顶堆
*
* @param A
*/
public static int[] createHeap(int[] A,int max) {
int[] B = new int[A.length-max];
for (int i = 0; i < B.length; i++) {
B[i] = A[i];
}
for (int i = B.length / 2 - 1; i >= 0; i--) {
comparePartOfHeapSort(B, i);
}
for (int i = 0; i < B.length; i++) {
A[i] = B[i];
}
return A;
}
public static void comparePartOfHeapSort(int[] A, int index) {
// 控制元素的下沉
int j = index * 2 + 1;
int k = index * 2 + 2;
if (j < A.length) {
if (A[index] < A[j]) {
// 比左子树要小,那么进行交换
int temp = A[index];
A[index] = A[j];
A[j] = temp;
comparePartOfHeapSort(A, j);
}
if (k < A.length && A[index] < A[k]) {
// 比左子树要大,且右子树存在时,就跟右子树进行比较
// 比右子树要小,那么交换
int temp = A[index];
A[index] = A[k];
A[k] = temp;
comparePartOfHeapSort(A, k);
}
// 比左右节点都要大,那么就说明该节点在这个位置是正确的,然后返回
return;
} else {
// 连左子树都不存在,说明该节点已经为叶子节点
return;
}
}

}
Author: TankNee
Link: https://www.tanknee.cn/2019/11/13/%E5%A0%86%E6%8E%92%E5%BA%8F/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.