堆结构及堆排序详解
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
堆结构及堆排序详解
⼀、物理结构和概念结构
学习堆必须明确,堆有两个结构,⼀个是真实存在的物理结构,⼀个是有助于理解的概念结构。
1. 堆⼀般由数组实现,但是我们平时在理解堆的时候,会把他构建成⼀个完全⼆叉树结构。
堆分为⼤根堆和⼩根堆:⼤根堆,就是这颗树⾥的每⼀个结点都是以它为根结点的树中的最⼤值;⼩根堆则与之相反。
(注意⼀定要是完全⼆叉树)
2. 物理结构:从 0 开始的数组。
怎么将数组和⼆叉树联系起来呢?
当⼀个结点在数组中的下标为 index,那么这个结点对应的⽗节点的下标为( index-1 ) / 2,左孩⼦的下标为 2 * index +1 ,右孩⼦的下标为 2 * index +2 。
上⾯是以 0 开始的数组中各结点对应的关系,数组也可以以 1 开始,此时⽗节点下标为 index / 2,左孩⼦下标为 2 * index,右孩⼦下标为2 * index + 1。
有⼀个物理数组下标从 0 - 8
树结构为:
⼆、heapInsert
当数组中 0 ~ index -1 的位置已经是⼤根堆,现在添加⼀个元素到下标为 index ,需要怎么做才能继续保持⼤根堆的结构呢?
1. 将新增元素index 与⽗节点 ( index-1 ) / 2 ⽐较,若⽐⽗节点⼤,则与⽗节点交换位置;
2. 交换位置后,新增元素下标变为⽗节点的下标,再与现在这个节点的⽗节点⽐较,周⽽复始;
3. 直⾄新增节点不再⽐⽗节点⼤或者已经到达了根结点,则新增节点的插⼊位置确定
例⼦:现在有⼀个已经在 0 ~ 7 形成⼤根堆的数组 [ 24, 18, 20, 10, 9, 17, 8, 5 ] ,在下标为 8 的位置插⼊元素 22.
JAVA 实现:
public static void heapInsert(int[] arr, int index) {
// 停⽌条件1:新增结点不再⽐⽗节点⼤
// 停⽌条件2:已经到达了整棵树的根结点 0 ,当 index = 0,( 0-1)/2 =0,所以arr[index] 和 arr[(index - 1) / 2] 相等
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
三、heapify
当将最⼤值 pop 出去之后,需要对这个堆进⾏调整,最常⽤的就是,将堆结构中最后⼀个的数提到 0 下标,然后将这个数从 0 开始下沉。
某个数在 index 位置,看是否可以往下沉。
这就是 heapify。
当index 还有孩⼦节点时,⽐较左右两个节点的⼤⼩,选取节点值较⼤的⼀个,与index进⾏⽐较,若⼦节点的值较⼤,⽗节点下沉,较⼤孩⼦上来。
直⾄⽐孩⼦节点⼤或者没有孩⼦节点。
JAVA 实现:
public static void heapify(int[] arr, int index, int heapSize) {
int left = index * 2 + 1;// 左孩⼦的下标
while (left < heapSize) {// 下⽅还有孩⼦的时候
// 两个孩⼦中,谁的值⼤,把下标给 largest变量
int largest = (left + 1 < heapSize) && (arr[left + 1] > arr[left]) ? left + 1 : left;
// ⽗与较⼤的孩⼦之间,谁的值⼤,吧下标给 largest
largest = arr[largest] > arr[index] ? largest : index;
if (index == largest) {
break;
}
swap(arr, index, largest);
index = largest;
left = index * 2 + 1;
}
}
四、堆排序(⾮递减)
堆排序⼀共分为两步,第⼀步是将数组构建成⼀个⼤根堆,第⼆步将堆结构中的根结点,也就是最⼤值,移到堆的最后,然后将这个结点从堆中移除。
1. 将数组构建成⼀个⼤根堆。
有两种⽅法,但是这两种⽅法会有不同的事件复杂度。
(1)使⽤ heapInsert。
index 从 1 开始(因为从 0 开始的话,只有⼀个元素,没有必要),直⾄最后⼀个元素 arr.length -1 ,每个元素都使⽤ heapInsert 的⽅式,逐个形成[ 0,..., index] 的⼤根堆.
for (int index = 1; index < arr.length; index++) {// O(N)
heapInsert(arr, index);// O(logN)
}
每个元素最多向上进⾏ log2(N)次⽐较和交换,⼀共有 N 各元素,所以需要 O(NlogN) 的事件复杂度。
这种⽅法⽐较适合逐个向数组中添加元素,但是此时排序,传进来的是整个数组,所以我们有⼀种可以将事件复杂度降低为 O(N) 的⽅式。
(2)使⽤ heapify。
从最后⼀个元素开始,不断下沉,使得以这个元素为根结点的数形成堆结构。
为什么说事件复杂度为 O(N) 呢?
Java 实现:
for (int index = arr.length - 1; index >= 0; index--) {
heapify(arr, index, arr.length);
}
2. 将堆结构中的根结点,也就是最⼤值,移到堆的最后,然后将这个结点从堆中移除。
将堆中最⼤值逐个放在 arr.length-1,arr.length-2,...,1 的位置。
使⽤ heapSize 记录堆中元素得个数,将堆中最⼤值往后,其实就是和堆中最后⼀个元素(下标为 heapSIze-1)交换位置,此时最后⼀个元素到达根结点 0 。
交换完成后 heapSize--,意味着这个元素已经排序完成并将它从堆中移除,再调⽤ heapify 将提到根结点 0 得元素下沉。
事件复杂度为 O(logN)
int heapSize = arr.length;// 记录堆中元素的个数,如果⼀个数排序完成(放在堆数组的最后),就将他从堆数组中除去(heapSize--);
swap(arr, 0, --heapSize);// 将堆中的最⼤值放在数组的最后,此时最⼤值排序完成,堆数组的个数减⼀
while (heapSize > 0) {// O(N)
heapify(arr, 0, heapSize);// O(logN)
swap(arr, 0, --heapSize);// O(1)
}
形成堆结构,我们采⽤第⼆种⽅法,所以最后堆排序的代码为:
// 堆排序
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
// 形成⼤根堆:
// O(N)
for (int index = arr.length - 1; index >= 0; index--) {
heapify(arr, index, arr.length);
}
// 将堆中最⼤值逐个放在 arr.length-1,arr.length-2,...,1 的位置
// O(N*logN)
int heapSize = arr.length;
swap(arr, 0, --heapSize);
while (heapSize > 0) {// O(N)
heapify(arr, 0, heapSize);// O(logN)
swap(arr, 0, --heapSize);// O(1)
}
}。