Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Heap lrc123 #18

Open
wants to merge 6 commits into
base: docsify
Choose a base branch
from
Open

Heap lrc123 #18

wants to merge 6 commits into from

Conversation

Lrc123
Copy link

@Lrc123 Lrc123 commented May 1, 2020

Heap章节首次提交

提交人: Lrc123

请审阅

@Lrc123 Lrc123 requested a review from liweiwei1419 May 1, 2020 12:12
@liweiwei1419 liweiwei1419 self-assigned this May 5, 2020

## 堆常见的操作:

HEAPIFY 建堆:把一个无序的数组变成堆结构的数组,时间复杂度为$O(N\log N)$。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里介绍 heapify 的操作我感觉还比较早。

「出队」和「入队」是可以介绍的。

堆与优先队列这个标题常常一起出现,实际上堆就是优先队列的一种实现方式。
我们在实例化优先队列的时候,也常常将实例名称写为maxHeap和minHeap。
+ `注意:这里的堆指的是一种数据结构,不是操作系统里的堆。`

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补充一下:如果数据有动态更新的特点,可以使用「优先队列」(堆)。

@@ -0,0 +1,178 @@
# 堆的构建和堆排序

在我们做算法题的大多数情况,不需要手动地构建堆这个数据结构。通常我们直接使用编程语言自带的实现,如java使用PriorityQueue。不过,在学习堆这个数据结构的时候,手动构建堆,可以加深理解,帮助我们更好地使用它。
Copy link
Contributor

@liweiwei1419 liweiwei1419 May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

第一:这里措辞可以修改一下:「不需要手动地构建堆这个数据结构」改成「可以直接使用库函数中提供的优先队列实现」;

Java:PriorityQueue,C++:priority_queue,Python:heap 或者 from queue import PriorityQueue

(这一条待讨论)初学的同学建议自己实现一个堆,在理解了堆的工作原理以后,才能在一些场合下合理使用堆。

第二:java 统一写成 Java。PriorityQueue 由于是一个专有的类的名称,应该加上引号 PriorityQueue,并且英文前后空一格,注意检查一下全篇类似的地方。


### 堆(最大堆)的构建

1. 在介绍章节中我们讲过堆的构建要通过一维数组来实现,那么如何维持一维数组中的逻辑关系呢?这里用到3个辅助方法, 来计算父节点和子节点的位置关系。
Copy link
Contributor

@liweiwei1419 liweiwei1419 May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修改建议:

  1. 我们以「最大堆」为例,介绍堆的工作原理,「最小堆」的工作原理类似,留给读者完成;
  2. 由于底层实现是数组,为了简便起见,我们的实现不考虑数组扩容的情况,如果要支持「动态扩容」,请在网上搜索「动态数组」的相关资料;
  3. 「堆」的实现其实有一个前提,下标为 0 的那个位置用还是不用。因此这里还是建议「画图」,让读者直观地感受到「父节点」和两个「孩子节点」在「数组」里下标的对应关系,这样 index * 2 + 1index * 2 + 2 的出现也会比较自然一些;
  4. shifUpshifDownheapiy 其实是需要让读者建立起这些操作的合理性的,并且也需要直观的感受,因此还是建议作图,并且用简短的文字说明:为啥是这些操作。其实这些操作都是:用最小的代价去维护「堆有序」的性质,也就是「堆」的定义

