This commit is contained in:
krahets
2026-04-03 18:46:15 +08:00
parent 377736b1bd
commit 9d21ca86b0
352 changed files with 46563 additions and 11262 deletions
+9 -9
View File
@@ -4,7 +4,7 @@ comments: true
# 10.1   Binary Search
<u>Binary search</u> is an efficient searching algorithm based on the divide-and-conquer strategy. It leverages the orderliness of data to reduce the search range by half in each round until the target element is found or the search interval becomes empty.
<u>Binary search</u> is an efficient search algorithm based on the divide-and-conquer strategy. It leverages the sorted order of the data to reduce the search range by half in each round until the target element is found or the search interval becomes empty.
!!! question
@@ -24,7 +24,7 @@ Next, perform the following two steps in a loop:
2. When `nums[m] > target`, it indicates that `target` is in the interval $[i, m - 1]$, so execute $j = m - 1$.
3. When `nums[m] = target`, it indicates that `target` has been found, so return index $m$.
If the array does not contain the target element, the search interval will eventually shrink to empty. In this case, return $-1$.
If the array does not contain the target element, the search interval will eventually become empty. In this case, return $-1$.
=== "<1>"
![Binary search process](binary_search.assets/binary_search_step1.png){ class="animation-figure" }
@@ -49,7 +49,7 @@ If the array does not contain the target element, the search interval will event
<p align="center"> Figure 10-2 &nbsp; Binary search process </p>
It's worth noting that since both $i$ and $j$ are of `int` type, **$i + j$ may exceed the range of the `int` type**. To avoid large number overflow, we typically use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint.
It's worth noting that since both $i$ and $j$ are of `int` type, **$i + j$ may exceed the range of the `int` type**. To avoid integer overflow, we typically use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint.
The code is shown below:
@@ -362,13 +362,13 @@ The code is shown below:
end
```
**Time complexity is $O(\log n)$**: In the binary loop, the interval is reduced by half each round, so the number of loops is $\log_2 n$.
**Time complexity is $O(\log n)$**: In the binary search loop, the interval is reduced by half each round, so the number of iterations is $\log_2 n$.
**Space complexity is $O(1)$**: Pointers $i$ and $j$ use constant-size space.
## 10.1.1 &nbsp; Interval Representation Methods
In addition to the closed interval mentioned above, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, meaning the left boundary includes itself while the right boundary does not. Under this representation, the interval $[i, j)$ is empty when $i = j$.
In addition to the closed interval mentioned above, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, meaning that the left boundary is inclusive while the right boundary is exclusive. Under this representation, the interval $[i, j)$ is empty when $i = j$.
We can implement a binary search algorithm with the same functionality based on this representation:
@@ -691,13 +691,13 @@ Since both the left and right boundaries in the "closed interval" representation
## 10.1.2 &nbsp; Advantages and Limitations
Binary search performs well in both time and space aspects.
Binary search offers good performance in both time and space.
- Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ loop rounds, while binary search only needs $\log_2 2^{20} = 20$ rounds.
- Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ iterations, while binary search only needs $\log_2 2^{20} = 20$ iterations.
- Binary search requires no extra space. Compared to searching algorithms that require additional space (such as hash-based search), binary search is more space-efficient.
However, binary search is not suitable for all situations, mainly for the following reasons:
- Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of $O(n \log n)$, which is higher than both linear search and binary search. For scenarios with frequent element insertions, maintaining array orderliness requires inserting elements at specific positions with a time complexity of $O(n)$, which is also very expensive.
- Binary search is only applicable to arrays. Binary search requires jump-style (non-contiguous) element access, and jump-style access has low efficiency in linked lists, making it unsuitable for linked lists or data structures based on linked list implementations.
- Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of $O(n \log n)$, which is higher than both linear search and binary search. For scenarios with frequent element insertions, keeping the array sorted requires inserting elements at specific positions with a time complexity of $O(n)$, which is also very expensive.
- Binary search is only applicable to arrays. Binary search requires non-contiguous, jump-style access to elements, and this kind of access is inefficient in linked lists, making it unsuitable for linked lists or linked-list-based data structures.
- For small data volumes, linear search performs better. In linear search, each round requires only 1 comparison operation; while in binary search, it requires 1 addition, 1 division, 1-3 comparison operations, and 1 addition (subtraction), totaling 4-6 unit operations. Therefore, when the data volume $n$ is small, linear search is actually faster than binary search.
@@ -2,13 +2,13 @@
comments: true
---
# 10.3 &nbsp; Binary Search Edge Cases
# 10.3 &nbsp; Binary Search Boundaries
## 10.3.1 &nbsp; Finding the Left Boundary
!!! question
Given a sorted array `nums` of length $n$ that may contain duplicate elements, return the index of the leftmost element `target` in the array. If the array does not contain the element, return $-1$.
Given a sorted array `nums` of length $n$ that may contain duplicate elements, return the index of the leftmost occurrence of `target`. If the array does not contain `target`, return $-1$.
Recall the method for finding the insertion point with binary search. After the search completes, $i$ points to the leftmost `target`, **so finding the insertion point is essentially finding the index of the leftmost `target`**.
@@ -232,9 +232,9 @@ Below we introduce two more clever methods.
### 1. &nbsp; Reusing Left Boundary Search
In fact, we can use the function for finding the leftmost element to find the rightmost element. The specific method is: **Convert finding the rightmost `target` into finding the leftmost `target + 1`**.
In fact, we can use the function for finding the leftmost `target` to find the rightmost `target`. The specific method is: **convert finding the rightmost `target` into finding the leftmost `target + 1`**.
As shown in Figure 10-7, after the search completes, pointer $i$ points to the leftmost `target + 1` (if it exists), while $j$ points to the rightmost `target`, **so we can simply return $j$**.
As shown in Figure 10-7, after the search completes, the pointer $i$ points to the leftmost `target + 1` (if it exists), while $j$ points to the rightmost `target`, **so we can return $j$**.
![Converting right boundary search to left boundary search](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" }
@@ -480,8 +480,8 @@ We know that when the array does not contain `target`, $i$ and $j$ will eventual
Therefore, as shown in Figure 10-8, we can construct an element that does not exist in the array to find the left and right boundaries.
- Finding the leftmost `target`: Can be converted to finding `target - 0.5` and returning pointer $i$.
- Finding the rightmost `target`: Can be converted to finding `target + 0.5` and returning pointer $j$.
- Finding the leftmost `target`: This can be converted to finding `target - 0.5` and returning the pointer $i$.
- Finding the rightmost `target`: This can be converted to finding `target + 0.5` and returning the pointer $j$.
![Converting boundary search to element search](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" }
@@ -489,5 +489,5 @@ Therefore, as shown in Figure 10-8, we can construct an element that does not ex
The code is omitted here, but the following two points are worth noting:
- Since the given array does not contain decimals, we don't need to worry about how to handle equal cases.
- Since the given array does not contain decimal values, we do not need to worry about how to handle equality.
- Because this method introduces decimals, the variable `target` in the function needs to be changed to a floating-point type (Python does not require this change).
@@ -4,13 +4,13 @@ comments: true
# 10.2 &nbsp; Binary Search Insertion Point
Binary search can not only be used to search for target elements but also to solve many variant problems, such as searching for the insertion position of a target element.
Binary search can be used not only to search for target elements, but also to solve many variant problems, such as finding the insertion position of a target element.
## 10.2.1 &nbsp; Case Without Duplicate Elements
!!! question
Given a sorted array `nums` of length $n$ and an element `target`, where the array contains no duplicate elements. Insert `target` into the array `nums` while maintaining its sorted order. If the array already contains the element `target`, insert it to its left. Return the index of `target` in the array after insertion. An example is shown in Figure 10-4.
Given a sorted array `nums` of length $n$ and an element `target`, where the array contains no duplicate elements, insert `target` into `nums` while maintaining its sorted order. If `target` already exists in the array, insert it to its left. Return the index of `target` after insertion. An example is shown below.
![Binary search insertion point example data](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" }
@@ -24,9 +24,9 @@ The problem requires inserting `target` to the left of equal elements, which mea
**Question 2**: When the array does not contain `target`, what is the insertion point index?
Further consider the binary search process: When `nums[m] < target`, $i$ moves, which means pointer $i$ is approaching elements greater than or equal to `target`. Similarly, pointer $j$ is always approaching elements less than or equal to `target`.
To analyze this further, consider the binary search process: when `nums[m] < target`, $i$ moves, meaning that pointer $i$ is approaching elements greater than or equal to `target`. Similarly, pointer $j$ is always approaching elements less than or equal to `target`.
Therefore, when the binary search ends, we must have: $i$ points to the first element greater than `target`, and $j$ points to the first element less than `target`. **It's easy to see that when the array does not contain `target`, the insertion index is $i$**. The code is shown below:
Therefore, when the binary search ends, $i$ must point to the first element greater than `target`, and $j$ must point to the first element less than `target`. **It follows that when the array does not contain `target`, the insertion index is $i$**. The code is shown below:
=== "Python"
@@ -323,7 +323,7 @@ Therefore, when the binary search ends, we must have: $i$ points to the first el
Suppose there are multiple `target` elements in the array. Ordinary binary search can only return the index of one `target`, **and cannot determine how many `target` elements are to the left and right of that element**.
The problem requires inserting the target element at the leftmost position, **so we need to find the index of the leftmost `target` in the array**. Initially, consider implementing this through the steps shown in Figure 10-5:
The problem requires inserting the target element at the leftmost position, **so we need to find the index of the leftmost `target` in the array**. A straightforward initial approach is to follow the steps shown in Figure 10-5:
1. Perform binary search to obtain the index of any `target`, denoted as $k$.
2. Starting from index $k$, perform linear traversal to the left, and return when the leftmost `target` is found.
@@ -334,10 +334,10 @@ The problem requires inserting the target element at the leftmost position, **so
Although this method works, it includes linear search, resulting in a time complexity of $O(n)$. When the array contains many duplicate `target` elements, this method is very inefficient.
Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: calculate the midpoint index $m$ in each round, then compare `target` with `nums[m]`, divided into the following cases:
Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: in each iteration, we first compute the midpoint index $m$, then compare `target` with `nums[m]`, leading to the following cases:
- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, so use the ordinary binary search interval narrowing operation to **make pointers $i$ and $j$ approach `target`**.
- When `nums[m] == target`, it means elements less than `target` are in the interval $[i, m - 1]$, so use $j = m - 1$ to narrow the interval, thereby **making pointer $j$ approach elements less than `target`**.
- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, so use the standard interval-shrinking operation of binary search to **move pointers $i$ and $j$ closer to `target`**.
- When `nums[m] == target`, it means elements less than `target` are in the interval $[i, m - 1]$, so use $j = m - 1$ to shrink the interval, thereby **moving pointer $j$ closer to elements less than `target`**.
After the loop completes, $i$ points to the leftmost `target`, and $j$ points to the first element less than `target`, **so index $i$ is the insertion point**.
@@ -367,7 +367,7 @@ After the loop completes, $i$ points to the leftmost `target`, and $j$ points to
<p align="center"> Figure 10-6 &nbsp; Steps for binary search insertion point of duplicate elements </p>
Observe the following code: the operations for branches `nums[m] > target` and `nums[m] == target` are the same, so the two can be merged.
Observe the following code: the branches `nums[m] > target` and `nums[m] == target` perform the same operation, so they can be merged.
Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable.
@@ -657,8 +657,8 @@ Even so, we can still keep the conditional branches expanded, as the logic is cl
!!! tip
The code in this section all uses the "closed interval" approach. Interested readers can implement the "left-closed right-open" approach themselves.
The code in this section uses the "closed interval" approach throughout. Interested readers can implement the "left-closed, right-open" approach themselves.
Overall, binary search is simply about setting search targets for pointers $i$ and $j$ separately. The target could be a specific element (such as `target`) or a range of elements (such as elements less than `target`).
Overall, binary search is simply a matter of setting separate search targets for pointers $i$ and $j$. The target may be a specific element (such as `target`) or a range of elements (such as elements less than `target`).
Through continuous binary iterations, both pointers $i$ and $j$ gradually approach their preset targets. Ultimately, they either successfully find the answer or stop after crossing the boundaries.
With each iteration of binary search, pointers $i$ and $j$ gradually approach their preset targets. Ultimately, they either find the answer or stop after crossing the boundary.
+3 -3
View File
@@ -16,8 +16,8 @@ icon: material/text-search
## Chapter contents
- [10.1 &nbsp; Binary Search](binary_search.md)
- [10.2 &nbsp; Binary Search Insertion](binary_search_insertion.md)
- [10.3 &nbsp; Binary Search Edge Cases](binary_search_edge.md)
- [10.2 &nbsp; Binary Search Insertion Point](binary_search_insertion.md)
- [10.3 &nbsp; Binary Search Boundaries](binary_search_edge.md)
- [10.4 &nbsp; Hash Optimization Strategy](replace_linear_by_hashing.md)
- [10.5 &nbsp; Search Algorithms Revisited](searching_algorithm_revisited.md)
- [10.5 &nbsp; Searching Algorithms Revisited](searching_algorithm_revisited.md)
- [10.6 &nbsp; Summary](summary.md)
@@ -8,11 +8,11 @@ In algorithm problems, **we often reduce the time complexity of algorithms by re
!!! question
Given an integer array `nums` and a target element `target`, search for two elements in the array whose "sum" equals `target`, and return their array indices. Any solution will do.
Given an integer array `nums` and a target value `target`, find two elements in the array whose sum is `target`, and return their indices. Any solution will do.
## 10.4.1 &nbsp; Linear Search: Trading Time for Space
Consider directly traversing all possible combinations. As shown in Figure 10-9, we open a two-layer loop and judge in each round whether the sum of two integers equals `target`. If so, return their indices.
Consider directly traversing all possible combinations. As shown in Figure 10-9, we use nested loops and check in each iteration whether the sum of two integers is `target`. If so, return their indices.
![Linear search solution for two sum](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" }
@@ -241,11 +241,11 @@ The code is shown below:
end
```
This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$, which is very time-consuming with large data volumes.
This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$, making it very time-consuming on large inputs.
## 10.4.2 &nbsp; Hash-Based Search: Trading Space for Time
Consider using a hash table where key-value pairs are array elements and element indices respectively. Loop through the array, performing the steps shown in Figure 10-10 in each round:
Consider using a hash table whose keys are array elements and whose values are their indices. Traverse the array and perform the steps shown in Figure 10-10 in each iteration:
1. Check if the number `target - nums[i]` is in the hash table. If so, directly return the indices of these two elements.
2. Add the key-value pair `nums[i]` and index `i` to the hash table.
@@ -261,7 +261,7 @@ Consider using a hash table where key-value pairs are array elements and element
<p align="center"> Figure 10-10 &nbsp; Hash table solution for two sum </p>
The implementation code is shown below, requiring only a single loop:
The implementation is shown below and requires only a single loop:
=== "Python"
@@ -533,4 +533,4 @@ The implementation code is shown below, requiring only a single loop:
This method reduces the time complexity from $O(n^2)$ to $O(n)$ through hash-based search, greatly improving runtime efficiency.
Since an additional hash table needs to be maintained, the space complexity is $O(n)$. **Nevertheless, this method achieves a more balanced overall time-space efficiency, making it the optimal solution for this problem**.
Since an additional hash table needs to be maintained, the space complexity is $O(n)$. **Nevertheless, this method offers a more balanced overall time-space trade-off, making it the optimal solution to this problem**.
@@ -9,9 +9,9 @@ comments: true
Searching algorithms can be divided into the following two categories based on their implementation approach:
- **Locating target elements by traversing the data structure**, such as traversing arrays, linked lists, trees, and graphs.
- **Achieving efficient element search by utilizing data organization structure or prior information contained in the data**, such as binary search, hash-based search, and binary search tree search.
- **Achieving efficient element lookup by leveraging the way data is organized or prior information about the data**, such as binary search, hash-based search, and binary search tree search.
It's not hard to see that these topics have all been covered in previous chapters, so searching algorithms are not unfamiliar to us. In this section, we will approach from a more systematic perspective and re-examine searching algorithms.
As these topics have already been introduced in earlier chapters, searching algorithms should already be familiar to us. In this section, we revisit them from a more systematic perspective.
## 10.5.1 &nbsp; Brute-Force Search
@@ -26,11 +26,11 @@ However, **the time complexity of such algorithms is $O(n)$**, where $n$ is the
## 10.5.2 &nbsp; Adaptive Search
Adaptive search utilizes the unique properties of data (such as orderliness) to optimize the search process, thereby locating target elements more efficiently.
Adaptive search leverages properties of the data itself (such as sorted order) to optimize the search process and locate target elements more efficiently.
- "Binary search" uses the orderliness of data to achieve efficient searching, applicable only to arrays.
- "Hash-based search" uses hash tables to establish key-value pair mappings between search data and target data, thereby achieving query operations.
- "Tree search" in specific tree structures (such as binary search trees), quickly eliminates nodes based on comparing node values to locate target elements.
- "Hash-based search" uses hash tables to store searchable data as key-value pairs, thereby enabling efficient queries.
- "Tree search" operates on specific tree structures (such as binary search trees), quickly ruling out nodes by comparing node values to locate the target element.
The advantage of such algorithms is high efficiency, **with time complexity reaching $O(\log n)$ or even $O(1)$**.
@@ -48,7 +48,7 @@ Given a dataset of size $n$, we can use linear search, binary search, tree searc
<p align="center"> Figure 10-11 &nbsp; Multiple search strategies </p>
The operational efficiency and characteristics of the above methods are as follows:
The efficiency and characteristics of these methods are summarized in Table 10-1.
<p align="center"> Table 10-1 &nbsp; Comparison of search algorithm efficiency </p>
@@ -69,26 +69,26 @@ The choice of search algorithm also depends on data volume, search performance r
**Linear search**
- Good generality, requiring no data preprocessing operations. If we only need to query the data once, the data preprocessing time for the other three methods would be longer than linear search.
- Good generality, requiring no data preprocessing operations. If we need to query the data only once, the preprocessing required by the other three methods can take longer than the linear search itself.
- Suitable for small data volumes, where time complexity has less impact on efficiency.
- Suitable for scenarios with high data update frequency, as this method does not require any additional data maintenance.
**Binary search**
- Suitable for large data volumes with stable efficiency performance, worst-case time complexity of $O(\log n)$.
- Suitable for large datasets, with stable performance and a worst-case time complexity of $O(\log n)$.
- Data volume cannot be too large, as storing arrays requires contiguous memory space.
- Not suitable for scenarios with frequent data insertion and deletion, as maintaining a sorted array has high overhead.
**Hash-based search**
- Suitable for scenarios with high query performance requirements, with an average time complexity of $O(1)$.
- Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain data orderliness.
- Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain the data in sorted order.
- High dependence on hash functions and hash collision handling strategies, with significant risk of performance degradation.
- Not suitable for excessively large data volumes, as hash tables require extra space to minimize collisions and thus provide good query performance.
**Tree search**
- Suitable for massive data, as tree nodes are stored dispersedly in memory.
- Suitable for scenarios requiring maintained ordered data or range searches.
- Suitable for massive datasets, as tree nodes are stored non-contiguously in memory.
- Suitable for scenarios that require maintaining ordered data or performing range searches.
- During continuous node insertion and deletion, binary search trees may become skewed, degrading time complexity to $O(n)$.
- If using AVL trees or red-black trees, all operations can run stably at $O(\log n)$ efficiency, but operations to maintain tree balance add extra overhead.
- If AVL trees or red-black trees are used, all operations can consistently run in $O(\log n)$ time, though maintaining tree balance adds extra overhead.
+3 -3
View File
@@ -6,9 +6,9 @@ comments: true
### 1. &nbsp; Key Review
- Binary search relies on data orderliness and progressively reduces the search interval by half through loops. It requires input data to be sorted and is only applicable to arrays or data structures based on array implementations.
- Brute-force search locates data by traversing the data structure. Linear search is applicable to arrays and linked lists, while breadth-first search and depth-first search are applicable to graphs and trees. Such algorithms have good generality and require no data preprocessing, but have a relatively high time complexity of $O(n)$.
- Binary search relies on ordered data and searches by repeatedly halving the search interval. It requires the input data to be sorted and applies only to arrays or array-based data structures.
- Brute-force search locates data by traversing the data structure. Linear search applies to arrays and linked lists, while breadth-first search and depth-first search apply to graphs and trees. These algorithms are broadly applicable and require no data preprocessing, but their relatively high time complexity is $O(n)$.
- Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching $O(\log n)$ or even $O(1)$, but typically require additional data structures.
- In practice, we need to analyze factors such as data scale, search performance requirements, and data query and update frequency to choose the appropriate search method.
- Linear search is suitable for small-scale or frequently updated data; binary search is suitable for large-scale, sorted data; hash-based search is suitable for data with high query efficiency requirements and no need for range queries; tree search is suitable for large-scale dynamic data that needs to maintain order and support range queries.
- Linear search is suitable for small datasets or data that is updated frequently; binary search is suitable for large sorted datasets; hash-based search is suitable when high query efficiency is required and range queries are unnecessary; tree search is suitable for large dynamic datasets that must maintain order and support range queries.
- Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from $O(n)$ to $O(1)$.