algorithm-base/animation-simulation/数据结构和算法/堆排序.md
2021-07-23 15:44:19 +00:00

335 lines
15 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### **堆排序**
> 如果阅读时发现错误或者动画不可以显示的问题可以添加我微信好友 **[tan45du_one](https://raw.githubusercontent.com/tan45du/tan45du.github.io/master/个人微信.15egrcgqd94w.jpg)** ,备注 github + 题目 + 问题 向我反馈
>
> 感谢支持该仓库会一直维护希望对各位有一丢丢帮助
> 刷题网站
> 另外希望手机阅读的同学可以来我的 <u>[**公众号袁厨的算法小屋**](https://raw.githubusercontent.com/tan45du/test/master/微信图片_20210320152235.2pthdebvh1c0.png)</u> 两个平台同步,想要和题友一起刷题,互相监督的同学,可以在我的小屋点击<u>[**刷题小队**](https://raw.githubusercontent.com/tan45du/test/master/微信图片_20210320152235.2pthdebvh1c0.png)</u>进入。
说堆排序之前我们先简单了解一些什么是堆堆这种数据结构应用场景非常多所以我们需要熟练掌握呀
那我们了解堆之前先来简单了解下什么是完全二叉树
我们来看下百度百科的定义完全二叉树叶子结点只能出现在最下层和次下层且最下层的叶子结点集中在树的左部
我们可以这样理解除了最后一层其他层的节点个数都是满的而且最后一层的叶子节点必须靠左
下面我们来看一下这几个例子
![微信图片_20210316124303](https://cdn.jsdelivr.net/gh/tan45du/test@master/photo/微信图片_20210316124303.1lo4nr3xhrwg.jpg)
上面的几个例子中14为完全二叉树23不是完全二叉树通过上面的几个例子我们了解了什么是完全二叉树
那么堆到底是什么呢
下面我们来看一下二叉堆的要求
1必须是完全二叉树
2二叉堆中的每一个节点都必须大于等于或小于等于其子树中每个节点的值
若是每个节点大于等于子树中的每个节点我们称之为大顶堆小于等于子树中的每个节点我们则称之为小顶堆见下图
下面我们再来看一下二叉堆的具体例子
![](https://cdn.jsdelivr.net/gh/tan45du/test@master/photo/微信截图_20210223221833.6slujxq1cb40.png)
上图则为大顶堆和小顶堆我们再来回顾一下堆的要求看下是否符合
1必须是完全二叉树
2堆中的每一个节点都必须大于等于或小于等于其子树中每个节点的值
好啦到这里我们已经完全掌握二叉堆了那么二叉堆又是怎么存储的呢因为堆是完全二叉树所以我们完全可以用数组存储具体思想见下图我们仅仅按照顺序将节点存入数组即可我们通过小顶堆进行演示
我们是从下标 1 开始存储的这样能省略一些计算下文中我们将二叉堆简称为堆
![](https://cdn.jsdelivr.net/gh/tan45du/test@master/photo/微信截图_20210223223621.3juf4t4hc9a0.png)
我们来看一下为什么我们可以用数组来存储堆呢
我们首先看根节点也就是值为 1 的节点它在数组中的下标为 1 ,它的左子节点也就是值为 4 的节点此时索引为 2右子节点也就是值为 2 的节点它的索引为 3
我们发现其中的关系了吗
数组中某节点非叶子节点的下标为 i , 那么其**左子节点下标为 2\*i** 这里可以直接通过相乘得到左孩子如果从 0 开始存需要 2*i+1 才行, 右子节点为 2*i+1****其父节点为 i/2 既然我们完全可以根据索引找到某节点的 **左子节点** **右子节点**那么我们用数组存储是完全没有问题的
好啦我们知道了什么是堆和如何用数组存储堆那我们如何完成堆排序呢
堆排序其实主要有两个步骤
- 建堆
- 排序
下面我们先来了解下建堆
我们刚才说了用数组来存储大顶小顶此时的元素已经满足某节点大于等于或小于等于子树节点但是随机给我们一个数组此时并不一定满足上诉要求所以我们需要调整数组使其满足大顶堆或小顶堆的要求这个就是堆化也可以称其为建堆
建堆我们这里提出两种方法利用上浮操作也就是不断插入元素进行建堆另一种是利用下沉操作遍历父节点不断将其下沉进行建堆我们一起来看吧
我们先来说下第一种建堆方式
**利用上浮操作建堆**
说之前我们先来了解下如何往已经建好的堆里插入新的元素我们直接看例子吧一下就懂啦
![](https://img-blog.csdnimg.cn/20210317193005203.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzODg1OTI0,size_16,color_FFFFFF,t_70#pic_center)
假设让我们插入新的元素 1 绿色节点我们发现此时 1 小于其父节点 的值 7 并不遵守小顶堆的规则那我们则需要移动元素 1 1 7 交换如果新插入元素大于父节点的值则说明插入新节点后仍满足小顶堆规则无需交换
之前我们说过我们可以用数组保存堆并且可以通过 i/2 得到其父节点的值那么此时我们就明白怎么做啦
![](https://img-blog.csdnimg.cn/20210317192914891.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzODg1OTI0,size_16,color_FFFFFF,t_70#pic_center)
将插入节点与其父节点交换
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210317192922435.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMzODg1OTI0,size_16,color_FFFFFF,t_70#pic_center)
交换之后我们继续将新插入元素也就是 1 与其父节点比较如果大于其父节点则无需交换循环结束若小于则需要继续交换直到 1 到达适合他的地方大家是不是已经直到该怎么做啦下面我们直接看动图吧
![](https://img-blog.csdnimg.cn/20210317193205782.gif#pic_center)
看完动图是不是就妥了其实很简单我们只需将新加入元素与其父节点比较判断是否小于堆顶元素小顶堆如果小于则进行交换让更小的节点为父节点直到符合堆的规则位置大顶堆则相反
**我们发现我们新插入的元素是不是一层层的上浮直到找到属于自己的位置我们将这个操作称之为上浮操作**
那我们知道了上浮岂不是就可以实现建堆了是的我们则可以依次遍历数组就好比不断往堆中插入新元素直至遍历结束这样我们就完成了建堆这种方法当然是可以的
我们一起来看一下上浮操作代码
Java Code:
```java
public void swim (int[] nums, int index) {
while (index > 1 && nums[index/2] > nums[index]) {
swap(index/2,index);//交换
index = index/2;
}
}
```
Python Code:
```python
def swim(nums: int, index: int):
while index > 1 and nums[int(index/2)] > nums[index]:
swap(int(index/2), index)# 交换
index = int(index/2)
```
既然利用上浮操作建堆已经搞懂啦那么我们再来了解一下利用下沉操作建堆吧也很容易理解
给我们一个无序数组不满足堆的要求见下图
![](https://cdn.jsdelivr.net/gh/tan45du/test@master/photo/微信截图_20210309143155.2fhvnp8lqe4g.png)
我们发现7 位于堆顶但是此时并不满足小顶堆的要求我们需要把 7 放到属于它的位置我们应该怎么做呢
废话不多说我们先来看视频模拟看完保准可以懂
![](https://img-blog.csdnimg.cn/20210317193217911.gif#pic_center)
看完视频是不是懂个大概了但是不知道大家有没有注意到这个地方为什么 7 第一次与其左孩子节点 2 交换第二次与右孩子节点 3 交换见下图
![](https://cdn.jsdelivr.net/gh/tan45du/test@master/photo/微信截图_20210309145953.1byz4zq0cx6o.png)
其实很容易理解我们需要与孩子节点中最小的那个交换因为我们需要交换后父节点小于两个孩子节点如果我们第一步7 5 进行交换的话则同样不能满足小顶堆
那我们怎么判断节点找到属于它的位置了呢主要有两个情况
- 待下沉元素小于大于两个子节点此时符合堆的规则无序下沉例如上图中的 6
- 下沉为叶子节点此时没有子节点例如 7 下沉到最后变成了叶子节点
我们将上面的操作称之为下沉操作
这时我们又有疑问了下沉操作我懂了但是这跟建堆有个锤子关系啊
不要急我们继续来看视频这次我们通过下沉操作建个大顶堆
> **初始数组 [8,5,7,9,2,10,1,4,6,3]**
![](https://img-blog.csdnimg.cn/20210317193229153.gif#pic_center)
我们一起来拆解一下视频我们只需要从最后一个非叶子节点开始依次执行下沉操作执行完毕后我们就能够完成堆化是不是一下就懂了呀
好啦我们一起看哈下沉操作的代码吧
Java Code:
```java
public void sink (int[] nums, int index,int len) {
while (true) {
//获取子节点
int j = 2 * index;
if (j < len-1 && nums[j] < nums[j+1]) {
j++;
}
//交换操作,父节点下沉,与最大的孩子节点交换
if (j < len && nums[index] < nums[j]) {
swap(nums,index,j);
} else {
break;
}
//继续下沉
index = j;
}
}
```
Python Code:
```python
def sink(nums: list, index: int, len: int):
while True:
# 获取子节点
j = 2 * index
if j < len-1 and nums[j] < nums[j+1]:
j += 1
# 交换操作父节点下沉与最大的孩子节点交换
if j < len and nums[index] < nums[j]:
swap(nums, index, j)
else:
break
# 继续下沉
index = j
```
好啦两种建堆方式我们都已经了解啦那么我们如何进行排序呢
了解排序之前我们先来看一下如何删除堆顶元素我们需要保证的是删除堆顶元素后其他元素仍能满足堆的要求我们思考一下如何实现呢见下图
![](https://cdn.jsdelivr.net/gh/tan45du/test@master/photo/微信截图_20210309200153.3jx6dvweliq0.png)
假设我们想要去除堆顶的 11 那我们则需要将其与堆的最后一个节点交换也就是 2 2 然后再执行下沉操作执行完毕后仍能满足堆的要求见下图
![](https://cdn.jsdelivr.net/gh/tan45du/test@master/photo/微信截图_20210309200301.5zqydpf44mo0.png)
好啦其实你已经学会如何排序啦你不信那我给你放视频
![](https://img-blog.csdnimg.cn/20210317193246980.gif#pic_center)
好啦大家是不是已经搞懂啦下面我们总结一下堆排序的具体执行过程
1.建堆通过下沉操作建堆效率更高具体过程是找到最后一个非叶子节点然后从后往前遍历执行下沉操作
2.排序将堆顶元素代表最大元素与最后一个元素交换然后新的堆顶元素进行下沉操作遍历执行上诉操作则可以完成排序
好啦下面我们一起看代码吧
Java Code:
```java
class Solution {
public int[] sortArray(int[] nums) {
int len = nums.length;
int[] a = new int[len + 1];
for (int i = 0; i < nums.length; ++i) {
a[i+1] = nums[i];
}
//下沉建堆
for (int i = len/2; i >= 1; --i) {
sink(a,i,len);
}
int k = len;
//排序
while (k > 1) {
swap(a,1,k--);
sink(a,1,k);
}
for (int i = 1; i < len+1; ++i) {
nums[i-1] = a[i];
}
return nums;
}
public void sink (int[] nums, int k,int end) {
//下沉
while (2 * k <= end) {
int j = 2 * k;
//找出子节点中最大或最小的那个
if (j + 1 <= end && nums[j + 1] > nums[j]) {
j++;
}
if (nums[j] > nums[k]) {
swap(nums, j, k);
} else {
break;
}
k = j;
}
}
public void swap (int nums[], int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
```
Python Code:
```python
def sortArray(nums: list)->list:
leng = len(nums)
a = [0] + nums
# 下沉建堆
for i in range(int(leng / 2), 0, -1):
sink(a, i, leng)
k = leng
# 排序
while k > 1:
swap(a, 1, k)
k -= 1
sink(a, 1, k)
for i in range(1, leng + 1):
nums[i - 1] = a[i]
return nums
def swap(nums: list, i: int, j: int):
temp = nums[i]
nums[i] = nums[j]
nums[j] = temp
def sink(nums: list, k: int, end: int):
while 2 * k <= end:
j = 2 * k
if j + 1 <= end and nums[j + 1] > nums[j]:
j += 1
if nums[j] > nums[k]:
swap(nums, j, k)
else:
break
k = j
```
好啦堆排序我们就到这里啦是不是搞定啦总的来说堆排序比其他排序算法稍微难理解一些重点就是建堆而且应用比较广泛大家记得打卡呀
好啦我们再来分析一下堆排序的时间复杂度空间复杂度以及稳定性
**堆排序时间复杂度分析**
因为我们建堆的时间复杂度为 O(n排序过程的时间复杂度为 O(nlogn),所以总的空间复杂度为 O(nlogn)
**堆排序空间复杂度分析**
这里需要注意我们上面的描述过程中为了更直观的描述空出数组的第一位这样我们就可以通过 i _ 2 i _ 2+1 来求得左孩子节点和右孩子节点 我们也可以根据 i _ 2 + 1 i _ 2 + 2 来获取孩子节点这样则不需要临时数组来处理原数组将所有元素后移一位所以堆排序的空间复杂度为 O(1),是原地排序算法
**堆排序稳定性分析**
堆排序不是稳定的排序算法在排序的过程我们会将堆的最后一个节点跟堆顶节点交换改变相同元素的原始相对位置
最后我们来比较一下我们快速排序和堆排序
1.对于快速排序来说数据是顺序访问的而对于堆排序来说数据是跳着访问的这样对 CPU 缓存是不友好的
2.相同的的数据排序过程中堆排序的数据交换次数要多于快速排序
所以上面两条也就说明了在实际开发中堆排序的性能不如快速排序性能好
好啦今天的内容就到这里啦咱们下期见