diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..ba9538f Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/Heap/01-introduction.md b/Heap/01-introduction.md new file mode 100644 index 0000000..06418f3 --- /dev/null +++ b/Heap/01-introduction.md @@ -0,0 +1,84 @@ +# 优先队列和堆 + +作者: Lrc123 审核: liweiwei1419 + +## 什么是优先队列? + +「普通队列」的特点是:先进先出,后进后出。「优先队列」是一种特殊的「队列」,入队与普通队列无异,在出队的时候按照「优先级顺序」出队。这里的「优先级顺序」可以是人为定义的。「优先队列」可以使用数组实现,或者是维护有序数组,或者是在出队的时候,线性扫描找到优先级最高的元素,但是只要是「线性结构」,最差情况下都得扫描数组一遍。 + +## 为什么使用优先队列? + +这里要引出优先队列的一个特点:「动态」。如果应用场景是不需要有动态地添加和取出元素的话,我们只需要对容器进行一次性的排序就足以解决问题。比如对班级学生绩点进行从大到小依次输出,快速排序这样的排序算法效率要比使用优先队列来说要高得多。但是如果对手游王者荣耀中攻击范围内血量最低的敌人进行进攻的优先级排序,则需要使用到优先队列。因为攻击范围内的敌人数量是在不断变化的。如果要对变化中的容器每次都做整体的排序,效率是很低的($N$ 次调用是 $N^2\log N$ )。我们这里要讨论的是由「堆」这种数据结构所实现的优先队列,它的效率会要比排序法高上许多 ( $N$ 次调用是 $N\log N$ ) 。 + + +## 什么是堆? + +为了避免「线性扫描」,需将数据组织成「树形结构」。二叉堆就是一种高效的「优先队列」实现。另外,还有二项式堆,最大-最小堆、斐波拉契堆等实现,针对普通的算法面试可以不用掌握。 + +二叉堆满足,从最大堆每次取出的堆顶元素是该堆中的最大元素,从最小堆每次取出的堆顶元素是该堆中的最小元素。在最大堆中,每一个父结点都是大于等于它的孩子结点。同理,最小堆则满足:每一个结点的值小于等于它的孩子结点(如果存在的话)的值。 + + +## 二叉堆 + +说到「二叉堆」,正如它名字所表示,它是一个使用二叉树来实现的数据结构。二叉树因其结构又有多种专有名词,我们这里对三种形态的二叉树做一个区分帮助大家理解。 + +### 满二叉树 Full Binary Tree +在国内普遍使用的教材《数据结构 c语言版》 (作者:「严敏蔚 吴伟民」)中,满二叉树的定义是:「 +一棵深度为 $k$ 且有 $2^k - 1$ 个结点的二叉树称为满二叉树」。 +那么它应该长这样: +![满二叉树](images/full-bt.png) + +但是,根据[ wiki ](https://en.wikipedia.org/wiki/Binary_tree)等外文信息网站的描述: +![满二叉树](images/full-bt2.png) +满二叉树的定义则是:「叶子结点的数量是非叶子结点数量加1的二叉树。」如上图,所以,当被问什么是满二叉树时。可以根据要求和语境作答。 + +### 完全二叉树 Complete Binary Tree +完全二叉树的定义是:「除了最后一层不一定是满的,完全二叉树的每一层结点都是满的。且最后一层的结点是从左至右依次排列。当最后一层也是满的时候,也称作完美二叉树」。 +![完全二叉树](images/complete.png) + +### 完美二叉树 Perfect Binary Tree +根据[ wiki ](https://en.wikipedia.org/wiki/Binary_tree)上的解释:「完美二叉树是所有非叶子结点都有左右两个孩子的二叉树,并且所有的叶子都有相同的高度」,又可以理解成最后一层是满的的完全二叉树,所以又可以说完美二叉树是特殊的完全二叉树。所以在这里,完美二叉树等于教材上的满二叉树。 +![完美二叉树](images/full-bt.png) + +后面我们只会用到完全二叉树,完全二叉树的定义以上文为准。 + +### 二叉堆图解 + +>「二叉堆(Binary Heap)是一个可以被看成近似完全二叉树的数组。树上的每一个结点对应数组的一个元素。除了最底层外,该树是完全充满的,而且是从左到右填充」
—— 《算法导论》
+ +上面这段引用什么意思呢,用一句话概括就是:`二叉堆在逻辑上是一颗完全二叉树,在实现上是普通的一维数组`。 +正是由于二叉堆是完全二叉树,因此可以二叉堆使用一维数组作为实现。 + +我们用两幅图直观地感受一下「二叉堆」: + +![完全二叉树(堆)](images/compare.png) +
图1. 最大堆和最小堆
+ +![普通一维数组](images/array.png) +
图2. 实现上的一维数组
+ +![当两者重合](images/merge.png) +
图3. 直观表示
+ + +## 堆常见的操作: + +HEAPPOP :弹出堆顶元素,并调整堆,替代到堆顶位置,时间复杂度为 $O(\log N)$ 。 + +HEAPPUSH :向数组末尾加入新的元素,并调整到正确的位置,维持堆的结构,时间复杂度为 $O(\log N)$ 。 + +HEAPIFY 建堆 :把一个无序的数组变成堆结构的数组,时间复杂度为 $O(N\log N)$ 。如果说 HEAPPOP 是动态地创建一个堆,那么 HEAPIFY 则是将一个数组一次转化成堆。具体的方法我们放在后面讲。 + +HEAPSORT :HEAPFY 维持堆的结构,通过 HEAPPOP ,每次将最堆顶元素排列到堆尾, size 减小。重复操作直到数组绝对有序,时间复杂度为 $O(N\log N)$ 。空间复杂度为 $O(1)$ 。 + + +## 学习建议 ++ 我们可以先通过手动实现一个堆的数据结构来认识堆。 ++ 当我们遇到题目中有优先级、大小关系或排序等字眼的时候,我们就可以想到堆这个数据结构,并尝试应用它。 + +# 堆的应用和刷题常见问题 ++ 堆与优先队列 + 堆与优先队列这个标题常常一起出现,实际上堆就是优先队列的一种实现方式。 + 我们在实例化优先队列的时候,也常常将实例名称写为 maxHeap 和 minHeap 。 ++ 如果数据有动态更新的特点,可以使用「优先队列」(堆)。 ++ `注意:这里的堆指的是一种数据结构,不是操作系统里的堆`。 diff --git a/Heap/02-template-1.md b/Heap/02-template-1.md new file mode 100644 index 0000000..cb77a7a --- /dev/null +++ b/Heap/02-template-1.md @@ -0,0 +1,338 @@ +# 堆的实现和堆排序 + +多数情况下,我们可以直接使用库函数中提供的优先队列实现。 ++ Java:`PriorityQueue` ++ C++:`priority_queue` ++ Python:`heap` 或者 `from queue import PriorityQueue` + +不过,在学习堆这个数据结构的时候,手动实现堆,可以帮助我们更好地理解和使用它。 + +### 堆的实现 + +### 1. 数组中元素的逻辑关系 +#### 1. 从1开始的数组实现 +我们以「最大堆」为例,介绍堆的工作原理,「最小堆」的工作原理类似,留给读者完成。 +由于底层实现是数组,为了简便起见,我们的实现不考虑数组扩容的情况,如果要支持「动态扩容」,请在网上搜索「动态数组」的相关资料; +用来实现堆的数组排列方式其实是有两种方案的。 +假如我们要从任意一个结点出发找到它的父结点或左右孩子结点: +从数组1这个位置开始,当前结点与父子结点会有如下的关系: +![从数组位置1开始](images/from1.png) + + +#### **Java** +```java +// 通过当前位置求父结点位置 +public int parent(int index) { + return data[index / 2]; +} + +// 通过当前位置求左孩子结点位置 +public int leftChild(int index) { + return data[index * 2]; +} + +// 通过当前位置求右孩子结点位置 +public int rightChild(int index) { + return data[index * 2 + 1]; +} + +``` + + +#### 2. 从0开始的数组实现 +从位置1开始直接好懂,不过会浪费 0 号位置这一个空间。我们仔细思考一下,发现从 0 位置开始也可以实现父子结点的对应关系。即,假设求父结点位置,我们先将 index 向左移动一位,即减一,然后再除以二。即可得到父结点位置。若求左孩子位置,则可以直接通过逆推求父结点的表达式得到,右孩子的位置则是左孩子位置加一。这里读者可以通过数学推导或者编写测试代码自行验证,帮助理解。 +![从数组位置0开始](images/from0.png) + + +#### **Java** +```java +// 通过当前位置求父结点位置 +public int parent(int index) { + return data[(index - 1) / 2]; +} + +// 通过当前位置求左孩子结点位置 +public int leftChild(int index) { + return data[index * 2 + 1]; +} + +// 通过当前位置求右孩子结点位置 +public int rightChild(int index) { + return data[index * 2 + 2]; +} + +``` + + +### 2. 向堆中添加元素和SiftUp + +`SiftUp`操作是为了向堆中添加元素,和直接对数组`heapify`(将一个数组堆化)不同的是,`SiftUp`用于动态地构造一个堆。将从数组尾部添加的新元素给移动到正确的位置。 + ++ 先从数组尾部添加新元素,再将该元素移动到正确的位置。而这个移动的过程就叫做`SiftUp`。 ++ `SiftUp`通过与父结点比较,如果比父结点大则交换。同时不要忘记交换后结点位置也要更新为原父结点的位置。 + +![siftup操作](images/siftup.gif) + + +#### **Java** +```java + +// 向堆中添加元素 +public void add(int num) { + data[size] = num; + siftUp(size); + size++; +} + +// siftup操作 +public void siftUp(int index) { + while (index <= size && parent(index) >= 0) { + if (data[index] > data[parent(index)]) { + swap(index, parent(index)); + index = parent(index); + } else { + break; + } + } +} + +``` + +### 思考 +为什么我们要以`SiftUp`这样的方式来添加元素呢? + +这里可以这样捋一下思路:首先,我们知道原本数组中元素排列的逻辑是堆,当添加进一个新的元素进来的时候,这个新元素毫无疑问是有可能会破坏我们原有堆结构的顺序关系的。那么,如何以最有效率的方式使新数组重新有序呢,通过观察可以看到,从数组末尾添加进的元素,它其实只和它的父结点有一个大小顺序要求的。假如较大的新元素恰好在左孩子位置上,它只需要和父亲结点交换就行了。通过从下至上每一次和父结点比较,可以很快地调整好堆的顺序(这个过程有点像冒泡排序,每次把元素通过多次比较交换放到正确位置上)。假如元素恰好在右孩子的位置上,由于原来的数组,父结点一定大于孩子结点(包括左孩子),所以,右孩子不需要和左孩子比较,也是直接和父结点交换即可。 + + + +### 3. 从堆中取出元素和SiftDown +![siftdown操作](images/siftdown.gif) + + +#### **Java** + +```java + +// 取出数组顶部元素,然后将数组末尾元素填充到堆顶,数组size减一,在将堆顶元素siftdown +public int extractMax() { + int ret = data[0]; + data[0] = data[size - 1]; + size--; + siftDown(0); + return ret; +} + +// siftdown j代表孩子结点中较大那个的位置 +public void siftDown(int index) { + while (leftChild(index) < data.size()) { + int j = leftChild(index); + if (j + 1 < data.size() && data.get(j + 1) > data.get(j)) { + j++; + } + if (data.get(index) > data.get(j)) { + return; + } + swap(data, index, j); + index = j; + } +} + +``` + +### 思考 +`SiftDown`这个操作也是非常有意思,为什么要把数组末尾的元素放到堆顶呢? + +我们可以分析一下,当堆顶元素被取走时,堆顶位置肯定就空缺了。那么怎么填补这个空缺,使数组重新有序呢?很自然会先想到从堆顶的左右孩子中选择,然后依此循环直到有序。乍一想好像「计划通」。然而通过些许的验证,其实就能发现问题,我们先想象一下堆作为二叉树的样子,假设这个替换过程结束的时候,是把二叉树最后一层的左边某个结点给替换了,而右边的结点还在。这棵二叉树就不再是完全二叉树。从数组上也可以发现空缺的元素将不会是在末尾,而是在数组之中的某个位置。我们回过头看看我们的`add`操作其实就是通过不断在数组末尾添加元素来实现的。空缺在数组中间的话,是没有办法填补的。所以,这种调整方法会破坏堆的结构,是不可行的。而`SiftDown`把数组末尾的元素直接放到堆顶就是一个非常巧妙的做法,因为,`extractMax`这个弹出操作,必然会使堆中元素减少,那么我们就直接把末尾的元素给移动到空缺使数组减小,堆会继续调整,但绝不会再影响(使用)到末尾这个位置了,内部的排列自然不会有空缺。试想,从数组中任何一个其他的位置,抽出元素来填补堆顶,都没办法做到这样。所以`SiftDown`操作是非常合理的。 + + + + +#### 完整代码 + + +```java +public class Heap { + + int[] data; + int size; + + public Heap(int capacity) { + data = new int[capacity]; + size = 0; + } + + public int parent(int index) { + return (index - 1) / 2; + } + + public int leftChild(int index) { + return index * 2 + 1; + } + + public int rightChild(int index) { + return index * 2 + 2; + } + + public int getSize() { + return size; + } + + public void swap(int i, int j) { + int tmp = data[i]; + data[i] = data[j]; + data[j] = tmp; + } + + public void add(int num) { + data[size] = num; + siftUp(size); + size++; + } + + public int extractMax() { + int ret = data[0]; + data[0] = data[size - 1]; + size--; + siftDown(0); + return ret; + } + + + public void siftUp(int index) { + while (index < size && parent(index) >= 0) { + if (data[index] > data[parent(index)]) { + swap(index, parent(index)); + index = parent(index); + } else { + break; + } + } + } + + public void siftDown(int index) { + while (index < size && leftChild(index) <= size) { + int j = leftChild(index); + if (j + 1 <= size && data[j] < data[j + 1]) { + j++; + } + if (data[j] <= data[index]) { + return; + } + swap(j, index); + index = j; + } + } + + public void printHeap() { + StringBuilder sb = new StringBuilder(); + sb.append("["); + for (int i = 0; i < size; i++) { + if (i != size - 1) { + sb.append(data[i]).append(","); + } else { + sb.append(data[i]).append("]"); + } + } + System.out.println(sb.toString()); + } + +// public static void main(String[] args) { +// Heap heap = new Heap(10); +// heap.add(11); +// heap.add(2); +// heap.add(10); +// heap.add(5); +// heap.add(3); +// heap.printHeap(); +// System.out.println(heap.extractMax()); +// System.out.println(heap.extractMax()); +// System.out.println(heap.extractMax()); +// System.out.println(heap.extractMax()); +// } + +} + +``` + + + +## 堆排序 + ++ 其实有了「堆」就可以借助它进行排序,但是我们可以直接把「原始数组」构建成「堆」(这个操作称之为 heapify),这样的排序方法称之为「堆排序」。(区别于使用一个额外的堆。) + + +#### **Java** + +```java +public class Solution { + + // heapsort是一种in-place排序,就是直接对原数组进行位置上的交换来排序 + public void heapSort(int[] nums) { + int size = nums.length - 1; + // 先对数组进行最大堆化 + heapify(nums, size); + // 然后每次将堆顶的最大元素移动到数组末尾,将交换至堆顶的较小元素再进行下沉操作,使数组再次堆化有序。使size减一,堆化的操作将不影响末尾已经排序好的元素。重复操作直到堆中元素都按照从大到小,从后往前的方式排列在原数组上。 + while (size > 0) { + swap(nums, 0, size); + siftDown(nums, 0, size); + size--; + } + } + + //堆化,从末尾开始将较小元素下沉 + public void heapify(int[] nums, int size) { + for (int i = parent(size); i > 0; i--) { + siftDown(nums, i, size); + } + } + + //下沉操作 + public void siftDown(int[] nums, int index, int size) { + while (leftChild(index) < size) { + // 这里声明的j变量是用来选择当前结点的两个左右孩子的较大结点 + int j = leftChild(index); + if (j + 1 < size && nums[j + 1] > nums[j]) { + j++; + } + // 如果两个孩子结点的较大值都比当前结点小,则不需要继续swap,直接return + if (nums[index] > nums[j]) { + return; + } + // 如果较大的那个比当前结点大,则交换 + swap(nums, index, j); + // 当前结点的index也要移动到被交换的孩子结点上,为下一次比较做准备 + index = j; + } + } + + // 对原数组进行操作,交换两个位置的变量 + public void swap(int[] nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } + + // 输入当前结点的位置,计算得到父结点的位置 + public int parent(int index) { + return (index - 1) / 2; + } + + // 计算左孩子结点的位置 + public int leftChild(int index) { + return index * 2 + 1; + } + + // 计算右孩子结点的位置 + public int rightChild(int index) { + return index * 2 + 2; + } +} + +``` + + + +**复杂度分析**: + ++ 时间复杂度:$O(N\log N)$,这里 $N$ 是数组的长度,每次抽取出堆顶为 $O(\log N)$ , $N$ 个元素就 $N$ 次操作。 ++ 空间复杂度:$O(1)$。 in-place 排序,不需要额外空间。 \ No newline at end of file diff --git a/Heap/03-template-2.md b/Heap/03-template-2.md new file mode 100644 index 0000000..2e2925d --- /dev/null +++ b/Heap/03-template-2.md @@ -0,0 +1,39 @@ +# 求(前/第)K(大/小/个)问题 + +当我们遇到题目中有优先级、大小关系或排序等字眼的时候,我们就可以想到堆这个数据结构。 + + +1. 第K大/小问题 +问题分析:这类问题求解的关键在于如何从无序的数组中,得到必须在有序前提下才能得到的第k个元素。 +使无序变为有序的最简单方法就是排序,但是要得到我们需要的解却并不需要绝对有序。而堆,就可以使数组呈现一种整体有序的效果,从而更高效地求解问题。 + +例题 1: 「力扣」第 215 题」:[数组中的第K个最大元素(easy)](https://leetcode-cn.com/problems/kth-largest-element-in-an-array/) + ++ 当我们求数组中第 `k` 大的数字的时候,首先会想到的是构建一个最大堆存储所有的元素,然后将前面 `k - 1` 个元素给弹出。最后返回堆顶。这确实是一个可行的办法。不过,反直觉的是,我们可以构建一个最小堆来更快地得到第k大的数字。 + ++ 因为,第 `k` 大的元素同时也是前 `k` 个最大元素中的最小值。这对应了我们一开始说的,最大最小想到堆。我们可以构建一个容量为 `k` 的最小堆来存储数组中最大的 `k` 个元素:当元素的数量超过 `k` 时,我们将堆顶最小的元素弹出,弹出不需要的 arr.length - `k` 个元素,剩下的就是我们需要的最大的 `k` 个元素,而堆顶也正式整个数组的第 `k` 大元素。 + + +#### **Java** + +```java + +public int findKthLargest(int[] nums, int k) { + PriorityQueue minHeap = new PriorityQueue<>(); + + for (Integer num : nums) { + minHeap.add(num); + if (minHeap.size() > k) { + minHeap.poll(); + } + } + return minHeap.peek(); +} + +``` + + +**复杂度分析**: + ++ 时间复杂度: $O(N\log N)$ , $N$ 是数组的长度。 ++ 空间复杂度: $O(k)$ 。 \ No newline at end of file diff --git a/Heap/04-template-3.md b/Heap/04-template-3.md new file mode 100644 index 0000000..7c2c73a --- /dev/null +++ b/Heap/04-template-3.md @@ -0,0 +1,155 @@ +# 双堆法(大小堆法) + +双堆法用于解决的题型有:中位数问题。 + +1. 中位数问题 +例题 1: 「力扣」第 295 题 」:[数据流的中位数(hard)](https://leetcode-cn.com/problems/find-median-from-data-stream/) + +`问题分析`: +当需要求一组数的中位数的时候,我们需要先将该组数排序,然后找到中间位置。假如数组的长度是奇数,则返回中间那个数。假如数组长度为偶数,则返回中间两个数字和的一半。 + +这是非常自然想到的思路。 + +那么,如果这个数组是不断增加的呢? +我们会想到,新元素插入到排序数组里,然后再计算中位数对应下标的位置,就可以得到结果了。 +这是没有错的。不过,我们同时知道,在一个有序数组中插入一个值,时间复杂度为 $O(N)$ 。这是因为,数组中插入一个元素,要挪动插入位置之后的所有数字。我们插入 N 次,大概可以得到 $O(N^2)$ 的复杂度。有没有每次插入更快一点的数据结构呢。很显然,就是堆。每次插入的时间复杂度为 $O(\log N)$ , N 次插入大约为 $O(N\log N)$ 。 + +双堆法的思想在于,将数组划分为大小相差不超过 1 的两个堆,保持这样的平衡。两个堆的顶部就会恰好是数组中间的值。 + +### 求中位数 +基于双堆,我们可以很快写出求中位数的方法。 + ++ 首先,如果两个堆都为空,则返回 0.0 。然后,如果最小堆为空,(第二个 if 已经排除了均为空的情况),则返回最大堆。最后,(都不为空)如果 size 相等,就是偶数,不相等,就是奇数。返回对应的答案即可。 ++ 注意,为偶数的情况,我们的中位数计算还是要使用经典的写法:先减后加来防止溢出。 + + +#### **Java** + +```java + +public double findMedian() { + if (maxHeap.isEmpty() && minHeap.isEmpty()) { + return 0.0; + } + if (minHeap.isEmpty()) { + return maxHeap.peek(); + } + if (maxHeap.size() == minHeap.size()) { + return maxHeap.peek() + (minHeap.peek() - maxHeap.peek()) / 2.0; + } + return maxHeap.peek(); +} + +``` + + +### 添加元素 + ++ 基于刚写好的 findMedian() 方法,我们的添加操作非常轻松。如果要添加的值大于原数组的中位数,我们就把它放到较大的那一组,也就是最小堆里面。如果小于或者等于中位数,就把它放到最大堆里面。 ++ 这个时候,我们不要忘记了一件事。就是,每次添加了新的元素,我们的中位数在数组中的位置是会向右偏移的,所以我们要平衡一下两个堆里面元素的个数。 + + +#### **Java** + +```java + +public void addNum(int num) { + if (num <= findMedian()) { + maxHeap.add(num); + } else { + minHeap.add(num); + } + balance(); +} + +``` + + ++ 这个 balance 的过程非常简单,就是把多了元素的堆,堆顶元素给弹出加入到另一个堆中。即可。 ++ 注意,我们这里的实现,默认是最大堆可以比最小堆多一个元素的。 + + +#### **Java** + +```java + +public void balance() { + if (maxHeap.isEmpty() && minHeap.isEmpty()) { + return; + } + while (minHeap.size() > maxHeap.size()) { + maxHeap.add(minHeap.poll()); + } + while (maxHeap.size() - minHeap.size() > 1) { + minHeap.add(maxHeap.poll()); + } +} + +``` + + +### 完整代码 + +#### **Java** + +```java + +class MedianFinder { + + PriorityQueue minHeap = null; + PriorityQueue maxHeap = null; + + + public MedianFinder() { + // java 中默认的实现是最小堆 + minHeap = new PriorityQueue<>(); + maxHeap = new PriorityQueue<>((a , b) -> b - a); + } + + public void addNum(int num) { + if (num <= findMedian()) { + maxHeap.add(num); + } else { + minHeap.add(num); + } + balance(); + } + + public void balance() { + if (maxHeap.isEmpty() && minHeap.isEmpty()) { + return; + } + while (minHeap.size() > maxHeap.size()) { + maxHeap.add(minHeap.poll()); + } + while (maxHeap.size() - minHeap.size() > 1) { + minHeap.add(maxHeap.poll()); + } + } + + public double findMedian() { + if (maxHeap.isEmpty() && minHeap.isEmpty()) { + return 0.0; + } + if (minHeap.isEmpty()) { + return maxHeap.peek(); + } + if (maxHeap.size() == minHeap.size()) { + return maxHeap.peek() + (minHeap.peek() - maxHeap.peek()) / 2.0; + } + return (double) maxHeap.peek(); + } + + // public static void main(String[] args) { + // MedianFinder finder = new MedianFinder(); + // System.out.println(finder.findMedian()); + // finder.addNum(1); + // finder.addNum(2); + // finder.addNum(3); + // finder.addNum(4); + // System.out.println(finder.findMedian()); + // } +} + +``` + \ No newline at end of file diff --git a/Heap/05-examples.md b/Heap/05-examples.md new file mode 100644 index 0000000..56a2e1a --- /dev/null +++ b/Heap/05-examples.md @@ -0,0 +1,52 @@ +# 精选例题 + +## 题型1: 前k个XX问题 +前 k 个元素的输出其实很简单,前 k 大的就构建最大堆输出 k 次。前 k 小的就构建最小堆输出 k 次。不过,在 leetcode 中,前 k 个 XX 问题常常和频率结合在一起,组成了一个先统计再排序的问题。 + + +例题 1: 「力扣」:[692.前 K 个高频单词 (medium)](https://leetcode-cn.com/problems/top-k-frequent-words/) + +思路: 先使用 hashmap 统计单词出现频率,再存入堆,每次输出堆顶。 + + +#### **Java** + +```java + +class Solution { + public List topKFrequent(String[] words, int k) { + + Map map = new HashMap<>(); + + for (String word : words) { + map.put(word, map.getOrDefault(word, 0) + 1); + } + + PriorityQueue heap = new PriorityQueue<>( + (a, b) -> map.get(b).equals(map.get(a)) ? b.compareTo(a) : map.get(a) - map.get(b) + ); + + for (String key : map.keySet()) { + heap.add(key); + if (heap.size() > k) { + heap.poll(); + } + } + + LinkedList res = new LinkedList<>(); + LinkedList freq = new LinkedList<>(); + while (!heap.isEmpty()) { + freq.add(map.get(heap.peek())); + res.add(heap.poll()); + } + + Collections.reverse(res); + return res; + } +} + + +``` + + + diff --git a/Heap/06-practices.md b/Heap/06-practices.md new file mode 100644 index 0000000..4ecac70 --- /dev/null +++ b/Heap/06-practices.md @@ -0,0 +1,21 @@ +# 精选练习 + +## 题型一: 前k个类型的题目 +| 题目 | 提示 | +| ------------------------------------------------------------ | -------------------------------------- | +| [23. 合并k个排序链表( hard )](https://leetcode-cn.com/problems/merge-k-sorted-lists/)(必做) | 经典题目 | +| [215. 数组中的第K个最大元素 (easy)](https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/)(必做) | 例题 | +| [264. 丑数 II ( medium )](https://leetcode-cn.com/problems/ugly-number-ii/) | | +| [347. 前 K 个高频元素 ( medium )](https://leetcode-cn.com/problems/top-k-frequent-elements/) (必做) | 这类题型的基础题 | +| [692. 前 K 个高频单词 ( medium )](https://leetcode-cn.com/problems/top-k-frequent-words/) | | +| [407. 接雨水 II ( hard )](https://leetcode-cn.com/problems/trapping-rain-water-ii/) | | +| [502. IPO( hard )](https://leetcode-cn.com/problems/ipo/) | 看起来难,用堆做简单 | + + +## 题型二: 中位数类型的题目 + +| 题目 | 提示 | +| ------------------------------------------------------------ | ------------------ | +| [4. 寻找两个有序数组的中位数( hard )](https://leetcode-cn.com/problems/median-of-two-sorted-arrays/) | 挺难的,官方题解用递归双指针啥的比较复杂,不过这里亲测直接用双堆法也可以过 | +| [295. 数据流的中位数( hard )](https://leetcode-cn.com/problems/find-median-from-data-stream/) (必做)| 例题必做 | +| [480. 滑动窗口的中位数( hard )](https://leetcode-cn.com/problems/sliding-window-median/) | 这题是数据流那题的进阶版,很好的练手题 | \ No newline at end of file diff --git a/Heap/images/.DS_Store b/Heap/images/.DS_Store new file mode 100644 index 0000000..6f2141b Binary files /dev/null and b/Heap/images/.DS_Store differ diff --git a/Heap/images/array.png b/Heap/images/array.png new file mode 100644 index 0000000..edd102f Binary files /dev/null and b/Heap/images/array.png differ diff --git a/Heap/images/compare.png b/Heap/images/compare.png new file mode 100644 index 0000000..ebcf4d8 Binary files /dev/null and b/Heap/images/compare.png differ diff --git a/Heap/images/complete.png b/Heap/images/complete.png new file mode 100644 index 0000000..1945163 Binary files /dev/null and b/Heap/images/complete.png differ diff --git a/Heap/images/from0.png b/Heap/images/from0.png new file mode 100644 index 0000000..b86342a Binary files /dev/null and b/Heap/images/from0.png differ diff --git a/Heap/images/from1.png b/Heap/images/from1.png new file mode 100644 index 0000000..671a666 Binary files /dev/null and b/Heap/images/from1.png differ diff --git a/Heap/images/full-bt.png b/Heap/images/full-bt.png new file mode 100644 index 0000000..1e489a1 Binary files /dev/null and b/Heap/images/full-bt.png differ diff --git a/Heap/images/full-bt2.png b/Heap/images/full-bt2.png new file mode 100644 index 0000000..d1aaeaa Binary files /dev/null and b/Heap/images/full-bt2.png differ diff --git a/Heap/images/maxheap.png b/Heap/images/maxheap.png new file mode 100644 index 0000000..aa27557 Binary files /dev/null and b/Heap/images/maxheap.png differ diff --git a/Heap/images/merge.png b/Heap/images/merge.png new file mode 100644 index 0000000..e94496d Binary files /dev/null and b/Heap/images/merge.png differ diff --git a/Heap/images/minheap.png b/Heap/images/minheap.png new file mode 100644 index 0000000..824c324 Binary files /dev/null and b/Heap/images/minheap.png differ diff --git a/Heap/images/siftdown.gif b/Heap/images/siftdown.gif new file mode 100644 index 0000000..fce7c3c Binary files /dev/null and b/Heap/images/siftdown.gif differ diff --git a/Heap/images/siftup.gif b/Heap/images/siftup.gif new file mode 100644 index 0000000..392d32f Binary files /dev/null and b/Heap/images/siftup.gif differ diff --git a/_sidebar.md b/_sidebar.md index c92c140..c391389 100644 --- a/_sidebar.md +++ b/_sidebar.md @@ -1,10 +1,19 @@ - 二分查找 - - [简介](BinarySearch/01-introduction.md) - - [模板一](BinarySearch/02-template-1.md) - - [模板二](BinarySearch/03-template-2.md) - - [模板三](BinarySearch/04-template-3.md) - - [例题](BinarySearch/05-examples.md) - - [练习](BinarySearch/06-practices.md) + - [简介](/BinarySearch/01-introduction.md) + - [模板一](/BinarySearch/02-template-1.md) + - [模板二](/BinarySearch/03-template-2.md) + - [模板三](/BinarySearch/04-template-3.md) + - [例题](/BinarySearch/05-examples.md) + - [练习](/BinarySearch/06-practices.md) + +- Heap + - [优先队列和堆](/Heap/01-introduction.md) + - [堆的实现和堆排序](/Heap/02-template-1.md) + - [求(前/第)K(大/小/个)问题](/Heap/03-template-2.md) + - [双堆法(大小堆法)](/Heap/04-template-3.md) + - [例题](/Heap/05-examples.md) + - [练习](/Heap/06-practices.md) + - 并查集 - [简介](UnionFind/01-introduction.md) - [模板一](UnionFind/02-template-1.md)