mirror of
https://github.com/krahets/hello-algo.git
synced 2026-08-14 20:50:58 +00:00
build
This commit is contained in:
@@ -2,39 +2,39 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 8.2 Heap construction operation
|
||||
# 8.2 Heap Construction Operation
|
||||
|
||||
In some cases, we want to build a heap using all elements of a list, and this process is known as "heap construction operation."
|
||||
In some cases, we want to build a heap using all elements of a list, and this process is called "heap construction operation."
|
||||
|
||||
## 8.2.1 Implementing with heap insertion operation
|
||||
## 8.2.1 Implementing with Element Insertion
|
||||
|
||||
First, we create an empty heap and then iterate through the list, performing the "heap insertion operation" on each element in turn. This means adding the element to the end of the heap and then "heapifying" it from bottom to top.
|
||||
We first create an empty heap, then iterate through the list, performing the "element insertion operation" on each element in sequence. This means adding the element to the bottom of the heap and then performing "bottom-to-top" heapify on that element.
|
||||
|
||||
Each time an element is added to the heap, the length of the heap increases by one. Since nodes are added to the binary tree from top to bottom, the heap is constructed "from top to bottom."
|
||||
Each time an element is inserted into the heap, the heap's length increases by one. Since nodes are added to the binary tree sequentially from top to bottom, the heap is constructed "from top to bottom."
|
||||
|
||||
Let the number of elements be $n$, and each element's insertion operation takes $O(\log{n})$ time, thus the time complexity of this heap construction method is $O(n \log n)$.
|
||||
Given $n$ elements, each element's insertion operation takes $O(\log{n})$ time, so the time complexity of this heap construction method is $O(n \log n)$.
|
||||
|
||||
## 8.2.2 Implementing by heapifying through traversal
|
||||
## 8.2.2 Implementing Through Heapify Traversal
|
||||
|
||||
In fact, we can implement a more efficient method of heap construction in two steps.
|
||||
In fact, we can implement a more efficient heap construction method in two steps.
|
||||
|
||||
1. Add all elements of the list as they are into the heap, at this point the properties of the heap are not yet satisfied.
|
||||
2. Traverse the heap in reverse order (reverse of level-order traversal), and perform "top to bottom heapify" on each non-leaf node.
|
||||
1. Add all elements of the list as-is to the heap, at which point the heap property is not yet satisfied.
|
||||
2. Traverse the heap in reverse order (reverse of level-order traversal), performing "top-to-bottom heapify" on each non-leaf node in sequence.
|
||||
|
||||
**After heapifying a node, the subtree with that node as the root becomes a valid sub-heap**. Since the traversal is in reverse order, the heap is built "from bottom to top."
|
||||
**After heapifying a node, the subtree rooted at that node becomes a valid sub-heap**. Since we traverse in reverse order, the heap is constructed "from bottom to top."
|
||||
|
||||
The reason for choosing reverse traversal is that it ensures the subtree below the current node is already a valid sub-heap, making the heapification of the current node effective.
|
||||
The reason for choosing reverse order traversal is that it ensures the subtree below the current node is already a valid sub-heap, making the heapification of the current node effective.
|
||||
|
||||
It's worth mentioning that **since leaf nodes have no children, they naturally form valid sub-heaps and do not need to be heapified**. As shown in the following code, the last non-leaf node is the parent of the last node; we start from it and traverse in reverse order to perform heapification:
|
||||
It's worth noting that **since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification**. As shown in the code below, the last non-leaf node is the parent of the last node; we start from it and traverse in reverse order to perform heapification:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="my_heap.py"
|
||||
def __init__(self, nums: list[int]):
|
||||
"""Constructor, build heap based on input list"""
|
||||
# Add all list elements into the heap
|
||||
# Add list elements to heap as is
|
||||
self.max_heap = nums
|
||||
# Heapify all nodes except leaves
|
||||
# Heapify all nodes except leaf nodes
|
||||
for i in range(self.parent(self.size() - 1), -1, -1):
|
||||
self.sift_down(i)
|
||||
```
|
||||
@@ -44,9 +44,9 @@ It's worth mentioning that **since leaf nodes have no children, they naturally f
|
||||
```cpp title="my_heap.cpp"
|
||||
/* Constructor, build heap based on input list */
|
||||
MaxHeap(vector<int> nums) {
|
||||
// Add all list elements into the heap
|
||||
// Add list elements to heap as is
|
||||
maxHeap = nums;
|
||||
// Heapify all nodes except leaves
|
||||
// Heapify all nodes except leaf nodes
|
||||
for (int i = parent(size() - 1); i >= 0; i--) {
|
||||
siftDown(i);
|
||||
}
|
||||
@@ -58,9 +58,9 @@ It's worth mentioning that **since leaf nodes have no children, they naturally f
|
||||
```java title="my_heap.java"
|
||||
/* Constructor, build heap based on input list */
|
||||
MaxHeap(List<Integer> nums) {
|
||||
// Add all list elements into the heap
|
||||
// Add list elements to heap as is
|
||||
maxHeap = new ArrayList<>(nums);
|
||||
// Heapify all nodes except leaves
|
||||
// Heapify all nodes except leaf nodes
|
||||
for (int i = parent(size() - 1); i >= 0; i--) {
|
||||
siftDown(i);
|
||||
}
|
||||
@@ -70,106 +70,295 @@ It's worth mentioning that **since leaf nodes have no children, they naturally f
|
||||
=== "C#"
|
||||
|
||||
```csharp title="my_heap.cs"
|
||||
[class]{MaxHeap}-[func]{MaxHeap}
|
||||
/* Constructor, build heap from input list */
|
||||
MaxHeap(IEnumerable<int> nums) {
|
||||
// Add list elements to heap as is
|
||||
maxHeap = new List<int>(nums);
|
||||
// Heapify all nodes except leaf nodes
|
||||
var size = Parent(this.Size() - 1);
|
||||
for (int i = size; i >= 0; i--) {
|
||||
SiftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="my_heap.go"
|
||||
[class]{maxHeap}-[func]{newMaxHeap}
|
||||
/* Constructor, build heap from slice */
|
||||
func newMaxHeap(nums []any) *maxHeap {
|
||||
// Add list elements to heap as is
|
||||
h := &maxHeap{data: nums}
|
||||
for i := h.parent(len(h.data) - 1); i >= 0; i-- {
|
||||
// Heapify all nodes except leaf nodes
|
||||
h.siftDown(i)
|
||||
}
|
||||
return h
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="my_heap.swift"
|
||||
[class]{MaxHeap}-[func]{init}
|
||||
/* Constructor, build heap based on input list */
|
||||
init(nums: [Int]) {
|
||||
// Add list elements to heap as is
|
||||
maxHeap = nums
|
||||
// Heapify all nodes except leaf nodes
|
||||
for i in (0 ... parent(i: size() - 1)).reversed() {
|
||||
siftDown(i: i)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="my_heap.js"
|
||||
[class]{MaxHeap}-[func]{constructor}
|
||||
/* Constructor, build empty heap or build heap from input list */
|
||||
constructor(nums) {
|
||||
// Add list elements to heap as is
|
||||
this.#maxHeap = nums === undefined ? [] : [...nums];
|
||||
// Heapify all nodes except leaf nodes
|
||||
for (let i = this.#parent(this.size() - 1); i >= 0; i--) {
|
||||
this.#siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="my_heap.ts"
|
||||
[class]{MaxHeap}-[func]{constructor}
|
||||
/* Constructor, build empty heap or build heap from input list */
|
||||
constructor(nums?: number[]) {
|
||||
// Add list elements to heap as is
|
||||
this.maxHeap = nums === undefined ? [] : [...nums];
|
||||
// Heapify all nodes except leaf nodes
|
||||
for (let i = this.parent(this.size() - 1); i >= 0; i--) {
|
||||
this.siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="my_heap.dart"
|
||||
[class]{MaxHeap}-[func]{MaxHeap}
|
||||
/* Constructor, build heap based on input list */
|
||||
MaxHeap(List<int> nums) {
|
||||
// Add list elements to heap as is
|
||||
_maxHeap = nums;
|
||||
// Heapify all nodes except leaf nodes
|
||||
for (int i = _parent(size() - 1); i >= 0; i--) {
|
||||
siftDown(i);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="my_heap.rs"
|
||||
[class]{MaxHeap}-[func]{new}
|
||||
/* Constructor, build heap based on input list */
|
||||
fn new(nums: Vec<i32>) -> Self {
|
||||
// Add list elements to heap as is
|
||||
let mut heap = MaxHeap { max_heap: nums };
|
||||
// Heapify all nodes except leaf nodes
|
||||
for i in (0..=Self::parent(heap.size() - 1)).rev() {
|
||||
heap.sift_down(i);
|
||||
}
|
||||
heap
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="my_heap.c"
|
||||
[class]{MaxHeap}-[func]{newMaxHeap}
|
||||
/* Constructor, build heap from slice */
|
||||
MaxHeap *newMaxHeap(int nums[], int size) {
|
||||
// Push all elements to heap
|
||||
MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap));
|
||||
maxHeap->size = size;
|
||||
memcpy(maxHeap->data, nums, size * sizeof(int));
|
||||
for (int i = parent(maxHeap, size - 1); i >= 0; i--) {
|
||||
// Heapify all nodes except leaf nodes
|
||||
siftDown(maxHeap, i);
|
||||
}
|
||||
return maxHeap;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="my_heap.kt"
|
||||
[class]{MaxHeap}-[func]{}
|
||||
/* Max heap */
|
||||
class MaxHeap(nums: MutableList<Int>?) {
|
||||
// Use list instead of array, no need to consider capacity expansion
|
||||
private val maxHeap = mutableListOf<Int>()
|
||||
|
||||
/* Constructor, build heap based on input list */
|
||||
init {
|
||||
// Add list elements to heap as is
|
||||
maxHeap.addAll(nums!!)
|
||||
// Heapify all nodes except leaf nodes
|
||||
for (i in parent(size() - 1) downTo 0) {
|
||||
siftDown(i)
|
||||
}
|
||||
}
|
||||
|
||||
/* Get index of left child node */
|
||||
private fun left(i: Int): Int {
|
||||
return 2 * i + 1
|
||||
}
|
||||
|
||||
/* Get index of right child node */
|
||||
private fun right(i: Int): Int {
|
||||
return 2 * i + 2
|
||||
}
|
||||
|
||||
/* Get index of parent node */
|
||||
private fun parent(i: Int): Int {
|
||||
return (i - 1) / 2 // Floor division
|
||||
}
|
||||
|
||||
/* Swap elements */
|
||||
private fun swap(i: Int, j: Int) {
|
||||
val temp = maxHeap[i]
|
||||
maxHeap[i] = maxHeap[j]
|
||||
maxHeap[j] = temp
|
||||
}
|
||||
|
||||
/* Get heap size */
|
||||
fun size(): Int {
|
||||
return maxHeap.size
|
||||
}
|
||||
|
||||
/* Check if heap is empty */
|
||||
fun isEmpty(): Boolean {
|
||||
/* Check if heap is empty */
|
||||
return size() == 0
|
||||
}
|
||||
|
||||
/* Access top element */
|
||||
fun peek(): Int {
|
||||
return maxHeap[0]
|
||||
}
|
||||
|
||||
/* Element enters heap */
|
||||
fun push(_val: Int) {
|
||||
// Add node
|
||||
maxHeap.add(_val)
|
||||
// Heapify from bottom to top
|
||||
siftUp(size() - 1)
|
||||
}
|
||||
|
||||
/* Starting from node i, heapify from bottom to top */
|
||||
private fun siftUp(it: Int) {
|
||||
// Kotlin function parameters are immutable, so create temporary variable
|
||||
var i = it
|
||||
while (true) {
|
||||
// Get parent node of node i
|
||||
val p = parent(i)
|
||||
// When "crossing root node" or "node needs no repair", end heapify
|
||||
if (p < 0 || maxHeap[i] <= maxHeap[p]) break
|
||||
// Swap two nodes
|
||||
swap(i, p)
|
||||
// Loop upward heapify
|
||||
i = p
|
||||
}
|
||||
}
|
||||
|
||||
/* Element exits heap */
|
||||
fun pop(): Int {
|
||||
// Handle empty case
|
||||
if (isEmpty()) throw IndexOutOfBoundsException()
|
||||
// Delete node
|
||||
swap(0, size() - 1)
|
||||
// Remove node
|
||||
val _val = maxHeap.removeAt(size() - 1)
|
||||
// Return top element
|
||||
siftDown(0)
|
||||
// Return heap top element
|
||||
return _val
|
||||
}
|
||||
|
||||
/* Starting from node i, heapify from top to bottom */
|
||||
private fun siftDown(it: Int) {
|
||||
// Kotlin function parameters are immutable, so create temporary variable
|
||||
var i = it
|
||||
while (true) {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
val l = left(i)
|
||||
val r = right(i)
|
||||
var ma = i
|
||||
if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l
|
||||
if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r
|
||||
// Swap two nodes
|
||||
if (ma == i) break
|
||||
// Swap two nodes
|
||||
swap(i, ma)
|
||||
// Loop downwards heapification
|
||||
i = ma
|
||||
}
|
||||
}
|
||||
|
||||
/* Driver Code */
|
||||
fun print() {
|
||||
val queue = PriorityQueue { a: Int, b: Int -> b - a }
|
||||
queue.addAll(maxHeap)
|
||||
printHeap(queue)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="my_heap.rb"
|
||||
[class]{MaxHeap}-[func]{initialize}
|
||||
### Constructor, build heap from input list ###
|
||||
def initialize(nums)
|
||||
# Add list elements to heap as is
|
||||
@max_heap = nums
|
||||
# Heapify all nodes except leaf nodes
|
||||
parent(size - 1).downto(0) do |i|
|
||||
sift_down(i)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 8.2.3 Complexity Analysis
|
||||
|
||||
```zig title="my_heap.zig"
|
||||
[class]{MaxHeap}-[func]{init}
|
||||
```
|
||||
Next, let's attempt to derive the time complexity of this second heap construction method.
|
||||
|
||||
## 8.2.3 Complexity analysis
|
||||
- Assuming the complete binary tree has $n$ nodes, then the number of leaf nodes is $(n + 1) / 2$, where $/$ is floor division. Therefore, the number of nodes that need heapification is $(n - 1) / 2$.
|
||||
- In the top-to-bottom heapify process, each node is heapified at most to the leaf nodes, so the maximum number of iterations is the binary tree height $\log n$.
|
||||
|
||||
Next, let's attempt to calculate the time complexity of this second method of heap construction.
|
||||
Multiplying these two together, we get a time complexity of $O(n \log n)$ for the heap construction process. **However, this estimate is not accurate because it doesn't account for the property that binary trees have far more nodes at lower levels than at upper levels**.
|
||||
|
||||
- Assuming the number of nodes in the complete binary tree is $n$, then the number of leaf nodes is $(n + 1) / 2$, where $/$ is integer division. Therefore, the number of nodes that need to be heapified is $(n - 1) / 2$.
|
||||
- In the process of "top to bottom heapification," each node is heapified to the leaf nodes at most, so the maximum number of iterations is the height of the binary tree $\log n$.
|
||||
Let's perform a more accurate calculation. To reduce calculation difficulty, assume a "perfect binary tree" with $n$ nodes and height $h$; this assumption does not affect the correctness of the result.
|
||||
|
||||
Multiplying the two, we get the time complexity of the heap construction process as $O(n \log n)$. **But this estimate is not accurate, because it does not take into account the nature of the binary tree having far more nodes at the lower levels than at the top.**
|
||||
{ class="animation-figure" }
|
||||
|
||||
Let's perform a more accurate calculation. To simplify the calculation, assume a "perfect binary tree" with $n$ nodes and height $h$; this assumption does not affect the correctness of the result.
|
||||
<p align="center"> Figure 8-5 Node count at each level of a perfect binary tree </p>
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 8-5 Node counts at each level of a perfect binary tree </p>
|
||||
|
||||
As shown in Figure 8-5, the maximum number of iterations for a node "to be heapified from top to bottom" is equal to the distance from that node to the leaf nodes, which is precisely "node height." Therefore, we can sum the "number of nodes $\times$ node height" at each level, **to get the total number of heapification iterations for all nodes**.
|
||||
As shown in Figure 8-5, the maximum number of iterations for a node's "top-to-bottom heapify" equals the distance from that node to the leaf nodes, which is precisely the "node height." Therefore, we can sum the "number of nodes $\times$ node height" at each level to **obtain the total number of heapify iterations for all nodes**.
|
||||
|
||||
$$
|
||||
T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1
|
||||
$$
|
||||
|
||||
To simplify the above equation, we need to use knowledge of sequences from high school, first multiply $T(h)$ by $2$, to get:
|
||||
To simplify the above expression, we need to use sequence knowledge from high school. First, multiply $T(h)$ by $2$ to get:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline
|
||||
2T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^h\times1 \newline
|
||||
2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^{h}\times1 \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
By subtracting $T(h)$ from $2T(h)$ using the method of displacement, we get:
|
||||
Using the method of differences, subtract the first equation $T(h)$ from the second equation $2 T(h)$ to get:
|
||||
|
||||
$$
|
||||
2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h
|
||||
$$
|
||||
|
||||
Observing the equation, $T(h)$ is an geometric series, which can be directly calculated using the sum formula, resulting in a time complexity of:
|
||||
Observing the above expression, we find that $T(h)$ is a geometric series, which can be calculated directly using the sum formula, yielding a time complexity of:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -179,4 +368,4 @@ T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Further, a perfect binary tree with height $h$ has $n = 2^{h+1} - 1$ nodes, thus the complexity is $O(2^h) = O(n)$. This calculation shows that **the time complexity of inputting a list and constructing a heap is $O(n)$, which is very efficient**.
|
||||
Furthermore, a perfect binary tree with height $h$ has $n = 2^{h+1} - 1$ nodes, so the complexity is $O(2^h) = O(n)$. This derivation shows that **the time complexity of building a heap from an input list is $O(n)$, which is highly efficient**.
|
||||
|
||||
+881
-273
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,13 @@ icon: material/family-tree
|
||||
|
||||
!!! abstract
|
||||
|
||||
Heaps resemble mountains and their jagged peaks, layered and undulating, each with its unique form.
|
||||
Heaps are like mountain peaks, layered and undulating, each with its unique form.
|
||||
|
||||
Each mountain peak rises and falls in scattered heights, yet the tallest always captures attention first.
|
||||
The peaks rise and fall at varying heights, yet the tallest peak always catches the eye first.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [8.1 Heap](heap.md)
|
||||
- [8.2 Building a heap](build_heap.md)
|
||||
- [8.3 Top-k problem](top_k.md)
|
||||
- [8.2 Building a Heap](build_heap.md)
|
||||
- [8.3 Top-K Problem](top_k.md)
|
||||
- [8.4 Summary](summary.md)
|
||||
|
||||
@@ -4,18 +4,18 @@ comments: true
|
||||
|
||||
# 8.4 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
- A heap is a complete binary tree that can be categorized as either a max heap or a min heap based on its building property, where the top element of a max heap is the largest and the top element of a min heap is the smallest.
|
||||
- A priority queue is defined as a queue with dequeue priority, usually implemented using a heap.
|
||||
- Common operations of a heap and their corresponding time complexities include: element insertion into the heap $O(\log n)$, removing the top element from the heap $O(\log n)$, and accessing the top element of the heap $O(1)$.
|
||||
- A complete binary tree is well-suited to be represented by an array, thus heaps are commonly stored using arrays.
|
||||
- Heapify operations are used to maintain the properties of the heap and are used in both heap insertion and removal operations.
|
||||
- The time complexity of building a heap given an input of $n$ elements can be optimized to $O(n)$, which is highly efficient.
|
||||
- A heap is a complete binary tree that can be categorized as a max heap or min heap based on its property. The heap top element of a max heap (min heap) is the largest (smallest).
|
||||
- A priority queue is defined as a queue with priority sorting, typically implemented using heaps.
|
||||
- Common heap operations and their corresponding time complexities include: element insertion $O(\log n)$, heap top element removal $O(\log n)$, and accessing the heap top element $O(1)$.
|
||||
- Complete binary trees are well-suited for array representation, so we typically use arrays to store heaps.
|
||||
- Heapify operations are used to maintain the heap property and are employed in both element insertion and removal operations.
|
||||
- The time complexity of building a heap with $n$ input elements can be optimized to $O(n)$, which is highly efficient.
|
||||
- Top-k is a classic algorithm problem that can be efficiently solved using the heap data structure, with a time complexity of $O(n \log k)$.
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**: Is the "heap" in data structures the same concept as the "heap" in memory management?
|
||||
**Q**: Are the "heap" in data structures and the "heap" in memory management the same concept?
|
||||
|
||||
The two are not the same concept, even though they are both referred to as "heap". The heap in computer system memory is part of dynamic memory allocation, where the program can use it to store data during execution. The program can request a certain amount of heap memory to store complex structures like objects and arrays. When the allocated data is no longer needed, the program needs to release this memory to prevent memory leaks. Compared to stack memory, the management and usage of heap memory demands more caution, as improper use may lead to memory leaks and dangling pointers.
|
||||
The two are not the same concept; they just happen to share the name "heap." The heap in computer system memory is part of dynamic memory allocation, where programs can use it to store data during runtime. Programs can request a certain amount of heap memory to store complex structures such as objects and arrays. When this data is no longer needed, the program needs to release this memory to prevent memory leaks. Compared to stack memory, heap memory management and usage require more caution, as improper use can lead to issues such as memory leaks and dangling pointers.
|
||||
|
||||
+294
-62
@@ -2,33 +2,33 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 8.3 Top-k problem
|
||||
# 8.3 Top-K Problem
|
||||
|
||||
!!! question
|
||||
|
||||
Given an unordered array `nums` of length $n$, return the largest $k$ elements in the array.
|
||||
|
||||
For this problem, we will first introduce two straightforward solutions, then explain a more efficient heap-based method.
|
||||
For this problem, we'll first introduce two solutions with relatively straightforward approaches, then introduce a more efficient heap-based solution.
|
||||
|
||||
## 8.3.1 Method 1: Iterative selection
|
||||
## 8.3.1 Method 1: Iterative Selection
|
||||
|
||||
We can perform $k$ rounds of iterations as shown in Figure 8-6, extracting the $1^{st}$, $2^{nd}$, $\dots$, $k^{th}$ largest elements in each round, with a time complexity of $O(nk)$.
|
||||
We can perform $k$ rounds of traversal as shown in Figure 8-6, extracting the $1^{st}$, $2^{nd}$, $\dots$, $k^{th}$ largest elements in each round, with a time complexity of $O(nk)$.
|
||||
|
||||
This method is only suitable when $k \ll n$, as the time complexity approaches $O(n^2)$ when $k$ is close to $n$, which is very time-consuming.
|
||||
This method is only suitable when $k \ll n$, because when $k$ is close to $n$, the time complexity approaches $O(n^2)$, which is very time-consuming.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 8-6 Iteratively finding the largest k elements </p>
|
||||
<p align="center"> Figure 8-6 Traversing to find the largest k elements </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
When $k = n$, we can obtain a complete ordered sequence, which is equivalent to the "selection sort" algorithm.
|
||||
When $k = n$, we can obtain a complete sorted sequence, which is equivalent to the "selection sort" algorithm.
|
||||
|
||||
## 8.3.2 Method 2: Sorting
|
||||
|
||||
As shown in Figure 8-7, we can first sort the array `nums` and then return the last $k$ elements, with a time complexity of $O(n \log n)$.
|
||||
As shown in Figure 8-7, we can first sort the array `nums`, then return the rightmost $k$ elements, with a time complexity of $O(n \log n)$.
|
||||
|
||||
Clearly, this method "overachieves" the task, as we only need to find the largest $k$ elements, without the need to sort the other elements.
|
||||
Clearly, this method "overachieves" the task, as we only need to find the largest $k$ elements, without needing to sort the other elements.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -36,15 +36,15 @@ Clearly, this method "overachieves" the task, as we only need to find the larges
|
||||
|
||||
## 8.3.3 Method 3: Heap
|
||||
|
||||
We can solve the Top-k problem more efficiently based on heaps, as shown in the following process.
|
||||
We can solve the Top-k problem more efficiently using heaps, with the process shown in Figure 8-8.
|
||||
|
||||
1. Initialize a min heap, where the top element is the smallest.
|
||||
2. First, insert the first $k$ elements of the array into the heap.
|
||||
3. Starting from the $k + 1^{th}$ element, if the current element is greater than the top element of the heap, remove the top element of the heap and insert the current element into the heap.
|
||||
4. After completing the traversal, the heap contains the largest $k$ elements.
|
||||
1. Initialize a min heap, where the heap top element is the smallest.
|
||||
2. First, insert the first $k$ elements of the array into the heap in sequence.
|
||||
3. Starting from the $(k + 1)^{th}$ element, if the current element is greater than the heap top element, remove the heap top element and insert the current element into the heap.
|
||||
4. After traversal is complete, the heap contains the largest $k$ elements.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -70,7 +70,7 @@ We can solve the Top-k problem more efficiently based on heaps, as shown in the
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 8-8 Find the largest k elements based on heap </p>
|
||||
<p align="center"> Figure 8-8 Finding the largest k elements using a heap </p>
|
||||
|
||||
Example code is as follows:
|
||||
|
||||
@@ -78,15 +78,15 @@ Example code is as follows:
|
||||
|
||||
```python title="top_k.py"
|
||||
def top_k_heap(nums: list[int], k: int) -> list[int]:
|
||||
"""Using heap to find the largest k elements in an array"""
|
||||
# Initialize min-heap
|
||||
"""Find the largest k elements in array based on heap"""
|
||||
# Initialize min heap
|
||||
heap = []
|
||||
# Enter the first k elements of the array into the heap
|
||||
# Enter the first k elements of array into heap
|
||||
for i in range(k):
|
||||
heapq.heappush(heap, nums[i])
|
||||
# From the k+1th element, keep the heap length as k
|
||||
# Starting from the (k+1)th element, maintain heap length as k
|
||||
for i in range(k, len(nums)):
|
||||
# If the current element is larger than the heap top element, remove the heap top element and enter the current element into the heap
|
||||
# If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if nums[i] > heap[0]:
|
||||
heapq.heappop(heap)
|
||||
heapq.heappush(heap, nums[i])
|
||||
@@ -96,17 +96,17 @@ Example code is as follows:
|
||||
=== "C++"
|
||||
|
||||
```cpp title="top_k.cpp"
|
||||
/* Using heap to find the largest k elements in an array */
|
||||
/* Find the largest k elements in array based on heap */
|
||||
priority_queue<int, vector<int>, greater<int>> topKHeap(vector<int> &nums, int k) {
|
||||
// Initialize min-heap
|
||||
// Python's heapq module implements min heap by default
|
||||
priority_queue<int, vector<int>, greater<int>> heap;
|
||||
// Enter the first k elements of the array into the heap
|
||||
// Enter the first k elements of array into heap
|
||||
for (int i = 0; i < k; i++) {
|
||||
heap.push(nums[i]);
|
||||
}
|
||||
// From the k+1th element, keep the heap length as k
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for (int i = k; i < nums.size(); i++) {
|
||||
// If the current element is larger than the heap top element, remove the heap top element and enter the current element into the heap
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if (nums[i] > heap.top()) {
|
||||
heap.pop();
|
||||
heap.push(nums[i]);
|
||||
@@ -119,17 +119,17 @@ Example code is as follows:
|
||||
=== "Java"
|
||||
|
||||
```java title="top_k.java"
|
||||
/* Using heap to find the largest k elements in an array */
|
||||
/* Find the largest k elements in array based on heap */
|
||||
Queue<Integer> topKHeap(int[] nums, int k) {
|
||||
// Initialize min-heap
|
||||
// Python's heapq module implements min heap by default
|
||||
Queue<Integer> heap = new PriorityQueue<Integer>();
|
||||
// Enter the first k elements of the array into the heap
|
||||
// Enter the first k elements of array into heap
|
||||
for (int i = 0; i < k; i++) {
|
||||
heap.offer(nums[i]);
|
||||
}
|
||||
// From the k+1th element, keep the heap length as k
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for (int i = k; i < nums.length; i++) {
|
||||
// If the current element is larger than the heap top element, remove the heap top element and enter the current element into the heap
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if (nums[i] > heap.peek()) {
|
||||
heap.poll();
|
||||
heap.offer(nums[i]);
|
||||
@@ -142,93 +142,325 @@ Example code is as follows:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="top_k.cs"
|
||||
[class]{top_k}-[func]{TopKHeap}
|
||||
/* Find the largest k elements in array based on heap */
|
||||
PriorityQueue<int, int> TopKHeap(int[] nums, int k) {
|
||||
// Python's heapq module implements min heap by default
|
||||
PriorityQueue<int, int> heap = new();
|
||||
// Enter the first k elements of array into heap
|
||||
for (int i = 0; i < k; i++) {
|
||||
heap.Enqueue(nums[i], nums[i]);
|
||||
}
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for (int i = k; i < nums.Length; i++) {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if (nums[i] > heap.Peek()) {
|
||||
heap.Dequeue();
|
||||
heap.Enqueue(nums[i], nums[i]);
|
||||
}
|
||||
}
|
||||
return heap;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="top_k.go"
|
||||
[class]{}-[func]{topKHeap}
|
||||
/* Find the largest k elements in array based on heap */
|
||||
func topKHeap(nums []int, k int) *minHeap {
|
||||
// Python's heapq module implements min heap by default
|
||||
h := &minHeap{}
|
||||
heap.Init(h)
|
||||
// Enter the first k elements of array into heap
|
||||
for i := 0; i < k; i++ {
|
||||
heap.Push(h, nums[i])
|
||||
}
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for i := k; i < len(nums); i++ {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if nums[i] > h.Top().(int) {
|
||||
heap.Pop(h)
|
||||
heap.Push(h, nums[i])
|
||||
}
|
||||
}
|
||||
return h
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="top_k.swift"
|
||||
[class]{}-[func]{topKHeap}
|
||||
/* Find the largest k elements in array based on heap */
|
||||
func topKHeap(nums: [Int], k: Int) -> [Int] {
|
||||
// Initialize min heap and build heap with first k elements
|
||||
var heap = Heap(nums.prefix(k))
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for i in nums.indices.dropFirst(k) {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if nums[i] > heap.min()! {
|
||||
_ = heap.removeMin()
|
||||
heap.insert(nums[i])
|
||||
}
|
||||
}
|
||||
return heap.unordered
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="top_k.js"
|
||||
[class]{}-[func]{pushMinHeap}
|
||||
/* Element enters heap */
|
||||
function pushMinHeap(maxHeap, val) {
|
||||
// Negate element
|
||||
maxHeap.push(-val);
|
||||
}
|
||||
|
||||
[class]{}-[func]{popMinHeap}
|
||||
/* Element exits heap */
|
||||
function popMinHeap(maxHeap) {
|
||||
// Negate element
|
||||
return -maxHeap.pop();
|
||||
}
|
||||
|
||||
[class]{}-[func]{peekMinHeap}
|
||||
/* Access top element */
|
||||
function peekMinHeap(maxHeap) {
|
||||
// Negate element
|
||||
return -maxHeap.peek();
|
||||
}
|
||||
|
||||
[class]{}-[func]{getMinHeap}
|
||||
/* Extract elements from heap */
|
||||
function getMinHeap(maxHeap) {
|
||||
// Negate element
|
||||
return maxHeap.getMaxHeap().map((num) => -num);
|
||||
}
|
||||
|
||||
[class]{}-[func]{topKHeap}
|
||||
/* Find the largest k elements in array based on heap */
|
||||
function topKHeap(nums, k) {
|
||||
// Python's heapq module implements min heap by default
|
||||
// Note: We negate all heap elements to simulate min heap using max heap
|
||||
const maxHeap = new MaxHeap([]);
|
||||
// Enter the first k elements of array into heap
|
||||
for (let i = 0; i < k; i++) {
|
||||
pushMinHeap(maxHeap, nums[i]);
|
||||
}
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for (let i = k; i < nums.length; i++) {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if (nums[i] > peekMinHeap(maxHeap)) {
|
||||
popMinHeap(maxHeap);
|
||||
pushMinHeap(maxHeap, nums[i]);
|
||||
}
|
||||
}
|
||||
// Return elements in heap
|
||||
return getMinHeap(maxHeap);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="top_k.ts"
|
||||
[class]{}-[func]{pushMinHeap}
|
||||
/* Element enters heap */
|
||||
function pushMinHeap(maxHeap: MaxHeap, val: number): void {
|
||||
// Negate element
|
||||
maxHeap.push(-val);
|
||||
}
|
||||
|
||||
[class]{}-[func]{popMinHeap}
|
||||
/* Element exits heap */
|
||||
function popMinHeap(maxHeap: MaxHeap): number {
|
||||
// Negate element
|
||||
return -maxHeap.pop();
|
||||
}
|
||||
|
||||
[class]{}-[func]{peekMinHeap}
|
||||
/* Access top element */
|
||||
function peekMinHeap(maxHeap: MaxHeap): number {
|
||||
// Negate element
|
||||
return -maxHeap.peek();
|
||||
}
|
||||
|
||||
[class]{}-[func]{getMinHeap}
|
||||
/* Extract elements from heap */
|
||||
function getMinHeap(maxHeap: MaxHeap): number[] {
|
||||
// Negate element
|
||||
return maxHeap.getMaxHeap().map((num: number) => -num);
|
||||
}
|
||||
|
||||
[class]{}-[func]{topKHeap}
|
||||
/* Find the largest k elements in array based on heap */
|
||||
function topKHeap(nums: number[], k: number): number[] {
|
||||
// Python's heapq module implements min heap by default
|
||||
// Note: We negate all heap elements to simulate min heap using max heap
|
||||
const maxHeap = new MaxHeap([]);
|
||||
// Enter the first k elements of array into heap
|
||||
for (let i = 0; i < k; i++) {
|
||||
pushMinHeap(maxHeap, nums[i]);
|
||||
}
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for (let i = k; i < nums.length; i++) {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if (nums[i] > peekMinHeap(maxHeap)) {
|
||||
popMinHeap(maxHeap);
|
||||
pushMinHeap(maxHeap, nums[i]);
|
||||
}
|
||||
}
|
||||
// Return elements in heap
|
||||
return getMinHeap(maxHeap);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="top_k.dart"
|
||||
[class]{}-[func]{topKHeap}
|
||||
/* Find the largest k elements in array based on heap */
|
||||
MinHeap topKHeap(List<int> nums, int k) {
|
||||
// Initialize min heap, push first k elements of array to heap
|
||||
MinHeap heap = MinHeap(nums.sublist(0, k));
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for (int i = k; i < nums.length; i++) {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if (nums[i] > heap.peek()) {
|
||||
heap.pop();
|
||||
heap.push(nums[i]);
|
||||
}
|
||||
}
|
||||
return heap;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="top_k.rs"
|
||||
[class]{}-[func]{top_k_heap}
|
||||
/* Find the largest k elements in array based on heap */
|
||||
fn top_k_heap(nums: Vec<i32>, k: usize) -> BinaryHeap<Reverse<i32>> {
|
||||
// BinaryHeap is a max heap, use Reverse to negate elements to implement min heap
|
||||
let mut heap = BinaryHeap::<Reverse<i32>>::new();
|
||||
// Enter the first k elements of array into heap
|
||||
for &num in nums.iter().take(k) {
|
||||
heap.push(Reverse(num));
|
||||
}
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for &num in nums.iter().skip(k) {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if num > heap.peek().unwrap().0 {
|
||||
heap.pop();
|
||||
heap.push(Reverse(num));
|
||||
}
|
||||
}
|
||||
heap
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="top_k.c"
|
||||
[class]{}-[func]{pushMinHeap}
|
||||
/* Element enters heap */
|
||||
void pushMinHeap(MaxHeap *maxHeap, int val) {
|
||||
// Negate element
|
||||
push(maxHeap, -val);
|
||||
}
|
||||
|
||||
[class]{}-[func]{popMinHeap}
|
||||
/* Element exits heap */
|
||||
int popMinHeap(MaxHeap *maxHeap) {
|
||||
// Negate element
|
||||
return -pop(maxHeap);
|
||||
}
|
||||
|
||||
[class]{}-[func]{peekMinHeap}
|
||||
/* Access top element */
|
||||
int peekMinHeap(MaxHeap *maxHeap) {
|
||||
// Negate element
|
||||
return -peek(maxHeap);
|
||||
}
|
||||
|
||||
[class]{}-[func]{getMinHeap}
|
||||
/* Extract elements from heap */
|
||||
int *getMinHeap(MaxHeap *maxHeap) {
|
||||
// Negate all heap elements and store in res array
|
||||
int *res = (int *)malloc(maxHeap->size * sizeof(int));
|
||||
for (int i = 0; i < maxHeap->size; i++) {
|
||||
res[i] = -maxHeap->data[i];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
[class]{}-[func]{topKHeap}
|
||||
/* Extract elements from heap */
|
||||
int *getMinHeap(MaxHeap *maxHeap) {
|
||||
// Negate all heap elements and store in res array
|
||||
int *res = (int *)malloc(maxHeap->size * sizeof(int));
|
||||
for (int i = 0; i < maxHeap->size; i++) {
|
||||
res[i] = -maxHeap->data[i];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Function to find k largest elements in array using heap
|
||||
int *topKHeap(int *nums, int sizeNums, int k) {
|
||||
// Python's heapq module implements min heap by default
|
||||
// Note: We negate all heap elements to simulate min heap using max heap
|
||||
int *empty = (int *)malloc(0);
|
||||
MaxHeap *maxHeap = newMaxHeap(empty, 0);
|
||||
// Enter the first k elements of array into heap
|
||||
for (int i = 0; i < k; i++) {
|
||||
pushMinHeap(maxHeap, nums[i]);
|
||||
}
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for (int i = k; i < sizeNums; i++) {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if (nums[i] > peekMinHeap(maxHeap)) {
|
||||
popMinHeap(maxHeap);
|
||||
pushMinHeap(maxHeap, nums[i]);
|
||||
}
|
||||
}
|
||||
int *res = getMinHeap(maxHeap);
|
||||
// Free memory
|
||||
delMaxHeap(maxHeap);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="top_k.kt"
|
||||
[class]{}-[func]{topKHeap}
|
||||
/* Find the largest k elements in array based on heap */
|
||||
fun topKHeap(nums: IntArray, k: Int): Queue<Int> {
|
||||
// Python's heapq module implements min heap by default
|
||||
val heap = PriorityQueue<Int>()
|
||||
// Enter the first k elements of array into heap
|
||||
for (i in 0..<k) {
|
||||
heap.offer(nums[i])
|
||||
}
|
||||
// Starting from the (k+1)th element, maintain heap length as k
|
||||
for (i in k..<nums.size) {
|
||||
// If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if (nums[i] > heap.peek()) {
|
||||
heap.poll()
|
||||
heap.offer(nums[i])
|
||||
}
|
||||
}
|
||||
return heap
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="top_k.rb"
|
||||
[class]{}-[func]{top_k_heap}
|
||||
### Find largest k elements in array using heap ###
|
||||
def top_k_heap(nums, k)
|
||||
# Python's heapq module implements min heap by default
|
||||
# Note: We negate all heap elements to simulate min heap using max heap
|
||||
max_heap = MaxHeap.new([])
|
||||
|
||||
# Enter the first k elements of array into heap
|
||||
for i in 0...k
|
||||
push_min_heap(max_heap, nums[i])
|
||||
end
|
||||
|
||||
# Starting from the (k+1)th element, maintain heap length as k
|
||||
for i in k...nums.length
|
||||
# If current element is greater than top element, top element exits heap, current element enters heap
|
||||
if nums[i] > peek_min_heap(max_heap)
|
||||
pop_min_heap(max_heap)
|
||||
push_min_heap(max_heap, nums[i])
|
||||
end
|
||||
end
|
||||
|
||||
get_min_heap(max_heap)
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
A total of $n$ rounds of heap insertions and removals are performed, with the heap's maximum length being $k$, so the time complexity is $O(n \log k)$. This method is very efficient; when $k$ is small, the time complexity approaches $O(n)$; when $k$ is large, the time complexity does not exceed $O(n \log n)$.
|
||||
|
||||
```zig title="top_k.zig"
|
||||
[class]{}-[func]{topKHeap}
|
||||
```
|
||||
|
||||
A total of $n$ rounds of heap insertions and deletions are performed, with the maximum heap size being $k$, hence the time complexity is $O(n \log k)$. This method is very efficient; when $k$ is small, the time complexity tends towards $O(n)$; when $k$ is large, the time complexity will not exceed $O(n \log n)$.
|
||||
|
||||
Additionally, this method is suitable for scenarios with dynamic data streams. By continuously adding data, we can maintain the elements within the heap, thereby achieving dynamic updates of the largest $k$ elements.
|
||||
Additionally, this method is suitable for dynamic data stream scenarios. By continuously adding data, we can maintain the elements in the heap, thus achieving dynamic updates of the largest $k$ elements.
|
||||
|
||||
Reference in New Issue
Block a user