* 先从数组尾部添加新元素,再将该元素移动到正确的位置。而这个移动的过程就叫做siftUp。
*/
public void add(int num){
data.add(num);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

data.add(num); 这个操作可以看出堆的底层是「动态数组」实现。但是下文 swap 操作,堆又变成了「静态」的数组实现,这里最好统一。示例给出的代码应该是一个完整的,可以直接使用的,经过测试的类,不一定要实现很完美的功能,例如可以不支持泛型。


## 堆排序

+ 堆排序的原理是:假如要对一个数组进行从小到大的排列,则对数组`heapify`成一个最大堆。然后,每次将堆顶元素放到数组末尾,使`size`减一。当每次取出的当前最大元素按照`size`递减的位置排列后,就得到了一个有序的数组。
Copy link
Contributor

@liweiwei1419 liweiwei1419 May 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

其实有了「堆」就可以借助它进行排序,但是我们可以直接把「原始数组」构建成「堆」(这个操作称之为 heapify),这样的排序方法称之为「堆排序」。(区别于使用一个额外的堆。)


在我们做算法题的大多数情况,不需要手动地构建堆这个数据结构。通常我们直接使用编程语言自带的实现,如java使用PriorityQueue。不过,在学习堆这个数据结构的时候,手动构建堆,可以加深理解,帮助我们更好地使用它。

### 堆(最大堆)的构建
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「构建」改成「实现」可能会好一点。

这一节的内容比较难讲,所以可能需要费一点心思,希望读者阅读完以后,能够建立起来堆里面相关操作的直观感受。


for (Integer num : nums) {
minHeap.add(num);
if(minHeap.size() > k){
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ifwhile 后面按照规范是要空一格的,用 ide 工具的格式化按钮就可以一下子调整对。

if(minHeap.isEmpty()){
return maxHeap.peek();
}
return maxHeap.size() == minHeap.size() ? maxHeap.peek() + (minHeap.peek() - maxHeap.peek()) / 2.0 : (double)maxHeap.peek();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「参考建议」三目运算符会搞得一行变得比较长,可能会影响阅读(因为用户可能会在手机上阅读)和可读性,除非是很简单的 ifelse 场景,建议少用三目运算符。

freq.add(map.get(heap.peek()));
res.add(heap.poll());
}
for (Integer item : freq) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

调试的语句要出现,也应该是注释掉的。

(a, b) -> map.get(b).equals(map.get(a)) ? b.compareTo(a) : map.get(a) - map.get(b)
);

map.keySet().forEach(item -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java 流式语法可能有些读者不是很熟悉,可能需要注释。

maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
@Override
public int compare(Integer i1, Integer i2) {
return i2.compareTo(i1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里用实现匿名函数,后面的例子用 lambda 语法,最好统一一下。

@liweiwei1419
Copy link
Contributor

在讲解「堆」的实现那里我感觉需要写得细致一点,分步写出来。倒是例题的部分讲解太细了。个人意见,仅供参考。

@liweiwei1419 liweiwei1419 reopened this May 6, 2020
@Lrc123
Copy link
Author

Lrc123 commented May 6, 2020

收到,谢谢你详细的审批。我会再仔细考量,按要求修改。

@Lrc123 Lrc123 requested a review from liweiwei1419 May 29, 2020 07:40
Copy link
Contributor

@liweiwei1419 liweiwei1419 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • 遇到数字和字母,前后要加上一个空格,「。」和「,」号末尾可以不加,例如 wiki。
  • 文中,「结点」和「节点」需统一,可以使用文本替换功能,避免手工查找。

后面我们只会用到完全二叉树,完全二叉树的定义以上文为准。

### 二叉堆图解

>「二叉堆(Binary Heap)是一个可以被看成近似完全二叉树的数组。树上的每一个结点对应数组的一个元素。除了最底层外,该树是完全充满的,而且是从左到右填充。」 <div style="text-align: right">—— 《算法导论》</div>

上面这段引用什么意思呢,用一句话概括就是:`二叉堆在逻辑上是一颗完全二叉树,在实现上是普通的一维数组。`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

正是由于二叉堆是完全二叉树,因此可以二叉堆使用一维数组作为实现。

说到「二叉堆」,正如它名字所表示,它是一个使用二叉树来实现的数据结构。二叉树因其结构又有多种专有名词,我们这里对三种形态的二叉树做一个区分帮助大家理解。

### 满二叉树 Full BinaryTree
在国内普遍使用的教材《数据结构 c语言版》中,满二叉树的定义是:「
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

《数据结构 (C 语言版)》(最好加上作者)。

完全二叉树的定义是:「除了最后一层不一定是满的,完全二叉树的每一层结点都是满的。且最后一层的结点是从左至右依次排列。当最后一层也是满的时候,也称作完美二叉树」。
![完全二叉树](images/complete.png)

### 完美二叉树 Perfect BinaryTree
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「满二叉树」和「完美二叉树」目前看上去是一个意思,所以真的是一个意思吗?如果是的话,可以把两个合并在一起写。

@Lrc123 Lrc123 requested a review from liweiwei1419 June 26, 2020 10:59

## 什么是优先队列?

「普通队列」的特点是:先进先出,后进后出。「优先队列」是一种特殊的「队列」,入队与普通队列无异,在出队的时候按照「优先级顺序」出队。这里的「优先级顺序」可以是人为定义的。「优先队列」可以使用数组实现,或者是维护有序数组,或者是在出队的时候,线性扫描找到优先级最高的元素,但是只要是「线性结构」,最差情况下都得扫描数组一遍。

## 为什么使用优先队列?

这里要引出优先队列的一个特点:「动态」。如果应用场景是不需要有动态地添加和取出元素的话,我们只需要对容器进行一次性的排序就足以解决问题。比如对班级学生绩点进行从大到小依次输出,快速排序这样的排序算法效率要比使用优先队列来说要高得多。但是如果对手游王者荣耀中攻击范围内血量最低的敌人进行进攻的优先级排序,则需要使用到优先队列。因为攻击范围内的敌人数量是在不断变化的。如果要对变化中的容器每次都做整体的排序,效率是很低的($N$次调用是$N^2logN$)。我们这里要讨论的是由「堆」这种数据结构所实现的优先队列,它的效率会要比排序法高上许多($N$次调用是$NlogN$)
这里要引出优先队列的一个特点:「动态」。如果应用场景是不需要有动态地添加和取出元素的话,我们只需要对容器进行一次性的排序就足以解决问题。比如对班级学生绩点进行从大到小依次输出,快速排序这样的排序算法效率要比使用优先队列来说要高得多。但是如果对手游王者荣耀中攻击范围内血量最低的敌人进行进攻的优先级排序,则需要使用到优先队列。因为攻击范围内的敌人数量是在不断变化的。如果要对变化中的容器每次都做整体的排序,效率是很低的($N$ 次调用是 $N^2logN$ )。我们这里要讨论的是由「堆」这种数据结构所实现的优先队列,它的效率会要比排序法高上许多 ( $N$ 次调用是 $NlogN$ )
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

数学公式里面 log 、max、min 是专有函数,需要这样使用:\log 、\max、\min,注意后面有一个空格。

这里应写作:$N^2 \log N$。



## 什么是堆?

为了避免「线性扫描」,需将数据组织成「树形结构」。二叉堆就是一种高效的「优先队列」实现。另外,还有二项式堆,最大-最小堆、斐波拉契堆等实现,针对普通的算法面试可以不用掌握。

二叉堆满足,从最大堆每次取出的堆顶元素是该堆中的最大元素,从最小堆每次取出的堆顶元素是该堆中的最小元素。在最大堆中,每一个父节点都是大于等于它的孩子节。同理,最小堆则是父节点要小于等于孩子节点
二叉堆满足,从最大堆每次取出的堆顶元素是该堆中的最大元素,从最小堆每次取出的堆顶元素是该堆中的最小元素。在最大堆中,每一个父结点都是大于等于它的孩子节。同理,最小堆则是父结点要小于等于孩子结点
Copy link
Contributor

@liweiwei1419 liweiwei1419 Jun 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「孩子节」应作「孩子结点」。

「最小堆则是父结点要小于等于孩子结点」这句话有歧义,会被理解成,「爷爷结点的值」和「孙子结点的值」的关系。

可能这样描述会好一些:最小堆则满足:每一个结点的值小于等于它的孩子结点(如果存在的话)的值。



## 什么是堆?

为了避免「线性扫描」,需将数据组织成「树形结构」。二叉堆就是一种高效的「优先队列」实现。另外,还有二项式堆,最大-最小堆、斐波拉契堆等实现,针对普通的算法面试可以不用掌握。

二叉堆满足,从最大堆每次取出的堆顶元素是该堆中的最大元素,从最小堆每次取出的堆顶元素是该堆中的最小元素。在最大堆中,每一个父节点都是大于等于它的孩子节。同理,最小堆则是父节点要小于等于孩子节点。
二叉堆满足,从最大堆每次取出的堆顶元素是该堆中的最大元素,从最小堆每次取出的堆顶元素是该堆中的最小元素。在最大堆中,每一个父结点都是大于等于它的孩子节。同理,最小堆则是父结点要小于等于孩子结点。


## 二叉堆

说到「二叉堆」,正如它名字所表示,它是一个使用二叉树来实现的数据结构。二叉树因其结构又有多种专有名词,我们这里对三种形态的二叉树做一个区分帮助大家理解。

### 满二叉树 Full BinaryTree
Copy link
Contributor

@liweiwei1419 liweiwei1419 Jun 27, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BinaryTree 的 Binary 和 Tree 中间是不是要加一个空格合适一点(我不确定,待讨论)。

![满二叉树](images/full-bt2.png)
满二叉树的定义则是:「叶子节点的数量是非叶子节点数量+1的二叉树。」如上图,所以,当被问什么是满二叉树时。可以根据要求和语境作答。
满二叉树的定义则是:「叶子结点的数量是非叶子结点数量+1的二叉树。」如上图,所以,当被问什么是满二叉树时。可以根据要求和语境作答。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 作「加 1」 可能更符合书面语规范。


### 完全二叉树 Complete BinaryTree
完全二叉树的定义是:「除了最后一层不一定是满的,完全二叉树的每一层结点都是满的。且最后一层的结点是从左至右依次排列。当最后一层也是满的时候,也称作完美二叉树」。
![完全二叉树](images/complete.png)

### 完美二叉树 Perfect BinaryTree
根据wiki上的解释:「完美二叉树是所有非叶子结点都有左右两个孩子的二叉树」,又可以理解成最后一层是满的的完全二叉树,所以又可以说完美二叉树是特殊的完全二叉树。
根据wiki上的解释:「完美二叉树是所有非叶子结点都有左右两个孩子的二叉树」,又可以理解成最后一层是满的的完全二叉树,所以又可以说完美二叉树是特殊的完全二叉树。所以在这里,完美二叉树等于教材上的满二叉树。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wiki 左右加空格,最好附上链接。

+ 时间复杂度:$O(N\log N)$,这里 $N$ 是数组的长度,每次抽取出堆顶为 $O(\log N)$ , $N$ 个元素就 N 次操作。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

「N 次操作」就也加一下 $N$ ,虽说问题不大。

@@ -9,9 +9,9 @@

例题 1: 「力扣」第 215 题」:[数组中的第K个最大元素(easy)](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/)

+ 当我们求数组中第k大的数字的时候,首先会想到的是构建一个最大堆存储所有的元素,然后将前面(k-1)个元素给弹出。最后返回堆顶。这确实是一个可行的办法。不过,反直觉的是,我们可以构建一个最小堆来更快地得到第k大的数字。
+ 当我们求数组中第 k 大的数字的时候,首先会想到的是构建一个最大堆存储所有的元素,然后将前面 (k-1) 个元素给弹出。最后返回堆顶。这确实是一个可行的办法。不过,反直觉的是,我们可以构建一个最小堆来更快地得到第k大的数字。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这一页问题不大,主要还是注意一下 k 前后最好加上 ``,然后英文前后空一格,即使在注释(前面两页)里面也最好这样做。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(k-1) 写作 k - 1

+ 时间复杂度: $O(N\log N)$ 。
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

补充一下:$N$ 是数组的长度。

@Lrc123 Lrc123 requested a review from liweiwei1419 July 8, 2020 09:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants