堆结构相关操作和堆排序的理解
总述
堆结构是用数组实现的完全二叉树结构,其完全二叉树中如果每棵子树的最大值在顶部就是大根堆(根重),最小值在顶部就是小根堆(根轻);
理解堆结构最重要的思想就是它在抽象上是一颗或许具有大根堆\小根堆性质的完全二叉树,而在代码里它是一个数组。这个双重身份是理解堆结构的关键。
关于堆的最重要的两个操作就是heapInsert操作(数向上浮)和heapify操作(数向下落):
以大根堆为例,因为大根堆的规则是头重脚轻,所以当有大的数据在较下面的时候,得上浮到二叉树中正确的位置;若有小的数在较上面,得下落到二叉树中正确的位置,才能形成大根堆。所以有了此两个操作。小根堆同理。
heapInsert操作在完全二叉树上的抽象含义就是将要向树尾插入的数据或原本在不正确位置的数据(此数需得上浮)向子树根节点层层向上跃迁到它的值所应该在的位置,以此操作维持此完全二叉树的大/小根堆结构。
heapify操作在完全二叉树上的抽象含义就是将要向树根插入的数据或原本在不正确位置的数据(此数需得下落)向它的大的孩子层层下落到它的值应该所在的位置,以此操作维持此完全二叉树的大/小根堆结构。
以上讨论了heapInsert与heapify的抽象描述,而在代码层面这两个操作是以操作完全二叉树数组的方式实现的。这些操作皆利用到了完全二叉树映射到数组关系。
总述中以完全二叉树抽象方式描述了heapInsert和heapify及堆排序的操作,而在代码层面,关于堆的各种操作是堆构成完全二叉树的数组进行操作来实现的。接下来讨论代码的具体实现。
完全二叉树的数组实现与其节点之间的关系
若树其中一个节点为在数组上的位置为i
-
则其左孩子位置是2*i+1
-
右孩子位置是2*i+2。
-
它的爸爸是(int)(i - 1) / 2
所以完全二叉树的数组可以实现时间复杂度为O(1)的随机存取。
heapInsert操作的实现(log(N))
heapInsert效果为:当用户一次次在数组最后插入数据时,仍使整个完全二叉树保持大根堆状态。
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; //然后位置移动到此节点上,继续与其新的父节点相比较
}
}
代码在完全二叉树上的抽象含义流程是:
-
在完全二叉树(大根堆)最后插入一个数,若此数比其父节点大,则与父节点交换;
-
交换后若比父节点大,则继续交换,直到比父节点小为止。
heapiyf操作的实现(log(N))
引例:在一个大根堆中,若用户想返回此根堆的最大值,并将其从大根堆移除,移除后让剩下的这些数仍然形成大根堆。
首先,最大值直接用一个变量将arr[0]存储就行。那么如何能将其移除后仍使新数组继续保持大根堆形式呢?
- 将数组中最后一个数复制在大根堆根上(即复制到arr[0]上,覆盖原来的数,这样原来的数就被移除了),同时heapSize--(即这最后一个数被踢出大根堆)
- 然后从这个数(现在在大根堆顶)的两个孩子里选大的那个孩子,若它这个大孩子比它大,则它与这个大孩子交换。
- 交换完继续找到这个数的新的大孩子,若它大孩子比它大,则交换。
- 重复3,直到它没孩子,或者它的孩子都比它小,则停止交换。
- 此时数组为大根堆。
//某个数在index位置,能否向下移动
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;
//经过上两行代码的折腾,largest下标就是父亲和孩子整个环境中的那个最大值的下标。
if(largest == index){ //这个最大值的下标就是父亲的下标,则父亲停止移动
break;
}
//如果largest不等于index,那么父一定要往下走的。
swap(arr,largest,index);//父亲和大孩子交换
index = largest;//此时父亲的下标为它之前大孩子的下标
left = index * 2 + 1; //父亲现在的左孩子下标
}
}
堆排序的实现
而堆排序正与以上两个操作有关。堆排序的抽象实质是:
堆排序简要地讲:
将想要排序的杂乱数组形成大根堆;之后把最大的根节点和此时数组最后一个元素交换,这最后一个元素就是排完序的第一个元素(最大),同时大根堆容量-1;重新形成大根堆,把最大根节点和此时数组最后一个元素交换,大根堆容量-1;以此往复,直到大根堆容量=0;此时数组为从小到大排列。
堆排序详细的讲就是:
- 将给定想要排序的数组的每个元素进行heapInsert操作,操作完成后,此数组顺序就形成了以完全二叉树为抽象逻辑的大根堆;
- 此时完全二叉树(大根堆)最顶的根节点(数组下标为0)的元素就是此数组中最大值,将此元素与数组中最后一个元素交换,同时规定heapsize--;至此,数组中最大的元素就跑到了原全数组最后一位,且这个在数组最后一位的最大值数在逻辑上被踢出了大根堆(被踢出了构成根堆的数组,因为heapSize规定被减了1);
- 然后对此时的整个树的根节点(最开始的时候数组的最后一位数因为交换跑到了这里)进行heapify操作,于是它向下层层递落到正确的位置,使整个完全二叉树重新形成大根堆;
- 将重新形成大根堆的根节点(数组中第一个元素)与数组中最后一个元素交换,heapSize--;
- 重复3,4,操作,直至heapSize > 0 为false(即大根堆中的数全部。
- 此时,数组为从小到大顺序排列。
public static void heapSort(int [] arr){
if(arr == null || arr.length < 2){
return;
}
//将数组整理为大根堆
for(int i = 0; i < arr.length; i++){
heapInsert(arr,i);
}
int heapSize = arr.length;
swap(arr,0,--heapSize);
while(heapSize > 0){
heapify(arr,0,heapSize);
swap(arr,0,--heaspSize);
}
}
堆排序的扩展题目
本文作者:逐东
本文链接:https://www.cnblogs.com/vuds/articles/15625954.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步