mirror of
https://github.com/krahets/hello-algo.git
synced 2026-08-11 19:30:59 +00:00
build
This commit is contained in:
@@ -2,45 +2,45 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 16.2 Contributing
|
||||
# 16.2 Contributing Together
|
||||
|
||||
Due to the limited abilities of the author, some omissions and errors are inevitable in this book. Please understand. If you discover any typos, broken links, missing content, textual ambiguities, unclear explanations, or unreasonable text structures, please assist us in making corrections to provide readers with better quality learning resources.
|
||||
Due to limited capacity, there may be inevitable omissions and errors in this book. We appreciate your understanding and are grateful for your help in correcting them. If you discover typos, broken links, missing content, ambiguous wording, unclear explanations, or structural issues, please help us make corrections to provide readers with higher-quality learning resources.
|
||||
|
||||
The GitHub IDs of all [contributors](https://github.com/krahets/hello-algo/graphs/contributors) will be displayed on the repository, web, and PDF versions of the homepage of this book to thank them for their selfless contributions to the open-source community.
|
||||
The GitHub IDs of all [contributors](https://github.com/krahets/hello-algo/graphs/contributors) will be displayed on the homepage of the book repository, the web version, and the PDF version to acknowledge their selfless contributions to the open source community.
|
||||
|
||||
!!! success "The charm of open source"
|
||||
!!! success "The Charm of Open Source"
|
||||
|
||||
The interval between two printings of a paper book is often long, making content updates very inconvenient.
|
||||
|
||||
In this open-source book, however, the content update cycle is shortened to just a few days or even hours.
|
||||
The interval between two printings of a physical book is often quite long, making content updates very inconvenient.
|
||||
|
||||
### 1. Content fine-tuning
|
||||
In this open source book, the time for content updates has been shortened to just days or even hours.
|
||||
|
||||
As shown in Figure 16-3, there is an "edit icon" in the upper right corner of each page. You can follow these steps to modify text or code.
|
||||
### 1. Minor Content Adjustments
|
||||
|
||||
1. Click the "edit icon". If prompted to "fork this repository", please agree to do so.
|
||||
2. Modify the Markdown source file content, check the accuracy of the content, and try to keep the formatting consistent.
|
||||
3. Fill in the modification description at the bottom of the page, then click the "Propose file change" button. After the page redirects, click the "Create pull request" button to initiate the pull request.
|
||||
As shown in Figure 16-3, there is an "edit icon" in the top-right corner of each page. You can modify text or code by following these steps.
|
||||
|
||||
{ class="animation-figure" }
|
||||
1. Click the "edit icon". If you encounter a prompt asking you to "Fork this repository", please approve the operation.
|
||||
2. Modify the content of the Markdown source file, verify the correctness of the content, and maintain consistent formatting as much as possible.
|
||||
3. Fill in a description of your changes at the bottom of the page, then click the "Propose file change" button. After the page transitions, click the "Create pull request" button to submit your pull request.
|
||||
|
||||
<p align="center"> Figure 16-3 Edit page button </p>
|
||||
{ class="animation-figure" }
|
||||
|
||||
Figures cannot be directly modified and require the creation of a new [Issue](https://github.com/krahets/hello-algo/issues) or a comment to describe the problem. We will redraw and replace the figures as soon as possible.
|
||||
<p align="center"> Figure 16-3 Page edit button </p>
|
||||
|
||||
### 2. Content creation
|
||||
Images cannot be directly modified. Please describe the issue by creating a new [Issue](https://github.com/krahets/hello-algo/issues) or leaving a comment. We will promptly redraw and replace the images.
|
||||
|
||||
If you are interested in participating in this open-source project, including translating code into other programming languages or expanding article content, then the following Pull Request workflow needs to be implemented.
|
||||
### 2. Content Creation
|
||||
|
||||
1. Log in to GitHub and Fork the [code repository](https://github.com/krahets/hello-algo) of this book to your personal account.
|
||||
2. Go to your Forked repository web page and use the `git clone` command to clone the repository to your local machine.
|
||||
3. Create content locally and perform complete tests to verify the correctness of the code.
|
||||
4. Commit the changes made locally, then push them to the remote repository.
|
||||
5. Refresh the repository webpage and click the "Create pull request" button to initiate the pull request.
|
||||
If you are interested in contributing to this open source project, including translating code into other programming languages or expanding article content, you will need to follow the Pull Request workflow below.
|
||||
|
||||
### 3. Docker deployment
|
||||
1. Log in to GitHub and Fork the book's [code repository](https://github.com/krahets/hello-algo) to your personal account.
|
||||
2. Enter your forked repository webpage and use the `git clone` command to clone the repository to your local machine.
|
||||
3. Create content locally and conduct comprehensive tests to verify code correctness.
|
||||
4. Commit your local changes and push them to the remote repository.
|
||||
5. Refresh the repository webpage and click the "Create pull request" button to submit your pull request.
|
||||
|
||||
In the `hello-algo` root directory, execute the following Docker script to access the project at `http://localhost:8000`:
|
||||
### 3. Docker Deployment
|
||||
|
||||
From the root directory of `hello-algo`, run the following Docker script to access the project at `http://localhost:8000`:
|
||||
|
||||
```shell
|
||||
docker-compose up -d
|
||||
|
||||
@@ -9,6 +9,6 @@ icon: material/help-circle-outline
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [16.1 Installation](installation.md)
|
||||
- [16.2 Contributing](contribution.md)
|
||||
- [16.3 Terminology](terminology.md)
|
||||
- [16.1 Programming Environment Installation](installation.md)
|
||||
- [16.2 Contributing Together](contribution.md)
|
||||
- [16.3 Terminology Table](terminology.md)
|
||||
|
||||
@@ -2,75 +2,75 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 16.1 Installation
|
||||
# 16.1 Programming Environment Installation
|
||||
|
||||
## 16.1.1 Install IDE
|
||||
## 16.1.1 Installing Ide
|
||||
|
||||
We recommend using the open-source, lightweight VS Code as your local Integrated Development Environment (IDE). Visit the [VS Code official website](https://code.visualstudio.com/) and choose the version of VS Code appropriate for your operating system to download and install.
|
||||
We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the [VS Code official website](https://code.visualstudio.com/), and download and install the appropriate version of VS Code according to your operating system.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 16-1 Download VS Code from the official website </p>
|
||||
<p align="center"> Figure 16-1 Download VS Code from the Official Website </p>
|
||||
|
||||
VS Code has a powerful extension ecosystem, supporting the execution and debugging of most programming languages. For example, after installing the "Python Extension Pack," you can debug Python code. The installation steps are shown in Figure 16-2.
|
||||
VS Code has a powerful ecosystem of extensions that supports running and debugging most programming languages. For example, after installing the "Python Extension Pack" extension, you can debug Python code. The installation steps are shown in the following figure.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 16-2 Install VS Code Extension Pack </p>
|
||||
<p align="center"> Figure 16-2 Install VS Code Extensions </p>
|
||||
|
||||
## 16.1.2 Install language environments
|
||||
## 16.1.2 Installing Language Environments
|
||||
|
||||
### 1. Python environment
|
||||
### 1. Python Environment
|
||||
|
||||
1. Download and install [Miniconda3](https://docs.conda.io/en/latest/miniconda.html), requiring Python 3.10 or newer.
|
||||
2. In the VS Code extension marketplace, search for `python` and install the Python Extension Pack.
|
||||
3. (Optional) Enter `pip install black` in the command line to install the code formatting tool.
|
||||
1. Download and install [Miniconda3](https://docs.conda.io/en/latest/miniconda.html), which requires Python 3.10 or newer.
|
||||
2. Search for `python` in the VS Code extension marketplace and install the Python Extension Pack.
|
||||
3. (Optional) Enter `pip install black` on the command line to install the code formatter.
|
||||
|
||||
### 2. C/C++ environment
|
||||
### 2. C/c++ Environment
|
||||
|
||||
1. Windows systems need to install [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([Configuration tutorial](https://blog.csdn.net/qq_33698226/article/details/129031241)); MacOS comes with Clang, so no installation is necessary.
|
||||
2. In the VS Code extension marketplace, search for `c++` and install the C/C++ Extension Pack.
|
||||
1. Windows systems need to install [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([configuration tutorial](https://blog.csdn.net/qq_33698226/article/details/129031241)); macOS comes with Clang built-in and does not require installation.
|
||||
2. Search for `c++` in the VS Code extension marketplace and install the C/C++ Extension Pack.
|
||||
3. (Optional) Open the Settings page, search for the `Clang_format_fallback Style` code formatting option, and set it to `{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }`.
|
||||
|
||||
### 3. Java environment
|
||||
### 3. Java Environment
|
||||
|
||||
1. Download and install [OpenJDK](https://jdk.java.net/18/) (version must be > JDK 9).
|
||||
2. In the VS Code extension marketplace, search for `java` and install the Extension Pack for Java.
|
||||
2. Search for `java` in the VS Code extension marketplace and install the Extension Pack for Java.
|
||||
|
||||
### 4. C# environment
|
||||
### 4. C# Environment
|
||||
|
||||
1. Download and install [.Net 8.0](https://dotnet.microsoft.com/en-us/download).
|
||||
2. In the VS Code extension marketplace, search for `C# Dev Kit` and install the C# Dev Kit ([Configuration tutorial](https://code.visualstudio.com/docs/csharp/get-started)).
|
||||
3. You can also use Visual Studio ([Installation tutorial](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022)).
|
||||
2. Search for `C# Dev Kit` in the VS Code extension marketplace and install C# Dev Kit ([configuration tutorial](https://code.visualstudio.com/docs/csharp/get-started)).
|
||||
3. You can also use Visual Studio ([installation tutorial](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022)).
|
||||
|
||||
### 5. Go environment
|
||||
### 5. Go Environment
|
||||
|
||||
1. Download and install [go](https://go.dev/dl/).
|
||||
2. In the VS Code extension marketplace, search for `go` and install Go.
|
||||
3. Press `Ctrl + Shift + P` to call up the command bar, enter go, choose `Go: Install/Update Tools`, select all and install.
|
||||
1. Download and install [Go](https://go.dev/dl/).
|
||||
2. Search for `go` in the VS Code extension marketplace and install Go.
|
||||
3. Press `Ctrl + Shift + P` to open the command palette, type `go`, select `Go: Install/Update Tools`, check all options and install.
|
||||
|
||||
### 6. Swift environment
|
||||
### 6. Swift Environment
|
||||
|
||||
1. Download and install [Swift](https://www.swift.org/download/).
|
||||
2. In the VS Code extension marketplace, search for `swift` and install [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang).
|
||||
2. Search for `swift` in the VS Code extension marketplace and install [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang).
|
||||
|
||||
### 7. JavaScript environment
|
||||
### 7. Javascript Environment
|
||||
|
||||
1. Download and install [Node.js](https://nodejs.org/en/).
|
||||
2. (Optional) In the VS Code extension marketplace, search for `Prettier` and install the code formatting tool.
|
||||
2. (Optional) Search for `Prettier` in the VS Code extension marketplace and install the code formatter.
|
||||
|
||||
### 8. TypeScript environment
|
||||
### 8. Typescript Environment
|
||||
|
||||
1. Follow the same installation steps as the JavaScript environment.
|
||||
2. Install [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation).
|
||||
3. In the VS Code extension marketplace, search for `typescript` and install [Pretty TypeScript Errors](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors).
|
||||
3. Search for `typescript` in the VS Code extension marketplace and install [Pretty TypeScript Errors](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors).
|
||||
|
||||
### 9. Dart environment
|
||||
### 9. Dart Environment
|
||||
|
||||
1. Download and install [Dart](https://dart.dev/get-dart).
|
||||
2. In the VS Code extension marketplace, search for `dart` and install [Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code).
|
||||
2. Search for `dart` in the VS Code extension marketplace and install [Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code).
|
||||
|
||||
### 10. Rust environment
|
||||
### 10. Rust Environment
|
||||
|
||||
1. Download and install [Rust](https://www.rust-lang.org/tools/install).
|
||||
2. In the VS Code extension marketplace, search for `rust` and install [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
||||
2. Search for `rust` in the VS Code extension marketplace and install [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer).
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 16.3 Glossary
|
||||
# 16.3 Terminology Table
|
||||
|
||||
Table 16-1 lists the important terms that appear in the book, and it is worth noting the following points.
|
||||
The following table lists important terms that appear in this book. It is worth noting the following points:
|
||||
|
||||
- It is recommended to remember the English names of the terms to facilitate reading English literature.
|
||||
- Some terms have different names in Simplified and Traditional Chinese.
|
||||
- We recommend remembering the English names of terms to help with reading English literature.
|
||||
- Some terms have different names in Simplified Chinese and Traditional Chinese.
|
||||
|
||||
<p align="center"> Table 16-1 Important Terms in Data Structures and Algorithms </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| English | 简体中文 | 繁体中文 |
|
||||
| ------------------------------ | -------------- | -------------- |
|
||||
| English | Simplified Chinese | Traditional Chinese |
|
||||
| ------------------------------ | ------------------ | ------------------- |
|
||||
| algorithm | 算法 | 演算法 |
|
||||
| data structure | 数据结构 | 資料結構 |
|
||||
| code | 代码 | 程式碼 |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,20 +3,20 @@ comments: true
|
||||
icon: material/view-list-outline
|
||||
---
|
||||
|
||||
# Chapter 4. Arrays and linked lists
|
||||
# Chapter 4. Array and Linked List
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
The world of data structures resembles a sturdy brick wall.
|
||||
The world of data structures is like a solid brick wall.
|
||||
|
||||
In arrays, envision bricks snugly aligned, each resting seamlessly beside the next, creating a unified formation. Meanwhile, in linked lists, these bricks disperse freely, embraced by vines gracefully knitting connections between them.
|
||||
Array bricks are neatly arranged, tightly packed one by one. Linked list bricks are scattered everywhere, with connecting vines freely weaving through the gaps between bricks.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [4.1 Array](array.md)
|
||||
- [4.2 Linked list](linked_list.md)
|
||||
- [4.2 Linked List](linked_list.md)
|
||||
- [4.3 List](list.md)
|
||||
- [4.4 Memory and cache *](ram_and_cache.md)
|
||||
- [4.4 Memory and Cache *](ram_and_cache.md)
|
||||
- [4.5 Summary](summary.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,82 +2,82 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 4.4 Memory and cache *
|
||||
# 4.4 Random-Access Memory and Cache *
|
||||
|
||||
In the first two sections of this chapter, we explored arrays and linked lists, two fundamental data structures that represent "continuous storage" and "dispersed storage," respectively.
|
||||
In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent "contiguous storage" and "distributed storage" as two physical structures, respectively.
|
||||
|
||||
In fact, **the physical structure largely determines how efficiently a program utilizes memory and cache**, which in turn affects the overall performance of the algorithm.
|
||||
In fact, **physical structure largely determines the efficiency with which programs utilize memory and cache**, which in turn affects the overall performance of algorithmic programs.
|
||||
|
||||
## 4.4.1 Computer storage devices
|
||||
## 4.4.1 Computer Storage Devices
|
||||
|
||||
There are three types of storage devices in computers: <u>hard disk</u>, <u>random-access memory (RAM)</u>, and <u>cache memory</u>. The following table shows their respective roles and performance characteristics in computer systems.
|
||||
Computers include three types of storage devices: <u>hard disk</u>, <u>random-access memory (RAM)</u>, and <u>cache memory</u>. The following table shows their different roles and performance characteristics in a computer system.
|
||||
|
||||
<p align="center"> Table 4-2 Computer storage devices </p>
|
||||
<p align="center"> Table 4-2 Computer Storage Devices </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | Hard Disk | Memory | Cache |
|
||||
| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| Usage | Long-term storage of data, including OS, programs, files, etc. | Temporary storage of currently running programs and data being processed | Stores frequently accessed data and instructions, reducing the number of CPU accesses to memory |
|
||||
| Volatility | Data is not lost after power off | Data is lost after power off | Data is lost after power off |
|
||||
| Capacity | Larger, TB level | Smaller, GB level | Very small, MB level |
|
||||
| Speed | Slower, several hundred to thousands MB/s | Faster, several tens of GB/s | Very fast, several tens to hundreds of GB/s |
|
||||
| Price (USD) | Cheaper, a few cents / GB | More expensive, a few dollars / GB | Very expensive, priced with CPU |
|
||||
| | Hard Disk | RAM | Cache |
|
||||
| -------------- | ------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------- |
|
||||
| Purpose | Long-term storage of data, including operating systems, programs, and files | Temporary storage of currently running programs and data being processed | Storage of frequently accessed data and instructions to reduce CPU's accesses to memory |
|
||||
| Volatility | Data is not lost after power-off | Data is lost after power-off | Data is lost after power-off |
|
||||
| Capacity | Large, on the order of terabytes (TB) | Small, on the order of gigabytes (GB) | Very small, on the order of megabytes (MB) |
|
||||
| Speed | Slow, hundreds to thousands of MB/s | Fast, tens of GB/s | Very fast, tens to hundreds of GB/s |
|
||||
| Cost (USD/GB) | Inexpensive, fractions of a dollar to a few dollars per GB | Expensive, tens to hundreds of dollars per GB | Very expensive, priced as part of the CPU package |
|
||||
|
||||
</div>
|
||||
|
||||
The computer storage system can be visualized as a pyramid, as shown in Figure 4-9. The storage devices at the top of the pyramid are faster, have smaller capacities, and are more expensive. This multi-level design is not accidental, but a deliberate outcome of careful consideration by computer scientists and engineers.
|
||||
We can imagine the computer storage system as a pyramid structure as shown in the diagram below. Storage devices closer to the top of the pyramid are faster, have smaller capacity, and are more expensive. This multi-layered design is not by accident, but rather the result of careful consideration by computer scientists and engineers.
|
||||
|
||||
- **Replacing hard disks with memory is challenging**. Firstly, data in memory is lost after power off, making it unsuitable for long-term data storage; secondly, memory is significantly more expensive than hard disks, limiting its feasibility for widespread use in the consumer market.
|
||||
- **Caches face a trade-off between large capacity and high speed**. As the capacity of L1, L2, and L3 caches increases, their physical size grows, increasing the distance from the CPU core. This results in longer data transfer times and higher access latency. With current technology, a multi-level cache structure provides the optimal balance between capacity, speed, and cost.
|
||||
- **Hard disk cannot be easily replaced by RAM**. First, data in memory is lost after power-off, making it unsuitable for long-term data storage. Second, memory is tens of times more expensive than hard disk, which makes it difficult to popularize in the consumer market.
|
||||
- **Cache cannot simultaneously achieve large capacity and high speed**. As the capacity of L1, L2, and L3 caches increases, their physical size becomes larger, and the physical distance between them and the CPU core increases, resulting in longer data transmission time and higher element access latency. With current technology, the multi-layered cache structure represents the best balance point between capacity, speed, and cost.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-9 Computer storage system </p>
|
||||
<p align="center"> Figure 4-9 Computer Storage System </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
The storage hierarchy in computers reflects a careful balance between speed, capacity, and cost. This type of trade-off is common across various industries, where finding the optimal balance between benefits and limitations is essential.
|
||||
The storage hierarchy of computers embodies a delicate balance among speed, capacity, and cost. In fact, such trade-offs are common across all industrial fields, requiring us to find the optimal balance point between different advantages and constraints.
|
||||
|
||||
Overall, **hard disks provide long-term storage for large volumes of data, memory serves as temporary storage for data being processed during program execution, and cache stores frequently accessed data and instructions to enhance execution efficiency**. Together, they ensure the efficient operation of computer systems.
|
||||
In summary, **hard disk is used for long-term storage of large amounts of data, RAM is used for temporary storage of data being processed during program execution, and cache is used for storage of frequently accessed data and instructions**, to improve program execution efficiency. The three work together to ensure efficient operation of the computer system.
|
||||
|
||||
As shown in Figure 4-10, during program execution, data is read from the hard disk into memory for CPU computation. The cache, acting as an extension of the CPU, **intelligently preloads data from memory**, enabling faster data access for the CPU. This greatly improves program execution efficiency while reducing reliance on slower memory.
|
||||
As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU, **it intelligently loads data from RAM**, providing the CPU with high-speed data reading, thereby significantly improving program execution efficiency and reducing reliance on slower RAM.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 4-10 Data flow between hard disk, memory, and cache </p>
|
||||
<p align="center"> Figure 4-10 Data Flow Among Hard Disk, RAM, and Cache </p>
|
||||
|
||||
## 4.4.2 Memory efficiency of data structures
|
||||
## 4.4.2 Memory Efficiency of Data Structures
|
||||
|
||||
In terms of memory space utilization, arrays and linked lists have their advantages and limitations.
|
||||
In terms of memory space utilization, arrays and linked lists each have advantages and limitations.
|
||||
|
||||
On one hand, **memory is limited and cannot be shared by multiple programs**, so optimizing space usage in data structures is crucial. Arrays are space-efficient because their elements are tightly packed, without requiring extra memory for references (pointers) as in linked lists. However, arrays require pre-allocating a contiguous block of memory, which can lead to waste if the allocated space exceeds the actual need. Expanding an array also incurs additional time and space overhead. In contrast, linked lists allocate and free memory dynamically for each node, offering greater flexibility at the cost of additional memory for pointers.
|
||||
On one hand, **memory is limited, and the same memory cannot be shared by multiple programs**, so we hope data structures can utilize space as efficiently as possible. Array elements are tightly packed and do not require additional space to store references (pointers) between linked list nodes, thus having higher space efficiency. However, arrays need to allocate sufficient contiguous memory space at once, which may lead to memory waste, and array expansion requires additional time and space costs. In comparison, linked lists perform dynamic memory allocation and deallocation on a "node" basis, providing greater flexibility.
|
||||
|
||||
On the other hand, during program execution, **repeated memory allocation and deallocation increase memory fragmentation**, reducing memory utilization efficiency. Arrays, due to their continuous storage method, are relatively less likely to cause memory fragmentation. In contrast, linked lists store elements in non-contiguous locations, and frequent insertions and deletions can exacerbate memory fragmentation.
|
||||
On the other hand, during program execution, **as memory is repeatedly allocated and freed, the degree of fragmentation of free memory becomes increasingly severe**, leading to reduced memory utilization efficiency. Arrays, due to their contiguous storage approach, are relatively less prone to memory fragmentation. Conversely, linked list elements are distributed in storage, and frequent insertion and deletion operations are more likely to cause memory fragmentation.
|
||||
|
||||
## 4.4.3 Cache efficiency of data structures
|
||||
## 4.4.3 Cache Efficiency of Data Structures
|
||||
|
||||
Although caches are much smaller in space capacity than memory, they are much faster and play a crucial role in program execution speed. Due to their limited capacity, caches can only store a subset of frequently accessed data. When the CPU attempts to access data not present in the cache, a <u>cache miss</u> occurs, requiring the CPU to retrieve the needed data from slower memory, which can impact performance.
|
||||
Although cache has much smaller space capacity than memory, it is much faster than memory and plays a crucial role in program execution speed. Since cache capacity is limited and can only store a small portion of frequently accessed data, when the CPU attempts to access data that is not in the cache, a <u>cache miss</u> occurs, and the CPU must load the required data from the slower memory.
|
||||
|
||||
Clearly, **the fewer the cache misses, the higher the CPU's data read-write efficiency**, and the better the program performance. The proportion of successful data retrieval from the cache by the CPU is called the <u>cache hit rate</u>, a metric often used to measure cache efficiency.
|
||||
Clearly, **the fewer "cache misses," the higher the efficiency of CPU data reads and writes**, and the better the program performance. We call the proportion of data that the CPU successfully obtains from the cache the <u>cache hit rate</u>, a metric typically used to measure cache efficiency.
|
||||
|
||||
To achieve higher efficiency, caches adopt the following data loading mechanisms.
|
||||
To achieve the highest efficiency possible, cache employs the following data loading mechanisms.
|
||||
|
||||
- **Cache lines**: Caches operate by storing and loading data in units called cache lines, rather than individual bytes. This approach improves efficiency by transferring larger blocks of data at once.
|
||||
- **Prefetch mechanism**: Processors predict data access patterns (e.g., sequential or fixed-stride access) and preload data into the cache based on these patterns to increase the cache hit rate.
|
||||
- **Spatial locality**: When a specific piece of data is accessed, nearby data is likely to be accessed soon. To leverage this, caches load adjacent data along with the requested data, improving hit rates.
|
||||
- **Temporal locality**: If data is accessed, it's likely to be accessed again in the near future. Caches use this principle to retain recently accessed data to improve the hit rate.
|
||||
- **Cache lines**: The cache does not store and load data on a byte-by-byte basis, but rather as cache lines. Compared to byte-by-byte transmission, cache line transmission is more efficient.
|
||||
- **Prefetching mechanism**: The processor attempts to predict data access patterns (e.g., sequential access, fixed-stride jumping access, etc.) and loads data into the cache according to specific patterns, thereby improving hit rate.
|
||||
- **Spatial locality**: If a piece of data is accessed, nearby data may also be accessed in the near future. Therefore, when the cache loads a particular piece of data, it also loads nearby data to improve hit rate.
|
||||
- **Temporal locality**: If a piece of data is accessed, it is likely to be accessed again in the near future. Cache leverages this principle by retaining recently accessed data to improve hit rate.
|
||||
|
||||
In fact, **arrays and linked lists have different cache utilization efficiencies**, which is mainly reflected in the following aspects.
|
||||
In fact, **arrays and linked lists have different efficiencies in utilizing cache**, manifested in the following aspects.
|
||||
|
||||
- **Occupied space**: Linked list elements take up more space than array elements, resulting in less effective data being held in the cache.
|
||||
- **Cache lines**: Linked list data is scattered throughout the memory, and cache is "loaded by row", so the proportion of invalid data loaded is higher.
|
||||
- **Prefetch mechanism**: The data access pattern of arrays is more "predictable" than that of linked lists, that is, it is easier for the system to guess the data that is about to be loaded.
|
||||
- **Spatial locality**: Arrays are stored in a continuous memory space, so data near the data being loaded is more likely to be accessed soon.
|
||||
- **Space occupied**: Linked list elements occupy more space than array elements, resulting in fewer effective data in the cache.
|
||||
- **Cache lines**: Linked list data are scattered throughout memory, while cache loads "by lines," so the proportion of invalid data loaded is higher.
|
||||
- **Prefetching mechanism**: Arrays have more "predictable" data access patterns than linked lists, making it easier for the system to guess which data will be loaded next.
|
||||
- **Spatial locality**: Arrays are stored in centralized memory space, so data near loaded data is more likely to be accessed soon.
|
||||
|
||||
Overall, **arrays have a higher cache hit rate and are generally more efficient in operation than linked lists**. This makes data structures based on arrays more popular in solving algorithmic problems.
|
||||
Overall, **arrays have higher cache hit rates, thus they usually outperform linked lists in operation efficiency**. This makes data structures implemented based on arrays more popular when solving algorithmic problems.
|
||||
|
||||
It should be noted that **high cache efficiency does not mean that arrays are always better than linked lists**. The choice of data structure should depend on specific application requirements. For example, both arrays and linked lists can implement the "stack" data structure (which will be detailed in the next chapter), but they are suitable for different scenarios.
|
||||
It is important to note that **high cache efficiency does not mean arrays are superior to linked lists in all cases**. In practical applications, which data structure to choose should be determined based on specific requirements. For example, both arrays and linked lists can implement the "stack" data structure (which will be discussed in detail in the next chapter), but they are suitable for different scenarios.
|
||||
|
||||
- In algorithm problems, we tend to choose stacks based on arrays because they provide higher operational efficiency and random access capabilities, with the only cost being the need to pre-allocate a certain amount of memory space for the array.
|
||||
- If the data volume is very large, highly dynamic, and the expected size of the stack is difficult to estimate, then a stack based on a linked list is a better choice. Linked lists can distribute a large amount of data in different parts of the memory and avoid the additional overhead of array expansion.
|
||||
- When solving algorithm problems, we tend to prefer stack implementations based on arrays, because they provide higher operation efficiency and the ability of random access, at the cost of needing to pre-allocate a certain amount of memory space for the array.
|
||||
- If the data volume is very large, the dynamic nature is high, and the expected size of the stack is difficult to estimate, then a stack implementation based on linked lists is more suitable. Linked lists can distribute large amounts of data across different parts of memory and avoid the additional overhead produced by array expansion.
|
||||
|
||||
@@ -4,82 +4,87 @@ comments: true
|
||||
|
||||
# 4.5 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
- Arrays and linked lists are two basic data structures, representing two storage methods in computer memory: contiguous space storage and non-contiguous space storage. Their characteristics complement each other.
|
||||
- Arrays support random access and use less memory; however, they are inefficient in inserting and deleting elements and have a fixed length after initialization.
|
||||
- Linked lists implement efficient node insertion and deletion through changing references (pointers) and can flexibly adjust their length; however, they have lower node access efficiency and consume more memory.
|
||||
- Common types of linked lists include singly linked lists, circular linked lists, and doubly linked lists, each with its own application scenarios.
|
||||
- Lists are ordered collections of elements that support addition, deletion, and modification, typically implemented based on dynamic arrays, retaining the advantages of arrays while allowing flexible length adjustment.
|
||||
- The advent of lists significantly enhanced the practicality of arrays but may lead to some memory space wastage.
|
||||
- During program execution, data is mainly stored in memory. Arrays provide higher memory space efficiency, while linked lists are more flexible in memory usage.
|
||||
- Caches provide fast data access to CPUs through mechanisms like cache lines, prefetching, spatial locality, and temporal locality, significantly enhancing program execution efficiency.
|
||||
- Due to higher cache hit rates, arrays are generally more efficient than linked lists. When choosing a data structure, the appropriate choice should be made based on specific needs and scenarios.
|
||||
- Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous memory storage and scattered memory storage. The characteristics of the two complement each other.
|
||||
- Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization.
|
||||
- Linked lists achieve efficient insertion and deletion of nodes by modifying references (pointers), and can flexibly adjust length; however, node access is inefficient and memory consumption is higher. Common linked list types include singly linked lists, circular linked lists, and doubly linked lists.
|
||||
- A list is an ordered collection of elements that supports insertion, deletion, search, and modification, typically implemented based on dynamic arrays. It retains the advantages of arrays while allowing flexible adjustment of length.
|
||||
- The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space.
|
||||
- During program execution, data is primarily stored in memory. Arrays provide higher memory space efficiency, while linked lists offer greater flexibility in memory usage.
|
||||
- Caches provide fast data access to the CPU through mechanisms such as cache lines, prefetching, and spatial and temporal locality, significantly improving program execution efficiency.
|
||||
- Because arrays have higher cache hit rates, they are generally more efficient than linked lists. When choosing a data structure, appropriate selection should be made based on specific requirements and scenarios.
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**: Does storing arrays on the stack versus the heap affect time and space efficiency?
|
||||
**Q**: Does storing an array on the stack versus on the heap affect time efficiency and space efficiency?
|
||||
|
||||
Arrays stored on both the stack and heap are stored in contiguous memory spaces, and data operation efficiency is essentially the same. However, stacks and heaps have their own characteristics, leading to the following differences.
|
||||
Arrays stored on the stack and on the heap are both stored in contiguous memory space, so data operation efficiency is basically the same. However, the stack and heap have their own characteristics, leading to the following differences.
|
||||
|
||||
1. Allocation and release efficiency: The stack is a smaller memory block, allocated automatically by the compiler; the heap memory is relatively larger and can be dynamically allocated in the code, more prone to fragmentation. Therefore, allocation and release operations on the heap are generally slower than on the stack.
|
||||
2. Size limitation: Stack memory is relatively small, while the heap size is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays.
|
||||
3. Flexibility: The size of arrays on the stack needs to be determined at compile-time, while the size of arrays on the heap can be dynamically determined at runtime.
|
||||
1. Allocation and deallocation efficiency: The stack is a relatively small piece of memory, with allocation automatically handled by the compiler; the heap is relatively larger and can be dynamically allocated in code, more prone to fragmentation. Therefore, allocation and deallocation operations on the heap are usually slower than on the stack.
|
||||
2. Size limitations: Stack memory is relatively small, and the heap size is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays.
|
||||
3. Flexibility: The size of an array on the stack must be determined at compile time, while the size of an array on the heap can be determined dynamically at runtime.
|
||||
|
||||
**Q**: Why do arrays require elements of the same type, while linked lists do not emphasize same-type elements?
|
||||
**Q**: Why do arrays require elements of the same type, while linked lists do not emphasize this requirement?
|
||||
|
||||
Linked lists consist of nodes connected by references (pointers), and each node can store data of different types, such as int, double, string, object, etc.
|
||||
Linked lists are composed of nodes, with nodes connected through references (pointers), and each node can store different types of data, such as `int`, `double`, `string`, `object`, etc.
|
||||
|
||||
In contrast, array elements must be of the same type, allowing the calculation of offsets to access the corresponding element positions. For example, an array containing both int and long types, with single elements occupying 4 bytes and 8 bytes respectively, cannot use the following formula to calculate offsets, as the array contains elements of two different lengths.
|
||||
In contrast, array elements must be of the same type, so that the corresponding element position can be obtained by calculating the offset. For example, if an array contains both `int` and `long` types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different "element lengths".
|
||||
|
||||
```shell
|
||||
# Element memory address = array memory address + element length * element index
|
||||
# Element Memory Address = Array Memory Address (first Element Memory address) + Element Length * Element Index
|
||||
```
|
||||
|
||||
**Q**: After deleting a node, is it necessary to set `P.next` to `None`?
|
||||
**Q**: After deleting node `P`, do we need to set `P.next` to `None`?
|
||||
|
||||
Not modifying `P.next` is also acceptable. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter `P`. This means that node `P` has been effectively removed from the list, and where `P` points no longer affects the list.
|
||||
It is not necessary to modify `P.next`. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter `P`. This means that node `P` has been removed from the linked list, and it doesn't matter where node `P` points to at this time—it won't affect the linked list.
|
||||
|
||||
From a garbage collection perspective, for languages with automatic garbage collection mechanisms like Java, Python, and Go, whether node `P` is collected depends on whether there are still references pointing to it, not on the value of `P.next`. In languages like C and C++, we need to manually free the node's memory.
|
||||
From a data structures and algorithms perspective (problem-solving), not disconnecting the pointer doesn't matter as long as the program logic is correct. From the perspective of standard libraries, disconnecting is safer and the logic is clearer. If not disconnected, assuming the deleted node is not properly reclaimed, it may affect the memory reclamation of its successor nodes.
|
||||
|
||||
**Q**: In linked lists, the time complexity for insertion and deletion operations is `O(1)`. But searching for the element before insertion or deletion takes `O(n)` time, so why isn't the time complexity `O(n)`?
|
||||
**Q**: In a linked list, the time complexity of insertion and deletion operations is $O(1)$. However, both insertion and deletion require $O(n)$ time to find the element; why isn't the time complexity $O(n)$?
|
||||
|
||||
If an element is searched first and then deleted, the time complexity is indeed `O(n)`. However, the `O(1)` advantage of linked lists in insertion and deletion can be realized in other applications. For example, in the implementation of double-ended queues using linked lists, we maintain pointers always pointing to the head and tail nodes, making each insertion and deletion operation `O(1)`.
|
||||
If the element is first found and then deleted, the time complexity is indeed $O(n)$. However, the advantage of $O(1)$ insertion and deletion in linked lists can be demonstrated in other applications. For example, a deque is well-suited for linked list implementation, where we maintain pointer variables always pointing to the head and tail nodes, with each insertion and deletion operation being $O(1)$.
|
||||
|
||||
**Q**: In the figure "Linked List Definition and Storage Method", do the light blue storage nodes occupy a single memory address, or do they share half with the node value?
|
||||
**Q**: In the diagram "Linked List Definition and Storage Methods", does the light blue pointer node occupy a single memory address, or does it share equally with the node value?
|
||||
|
||||
The figure is just a qualitative representation; quantitative analysis depends on specific situations.
|
||||
This diagram is a qualitative representation; a quantitative representation requires analysis based on the specific situation.
|
||||
|
||||
- Different types of node values occupy different amounts of space, such as int, long, double, and object instances.
|
||||
- The memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes.
|
||||
- Different types of node values occupy different amounts of space, such as `int`, `long`, `double`, and instance objects, etc.
|
||||
- The amount of memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes.
|
||||
|
||||
**Q**: Is adding elements to the end of a list always `O(1)`?
|
||||
**Q**: Is appending an element at the end of a list always $O(1)$?
|
||||
|
||||
If adding an element exceeds the list length, the list needs to be expanded first. The system will request a new memory block and move all elements of the original list over, in which case the time complexity becomes `O(n)`.
|
||||
If appending an element exceeds the list length, the list must first be expanded before adding. The system allocates a new block of memory and moves all elements from the original list to it, in which case the time complexity becomes $O(n)$.
|
||||
|
||||
**Q**: The statement "The emergence of lists greatly improves the practicality of arrays, but may lead to some memory space wastage" - does this refer to the memory occupied by additional variables like capacity, length, and expansion multiplier?
|
||||
**Q**: "The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space"—does this space waste refer to the memory occupied by additional variables such as capacity, length, and expansion factor?
|
||||
|
||||
The space wastage here mainly refers to two aspects: on the one hand, lists are set with an initial length, which we may not always need; on the other hand, to prevent frequent expansion, expansion usually multiplies by a coefficient, such as $\times 1.5$. This results in many empty slots, which we typically cannot fully fill.
|
||||
This space waste mainly has two aspects: on one hand, lists typically set an initial length, which we may not need to fully utilize; on the other hand, to prevent frequent expansion, expansion generally multiplies by a coefficient, such as $\times 1.5$. As a result, there will be many empty positions that we typically cannot completely fill.
|
||||
|
||||
**Q**: In Python, after initializing `n = [1, 2, 3]`, the addresses of these 3 elements are contiguous, but initializing `m = [2, 1, 3]` shows that each element's `id` is not consecutive but identical to those in `n`. If the addresses of these elements are not contiguous, is `m` still an array?
|
||||
**Q**: In Python, after initializing `n = [1, 2, 3]`, the addresses of these 3 elements are contiguous, but initializing `m = [2, 1, 3]` reveals that each element's id is not continuous; rather, they are the same as those in `n`. Since the addresses of these elements are not contiguous, is `m` still an array?
|
||||
|
||||
If we replace list elements with linked list nodes `n = [n1, n2, n3, n4, n5]`, these 5 node objects are also typically dispersed throughout memory. However, given a list index, we can still access the node's memory address in `O(1)` time, thereby accessing the corresponding node. This is because the array stores references to the nodes, not the nodes themselves.
|
||||
If we replace list elements with linked list nodes `n = [n1, n2, n3, n4, n5]`, usually these 5 node objects are also scattered throughout memory. However, given a list index, we can still obtain the node memory address in $O(1)$ time, thereby accessing the corresponding node. This is because the array stores references to nodes, not the nodes themselves.
|
||||
|
||||
Unlike many languages, in Python, numbers are also wrapped as objects, and lists store references to these numbers, not the numbers themselves. Therefore, we find that the same number in two arrays has the same `id`, and these numbers' memory addresses need not be contiguous.
|
||||
Unlike many languages, numbers in Python are wrapped as objects, and lists store not the numbers themselves, but references to the numbers. Therefore, we find that the same numbers in two arrays have the same id, and the memory addresses of these numbers need not be contiguous.
|
||||
|
||||
**Q**: The `std::list` in C++ STL has already implemented a doubly linked list, but it seems that some algorithm books don't directly use it. Is there any limitation?
|
||||
**Q**: C++ STL has `std::list` which has already implemented a doubly linked list, but it seems that some algorithm books don't use it directly. Is there a limitation?
|
||||
|
||||
On the one hand, we often prefer to use arrays to implement algorithms, only using linked lists when necessary, mainly for two reasons.
|
||||
On one hand, we often prefer to use arrays for implementing algorithms and only use linked lists when necessary, mainly for two reasons.
|
||||
|
||||
- Space overhead: Since each element requires two additional pointers (one for the previous element and one for the next), `std::list` usually occupies more space than `std::vector`.
|
||||
- Cache unfriendly: As the data is not stored continuously, `std::list` has a lower cache utilization rate. Generally, `std::vector` performs better.
|
||||
- Space overhead: Since each element requires two additional pointers (one for the previous element and one for the next element), `std::list` typically consumes more space than `std::vector`.
|
||||
- Cache unfriendliness: Since data is not stored contiguously, `std::list` has lower cache utilization. In general, `std::vector` has better performance.
|
||||
|
||||
On the other hand, linked lists are primarily necessary for binary trees and graphs. Stacks and queues are often implemented using the programming language's `stack` and `queue` classes, rather than linked lists.
|
||||
On the other hand, cases where linked lists are necessary mainly involve binary trees and graphs. Stacks and queues usually use the `stack` and `queue` provided by the programming language, rather than linked lists.
|
||||
|
||||
**Q**: Does initializing a list `res = [0] * self.size()` result in each element of `res` referencing the same address?
|
||||
**Q**: Does the operation `res = [[0]] * n` create a 2D list where each `[0]` is independent?
|
||||
|
||||
No. However, this issue arises with two-dimensional arrays, for example, initializing a two-dimensional list `res = [[0]] * self.size()` would reference the same list `[0]` multiple times.
|
||||
No, they are not independent. In this 2D list, all the `[0]` are actually references to the same object. If we modify one element, we will find that all corresponding elements change accordingly.
|
||||
|
||||
**Q**: In deleting a node, is it necessary to break the reference to its successor node?
|
||||
If we want each `[0]` in the 2D list to be independent, we can use `res = [[0] for _ in range(n)]` to achieve this. The principle of this approach is to initialize $n$ independent `[0]` list objects.
|
||||
|
||||
From the perspective of data structures and algorithms (problem-solving), it's okay not to break the link, as long as the program's logic is correct. From the perspective of standard libraries, breaking the link is safer and more logically clear. If the link is not broken, and the deleted node is not properly recycled, it could affect the recycling of the successor node's memory.
|
||||
**Q**: Does the operation `res = [0] * n` create a list where each integer 0 is independent?
|
||||
|
||||
In this list, all integer 0s are references to the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance.
|
||||
|
||||
Although they point to the same object, we can still independently modify each element in the list. This is because Python integers are "immutable objects". When we modify an element, we are actually switching to a reference of another object, rather than changing the original object itself.
|
||||
|
||||
However, when list elements are "mutable objects" (such as lists, dictionaries, or class instances), modifying an element directly changes the object itself, and all elements referencing that object will have the same change.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,14 +9,14 @@ icon: material/map-marker-path
|
||||
|
||||
!!! abstract
|
||||
|
||||
Like explorers in a maze, we may encounter obstacles on our path forward.
|
||||
We are like explorers in a maze, and may encounter difficulties on the path forward.
|
||||
|
||||
The power of backtracking lets us begin anew, keep trying, and eventually find the exit leading to the light.
|
||||
The power of backtracking allows us to start over, keep trying, and eventually find the exit leading to light.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [13.1 Backtracking algorithms](backtracking_algorithm.md)
|
||||
- [13.2 Permutation problem](permutations_problem.md)
|
||||
- [13.3 Subset sum problem](subset_sum_problem.md)
|
||||
- [13.4 n queens problem](n_queens_problem.md)
|
||||
- [13.1 Backtracking Algorithm](backtracking_algorithm.md)
|
||||
- [13.2 Permutations Problem](permutations_problem.md)
|
||||
- [13.3 Subset-Sum Problem](subset_sum_problem.md)
|
||||
- [13.4 N-Queens Problem](n_queens_problem.md)
|
||||
- [13.5 Summary](summary.md)
|
||||
|
||||
@@ -2,59 +2,59 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 13.4 n queens problem
|
||||
# 13.4 N-Queens Problem
|
||||
|
||||
!!! question
|
||||
|
||||
According to the rules of chess, a queen can attack pieces in the same row, column, or diagonal line. Given $n$ queens and an $n \times n$ chessboard, find arrangements where no two queens can attack each other.
|
||||
According to the rules of chess, a queen can attack pieces that share the same row, column, or diagonal line. Given $n$ queens and an $n \times n$ chessboard, find a placement scheme such that no two queens can attack each other.
|
||||
|
||||
As shown in Figure 13-15, there are two solutions when $n = 4$. From the perspective of the backtracking algorithm, an $n \times n$ chessboard has $n^2$ squares, presenting all possible choices `choices`. The state of the chessboard `state` changes continuously as each queen is placed.
|
||||
As shown in Figure 13-15, when $n = 4$, there are two solutions that can be found. From the perspective of the backtracking algorithm, an $n \times n$ chessboard has $n^2$ squares, which provide all the choices `choices`. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state `state`.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 13-15 Solution to the 4 queens problem </p>
|
||||
<p align="center"> Figure 13-15 Solution to the 4-queens problem </p>
|
||||
|
||||
Figure 13-16 shows the three constraints of this problem: **multiple queens cannot occupy the same row, column, or diagonal**. It is important to note that diagonals are divided into the main diagonal `\` and the secondary diagonal `/`.
|
||||
Figure 13-16 illustrates the three constraints of this problem: **multiple queens cannot be in the same row, the same column, or on the same diagonal**. It is worth noting that diagonals are divided into two types: the main diagonal `\` and the anti-diagonal `/`.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 13-16 Constraints of the n queens problem </p>
|
||||
<p align="center"> Figure 13-16 Constraints of the n-queens problem </p>
|
||||
|
||||
### 1. Row-by-row placing strategy
|
||||
### 1. Row-By-Row Placement Strategy
|
||||
|
||||
As the number of queens equals the number of rows on the chessboard, both being $n$, it is easy to conclude that **each row on the chessboard allows and only allows one queen to be placed**.
|
||||
Since both the number of queens and the number of rows on the chessboard are $n$, we can easily derive a conclusion: **each row of the chessboard allows and only allows exactly one queen to be placed**.
|
||||
|
||||
This means that we can adopt a row-by-row placing strategy: starting from the first row, place one queen per row until the last row is reached.
|
||||
This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed.
|
||||
|
||||
Figure 13-17 shows the row-by-row placing process for the 4 queens problem. Due to space limitations, the figure only expands one search branch of the first row, and prunes any placements that do not meet the column and diagonal constraints.
|
||||
Figure 13-17 shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that do not satisfy the column constraint and diagonal constraints are pruned.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 13-17 Row-by-row placing strategy </p>
|
||||
<p align="center"> Figure 13-17 Row-by-row placement strategy </p>
|
||||
|
||||
Essentially, **the row-by-row placing strategy serves as a pruning function**, eliminating all search branches that would place multiple queens in the same row.
|
||||
Essentially, **the row-by-row placement strategy serves a pruning function**, as it avoids all search branches where multiple queens appear in the same row.
|
||||
|
||||
### 2. Column and diagonal pruning
|
||||
### 2. Column and Diagonal Pruning
|
||||
|
||||
To satisfy column constraints, we can use a boolean array `cols` of length $n$ to track whether a queen occupies each column. Before each placement decision, `cols` is used to prune the columns that already have queens, and it is dynamically updated during backtracking.
|
||||
To satisfy the column constraint, we can use a boolean array `cols` of length $n$ to record whether each column has a queen. Before each placement decision, we use `cols` to prune columns that already have queens, and dynamically update the state of `cols` during backtracking.
|
||||
|
||||
!!! tip
|
||||
|
||||
Note that the origin of the matrix is located in the upper left corner, where the row index increases from top to bottom, and the column index increases from left to right.
|
||||
Please note that the origin of the matrix is located in the upper-left corner, where the row index increases from top to bottom, and the column index increases from left to right.
|
||||
|
||||
How about the diagonal constraints? Let the row and column indices of a certain cell on the chessboard be $(row, col)$. By selecting a specific main diagonal, we notice that the difference $row - col$ is the same for all cells on that diagonal, **meaning that $row - col$ is a constant value on the main diagonal**.
|
||||
So how do we handle diagonal constraints? Consider a square on the chessboard with row and column indices $(row, col)$. If we select a specific main diagonal in the matrix, we find that all squares on that diagonal have the same difference between their row and column indices, **meaning that $row - col$ is a constant value for all squares on the main diagonal**.
|
||||
|
||||
In other words, if two cells satisfy $row_1 - col_1 = row_2 - col_2$, they are definitely on the same main diagonal. Using this pattern, we can utilize the array `diags1` shown in Figure 13-18 to track whether a queen is on any main diagonal.
|
||||
In other words, if two squares satisfy $row_1 - col_1 = row_2 - col_2$, they must be on the same main diagonal. Using this pattern, we can use the array `diags1` shown in Figure 13-18 to record whether there is a queen on each main diagonal.
|
||||
|
||||
Similarly, **the sum of $row + col$ is a constant value for all cells on the secondary diagonal**. We can also use the array `diags2` to handle secondary diagonal constraints.
|
||||
Similarly, **for all squares on an anti-diagonal, the sum $row + col$ is a constant value**. We can likewise use the array `diags2` to handle anti-diagonal constraints.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 13-18 Handling column and diagonal constraints </p>
|
||||
|
||||
### 3. Code implementation
|
||||
### 3. Code Implementation
|
||||
|
||||
Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Consequently, the number of both main and secondary diagonals is $2n - 1$, meaning the length of the arrays `diags1` and `diags2` is $2n - 1$.
|
||||
Please note that in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Therefore, the number of both main diagonals and anti-diagonals is $2n - 1$, meaning the length of both arrays `diags1` and `diags2` is $2n - 1$.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -68,34 +68,34 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[
|
||||
diags1: list[bool],
|
||||
diags2: list[bool],
|
||||
):
|
||||
"""Backtracking algorithm: n queens"""
|
||||
"""Backtracking algorithm: N queens"""
|
||||
# When all rows are placed, record the solution
|
||||
if row == n:
|
||||
res.append([list(row) for row in state])
|
||||
return
|
||||
# Traverse all columns
|
||||
for col in range(n):
|
||||
# Calculate the main and minor diagonals corresponding to the cell
|
||||
# Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
diag1 = row - col + n - 1
|
||||
diag2 = row + col
|
||||
# Pruning: do not allow queens on the column, main diagonal, or minor diagonal of the cell
|
||||
# Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if not cols[col] and not diags1[diag1] and not diags2[diag2]:
|
||||
# Attempt: place the queen in the cell
|
||||
# Attempt: place the queen in this cell
|
||||
state[row][col] = "Q"
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = True
|
||||
# Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2)
|
||||
# Retract: restore the cell to an empty spot
|
||||
# Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = "#"
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = False
|
||||
|
||||
def n_queens(n: int) -> list[list[list[str]]]:
|
||||
"""Solve n queens"""
|
||||
# Initialize an n*n size chessboard, where 'Q' represents the queen and '#' represents an empty spot
|
||||
"""Solve N queens"""
|
||||
# Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
state = [["#" for _ in range(n)] for _ in range(n)]
|
||||
cols = [False] * n # Record columns with queens
|
||||
diags1 = [False] * (2 * n - 1) # Record main diagonals with queens
|
||||
diags2 = [False] * (2 * n - 1) # Record minor diagonals with queens
|
||||
cols = [False] * n # Record whether there is a queen in the column
|
||||
diags1 = [False] * (2 * n - 1) # Record whether there is a queen on the main diagonal
|
||||
diags2 = [False] * (2 * n - 1) # Record whether there is a queen on the anti-diagonal
|
||||
res = []
|
||||
backtrack(0, n, state, res, cols, diags1, diags2)
|
||||
|
||||
@@ -105,7 +105,7 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[
|
||||
=== "C++"
|
||||
|
||||
```cpp title="n_queens.cpp"
|
||||
/* Backtracking algorithm: n queens */
|
||||
/* Backtracking algorithm: N queens */
|
||||
void backtrack(int row, int n, vector<vector<string>> &state, vector<vector<vector<string>>> &res, vector<bool> &cols,
|
||||
vector<bool> &diags1, vector<bool> &diags2) {
|
||||
// When all rows are placed, record the solution
|
||||
@@ -115,30 +115,30 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[
|
||||
}
|
||||
// Traverse all columns
|
||||
for (int col = 0; col < n; col++) {
|
||||
// Calculate the main and minor diagonals corresponding to the cell
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// Pruning: do not allow queens on the column, main diagonal, or minor diagonal of the cell
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// Attempt: place the queen in the cell
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = "Q";
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// Retract: restore the cell to an empty spot
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = "#";
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Solve n queens */
|
||||
/* Solve N queens */
|
||||
vector<vector<vector<string>>> nQueens(int n) {
|
||||
// Initialize an n*n size chessboard, where 'Q' represents the queen and '#' represents an empty spot
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
vector<vector<string>> state(n, vector<string>(n, "#"));
|
||||
vector<bool> cols(n, false); // Record columns with queens
|
||||
vector<bool> diags1(2 * n - 1, false); // Record main diagonals with queens
|
||||
vector<bool> diags2(2 * n - 1, false); // Record minor diagonals with queens
|
||||
vector<bool> cols(n, false); // Record whether there is a queen in the column
|
||||
vector<bool> diags1(2 * n - 1, false); // Record whether there is a queen on the main diagonal
|
||||
vector<bool> diags2(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal
|
||||
vector<vector<vector<string>>> res;
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
@@ -150,7 +150,7 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[
|
||||
=== "Java"
|
||||
|
||||
```java title="n_queens.java"
|
||||
/* Backtracking algorithm: n queens */
|
||||
/* Backtracking algorithm: N queens */
|
||||
void backtrack(int row, int n, List<List<String>> state, List<List<List<String>>> res,
|
||||
boolean[] cols, boolean[] diags1, boolean[] diags2) {
|
||||
// When all rows are placed, record the solution
|
||||
@@ -164,26 +164,26 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[
|
||||
}
|
||||
// Traverse all columns
|
||||
for (int col = 0; col < n; col++) {
|
||||
// Calculate the main and minor diagonals corresponding to the cell
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// Pruning: do not allow queens on the column, main diagonal, or minor diagonal of the cell
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// Attempt: place the queen in the cell
|
||||
// Attempt: place the queen in this cell
|
||||
state.get(row).set(col, "Q");
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// Retract: restore the cell to an empty spot
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state.get(row).set(col, "#");
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Solve n queens */
|
||||
/* Solve N queens */
|
||||
List<List<List<String>>> nQueens(int n) {
|
||||
// Initialize an n*n size chessboard, where 'Q' represents the queen and '#' represents an empty spot
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
List<List<String>> state = new ArrayList<>();
|
||||
for (int i = 0; i < n; i++) {
|
||||
List<String> row = new ArrayList<>();
|
||||
@@ -192,9 +192,9 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[
|
||||
}
|
||||
state.add(row);
|
||||
}
|
||||
boolean[] cols = new boolean[n]; // Record columns with queens
|
||||
boolean[] diags1 = new boolean[2 * n - 1]; // Record main diagonals with queens
|
||||
boolean[] diags2 = new boolean[2 * n - 1]; // Record minor diagonals with queens
|
||||
boolean[] cols = new boolean[n]; // Record whether there is a queen in the column
|
||||
boolean[] diags1 = new boolean[2 * n - 1]; // Record whether there is a queen on the main diagonal
|
||||
boolean[] diags2 = new boolean[2 * n - 1]; // Record whether there is a queen on the anti-diagonal
|
||||
List<List<List<String>>> res = new ArrayList<>();
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
@@ -206,91 +206,544 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[
|
||||
=== "C#"
|
||||
|
||||
```csharp title="n_queens.cs"
|
||||
[class]{n_queens}-[func]{Backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
void Backtrack(int row, int n, List<List<string>> state, List<List<List<string>>> res,
|
||||
bool[] cols, bool[] diags1, bool[] diags2) {
|
||||
// When all rows are placed, record the solution
|
||||
if (row == n) {
|
||||
List<List<string>> copyState = [];
|
||||
foreach (List<string> sRow in state) {
|
||||
copyState.Add(new List<string>(sRow));
|
||||
}
|
||||
res.Add(copyState);
|
||||
return;
|
||||
}
|
||||
// Traverse all columns
|
||||
for (int col = 0; col < n; col++) {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = "Q";
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// Place the next row
|
||||
Backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = "#";
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class]{n_queens}-[func]{NQueens}
|
||||
/* Solve N queens */
|
||||
List<List<List<string>>> NQueens(int n) {
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
List<List<string>> state = [];
|
||||
for (int i = 0; i < n; i++) {
|
||||
List<string> row = [];
|
||||
for (int j = 0; j < n; j++) {
|
||||
row.Add("#");
|
||||
}
|
||||
state.Add(row);
|
||||
}
|
||||
bool[] cols = new bool[n]; // Record whether there is a queen in the column
|
||||
bool[] diags1 = new bool[2 * n - 1]; // Record whether there is a queen on the main diagonal
|
||||
bool[] diags2 = new bool[2 * n - 1]; // Record whether there is a queen on the anti-diagonal
|
||||
List<List<List<string>>> res = [];
|
||||
|
||||
Backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="n_queens.go"
|
||||
[class]{}-[func]{backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
func backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) {
|
||||
// When all rows are placed, record the solution
|
||||
if row == n {
|
||||
newState := make([][]string, len(*state))
|
||||
for i, _ := range newState {
|
||||
newState[i] = make([]string, len((*state)[0]))
|
||||
copy(newState[i], (*state)[i])
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
}
|
||||
*res = append(*res, newState)
|
||||
return
|
||||
}
|
||||
// Traverse all columns
|
||||
for col := 0; col < n; col++ {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
diag1 := row - col + n - 1
|
||||
diag2 := row + col
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] {
|
||||
// Attempt: place the queen in this cell
|
||||
(*state)[row][col] = "Q"
|
||||
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true
|
||||
// Place the next row
|
||||
backtrack(row+1, n, state, res, cols, diags1, diags2)
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
(*state)[row][col] = "#"
|
||||
(*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Solve N queens */
|
||||
func nQueens(n int) [][][]string {
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
state := make([][]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
row := make([]string, n)
|
||||
for i := 0; i < n; i++ {
|
||||
row[i] = "#"
|
||||
}
|
||||
state[i] = row
|
||||
}
|
||||
// Record whether there is a queen in the column
|
||||
cols := make([]bool, n)
|
||||
diags1 := make([]bool, 2*n-1)
|
||||
diags2 := make([]bool, 2*n-1)
|
||||
res := make([][][]string, 0)
|
||||
backtrack(0, n, &state, &res, &cols, &diags1, &diags2)
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="n_queens.swift"
|
||||
[class]{}-[func]{backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
func backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) {
|
||||
// When all rows are placed, record the solution
|
||||
if row == n {
|
||||
res.append(state)
|
||||
return
|
||||
}
|
||||
// Traverse all columns
|
||||
for col in 0 ..< n {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
let diag1 = row - col + n - 1
|
||||
let diag2 = row + col
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = "Q"
|
||||
cols[col] = true
|
||||
diags1[diag1] = true
|
||||
diags2[diag2] = true
|
||||
// Place the next row
|
||||
backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = "#"
|
||||
cols[col] = false
|
||||
diags1[diag1] = false
|
||||
diags2[diag2] = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
/* Solve N queens */
|
||||
func nQueens(n: Int) -> [[[String]]] {
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
var state = Array(repeating: Array(repeating: "#", count: n), count: n)
|
||||
var cols = Array(repeating: false, count: n) // Record whether there is a queen in the column
|
||||
var diags1 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the main diagonal
|
||||
var diags2 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the anti-diagonal
|
||||
var res: [[[String]]] = []
|
||||
|
||||
backtrack(row: 0, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2)
|
||||
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="n_queens.js"
|
||||
[class]{}-[func]{backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
function backtrack(row, n, state, res, cols, diags1, diags2) {
|
||||
// When all rows are placed, record the solution
|
||||
if (row === n) {
|
||||
res.push(state.map((row) => row.slice()));
|
||||
return;
|
||||
}
|
||||
// Traverse all columns
|
||||
for (let col = 0; col < n; col++) {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
const diag1 = row - col + n - 1;
|
||||
const diag2 = row + col;
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = 'Q';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = '#';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
/* Solve N queens */
|
||||
function nQueens(n) {
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
const state = Array.from({ length: n }, () => Array(n).fill('#'));
|
||||
const cols = Array(n).fill(false); // Record whether there is a queen in the column
|
||||
const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal
|
||||
const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal
|
||||
const res = [];
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="n_queens.ts"
|
||||
[class]{}-[func]{backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
function backtrack(
|
||||
row: number,
|
||||
n: number,
|
||||
state: string[][],
|
||||
res: string[][][],
|
||||
cols: boolean[],
|
||||
diags1: boolean[],
|
||||
diags2: boolean[]
|
||||
): void {
|
||||
// When all rows are placed, record the solution
|
||||
if (row === n) {
|
||||
res.push(state.map((row) => row.slice()));
|
||||
return;
|
||||
}
|
||||
// Traverse all columns
|
||||
for (let col = 0; col < n; col++) {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
const diag1 = row - col + n - 1;
|
||||
const diag2 = row + col;
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = 'Q';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = '#';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
/* Solve N queens */
|
||||
function nQueens(n: number): string[][][] {
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
const state = Array.from({ length: n }, () => Array(n).fill('#'));
|
||||
const cols = Array(n).fill(false); // Record whether there is a queen in the column
|
||||
const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal
|
||||
const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal
|
||||
const res: string[][][] = [];
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="n_queens.dart"
|
||||
[class]{}-[func]{backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
void backtrack(
|
||||
int row,
|
||||
int n,
|
||||
List<List<String>> state,
|
||||
List<List<List<String>>> res,
|
||||
List<bool> cols,
|
||||
List<bool> diags1,
|
||||
List<bool> diags2,
|
||||
) {
|
||||
// When all rows are placed, record the solution
|
||||
if (row == n) {
|
||||
List<List<String>> copyState = [];
|
||||
for (List<String> sRow in state) {
|
||||
copyState.add(List.from(sRow));
|
||||
}
|
||||
res.add(copyState);
|
||||
return;
|
||||
}
|
||||
// Traverse all columns
|
||||
for (int col = 0; col < n; col++) {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = "Q";
|
||||
cols[col] = true;
|
||||
diags1[diag1] = true;
|
||||
diags2[diag2] = true;
|
||||
// Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = "#";
|
||||
cols[col] = false;
|
||||
diags1[diag1] = false;
|
||||
diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
/* Solve N queens */
|
||||
List<List<List<String>>> nQueens(int n) {
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
List<List<String>> state = List.generate(n, (index) => List.filled(n, "#"));
|
||||
List<bool> cols = List.filled(n, false); // Record whether there is a queen in the column
|
||||
List<bool> diags1 = List.filled(2 * n - 1, false); // Record whether there is a queen on the main diagonal
|
||||
List<bool> diags2 = List.filled(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal
|
||||
List<List<List<String>>> res = [];
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2);
|
||||
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="n_queens.rs"
|
||||
[class]{}-[func]{backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
fn backtrack(
|
||||
row: usize,
|
||||
n: usize,
|
||||
state: &mut Vec<Vec<String>>,
|
||||
res: &mut Vec<Vec<Vec<String>>>,
|
||||
cols: &mut [bool],
|
||||
diags1: &mut [bool],
|
||||
diags2: &mut [bool],
|
||||
) {
|
||||
// When all rows are placed, record the solution
|
||||
if row == n {
|
||||
res.push(state.clone());
|
||||
return;
|
||||
}
|
||||
// Traverse all columns
|
||||
for col in 0..n {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
let diag1 = row + n - 1 - col;
|
||||
let diag2 = row + col;
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if !cols[col] && !diags1[diag1] && !diags2[diag2] {
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = "Q".into();
|
||||
(cols[col], diags1[diag1], diags2[diag2]) = (true, true, true);
|
||||
// Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2);
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = "#".into();
|
||||
(cols[col], diags1[diag1], diags2[diag2]) = (false, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{n_queens}
|
||||
/* Solve N queens */
|
||||
fn n_queens(n: usize) -> Vec<Vec<Vec<String>>> {
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
let mut state: Vec<Vec<String>> = vec![vec!["#".to_string(); n]; n];
|
||||
let mut cols = vec![false; n]; // Record whether there is a queen in the column
|
||||
let mut diags1 = vec![false; 2 * n - 1]; // Record whether there is a queen on the main diagonal
|
||||
let mut diags2 = vec![false; 2 * n - 1]; // Record whether there is a queen on the anti-diagonal
|
||||
let mut res: Vec<Vec<Vec<String>>> = Vec::new();
|
||||
|
||||
backtrack(
|
||||
0,
|
||||
n,
|
||||
&mut state,
|
||||
&mut res,
|
||||
&mut cols,
|
||||
&mut diags1,
|
||||
&mut diags2,
|
||||
);
|
||||
|
||||
res
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="n_queens.c"
|
||||
[class]{}-[func]{backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
void backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE],
|
||||
bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) {
|
||||
// When all rows are placed, record the solution
|
||||
if (row == n) {
|
||||
res[*resSize] = (char **)malloc(sizeof(char *) * n);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1));
|
||||
strcpy(res[*resSize][i], state[i]);
|
||||
}
|
||||
(*resSize)++;
|
||||
return;
|
||||
}
|
||||
// Traverse all columns
|
||||
for (int col = 0; col < n; col++) {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
int diag1 = row - col + n - 1;
|
||||
int diag2 = row + col;
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = 'Q';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true;
|
||||
// Place the next row
|
||||
backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2);
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = '#';
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
/* Solve N queens */
|
||||
char ***nQueens(int n, int *returnSize) {
|
||||
char state[MAX_SIZE][MAX_SIZE];
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
for (int i = 0; i < n; ++i) {
|
||||
for (int j = 0; j < n; ++j) {
|
||||
state[i][j] = '#';
|
||||
}
|
||||
state[i][n] = '\0';
|
||||
}
|
||||
bool cols[MAX_SIZE] = {false}; // Record whether there is a queen in the column
|
||||
bool diags1[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the main diagonal
|
||||
bool diags2[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the anti-diagonal
|
||||
|
||||
char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE);
|
||||
*returnSize = 0;
|
||||
backtrack(0, n, state, res, returnSize, cols, diags1, diags2);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="n_queens.kt"
|
||||
[class]{}-[func]{backtrack}
|
||||
/* Backtracking algorithm: N queens */
|
||||
fun backtrack(
|
||||
row: Int,
|
||||
n: Int,
|
||||
state: MutableList<MutableList<String>>,
|
||||
res: MutableList<MutableList<MutableList<String>>?>,
|
||||
cols: BooleanArray,
|
||||
diags1: BooleanArray,
|
||||
diags2: BooleanArray
|
||||
) {
|
||||
// When all rows are placed, record the solution
|
||||
if (row == n) {
|
||||
val copyState = mutableListOf<MutableList<String>>()
|
||||
for (sRow in state) {
|
||||
copyState.add(sRow.toMutableList())
|
||||
}
|
||||
res.add(copyState)
|
||||
return
|
||||
}
|
||||
// Traverse all columns
|
||||
for (col in 0..<n) {
|
||||
// Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
val diag1 = row - col + n - 1
|
||||
val diag2 = row + col
|
||||
// Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if (!cols[col] && !diags1[diag1] && !diags2[diag2]) {
|
||||
// Attempt: place the queen in this cell
|
||||
state[row][col] = "Q"
|
||||
diags2[diag2] = true
|
||||
diags1[diag1] = diags2[diag2]
|
||||
cols[col] = diags1[diag1]
|
||||
// Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2)
|
||||
// Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = "#"
|
||||
diags2[diag2] = false
|
||||
diags1[diag1] = diags2[diag2]
|
||||
cols[col] = diags1[diag1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
/* Solve N queens */
|
||||
fun nQueens(n: Int): MutableList<MutableList<MutableList<String>>?> {
|
||||
// Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
val state = mutableListOf<MutableList<String>>()
|
||||
for (i in 0..<n) {
|
||||
val row = mutableListOf<String>()
|
||||
for (j in 0..<n) {
|
||||
row.add("#")
|
||||
}
|
||||
state.add(row)
|
||||
}
|
||||
val cols = BooleanArray(n) // Record whether there is a queen in the column
|
||||
val diags1 = BooleanArray(2 * n - 1) // Record whether there is a queen on the main diagonal
|
||||
val diags2 = BooleanArray(2 * n - 1) // Record whether there is a queen on the anti-diagonal
|
||||
val res = mutableListOf<MutableList<MutableList<String>>?>()
|
||||
|
||||
backtrack(0, n, state, res, cols, diags1, diags2)
|
||||
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="n_queens.rb"
|
||||
[class]{}-[func]{backtrack}
|
||||
### Backtracking: n queens ###
|
||||
def backtrack(row, n, state, res, cols, diags1, diags2)
|
||||
# When all rows are placed, record the solution
|
||||
if row == n
|
||||
res << state.map { |row| row.dup }
|
||||
return
|
||||
end
|
||||
|
||||
[class]{}-[func]{n_queens}
|
||||
# Traverse all columns
|
||||
for col in 0...n
|
||||
# Calculate the main diagonal and anti-diagonal corresponding to this cell
|
||||
diag1 = row - col + n - 1
|
||||
diag2 = row + col
|
||||
# Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell
|
||||
if !cols[col] && !diags1[diag1] && !diags2[diag2]
|
||||
# Attempt: place the queen in this cell
|
||||
state[row][col] = "Q"
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = true
|
||||
# Place the next row
|
||||
backtrack(row + 1, n, state, res, cols, diags1, diags2)
|
||||
# Backtrack: restore this cell to an empty cell
|
||||
state[row][col] = "#"
|
||||
cols[col] = diags1[diag1] = diags2[diag2] = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
### Solve n queens ###
|
||||
def n_queens(n)
|
||||
# Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell
|
||||
state = Array.new(n) { Array.new(n, "#") }
|
||||
cols = Array.new(n, false) # Record whether there is a queen in the column
|
||||
diags1 = Array.new(2 * n - 1, false) # Record whether there is a queen on the main diagonal
|
||||
diags2 = Array.new(2 * n - 1, false) # Record whether there is a queen on the anti-diagonal
|
||||
res = []
|
||||
backtrack(0, n, state, res, cols, diags1, diags2)
|
||||
|
||||
res
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
Placing $n$ queens row by row, considering the column constraint, from the first row to the last row there are $n$, $n-1$, $\dots$, $2$, $1$ choices, using $O(n!)$ time. When recording a solution, it is necessary to copy the matrix `state` and add it to `res`, and the copy operation uses $O(n^2)$ time. Therefore, **the overall time complexity is $O(n! \cdot n^2)$**. In practice, pruning based on diagonal constraints can also significantly reduce the search space, so the search efficiency is often better than the time complexity mentioned above.
|
||||
|
||||
```zig title="n_queens.zig"
|
||||
[class]{}-[func]{backtrack}
|
||||
|
||||
[class]{}-[func]{nQueens}
|
||||
```
|
||||
|
||||
Placing $n$ queens row-by-row, considering column constraints, from the first row to the last row, there are $n$, $n-1$, $\dots$, $2$, $1$ choices, using $O(n!)$ time. When recording a solution, it is necessary to copy the matrix `state` and add it to `res`, with the copying operation using $O(n^2)$ time. Therefore, **the overall time complexity is $O(n! \cdot n^2)$**. In practice, pruning based on diagonal constraints can significantly reduce the search space, thus often the search efficiency is better than the aforementioned time complexity.
|
||||
|
||||
Array `state` uses $O(n^2)$ space, and arrays `cols`, `diags1`, and `diags2` each use $O(n)$ space as well. The maximum recursion depth is $n$, using $O(n)$ stack frame space. Therefore, **the space complexity is $O(n^2)$**.
|
||||
The array `state` uses $O(n^2)$ space, and the arrays `cols`, `diags1`, and `diags2` each use $O(n)$ space. The maximum recursion depth is $n$, using $O(n)$ stack frame space. Therefore, **the space complexity is $O(n^2)$**.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,24 +4,24 @@ comments: true
|
||||
|
||||
# 13.5 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
- The essence of the backtracking algorithm is exhaustive search. It seeks solutions that meet the conditions by performing a depth-first traversal of the solution space. During the search, if a satisfying solution is found, it is recorded, until all solutions are found or the traversal is completed.
|
||||
- The search process of the backtracking algorithm includes trying and backtracking. It uses depth-first search to explore various choices, and when a choice does not meet the constraints, the previous choice is undone. Then it reverts to the previous state and continues to try other options. Trying and backtracking are operations in opposite directions.
|
||||
- Backtracking problems usually contain multiple constraints. These constraints can be used to perform pruning operations. Pruning can terminate unnecessary search branches in advance, greatly enhancing search efficiency.
|
||||
- The backtracking algorithm is mainly used to solve search problems and constraint satisfaction problems. Although combinatorial optimization problems can be solved using backtracking, there are often more efficient or effective solutions available.
|
||||
- The permutation problem aims to search for all possible permutations of the elements in a given set. We use an array to record whether each element has been chosen, avoiding repeated selection of the same element. This ensures that each element is chosen only once.
|
||||
- In permutation problems, if the set contains duplicate elements, the final result will include duplicate permutations. We need to restrict that identical elements can only be selected once in each round, which is usually implemented using a hash set.
|
||||
- The subset-sum problem aims to find all subsets in a given set that sum to a target value. The set does not distinguish the order of elements, but the search process may generate duplicate subsets. This occurs because the algorithm explores different element orders as unique paths. Before backtracking, we sort the data and set a variable to indicate the starting point of the traversal for each round. This allows us to prune the search branches that generate duplicate subsets.
|
||||
- For the subset-sum problem, equal elements in the array can produce duplicate sets. Using the precondition that the array is already sorted, we prune by determining if adjacent elements are equal. This ensures that equal elements are only selected once per round.
|
||||
- The $n$ queens problem aims to find schemes to place $n$ queens on an $n \times n$ chessboard such that no two queens can attack each other. The constraints of the problem include row constraints, column constraints, and constraints on the main and secondary diagonals. To meet the row constraint, we adopt a strategy of placing one queen per row, ensuring each row has one queen placed.
|
||||
- The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether there is a queen in each column, thereby indicating whether the selected cell is legal. For diagonal constraints, we use two arrays to respectively record the presence of queens on the main and secondary diagonals. The challenge is to determine the relationship between row and column indices for cells on the same main or secondary diagonal.
|
||||
- The backtracking algorithm is fundamentally an exhaustive search method. It finds solutions that meet specified conditions by performing a depth-first traversal of the solution space. During the search process, when a solution satisfying the conditions is found, it is recorded. The search ends either after finding all solutions or when the traversal is complete.
|
||||
- The backtracking algorithm search process consists of two parts: attempting and backtracking. It tries various choices through depth-first search. When encountering situations that violate constraints, it reverts the previous choice, returns to the previous state, and continues exploring other options. Attempting and backtracking are operations in opposite directions.
|
||||
- Backtracking problems typically contain multiple constraints, which can be utilized to implement pruning operations. Pruning can terminate unnecessary search branches early, significantly improving search efficiency.
|
||||
- The backtracking algorithm is primarily used to solve search problems and constraint satisfaction problems. While combinatorial optimization problems can be solved with backtracking, there are often more efficient or better-performing solutions available.
|
||||
- The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once.
|
||||
- In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set.
|
||||
- The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets.
|
||||
- For the subset-sum problem, equal elements in the array produce duplicate sets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round.
|
||||
- The $n$ queens problem aims to find placements of $n$ queens on an $n \times n$ chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row.
|
||||
- The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal.
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**: How can we understand the relationship between backtracking and recursion?
|
||||
**Q**: How should we understand the relationship between backtracking and recursion?
|
||||
|
||||
Overall, backtracking is an "algorithmic strategy," while recursion is more of a "tool."
|
||||
Overall, backtracking is an "algorithm strategy", while recursion is more like a "tool".
|
||||
|
||||
- Backtracking algorithms are typically based on recursion. However, backtracking is one of the application scenarios of recursion, specifically in search problems.
|
||||
- The structure of recursion reflects the problem-solving paradigm of "sub-problem decomposition." It is commonly used in solving problems involving divide and conquer, backtracking, and dynamic programming (memoized recursion).
|
||||
- The backtracking algorithm is typically implemented based on recursion. However, backtracking is one application scenario of recursion and represents the application of recursion in search problems.
|
||||
- The structure of recursion embodies the "subproblem decomposition" problem-solving paradigm, commonly used to solve problems involving divide-and-conquer, backtracking, and dynamic programming (memoized recursion).
|
||||
|
||||
@@ -3,20 +3,20 @@ comments: true
|
||||
icon: material/timer-sand
|
||||
---
|
||||
|
||||
# Chapter 2. Complexity analysis
|
||||
# Chapter 2. Complexity Analysis
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
Complexity analysis is like a space-time navigator in the vast universe of algorithms.
|
||||
Complexity analysis is like a space-time guide in the vast universe of algorithms.
|
||||
|
||||
It guides us in exploring deeper within the dimensions of time and space, seeking more elegant solutions.
|
||||
It leads us to explore deeply within the two dimensions of time and space, seeking more elegant solutions.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [2.1 Algorithm efficiency assessment](performance_evaluation.md)
|
||||
- [2.2 Iteration and recursion](iteration_and_recursion.md)
|
||||
- [2.3 Time complexity](time_complexity.md)
|
||||
- [2.4 Space complexity](space_complexity.md)
|
||||
- [2.1 Algorithm Efficiency Evaluation](performance_evaluation.md)
|
||||
- [2.2 Iteration and Recursion](iteration_and_recursion.md)
|
||||
- [2.3 Time Complexity](time_complexity.md)
|
||||
- [2.4 Space Complexity](space_complexity.md)
|
||||
- [2.5 Summary](summary.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,52 +2,52 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 2.1 Algorithm efficiency assessment
|
||||
# 2.1 Algorithm Efficiency Evaluation
|
||||
|
||||
In algorithm design, we pursue the following two objectives in sequence.
|
||||
In algorithm design, we pursue the following two levels of objectives sequentially.
|
||||
|
||||
1. **Finding a Solution to the Problem**: The algorithm should reliably find the correct solution within the specified range of inputs.
|
||||
2. **Seeking the Optimal Solution**: For the same problem, multiple solutions might exist, and we aim to find the most efficient algorithm possible.
|
||||
1. **Finding a solution to the problem**: The algorithm must reliably obtain the correct solution within the specified input range.
|
||||
2. **Seeking the optimal solution**: Multiple solutions may exist for the same problem, and we hope to find an algorithm that is as efficient as possible.
|
||||
|
||||
In other words, under the premise of being able to solve the problem, algorithm efficiency has become the main criterion for evaluating an algorithm, which includes the following two dimensions.
|
||||
In other words, under the premise of being able to solve the problem, algorithm efficiency has become the primary evaluation criterion for measuring the quality of algorithms. It includes the following two dimensions.
|
||||
|
||||
- **Time efficiency**: The speed at which an algorithm runs.
|
||||
- **Space efficiency**: The size of the memory space occupied by an algorithm.
|
||||
- **Time efficiency**: The length of time the algorithm runs.
|
||||
- **Space efficiency**: The size of memory space the algorithm occupies.
|
||||
|
||||
In short, **our goal is to design data structures and algorithms that are both fast and memory-efficient**. Effectively assessing algorithm efficiency is crucial because only then can we compare various algorithms and guide the process of algorithm design and optimization.
|
||||
In short, **our goal is to design data structures and algorithms that are "both fast and memory-efficient"**. Effectively evaluating algorithm efficiency is crucial, because only in this way can we compare various algorithms and guide the algorithm design and optimization process.
|
||||
|
||||
There are mainly two methods of efficiency assessment: actual testing and theoretical estimation.
|
||||
Efficiency evaluation methods are mainly divided into two types: actual testing and theoretical estimation.
|
||||
|
||||
## 2.1.1 Actual testing
|
||||
## 2.1.1 Actual Testing
|
||||
|
||||
Suppose we have algorithms `A` and `B`, both capable of solving the same problem, and we need to compare their efficiencies. The most direct method is to use a computer to run these two algorithms, monitor and record their runtime and memory usage. This assessment method reflects the actual situation, but it has significant limitations.
|
||||
Suppose we now have algorithm `A` and algorithm `B`, both of which can solve the same problem, and we need to compare the efficiency of these two algorithms. The most direct method is to find a computer, run these two algorithms, and monitor and record their running time and memory usage. This evaluation approach can reflect the real situation, but it also has considerable limitations.
|
||||
|
||||
On one hand, **it's difficult to eliminate interference from the testing environment**. Hardware configurations can affect algorithm performance. For example, an algorithm with a high degree of parallelism is better suited for running on multi-core CPUs, while an algorithm that involves intensive memory operations performs better with high-performance memory. The test results of an algorithm may vary across different machines. This means testing across multiple machines to calculate average efficiency becomes impractical.
|
||||
On one hand, **it is difficult to eliminate interference factors from the testing environment**. Hardware configuration affects the performance of algorithms. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm has intensive memory operations, it will perform better on high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical.
|
||||
|
||||
On the other hand, **conducting a full test is very resource-intensive**. Algorithm efficiency varies with input data size. For example, with smaller data volumes, algorithm `A` might run faster than `B`, but with larger data volumes, the test results may be the opposite. Therefore, to draw convincing conclusions, we need to test a wide range of input data sizes, which requires excessive computational resources.
|
||||
On the other hand, **conducting complete testing is very resource-intensive**. As the input data volume changes, the algorithm will exhibit different efficiencies. For example, when the input data volume is small, the running time of algorithm `A` is shorter than algorithm `B`; but when the input data volume is large, the test results may be exactly the opposite. Therefore, to obtain convincing conclusions, we need to test input data of various scales, which requires a large amount of computational resources.
|
||||
|
||||
## 2.1.2 Theoretical estimation
|
||||
## 2.1.2 Theoretical Estimation
|
||||
|
||||
Due to the significant limitations of actual testing, we can consider evaluating algorithm efficiency solely through calculations. This estimation method is known as <u>asymptotic complexity analysis</u>, or simply <u>complexity analysis</u>.
|
||||
Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through calculations alone. This estimation method is called <u>asymptotic complexity analysis</u>, or <u>complexity analysis</u> for short.
|
||||
|
||||
Complexity analysis reflects the relationship between the time and space resources required for algorithm execution and the size of the input data. **It describes the trend of growth in the time and space required by the algorithm as the size of the input data increases**. This definition might sound complex, but we can break it down into three key points to understand it better.
|
||||
Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. **It describes the growth trend of the time and space required for algorithm execution as the input data scale increases**. This definition is somewhat convoluted, so we can break it down into three key points to understand.
|
||||
|
||||
- "Time and space resources" correspond to <u>time complexity</u> and <u>space complexity</u>, respectively.
|
||||
- "As the size of input data increases" means that complexity reflects the relationship between algorithm efficiency and the volume of input data.
|
||||
- "The trend of growth in time and space" indicates that complexity analysis focuses not on the specific values of runtime or space occupied, but on the "rate" at which time or space increases.
|
||||
- "As the input data scale increases" means that complexity reflects the relationship between algorithm running efficiency and input data scale.
|
||||
- "Growth trend of time and space" indicates that complexity analysis focuses not on the specific values of running time or occupied space, but on how "fast" time or space grows.
|
||||
|
||||
**Complexity analysis overcomes the disadvantages of actual testing methods**, reflected in the following aspects:
|
||||
**Complexity analysis overcomes the drawbacks of the actual testing method**, reflected in the following aspects.
|
||||
|
||||
- It does not require actually running the code, making it more environmentally friendly and energy efficient.
|
||||
- It is independent of the testing environment and applicable to all operating platforms.
|
||||
- It can reflect algorithm efficiency under different data volumes, especially in the performance of algorithms with large data volumes.
|
||||
- It does not need to actually run the code, making it more environmentally friendly and energy-efficient.
|
||||
- It is independent of the testing environment, and the analysis results are applicable to all running platforms.
|
||||
- It can reflect algorithm efficiency at different data volumes, especially algorithm performance at large data volumes.
|
||||
|
||||
!!! tip
|
||||
|
||||
If you're still confused about the concept of complexity, don't worry. We will cover it in detail in subsequent chapters.
|
||||
If you are still confused about the concept of complexity, don't worry—we will introduce it in detail in subsequent chapters.
|
||||
|
||||
Complexity analysis provides us with a "ruler" to evaluate the efficiency of an algorithm, enabling us to measure the time and space resources required to execute it and compare the efficiency of different algorithms.
|
||||
Complexity analysis provides us with a "ruler" for evaluating algorithm efficiency, allowing us to measure the time and space resources required to execute a certain algorithm and compare the efficiency between different algorithms.
|
||||
|
||||
Complexity is a mathematical concept that might be abstract and challenging for beginners. From this perspective, complexity analysis might not be the most suitable topic to introduce first. However, when discussing the characteristics of a particular data structure or algorithm, it's hard to avoid analyzing its speed and space usage.
|
||||
Complexity is a mathematical concept that may be relatively abstract for beginners, with a relatively high learning difficulty. From this perspective, complexity analysis may not be very suitable as the first content to be introduced. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage.
|
||||
|
||||
In summary, it is recommended to develop a basic understanding of complexity analysis before diving deep into data structures and algorithms, **so that you can perform complexity analysis on simple algorithms**.
|
||||
In summary, it is recommended that before diving deep into data structures and algorithms, **you first establish a preliminary understanding of complexity analysis so that you can complete complexity analysis of simple algorithms**.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,50 +4,56 @@ comments: true
|
||||
|
||||
# 2.5 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
**Algorithm Efficiency Assessment**
|
||||
|
||||
- Time efficiency and space efficiency are the two main criteria for assessing the merits of an algorithm.
|
||||
- We can assess algorithm efficiency through actual testing, but it's challenging to eliminate the influence of the test environment, and it consumes substantial computational resources.
|
||||
- Complexity analysis can overcome the disadvantages of actual testing. Its results are applicable across all operating platforms and can reveal the efficiency of algorithms at different data scales.
|
||||
- Time efficiency and space efficiency are the two primary evaluation metrics for measuring algorithm performance.
|
||||
- We can evaluate algorithm efficiency through actual testing, but it is difficult to eliminate the influence of the testing environment, and it consumes substantial computational resources.
|
||||
- Complexity analysis can eliminate the drawbacks of actual testing, with results applicable to all running platforms, and it can reveal algorithm efficiency under different data scales.
|
||||
|
||||
**Time Complexity**
|
||||
|
||||
- Time complexity measures the trend of an algorithm's running time with the increase in data volume, effectively assessing algorithm efficiency. However, it can fail in certain cases, such as with small input data volumes or when time complexities are the same, making it challenging to precisely compare the efficiency of algorithms.
|
||||
- Worst-case time complexity is denoted using big-$O$ notation, representing the asymptotic upper bound, reflecting the growth level of the number of operations $T(n)$ as $n$ approaches infinity.
|
||||
- Calculating time complexity involves two steps: first counting the number of operations, then determining the asymptotic upper bound.
|
||||
- Common time complexities, arranged from low to high, include $O(1)$, $O(\log n)$, $O(n)$, $O(n \log n)$, $O(n^2)$, $O(2^n)$, and $O(n!)$, among others.
|
||||
- The time complexity of some algorithms is not fixed and depends on the distribution of input data. Time complexities are divided into worst, best, and average cases. The best case is rarely used because input data generally needs to meet strict conditions to achieve the best case.
|
||||
- Average time complexity reflects the efficiency of an algorithm under random data inputs, closely resembling the algorithm's performance in actual applications. Calculating average time complexity requires accounting for the distribution of input data and the subsequent mathematical expectation.
|
||||
- Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but may fail in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency.
|
||||
- Worst-case time complexity is represented using Big $O$ notation, corresponding to the asymptotic upper bound of a function, reflecting the growth level of the number of operations $T(n)$ as $n$ approaches positive infinity.
|
||||
- Deriving time complexity involves two steps: first, counting the number of operations, then determining the asymptotic upper bound.
|
||||
- Common time complexities arranged from low to high include $O(1)$, $O(\log n)$, $O(n)$, $O(n \log n)$, $O(n^2)$, $O(2^n)$, and $O(n!)$.
|
||||
- The time complexity of some algorithms is not fixed, but rather depends on the distribution of input data. Time complexity is divided into worst-case, best-case, and average-case time complexity. Best-case time complexity is rarely used because input data generally needs to satisfy strict conditions to achieve the best case.
|
||||
- Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires statistical analysis of input data distribution and the combined mathematical expectation.
|
||||
|
||||
**Space Complexity**
|
||||
|
||||
- Space complexity, similar to time complexity, measures the trend of memory space occupied by an algorithm with the increase in data volume.
|
||||
- The relevant memory space used during the algorithm's execution can be divided into input space, temporary space, and output space. Generally, input space is not included in space complexity calculations. Temporary space can be divided into temporary data, stack frame space, and instruction space, where stack frame space usually affects space complexity only in recursive functions.
|
||||
- We usually focus only on the worst-case space complexity, which means calculating the space complexity of the algorithm under the worst input data and at the worst moment of operation.
|
||||
- Common space complexities, arranged from low to high, include $O(1)$, $O(\log n)$, $O(n)$, $O(n^2)$, and $O(2^n)$, among others.
|
||||
- Space complexity serves a similar purpose to time complexity, used to measure the trend of algorithm memory usage as data volume increases.
|
||||
- The memory space related to algorithm execution can be divided into input space, temporary space, and output space. Typically, input space is not included in space complexity calculations. Temporary space can be divided into temporary data, stack frame space, and instruction space, where stack frame space usually affects space complexity only in recursive functions.
|
||||
- We typically only focus on worst-case space complexity, which is the space complexity of an algorithm under worst-case input data and worst-case runtime.
|
||||
- Common space complexities arranged from low to high include $O(1)$, $O(\log n)$, $O(n)$, $O(n^2)$, and $O(2^n)$.
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**: Is the space complexity of tail recursion $O(1)$?
|
||||
|
||||
Theoretically, the space complexity of a tail-recursive function can be optimized to $O(1)$. However, most programming languages (such as Java, Python, C++, Go, C#) do not support automatic optimization of tail recursion, so it's generally considered to have a space complexity of $O(n)$.
|
||||
Theoretically, the space complexity of tail recursive functions can be optimized to $O(1)$. However, most programming languages (such as Java, Python, C++, Go, C#, etc.) do not support automatic tail recursion optimization, so the space complexity is generally considered to be $O(n)$.
|
||||
|
||||
**Q**: What is the difference between the terms "function" and "method"?
|
||||
**Q**: What is the difference between the terms function and method?
|
||||
|
||||
A <u>function</u> can be executed independently, with all parameters passed explicitly. A <u>method</u> is associated with an object and is implicitly passed to the object calling it, able to operate on the data contained within an instance of a class.
|
||||
A <u>function</u> can be executed independently, with all parameters passed explicitly. A <u>method</u> is associated with an object, is implicitly passed to the object that invokes it, and can operate on data contained in class instances.
|
||||
|
||||
Here are some examples from common programming languages:
|
||||
The following examples use several common programming languages for illustration.
|
||||
|
||||
- C is a procedural programming language without object-oriented concepts, so it only has functions. However, we can simulate object-oriented programming by creating structures (struct), and functions associated with these structures are equivalent to methods in other programming languages.
|
||||
- C is a procedural programming language without object-oriented concepts, so it only has functions. However, we can simulate object-oriented programming by creating structures (struct), and functions associated with structures are equivalent to methods in other programming languages.
|
||||
- Java and C# are object-oriented programming languages where code blocks (methods) are typically part of a class. Static methods behave like functions because they are bound to the class and cannot access specific instance variables.
|
||||
- C++ and Python support both procedural programming (functions) and object-oriented programming (methods).
|
||||
|
||||
**Q**: Does the "Common Types of Space Complexity" figure reflect the absolute size of occupied space?
|
||||
**Q**: Does the diagram for "common space complexity types" reflect the absolute size of occupied space?
|
||||
|
||||
No, the figure shows space complexities, which reflect growth trends, not the absolute size of the occupied space.
|
||||
No, the diagram shows space complexity, which reflects growth trends rather than the absolute size of occupied space.
|
||||
|
||||
If you take $n = 8$, you might find that the values of each curve don't correspond to their functions. This is because each curve includes a constant term, intended to compress the value range into a visually comfortable range.
|
||||
Assuming $n = 8$, you might find that the values of each curve do not correspond to the functions. This is because each curve contains a constant term used to compress the value range into a visually comfortable range.
|
||||
|
||||
In practice, since we usually don't know the "constant term" complexity of each method, it's generally not possible to choose the best solution for $n = 8$ based solely on complexity. However, for $n = 8^5$, it's much easier to choose, as the growth trend becomes dominant.
|
||||
In practice, because we generally do not know what the "constant term" complexity of each method is, we usually cannot select the optimal solution for $n = 8$ based on complexity alone. But for $n = 8^5$, the choice is straightforward, as the growth trend already dominates.
|
||||
|
||||
**Q**: Are there situations where algorithms are designed to sacrifice time (or space) based on actual use cases?
|
||||
|
||||
In practical applications, most situations choose to sacrifice space for time. For example, with database indexes, we typically choose to build B+ trees or hash indexes, occupying substantial memory space in exchange for efficient queries of $O(\log n)$ or even $O(1)$.
|
||||
|
||||
In scenarios where space resources are precious, time may be sacrificed for space. For example, in embedded development, device memory is precious, and engineers may forgo using hash tables and choose to use array sequential search to save memory usage, at the cost of slower searches.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,73 +2,73 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.2 Basic data types
|
||||
# 3.2 Basic Data Types
|
||||
|
||||
When discussing data in computers, various forms like text, images, videos, voice and 3D models comes to mind. Despite their different organizational forms, they are all composed of various basic data types.
|
||||
When we talk about data in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these data are organized in different ways, they are all composed of various basic data types.
|
||||
|
||||
**Basic data types are those that the CPU can directly operate on** and are directly used in algorithms, mainly including the following.
|
||||
**Basic data types are types that the CPU can directly operate on**, and they are directly used in algorithms, mainly including the following.
|
||||
|
||||
- Integer types: `byte`, `short`, `int`, `long`.
|
||||
- Floating-point types: `float`, `double`, used to represent decimals.
|
||||
- Character type: `char`, used to represent letters, punctuation, and even emojis in various languages.
|
||||
- Boolean type: `bool`, used to represent "yes" or "no" decisions.
|
||||
- Integer types `byte`, `short`, `int`, `long`.
|
||||
- Floating-point types `float`, `double`, used to represent decimal numbers.
|
||||
- Character type `char`, used to represent letters, punctuation marks, and even emojis in various languages.
|
||||
- Boolean type `bool`, used to represent "yes" and "no" judgments.
|
||||
|
||||
**Basic data types are stored in computers in binary form**. One binary digit is 1 bit. In most modern operating systems, 1 byte consists of 8 bits.
|
||||
**Basic data types are stored in binary form in computers**. One binary bit is $1$ bit. In most modern operating systems, $1$ byte consists of $8$ bits.
|
||||
|
||||
The range of values for basic data types depends on the size of the space they occupy. Below, we take Java as an example.
|
||||
The range of values for basic data types depends on the size of the space they occupy. Below is an example using Java.
|
||||
|
||||
- The integer type `byte` occupies 1 byte = 8 bits and can represent $2^8$ numbers.
|
||||
- The integer type `int` occupies 4 bytes = 32 bits and can represent $2^{32}$ numbers.
|
||||
- Integer type `byte` occupies $1$ byte = $8$ bits, and can represent $2^{8}$ numbers.
|
||||
- Integer type `int` occupies $4$ bytes = $32$ bits, and can represent $2^{32}$ numbers.
|
||||
|
||||
The following table lists the space occupied, value range, and default values of various basic data types in Java. While memorizing this table isn't necessary, having a general understanding of it and referencing it when required is recommended.
|
||||
The following table lists the space occupied, value ranges, and default values of various basic data types in Java. You don't need to memorize this table; a general understanding is sufficient, and you can refer to it when needed.
|
||||
|
||||
<p align="center"> Table 3-1 Space occupied and value range of basic data types </p>
|
||||
<p align="center"> Table 3-1 Space occupied and value ranges of basic data types </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| Type | Symbol | Space Occupied | Minimum Value | Maximum Value | Default Value |
|
||||
| ------- | -------- | -------------- | ------------------------ | ----------------------- | -------------- |
|
||||
| Integer | `byte` | 1 byte | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | 0 |
|
||||
| | `short` | 2 bytes | $-2^{15}$ | $2^{15} - 1$ | 0 |
|
||||
| | `int` | 4 bytes | $-2^{31}$ | $2^{31} - 1$ | 0 |
|
||||
| | `long` | 8 bytes | $-2^{63}$ | $2^{63} - 1$ | 0 |
|
||||
| Float | `float` | 4 bytes | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ |
|
||||
| | `double` | 8 bytes | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | 0.0 |
|
||||
| Char | `char` | 2 bytes | 0 | $2^{16} - 1$ | 0 |
|
||||
| Boolean | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ |
|
||||
| Type | Symbol | Space Occupied | Minimum Value | Maximum Value | Default Value |
|
||||
| ---------- | -------- | -------------- | ------------------------ | ----------------------- | -------------- |
|
||||
| Integer | `byte` | 1 byte | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | $0$ |
|
||||
| | `short` | 2 bytes | $-2^{15}$ | $2^{15} - 1$ | $0$ |
|
||||
| | `int` | 4 bytes | $-2^{31}$ | $2^{31} - 1$ | $0$ |
|
||||
| | `long` | 8 bytes | $-2^{63}$ | $2^{63} - 1$ | $0$ |
|
||||
| Float | `float` | 4 bytes | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ |
|
||||
| | `double` | 8 bytes | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | $0.0$ |
|
||||
| Character | `char` | 2 bytes | $0$ | $2^{16} - 1$ | $0$ |
|
||||
| Boolean | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ |
|
||||
|
||||
</div>
|
||||
|
||||
Please note that the above table is specific to Java's basic data types. Every programming language has its own data type definitions, which might differ in space occupied, value ranges, and default values.
|
||||
Please note that the above table is specific to Java's basic data types. Each programming language has its own data type definitions, and their space occupied, value ranges, and default values may vary.
|
||||
|
||||
- In Python, the integer type `int` can be of any size, limited only by available memory; the floating-point `float` is double precision 64-bit; there is no `char` type, as a single character is actually a string `str` of length 1.
|
||||
- C and C++ do not specify the size of basic data types, it varies with implementation and platform. The above table follows the LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), used for Unix 64-bit operating systems including Linux and macOS.
|
||||
- The size of `char` in C and C++ is 1 byte, while in most programming languages, it depends on the specific character encoding method, as detailed in the "Character Encoding" chapter.
|
||||
- Even though representing a boolean only requires 1 bit (0 or 1), it is usually stored in memory as 1 byte. This is because modern computer CPUs typically use 1 byte as the smallest addressable memory unit.
|
||||
- In Python, the integer type `int` can be of any size, limited only by available memory; the floating-point type `float` is double-precision 64-bit; there is no `char` type, a single character is actually a string `str` of length 1.
|
||||
- C and C++ do not explicitly specify the size of basic data types, which varies by implementation and platform. The above table follows the LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), which is used in Unix 64-bit operating systems including Linux and macOS.
|
||||
- The size of character `char` is 1 byte in C and C++, and in most programming languages it depends on the specific character encoding method, as detailed in the "Character Encoding" section.
|
||||
- Even though representing a boolean value requires only 1 bit ($0$ or $1$), it is usually stored as 1 byte in memory. This is because modern computer CPUs typically use 1 byte as the minimum addressable memory unit.
|
||||
|
||||
So, what is the connection between basic data types and data structures? We know that data structures are ways to organize and store data in computers. The focus here is on "structure" rather than "data".
|
||||
So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. The subject of this statement is "structure", not "data".
|
||||
|
||||
If we want to represent "a row of numbers", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and the ordering of the numbers, but whether the stored content is an integer `int`, a decimal `float`, or a character `char`, is irrelevant to the "data structure".
|
||||
If we want to represent "a row of numbers", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but the content stored—whether integer `int`, floating-point `float`, or character `char`—is unrelated to the "data structure".
|
||||
|
||||
In other words, **basic data types provide the "content type" of data, while data structures provide the "way of organizing" data**. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including `int`, `float`, `char`, `bool`, etc.
|
||||
In other words, **basic data types provide the "content type" of data, while data structures provide the "organization method" of data**. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including `int`, `float`, `char`, `bool`, etc.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
# Using various basic data types to initialize arrays
|
||||
# Initialize arrays using various basic data types
|
||||
numbers: list[int] = [0] * 5
|
||||
decimals: list[float] = [0.0] * 5
|
||||
# Python's characters are actually strings of length 1
|
||||
# In Python, characters are actually strings of length 1
|
||||
characters: list[str] = ['0'] * 5
|
||||
bools: list[bool] = [False] * 5
|
||||
# Python's lists can freely store various basic data types and object references
|
||||
# Python lists can freely store various basic data types and object references
|
||||
data = [0, 0.0, 'a', False, ListNode(0)]
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
int numbers[5];
|
||||
float decimals[5];
|
||||
char characters[5];
|
||||
@@ -78,7 +78,7 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
int[] numbers = new int[5];
|
||||
float[] decimals = new float[5];
|
||||
char[] characters = new char[5];
|
||||
@@ -88,7 +88,7 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
int[] numbers = new int[5];
|
||||
float[] decimals = new float[5];
|
||||
char[] characters = new char[5];
|
||||
@@ -98,7 +98,7 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
var numbers = [5]int{}
|
||||
var decimals = [5]float64{}
|
||||
var characters = [5]byte{}
|
||||
@@ -108,7 +108,7 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
let numbers = Array(repeating: 0, count: 5)
|
||||
let decimals = Array(repeating: 0.0, count: 5)
|
||||
let characters: [Character] = Array(repeating: "a", count: 5)
|
||||
@@ -118,14 +118,14 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
// JavaScript's arrays can freely store various basic data types and objects
|
||||
// JavaScript arrays can freely store various basic data types and objects
|
||||
const array = [0, 0.0, 'a', false];
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
const numbers: number[] = [];
|
||||
const characters: string[] = [];
|
||||
const bools: boolean[] = [];
|
||||
@@ -134,7 +134,7 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
List<int> numbers = List.filled(5, 0);
|
||||
List<double> decimals = List.filled(5, 0.0);
|
||||
List<String> characters = List.filled(5, 'a');
|
||||
@@ -144,9 +144,9 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
let numbers: Vec<i32> = vec![0; 5];
|
||||
let decimals: Vec<f32> = vec![0.0, 5];
|
||||
let decimals: Vec<f32> = vec![0.0; 5];
|
||||
let characters: Vec<char> = vec!['0'; 5];
|
||||
let bools: Vec<bool> = vec![false; 5];
|
||||
```
|
||||
@@ -154,7 +154,7 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
// Initialize arrays using various basic data types
|
||||
int numbers[10];
|
||||
float decimals[10];
|
||||
char characters[10];
|
||||
@@ -164,15 +164,20 @@ In other words, **basic data types provide the "content type" of data, while dat
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
|
||||
// Initialize arrays using various basic data types
|
||||
val numbers = IntArray(5)
|
||||
val decinals = FloatArray(5)
|
||||
val characters = CharArray(5)
|
||||
val bools = BooleanArray(5)
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
=== "Ruby"
|
||||
|
||||
```zig title=""
|
||||
// Using various basic data types to initialize arrays
|
||||
var numbers: [5]i32 = undefined;
|
||||
var decimals: [5]f32 = undefined;
|
||||
var characters: [5]u8 = undefined;
|
||||
var bools: [5]bool = undefined;
|
||||
```ruby title=""
|
||||
# Ruby lists can freely store various basic data types and object references
|
||||
data = [0, 0.0, 'a', false, ListNode(0)]
|
||||
```
|
||||
|
||||
??? pythontutor "Visualized Execution"
|
||||
|
||||
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
@@ -2,96 +2,96 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.4 Character encoding *
|
||||
# 3.4 Character Encoding *
|
||||
|
||||
In the computer system, all data is stored in binary form, and `char` is no exception. To represent characters, we need to develop a "character set" that defines a one-to-one mapping between each character and binary numbers. With the character set, computers can convert binary numbers to characters by looking up the table.
|
||||
In computers, all data is stored in binary form, and character `char` is no exception. To represent characters, we need to establish a "character set" that defines a one-to-one correspondence between each character and binary numbers. With a character set, computers can convert binary numbers to characters by looking up the table.
|
||||
|
||||
## 3.4.1 ASCII character set
|
||||
## 3.4.1 Ascii Character Set
|
||||
|
||||
The <u>ASCII code</u> is one of the earliest character sets, officially known as the American Standard Code for Information Interchange. It uses 7 binary digits (the lower 7 bits of a byte) to represent a character, allowing for a maximum of 128 different characters. As shown in Figure 3-6, ASCII includes uppercase and lowercase English letters, numbers 0 ~ 9, various punctuation marks, and certain control characters (such as newline and tab).
|
||||
<u>ASCII code</u> is the earliest character set, with the full name American Standard Code for Information Interchange. It uses 7 binary bits (the lower 7 bits of one byte) to represent a character, and can represent a maximum of 128 different characters. As shown in Figure 3-6, ASCII code includes uppercase and lowercase English letters, numbers 0 ~ 9, some punctuation marks, and some control characters (such as newline and tab).
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-6 ASCII code </p>
|
||||
|
||||
However, **ASCII can only represent English characters**. With the globalization of computers, a character set called <u>EASCII</u> was developed to represent more languages. It expands from the 7-bit structure of ASCII to 8 bits, enabling the representation of 256 characters.
|
||||
However, **ASCII code can only represent English**. With the globalization of computers, a character set called <u>EASCII</u> that can represent more languages emerged. It expands from the 7-bit basis of ASCII to 8 bits, and can represent 256 different characters.
|
||||
|
||||
Globally, various region-specific EASCII character sets have been introduced. The first 128 characters of these sets are consistent with the ASCII, while the remaining 128 characters are defined differently to accommodate the requirements of different languages.
|
||||
Worldwide, a batch of EASCII character sets suitable for different regions have appeared successively. The first 128 characters of these character sets are unified as ASCII code, and the last 128 characters are defined differently to adapt to the needs of different languages.
|
||||
|
||||
## 3.4.2 GBK character set
|
||||
## 3.4.2 Gbk Character Set
|
||||
|
||||
Later, it was found that **EASCII still could not meet the character requirements of many languages**. For instance, there are nearly a hundred thousand Chinese characters, with several thousand used regularly. In 1980, the Standardization Administration of China released the <u>GB2312</u> character set, which included 6763 Chinese characters, essentially fulfilling the computer processing needs for the Chinese language.
|
||||
Later, people found that **EASCII code still cannot meet the character quantity requirements of many languages**. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used daily. In 1980, the China National Standardization Administration released the <u>GB2312</u> character set, which included 6,763 Chinese characters, basically meeting the needs for computer processing of Chinese characters.
|
||||
|
||||
However, GB2312 could not handle some rare and traditional characters. The <u>GBK</u> character set expands GB2312 and includes 21886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented with one byte, while Chinese characters use two bytes.
|
||||
However, GB2312 cannot handle some rare characters and traditional Chinese characters. The <u>GBK</u> character set is an extension based on GB2312, which includes a total of 21,886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented using one byte, and Chinese characters are represented using two bytes.
|
||||
|
||||
## 3.4.3 Unicode character set
|
||||
## 3.4.3 Unicode Character Set
|
||||
|
||||
With the rapid evolution of computer technology and a plethora of character sets and encoding standards, numerous problems arose. On the one hand, these character sets generally only defined characters for specific languages and could not function properly in multilingual environments. On the other hand, the existence of multiple character set standards for the same language caused garbled text when information was exchanged between computers using different encoding standards.
|
||||
With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission.
|
||||
|
||||
Researchers of that era thought: **What if a comprehensive character set encompassing all global languages and symbols was developed? Wouldn't this resolve the issues associated with cross-linguistic environments and garbled text?** Inspired by this idea, the extensive character set, Unicode, was born.
|
||||
Researchers of that era thought: **If a sufficiently complete character set is released that includes all languages and symbols in the world, wouldn't it be possible to solve cross-language environment and garbled character problems**? Driven by this idea, a large and comprehensive character set, Unicode, was born.
|
||||
|
||||
<u>Unicode</u> is referred to as "统一码" (Unified Code) in Chinese, theoretically capable of accommodating over a million characters. It aims to incorporate characters from all over the world into a single set, providing a universal character set for processing and displaying various languages and reducing the issues of garbled text due to different encoding standards.
|
||||
<u>Unicode</u> is called "统一码" (Unified Code) in Chinese and can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards.
|
||||
|
||||
Since its release in 1991, Unicode has continually expanded to include new languages and characters. As of September 2022, Unicode contains 149,186 characters, including characters, symbols, and even emojis from various languages. In the vast Unicode character set, commonly used characters occupy 2 bytes, while some rare characters may occupy 3 or even 4 bytes.
|
||||
Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. In the vast Unicode character set, commonly used characters occupy 2 bytes, and some rare characters occupy 3 bytes or even 4 bytes.
|
||||
|
||||
Unicode is a universal character set that assigns a number (called a "code point") to each character, **but it does not specify how these character code points should be stored in a computer system**. One might ask: How does a system interpret Unicode code points of varying lengths within a text? For example, given a 2-byte code, how does the system determine if it represents a single 2-byte character or two 1-byte characters?
|
||||
Unicode is a universal character set that essentially assigns a number (called a "code point") to each character, **but it does not specify how to store these character code points in computers**. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters?
|
||||
|
||||
**A straightforward solution to this problem is to store all characters as equal-length encodings**. As shown in Figure 3-7, each character in "Hello" occupies 1 byte, while each character in "算法" (algorithm) occupies 2 bytes. We could encode all characters in "Hello 算法" as 2 bytes by padding the higher bits with zeros. This method would enable the system to interpret a character every 2 bytes, recovering the content of the phrase.
|
||||
For the above problem, **a straightforward solution is to store all characters as equal-length encodings**. As shown in Figure 3-7, each character in "Hello" occupies 1 byte, and each character in "算法" (algorithm) occupies 2 bytes. We can encode all characters in "Hello 算法" as 2 bytes in length by padding the high bits with 0. In this way, the system can parse one character every 2 bytes and restore the content of this phrase.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-7 Unicode encoding example </p>
|
||||
|
||||
However, as ASCII has shown us, encoding English only requires 1 byte. Using the above approach would double the space occupied by English text compared to ASCII encoding, which is a waste of memory space. Therefore, a more efficient Unicode encoding method is needed.
|
||||
However, ASCII code has already proven to us that encoding English only requires 1 byte. If the above scheme is adopted, the size of English text will be twice that under ASCII encoding, which is very wasteful of memory space. Therefore, we need a more efficient Unicode encoding method.
|
||||
|
||||
## 3.4.4 UTF-8 encoding
|
||||
## 3.4.4 Utf-8 Encoding
|
||||
|
||||
Currently, UTF-8 has become the most widely used Unicode encoding method internationally. **It is a variable-length encoding**, using 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters need only 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters need 3 bytes, and some other rare characters need 4 bytes.
|
||||
Currently, UTF-8 has become the most widely used Unicode encoding method internationally. **It is a variable-length encoding** that uses 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters only require 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters require 3 bytes, and some other rare characters require 4 bytes.
|
||||
|
||||
The encoding rules for UTF-8 are not complex and can be divided into two cases:
|
||||
The encoding rules of UTF-8 are not complicated and can be divided into the following two cases.
|
||||
|
||||
- For 1-byte characters, set the highest bit to $0$, and the remaining 7 bits to the Unicode code point. Notably, ASCII characters occupy the first 128 code points in the Unicode set. This means that **UTF-8 encoding is backward compatible with ASCII**. This implies that UTF-8 can be used to parse ancient ASCII text.
|
||||
- For characters of length $n$ bytes (where $n > 1$), set the highest $n$ bits of the first byte to $1$, and the $(n + 1)^{\text{th}}$ bit to $0$; starting from the second byte, set the highest 2 bits of each byte to $10$; the rest of the bits are used to fill the Unicode code point.
|
||||
- For 1-byte characters, set the highest bit to $0$, and set the remaining 7 bits to the Unicode code point. It is worth noting that ASCII characters occupy the first 128 code points in the Unicode character set. That is to say, **UTF-8 encoding is backward compatible with ASCII code**. This means we can use UTF-8 to parse very old ASCII code text.
|
||||
- For characters with a length of $n$ bytes (where $n > 1$), set the highest $n$ bits of the first byte to $1$, and set the $(n + 1)$-th bit to $0$; starting from the second byte, set the highest 2 bits of each byte to $10$; use all remaining bits to fill in the Unicode code point of the character.
|
||||
|
||||
Figure 3-8 shows the UTF-8 encoding for "Hello算法". It can be observed that since the highest $n$ bits are set to $1$, the system can determine the length of the character as $n$ by counting the number of highest bits set to $1$.
|
||||
Figure 3-8 shows the UTF-8 encoding corresponding to "Hello算法". It can be observed that since the highest $n$ bits are all set to $1$, the system can parse the length of the character as $n$ by reading the number of highest bits that are $1$.
|
||||
|
||||
But why set the highest 2 bits of the remaining bytes to $10$? Actually, this $10$ serves as a kind of checksum. If the system starts parsing text from an incorrect byte, the $10$ at the beginning of the byte can help the system quickly detect anomalies.
|
||||
But why set the highest 2 bits of all other bytes to $10$? In fact, this $10$ can serve as a check symbol. Assuming the system starts parsing text from an incorrect byte, the $10$ at the beginning of the byte can help the system quickly determine an anomaly.
|
||||
|
||||
The reason for using $10$ as a checksum is that, under UTF-8 encoding rules, it's impossible for the highest two bits of a character to be $10$. This can be proven by contradiction: If the highest two bits of a character are $10$, it indicates that the character's length is $1$, corresponding to ASCII. However, the highest bit of an ASCII character should be $0$, which contradicts the assumption.
|
||||
The reason for using $10$ as a check symbol is that under UTF-8 encoding rules, it is impossible for a character's highest two bits to be $10$. This conclusion can be proven by contradiction: assuming the highest two bits of a character are $10$, it means the length of the character is $1$, corresponding to ASCII code. However, the highest bit of ASCII code should be $0$, which contradicts the assumption.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-8 UTF-8 encoding example </p>
|
||||
|
||||
Apart from UTF-8, other common encoding methods include:
|
||||
In addition to UTF-8, common encoding methods also include the following two.
|
||||
|
||||
- **UTF-16 encoding**: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters require 4 bytes. For 2-byte characters, the UTF-16 encoding equals the Unicode code point.
|
||||
- **UTF-32 encoding**: Every character uses 4 bytes. This means UTF-32 occupies more space than UTF-8 and UTF-16, especially for texts with a high proportion of ASCII characters.
|
||||
- **UTF-16 encoding**: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters need to use 4 bytes. For 2-byte characters, UTF-16 encoding is equal to the Unicode code point.
|
||||
- **UTF-32 encoding**: Every character uses 4 bytes. This means that UTF-32 takes up more space than UTF-8 and UTF-16, especially for text with a high proportion of ASCII characters.
|
||||
|
||||
From the perspective of storage space, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 to encode some non-English characters (such as Chinese) can be more efficient because it only requires 2 bytes, while UTF-8 might need 3 bytes.
|
||||
From the perspective of storage space occupation, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 encoding for some non-English characters (such as Chinese) will be more efficient because it only requires 2 bytes, while UTF-8 may require 3 bytes.
|
||||
|
||||
From a compatibility perspective, UTF-8 is the most versatile, with many tools and libraries supporting UTF-8 as a priority.
|
||||
From a compatibility perspective, UTF-8 has the best universality, and many tools and libraries support UTF-8 first.
|
||||
|
||||
## 3.4.5 Character encoding in programming languages
|
||||
## 3.4.5 Character Encoding in Programming Languages
|
||||
|
||||
Historically, many programming languages utilized fixed-length encodings such as UTF-16 or UTF-32 for processing strings during program execution. This allows strings to be handled as arrays, offering several advantages:
|
||||
For most past programming languages, strings during program execution use fixed-length encodings such as UTF-16 or UTF-32. Under fixed-length encoding, we can treat strings as arrays for processing, and this approach has the following advantages.
|
||||
|
||||
- **Random access**: Strings encoded in UTF-16 can be accessed randomly with ease. For UTF-8, which is a variable-length encoding, locating the $i^{th}$ character requires traversing the string from the start to the $i^{th}$ position, taking $O(n)$ time.
|
||||
- **Character counting**: Similar to random access, counting the number of characters in a UTF-16 encoded string is an $O(1)$ operation. However, counting characters in a UTF-8 encoded string requires traversing the entire string.
|
||||
- **String operations**: Many string operations like splitting, concatenating, inserting, and deleting are easier on UTF-16 encoded strings. These operations generally require additional computation on UTF-8 encoded strings to ensure the validity of the UTF-8 encoding.
|
||||
- **Random access**: UTF-16 encoded strings can be easily accessed randomly. UTF-8 is a variable-length encoding. To find the $i$-th character, we need to traverse from the beginning of the string to the $i$-th character, which requires $O(n)$ time.
|
||||
- **Character counting**: Similar to random access, calculating the length of a UTF-16 encoded string is also an $O(1)$ operation. However, calculating the length of a UTF-8 encoded string requires traversing the entire string.
|
||||
- **String operations**: Many string operations (such as splitting, joining, inserting, deleting, etc.) on UTF-16 encoded strings are easier to perform. Performing these operations on UTF-8 encoded strings usually requires additional calculations to ensure that invalid UTF-8 encoding is not generated.
|
||||
|
||||
The design of character encoding schemes in programming languages is an interesting topic involving various factors:
|
||||
In fact, the design of character encoding schemes for programming languages is a very interesting topic involving many factors.
|
||||
|
||||
- Java’s `String` type uses UTF-16 encoding, with each character occupying 2 bytes. This was based on the initial belief that 16 bits were sufficient to represent all possible characters and proven incorrect later. As the Unicode standard expanded beyond 16 bits, characters in Java may now be represented by a pair of 16-bit values, known as “surrogate pairs.”
|
||||
- JavaScript and TypeScript use UTF-16 encoding for similar reasons as Java. When JavaScript was first introduced by Netscape in 1995, Unicode was still in its early stages, and 16-bit encoding was sufficient to represent all Unicode characters.
|
||||
- C# uses UTF-16 encoding, largely because the .NET platform, designed by Microsoft, and many Microsoft technologies, including the Windows operating system, extensively use UTF-16 encoding.
|
||||
- Java's `String` type uses UTF-16 encoding, with each character occupying 2 bytes. This is because at the beginning of Java language design, people believed that 16 bits were sufficient to represent all possible characters. However, this was an incorrect judgment. Later, the Unicode specification expanded beyond 16 bits, so characters in Java may now be represented by a pair of 16-bit values (called "surrogate pairs").
|
||||
- The strings of JavaScript and TypeScript use UTF-16 encoding for reasons similar to Java. When Netscape first introduced the JavaScript language in 1995, Unicode was still in its early stages of development, and at that time, using 16-bit encoding was sufficient to represent all Unicode characters.
|
||||
- C# uses UTF-16 encoding mainly because the .NET platform was designed by Microsoft, and many of Microsoft's technologies (including the Windows operating system) extensively use UTF-16 encoding.
|
||||
|
||||
Due to the underestimation of character counts, these languages had to use "surrogate pairs" to represent Unicode characters exceeding 16 bits. This approach has its drawbacks: strings containing surrogate pairs may have characters occupying 2 or 4 bytes, losing the advantage of fixed-length encoding. Additionally, handling surrogate pairs adds complexity and debugging difficulty to programming.
|
||||
Due to the underestimation of character quantities by the above programming languages, they had to adopt the "surrogate pair" method to represent Unicode characters with lengths exceeding 16 bits. This is a reluctant compromise. On the one hand, in strings containing surrogate pairs, one character may occupy 2 bytes or 4 bytes, thus losing the advantage of fixed-length encoding. On the other hand, handling surrogate pairs requires additional code, which increases the complexity and difficulty of debugging in programming.
|
||||
|
||||
Addressing these challenges, some languages have adopted alternative encoding strategies:
|
||||
For the above reasons, some programming languages have proposed different encoding schemes.
|
||||
|
||||
- Python’s `str` type uses Unicode encoding with a flexible representation where the storage length of characters depends on the largest Unicode code point in the string. If all characters are ASCII, each character occupies 1 byte, 2 bytes for characters within the Basic Multilingual Plane (BMP), and 4 bytes for characters beyond the BMP.
|
||||
- Go’s `string` type internally uses UTF-8 encoding. Go also provides the `rune` type for representing individual Unicode code points.
|
||||
- Rust’s `str` and `String` types use UTF-8 encoding internally. Rust also offers the `char` type for individual Unicode code points.
|
||||
- Python's `str` uses Unicode encoding and adopts a flexible string representation where the stored character length depends on the largest Unicode code point in the string. If all characters in the string are ASCII characters, each character occupies 1 byte; if there are characters exceeding the ASCII range but all within the Basic Multilingual Plane (BMP), each character occupies 2 bytes; if there are characters exceeding the BMP, each character occupies 4 bytes.
|
||||
- Go language's `string` type uses UTF-8 encoding internally. Go language also provides the `rune` type, which is used to represent a single Unicode code point.
|
||||
- Rust language's `str` and `String` types use UTF-8 encoding internally. Rust also provides the `char` type for representing a single Unicode code point.
|
||||
|
||||
It’s important to note that the above discussion pertains to how strings are stored in programming languages, **which is different from how strings are stored in files or transmitted over networks**. For file storage or network transmission, strings are usually encoded in UTF-8 format for optimal compatibility and space efficiency.
|
||||
It should be noted that the above discussion is about how strings are stored in programming languages, **which is different from how strings are stored in files or transmitted over networks**. In file storage or network transmission, we usually encode strings into UTF-8 format to achieve optimal compatibility and space efficiency.
|
||||
|
||||
@@ -2,57 +2,57 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.1 Classification of data structures
|
||||
# 3.1 Classification of Data Structures
|
||||
|
||||
Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified into "logical structure" and "physical structure".
|
||||
Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified from two dimensions: "logical structure" and "physical structure".
|
||||
|
||||
## 3.1.1 Logical structure: linear and non-linear
|
||||
## 3.1.1 Logical Structure: Linear and Non-Linear
|
||||
|
||||
**The logical structures reveal the logical relationships between data elements**. In arrays and linked lists, data are arranged in a specific sequence, demonstrating the linear relationship between data; while in trees, data are arranged hierarchically from the top down, showing the derived relationship between "ancestors" and "descendants"; and graphs are composed of nodes and edges, reflecting the intricate network relationship.
|
||||
**Logical structure reveals the logical relationships between data elements**. In arrays and linked lists, data is arranged in a certain order, embodying the linear relationship between data; while in trees, data is arranged hierarchically from top to bottom, showing the derived relationship between "ancestors" and "descendants"; graphs are composed of nodes and edges, reflecting complex network relationships.
|
||||
|
||||
As shown in Figure 3-1, logical structures can be divided into two major categories: "linear" and "non-linear". Linear structures are more intuitive, indicating data is arranged linearly in logical relationships; non-linear structures, conversely, are arranged non-linearly.
|
||||
As shown in Figure 3-1, logical structures can be divided into two major categories: "linear" and "non-linear". Linear structures are more intuitive, indicating that data is linearly arranged in logical relationships; non-linear structures are the opposite, arranged non-linearly.
|
||||
|
||||
- **Linear data structures**: Arrays, Linked Lists, Stacks, Queues, Hash Tables, where elements have a one-to-one sequential relationship.
|
||||
- **Non-linear data structures**: Trees, Heaps, Graphs, Hash Tables.
|
||||
- **Linear data structures**: Arrays, linked lists, stacks, queues, hash tables, where elements have a one-to-one sequential relationship.
|
||||
- **Non-linear data structures**: Trees, heaps, graphs, hash tables.
|
||||
|
||||
Non-linear data structures can be further divided into tree structures and network structures.
|
||||
|
||||
- **Tree structures**: Trees, Heaps, Hash Tables, where elements have a one-to-many relationship.
|
||||
- **Network structures**: Graphs, where elements have a many-to-many relationships.
|
||||
- **Tree structures**: Trees, heaps, hash tables, where elements have a one-to-many relationship.
|
||||
- **Network structures**: Graphs, where elements have a many-to-many relationship.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-1 Linear and non-linear data structures </p>
|
||||
|
||||
## 3.1.2 Physical structure: contiguous and dispersed
|
||||
## 3.1.2 Physical Structure: Contiguous and Dispersed
|
||||
|
||||
**During the execution of an algorithm, the data being processed is stored in memory**. Figure 3-2 shows a computer memory stick where each black square is a physical memory space. We can think of memory as a vast Excel spreadsheet, with each cell capable of storing a certain amount of data.
|
||||
**When an algorithm program runs, the data being processed is mainly stored in memory**. Figure 3-2 shows a computer memory stick, where each black square contains a memory space. We can imagine memory as a huge Excel spreadsheet, where each cell can store a certain amount of data.
|
||||
|
||||
**The system accesses the data at the target location by means of a memory address**. As shown in Figure 3-2, the computer assigns a unique identifier to each cell in the table according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access the data stored in memory.
|
||||
**The system accesses data at the target location through memory addresses**. As shown in Figure 3-2, the computer assigns a number to each cell in the spreadsheet according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access data in memory.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-2 Memory stick, memory spaces, memory addresses </p>
|
||||
<p align="center"> Figure 3-2 Memory stick, memory space, memory address </p>
|
||||
|
||||
!!! tip
|
||||
|
||||
It's worth noting that comparing memory to an Excel spreadsheet is a simplified analogy. The actual working mechanism of memory is more complex, involving concepts like address space, memory management, cache mechanisms, virtual memory, and physical memory.
|
||||
It is worth noting that comparing memory to an Excel spreadsheet is a simplified analogy. The actual working mechanism of memory is quite complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory.
|
||||
|
||||
Memory is a shared resource for all programs. When a block of memory is occupied by one program, it cannot be simultaneously used by other programs. **Therefore, memory resources are an important consideration in the design of data structures and algorithms**. For instance, the algorithm's peak memory usage should not exceed the remaining free memory of the system; if there is a lack of contiguous memory blocks, then the data structure chosen must be able to be stored in non-contiguous memory blocks.
|
||||
Memory is a shared resource for all programs. When a block of memory is occupied by a program, it usually cannot be used by other programs at the same time. **Therefore, in the design of data structures and algorithms, memory resources are an important consideration**. For example, the peak memory occupied by an algorithm should not exceed the remaining free memory of the system; if there is a lack of contiguous large memory blocks, then the data structure chosen must be able to be stored in dispersed memory spaces.
|
||||
|
||||
As illustrated in Figure 3-3, **the physical structure reflects the way data is stored in computer memory** and it can be divided into contiguous space storage (arrays) and non-contiguous space storage (linked lists). The two types of physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency.
|
||||
As shown in Figure 3-3, **physical structure reflects the way data is stored in computer memory**, and can be divided into contiguous space storage (arrays) and dispersed space storage (linked lists). The two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-3 Contiguous space storage and dispersed space storage </p>
|
||||
|
||||
**It is worth noting that all data structures are implemented based on arrays, linked lists, or a combination of both**. For example, stacks and queues can be implemented using either arrays or linked lists; while implementations of hash tables may involve both arrays and linked lists.
|
||||
It is worth noting that **all data structures are implemented based on arrays, linked lists, or a combination of both**. For example, stacks and queues can be implemented using either arrays or linked lists; while the implementation of hash tables may include both arrays and linked lists.
|
||||
|
||||
- **Array-based implementations**: Stacks, Queues, Hash Tables, Trees, Heaps, Graphs, Matrices, Tensors (arrays with dimensions $\geq 3$).
|
||||
- **Linked-list-based implementations**: Stacks, Queues, Hash Tables, Trees, Heaps, Graphs, etc.
|
||||
- **Can be implemented based on arrays**: Stacks, queues, hash tables, trees, heaps, graphs, matrices, tensors (arrays with dimensions $\geq 3$), etc.
|
||||
- **Can be implemented based on linked lists**: Stacks, queues, hash tables, trees, heaps, graphs, etc.
|
||||
|
||||
Data structures implemented based on arrays are also called “Static Data Structures,” meaning their length cannot be changed after initialization. Conversely, those based on linked lists are called “Dynamic Data Structures,” which can still adjust their size during program execution.
|
||||
After initialization, linked lists can still adjust their length during program execution, so they are also called "dynamic data structures". After initialization, the length of arrays cannot be changed, so they are also called "static data structures". It is worth noting that arrays can achieve length changes by reallocating memory, thus possessing a certain degree of "dynamism".
|
||||
|
||||
!!! tip
|
||||
|
||||
If you find it challenging to comprehend the physical structure, it is recommended that you read the next chapter, "Arrays and Linked Lists," and revisit this section later.
|
||||
If you find it difficult to understand physical structure, it is recommended to read the next chapter first, and then review this section.
|
||||
|
||||
@@ -3,20 +3,20 @@ comments: true
|
||||
icon: material/shape-outline
|
||||
---
|
||||
|
||||
# Chapter 3. Data structures
|
||||
# Chapter 3. Data Structures
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
Data structures serve as a robust and diverse framework.
|
||||
Data structure is like a sturdy and diverse framework.
|
||||
|
||||
They offer a blueprint for the orderly organization of data, upon which algorithms come to life.
|
||||
It provides a blueprint for the orderly organization of data, upon which algorithms come to life.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [3.1 Classification of data structures](classification_of_data_structure.md)
|
||||
- [3.2 Basic data types](basic_data_types.md)
|
||||
- [3.3 Number encoding *](number_encoding.md)
|
||||
- [3.4 Character encoding *](character_encoding.md)
|
||||
- [3.1 Classification of Data Structures](classification_of_data_structure.md)
|
||||
- [3.2 Basic Data Types](basic_data_types.md)
|
||||
- [3.3 Number Encoding *](number_encoding.md)
|
||||
- [3.4 Character Encoding *](character_encoding.md)
|
||||
- [3.5 Summary](summary.md)
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 3.3 Number encoding *
|
||||
# 3.3 Number Encoding *
|
||||
|
||||
!!! tip
|
||||
|
||||
In this book, chapters marked with an asterisk '*' are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters.
|
||||
In this book, chapters marked with an asterisk * are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters.
|
||||
|
||||
## 3.3.1 Integer encoding
|
||||
## 3.3.1 Sign-Magnitude, 1's Complement, and 2's Complement
|
||||
|
||||
In the table from the previous section, we observed that all integer types can represent one more negative number than positive numbers, such as the `byte` range of $[-128, 127]$. This phenomenon seems counterintuitive, and its underlying reason involves knowledge of sign-magnitude, one's complement, and two's complement encoding.
|
||||
In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the `byte` range is $[-128, 127]$. This phenomenon is counterintuitive, and its underlying reason involves knowledge of sign-magnitude, 1's complement, and 2's complement.
|
||||
|
||||
Firstly, it's important to note that **numbers are stored in computers using the two's complement form**. Before analyzing why this is the case, let's define these three encoding methods:
|
||||
First, it should be noted that **numbers are stored in computers in the form of "2's complement"**. Before analyzing the reasons for this, let's first define these three concepts.
|
||||
|
||||
- **Sign-magnitude**: The highest bit of a binary representation of a number is considered the sign bit, where $0$ represents a positive number and $1$ represents a negative number. The remaining bits represent the value of the number.
|
||||
- **One's complement**: The one's complement of a positive number is the same as its sign-magnitude. For negative numbers, it's obtained by inverting all bits except the sign bit.
|
||||
- **Two's complement**: The two's complement of a positive number is the same as its sign-magnitude. For negative numbers, it's obtained by adding $1$ to their one's complement.
|
||||
- **Sign-magnitude**: We treat the highest bit of the binary representation of a number as the sign bit, where $0$ represents a positive number and $1$ represents a negative number, and the remaining bits represent the value of the number.
|
||||
- **1's complement**: The 1's complement of a positive number is the same as its sign-magnitude. For a negative number, the 1's complement is obtained by inverting all bits except the sign bit of its sign-magnitude.
|
||||
- **2's complement**: The 2's complement of a positive number is the same as its sign-magnitude. For a negative number, the 2's complement is obtained by adding $1$ to its 1's complement.
|
||||
|
||||
Figure 3-4 illustrates the conversions among sign-magnitude, one's complement, and two's complement:
|
||||
Figure 3-4 shows the conversion methods among sign-magnitude, 1's complement, and 2's complement.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-4 Conversions between sign-magnitude, one's complement, and two's complement </p>
|
||||
<p align="center"> Figure 3-4 Conversions among sign-magnitude, 1's complement, and 2's complement </p>
|
||||
|
||||
Although <u>sign-magnitude</u> is the most intuitive, it has limitations. For one, **negative numbers in sign-magnitude cannot be directly used in calculations**. For example, in sign-magnitude, calculating $1 + (-2)$ results in $-3$, which is incorrect.
|
||||
<u>Sign-magnitude</u>, although the most intuitive, has some limitations. On one hand, **the sign-magnitude of negative numbers cannot be directly used in operations**. For example, calculating $1 + (-2)$ in sign-magnitude yields $-3$, which is clearly incorrect.
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -35,20 +35,20 @@ $$
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
To address this, computers introduced the <u>one's complement</u>. If we convert to one's complement and calculate $1 + (-2)$, then convert the result back to sign-magnitude, we get the correct result of $-1$.
|
||||
To solve this problem, computers introduced <u>1's complement</u>. If we first convert sign-magnitude to 1's complement and calculate $1 + (-2)$ in 1's complement, then convert the result back to sign-magnitude, we can obtain the correct result of $-1$.
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
& 1 + (-2) \newline
|
||||
& \rightarrow 0000 \; 0001 \; \text{(Sign-magnitude)} + 1000 \; 0010 \; \text{(Sign-magnitude)} \newline
|
||||
& = 0000 \; 0001 \; \text{(One's complement)} + 1111 \; 1101 \; \text{(One's complement)} \newline
|
||||
& = 1111 \; 1110 \; \text{(One's complement)} \newline
|
||||
& = 0000 \; 0001 \; \text{(1's complement)} + 1111 \; 1101 \; \text{(1's complement)} \newline
|
||||
& = 1111 \; 1110 \; \text{(1's complement)} \newline
|
||||
& = 1000 \; 0001 \; \text{(Sign-magnitude)} \newline
|
||||
& \rightarrow -1
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Additionally, **there are two representations of zero in sign-magnitude**: $+0$ and $-0$. This means two different binary encodings for zero, which could lead to ambiguity. For example, in conditional checks, not differentiating between positive and negative zero might result in incorrect outcomes. Addressing this ambiguity would require additional checks, potentially reducing computational efficiency.
|
||||
On the other hand, **the sign-magnitude of the number zero has two representations, $+0$ and $-0$**. This means that the number zero corresponds to two different binary encodings, which may cause ambiguity. For example, in conditional judgments, if we don't distinguish between positive zero and negative zero, it may lead to incorrect judgment results. If we want to handle the ambiguity of positive and negative zero, we need to introduce additional judgment operations, which may reduce the computational efficiency of the computer.
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -57,67 +57,67 @@ $$
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Like sign-magnitude, one's complement also suffers from the positive and negative zero ambiguity. Therefore, computers further introduced the <u>two's complement</u>. Let's observe the conversion process for negative zero in sign-magnitude, one's complement, and two's complement:
|
||||
Like sign-magnitude, 1's complement also has the problem of positive and negative zero ambiguity. Therefore, computers further introduced <u>2's complement</u>. Let's first observe the conversion process of negative zero from sign-magnitude to 1's complement to 2's complement:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
-0 \rightarrow \; & 1000 \; 0000 \; \text{(Sign-magnitude)} \newline
|
||||
= \; & 1111 \; 1111 \; \text{(One's complement)} \newline
|
||||
= 1 \; & 0000 \; 0000 \; \text{(Two's complement)} \newline
|
||||
= \; & 1111 \; 1111 \; \text{(1's complement)} \newline
|
||||
= 1 \; & 0000 \; 0000 \; \text{(2's complement)} \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
Adding $1$ to the one's complement of negative zero produces a carry, but with `byte` length being only 8 bits, the carried-over $1$ to the 9th bit is discarded. Therefore, **the two's complement of negative zero is $0000 \; 0000$**, the same as positive zero, thus resolving the ambiguity.
|
||||
Adding $1$ to the 1's complement of negative zero produces a carry, but since the `byte` type has a length of only 8 bits, the $1$ that overflows to the 9th bit is discarded. That is to say, **the 2's complement of negative zero is $0000 \; 0000$, which is the same as the 2's complement of positive zero**. This means that in 2's complement representation, there is only one zero, and the positive and negative zero ambiguity is thus resolved.
|
||||
|
||||
One last puzzle is the $[-128, 127]$ range for `byte`, with an additional negative number, $-128$. We observe that for the interval $[-127, +127]$, all integers have corresponding sign-magnitude, one's complement, and two's complement, allowing for mutual conversion between them.
|
||||
One last question remains: the range of the `byte` type is $[-128, 127]$, and how is the extra negative number $-128$ obtained? We notice that all integers in the interval $[-127, +127]$ have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other.
|
||||
|
||||
However, **the two's complement $1000 \; 0000$ is an exception without a corresponding sign-magnitude**. According to the conversion method, its sign-magnitude would be $0000 \; 0000$, indicating zero. This presents a contradiction because its two's complement should represent itself. Computers designate this special two's complement $1000 \; 0000$ as representing $-128$. In fact, the calculation of $(-1) + (-127)$ in two's complement results in $-128$.
|
||||
However, **the 2's complement $1000 \; 0000$ is an exception, and it does not have a corresponding sign-magnitude**. According to the conversion method, we get that the sign-magnitude of this 2's complement is $0000 \; 0000$. This is clearly contradictory because this sign-magnitude represents the number $0$, and its 2's complement should be itself. The computer specifies that this special 2's complement $1000 \; 0000$ represents $-128$. In fact, the result of calculating $(-1) + (-127)$ in 2's complement is $-128$.
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
& (-127) + (-1) \newline
|
||||
& \rightarrow 1111 \; 1111 \; \text{(Sign-magnitude)} + 1000 \; 0001 \; \text{(Sign-magnitude)} \newline
|
||||
& = 1000 \; 0000 \; \text{(One's complement)} + 1111 \; 1110 \; \text{(One's complement)} \newline
|
||||
& = 1000 \; 0001 \; \text{(Two's complement)} + 1111 \; 1111 \; \text{(Two's complement)} \newline
|
||||
& = 1000 \; 0000 \; \text{(Two's complement)} \newline
|
||||
& = 1000 \; 0000 \; \text{(1's complement)} + 1111 \; 1110 \; \text{(1's complement)} \newline
|
||||
& = 1000 \; 0001 \; \text{(2's complement)} + 1111 \; 1111 \; \text{(2's complement)} \newline
|
||||
& = 1000 \; 0000 \; \text{(2's complement)} \newline
|
||||
& \rightarrow -128
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
As you might have noticed, all these calculations are additions, hinting at an important fact: **computers' internal hardware circuits are primarily designed around addition operations**. This is because addition is simpler to implement in hardware compared to other operations like multiplication, division, and subtraction, allowing for easier parallelization and faster computation.
|
||||
You may have noticed that all the above calculations are addition operations. This hints at an important fact: **the hardware circuits inside computers are mainly designed based on addition operations**. This is because addition operations are simpler to implement in hardware compared to other operations (such as multiplication, division, and subtraction), easier to parallelize, and have faster operation speeds.
|
||||
|
||||
It's important to note that this doesn't mean computers can only perform addition. **By combining addition with basic logical operations, computers can execute a variety of other mathematical operations**. For example, the subtraction $a - b$ can be translated into $a + (-b)$; multiplication and division can be translated into multiple additions or subtractions.
|
||||
Please note that this does not mean that computers can only perform addition. **By combining addition with some basic logical operations, computers can implement various other mathematical operations**. For example, calculating the subtraction $a - b$ can be converted to calculating the addition $a + (-b)$; calculating multiplication and division can be converted to calculating multiple additions or subtractions.
|
||||
|
||||
We can now summarize the reason for using two's complement in computers: with two's complement representation, computers can use the same circuits and operations to handle both positive and negative number addition, eliminating the need for special hardware circuits for subtraction and avoiding the ambiguity of positive and negative zero. This greatly simplifies hardware design and enhances computational efficiency.
|
||||
Now we can summarize the reasons why computers use 2's complement: based on 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without the need to design special hardware circuits to handle subtraction, and without the need to specially handle the ambiguity problem of positive and negative zero. This greatly simplifies hardware design and improves operational efficiency.
|
||||
|
||||
The design of two's complement is quite ingenious, and due to space constraints, we'll stop here. Interested readers are encouraged to explore further.
|
||||
The design of 2's complement is very ingenious. Due to space limitations, we will stop here. Interested readers are encouraged to explore further.
|
||||
|
||||
## 3.3.2 Floating-point number encoding
|
||||
## 3.3.2 Floating-Point Number Encoding
|
||||
|
||||
You might have noticed something intriguing: despite having the same length of 4 bytes, why does a `float` have a much larger range of values compared to an `int`? This seems counterintuitive, as one would expect the range to shrink for `float` since it needs to represent fractions.
|
||||
Careful readers may have noticed: `int` and `float` have the same length, both are 4 bytes, but why does `float` have a much larger range than `int`? This is very counterintuitive because it stands to reason that `float` needs to represent decimals, so the range should be smaller.
|
||||
|
||||
In fact, **this is due to the different representation method used by floating-point numbers (`float`)**. Let's consider a 32-bit binary number as:
|
||||
In fact, **this is because floating-point number `float` uses a different representation method**. Let's denote a 32-bit binary number as:
|
||||
|
||||
$$
|
||||
b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
|
||||
$$
|
||||
|
||||
According to the IEEE 754 standard, a 32-bit `float` consists of the following three parts:
|
||||
According to the IEEE 754 standard, a 32-bit `float` consists of the following three parts.
|
||||
|
||||
- Sign bit $\mathrm{S}$: Occupies 1 bit, corresponding to $b_{31}$.
|
||||
- Exponent bit $\mathrm{E}$: Occupies 8 bits, corresponding to $b_{30} b_{29} \ldots b_{23}$.
|
||||
- Fraction bit $\mathrm{N}$: Occupies 23 bits, corresponding to $b_{22} b_{21} \ldots b_0$.
|
||||
- Sign bit $\mathrm{S}$: occupies 1 bit, corresponding to $b_{31}$.
|
||||
- Exponent bit $\mathrm{E}$: occupies 8 bits, corresponding to $b_{30} b_{29} \ldots b_{23}$.
|
||||
- Fraction bit $\mathrm{N}$: occupies 23 bits, corresponding to $b_{22} b_{21} \ldots b_0$.
|
||||
|
||||
The value of a binary `float` number is calculated as:
|
||||
The calculation method for the value corresponding to the binary `float` is:
|
||||
|
||||
$$
|
||||
\text{val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2 - 127} \times \left(1 . b_{22} b_{21} \ldots b_0\right)_2
|
||||
\text {val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2-127} \times\left(1 . b_{22} b_{21} \ldots b_0\right)_2
|
||||
$$
|
||||
|
||||
Converted to a decimal formula, this becomes:
|
||||
Converted to decimal, the calculation formula is:
|
||||
|
||||
$$
|
||||
\text{val} = (-1)^{\mathrm{S}} \times 2^{\mathrm{E} - 127} \times (1 + \mathrm{N})
|
||||
\text {val}=(-1)^{\mathrm{S}} \times 2^{\mathrm{E} -127} \times (1 + \mathrm{N})
|
||||
$$
|
||||
|
||||
The range of each component is:
|
||||
@@ -125,23 +125,23 @@ The range of each component is:
|
||||
$$
|
||||
\begin{aligned}
|
||||
\mathrm{S} \in & \{ 0, 1\}, \quad \mathrm{E} \in \{ 1, 2, \dots, 254 \} \newline
|
||||
(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} \times 2^{-i}) \subset [1, 2 - 2^{-23}]
|
||||
(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} 2^{-i}) \subset [1, 2 - 2^{-23}]
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 3-5 Example calculation of a float in IEEE 754 standard </p>
|
||||
<p align="center"> Figure 3-5 Calculation example of float under IEEE 754 standard </p>
|
||||
|
||||
Observing Figure 3-5, given an example data $\mathrm{S} = 0$, $\mathrm{E} = 124$, $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$, we have:
|
||||
Observing Figure 3-5, given example data $\mathrm{S} = 0$, $\mathrm{E} = 124$, $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$, we have:
|
||||
|
||||
$$
|
||||
\text{val} = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
|
||||
\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
|
||||
$$
|
||||
|
||||
Now we can answer the initial question: **The representation of `float` includes an exponent bit, leading to a much larger range than `int`**. Based on the above calculation, the maximum positive number representable by `float` is approximately $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$, and the minimum negative number is obtained by switching the sign bit.
|
||||
Now we can answer the initial question: **the representation of `float` includes an exponent bit, resulting in a range far greater than `int`**. According to the above calculation, the maximum positive number that `float` can represent is $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$, and the minimum negative number can be obtained by switching the sign bit.
|
||||
|
||||
**However, the trade-off for `float`'s expanded range is a sacrifice in precision**. The integer type `int` uses all 32 bits to represent the number, with values evenly distributed; but due to the exponent bit, the larger the value of a `float`, the greater the difference between adjacent numbers.
|
||||
**Although floating-point number `float` expands the range, its side effect is sacrificing precision**. The integer type `int` uses all 32 bits to represent numbers, and the numbers are evenly distributed; however, due to the existence of the exponent bit, the larger the value of floating-point number `float`, the larger the difference between two adjacent numbers tends to be.
|
||||
|
||||
As shown in Table 3-2, exponent bits $\mathrm{E} = 0$ and $\mathrm{E} = 255$ have special meanings, **used to represent zero, infinity, $\mathrm{NaN}$, etc.**
|
||||
|
||||
@@ -151,12 +151,12 @@ As shown in Table 3-2, exponent bits $\mathrm{E} = 0$ and $\mathrm{E} = 255$ hav
|
||||
|
||||
| Exponent Bit E | Fraction Bit $\mathrm{N} = 0$ | Fraction Bit $\mathrm{N} \ne 0$ | Calculation Formula |
|
||||
| ------------------ | ----------------------------- | ------------------------------- | ---------------------------------------------------------------------- |
|
||||
| $0$ | $\pm 0$ | Subnormal Numbers | $(-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})$ |
|
||||
| $1, 2, \dots, 254$ | Normal Numbers | Normal Numbers | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ |
|
||||
| $0$ | $\pm 0$ | Subnormal Number | $(-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})$ |
|
||||
| $1, 2, \dots, 254$ | Normal Number | Normal Number | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ |
|
||||
| $255$ | $\pm \infty$ | $\mathrm{NaN}$ | |
|
||||
|
||||
</div>
|
||||
|
||||
It's worth noting that subnormal numbers significantly improve the precision of floating-point numbers. The smallest positive normal number is $2^{-126}$, and the smallest positive subnormal number is $2^{-126} \times 2^{-23}$.
|
||||
It is worth noting that subnormal numbers significantly improve the precision of floating-point numbers. The smallest positive normal number is $2^{-126}$, and the smallest positive subnormal number is $2^{-126} \times 2^{-23}$.
|
||||
|
||||
Double-precision `double` also uses a similar representation method to `float`, which is not elaborated here for brevity.
|
||||
Double-precision `double` also uses a representation method similar to `float`, which will not be elaborated here.
|
||||
|
||||
@@ -4,67 +4,67 @@ comments: true
|
||||
|
||||
# 3.5 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
- Data structures can be categorized from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data, while physical structure describes how data is stored in memory.
|
||||
- Frequently used logical structures include linear structures, trees, and networks. We usually divide data structures into linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures.
|
||||
- When a program is running, data is stored in memory. Each memory space has a corresponding address, and the program accesses data through these addresses.
|
||||
- Physical structures can be divided into continuous space storage (arrays) and discrete space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both.
|
||||
- The basic data types in computers include integers (`byte`, `short`, `int`, `long`), floating-point numbers (`float`, `double`), characters (`char`), and booleans (`bool`). The value range of a data type depends on its size and representation.
|
||||
- Sign-magnitude, 1's complement, 2's complement are three methods of encoding integers in computers, and they can be converted into each other. The most significant bit of the sign-magnitude is the sign bit, and the remaining bits represent the value of the number.
|
||||
- Integers are encoded by 2's complement in computers. The benefits of this representation include (i) the computer can unify the addition of positive and negative integers, (ii) no need to design special hardware circuits for subtraction, and (iii) no ambiguity of positive and negative zero.
|
||||
- The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bit, the range of floating-point numbers is much greater than that of integers, but at the cost of precision.
|
||||
- ASCII is the earliest English character set, with 1 byte in length and a total of 127 characters. GBK is a popular Chinese character set, which includes more than 20,000 Chinese characters. Unicode aims to provide a complete character set standard that includes characters from various languages in the world, thus solving the garbled character problem caused by inconsistent character encoding methods.
|
||||
- UTF-8 is the most popular and general Unicode encoding method. It is a variable-length encoding method with good scalability and space efficiency. UTF-16 and UTF-32 are fixed-length encoding methods. When encoding Chinese characters, UTF-16 takes up less space than UTF-8. Programming languages like Java and C# use UTF-16 encoding by default.
|
||||
- Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory.
|
||||
- Common logical structures include linear, tree, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures.
|
||||
- When a program runs, data is stored in computer memory. Each memory space has a corresponding memory address, and the program accesses data through these memory addresses.
|
||||
- Physical structures are primarily divided into contiguous space storage (arrays) and dispersed space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both.
|
||||
- Basic data types in computers include integers `byte`, `short`, `int`, `long`, floating-point numbers `float`, `double`, characters `char`, and booleans `bool`. Their value ranges depend on the size of space they occupy and their representation method.
|
||||
- Sign-magnitude, 1's complement, and 2's complement are three methods for encoding numbers in computers, and they can be converted into each other. The most significant bit of sign-magnitude is the sign bit, and the remaining bits represent the value of the number.
|
||||
- Integers are stored in computers in 2's complement form. Under 2's complement representation, computers can treat the addition of positive and negative numbers uniformly, without needing to design special hardware circuits for subtraction, and there is no ambiguity of positive and negative zero.
|
||||
- The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bits, the range of floating-point numbers is much larger than that of integers, at the cost of sacrificing precision.
|
||||
- ASCII is the earliest English character set, with a length of 1 byte, containing a total of 127 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods.
|
||||
- UTF-8 is the most popular Unicode encoding method, with excellent universality. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are fixed-length encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default.
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**: Why does a hash table contain both linear and non-linear data structures?
|
||||
**Q**: Why do hash tables contain both linear and non-linear data structures?
|
||||
|
||||
The underlying structure of a hash table is an array. To resolve hash collisions, we may use "chaining" (discussed in a later section, "Hash collision"): each bucket in the array points to a linked list, which may transform into a tree (usually a red-black tree) when its length is larger than a certain threshold.
|
||||
From a storage perspective, the underlying structure of a hash table is an array, where each bucket might contain a value, a linked list, or a tree. Therefore, hash tables may contain both linear data structures (arrays, linked lists) and non-linear data structures (trees).
|
||||
The underlying structure of a hash table is an array. To resolve hash collisions, we may use "chaining" (discussed in the subsequent "Hash Collision" section): each bucket in the array points to a linked list, which may be converted to a tree (usually a red-black tree) when the list length exceeds a certain threshold.
|
||||
|
||||
From a storage perspective, the underlying structure of a hash table is an array, where each bucket slot may contain a value, a linked list, or a tree. Therefore, hash tables may contain both linear data structures (arrays, linked lists) and non-linear data structures (trees).
|
||||
|
||||
**Q**: Is the length of the `char` type 1 byte?
|
||||
|
||||
The length of the `char` type is determined by the encoding method of the programming language. For example, Java, JavaScript, TypeScript, and C# all use UTF-16 encoding (to save Unicode code points), so the length of the `char` type is 2 bytes.
|
||||
The length of the `char` type is determined by the encoding method used by the programming language. For example, Java, JavaScript, TypeScript, and C# all use UTF-16 encoding (to store Unicode code points), so the `char` type has a length of 2 bytes.
|
||||
|
||||
**Q**: Is there any ambiguity when we refer to array-based data structures as "static data structures"? The stack can also perform "dynamic" operations such as popping and pushing.
|
||||
**Q**: Is there ambiguity in referring to array-based data structures as "static data structures"? Stacks can also perform "dynamic" operations such as push and pop.
|
||||
|
||||
The stack can implement dynamic data operations, but the data structure is still "static" (the length is fixed). Although array-based data structures can dynamically add or remove elements, their capacity is fixed. If the stack size exceeds the pre-allocated size, then the old array will be copied into a newly created and larger array.
|
||||
Stacks can indeed implement dynamic data operations, but the data structure is still "static" (fixed length). Although array-based data structures can dynamically add or remove elements, their capacity is fixed. If the data volume exceeds the pre-allocated size, a new larger array needs to be created, and the contents of the old array must be copied to the new array.
|
||||
|
||||
**Q**: When building a stack (queue), its size is not specified, so why are they "static data structures"?
|
||||
**Q**: When constructing a stack (queue), its size is not specified. Why are they "static data structures"?
|
||||
|
||||
In high-level programming languages, we do not need to manually specify the initial capacity of stacks (queues); this task is automatically completed within the class. For example, the initial capacity of Java's `ArrayList` is usually 10. Furthermore, the expansion operation is also completed automatically. See the subsequent "List" chapter for details.
|
||||
In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); this work is automatically completed within the class. For example, the initial capacity of Java's `ArrayList` is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent "List" section for details.
|
||||
|
||||
**Q**:The method of converting the sign-magnitude to the 2's complement is "first negate and then add 1", so converting the 2's complement to the sign-magnitude should be its inverse operation "first subtract 1 and then negate".
|
||||
However, the 2's complement can also be converted to the sign-magnitude through "first negate and then add 1", why is this?
|
||||
**Q**: The method of converting sign-magnitude to 2's complement is "first negate then add 1". So converting 2's complement to sign-magnitude should be the inverse operation "first subtract 1 then negate". However, 2's complement can also be converted to sign-magnitude through "first negate then add 1". Why is this?
|
||||
|
||||
**A**:This is because the mutual conversion between the sign-magnitude and the 2's complement is equivalent to computing the "complement". We first define the complement: assuming $a + b = c$, then we say that $a$ is the complement of $b$ to $c$, and vice versa, $b$ is the complement of $a$ to $c$.
|
||||
This is because the mutual conversion between sign-magnitude and 2's complement is actually the process of computing the "complement". Let us first define the complement: assuming $a + b = c$, then we say that $a$ is the complement of $b$ to $c$, and conversely, $b$ is the complement of $a$ to $c$.
|
||||
|
||||
Given a binary number $0010$ with length $n = 4$, if this number is the sign-magnitude (ignoring the sign bit), then its 2's complement can be obtained by "first negating and then adding 1":
|
||||
Given an $n = 4$ bit binary number $0010$, if we treat this number as sign-magnitude (ignoring the sign bit), then its 2's complement can be obtained through "first negate then add 1":
|
||||
|
||||
$$
|
||||
0010 \rightarrow 1101 \rightarrow 1110
|
||||
$$
|
||||
|
||||
Observe that the sum of the sign-magnitude and the 2's complement is $0010 + 1110 = 10000$, i.e., the 2's complement $1110$ is the "complement" of the sign-magnitude $0010$ to $10000$. **This means that the above "first negate and then add 1" is equivalent to computing the complement to $10000$**.
|
||||
We find that the sum of sign-magnitude and 2's complement is $0010 + 1110 = 10000$, which means the 2's complement $1110$ is the "complement" of sign-magnitude $0010$ to $10000$. **This means the above "first negate then add 1" is actually the process of computing the complement to $10000$**.
|
||||
|
||||
So, what is the "complement" of $1110$ to $10000$? We can still compute it by "negating first and then adding 1":
|
||||
So, what is the "complement" of 2's complement $1110$ to $10000$? We can still use "first negate then add 1" to obtain it:
|
||||
|
||||
$$
|
||||
1110 \rightarrow 0001 \rightarrow 0010
|
||||
$$
|
||||
|
||||
In other words, the sign-magnitude and the 2's complement are each other's "complement" to $10000$, so "sign-magnitude to 2's complement" and "2's complement to sign-magnitude" can be implemented with the same operation (first negate and then add 1).
|
||||
In other words, sign-magnitude and 2's complement are each other's "complement" to $10000$, so "sign-magnitude to 2's complement" and "2's complement to sign-magnitude" can be implemented using the same operation (first negate then add 1).
|
||||
|
||||
Of course, we can also use the inverse operation of "first negate and then add 1" to find the sign-magnitude of the 2's complement $1110$, that is, "first subtract 1 and then negate":
|
||||
Of course, we can also use the inverse operation to find the sign-magnitude of 2's complement $1110$, that is, "first subtract 1 then negate":
|
||||
|
||||
$$
|
||||
1110 \rightarrow 1101 \rightarrow 0010
|
||||
$$
|
||||
|
||||
To sum up, "first negate and then add 1" and "first subtract 1 and then negate" are both computing the complement to $10000$, and they are equivalent.
|
||||
In summary, both "first negate then add 1" and "first subtract 1 then negate" are computing the complement to $10000$, and they are equivalent.
|
||||
|
||||
Essentially, the "negate" operation is actually to find the complement to $1111$ (because `sign-magnitude + 1's complement = 1111` always holds); and the 1's complement plus 1 is equal to the 2's complement to $10000$.
|
||||
Essentially, the "negate" operation is actually finding the complement to $1111$ (because `sign-magnitude + 1's complement = 1111` always holds); and adding 1 to the 1's complement yields the 2's complement, which is the complement to $10000$.
|
||||
|
||||
We take $n = 4$ as an example in the above, and it can be generalized to any binary number with any number of digits.
|
||||
The above uses $n = 4$ as an example, and it can be generalized to binary numbers of any number of bits.
|
||||
|
||||
@@ -2,47 +2,47 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.2 Divide and conquer search strategy
|
||||
# 12.2 Divide and Conquer Search Strategy
|
||||
|
||||
We have learned that search algorithms fall into two main categories.
|
||||
We have already learned that search algorithms are divided into two major categories.
|
||||
|
||||
- **Brute-force search**: It is implemented by traversing the data structure, with a time complexity of $O(n)$.
|
||||
- **Adaptive search**: It utilizes a unique data organization form or prior information, and its time complexity can reach $O(\log n)$ or even $O(1)$.
|
||||
- **Brute-force search**: Implemented by traversing the data structure, with a time complexity of $O(n)$.
|
||||
- **Adaptive search**: Utilizes unique data organization forms or prior information, with time complexity reaching $O(\log n)$ or even $O(1)$.
|
||||
|
||||
In fact, **search algorithms with a time complexity of $O(\log n)$ are usually based on the divide-and-conquer strategy**, such as binary search and trees.
|
||||
In fact, **search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy**, such as binary search and trees.
|
||||
|
||||
- Each step of binary search divides the problem (searching for a target element in an array) into a smaller problem (searching for the target element in half of the array), continuing until the array is empty or the target element is found.
|
||||
- Trees represent the divide-and-conquer idea, where in data structures like binary search trees, AVL trees, and heaps, the time complexity of various operations is $O(\log n)$.
|
||||
- Trees are representative of the divide and conquer idea. In data structures such as binary search trees, AVL trees, and heaps, the time complexity of various operations is $O(\log n)$.
|
||||
|
||||
The divide-and-conquer strategy of binary search is as follows.
|
||||
The divide and conquer strategy of binary search is as follows.
|
||||
|
||||
- **The problem can be divided**: Binary search recursively divides the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element.
|
||||
- **Subproblems are independent**: In binary search, each round handles one subproblem, unaffected by other subproblems.
|
||||
- **The solutions of subproblems do not need to be merged**: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved.
|
||||
- **The problem can be decomposed**: Binary search recursively decomposes the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element.
|
||||
- **Subproblems are independent**: In binary search, each round only processes one subproblem, which is not affected by other subproblems.
|
||||
- **Solutions of subproblems do not need to be merged**: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved.
|
||||
|
||||
Divide-and-conquer can enhance search efficiency because brute-force search can only eliminate one option per round, **whereas divide-and-conquer can eliminate half of the options**.
|
||||
Divide and conquer can improve search efficiency because brute-force search can only eliminate one option per round, **while divide and conquer search can eliminate half of the options per round**.
|
||||
|
||||
### 1. Implementing binary search based on divide-and-conquer
|
||||
### 1. Implementing Binary Search Based on Divide and Conquer
|
||||
|
||||
In previous chapters, binary search was implemented based on iteration. Now, we implement it based on divide-and-conquer (recursion).
|
||||
In previous sections, binary search was implemented based on iteration. Now we implement it based on divide and conquer (recursion).
|
||||
|
||||
!!! question
|
||||
|
||||
Given an ordered array `nums` of length $n$, where all elements are unique, please find the element `target`.
|
||||
Given a sorted array `nums` of length $n$, where all elements are unique, find the element `target`.
|
||||
|
||||
From a divide-and-conquer perspective, we denote the subproblem corresponding to the search interval $[i, j]$ as $f(i, j)$.
|
||||
From a divide and conquer perspective, we denote the subproblem corresponding to the search interval $[i, j]$ as $f(i, j)$.
|
||||
|
||||
Starting from the original problem $f(0, n-1)$, perform the binary search through the following steps.
|
||||
Starting from the original problem $f(0, n-1)$, perform binary search through the following steps.
|
||||
|
||||
1. Calculate the midpoint $m$ of the search interval $[i, j]$, and use it to eliminate half of the search interval.
|
||||
2. Recursively solve the subproblem reduced by half in size, which could be $f(i, m-1)$ or $f(m+1, j)$.
|
||||
3. Repeat steps `1.` and `2.`, until `target` is found or the interval is empty and returns.
|
||||
3. Repeat steps `1.` and `2.` until `target` is found or the interval is empty and return.
|
||||
|
||||
Figure 12-4 shows the divide-and-conquer process of binary search for element $6$ in an array.
|
||||
Figure 12-4 shows the divide and conquer process of binary search for element $6$ in an array.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-4 The divide-and-conquer process of binary search </p>
|
||||
<p align="center"> Figure 12-4 Divide and conquer process of binary search </p>
|
||||
|
||||
In the implementation code, we declare a recursive function `dfs()` to solve the problem $f(i, j)$:
|
||||
|
||||
@@ -51,25 +51,25 @@ In the implementation code, we declare a recursive function `dfs()` to solve the
|
||||
```python title="binary_search_recur.py"
|
||||
def dfs(nums: list[int], target: int, i: int, j: int) -> int:
|
||||
"""Binary search: problem f(i, j)"""
|
||||
# If the interval is empty, indicating no target element, return -1
|
||||
# If the interval is empty, it means there is no target element, return -1
|
||||
if i > j:
|
||||
return -1
|
||||
# Calculate midpoint index m
|
||||
# Calculate the midpoint index m
|
||||
m = (i + j) // 2
|
||||
if nums[m] < target:
|
||||
# Recursive subproblem f(m+1, j)
|
||||
# Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j)
|
||||
elif nums[m] > target:
|
||||
# Recursive subproblem f(i, m-1)
|
||||
# Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1)
|
||||
else:
|
||||
# Found the target element, thus return its index
|
||||
# Found the target element, return its index
|
||||
return m
|
||||
|
||||
def binary_search(nums: list[int], target: int) -> int:
|
||||
"""Binary search"""
|
||||
n = len(nums)
|
||||
# Solve problem f(0, n-1)
|
||||
# Solve the problem f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1)
|
||||
```
|
||||
|
||||
@@ -78,20 +78,20 @@ In the implementation code, we declare a recursive function `dfs()` to solve the
|
||||
```cpp title="binary_search_recur.cpp"
|
||||
/* Binary search: problem f(i, j) */
|
||||
int dfs(vector<int> &nums, int target, int i, int j) {
|
||||
// If the interval is empty, indicating no target element, return -1
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// Calculate midpoint index m
|
||||
int m = i + (j - i) / 2;
|
||||
// Calculate the midpoint index m
|
||||
int m = (i + j) / 2;
|
||||
if (nums[m] < target) {
|
||||
// Recursive subproblem f(m+1, j)
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// Recursive subproblem f(i, m-1)
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// Found the target element, thus return its index
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ In the implementation code, we declare a recursive function `dfs()` to solve the
|
||||
/* Binary search */
|
||||
int binarySearch(vector<int> &nums, int target) {
|
||||
int n = nums.size();
|
||||
// Solve problem f(0, n-1)
|
||||
// Solve the problem f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
@@ -109,20 +109,20 @@ In the implementation code, we declare a recursive function `dfs()` to solve the
|
||||
```java title="binary_search_recur.java"
|
||||
/* Binary search: problem f(i, j) */
|
||||
int dfs(int[] nums, int target, int i, int j) {
|
||||
// If the interval is empty, indicating no target element, return -1
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// Calculate midpoint index m
|
||||
int m = i + (j - i) / 2;
|
||||
// Calculate the midpoint index m
|
||||
int m = (i + j) / 2;
|
||||
if (nums[m] < target) {
|
||||
// Recursive subproblem f(m+1, j)
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// Recursive subproblem f(i, m-1)
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// Found the target element, thus return its index
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
@@ -130,7 +130,7 @@ In the implementation code, we declare a recursive function `dfs()` to solve the
|
||||
/* Binary search */
|
||||
int binarySearch(int[] nums, int target) {
|
||||
int n = nums.length;
|
||||
// Solve problem f(0, n-1)
|
||||
// Solve the problem f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
@@ -138,87 +138,314 @@ In the implementation code, we declare a recursive function `dfs()` to solve the
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_search_recur.cs"
|
||||
[class]{binary_search_recur}-[func]{DFS}
|
||||
/* Binary search: problem f(i, j) */
|
||||
int DFS(int[] nums, int target, int i, int j) {
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// Calculate the midpoint index m
|
||||
int m = (i + j) / 2;
|
||||
if (nums[m] < target) {
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return DFS(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return DFS(nums, target, i, m - 1);
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{binary_search_recur}-[func]{BinarySearch}
|
||||
/* Binary search */
|
||||
int BinarySearch(int[] nums, int target) {
|
||||
int n = nums.Length;
|
||||
// Solve the problem f(0, n-1)
|
||||
return DFS(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search_recur.go"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Binary search: problem f(i, j) */
|
||||
func dfs(nums []int, target, i, j int) int {
|
||||
// If interval is empty, indicating no target element, return -1
|
||||
if i > j {
|
||||
return -1
|
||||
}
|
||||
// Calculate midpoint index
|
||||
m := i + ((j - i) >> 1)
|
||||
// Compare midpoint with target element
|
||||
if nums[m] < target {
|
||||
// If smaller, recurse on right half of array
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m+1, j)
|
||||
} else if nums[m] > target {
|
||||
// If larger, recurse on left half of array
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m-1)
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search */
|
||||
func binarySearch(nums []int, target int) int {
|
||||
n := len(nums)
|
||||
return dfs(nums, target, 0, n-1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search_recur.swift"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Binary search: problem f(i, j) */
|
||||
func dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int {
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if i > j {
|
||||
return -1
|
||||
}
|
||||
// Calculate the midpoint index m
|
||||
let m = (i + j) / 2
|
||||
if nums[m] < target {
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums: nums, target: target, i: m + 1, j: j)
|
||||
} else if nums[m] > target {
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums: nums, target: target, i: i, j: m - 1)
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search */
|
||||
func binarySearch(nums: [Int], target: Int) -> Int {
|
||||
// Solve the problem f(0, n-1)
|
||||
dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_search_recur.js"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Binary search: problem f(i, j) */
|
||||
function dfs(nums, target, i, j) {
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// Calculate the midpoint index m
|
||||
const m = i + ((j - i) >> 1);
|
||||
if (nums[m] < target) {
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search */
|
||||
function binarySearch(nums, target) {
|
||||
const n = nums.length;
|
||||
// Solve the problem f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_search_recur.ts"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Binary search: problem f(i, j) */
|
||||
function dfs(nums: number[], target: number, i: number, j: number): number {
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// Calculate the midpoint index m
|
||||
const m = i + ((j - i) >> 1);
|
||||
if (nums[m] < target) {
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search */
|
||||
function binarySearch(nums: number[], target: number): number {
|
||||
const n = nums.length;
|
||||
// Solve the problem f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_search_recur.dart"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Binary search: problem f(i, j) */
|
||||
int dfs(List<int> nums, int target, int i, int j) {
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// Calculate the midpoint index m
|
||||
int m = (i + j) ~/ 2;
|
||||
if (nums[m] < target) {
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search */
|
||||
int binarySearch(List<int> nums, int target) {
|
||||
int n = nums.length;
|
||||
// Solve the problem f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search_recur.rs"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Binary search: problem f(i, j) */
|
||||
fn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 {
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if i > j {
|
||||
return -1;
|
||||
}
|
||||
let m: i32 = i + (j - i) / 2;
|
||||
if nums[m as usize] < target {
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if nums[m as usize] > target {
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{binary_search}
|
||||
/* Binary search */
|
||||
fn binary_search(nums: &[i32], target: i32) -> i32 {
|
||||
let n = nums.len() as i32;
|
||||
// Solve the problem f(0, n-1)
|
||||
dfs(nums, target, 0, n - 1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_search_recur.c"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Binary search: problem f(i, j) */
|
||||
int dfs(int nums[], int target, int i, int j) {
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if (i > j) {
|
||||
return -1;
|
||||
}
|
||||
// Calculate the midpoint index m
|
||||
int m = (i + j) / 2;
|
||||
if (nums[m] < target) {
|
||||
// Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j);
|
||||
} else if (nums[m] > target) {
|
||||
// Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1);
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search */
|
||||
int binarySearch(int nums[], int target, int numsSize) {
|
||||
int n = numsSize;
|
||||
// Solve the problem f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_search_recur.kt"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Binary search: problem f(i, j) */
|
||||
fun dfs(
|
||||
nums: IntArray,
|
||||
target: Int,
|
||||
i: Int,
|
||||
j: Int
|
||||
): Int {
|
||||
// If the interval is empty, it means there is no target element, return -1
|
||||
if (i > j) {
|
||||
return -1
|
||||
}
|
||||
// Calculate the midpoint index m
|
||||
val m = (i + j) / 2
|
||||
return if (nums[m] < target) {
|
||||
// Recursion subproblem f(m+1, j)
|
||||
dfs(nums, target, m + 1, j)
|
||||
} else if (nums[m] > target) {
|
||||
// Recursion subproblem f(i, m-1)
|
||||
dfs(nums, target, i, m - 1)
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
m
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search */
|
||||
fun binarySearch(nums: IntArray, target: Int): Int {
|
||||
val n = nums.size
|
||||
// Solve the problem f(0, n-1)
|
||||
return dfs(nums, target, 0, n - 1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_search_recur.rb"
|
||||
[class]{}-[func]{dfs}
|
||||
### Binary search: problem f(i, j) ###
|
||||
def dfs(nums, target, i, j)
|
||||
# If the interval is empty, it means there is no target element, return -1
|
||||
return -1 if i > j
|
||||
|
||||
# Calculate the midpoint index m
|
||||
m = (i + j) / 2
|
||||
|
||||
[class]{}-[func]{binary_search}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_recur.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{binarySearch}
|
||||
if nums[m] < target
|
||||
# Recursion subproblem f(m+1, j)
|
||||
return dfs(nums, target, m + 1, j)
|
||||
elsif nums[m] > target
|
||||
# Recursion subproblem f(i, m-1)
|
||||
return dfs(nums, target, i, m - 1)
|
||||
else
|
||||
# Found the target element, return its index
|
||||
return m
|
||||
end
|
||||
end
|
||||
|
||||
### Binary search ###
|
||||
def binary_search(nums, target)
|
||||
n = nums.length
|
||||
# Solve the problem f(0, n-1)
|
||||
dfs(nums, target, 0, n - 1)
|
||||
end
|
||||
```
|
||||
|
||||
@@ -2,74 +2,74 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.3 Building a binary tree problem
|
||||
# 12.3 Building a Binary Tree Problem
|
||||
|
||||
!!! question
|
||||
|
||||
Given the pre-order traversal `preorder` sequence and the in-order traversal `inorder` sequence of a binary tree, construct the binary tree and return its root node. Assume there are no duplicate node values in the binary tree (as shown in Figure 12-5).
|
||||
Given the preorder traversal `preorder` and inorder traversal `inorder` of a binary tree, construct the binary tree and return the root node of the binary tree. Assume there are no duplicate node values in the binary tree (as shown in Figure 12-5).
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-5 Example data for building a binary tree </p>
|
||||
|
||||
### 1. Determining if it is a divide-and-conquer problem
|
||||
### 1. Determining If It Is a Divide and Conquer Problem
|
||||
|
||||
The original problem of building a binary tree from the `preorder` and the `inorder` sequences is a typical divide-and-conquer problem.
|
||||
The original problem is defined as constructing a binary tree from `preorder` and `inorder`, which is a typical divide and conquer problem.
|
||||
|
||||
- **The problem can be decomposed**: From the perspective of divide-and-conquer, we can divide the original problem into two subproblems—building the left subtree and building the right subtree—plus one operation of initializing the root node. For each subtree (subproblem), we continue applying the same approach, partitioning it into smaller subtrees (subproblems), until reaching the smallest subproblem (an empty subtree).
|
||||
- **The subproblems are independent**: The left and right subtrees do not overlap. When building the left subtree, we only need the segments of the in-order and pre-order traversals that correspond to the left subtree. The same approach applies to the right subtree.
|
||||
- **Solutions to subproblems can be combined**: Once we have constructed the left and right subtrees (the subproblem solutions), we can attach them to the root node to obtain the solution to the original problem.
|
||||
- **The problem can be decomposed**: From a divide and conquer perspective, we can divide the original problem into two subproblems: constructing the left subtree and constructing the right subtree, plus one operation: initializing the root node. For each subtree (subproblem), we can still reuse the above division method, dividing it into smaller subtrees (subproblems) until the smallest subproblem (empty subtree) is reached.
|
||||
- **Subproblems are independent**: The left and right subtrees are independent of each other; there is no overlap between them. When constructing the left subtree, we only need to focus on the parts of the inorder and preorder traversals corresponding to the left subtree. The same applies to the right subtree.
|
||||
- **Solutions of subproblems can be merged**: Once we have the left and right subtrees (solutions of subproblems), we can link them to the root node to obtain the solution to the original problem.
|
||||
|
||||
### 2. How to divide the subtrees
|
||||
### 2. How to Divide Subtrees
|
||||
|
||||
Based on the above analysis, this problem can be solved using divide-and-conquer. **However, how do we use the pre-order traversal `preorder` sequence and the in-order traversal `inorder` sequence to divide the left and right subtrees?**
|
||||
Based on the above analysis, this problem can be solved using divide and conquer, **but how do we divide the left and right subtrees through the preorder traversal `preorder` and inorder traversal `inorder`**?
|
||||
|
||||
By definition, both the `preorder` and `inorder` sequences can be divided into three parts:
|
||||
According to the definition, both `preorder` and `inorder` can be divided into three parts.
|
||||
|
||||
- Pre-order traversal: `[ Root | Left Subtree | Right Subtree ]`. For example, in the figure, the tree corresponds to `[ 3 | 9 | 2 1 7 ]`.
|
||||
- In-order traversal: `[ Left Subtree | Root | Right Subtree ]`. For example, in the figure, the tree corresponds to `[ 9 | 3 | 1 2 7 ]`.
|
||||
- Preorder traversal: `[ Root Node | Left Subtree | Right Subtree ]`, for example, the tree in Figure 12-5 corresponds to `[ 3 | 9 | 2 1 7 ]`.
|
||||
- Inorder traversal: `[ Left Subtree | Root Node | Right Subtree ]`, for example, the tree in Figure 12-5 corresponds to `[ 9 | 3 | 1 2 7 ]`.
|
||||
|
||||
Using the data from the preceding figure, we can follow the steps shown in the next figure to obtain the division results:
|
||||
Using the data from the figure above as an example, we can obtain the division results through the steps shown in Figure 12-6.
|
||||
|
||||
1. The first element 3 in the pre-order traversal is the value of the root node.
|
||||
2. Find the index of the root node 3 in the `inorder` sequence, and use this index to split `inorder` into `[ 9 | 3 | 1 2 7 ]`.
|
||||
3. According to the split of the `inorder` sequence, it is straightforward to determine that the left and right subtrees contain 1 and 3 nodes, respectively, so we can split the `preorder` sequence into `[ 3 | 9 | 2 1 7 ]` accordingly.
|
||||
1. The first element 3 in the preorder traversal is the value of the root node.
|
||||
2. Find the index of root node 3 in `inorder`, and use this index to divide `inorder` into `[ 9 | 3 | 1 2 7 ]`.
|
||||
3. Based on the division result of `inorder`, it is easy to determine that the left and right subtrees have 1 and 3 nodes respectively, allowing us to divide `preorder` into `[ 3 | 9 | 2 1 7 ]`.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-6 Dividing the subtrees in pre-order and in-order traversals </p>
|
||||
<p align="center"> Figure 12-6 Dividing subtrees in preorder and inorder traversals </p>
|
||||
|
||||
### 3. Describing subtree ranges based on variables
|
||||
### 3. Describing Subtree Intervals Based on Variables
|
||||
|
||||
Based on the above division method, **we have now obtained the index ranges of the root, left subtree, and right subtree in the `preorder` and `inorder` sequences**. To describe these index ranges, we use several pointer variables.
|
||||
Based on the above division method, **we have obtained the index intervals of the root node, left subtree, and right subtree in `preorder` and `inorder`**. To describe these index intervals, we need to use several pointer variables.
|
||||
|
||||
- Let the index of the current tree's root node in the `preorder` sequence be denoted as $i$.
|
||||
- Let the index of the current tree's root node in the `inorder` sequence be denoted as $m$.
|
||||
- Let the index range of the current tree in the `inorder` sequence be denoted as $[l, r]$.
|
||||
- Denote the index of the current tree's root node in `preorder` as $i$.
|
||||
- Denote the index of the current tree's root node in `inorder` as $m$.
|
||||
- Denote the index interval of the current tree in `inorder` as $[l, r]$.
|
||||
|
||||
As shown in Table 12-1, these variables represent the root node’s index in the `preorder` sequence and the index ranges of the subtrees in the `inorder` sequence.
|
||||
As shown in Table 12-1, through these variables we can represent the index of the root node in `preorder` and the index intervals of the subtrees in `inorder`.
|
||||
|
||||
<p align="center"> Table 12-1 Indexes of the root node and subtrees in pre-order and in-order traversals </p>
|
||||
<p align="center"> Table 12-1 Indices of root node and subtrees in preorder and inorder traversals </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | Root node index in `preorder` | Subtree index range in `inorder` |
|
||||
| ------------- | ----------------------------- | ----------------------------------- |
|
||||
| Current tree | $i$ | $[l, r]$ |
|
||||
| Left subtree | $i + 1$ | $[l, m-1]$ |
|
||||
| Right subtree | $i + 1 + (m - l)$ | $[m+1, r]$ |
|
||||
| | Root node index in `preorder` | Subtree index interval in `inorder` |
|
||||
| ------------ | ----------------------------- | ----------------------------------- |
|
||||
| Current tree | $i$ | $[l, r]$ |
|
||||
| Left subtree | $i + 1$ | $[l, m-1]$ |
|
||||
| Right subtree| $i + 1 + (m - l)$ | $[m+1, r]$ |
|
||||
|
||||
</div>
|
||||
|
||||
Please note that $(m-l)$ in the right subtree root index represents "the number of nodes in the left subtree." It may help to consult Figure 12-7 for a clearer understanding.
|
||||
Please note that $(m-l)$ in the right subtree root node index means "the number of nodes in the left subtree". It is recommended to understand this in conjunction with Figure 12-7.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-7 Indexes of the root node and left and right subtrees </p>
|
||||
<p align="center"> Figure 12-7 Index interval representation of root node and left and right subtrees </p>
|
||||
|
||||
### 4. Code implementation
|
||||
### 4. Code Implementation
|
||||
|
||||
To improve the efficiency of querying $m$, we use a hash table `hmap` to store the mapping from elements in the `inorder` sequence to their indexes:
|
||||
To improve the efficiency of querying $m$, we use a hash table `hmap` to store the mapping from elements in the `inorder` array to their indices:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -81,24 +81,24 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t
|
||||
l: int,
|
||||
r: int,
|
||||
) -> TreeNode | None:
|
||||
"""Build binary tree: Divide and conquer"""
|
||||
# Terminate when subtree interval is empty
|
||||
"""Build binary tree: divide and conquer"""
|
||||
# Terminate when the subtree interval is empty
|
||||
if r - l < 0:
|
||||
return None
|
||||
# Initialize root node
|
||||
# Initialize the root node
|
||||
root = TreeNode(preorder[i])
|
||||
# Query m to divide left and right subtrees
|
||||
# Query m to divide the left and right subtrees
|
||||
m = inorder_map[preorder[i]]
|
||||
# Subproblem: build left subtree
|
||||
# Subproblem: build the left subtree
|
||||
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
|
||||
# Subproblem: build right subtree
|
||||
# Subproblem: build the right subtree
|
||||
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
|
||||
# Return root node
|
||||
# Return the root node
|
||||
return root
|
||||
|
||||
def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None:
|
||||
"""Build binary tree"""
|
||||
# Initialize hash table, storing in-order elements to indices mapping
|
||||
# Initialize hash map, storing the mapping from inorder elements to indices
|
||||
inorder_map = {val: i for i, val in enumerate(inorder)}
|
||||
root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1)
|
||||
return root
|
||||
@@ -107,26 +107,26 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t
|
||||
=== "C++"
|
||||
|
||||
```cpp title="build_tree.cpp"
|
||||
/* Build binary tree: Divide and conquer */
|
||||
/* Build binary tree: divide and conquer */
|
||||
TreeNode *dfs(vector<int> &preorder, unordered_map<int, int> &inorderMap, int i, int l, int r) {
|
||||
// Terminate when subtree interval is empty
|
||||
// Terminate when the subtree interval is empty
|
||||
if (r - l < 0)
|
||||
return NULL;
|
||||
// Initialize root node
|
||||
// Initialize the root node
|
||||
TreeNode *root = new TreeNode(preorder[i]);
|
||||
// Query m to divide left and right subtrees
|
||||
// Query m to divide the left and right subtrees
|
||||
int m = inorderMap[preorder[i]];
|
||||
// Subproblem: build left subtree
|
||||
// Subproblem: build the left subtree
|
||||
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// Subproblem: build right subtree
|
||||
// Subproblem: build the right subtree
|
||||
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// Return root node
|
||||
// Return the root node
|
||||
return root;
|
||||
}
|
||||
|
||||
/* Build binary tree */
|
||||
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
|
||||
// Initialize hash table, storing in-order elements to indices mapping
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
unordered_map<int, int> inorderMap;
|
||||
for (int i = 0; i < inorder.size(); i++) {
|
||||
inorderMap[inorder[i]] = i;
|
||||
@@ -139,26 +139,26 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t
|
||||
=== "Java"
|
||||
|
||||
```java title="build_tree.java"
|
||||
/* Build binary tree: Divide and conquer */
|
||||
/* Build binary tree: divide and conquer */
|
||||
TreeNode dfs(int[] preorder, Map<Integer, Integer> inorderMap, int i, int l, int r) {
|
||||
// Terminate when subtree interval is empty
|
||||
// Terminate when the subtree interval is empty
|
||||
if (r - l < 0)
|
||||
return null;
|
||||
// Initialize root node
|
||||
// Initialize the root node
|
||||
TreeNode root = new TreeNode(preorder[i]);
|
||||
// Query m to divide left and right subtrees
|
||||
// Query m to divide the left and right subtrees
|
||||
int m = inorderMap.get(preorder[i]);
|
||||
// Subproblem: build left subtree
|
||||
// Subproblem: build the left subtree
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// Subproblem: build right subtree
|
||||
// Subproblem: build the right subtree
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// Return root node
|
||||
// Return the root node
|
||||
return root;
|
||||
}
|
||||
|
||||
/* Build binary tree */
|
||||
TreeNode buildTree(int[] preorder, int[] inorder) {
|
||||
// Initialize hash table, storing in-order elements to indices mapping
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
Map<Integer, Integer> inorderMap = new HashMap<>();
|
||||
for (int i = 0; i < inorder.length; i++) {
|
||||
inorderMap.put(inorder[i], i);
|
||||
@@ -171,92 +171,348 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t
|
||||
=== "C#"
|
||||
|
||||
```csharp title="build_tree.cs"
|
||||
[class]{build_tree}-[func]{DFS}
|
||||
/* Build binary tree: divide and conquer */
|
||||
TreeNode? DFS(int[] preorder, Dictionary<int, int> inorderMap, int i, int l, int r) {
|
||||
// Terminate when the subtree interval is empty
|
||||
if (r - l < 0)
|
||||
return null;
|
||||
// Initialize the root node
|
||||
TreeNode root = new(preorder[i]);
|
||||
// Query m to divide the left and right subtrees
|
||||
int m = inorderMap[preorder[i]];
|
||||
// Subproblem: build the left subtree
|
||||
root.left = DFS(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// Subproblem: build the right subtree
|
||||
root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// Return the root node
|
||||
return root;
|
||||
}
|
||||
|
||||
[class]{build_tree}-[func]{BuildTree}
|
||||
/* Build binary tree */
|
||||
TreeNode? BuildTree(int[] preorder, int[] inorder) {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
Dictionary<int, int> inorderMap = [];
|
||||
for (int i = 0; i < inorder.Length; i++) {
|
||||
inorderMap.TryAdd(inorder[i], i);
|
||||
}
|
||||
TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="build_tree.go"
|
||||
[class]{}-[func]{dfsBuildTree}
|
||||
/* Build binary tree: divide and conquer */
|
||||
func dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode {
|
||||
// Terminate when the subtree interval is empty
|
||||
if r-l < 0 {
|
||||
return nil
|
||||
}
|
||||
// Initialize the root node
|
||||
root := NewTreeNode(preorder[i])
|
||||
// Query m to divide the left and right subtrees
|
||||
m := inorderMap[preorder[i]]
|
||||
// Subproblem: build the left subtree
|
||||
root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1)
|
||||
// Subproblem: build the right subtree
|
||||
root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r)
|
||||
// Return the root node
|
||||
return root
|
||||
}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
/* Build binary tree */
|
||||
func buildTree(preorder, inorder []int) *TreeNode {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
inorderMap := make(map[int]int, len(inorder))
|
||||
for i := 0; i < len(inorder); i++ {
|
||||
inorderMap[inorder[i]] = i
|
||||
}
|
||||
|
||||
root := dfsBuildTree(preorder, inorderMap, 0, 0, len(inorder)-1)
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="build_tree.swift"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Build binary tree: divide and conquer */
|
||||
func dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? {
|
||||
// Terminate when the subtree interval is empty
|
||||
if r - l < 0 {
|
||||
return nil
|
||||
}
|
||||
// Initialize the root node
|
||||
let root = TreeNode(x: preorder[i])
|
||||
// Query m to divide the left and right subtrees
|
||||
let m = inorderMap[preorder[i]]!
|
||||
// Subproblem: build the left subtree
|
||||
root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1)
|
||||
// Subproblem: build the right subtree
|
||||
root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r)
|
||||
// Return the root node
|
||||
return root
|
||||
}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
/* Build binary tree */
|
||||
func buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset }
|
||||
return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="build_tree.js"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Build binary tree: divide and conquer */
|
||||
function dfs(preorder, inorderMap, i, l, r) {
|
||||
// Terminate when the subtree interval is empty
|
||||
if (r - l < 0) return null;
|
||||
// Initialize the root node
|
||||
const root = new TreeNode(preorder[i]);
|
||||
// Query m to divide the left and right subtrees
|
||||
const m = inorderMap.get(preorder[i]);
|
||||
// Subproblem: build the left subtree
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// Subproblem: build the right subtree
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// Return the root node
|
||||
return root;
|
||||
}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
/* Build binary tree */
|
||||
function buildTree(preorder, inorder) {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
let inorderMap = new Map();
|
||||
for (let i = 0; i < inorder.length; i++) {
|
||||
inorderMap.set(inorder[i], i);
|
||||
}
|
||||
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="build_tree.ts"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Build binary tree: divide and conquer */
|
||||
function dfs(
|
||||
preorder: number[],
|
||||
inorderMap: Map<number, number>,
|
||||
i: number,
|
||||
l: number,
|
||||
r: number
|
||||
): TreeNode | null {
|
||||
// Terminate when the subtree interval is empty
|
||||
if (r - l < 0) return null;
|
||||
// Initialize the root node
|
||||
const root: TreeNode = new TreeNode(preorder[i]);
|
||||
// Query m to divide the left and right subtrees
|
||||
const m = inorderMap.get(preorder[i]);
|
||||
// Subproblem: build the left subtree
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// Subproblem: build the right subtree
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// Return the root node
|
||||
return root;
|
||||
}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
/* Build binary tree */
|
||||
function buildTree(preorder: number[], inorder: number[]): TreeNode | null {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
let inorderMap = new Map<number, number>();
|
||||
for (let i = 0; i < inorder.length; i++) {
|
||||
inorderMap.set(inorder[i], i);
|
||||
}
|
||||
const root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="build_tree.dart"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Build binary tree: divide and conquer */
|
||||
TreeNode? dfs(
|
||||
List<int> preorder,
|
||||
Map<int, int> inorderMap,
|
||||
int i,
|
||||
int l,
|
||||
int r,
|
||||
) {
|
||||
// Terminate when the subtree interval is empty
|
||||
if (r - l < 0) {
|
||||
return null;
|
||||
}
|
||||
// Initialize the root node
|
||||
TreeNode? root = TreeNode(preorder[i]);
|
||||
// Query m to divide the left and right subtrees
|
||||
int m = inorderMap[preorder[i]]!;
|
||||
// Subproblem: build the left subtree
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1);
|
||||
// Subproblem: build the right subtree
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r);
|
||||
// Return the root node
|
||||
return root;
|
||||
}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
/* Build binary tree */
|
||||
TreeNode? buildTree(List<int> preorder, List<int> inorder) {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
Map<int, int> inorderMap = {};
|
||||
for (int i = 0; i < inorder.length; i++) {
|
||||
inorderMap[inorder[i]] = i;
|
||||
}
|
||||
TreeNode? root = dfs(preorder, inorderMap, 0, 0, inorder.length - 1);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="build_tree.rs"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Build binary tree: divide and conquer */
|
||||
fn dfs(
|
||||
preorder: &[i32],
|
||||
inorder_map: &HashMap<i32, i32>,
|
||||
i: i32,
|
||||
l: i32,
|
||||
r: i32,
|
||||
) -> Option<Rc<RefCell<TreeNode>>> {
|
||||
// Terminate when the subtree interval is empty
|
||||
if r - l < 0 {
|
||||
return None;
|
||||
}
|
||||
// Initialize the root node
|
||||
let root = TreeNode::new(preorder[i as usize]);
|
||||
// Query m to divide the left and right subtrees
|
||||
let m = inorder_map.get(&preorder[i as usize]).unwrap();
|
||||
// Subproblem: build the left subtree
|
||||
root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1);
|
||||
// Subproblem: build the right subtree
|
||||
root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r);
|
||||
// Return the root node
|
||||
Some(root)
|
||||
}
|
||||
|
||||
[class]{}-[func]{build_tree}
|
||||
/* Build binary tree */
|
||||
fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
let mut inorder_map: HashMap<i32, i32> = HashMap::new();
|
||||
for i in 0..inorder.len() {
|
||||
inorder_map.insert(inorder[i], i as i32);
|
||||
}
|
||||
let root = dfs(preorder, &inorder_map, 0, 0, inorder.len() as i32 - 1);
|
||||
root
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="build_tree.c"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Build binary tree: divide and conquer */
|
||||
TreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) {
|
||||
// Terminate when the subtree interval is empty
|
||||
if (r - l < 0)
|
||||
return NULL;
|
||||
// Initialize the root node
|
||||
TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode));
|
||||
root->val = preorder[i];
|
||||
root->left = NULL;
|
||||
root->right = NULL;
|
||||
// Query m to divide the left and right subtrees
|
||||
int m = inorderMap[preorder[i]];
|
||||
// Subproblem: build the left subtree
|
||||
root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size);
|
||||
// Subproblem: build the right subtree
|
||||
root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size);
|
||||
// Return the root node
|
||||
return root;
|
||||
}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
/* Build binary tree */
|
||||
TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE);
|
||||
for (int i = 0; i < inorderSize; i++) {
|
||||
inorderMap[inorder[i]] = i;
|
||||
}
|
||||
TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize);
|
||||
free(inorderMap);
|
||||
return root;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="build_tree.kt"
|
||||
[class]{}-[func]{dfs}
|
||||
/* Build binary tree: divide and conquer */
|
||||
fun dfs(
|
||||
preorder: IntArray,
|
||||
inorderMap: Map<Int?, Int?>,
|
||||
i: Int,
|
||||
l: Int,
|
||||
r: Int
|
||||
): TreeNode? {
|
||||
// Terminate when the subtree interval is empty
|
||||
if (r - l < 0) return null
|
||||
// Initialize the root node
|
||||
val root = TreeNode(preorder[i])
|
||||
// Query m to divide the left and right subtrees
|
||||
val m = inorderMap[preorder[i]]!!
|
||||
// Subproblem: build the left subtree
|
||||
root.left = dfs(preorder, inorderMap, i + 1, l, m - 1)
|
||||
// Subproblem: build the right subtree
|
||||
root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r)
|
||||
// Return the root node
|
||||
return root
|
||||
}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
/* Build binary tree */
|
||||
fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? {
|
||||
// Initialize hash map, storing the mapping from inorder elements to indices
|
||||
val inorderMap = HashMap<Int?, Int?>()
|
||||
for (i in inorder.indices) {
|
||||
inorderMap[inorder[i]] = i
|
||||
}
|
||||
val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1)
|
||||
return root
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="build_tree.rb"
|
||||
[class]{}-[func]{dfs}
|
||||
### Build binary tree: divide and conquer ###
|
||||
def dfs(preorder, inorder_map, i, l, r)
|
||||
# Terminate when the subtree interval is empty
|
||||
return if r - l < 0
|
||||
|
||||
[class]{}-[func]{build_tree}
|
||||
# Initialize the root node
|
||||
root = TreeNode.new(preorder[i])
|
||||
# Query m to divide the left and right subtrees
|
||||
m = inorder_map[preorder[i]]
|
||||
# Subproblem: build the left subtree
|
||||
root.left = dfs(preorder, inorder_map, i + 1, l, m - 1)
|
||||
# Subproblem: build the right subtree
|
||||
root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r)
|
||||
|
||||
# Return the root node
|
||||
root
|
||||
end
|
||||
|
||||
### Build binary tree ###
|
||||
def build_tree(preorder, inorder)
|
||||
# Initialize hash map, storing the mapping from inorder elements to indices
|
||||
inorder_map = {}
|
||||
inorder.each_with_index { |val, i| inorder_map[val] = i }
|
||||
dfs(preorder, inorder_map, 0, 0, inorder.length - 1)
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="build_tree.zig"
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{buildTree}
|
||||
```
|
||||
|
||||
Figure 12-8 shows the recursive process of building the binary tree. Each node is created during the "descending" phase of the recursion, and each edge (reference) is formed during the "ascending" phase.
|
||||
Figure 12-8 shows the recursive process of building the binary tree. Each node is established during the downward "recursion" process, while each edge (reference) is established during the upward "return" process.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
@@ -287,12 +543,12 @@ Figure 12-8 shows the recursive process of building the binary tree. Each node i
|
||||
|
||||
<p align="center"> Figure 12-8 Recursive process of building a binary tree </p>
|
||||
|
||||
Each recursive function's division of the `preorder` and `inorder` sequences is illustrated in Figure 12-9.
|
||||
The division results of the preorder traversal `preorder` and inorder traversal `inorder` within each recursive function are shown in Figure 12-9.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-9 Division in each recursive function </p>
|
||||
<p align="center"> Figure 12-9 Division results in each recursive function </p>
|
||||
|
||||
Assuming the binary tree has $n$ nodes, initializing each node (calling the recursive function `dfs()`) takes $O(1)$ time. **Therefore, the overall time complexity is $O(n)$**.
|
||||
Let the number of nodes in the tree be $n$. Initializing each node (executing one recursive function `dfs()`) takes $O(1)$ time. **Therefore, the overall time complexity is $O(n)$**.
|
||||
|
||||
Because the hash table stores the mapping from `inorder` elements to their indexes, it requires $O(n)$ space. In the worst case, if the binary tree degenerates into a linked list, the recursive depth can reach $n$, consuming $O(n)$ stack space. **Hence, the overall space complexity is $O(n)$**.
|
||||
The hash table stores the mapping from `inorder` elements to their indices, with a space complexity of $O(n)$. In the worst case, when the binary tree degenerates into a linked list, the recursion depth reaches $n$, using $O(n)$ stack frame space. **Therefore, the overall space complexity is $O(n)$**.
|
||||
|
||||
@@ -2,55 +2,55 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.1 Divide and conquer algorithms
|
||||
# 12.1 Divide and Conquer Algorithms
|
||||
|
||||
<u>Divide and conquer</u> is an important and popular algorithm strategy. As the name suggests, the algorithm is typically implemented recursively and consists of two steps: "divide" and "conquer".
|
||||
<u>Divide and conquer</u> is a very important and common algorithm strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: "divide" and "conquer".
|
||||
|
||||
1. **Divide (partition phase)**: Recursively break down the original problem into two or more smaller sub-problems until the smallest sub-problem is reached.
|
||||
2. **Conquer (merge phase)**: Starting from the smallest sub-problem with known solution, we construct the solution to the original problem by merging the solutions of sub-problems in a bottom-up manner.
|
||||
1. **Divide (partition phase)**: Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached.
|
||||
2. **Conquer (merge phase)**: Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem.
|
||||
|
||||
As shown in Figure 12-1, "merge sort" is one of the typical applications of the divide and conquer strategy.
|
||||
|
||||
1. **Divide**: Recursively divide the original array (original problem) into two sub-arrays (sub-problems), until the sub-array has only one element (smallest sub-problem).
|
||||
2. **Conquer**: Merge the ordered sub-arrays (solutions to the sub-problems) from bottom to top to obtain an ordered original array (solution to the original problem).
|
||||
1. **Divide**: Recursively divide the original array (original problem) into two subarrays (subproblems) until the subarray has only one element (smallest subproblem).
|
||||
2. **Conquer**: Merge the sorted subarrays (solutions to subproblems) from bottom to top to obtain a sorted original array (solution to the original problem).
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-1 Merge sort's divide and conquer strategy </p>
|
||||
<p align="center"> Figure 12-1 Divide and conquer strategy of merge sort </p>
|
||||
|
||||
## 12.1.1 How to identify divide and conquer problems
|
||||
## 12.1.1 How to Determine Divide and Conquer Problems
|
||||
|
||||
Whether a problem is suitable for a divide-and-conquer solution can usually be decided based on the following criteria.
|
||||
Whether a problem is suitable for solving with divide and conquer can usually be determined based on the following criteria.
|
||||
|
||||
1. **The problem can be broken down into smaller ones**: The original problem can be divided into smaller, similar sub-problems and such process can be recursively done in the same manner.
|
||||
2. **Sub-problems are independent**: There is no overlap between sub-problems, and they are independent and can be solved separately.
|
||||
3. **Solutions to sub-problems can be merged**: The solution to the original problem is derived by combining the solutions of the sub-problems.
|
||||
1. **The problem can be decomposed**: The original problem can be divided into smaller, similar subproblems, and can be recursively divided in the same way.
|
||||
2. **Subproblems are independent**: There is no overlap between subproblems, they are independent of each other and can be solved independently.
|
||||
3. **Solutions of subproblems can be merged**: The solution to the original problem is obtained by merging the solutions of subproblems.
|
||||
|
||||
Clearly, merge sort meets these three criteria.
|
||||
Clearly, merge sort satisfies these three criteria.
|
||||
|
||||
1. **The problem can be broken down into smaller ones**: Recursively divide the array (original problem) into two sub-arrays (sub-problems).
|
||||
2. **Sub-problems are independent**: Each sub-array can be sorted independently (sub-problems can be solved independently).
|
||||
3. **Solutions to sub-problems can be merged**: Two ordered sub-arrays (solutions to the sub-problems) can be merged into one ordered array (solution to the original problem).
|
||||
1. **The problem can be decomposed**: Recursively divide the array (original problem) into two subarrays (subproblems).
|
||||
2. **Subproblems are independent**: Each subarray can be sorted independently (subproblems can be solved independently).
|
||||
3. **Solutions of subproblems can be merged**: Two sorted subarrays (solutions of subproblems) can be merged into one sorted array (solution of the original problem).
|
||||
|
||||
## 12.1.2 Improve efficiency through divide and conquer
|
||||
## 12.1.2 Improving Efficiency Through Divide and Conquer
|
||||
|
||||
The **divide-and-conquer strategy not only effectively solves algorithm problems but also often enhances efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection sort, bubble sort, and insertion sort because they apply the divide-and-conquer strategy.
|
||||
**Divide and conquer can not only effectively solve algorithmic problems but often also improve algorithm efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy.
|
||||
|
||||
We may have a question in mind: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic?** In other words, why is breaking a problem into sub-problems, solving them, and combining their solutions to address the original problem offer more efficiency than directly solving the original problem? This question can be analyzed from two aspects: operation count and parallel computation.
|
||||
This raises the question: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic**? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation.
|
||||
|
||||
### 1. Optimization of operation count
|
||||
### 1. Operation Count Optimization
|
||||
|
||||
Taking "bubble sort" as an example, it requires $O(n^2)$ time to process an array of length $n$. Suppose we divide the array from the midpoint into two sub-arrays as shown in Figure 12-2, such division requires $O(n)$ time. Sorting each sub-array requires $O((n / 2)^2)$ time. And merging the two sub-arrays requires $O(n)$ time. Thus, the overall time complexity is:
|
||||
Taking "bubble sort" as an example, processing an array of length $n$ requires $O(n^2)$ time. Suppose we divide the array into two subarrays from the midpoint as shown in Figure 12-2, the division requires $O(n)$ time, sorting each subarray requires $O((n / 2)^2)$ time, and merging the two subarrays requires $O(n)$ time, resulting in an overall time complexity of:
|
||||
|
||||
$$
|
||||
O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n)
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-2 Bubble sort before and after array partition </p>
|
||||
<p align="center"> Figure 12-2 Bubble sort before and after array division </p>
|
||||
|
||||
Let's calculate the following inequality, where the left side represents the total number of operations before division and the right side represents the total number of operations after division, respectively:
|
||||
Next, we compute the following inequality, where the left and right sides represent the total number of operations before and after division, respectively:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -60,42 +60,42 @@ n(n - 4) & > 0
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
**This means that when $n > 4$, the number of operations after partitioning is fewer, leading to better performance**. Please note that the time complexity after partitioning is still quadratic $O(n^2)$, but the constant factor in the complexity has decreased.
|
||||
**This means that when $n > 4$, the number of operations after division is smaller, and sorting efficiency should be higher**. Note that the time complexity after division is still quadratic $O(n^2)$, but the constant term in the complexity has become smaller.
|
||||
|
||||
We can go even further. **How about keeping dividing the sub-arrays from their midpoints into two sub-arrays** until the sub-arrays have only one element left? This idea is actually "merge sort," with a time complexity of $O(n \log n)$.
|
||||
Going further, **what if we continuously divide the subarrays from their midpoints into two subarrays** until the subarrays have only one element? This approach is actually "merge sort", with a time complexity of $O(n \log n)$.
|
||||
|
||||
Let's try something a bit different again. **How about splitting into more partitions instead of just two?** For example, we evenly divide the original array into $k$ sub-arrays? This approach is very similar to "bucket sort," which is very suitable for sorting massive data. Theoretically, the time complexity can reach $O(n + k)$.
|
||||
Thinking further, **what if we set multiple division points** and evenly divide the original array into $k$ subarrays? This situation is very similar to "bucket sort", which is well-suited for sorting massive amounts of data, with a theoretical time complexity of $O(n + k)$.
|
||||
|
||||
### 2. Optimization through parallel computation
|
||||
### 2. Parallel Computation Optimization
|
||||
|
||||
We know that the sub-problems generated by divide and conquer are independent of each other, **which means that they can be solved in parallel.** As a result, divide and conquer not only reduces the algorithm's time complexity, **but also facilitates parallel optimization by modern operating systems.**
|
||||
We know that the subproblems generated by divide and conquer are independent of each other, **so they can typically be solved in parallel**. This means divide and conquer can not only reduce the time complexity of algorithms, **but also benefits from parallel optimization by operating systems**.
|
||||
|
||||
Parallel optimization is particularly effective in environments with multiple cores or processors. As the system can process multiple sub-problems simultaneously, fully utilizing computing resources, the overall runtime is significantly reduced.
|
||||
Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime.
|
||||
|
||||
For example, in the "bucket sort" shown in Figure 12-3, we break massive data evenly into various buckets. The jobs of sorting each bucket can be allocated to available computing units. Once all jobs are done, all sorted buckets are merged to produce the final result.
|
||||
For example, in the "bucket sort" shown in Figure 12-3, we evenly distribute massive data into various buckets, and the sorting tasks for all buckets can be distributed to various computing units. After completion, the results are merged.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-3 Bucket sort's parallel computation </p>
|
||||
<p align="center"> Figure 12-3 Parallel computation in bucket sort </p>
|
||||
|
||||
## 12.1.3 Common applications of divide and conquer
|
||||
## 12.1.3 Common Applications of Divide and Conquer
|
||||
|
||||
Divide and conquer can be used to solve many classic algorithm problems.
|
||||
On one hand, divide and conquer can be used to solve many classic algorithmic problems.
|
||||
|
||||
- **Finding the closest pair of points**: This algorithm works by dividing the set of points into two halves. Then it recursively finds the closest pair in each half. Finally it considers pairs that span the two halves to find the overall closest pair.
|
||||
- **Large integer multiplication**: One algorithm is called Karatsuba. It breaks down large integer multiplication into several smaller integer multiplications and additions.
|
||||
- **Matrix multiplication**: One example is the Strassen algorithm. It breaks down a large matrix multiplication into multiple small matrix multiplications and additions.
|
||||
- **Tower of Hanoi problem**: The Tower of Hanoi problem can be solved recursively, a typical application of the divide-and-conquer strategy.
|
||||
- **Solving inversion pairs**: In a sequence, if a preceding number is greater than a following number, then these two numbers constitute an inversion pair. Solving inversion pair problem can utilize the idea of divide and conquer, with the aid of merge sort.
|
||||
- **Finding the closest pair of points**: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts.
|
||||
- **Large integer multiplication**: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions.
|
||||
- **Matrix multiplication**: For example, the Strassen algorithm, which decomposes large matrix multiplication into multiple small matrix multiplications and additions.
|
||||
- **Hanota problem**: The hanota problem can be solved through recursion, which is a typical application of the divide and conquer strategy.
|
||||
- **Solving inversion pairs**: In a sequence, if a preceding number is greater than a following number, these two numbers form an inversion pair. Solving the inversion pair problem can utilize the divide and conquer approach with the help of merge sort.
|
||||
|
||||
Divide and conquer is also widely applied in the design of algorithms and data structures.
|
||||
On the other hand, divide and conquer is widely applied in the design of algorithms and data structures.
|
||||
|
||||
- **Binary search**: Binary search divides a sorted array into two halves from the midpoint index. And then based on the comparison result between the target value and the middle element value, one half is discarded. The search continues on the remaining half with the same process until the target is found or there is no remaining element.
|
||||
- **Merge sort**: Already introduced at the beginning of this section, no further elaboration is needed.
|
||||
- **Quicksort**: Quicksort picks a pivot value to divide the array into two sub-arrays, one with elements smaller than the pivot and the other with elements larger than the pivot. Such process goes on against each of these two sub-arrays until they hold only one element.
|
||||
- **Bucket sort**: The basic idea of bucket sort is to distribute data to multiple buckets. After sorting the elements within each bucket, retrieve the elements from the buckets in order to obtain an ordered array.
|
||||
- **Trees**: For example, binary search trees, AVL trees, red-black trees, B-trees, and B+ trees, etc. Their operations, such as search, insertion, and deletion, can all be regarded as applications of the divide-and-conquer strategy.
|
||||
- **Heap**: A heap is a special type of complete binary tree. Its various operations, such as insertion, deletion, and heapify, actually imply the idea of divide and conquer.
|
||||
- **Hash table**: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the strategy. For example, long lists in chained addressing may be converted to red-black trees to improve query efficiency.
|
||||
- **Binary search**: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary operation on the remaining interval.
|
||||
- **Merge sort**: Already introduced at the beginning of this section, no further elaboration needed.
|
||||
- **Quick sort**: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element.
|
||||
- **Bucket sort**: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array.
|
||||
- **Trees**: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy.
|
||||
- **Heaps**: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea.
|
||||
- **Hash tables**: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve query efficiency.
|
||||
|
||||
It can be seen that **divide and conquer is a subtly pervasive algorithmic idea**, embedded within various algorithms and data structures.
|
||||
It can be seen that **divide and conquer is a "subtly pervasive" algorithmic idea**, embedded in various algorithms and data structures.
|
||||
|
||||
@@ -2,27 +2,27 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 12.4 Tower of Hanoi Problem
|
||||
# 12.4 Hanota Problem
|
||||
|
||||
In both merge sort and binary tree construction, we break the original problem into two subproblems, each half the size of the original problem. However, for the Tower of Hanoi, we adopt a different decomposition strategy.
|
||||
In merge sort and building binary trees, we decompose the original problem into two subproblems, each half the size of the original problem. However, for the hanota problem, we adopt a different decomposition strategy.
|
||||
|
||||
!!! question
|
||||
|
||||
We are given three pillars, denoted as `A`, `B`, and `C`. Initially, pillar `A` has $n$ discs, arranged from top to bottom in ascending size. Our task is to move these $n$ discs to pillar `C`, maintaining their original order (as shown in Figure 12-10). The following rules apply during the movement:
|
||||
|
||||
1. A disc can be removed only from the top of a pillar and must be placed on the top of another pillar.
|
||||
Given three pillars, denoted as `A`, `B`, and `C`. Initially, pillar `A` has $n$ discs stacked on it, arranged from top to bottom in ascending order of size. Our task is to move these $n$ discs to pillar `C` while maintaining their original order (as shown in Figure 12-10). The following rules must be followed when moving the discs.
|
||||
|
||||
1. A disc can only be taken from the top of one pillar and placed on top of another pillar.
|
||||
2. Only one disc can be moved at a time.
|
||||
3. A smaller disc must always be on top of a larger disc.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-10 Example of the Tower of Hanoi </p>
|
||||
<p align="center"> Figure 12-10 Example of the hanota problem </p>
|
||||
|
||||
**We denote the Tower of Hanoi problem of size $i$ as $f(i)$**. For example, $f(3)$ represents moving $3$ discs from pillar `A` to pillar `C`.
|
||||
**We denote the hanota problem of size $i$ as $f(i)$**. For example, $f(3)$ represents moving $3$ discs from `A` to `C`.
|
||||
|
||||
### 1. Consider the base cases
|
||||
### 1. Considering the Base Cases
|
||||
|
||||
As shown in Figure 12-11, for the problem $f(1)$—which has only one disc—we can directly move it from `A` to `C`.
|
||||
As shown in Figure 12-11, for problem $f(1)$, when there is only one disc, we can move it directly from `A` to `C`.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
@@ -32,7 +32,7 @@ As shown in Figure 12-11, for the problem $f(1)$—which has only one disc—we
|
||||
|
||||
<p align="center"> Figure 12-11 Solution for a problem of size 1 </p>
|
||||
|
||||
For $f(2)$—which has two discs—**we rely on pillar `B` to help keep the smaller disc above the larger disc**, as illustrated in the following figure:
|
||||
As shown in Figure 12-12, for problem $f(2)$, when there are two discs, **since we must always keep the smaller disc on top of the larger disc, we need to use `B` to assist in the move**.
|
||||
|
||||
1. First, move the smaller disc from `A` to `B`.
|
||||
2. Then move the larger disc from `A` to `C`.
|
||||
@@ -52,17 +52,17 @@ For $f(2)$—which has two discs—**we rely on pillar `B` to help keep the smal
|
||||
|
||||
<p align="center"> Figure 12-12 Solution for a problem of size 2 </p>
|
||||
|
||||
The process of solving $f(2)$ can be summarized as: **moving two discs from `A` to `C` with the help of `B`**. Here, `C` is called the target pillar, and `B` is called the buffer pillar.
|
||||
The process of solving problem $f(2)$ can be summarized as: **moving two discs from `A` to `C` with the help of `B`**. Here, `C` is called the target pillar, and `B` is called the buffer pillar.
|
||||
|
||||
### 2. Decomposition of subproblems
|
||||
### 2. Subproblem Decomposition
|
||||
|
||||
For the problem $f(3)$—that is, when there are three discs—the situation becomes slightly more complicated.
|
||||
For problem $f(3)$, when there are three discs, the situation becomes slightly more complex.
|
||||
|
||||
Since we already know the solutions to $f(1)$ and $f(2)$, we can adopt a divide-and-conquer perspective and **treat the top two discs on `A` as a single unit**, performing the steps shown in Figure 12-13. This allows the three discs to be successfully moved from `A` to `C`.
|
||||
Since we already know the solutions to $f(1)$ and $f(2)$, we can think from a divide and conquer perspective, **treating the top two discs on `A` as a whole**, and execute the steps shown in Figure 12-13. This successfully moves the three discs from `A` to `C`.
|
||||
|
||||
1. Let `B` be the target pillar and `C` the buffer pillar, then move the two discs from `A` to `B`.
|
||||
1. Let `B` be the target pillar and `C` be the buffer pillar, and move two discs from `A` to `B`.
|
||||
2. Move the remaining disc from `A` directly to `C`.
|
||||
3. Let `C` be the target pillar and `A` the buffer pillar, then move the two discs from `B` to `C`.
|
||||
3. Let `C` be the target pillar and `A` be the buffer pillar, and move two discs from `B` to `C`.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
@@ -78,85 +78,85 @@ Since we already know the solutions to $f(1)$ and $f(2)$, we can adopt a divide-
|
||||
|
||||
<p align="center"> Figure 12-13 Solution for a problem of size 3 </p>
|
||||
|
||||
Essentially, **we decompose $f(3)$ into two $f(2)$ subproblems and one $f(1)$ subproblem**. By solving these three subproblems in sequence, the original problem is solved, indicating that the subproblems are independent and their solutions can be merged.
|
||||
Essentially, **we divide problem $f(3)$ into two subproblems $f(2)$ and one subproblem $f(1)$**. By solving these three subproblems in order, the original problem is solved. This shows that the subproblems are independent and their solutions can be merged.
|
||||
|
||||
From this, we can summarize the divide-and-conquer strategy for the Tower of Hanoi, illustrated in Figure 12-14. We divide the original problem $f(n)$ into two subproblems $f(n-1)$ and one subproblem $f(1)$, and solve these three subproblems in the following order:
|
||||
From this, we can summarize the divide and conquer strategy for solving the hanota problem shown in Figure 12-14: divide the original problem $f(n)$ into two subproblems $f(n-1)$ and one subproblem $f(1)$, and solve these three subproblems in the following order.
|
||||
|
||||
1. Move $n-1$ discs from `A` to `B`, using `C` as a buffer.
|
||||
2. Move the remaining disc directly from `A` to `C`.
|
||||
3. Move $n-1$ discs from `B` to `C`, using `A` as a buffer.
|
||||
1. Move $n-1$ discs from `A` to `B` with the help of `C`.
|
||||
2. Move the remaining $1$ disc directly from `A` to `C`.
|
||||
3. Move $n-1$ discs from `B` to `C` with the help of `A`.
|
||||
|
||||
For each $f(n-1)$ subproblem, **we can apply the same recursive partition** until we reach the smallest subproblem $f(1)$. Because $f(1)$ is already known to require just a single move, it is trivial to solve.
|
||||
For these two subproblems $f(n-1)$, **we can recursively divide them in the same way** until reaching the smallest subproblem $f(1)$. The solution to $f(1)$ is known and requires only one move operation.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-14 Divide-and-conquer strategy for solving the Tower of Hanoi </p>
|
||||
<p align="center"> Figure 12-14 Divide and conquer strategy for solving the hanota problem </p>
|
||||
|
||||
### 3. Code implementation
|
||||
### 3. Code Implementation
|
||||
|
||||
In the code, we define a recursive function `dfs(i, src, buf, tar)` which moves the top $i$ discs from pillar `src` to pillar `tar`, using pillar `buf` as a buffer:
|
||||
In the code, we declare a recursive function `dfs(i, src, buf, tar)`, whose purpose is to move the top $i$ discs from pillar `src` to target pillar `tar` with the help of buffer pillar `buf`:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="hanota.py"
|
||||
def move(src: list[int], tar: list[int]):
|
||||
"""Move a disc"""
|
||||
# Take out a disc from the top of src
|
||||
"""Move a disk"""
|
||||
# Take out a disk from the top of src
|
||||
pan = src.pop()
|
||||
# Place the disc on top of tar
|
||||
# Place the disk on top of tar
|
||||
tar.append(pan)
|
||||
|
||||
def dfs(i: int, src: list[int], buf: list[int], tar: list[int]):
|
||||
"""Solve the Tower of Hanoi problem f(i)"""
|
||||
# If only one disc remains on src, move it to tar
|
||||
# If there is only one disk left in src, move it directly to tar
|
||||
if i == 1:
|
||||
move(src, tar)
|
||||
return
|
||||
# Subproblem f(i-1): move the top i-1 discs from src with the help of tar to buf
|
||||
# Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf)
|
||||
# Subproblem f(1): move the remaining one disc from src to tar
|
||||
# Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar)
|
||||
# Subproblem f(i-1): move the top i-1 discs from buf with the help of src to tar
|
||||
# Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar)
|
||||
|
||||
def solve_hanota(A: list[int], B: list[int], C: list[int]):
|
||||
"""Solve the Tower of Hanoi problem"""
|
||||
n = len(A)
|
||||
# Move the top n discs from A with the help of B to C
|
||||
# Move the top n disks from A to C using B
|
||||
dfs(n, A, B, C)
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="hanota.cpp"
|
||||
/* Move a disc */
|
||||
/* Move a disk */
|
||||
void move(vector<int> &src, vector<int> &tar) {
|
||||
// Take out a disc from the top of src
|
||||
// Take out a disk from the top of src
|
||||
int pan = src.back();
|
||||
src.pop_back();
|
||||
// Place the disc on top of tar
|
||||
// Place the disk on top of tar
|
||||
tar.push_back(pan);
|
||||
}
|
||||
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
void dfs(int i, vector<int> &src, vector<int> &buf, vector<int> &tar) {
|
||||
// If only one disc remains on src, move it to tar
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if (i == 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 discs from src with the help of tar to buf
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// Subproblem f(1): move the remaining one disc from src to tar
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar);
|
||||
// Subproblem f(i-1): move the top i-1 discs from buf with the help of src to tar
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
void solveHanota(vector<int> &A, vector<int> &B, vector<int> &C) {
|
||||
int n = A.size();
|
||||
// Move the top n discs from A with the help of B to C
|
||||
// Move the top n disks from A to C using B
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
@@ -164,33 +164,33 @@ In the code, we define a recursive function `dfs(i, src, buf, tar)` which moves
|
||||
=== "Java"
|
||||
|
||||
```java title="hanota.java"
|
||||
/* Move a disc */
|
||||
/* Move a disk */
|
||||
void move(List<Integer> src, List<Integer> tar) {
|
||||
// Take out a disc from the top of src
|
||||
// Take out a disk from the top of src
|
||||
Integer pan = src.remove(src.size() - 1);
|
||||
// Place the disc on top of tar
|
||||
// Place the disk on top of tar
|
||||
tar.add(pan);
|
||||
}
|
||||
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
void dfs(int i, List<Integer> src, List<Integer> buf, List<Integer> tar) {
|
||||
// If only one disc remains on src, move it to tar
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if (i == 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 discs from src with the help of tar to buf
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// Subproblem f(1): move the remaining one disc from src to tar
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar);
|
||||
// Subproblem f(i-1): move the top i-1 discs from buf with the help of src to tar
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
void solveHanota(List<Integer> A, List<Integer> B, List<Integer> C) {
|
||||
int n = A.size();
|
||||
// Move the top n discs from A with the help of B to C
|
||||
// Move the top n disks from A to C using B
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
@@ -198,121 +198,358 @@ In the code, we define a recursive function `dfs(i, src, buf, tar)` which moves
|
||||
=== "C#"
|
||||
|
||||
```csharp title="hanota.cs"
|
||||
[class]{hanota}-[func]{Move}
|
||||
/* Move a disk */
|
||||
void Move(List<int> src, List<int> tar) {
|
||||
// Take out a disk from the top of src
|
||||
int pan = src[^1];
|
||||
src.RemoveAt(src.Count - 1);
|
||||
// Place the disk on top of tar
|
||||
tar.Add(pan);
|
||||
}
|
||||
|
||||
[class]{hanota}-[func]{DFS}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
void DFS(int i, List<int> src, List<int> buf, List<int> tar) {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if (i == 1) {
|
||||
Move(src, tar);
|
||||
return;
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
DFS(i - 1, src, tar, buf);
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
Move(src, tar);
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
DFS(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
[class]{hanota}-[func]{SolveHanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
void SolveHanota(List<int> A, List<int> B, List<int> C) {
|
||||
int n = A.Count;
|
||||
// Move the top n disks from A to C using B
|
||||
DFS(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="hanota.go"
|
||||
[class]{}-[func]{move}
|
||||
/* Move a disk */
|
||||
func move(src, tar *list.List) {
|
||||
// Take out a disk from the top of src
|
||||
pan := src.Back()
|
||||
// Place the disk on top of tar
|
||||
tar.PushBack(pan.Value)
|
||||
// Remove top disk from src
|
||||
src.Remove(pan)
|
||||
}
|
||||
|
||||
[class]{}-[func]{dfsHanota}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
func dfsHanota(i int, src, buf, tar *list.List) {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if i == 1 {
|
||||
move(src, tar)
|
||||
return
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfsHanota(i-1, src, tar, buf)
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar)
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfsHanota(i-1, buf, src, tar)
|
||||
}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
func solveHanota(A, B, C *list.List) {
|
||||
n := A.Len()
|
||||
// Move the top n disks from A to C using B
|
||||
dfsHanota(n, A, B, C)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="hanota.swift"
|
||||
[class]{}-[func]{move}
|
||||
/* Move a disk */
|
||||
func move(src: inout [Int], tar: inout [Int]) {
|
||||
// Take out a disk from the top of src
|
||||
let pan = src.popLast()!
|
||||
// Place the disk on top of tar
|
||||
tar.append(pan)
|
||||
}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
func dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if i == 1 {
|
||||
move(src: &src, tar: &tar)
|
||||
return
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i: i - 1, src: &src, buf: &tar, tar: &buf)
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src: &src, tar: &tar)
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i: i - 1, src: &buf, buf: &src, tar: &tar)
|
||||
}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
func solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) {
|
||||
let n = A.count
|
||||
// The tail of the list is the top of the rod
|
||||
// Move top n disks from src to C using B
|
||||
dfs(i: n, src: &A, buf: &B, tar: &C)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="hanota.js"
|
||||
[class]{}-[func]{move}
|
||||
/* Move a disk */
|
||||
function move(src, tar) {
|
||||
// Take out a disk from the top of src
|
||||
const pan = src.pop();
|
||||
// Place the disk on top of tar
|
||||
tar.push(pan);
|
||||
}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
function dfs(i, src, buf, tar) {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if (i === 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar);
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
function solveHanota(A, B, C) {
|
||||
const n = A.length;
|
||||
// Move the top n disks from A to C using B
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="hanota.ts"
|
||||
[class]{}-[func]{move}
|
||||
/* Move a disk */
|
||||
function move(src: number[], tar: number[]): void {
|
||||
// Take out a disk from the top of src
|
||||
const pan = src.pop();
|
||||
// Place the disk on top of tar
|
||||
tar.push(pan);
|
||||
}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
function dfs(i: number, src: number[], buf: number[], tar: number[]): void {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if (i === 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar);
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
function solveHanota(A: number[], B: number[], C: number[]): void {
|
||||
const n = A.length;
|
||||
// Move the top n disks from A to C using B
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="hanota.dart"
|
||||
[class]{}-[func]{move}
|
||||
/* Move a disk */
|
||||
void move(List<int> src, List<int> tar) {
|
||||
// Take out a disk from the top of src
|
||||
int pan = src.removeLast();
|
||||
// Place the disk on top of tar
|
||||
tar.add(pan);
|
||||
}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
void dfs(int i, List<int> src, List<int> buf, List<int> tar) {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if (i == 1) {
|
||||
move(src, tar);
|
||||
return;
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar);
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
void solveHanota(List<int> A, List<int> B, List<int> C) {
|
||||
int n = A.length;
|
||||
// Move the top n disks from A to C using B
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="hanota.rs"
|
||||
[class]{}-[func]{move_pan}
|
||||
/* Move a disk */
|
||||
fn move_pan(src: &mut Vec<i32>, tar: &mut Vec<i32>) {
|
||||
// Take out a disk from the top of src
|
||||
let pan = src.pop().unwrap();
|
||||
// Place the disk on top of tar
|
||||
tar.push(pan);
|
||||
}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
fn dfs(i: i32, src: &mut Vec<i32>, buf: &mut Vec<i32>, tar: &mut Vec<i32>) {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if i == 1 {
|
||||
move_pan(src, tar);
|
||||
return;
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf);
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move_pan(src, tar);
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar);
|
||||
}
|
||||
|
||||
[class]{}-[func]{solve_hanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
fn solve_hanota(A: &mut Vec<i32>, B: &mut Vec<i32>, C: &mut Vec<i32>) {
|
||||
let n = A.len() as i32;
|
||||
// Move the top n disks from A to C using B
|
||||
dfs(n, A, B, C);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="hanota.c"
|
||||
[class]{}-[func]{move}
|
||||
/* Move a disk */
|
||||
void move(int *src, int *srcSize, int *tar, int *tarSize) {
|
||||
// Take out a disk from the top of src
|
||||
int pan = src[*srcSize - 1];
|
||||
src[*srcSize - 1] = 0;
|
||||
(*srcSize)--;
|
||||
// Place the disk on top of tar
|
||||
tar[*tarSize] = pan;
|
||||
(*tarSize)++;
|
||||
}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
void dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if (i == 1) {
|
||||
move(src, srcSize, tar, tarSize);
|
||||
return;
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize);
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, srcSize, tar, tarSize);
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize);
|
||||
}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
void solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) {
|
||||
// Move the top n disks from A to C using B
|
||||
dfs(*ASize, A, ASize, B, BSize, C, CSize);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="hanota.kt"
|
||||
[class]{}-[func]{move}
|
||||
/* Move a disk */
|
||||
fun move(src: MutableList<Int>, tar: MutableList<Int>) {
|
||||
// Take out a disk from the top of src
|
||||
val pan = src.removeAt(src.size - 1)
|
||||
// Place the disk on top of tar
|
||||
tar.add(pan)
|
||||
}
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
/* Solve the Tower of Hanoi problem f(i) */
|
||||
fun dfs(i: Int, src: MutableList<Int>, buf: MutableList<Int>, tar: MutableList<Int>) {
|
||||
// If there is only one disk left in src, move it directly to tar
|
||||
if (i == 1) {
|
||||
move(src, tar)
|
||||
return
|
||||
}
|
||||
// Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf)
|
||||
// Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar)
|
||||
// Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar)
|
||||
}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
/* Solve the Tower of Hanoi problem */
|
||||
fun solveHanota(A: MutableList<Int>, B: MutableList<Int>, C: MutableList<Int>) {
|
||||
val n = A.size
|
||||
// Move the top n disks from A to C using B
|
||||
dfs(n, A, B, C)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="hanota.rb"
|
||||
[class]{}-[func]{move}
|
||||
### Move one disk ###
|
||||
def move(src, tar)
|
||||
# Take out a disk from the top of src
|
||||
pan = src.pop
|
||||
# Place the disk on top of tar
|
||||
tar << pan
|
||||
end
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
### Solve Tower of Hanoi f(i) ###
|
||||
def dfs(i, src, buf, tar)
|
||||
# If there is only one disk left in src, move it directly to tar
|
||||
if i == 1
|
||||
move(src, tar)
|
||||
return
|
||||
end
|
||||
|
||||
[class]{}-[func]{solve_hanota}
|
||||
# Subproblem f(i-1): move the top i-1 disks from src to buf using tar
|
||||
dfs(i - 1, src, tar, buf)
|
||||
# Subproblem f(1): move the remaining disk from src to tar
|
||||
move(src, tar)
|
||||
# Subproblem f(i-1): move the top i-1 disks from buf to tar using src
|
||||
dfs(i - 1, buf, src, tar)
|
||||
end
|
||||
|
||||
### Solve Tower of Hanoi ###
|
||||
def solve_hanota(_A, _B, _C)
|
||||
n = _A.length
|
||||
# Move the top n disks from A to C using B
|
||||
dfs(n, _A, _B, _C)
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
As shown in Figure 12-15, the hanota problem forms a recursion tree of height $n$, where each node represents a subproblem corresponding to an invocation of the `dfs()` function, **therefore the time complexity is $O(2^n)$ and the space complexity is $O(n)$**.
|
||||
|
||||
```zig title="hanota.zig"
|
||||
[class]{}-[func]{move}
|
||||
{ class="animation-figure" }
|
||||
|
||||
[class]{}-[func]{dfs}
|
||||
|
||||
[class]{}-[func]{solveHanota}
|
||||
```
|
||||
|
||||
As shown in Figure 12-15, the Tower of Hanoi problem can be visualized as a recursive tree of height $n$. Each node represents a subproblem, corresponding to a call to `dfs()`, **Hence, the time complexity is $O(2^n)$, and the space complexity is $O(n)$.**
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 12-15 Recursive tree of the Tower of Hanoi </p>
|
||||
<p align="center"> Figure 12-15 Recursion tree of the hanota problem </p>
|
||||
|
||||
!!! quote
|
||||
|
||||
The Tower of Hanoi originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and $64$ differently sized golden discs. They believed that when the last disc was correctly placed, the world would end.
|
||||
The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and $64$ golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end.
|
||||
|
||||
However, even if the monks moved one disc every second, it would take about $2^{64} \approx 1.84×10^{19}$ —approximately 585 billion years—far exceeding current estimates of the age of the universe. Thus, if the legend is true, we probably do not need to worry about the world ending.
|
||||
However, even if the monks moved one disc per second, it would take approximately $2^{64} \approx 1.84×10^{19}$ seconds, which is about $5850$ billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world.
|
||||
|
||||
@@ -3,20 +3,20 @@ comments: true
|
||||
icon: material/set-split
|
||||
---
|
||||
|
||||
# Chapter 12. Divide and conquer
|
||||
# Chapter 12. Divide and Conquer
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
Difficult problems are decomposed layer by layer, with each decomposition making them simpler.
|
||||
|
||||
Divide and conquer unveils a profound truth: begin with simplicity, and complexity dissolves.
|
||||
Divide and conquer reveals an important truth: start with simplicity, and nothing remains complex.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [12.1 Divide and conquer algorithms](divide_and_conquer.md)
|
||||
- [12.2 Divide and conquer search strategy](binary_search_recur.md)
|
||||
- [12.3 Building binary tree problem](build_binary_tree_problem.md)
|
||||
- [12.4 Tower of Hanoi Problem](hanota_problem.md)
|
||||
- [12.1 Divide and Conquer Algorithms](divide_and_conquer.md)
|
||||
- [12.2 Divide and Conquer Search Strategy](binary_search_recur.md)
|
||||
- [12.3 Building a Binary Tree Problem](build_binary_tree_problem.md)
|
||||
- [12.4 Hanoi Tower Problem](hanota_problem.md)
|
||||
- [12.5 Summary](summary.md)
|
||||
|
||||
@@ -4,12 +4,14 @@ comments: true
|
||||
|
||||
# 12.5 Summary
|
||||
|
||||
- Divide and conquer is a common algorithm design strategy that consists of two stages—divide (partition) and conquer (merge)—and is generally implemented using recursion.
|
||||
- To determine whether a problem is suited for a divide and conquer approach, we check if the problem can be decomposed, whether the subproblems are independent, and whether the subproblems can be merged.
|
||||
- Merge sort is a typical example of the divide and conquer strategy. It recursively splits an array into two equal-length subarrays until only one element remains, and then merges these subarrays layer by layer to complete the sorting.
|
||||
- Introducing the divide and conquer strategy often improves algorithm efficiency. On one hand, it reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division.
|
||||
- Divide and conquer can be applied to numerous algorithmic problems and is widely used in data structures and algorithm design, appearing in many scenarios.
|
||||
- Compared to brute force search, adaptive search is more efficient. Search algorithms with a time complexity of $O(\log n)$ are typically based on the divide and conquer strategy.
|
||||
- Binary search is another classic application of the divide-and-conquer strategy. It does not involve merging subproblem solutions and can be implemented via a recursive divide-and-conquer approach.
|
||||
- In the problem of constructing binary trees, building the tree (the original problem) can be divided into building the left subtree and right subtree (the subproblems). This can be achieved by partitioning the index ranges of the preorder and inorder traversals.
|
||||
- In the Tower of Hanoi problem, a problem of size $n$ can be broken down into two subproblems of size $n-1$ and one subproblem of size $1$. By solving these three subproblems in sequence, the original problem is resolved.
|
||||
### 1. Key Review
|
||||
|
||||
- Divide and conquer is a common algorithm design strategy, consisting of two phases: divide (partition) and conquer (merge), typically implemented based on recursion.
|
||||
- The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged.
|
||||
- Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting.
|
||||
- Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, the divide and conquer strategy reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division.
|
||||
- Divide and conquer can both solve many algorithmic problems and is widely applied in data structure and algorithm design, appearing everywhere.
|
||||
- Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy.
|
||||
- Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer.
|
||||
- In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals.
|
||||
- In the hanota problem, a problem of size $n$ can be divided into two subproblems of size $n-1$ and one subproblem of size $1$. After solving these three subproblems in order, the original problem is solved.
|
||||
|
||||
@@ -2,55 +2,55 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 14.2 Characteristics of dynamic programming problems
|
||||
# 14.2 Characteristics of Dynamic Programming Problems
|
||||
|
||||
In the previous section, we learned how dynamic programming solves the original problem by decomposing it into subproblems. In fact, subproblem decomposition is a general algorithmic approach, with different emphases in divide and conquer, dynamic programming, and backtracking.
|
||||
|
||||
- Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and combine the solutions of the subproblems during backtracking to ultimately obtain the solution to the original problem.
|
||||
- Dynamic programming also decomposes the problem recursively, but the main difference from divide and conquer algorithms is that the subproblems in dynamic programming are interdependent, and many overlapping subproblems will appear during the decomposition process.
|
||||
- Backtracking algorithms exhaust all possible solutions through trial and error and avoid unnecessary search branches by pruning. The solution to the original problem consists of a series of decision steps, and we can consider each sub-sequence before each decision step as a subproblem.
|
||||
- Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and merge the solutions to the subproblems during backtracking to ultimately obtain the solution to the original problem.
|
||||
- Dynamic programming also recursively decomposes problems, but the main difference from divide and conquer algorithms is that subproblems in dynamic programming are interdependent, and many overlapping subproblems appear during the decomposition process.
|
||||
- Backtracking algorithms enumerate all possible solutions through trial and error, and avoid unnecessary search branches through pruning. The solution to the original problem consists of a series of decision steps, and we can regard the subsequence before each decision step as a subproblem.
|
||||
|
||||
In fact, dynamic programming is commonly used to solve optimization problems, which not only include overlapping subproblems but also have two other major characteristics: optimal substructure and statelessness.
|
||||
In fact, dynamic programming is commonly used to solve optimization problems, which not only contain overlapping subproblems but also have two other major characteristics: optimal substructure and no aftereffects.
|
||||
|
||||
## 14.2.1 Optimal substructure
|
||||
## 14.2.1 Optimal Substructure
|
||||
|
||||
We make a slight modification to the stair climbing problem to make it more suitable to demonstrate the concept of optimal substructure.
|
||||
We make a slight modification to the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure.
|
||||
|
||||
!!! question "Minimum cost of climbing stairs"
|
||||
!!! question "Climbing stairs with minimum cost"
|
||||
|
||||
Given a staircase, you can step up 1 or 2 steps at a time, and each step on the staircase has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost you need to pay at the $i$-th step, $cost[0]$ is the ground (starting point). What is the minimum cost required to reach the top?
|
||||
Given a staircase, where you can climb $1$ or $2$ steps at a time, and each step has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost at the $i$-th step, and $cost[0]$ is the ground (starting point). What is the minimum cost required to reach the top?
|
||||
|
||||
As shown in Figure 14-6, if the costs of the 1st, 2nd, and 3rd steps are $1$, $10$, and $1$ respectively, then the minimum cost to climb to the 3rd step from the ground is $2$.
|
||||
As shown in Figure 14-6, if the costs of the $1$st, $2$nd, and $3$rd steps are $1$, $10$, and $1$ respectively, then climbing from the ground to the $3$rd step requires a minimum cost of $2$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-6 Minimum cost to climb to the 3rd step </p>
|
||||
|
||||
Let $dp[i]$ be the cumulative cost of climbing to the $i$-th step. Since the $i$-th step can only come from the $i-1$ or $i-2$ step, $dp[i]$ can only be either $dp[i-1] + cost[i]$ or $dp[i-2] + cost[i]$. To minimize the cost, we should choose the smaller of the two:
|
||||
Let $dp[i]$ be the accumulated cost of climbing to the $i$-th step. Since the $i$-th step can only come from the $i-1$-th or $i-2$-th step, $dp[i]$ can only equal $dp[i-1] + cost[i]$ or $dp[i-2] + cost[i]$. To minimize the cost, we should choose the smaller of the two:
|
||||
|
||||
$$
|
||||
dp[i] = \min(dp[i-1], dp[i-2]) + cost[i]
|
||||
$$
|
||||
|
||||
This leads us to the meaning of optimal substructure: **The optimal solution to the original problem is constructed from the optimal solutions of subproblems**.
|
||||
This leads us to the meaning of optimal substructure: **the optimal solution to the original problem is constructed from the optimal solutions to the subproblems**.
|
||||
|
||||
This problem obviously has optimal substructure: we select the better one from the optimal solutions of the two subproblems, $dp[i-1]$ and $dp[i-2]$, and use it to construct the optimal solution for the original problem $dp[i]$.
|
||||
This problem clearly has optimal substructure: we select the better one from the optimal solutions to the two subproblems $dp[i-1]$ and $dp[i-2]$, and use it to construct the optimal solution to the original problem $dp[i]$.
|
||||
|
||||
So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to solve for the number of solutions, which seems to be a counting problem, but if we ask in another way: "Solve for the maximum number of solutions". We surprisingly find that **although the problem has changed, the optimal substructure has emerged**: the maximum number of solutions at the $n$-th step equals the sum of the maximum number of solutions at the $n-1$ and $n-2$ steps. Thus, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems.
|
||||
So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to find the number of ways, which seems to be a counting problem, but if we change the question: "Find the maximum number of ways". We surprisingly discover that **although the problem before and after modification are equivalent, the optimal substructure has emerged**: the maximum number of ways for the $n$-th step equals the sum of the maximum number of ways for the $n-1$-th and $n-2$-th steps. Therefore, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems.
|
||||
|
||||
According to the state transition equation, and the initial states $dp[1] = cost[1]$ and $dp[2] = cost[2]$, we can obtain the dynamic programming code:
|
||||
According to the state transition equation and the initial states $dp[1] = cost[1]$ and $dp[2] = cost[2]$, we can obtain the dynamic programming code:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="min_cost_climbing_stairs_dp.py"
|
||||
def min_cost_climbing_stairs_dp(cost: list[int]) -> int:
|
||||
"""Climbing stairs with minimum cost: Dynamic programming"""
|
||||
"""Minimum cost climbing stairs: Dynamic programming"""
|
||||
n = len(cost) - 1
|
||||
if n == 1 or n == 2:
|
||||
return cost[n]
|
||||
# Initialize dp table, used to store subproblem solutions
|
||||
# Initialize dp table, used to store solutions to subproblems
|
||||
dp = [0] * (n + 1)
|
||||
# Initial state: preset the smallest subproblem solution
|
||||
# Initial state: preset the solution to the smallest subproblem
|
||||
dp[1], dp[2] = cost[1], cost[2]
|
||||
# State transition: gradually solve larger subproblems from smaller ones
|
||||
for i in range(3, n + 1):
|
||||
@@ -61,14 +61,14 @@ According to the state transition equation, and the initial states $dp[1] = cost
|
||||
=== "C++"
|
||||
|
||||
```cpp title="min_cost_climbing_stairs_dp.cpp"
|
||||
/* Climbing stairs with minimum cost: Dynamic programming */
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
int minCostClimbingStairsDP(vector<int> &cost) {
|
||||
int n = cost.size() - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
// Initialize dp table, used to store subproblem solutions
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
vector<int> dp(n + 1);
|
||||
// Initial state: preset the smallest subproblem solution
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
@@ -82,14 +82,14 @@ According to the state transition equation, and the initial states $dp[1] = cost
|
||||
=== "Java"
|
||||
|
||||
```java title="min_cost_climbing_stairs_dp.java"
|
||||
/* Climbing stairs with minimum cost: Dynamic programming */
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
int minCostClimbingStairsDP(int[] cost) {
|
||||
int n = cost.length - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
// Initialize dp table, used to store subproblem solutions
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
int[] dp = new int[n + 1];
|
||||
// Initial state: preset the smallest subproblem solution
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
@@ -103,82 +103,234 @@ According to the state transition equation, and the initial states $dp[1] = cost
|
||||
=== "C#"
|
||||
|
||||
```csharp title="min_cost_climbing_stairs_dp.cs"
|
||||
[class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDP}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
int MinCostClimbingStairsDP(int[] cost) {
|
||||
int n = cost.Length - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
int[] dp = new int[n + 1];
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i] = Math.Min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="min_cost_climbing_stairs_dp.go"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
func minCostClimbingStairsDP(cost []int) int {
|
||||
n := len(cost) - 1
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n]
|
||||
}
|
||||
min := func(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
dp := make([]int, n+1)
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1]
|
||||
dp[2] = cost[2]
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for i := 3; i <= n; i++ {
|
||||
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
|
||||
}
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="min_cost_climbing_stairs_dp.swift"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
func minCostClimbingStairsDP(cost: [Int]) -> Int {
|
||||
let n = cost.count - 1
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n]
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
var dp = Array(repeating: 0, count: n + 1)
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1]
|
||||
dp[2] = cost[2]
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for i in 3 ... n {
|
||||
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
|
||||
}
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="min_cost_climbing_stairs_dp.js"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
function minCostClimbingStairsDP(cost) {
|
||||
const n = cost.length - 1;
|
||||
if (n === 1 || n === 2) {
|
||||
return cost[n];
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
const dp = new Array(n + 1);
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (let i = 3; i <= n; i++) {
|
||||
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="min_cost_climbing_stairs_dp.ts"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
function minCostClimbingStairsDP(cost: Array<number>): number {
|
||||
const n = cost.length - 1;
|
||||
if (n === 1 || n === 2) {
|
||||
return cost[n];
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
const dp = new Array(n + 1);
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (let i = 3; i <= n; i++) {
|
||||
dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="min_cost_climbing_stairs_dp.dart"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
int minCostClimbingStairsDP(List<int> cost) {
|
||||
int n = cost.length - 1;
|
||||
if (n == 1 || n == 2) return cost[n];
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
List<int> dp = List.filled(n + 1, 0);
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
return dp[n];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="min_cost_climbing_stairs_dp.rs"
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
fn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 {
|
||||
let n = cost.len() - 1;
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n];
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
let mut dp = vec![-1; n + 1];
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for i in 3..=n {
|
||||
dp[i] = cmp::min(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="min_cost_climbing_stairs_dp.c"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
int minCostClimbingStairsDP(int cost[], int costSize) {
|
||||
int n = costSize - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
int *dp = calloc(n + 1, sizeof(int));
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1];
|
||||
dp[2] = cost[2];
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i];
|
||||
}
|
||||
int res = dp[n];
|
||||
// Free memory
|
||||
free(dp);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="min_cost_climbing_stairs_dp.kt"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
/* Minimum cost climbing stairs: Dynamic programming */
|
||||
fun minCostClimbingStairsDP(cost: IntArray): Int {
|
||||
val n = cost.size - 1
|
||||
if (n == 1 || n == 2) return cost[n]
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
val dp = IntArray(n + 1)
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1] = cost[1]
|
||||
dp[2] = cost[2]
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (i in 3..n) {
|
||||
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
|
||||
}
|
||||
return dp[n]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="min_cost_climbing_stairs_dp.rb"
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="min_cost_climbing_stairs_dp.zig"
|
||||
[class]{}-[func]{minCostClimbingStairsDP}
|
||||
### Minimum cost climbing stairs: DP ###
|
||||
def min_cost_climbing_stairs_dp(cost)
|
||||
n = cost.length - 1
|
||||
return cost[n] if n == 1 || n == 2
|
||||
# Initialize dp table, used to store solutions to subproblems
|
||||
dp = Array.new(n + 1, 0)
|
||||
# Initial state: preset the solution to the smallest subproblem
|
||||
dp[1], dp[2] = cost[1], cost[2]
|
||||
# State transition: gradually solve larger subproblems from smaller ones
|
||||
(3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }
|
||||
dp[n]
|
||||
end
|
||||
```
|
||||
|
||||
Figure 14-7 shows the dynamic programming process for the above code.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-7 Dynamic programming process for minimum cost of climbing stairs </p>
|
||||
<p align="center"> Figure 14-7 Dynamic programming process for climbing stairs with minimum cost </p>
|
||||
|
||||
This problem can also be space-optimized, compressing one dimension to zero, reducing the space complexity from $O(n)$ to $O(1)$:
|
||||
This problem can also be space-optimized, compressing from one dimension to zero, reducing the space complexity from $O(n)$ to $O(1)$:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="min_cost_climbing_stairs_dp.py"
|
||||
def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int:
|
||||
"""Climbing stairs with minimum cost: Space-optimized dynamic programming"""
|
||||
"""Minimum cost climbing stairs: Space-optimized dynamic programming"""
|
||||
n = len(cost) - 1
|
||||
if n == 1 or n == 2:
|
||||
return cost[n]
|
||||
@@ -191,7 +343,7 @@ This problem can also be space-optimized, compressing one dimension to zero, red
|
||||
=== "C++"
|
||||
|
||||
```cpp title="min_cost_climbing_stairs_dp.cpp"
|
||||
/* Climbing stairs with minimum cost: Space-optimized dynamic programming */
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
int minCostClimbingStairsDPComp(vector<int> &cost) {
|
||||
int n = cost.size() - 1;
|
||||
if (n == 1 || n == 2)
|
||||
@@ -209,7 +361,7 @@ This problem can also be space-optimized, compressing one dimension to zero, red
|
||||
=== "Java"
|
||||
|
||||
```java title="min_cost_climbing_stairs_dp.java"
|
||||
/* Climbing stairs with minimum cost: Space-optimized dynamic programming */
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
int minCostClimbingStairsDPComp(int[] cost) {
|
||||
int n = cost.length - 1;
|
||||
if (n == 1 || n == 2)
|
||||
@@ -227,97 +379,231 @@ This problem can also be space-optimized, compressing one dimension to zero, red
|
||||
=== "C#"
|
||||
|
||||
```csharp title="min_cost_climbing_stairs_dp.cs"
|
||||
[class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDPComp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
int MinCostClimbingStairsDPComp(int[] cost) {
|
||||
int n = cost.Length - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
int a = cost[1], b = cost[2];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int tmp = b;
|
||||
b = Math.Min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="min_cost_climbing_stairs_dp.go"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
func minCostClimbingStairsDPComp(cost []int) int {
|
||||
n := len(cost) - 1
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n]
|
||||
}
|
||||
min := func(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
a, b := cost[1], cost[2]
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for i := 3; i <= n; i++ {
|
||||
tmp := b
|
||||
b = min(a, tmp) + cost[i]
|
||||
a = tmp
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="min_cost_climbing_stairs_dp.swift"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
func minCostClimbingStairsDPComp(cost: [Int]) -> Int {
|
||||
let n = cost.count - 1
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n]
|
||||
}
|
||||
var (a, b) = (cost[1], cost[2])
|
||||
for i in 3 ... n {
|
||||
(a, b) = (b, min(a, b) + cost[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="min_cost_climbing_stairs_dp.js"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
function minCostClimbingStairsDPComp(cost) {
|
||||
const n = cost.length - 1;
|
||||
if (n === 1 || n === 2) {
|
||||
return cost[n];
|
||||
}
|
||||
let a = cost[1],
|
||||
b = cost[2];
|
||||
for (let i = 3; i <= n; i++) {
|
||||
const tmp = b;
|
||||
b = Math.min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="min_cost_climbing_stairs_dp.ts"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
function minCostClimbingStairsDPComp(cost: Array<number>): number {
|
||||
const n = cost.length - 1;
|
||||
if (n === 1 || n === 2) {
|
||||
return cost[n];
|
||||
}
|
||||
let a = cost[1],
|
||||
b = cost[2];
|
||||
for (let i = 3; i <= n; i++) {
|
||||
const tmp = b;
|
||||
b = Math.min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="min_cost_climbing_stairs_dp.dart"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
int minCostClimbingStairsDPComp(List<int> cost) {
|
||||
int n = cost.length - 1;
|
||||
if (n == 1 || n == 2) return cost[n];
|
||||
int a = cost[1], b = cost[2];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int tmp = b;
|
||||
b = min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="min_cost_climbing_stairs_dp.rs"
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
fn min_cost_climbing_stairs_dp_comp(cost: &[i32]) -> i32 {
|
||||
let n = cost.len() - 1;
|
||||
if n == 1 || n == 2 {
|
||||
return cost[n];
|
||||
};
|
||||
let (mut a, mut b) = (cost[1], cost[2]);
|
||||
for i in 3..=n {
|
||||
let tmp = b;
|
||||
b = cmp::min(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
b
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="min_cost_climbing_stairs_dp.c"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
int minCostClimbingStairsDPComp(int cost[], int costSize) {
|
||||
int n = costSize - 1;
|
||||
if (n == 1 || n == 2)
|
||||
return cost[n];
|
||||
int a = cost[1], b = cost[2];
|
||||
for (int i = 3; i <= n; i++) {
|
||||
int tmp = b;
|
||||
b = myMin(a, tmp) + cost[i];
|
||||
a = tmp;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="min_cost_climbing_stairs_dp.kt"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
/* Minimum cost climbing stairs: Space-optimized dynamic programming */
|
||||
fun minCostClimbingStairsDPComp(cost: IntArray): Int {
|
||||
val n = cost.size - 1
|
||||
if (n == 1 || n == 2) return cost[n]
|
||||
var a = cost[1]
|
||||
var b = cost[2]
|
||||
for (i in 3..n) {
|
||||
val tmp = b
|
||||
b = min(a, tmp) + cost[i]
|
||||
a = tmp
|
||||
}
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="min_cost_climbing_stairs_dp.rb"
|
||||
[class]{}-[func]{min_cost_climbing_stairs_dp_comp}
|
||||
### Minimum cost climbing stairs: DP ###
|
||||
def min_cost_climbing_stairs_dp(cost)
|
||||
n = cost.length - 1
|
||||
return cost[n] if n == 1 || n == 2
|
||||
# Initialize dp table, used to store solutions to subproblems
|
||||
dp = Array.new(n + 1, 0)
|
||||
# Initial state: preset the solution to the smallest subproblem
|
||||
dp[1], dp[2] = cost[1], cost[2]
|
||||
# State transition: gradually solve larger subproblems from smaller ones
|
||||
(3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] }
|
||||
dp[n]
|
||||
end
|
||||
|
||||
# Minimum cost climbing stairs: Space-optimized dynamic programming
|
||||
def min_cost_climbing_stairs_dp_comp(cost)
|
||||
n = cost.length - 1
|
||||
return cost[n] if n == 1 || n == 2
|
||||
a, b = cost[1], cost[2]
|
||||
(3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] }
|
||||
b
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 14.2.2 No Aftereffects
|
||||
|
||||
```zig title="min_cost_climbing_stairs_dp.zig"
|
||||
[class]{}-[func]{minCostClimbingStairsDPComp}
|
||||
```
|
||||
No aftereffects is one of the important characteristics that enable dynamic programming to solve problems effectively. Its definition is: **given a certain state, its future development is only related to the current state and has nothing to do with all past states**.
|
||||
|
||||
## 14.2.2 Statelessness
|
||||
|
||||
Statelessness is one of the important characteristics that make dynamic programming effective in solving problems. Its definition is: **Given a certain state, its future development is only related to the current state and unrelated to all past states experienced**.
|
||||
|
||||
Taking the stair climbing problem as an example, given state $i$, it will develop into states $i+1$ and $i+2$, corresponding to jumping 1 step and 2 steps respectively. When making these two choices, we do not need to consider the states before state $i$, as they do not affect the future of state $i$.
|
||||
Taking the stair climbing problem as an example, given state $i$, it will develop into states $i+1$ and $i+2$, corresponding to jumping $1$ step and jumping $2$ steps, respectively. When making these two choices, we do not need to consider the states before state $i$, as they have no effect on the future of state $i$.
|
||||
|
||||
However, if we add a constraint to the stair climbing problem, the situation changes.
|
||||
|
||||
!!! question "Stair climbing with constraints"
|
||||
!!! question "Climbing stairs with constraint"
|
||||
|
||||
Given a staircase with $n$ steps, you can go up 1 or 2 steps each time, **but you cannot jump 1 step twice in a row**. How many ways are there to climb to the top?
|
||||
Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time, **but you cannot jump $1$ step in two consecutive rounds**. How many ways are there to climb to the top?
|
||||
|
||||
As shown in Figure 14-8, there are only 2 feasible options for climbing to the 3rd step, among which the option of jumping 1 step three times in a row does not meet the constraint condition and is therefore discarded.
|
||||
As shown in Figure 14-8, there are only $2$ feasible ways to climb to the $3$rd step. The way of jumping $1$ step three consecutive times does not satisfy the constraint and is therefore discarded.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-8 Number of feasible options for climbing to the 3rd step with constraints </p>
|
||||
<p align="center"> Figure 14-8 Number of ways to climb to the 3rd step with constraint </p>
|
||||
|
||||
In this problem, if the last round was a jump of 1 step, then the next round must be a jump of 2 steps. This means that **the next step choice cannot be independently determined by the current state (current stair step), but also depends on the previous state (last round's stair step)**.
|
||||
In this problem, if the previous round was a jump of $1$ step, then the next round must jump $2$ steps. This means that **the next choice cannot be determined solely by the current state (current stair step number), but also depends on the previous state (the stair step number from the previous round)**.
|
||||
|
||||
It is not difficult to find that this problem no longer satisfies statelessness, and the state transition equation $dp[i] = dp[i-1] + dp[i-2]$ also fails, because $dp[i-1]$ represents this round's jump of 1 step, but it includes many "last round was a jump of 1 step" options, which, to meet the constraint, cannot be directly included in $dp[i]$.
|
||||
It is not difficult to see that this problem no longer satisfies no aftereffects, and the state transition equation $dp[i] = dp[i-1] + dp[i-2]$ also fails, because $dp[i-1]$ represents jumping $1$ step in this round, but it includes many solutions where "the previous round was a jump of $1$ step", which cannot be directly counted in $dp[i]$ to satisfy the constraint.
|
||||
|
||||
For this, we need to expand the state definition: **State $[i, j]$ represents being on the $i$-th step and the last round was a jump of $j$ steps**, where $j \in \{1, 2\}$. This state definition effectively distinguishes whether the last round was a jump of 1 step or 2 steps, and we can judge accordingly where the current state came from.
|
||||
For this reason, we need to expand the state definition: **state $[i, j]$ represents being on the $i$-th step with the previous round having jumped $j$ steps**, where $j \in \{1, 2\}$. This state definition effectively distinguishes whether the previous round was a jump of $1$ step or $2$ steps, allowing us to determine where the current state came from.
|
||||
|
||||
- When the last round was a jump of 1 step, the round before last could only choose to jump 2 steps, that is, $dp[i, 1]$ can only be transferred from $dp[i-1, 2]$.
|
||||
- When the last round was a jump of 2 steps, the round before last could choose to jump 1 step or 2 steps, that is, $dp[i, 2]$ can be transferred from $dp[i-2, 1]$ or $dp[i-2, 2]$.
|
||||
- When the previous round jumped $1$ step, the round before that could only choose to jump $2$ steps, i.e., $dp[i, 1]$ can only be transferred from $dp[i-1, 2]$.
|
||||
- When the previous round jumped $2$ steps, the round before that could choose to jump $1$ step or $2$ steps, i.e., $dp[i, 2]$ can be transferred from $dp[i-2, 1]$ or $dp[i-2, 2]$.
|
||||
|
||||
As shown in Figure 14-9, $dp[i, j]$ represents the number of solutions for state $[i, j]$. At this point, the state transition equation is:
|
||||
As shown in Figure 14-9, under this definition, $dp[i, j]$ represents the number of ways for state $[i, j]$. The state transition equation is then:
|
||||
|
||||
$$
|
||||
\begin{cases}
|
||||
@@ -326,22 +612,22 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2]
|
||||
\end{cases}
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-9 Recursive relationship considering constraints </p>
|
||||
<p align="center"> Figure 14-9 Recurrence relation considering constraints </p>
|
||||
|
||||
In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two representing the total number of solutions for climbing to the $n$-th step:
|
||||
Finally, return $dp[n, 1] + dp[n, 2]$, where the sum of the two represents the total number of ways to climb to the $n$-th step:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="climbing_stairs_constraint_dp.py"
|
||||
def climbing_stairs_constraint_dp(n: int) -> int:
|
||||
"""Constrained climbing stairs: Dynamic programming"""
|
||||
"""Climbing stairs with constraint: Dynamic programming"""
|
||||
if n == 1 or n == 2:
|
||||
return 1
|
||||
# Initialize dp table, used to store subproblem solutions
|
||||
# Initialize dp table, used to store solutions to subproblems
|
||||
dp = [[0] * 3 for _ in range(n + 1)]
|
||||
# Initial state: preset the smallest subproblem solution
|
||||
# Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1], dp[1][2] = 1, 0
|
||||
dp[2][1], dp[2][2] = 0, 1
|
||||
# State transition: gradually solve larger subproblems from smaller ones
|
||||
@@ -354,14 +640,14 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen
|
||||
=== "C++"
|
||||
|
||||
```cpp title="climbing_stairs_constraint_dp.cpp"
|
||||
/* Constrained climbing stairs: Dynamic programming */
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
int climbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// Initialize dp table, used to store subproblem solutions
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
vector<vector<int>> dp(n + 1, vector<int>(3, 0));
|
||||
// Initial state: preset the smallest subproblem solution
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
@@ -378,14 +664,14 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen
|
||||
=== "Java"
|
||||
|
||||
```java title="climbing_stairs_constraint_dp.java"
|
||||
/* Constrained climbing stairs: Dynamic programming */
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
int climbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// Initialize dp table, used to store subproblem solutions
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
int[][] dp = new int[n + 1][3];
|
||||
// Initial state: preset the smallest subproblem solution
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
@@ -402,75 +688,256 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen
|
||||
=== "C#"
|
||||
|
||||
```csharp title="climbing_stairs_constraint_dp.cs"
|
||||
[class]{climbing_stairs_constraint_dp}-[func]{ClimbingStairsConstraintDP}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
int ClimbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
int[,] dp = new int[n + 1, 3];
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1, 1] = 1;
|
||||
dp[1, 2] = 0;
|
||||
dp[2, 1] = 0;
|
||||
dp[2, 2] = 1;
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i, 1] = dp[i - 1, 2];
|
||||
dp[i, 2] = dp[i - 2, 1] + dp[i - 2, 2];
|
||||
}
|
||||
return dp[n, 1] + dp[n, 2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="climbing_stairs_constraint_dp.go"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
func climbingStairsConstraintDP(n int) int {
|
||||
if n == 1 || n == 2 {
|
||||
return 1
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
dp := make([][3]int, n+1)
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1
|
||||
dp[1][2] = 0
|
||||
dp[2][1] = 0
|
||||
dp[2][2] = 1
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for i := 3; i <= n; i++ {
|
||||
dp[i][1] = dp[i-1][2]
|
||||
dp[i][2] = dp[i-2][1] + dp[i-2][2]
|
||||
}
|
||||
return dp[n][1] + dp[n][2]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="climbing_stairs_constraint_dp.swift"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
func climbingStairsConstraintDP(n: Int) -> Int {
|
||||
if n == 1 || n == 2 {
|
||||
return 1
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1)
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1
|
||||
dp[1][2] = 0
|
||||
dp[2][1] = 0
|
||||
dp[2][2] = 1
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for i in 3 ... n {
|
||||
dp[i][1] = dp[i - 1][2]
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
|
||||
}
|
||||
return dp[n][1] + dp[n][2]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="climbing_stairs_constraint_dp.js"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
function climbingStairsConstraintDP(n) {
|
||||
if (n === 1 || n === 2) {
|
||||
return 1;
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
const dp = Array.from(new Array(n + 1), () => new Array(3));
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (let i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="climbing_stairs_constraint_dp.ts"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
function climbingStairsConstraintDP(n: number): number {
|
||||
if (n === 1 || n === 2) {
|
||||
return 1;
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
const dp = Array.from({ length: n + 1 }, () => new Array(3));
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (let i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="climbing_stairs_constraint_dp.dart"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
int climbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
List<List<int>> dp = List.generate(n + 1, (index) => List.filled(3, 0));
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
return dp[n][1] + dp[n][2];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="climbing_stairs_constraint_dp.rs"
|
||||
[class]{}-[func]{climbing_stairs_constraint_dp}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
fn climbing_stairs_constraint_dp(n: usize) -> i32 {
|
||||
if n == 1 || n == 2 {
|
||||
return 1;
|
||||
};
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
let mut dp = vec![vec![-1; 3]; n + 1];
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for i in 3..=n {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
dp[n][1] + dp[n][2]
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="climbing_stairs_constraint_dp.c"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
int climbingStairsConstraintDP(int n) {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1;
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
int **dp = malloc((n + 1) * sizeof(int *));
|
||||
for (int i = 0; i <= n; i++) {
|
||||
dp[i] = calloc(3, sizeof(int));
|
||||
}
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1;
|
||||
dp[1][2] = 0;
|
||||
dp[2][1] = 0;
|
||||
dp[2][2] = 1;
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (int i = 3; i <= n; i++) {
|
||||
dp[i][1] = dp[i - 1][2];
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2];
|
||||
}
|
||||
int res = dp[n][1] + dp[n][2];
|
||||
// Free memory
|
||||
for (int i = 0; i <= n; i++) {
|
||||
free(dp[i]);
|
||||
}
|
||||
free(dp);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="climbing_stairs_constraint_dp.kt"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
/* Climbing stairs with constraint: Dynamic programming */
|
||||
fun climbingStairsConstraintDP(n: Int): Int {
|
||||
if (n == 1 || n == 2) {
|
||||
return 1
|
||||
}
|
||||
// Initialize dp table, used to store solutions to subproblems
|
||||
val dp = Array(n + 1) { IntArray(3) }
|
||||
// Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1] = 1
|
||||
dp[1][2] = 0
|
||||
dp[2][1] = 0
|
||||
dp[2][2] = 1
|
||||
// State transition: gradually solve larger subproblems from smaller ones
|
||||
for (i in 3..n) {
|
||||
dp[i][1] = dp[i - 1][2]
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
|
||||
}
|
||||
return dp[n][1] + dp[n][2]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="climbing_stairs_constraint_dp.rb"
|
||||
[class]{}-[func]{climbing_stairs_constraint_dp}
|
||||
### Climbing stairs with constraint: DP ###
|
||||
def climbing_stairs_constraint_dp(n)
|
||||
return 1 if n == 1 || n == 2
|
||||
|
||||
# Initialize dp table, used to store solutions to subproblems
|
||||
dp = Array.new(n + 1) { Array.new(3, 0) }
|
||||
# Initial state: preset the solution to the smallest subproblem
|
||||
dp[1][1], dp[1][2] = 1, 0
|
||||
dp[2][1], dp[2][2] = 0, 1
|
||||
# State transition: gradually solve larger subproblems from smaller ones
|
||||
for i in 3...(n + 1)
|
||||
dp[i][1] = dp[i - 1][2]
|
||||
dp[i][2] = dp[i - 2][1] + dp[i - 2][2]
|
||||
end
|
||||
|
||||
dp[n][1] + dp[n][2]
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
In the above case, since we only need to consider one more preceding state, we can still make the problem satisfy no aftereffects by expanding the state definition. However, some problems have very severe "aftereffects".
|
||||
|
||||
```zig title="climbing_stairs_constraint_dp.zig"
|
||||
[class]{}-[func]{climbingStairsConstraintDP}
|
||||
```
|
||||
!!! question "Climbing stairs with obstacle generation"
|
||||
|
||||
In the above cases, since we only need to consider the previous state, we can still meet the statelessness by expanding the state definition. However, some problems have very serious "state effects".
|
||||
Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time. **It is stipulated that when climbing to the $i$-th step, the system will automatically place an obstacle on the $2i$-th step, and thereafter no round is allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the $2$nd and $3$rd steps, then afterwards you cannot jump to the $4$th and $6$th steps. How many ways are there to climb to the top?
|
||||
|
||||
!!! question "Stair climbing with obstacle generation"
|
||||
In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve.
|
||||
|
||||
Given a staircase with $n$ steps, you can go up 1 or 2 steps each time. **It is stipulated that when climbing to the $i$-th step, the system automatically places an obstacle on the $2i$-th step, and thereafter all rounds are not allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the 2nd and 3rd steps, then later you cannot jump to the 4th and 6th steps. How many ways are there to climb to the top?
|
||||
|
||||
In this problem, the next jump depends on all past states, as each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming often struggles to solve.
|
||||
|
||||
In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy statelessness. For these kinds of problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time.
|
||||
In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,80 +2,80 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 14.6 Edit distance problem
|
||||
# 14.6 Edit Distance Problem
|
||||
|
||||
Edit distance, also known as Levenshtein distance, refers to the minimum number of modifications required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.
|
||||
Edit distance, also known as Levenshtein distance, refers to the minimum number of edits required to transform one string into another, commonly used in information retrieval and natural language processing to measure the similarity between two sequences.
|
||||
|
||||
!!! question
|
||||
|
||||
Given two strings $s$ and $t$, return the minimum number of edits required to transform $s$ into $t$.
|
||||
|
||||
You can perform three types of edits on a string: insert a character, delete a character, or replace a character with any other character.
|
||||
You can perform three types of edit operations on a string: insert a character, delete a character, or replace a character with any other character.
|
||||
|
||||
As shown in Figure 14-27, transforming `kitten` into `sitting` requires 3 edits, including 2 replacements and 1 insertion; transforming `hello` into `algo` requires 3 steps, including 2 replacements and 1 deletion.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-27 Example data of edit distance </p>
|
||||
<p align="center"> Figure 14-27 Example data for edit distance </p>
|
||||
|
||||
**The edit distance problem can naturally be explained with a decision tree model**. Strings correspond to tree nodes, and a round of decision (an edit operation) corresponds to an edge of the tree.
|
||||
**The edit distance problem can be naturally explained using the decision tree model**. Strings correspond to tree nodes, and a round of decision (one edit operation) corresponds to an edge of the tree.
|
||||
|
||||
As shown in Figure 14-28, with unrestricted operations, each node can derive many edges, each corresponding to one operation, meaning there are many possible paths to transform `hello` into `algo`.
|
||||
As shown in Figure 14-28, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform `hello` into `algo`.
|
||||
|
||||
From the perspective of the decision tree, the goal of this problem is to find the shortest path between the node `hello` and the node `algo`.
|
||||
From the perspective of the decision tree, the goal of this problem is to find the shortest path between node `hello` and node `algo`.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-28 Edit distance problem represented based on decision tree model </p>
|
||||
<p align="center"> Figure 14-28 Representing edit distance problem based on decision tree model </p>
|
||||
|
||||
### 1. Dynamic programming approach
|
||||
### 1. Dynamic Programming Approach
|
||||
|
||||
**Step one: Think about each round of decision, define the state, thus obtaining the $dp$ table**
|
||||
**Step 1: Think about the decisions in each round, define the state, and thus obtain the $dp$ table**
|
||||
|
||||
Each round of decision involves performing one edit operation on string $s$.
|
||||
|
||||
We aim to gradually reduce the problem size during the edit process, which enables us to construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$, respectively. We first consider the tail characters of both strings $s[n-1]$ and $t[m-1]$.
|
||||
We want the problem scale to gradually decrease during the editing process, which allows us to construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$ respectively. We first consider the tail characters of the two strings, $s[n-1]$ and $t[m-1]$.
|
||||
|
||||
- If $s[n-1]$ and $t[m-1]$ are the same, we can skip them and directly consider $s[n-2]$ and $t[m-2]$.
|
||||
- If $s[n-1]$ and $t[m-1]$ are different, we need to perform one edit on $s$ (insert, delete, replace) so that the tail characters of the two strings match, allowing us to skip them and consider a smaller-scale problem.
|
||||
- If $s[n-1]$ and $t[m-1]$ are different, we need to perform one edit on $s$ (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem.
|
||||
|
||||
Thus, each round of decision (edit operation) in string $s$ changes the remaining characters in $s$ and $t$ to be matched. Therefore, the state is the $i$-th and $j$-th characters currently considered in $s$ and $t$, denoted as $[i, j]$.
|
||||
In other words, each round of decision (edit operation) we make on string $s$ will change the remaining characters to be matched in $s$ and $t$. Therefore, the state is the $i$-th and $j$-th characters currently being considered in $s$ and $t$, denoted as $[i, j]$.
|
||||
|
||||
State $[i, j]$ corresponds to the subproblem: **The minimum number of edits required to change the first $i$ characters of $s$ into the first $j$ characters of $t$**.
|
||||
State $[i, j]$ corresponds to the subproblem: **the minimum number of edits required to change the first $i$ characters of $s$ into the first $j$ characters of $t$**.
|
||||
|
||||
From this, we obtain a two-dimensional $dp$ table of size $(i+1) \times (j+1)$.
|
||||
|
||||
**Step two: Identify the optimal substructure and then derive the state transition equation**
|
||||
**Step 2: Identify the optimal substructure, and then derive the state transition equation**
|
||||
|
||||
Consider the subproblem $dp[i, j]$, whose corresponding tail characters of the two strings are $s[i-1]$ and $t[j-1]$, which can be divided into three scenarios as shown in Figure 14-29.
|
||||
Consider subproblem $dp[i, j]$, where the tail characters of the corresponding two strings are $s[i-1]$ and $t[j-1]$, which can be divided into the three cases shown in Figure 14-29 based on different edit operations.
|
||||
|
||||
1. Add $t[j-1]$ after $s[i-1]$, then the remaining subproblem is $dp[i, j-1]$.
|
||||
1. Insert $t[j-1]$ after $s[i-1]$, then the remaining subproblem is $dp[i, j-1]$.
|
||||
2. Delete $s[i-1]$, then the remaining subproblem is $dp[i-1, j]$.
|
||||
3. Replace $s[i-1]$ with $t[j-1]$, then the remaining subproblem is $dp[i-1, j-1]$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-29 State transition of edit distance </p>
|
||||
<p align="center"> Figure 14-29 State transition for edit distance </p>
|
||||
|
||||
Based on the analysis above, we can determine the optimal substructure: The minimum number of edits for $dp[i, j]$ is the minimum among $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the edit step $1$. The corresponding state transition equation is:
|
||||
Based on the above analysis, the optimal substructure can be obtained: the minimum number of edits for $dp[i, j]$ equals the minimum among the minimum edit steps of $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the edit step $1$ for this time. The corresponding state transition equation is:
|
||||
|
||||
$$
|
||||
dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1
|
||||
$$
|
||||
|
||||
Please note, **when $s[i-1]$ and $t[j-1]$ are the same, no edit is required for the current character**, in which case the state transition equation is:
|
||||
Please note that **when $s[i-1]$ and $t[j-1]$ are the same, no edit is required for the current character**, in which case the state transition equation is:
|
||||
|
||||
$$
|
||||
dp[i, j] = dp[i-1, j-1]
|
||||
$$
|
||||
|
||||
**Step three: Determine the boundary conditions and the order of state transitions**
|
||||
**Step 3: Determine boundary conditions and state transition order**
|
||||
|
||||
When both strings are empty, the number of edits is $0$, i.e., $dp[0, 0] = 0$. When $s$ is empty but $t$ is not, the minimum number of edits equals the length of $t$, that is, the first row $dp[0, j] = j$. When $s$ is not empty but $t$ is, the minimum number of edits equals the length of $s$, that is, the first column $dp[i, 0] = i$.
|
||||
When both strings are empty, the number of edit steps is $0$, i.e., $dp[0, 0] = 0$. When $s$ is empty but $t$ is not, the minimum number of edit steps equals the length of $t$, i.e., the first row $dp[0, j] = j$. When $s$ is not empty but $t$ is empty, the minimum number of edit steps equals the length of $s$, i.e., the first column $dp[i, 0] = i$.
|
||||
|
||||
Observing the state transition equation, solving $dp[i, j]$ depends on the solutions to the left, above, and upper left, so a double loop can be used to traverse the entire $dp$ table in the correct order.
|
||||
Observing the state transition equation, the solution $dp[i, j]$ depends on solutions to the left, above, and upper-left, so the entire $dp$ table can be traversed in order through two nested loops.
|
||||
|
||||
### 2. Code implementation
|
||||
### 2. Code Implementation
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -89,14 +89,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
|
||||
dp[i][0] = i
|
||||
for j in range(1, m + 1):
|
||||
dp[0][j] = j
|
||||
# State transition: the rest of the rows and columns
|
||||
# State transition: rest of the rows and columns
|
||||
for i in range(1, n + 1):
|
||||
for j in range(1, m + 1):
|
||||
if s[i - 1] == t[j - 1]:
|
||||
# If the two characters are equal, skip these two characters
|
||||
# If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
else:
|
||||
# The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
|
||||
# Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
|
||||
return dp[n][m]
|
||||
```
|
||||
@@ -115,14 +115,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// State transition: the rest of the rows and columns
|
||||
// State transition: rest of the rows and columns
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If the two characters are equal, skip these two characters
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
@@ -145,14 +145,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// State transition: the rest of the rows and columns
|
||||
// State transition: rest of the rows and columns
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s.charAt(i - 1) == t.charAt(j - 1)) {
|
||||
// If the two characters are equal, skip these two characters
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
@@ -164,73 +164,323 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut
|
||||
=== "C#"
|
||||
|
||||
```csharp title="edit_distance.cs"
|
||||
[class]{edit_distance}-[func]{EditDistanceDP}
|
||||
/* Edit distance: Dynamic programming */
|
||||
int EditDistanceDP(string s, string t) {
|
||||
int n = s.Length, m = t.Length;
|
||||
int[,] dp = new int[n + 1, m + 1];
|
||||
// State transition: first row and first column
|
||||
for (int i = 1; i <= n; i++) {
|
||||
dp[i, 0] = i;
|
||||
}
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0, j] = j;
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i, j] = dp[i - 1, j - 1];
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i, j] = Math.Min(Math.Min(dp[i, j - 1], dp[i - 1, j]), dp[i - 1, j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n, m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="edit_distance.go"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
/* Edit distance: Dynamic programming */
|
||||
func editDistanceDP(s string, t string) int {
|
||||
n := len(s)
|
||||
m := len(t)
|
||||
dp := make([][]int, n+1)
|
||||
for i := 0; i <= n; i++ {
|
||||
dp[i] = make([]int, m+1)
|
||||
}
|
||||
// State transition: first row and first column
|
||||
for i := 1; i <= n; i++ {
|
||||
dp[i][0] = i
|
||||
}
|
||||
for j := 1; j <= m; j++ {
|
||||
dp[0][j] = j
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for i := 1; i <= n; i++ {
|
||||
for j := 1; j <= m; j++ {
|
||||
if s[i-1] == t[j-1] {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i-1][j-1]
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = MinInt(MinInt(dp[i][j-1], dp[i-1][j]), dp[i-1][j-1]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="edit_distance.swift"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
/* Edit distance: Dynamic programming */
|
||||
func editDistanceDP(s: String, t: String) -> Int {
|
||||
let n = s.utf8CString.count
|
||||
let m = t.utf8CString.count
|
||||
var dp = Array(repeating: Array(repeating: 0, count: m + 1), count: n + 1)
|
||||
// State transition: first row and first column
|
||||
for i in 1 ... n {
|
||||
dp[i][0] = i
|
||||
}
|
||||
for j in 1 ... m {
|
||||
dp[0][j] = j
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for i in 1 ... n {
|
||||
for j in 1 ... m {
|
||||
if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="edit_distance.js"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
/* Edit distance: Dynamic programming */
|
||||
function editDistanceDP(s, t) {
|
||||
const n = s.length,
|
||||
m = t.length;
|
||||
const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
||||
// State transition: first row and first column
|
||||
for (let i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for (let i = 1; i <= n; i++) {
|
||||
for (let j = 1; j <= m; j++) {
|
||||
if (s.charAt(i - 1) === t.charAt(j - 1)) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] =
|
||||
Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="edit_distance.ts"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
/* Edit distance: Dynamic programming */
|
||||
function editDistanceDP(s: string, t: string): number {
|
||||
const n = s.length,
|
||||
m = t.length;
|
||||
const dp = Array.from({ length: n + 1 }, () =>
|
||||
Array.from({ length: m + 1 }, () => 0)
|
||||
);
|
||||
// State transition: first row and first column
|
||||
for (let i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for (let i = 1; i <= n; i++) {
|
||||
for (let j = 1; j <= m; j++) {
|
||||
if (s.charAt(i - 1) === t.charAt(j - 1)) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] =
|
||||
Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="edit_distance.dart"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
/* Edit distance: Dynamic programming */
|
||||
int editDistanceDP(String s, String t) {
|
||||
int n = s.length, m = t.length;
|
||||
List<List<int>> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0));
|
||||
// State transition: first row and first column
|
||||
for (int i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="edit_distance.rs"
|
||||
[class]{}-[func]{edit_distance_dp}
|
||||
/* Edit distance: Dynamic programming */
|
||||
fn edit_distance_dp(s: &str, t: &str) -> i32 {
|
||||
let (n, m) = (s.len(), t.len());
|
||||
let mut dp = vec![vec![0; m + 1]; n + 1];
|
||||
// State transition: first row and first column
|
||||
for i in 1..=n {
|
||||
dp[i][0] = i as i32;
|
||||
}
|
||||
for j in 1..m {
|
||||
dp[0][j] = j as i32;
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for i in 1..=n {
|
||||
for j in 1..=m {
|
||||
if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] =
|
||||
std::cmp::min(std::cmp::min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="edit_distance.c"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
/* Edit distance: Dynamic programming */
|
||||
int editDistanceDP(char *s, char *t, int n, int m) {
|
||||
int **dp = malloc((n + 1) * sizeof(int *));
|
||||
for (int i = 0; i <= n; i++) {
|
||||
dp[i] = calloc(m + 1, sizeof(int));
|
||||
}
|
||||
// State transition: first row and first column
|
||||
for (int i = 1; i <= n; i++) {
|
||||
dp[i][0] = i;
|
||||
}
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[0][j] = j;
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for (int i = 1; i <= n; i++) {
|
||||
for (int j = 1; j <= m; j++) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1];
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
int res = dp[n][m];
|
||||
// Free memory
|
||||
for (int i = 0; i <= n; i++) {
|
||||
free(dp[i]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="edit_distance.kt"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
/* Edit distance: Dynamic programming */
|
||||
fun editDistanceDP(s: String, t: String): Int {
|
||||
val n = s.length
|
||||
val m = t.length
|
||||
val dp = Array(n + 1) { IntArray(m + 1) }
|
||||
// State transition: first row and first column
|
||||
for (i in 1..n) {
|
||||
dp[i][0] = i
|
||||
}
|
||||
for (j in 1..m) {
|
||||
dp[0][j] = j
|
||||
}
|
||||
// State transition: rest of the rows and columns
|
||||
for (i in 1..n) {
|
||||
for (j in 1..m) {
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return dp[n][m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="edit_distance.rb"
|
||||
[class]{}-[func]{edit_distance_dp}
|
||||
### Edit distance: dynamic programming ###
|
||||
def edit_distance_dp(s, t)
|
||||
n, m = s.length, t.length
|
||||
dp = Array.new(n + 1) { Array.new(m + 1, 0) }
|
||||
# State transition: first row and first column
|
||||
(1...(n + 1)).each { |i| dp[i][0] = i }
|
||||
(1...(m + 1)).each { |j| dp[0][j] = j }
|
||||
# State transition: rest of the rows and columns
|
||||
for i in 1...(n + 1)
|
||||
for j in 1...(m +1)
|
||||
if s[i - 1] == t[j - 1]
|
||||
# If two characters are equal, skip both characters
|
||||
dp[i][j] = dp[i - 1][j - 1]
|
||||
else
|
||||
# Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
dp[n][m]
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="edit_distance.zig"
|
||||
[class]{}-[func]{editDistanceDP}
|
||||
```
|
||||
|
||||
As shown in Figure 14-30, the process of state transition in the edit distance problem is very similar to that in the knapsack problem, which can be seen as filling a two-dimensional grid.
|
||||
As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to the knapsack problem and can both be viewed as the process of filling a two-dimensional grid.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -274,13 +524,13 @@ As shown in Figure 14-30, the process of state transition in the edit distance p
|
||||
=== "<15>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 14-30 Dynamic programming process of edit distance </p>
|
||||
<p align="center"> Figure 14-30 Dynamic programming process for edit distance </p>
|
||||
|
||||
### 3. Space optimization
|
||||
### 3. Space Optimization
|
||||
|
||||
Since $dp[i, j]$ is derived from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper left $dp[i-1, j-1]$, and direct traversal will lose the upper left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, therefore, both traversal orders are not feasible.
|
||||
Since $dp[i, j]$ is transferred from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper-left $dp[i-1, j-1]$, forward traversal will lose the upper-left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, so neither traversal order is feasible.
|
||||
|
||||
For this reason, we can use a variable `leftup` to temporarily store the solution from the upper left $dp[i-1, j-1]$, thus only needing to consider the solutions to the left and above. This situation is similar to the unbounded knapsack problem, allowing for direct traversal. The code is as follows:
|
||||
For this reason, we can use a variable `leftup` to temporarily store the upper-left solution $dp[i-1, j-1]$, so we only need to consider the solutions to the left and above. This situation is the same as the unbounded knapsack problem, allowing for forward traversal. The code is as follows:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -292,21 +542,21 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
|
||||
# State transition: first row
|
||||
for j in range(1, m + 1):
|
||||
dp[j] = j
|
||||
# State transition: the rest of the rows
|
||||
# State transition: rest of the rows
|
||||
for i in range(1, n + 1):
|
||||
# State transition: first column
|
||||
leftup = dp[0] # Temporarily store dp[i-1, j-1]
|
||||
dp[0] += 1
|
||||
# State transition: the rest of the columns
|
||||
# State transition: rest of the columns
|
||||
for j in range(1, m + 1):
|
||||
temp = dp[j]
|
||||
if s[i - 1] == t[j - 1]:
|
||||
# If the two characters are equal, skip these two characters
|
||||
# If two characters are equal, skip both characters
|
||||
dp[j] = leftup
|
||||
else:
|
||||
# The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
|
||||
# Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = min(dp[j - 1], dp[j], leftup) + 1
|
||||
leftup = temp # Update for the next round of dp[i-1, j-1]
|
||||
leftup = temp # Update for next round's dp[i-1, j-1]
|
||||
return dp[m]
|
||||
```
|
||||
|
||||
@@ -321,22 +571,22 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// State transition: the rest of the rows
|
||||
// State transition: rest of the rows
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// State transition: first column
|
||||
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// State transition: the rest of the columns
|
||||
// State transition: rest of the columns
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If the two characters are equal, skip these two characters
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // Update for the next round of dp[i-1, j-1]
|
||||
leftup = temp; // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
@@ -354,22 +604,22 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// State transition: the rest of the rows
|
||||
// State transition: rest of the rows
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// State transition: first column
|
||||
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// State transition: the rest of the columns
|
||||
// State transition: rest of the columns
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s.charAt(i - 1) == t.charAt(j - 1)) {
|
||||
// If the two characters are equal, skip these two characters
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // Update for the next round of dp[i-1, j-1]
|
||||
leftup = temp; // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
@@ -379,65 +629,334 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio
|
||||
=== "C#"
|
||||
|
||||
```csharp title="edit_distance.cs"
|
||||
[class]{edit_distance}-[func]{EditDistanceDPComp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
int EditDistanceDPComp(string s, string t) {
|
||||
int n = s.Length, m = t.Length;
|
||||
int[] dp = new int[m + 1];
|
||||
// State transition: first row
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// State transition: first column
|
||||
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// State transition: rest of the columns
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="edit_distance.go"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
func editDistanceDPComp(s string, t string) int {
|
||||
n := len(s)
|
||||
m := len(t)
|
||||
dp := make([]int, m+1)
|
||||
// State transition: first row
|
||||
for j := 1; j <= m; j++ {
|
||||
dp[j] = j
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for i := 1; i <= n; i++ {
|
||||
// State transition: first column
|
||||
leftUp := dp[0] // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i
|
||||
// State transition: rest of the columns
|
||||
for j := 1; j <= m; j++ {
|
||||
temp := dp[j]
|
||||
if s[i-1] == t[j-1] {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftUp
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1
|
||||
}
|
||||
leftUp = temp // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="edit_distance.swift"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
func editDistanceDPComp(s: String, t: String) -> Int {
|
||||
let n = s.utf8CString.count
|
||||
let m = t.utf8CString.count
|
||||
var dp = Array(repeating: 0, count: m + 1)
|
||||
// State transition: first row
|
||||
for j in 1 ... m {
|
||||
dp[j] = j
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for i in 1 ... n {
|
||||
// State transition: first column
|
||||
var leftup = dp[0] // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i
|
||||
// State transition: rest of the columns
|
||||
for j in 1 ... m {
|
||||
let temp = dp[j]
|
||||
if s.utf8CString[i - 1] == t.utf8CString[j - 1] {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1
|
||||
}
|
||||
leftup = temp // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="edit_distance.js"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
function editDistanceDPComp(s, t) {
|
||||
const n = s.length,
|
||||
m = t.length;
|
||||
const dp = new Array(m + 1).fill(0);
|
||||
// State transition: first row
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for (let i = 1; i <= n; i++) {
|
||||
// State transition: first column
|
||||
let leftup = dp[0]; // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// State transition: rest of the columns
|
||||
for (let j = 1; j <= m; j++) {
|
||||
const temp = dp[j];
|
||||
if (s.charAt(i - 1) === t.charAt(j - 1)) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
|
||||
}
|
||||
leftup = temp; // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="edit_distance.ts"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
function editDistanceDPComp(s: string, t: string): number {
|
||||
const n = s.length,
|
||||
m = t.length;
|
||||
const dp = new Array(m + 1).fill(0);
|
||||
// State transition: first row
|
||||
for (let j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for (let i = 1; i <= n; i++) {
|
||||
// State transition: first column
|
||||
let leftup = dp[0]; // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// State transition: rest of the columns
|
||||
for (let j = 1; j <= m; j++) {
|
||||
const temp = dp[j];
|
||||
if (s.charAt(i - 1) === t.charAt(j - 1)) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1;
|
||||
}
|
||||
leftup = temp; // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="edit_distance.dart"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
int editDistanceDPComp(String s, String t) {
|
||||
int n = s.length, m = t.length;
|
||||
List<int> dp = List.filled(m + 1, 0);
|
||||
// State transition: first row
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// State transition: first column
|
||||
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// State transition: rest of the columns
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="edit_distance.rs"
|
||||
[class]{}-[func]{edit_distance_dp_comp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
fn edit_distance_dp_comp(s: &str, t: &str) -> i32 {
|
||||
let (n, m) = (s.len(), t.len());
|
||||
let mut dp = vec![0; m + 1];
|
||||
// State transition: first row
|
||||
for j in 1..m {
|
||||
dp[j] = j as i32;
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for i in 1..=n {
|
||||
// State transition: first column
|
||||
let mut leftup = dp[0]; // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i as i32;
|
||||
// State transition: rest of the columns
|
||||
for j in 1..=m {
|
||||
let temp = dp[j];
|
||||
if s.chars().nth(i - 1) == t.chars().nth(j - 1) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="edit_distance.c"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
int editDistanceDPComp(char *s, char *t, int n, int m) {
|
||||
int *dp = calloc(m + 1, sizeof(int));
|
||||
// State transition: first row
|
||||
for (int j = 1; j <= m; j++) {
|
||||
dp[j] = j;
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for (int i = 1; i <= n; i++) {
|
||||
// State transition: first column
|
||||
int leftup = dp[0]; // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i;
|
||||
// State transition: rest of the columns
|
||||
for (int j = 1; j <= m; j++) {
|
||||
int temp = dp[j];
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup;
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1;
|
||||
}
|
||||
leftup = temp; // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
int res = dp[m];
|
||||
// Free memory
|
||||
free(dp);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="edit_distance.kt"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
/* Edit distance: Space-optimized dynamic programming */
|
||||
fun editDistanceDPComp(s: String, t: String): Int {
|
||||
val n = s.length
|
||||
val m = t.length
|
||||
val dp = IntArray(m + 1)
|
||||
// State transition: first row
|
||||
for (j in 1..m) {
|
||||
dp[j] = j
|
||||
}
|
||||
// State transition: rest of the rows
|
||||
for (i in 1..n) {
|
||||
// State transition: first column
|
||||
var leftup = dp[0] // Temporarily store dp[i-1, j-1]
|
||||
dp[0] = i
|
||||
// State transition: rest of the columns
|
||||
for (j in 1..m) {
|
||||
val temp = dp[j]
|
||||
if (s[i - 1] == t[j - 1]) {
|
||||
// If two characters are equal, skip both characters
|
||||
dp[j] = leftup
|
||||
} else {
|
||||
// Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1
|
||||
}
|
||||
leftup = temp // Update for next round's dp[i-1, j-1]
|
||||
}
|
||||
}
|
||||
return dp[m]
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="edit_distance.rb"
|
||||
[class]{}-[func]{edit_distance_dp_comp}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="edit_distance.zig"
|
||||
[class]{}-[func]{editDistanceDPComp}
|
||||
### Edit distance: space-optimized DP ###
|
||||
def edit_distance_dp_comp(s, t)
|
||||
n, m = s.length, t.length
|
||||
dp = Array.new(m + 1, 0)
|
||||
# State transition: first row
|
||||
(1...(m + 1)).each { |j| dp[j] = j }
|
||||
# State transition: rest of the rows
|
||||
for i in 1...(n + 1)
|
||||
# State transition: first column
|
||||
leftup = dp.first # Temporarily store dp[i-1, j-1]
|
||||
dp[0] += 1
|
||||
# State transition: rest of the columns
|
||||
for j in 1...(m + 1)
|
||||
temp = dp[j]
|
||||
if s[i - 1] == t[j - 1]
|
||||
# If two characters are equal, skip both characters
|
||||
dp[j] = leftup
|
||||
else
|
||||
# Minimum edit steps = minimum edit steps of insert, delete, replace + 1
|
||||
dp[j] = [dp[j - 1], dp[j], leftup].min + 1
|
||||
end
|
||||
leftup = temp # Update for next round's dp[i-1, j-1]
|
||||
end
|
||||
end
|
||||
dp[m]
|
||||
end
|
||||
```
|
||||
|
||||
@@ -3,22 +3,22 @@ comments: true
|
||||
icon: material/table-pivot
|
||||
---
|
||||
|
||||
# Chapter 14. Dynamic programming
|
||||
# Chapter 14. Dynamic Programming
|
||||
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
Streams merge into rivers, and rivers merge into the sea.
|
||||
|
||||
Dynamic programming weaves smaller problems’ solutions into larger ones, guiding us step by step toward the far shore—where the ultimate answer awaits.
|
||||
Streams converge into rivers, rivers converge into the sea.
|
||||
|
||||
Dynamic programming gathers solutions to small problems into answers to large problems, step by step guiding us to the shore of problem-solving.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [14.1 Introduction to dynamic programming](intro_to_dynamic_programming.md)
|
||||
- [14.2 Characteristics of DP problems](dp_problem_features.md)
|
||||
- [14.3 DP problem-solving approach¶](dp_solution_pipeline.md)
|
||||
- [14.4 0-1 Knapsack problem](knapsack_problem.md)
|
||||
- [14.5 Unbounded knapsack problem](unbounded_knapsack_problem.md)
|
||||
- [14.6 Edit distance problem](edit_distance_problem.md)
|
||||
- [14.1 Introduction to Dynamic Programming](intro_to_dynamic_programming.md)
|
||||
- [14.2 Characteristics of Dynamic Programming Problems](dp_problem_features.md)
|
||||
- [14.3 Dynamic Programming Problem-Solving Approach](dp_solution_pipeline.md)
|
||||
- [14.4 0-1 Knapsack Problem](knapsack_problem.md)
|
||||
- [14.5 Unbounded Knapsack Problem](unbounded_knapsack_problem.md)
|
||||
- [14.6 Edit Distance Problem](edit_distance_problem.md)
|
||||
- [14.7 Summary](summary.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,24 +4,26 @@ comments: true
|
||||
|
||||
# 14.7 Summary
|
||||
|
||||
- Dynamic programming decomposes problems and improves computational efficiency by avoiding redundant computations through storing solutions of subproblems.
|
||||
- Without considering time, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree has many overlapping subproblems, resulting in very low efficiency. By introducing a memorization list, it's possible to store solutions of all computed subproblems, ensuring that overlapping subproblems are only computed once.
|
||||
- Memorization search is a top-down recursive solution, whereas dynamic programming corresponds to a bottom-up iterative approach, akin to "filling out a table." Since the current state only depends on certain local states, we can eliminate one dimension of the dp table to reduce space complexity.
|
||||
- Decomposition of subproblems is a universal algorithmic approach, differing in characteristics among divide and conquer, dynamic programming, and backtracking.
|
||||
- Dynamic programming problems have three main characteristics: overlapping subproblems, optimal substructure, and no aftereffects.
|
||||
- If the optimal solution of the original problem can be constructed from the optimal solutions of its subproblems, it has an optimal substructure.
|
||||
- No aftereffects mean that the future development of a state depends only on the current state and not on all past states experienced. Many combinatorial optimization problems do not have this property and cannot be quickly solved using dynamic programming.
|
||||
### 1. Key Review
|
||||
|
||||
- Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency.
|
||||
- Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once.
|
||||
- Memoization is a top-down recursive solution, while the corresponding dynamic programming is a bottom-up iterative solution, similar to "filling in a table". Since the current state only depends on certain local states, we can eliminate one dimension of the $dp$ table to reduce space complexity.
|
||||
- Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking.
|
||||
- Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects.
|
||||
- If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure.
|
||||
- No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not have no aftereffects and cannot be quickly solved using dynamic programming.
|
||||
|
||||
**Knapsack problem**
|
||||
|
||||
- The knapsack problem is one of the most typical dynamic programming problems, with variants including the 0-1 knapsack, unbounded knapsack, and multiple knapsacks.
|
||||
- The state definition of the 0-1 knapsack is the maximum value in a knapsack of capacity $c$ with the first $i$ items. Based on decisions not to include or to include an item in the knapsack, optimal substructures can be identified and state transition equations constructed. In space optimization, since each state depends on the state directly above and to the upper left, the list should be traversed in reverse order to avoid overwriting the upper left state.
|
||||
- In the unbounded knapsack problem, there is no limit on the number of each kind of item that can be chosen, thus the state transition for including items differs from the 0-1 knapsack. Since the state depends on the state directly above and to the left, space optimization should involve forward traversal.
|
||||
- The coin change problem is a variant of the unbounded knapsack problem, shifting from seeking the “maximum” value to seeking the “minimum” number of coins, thus the state transition equation should change $\max()$ to $\min()$. From pursuing “not exceeding” the capacity of the knapsack to seeking exactly the target amount, thus use $amt + 1$ to represent the invalid solution of “unable to make up the target amount.”
|
||||
- Coin Change Problem II shifts from seeking the “minimum number of coins” to seeking the “number of coin combinations,” changing the state transition equation accordingly from $\min()$ to summation operator.
|
||||
- The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack.
|
||||
- The state definition for the 0-1 knapsack is the maximum value among the first $i$ items in a knapsack of capacity $c$. Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state.
|
||||
- The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal.
|
||||
- The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the "maximum" value to seeking the "minimum" number of coins, so $\max()$ in the state transition equation should be changed to $\min()$. It changes from seeking "not exceeding" the knapsack capacity to seeking "exactly" making up the target amount, so $amt + 1$ is used to represent the invalid solution of "unable to make up the target amount".
|
||||
- Coin change problem II changes from seeking the "minimum number of coins" to seeking the "number of coin combinations", so the state transition equation correspondingly changes from $\min()$ to a summation operator.
|
||||
|
||||
**Edit distance problem**
|
||||
|
||||
- Edit distance (Levenshtein distance) measures the similarity between two strings, defined as the minimum number of editing steps needed to change one string into another, with editing operations including adding, deleting, or replacing.
|
||||
- The state definition for the edit distance problem is the minimum number of editing steps needed to change the first $i$ characters of $s$ into the first $j$ characters of $t$. When $s[i] \ne t[j]$, there are three decisions: add, delete, replace, each with their corresponding residual subproblems. From this, optimal substructures can be identified, and state transition equations built. When $s[i] = t[j]$, no editing of the current character is necessary.
|
||||
- In edit distance, the state depends on the state directly above, to the left, and to the upper left. Therefore, after space optimization, neither forward nor reverse traversal can correctly perform state transitions. To address this, we use a variable to temporarily store the upper left state, making it equivalent to the situation in the unbounded knapsack problem, allowing for forward traversal after space optimization.
|
||||
- Edit distance (Levenshtein distance) is used to measure the similarity between two strings, defined as the minimum number of edit steps from one string to another, with edit operations including insert, delete, and replace.
|
||||
- The state definition for the edit distance problem is the minimum number of edit steps required to change the first $i$ characters of $s$ into the first $j$ characters of $t$. When $s[i] \ne t[j]$, there are three decisions: insert, delete, replace, each with corresponding remaining subproblems. From this, the optimal substructure can be identified and the state transition equation constructed. When $s[i] = t[j]$, no edit is required for the current character.
|
||||
- In edit distance, the state depends on the state directly above, directly to the left, and to the upper-left, so after space optimization, neither forward nor reverse traversal can correctly perform state transitions. For this reason, we use a variable to temporarily store the upper-left state, thus transforming to a situation equivalent to the unbounded knapsack problem, allowing for forward traversal after space optimization.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ comments: true
|
||||
|
||||
# 9.1 Graph
|
||||
|
||||
A <u>graph</u> is a type of nonlinear data structure, consisting of <u>vertices</u> and <u>edges</u>. A graph $G$ can be abstractly represented as a collection of a set of vertices $V$ and a set of edges $E$. The following example shows a graph containing 5 vertices and 7 edges.
|
||||
A <u>graph</u> is a nonlinear data structure consisting of <u>vertices</u> and <u>edges</u>. We can abstractly represent a graph $G$ as a set of vertices $V$ and a set of edges $E$. The following example shows a graph containing 5 vertices and 7 edges.
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -14,33 +14,33 @@ G & = \{ V, E \} \newline
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
If vertices are viewed as nodes and edges as references (pointers) connecting the nodes, graphs can be seen as a data structure that extends from linked lists. As shown in Figure 9-1, **compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) are more complex due to their higher degree of freedom**.
|
||||
If we view vertices as nodes and edges as references (pointers) connecting the nodes, we can see graphs as a data structure extended from linked lists. As shown in Figure 9-1, **compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex**.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 9-1 Relationship between linked lists, trees, and graphs </p>
|
||||
<p align="center"> Figure 9-1 Relationships among linked lists, trees, and graphs </p>
|
||||
|
||||
## 9.1.1 Common types and terminologies of graphs
|
||||
## 9.1.1 Common Types and Terminology of Graphs
|
||||
|
||||
Graphs can be divided into <u>undirected graphs</u> and <u>directed graphs</u> depending on whether edges have direction, as shown in Figure 9-2.
|
||||
Graphs can be divided into <u>undirected graphs</u> and <u>directed graphs</u> based on whether edges have direction, as shown in Figure 9-2.
|
||||
|
||||
- In undirected graphs, edges represent a "bidirectional" connection between two vertices, for example, the "friends" in Facebook.
|
||||
- In directed graphs, edges have directionality, that is, the edges $A \rightarrow B$ and $A \leftarrow B$ are independent of each other. For example, the "follow" and "followed" relationship on Instagram or TikTok.
|
||||
- In undirected graphs, edges represent a "bidirectional" connection between two vertices, such as the "friend relationship" on WeChat or QQ.
|
||||
- In directed graphs, edges have directionality, meaning edges $A \rightarrow B$ and $A \leftarrow B$ are independent of each other, such as the "follow" and "be followed" relationships on Weibo or TikTok.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 9-2 Directed and undirected graphs </p>
|
||||
|
||||
Depending on whether all vertices are connected, graphs can be divided into <u>connected graphs</u> and <u>disconnected graphs</u>, as shown in Figure 9-3.
|
||||
Graphs can be divided into <u>connected graphs</u> and <u>disconnected graphs</u> based on whether all vertices are connected, as shown in Figure 9-3.
|
||||
|
||||
- For connected graphs, it is possible to reach any other vertex starting from an arbitrary vertex.
|
||||
- For disconnected graphs, there is at least one vertex that cannot be reached from an arbitrary starting vertex.
|
||||
- For connected graphs, starting from any vertex, all other vertices can be reached.
|
||||
- For disconnected graphs, starting from a certain vertex, at least one vertex cannot be reached.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 9-3 Connected and disconnected graphs </p>
|
||||
|
||||
We can also add a weight variable to edges, resulting in <u>weighted graphs</u> as shown in Figure 9-4. For example, in Instagram, the system sorts your follower and following list by the level of interaction between you and other users (likes, views, comments, etc.). Such an interaction network can be represented by a weighted graph.
|
||||
We can also add a "weight" variable to edges, resulting in <u>weighted graphs</u> as shown in Figure 9-4. For example, in mobile games like "Honor of Kings", the system calculates the "intimacy" between players based on their shared game time, and such intimacy networks can be represented using weighted graphs.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -48,56 +48,56 @@ We can also add a weight variable to edges, resulting in <u>weighted graphs</u>
|
||||
|
||||
Graph data structures include the following commonly used terms.
|
||||
|
||||
- <u>Adjacency</u>: When there is an edge connecting two vertices, these two vertices are said to be "adjacent". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5.
|
||||
- <u>Path</u>: The sequence of edges passed from vertex A to vertex B is called a path from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4.
|
||||
- <u>Degree</u>: The number of edges a vertex has. For directed graphs, <u>in-degree</u> refers to how many edges point to the vertex, and <u>out-degree</u> refers to how many edges point out from the vertex.
|
||||
- <u>Adjacency</u>: When two vertices are connected by an edge, these two vertices are said to be "adjacent". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5.
|
||||
- <u>Path</u>: The sequence of edges from vertex A to vertex B is called a "path" from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4.
|
||||
- <u>Degree</u>: The number of edges a vertex has. For directed graphs, <u>in-degree</u> indicates how many edges point to the vertex, and <u>out-degree</u> indicates how many edges point out from the vertex.
|
||||
|
||||
## 9.1.2 Representation of graphs
|
||||
## 9.1.2 Representation of Graphs
|
||||
|
||||
Common representations of graphs include "adjacency matrix" and "adjacency list". The following examples use undirected graphs.
|
||||
Common representations of graphs include "adjacency matrices" and "adjacency lists". The following uses undirected graphs as examples.
|
||||
|
||||
### 1. Adjacency matrix
|
||||
### 1. Adjacency Matrix
|
||||
|
||||
Let the number of vertices in the graph be $n$, the <u>adjacency matrix</u> uses an $n \times n$ matrix to represent the graph, where each row (column) represents a vertex, and the matrix elements represent edges, with $1$ or $0$ indicating whether there is an edge between two vertices.
|
||||
Given a graph with $n$ vertices, an <u>adjacency matrix</u> uses an $n \times n$ matrix to represent the graph, where each row (column) represents a vertex, and matrix elements represent edges, using $1$ or $0$ to indicate whether an edge exists between two vertices.
|
||||
|
||||
As shown in Figure 9-5, let the adjacency matrix be $M$, and the list of vertices be $V$, then the matrix element $M[i, j] = 1$ indicates there is an edge between vertex $V[i]$ and vertex $V[j]$, conversely $M[i, j] = 0$ indicates there is no edge between the two vertices.
|
||||
As shown in Figure 9-5, let the adjacency matrix be $M$ and the vertex list be $V$. Then matrix element $M[i, j] = 1$ indicates that an edge exists between vertex $V[i]$ and vertex $V[j]$, whereas $M[i, j] = 0$ indicates no edge between the two vertices.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 9-5 Representation of a graph with an adjacency matrix </p>
|
||||
<p align="center"> Figure 9-5 Adjacency matrix representation of a graph </p>
|
||||
|
||||
Adjacency matrices have the following characteristics.
|
||||
Adjacency matrices have the following properties.
|
||||
|
||||
- A vertex cannot be connected to itself, so the elements on the main diagonal of the adjacency matrix are meaningless.
|
||||
- For undirected graphs, edges in both directions are equivalent, thus the adjacency matrix is symmetric with regard to the main diagonal.
|
||||
- By replacing the elements of the adjacency matrix from $1$ and $0$ to weights, we can represent weighted graphs.
|
||||
- In simple graphs, vertices cannot connect to themselves, so the elements on the main diagonal of the adjacency matrix are meaningless.
|
||||
- For undirected graphs, edges in both directions are equivalent, so the adjacency matrix is symmetric about the main diagonal.
|
||||
- Replacing the elements of the adjacency matrix from $1$ and $0$ to weights allows representation of weighted graphs.
|
||||
|
||||
When representing graphs with adjacency matrices, it is possible to directly access matrix elements to obtain edges, resulting in efficient operations of addition, deletion, lookup, and modification, all with a time complexity of $O(1)$. However, the space complexity of the matrix is $O(n^2)$, which consumes more memory.
|
||||
When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of $O(1)$. However, the space complexity of the matrix is $O(n^2)$, which consumes significant memory.
|
||||
|
||||
### 2. Adjacency list
|
||||
### 2. Adjacency List
|
||||
|
||||
The <u>adjacency list</u> uses $n$ linked lists to represent the graph, with each linked list node representing a vertex. The $i$-th linked list corresponds to vertex $i$ and contains all adjacent vertices (vertices connected to that vertex). Figure 9-6 shows an example of a graph stored using an adjacency list.
|
||||
An <u>adjacency list</u> uses $n$ linked lists to represent a graph, with linked list nodes representing vertices. The $i$-th linked list corresponds to vertex $i$ and stores all adjacent vertices of that vertex (vertices connected to that vertex). Figure 9-6 shows an example of a graph stored using an adjacency list.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 9-6 Representation of a graph with an adjacency list </p>
|
||||
<p align="center"> Figure 9-6 Adjacency list representation of a graph </p>
|
||||
|
||||
The adjacency list only stores actual edges, and the total number of edges is often much less than $n^2$, making it more space-efficient. However, finding edges in the adjacency list requires traversing the linked list, so its time efficiency is not as good as that of the adjacency matrix.
|
||||
Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than $n^2$, making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so its time efficiency is inferior to that of adjacency matrices.
|
||||
|
||||
Observing Figure 9-6, **the structure of the adjacency list is very similar to the "chaining" in hash tables, hence we can use similar methods to optimize efficiency**. For example, when the linked list is long, it can be transformed into an AVL tree or red-black tree, thus optimizing the time efficiency from $O(n)$ to $O(\log n)$; the linked list can also be transformed into a hash table, thus reducing the time complexity to $O(1)$.
|
||||
Observing Figure 9-6, **the structure of adjacency lists is very similar to "chaining" in hash tables, so we can adopt similar methods to optimize efficiency**. For example, when linked lists are long, they can be converted to AVL trees or red-black trees, thereby optimizing time efficiency from $O(n)$ to $O(\log n)$; linked lists can also be converted to hash tables, thereby reducing time complexity to $O(1)$.
|
||||
|
||||
## 9.1.3 Common applications of graphs
|
||||
## 9.1.3 Common Applications of Graphs
|
||||
|
||||
As shown in Table 9-1, many real-world systems can be modeled with graphs, and corresponding problems can be reduced to graph computing problems.
|
||||
As shown in Table 9-1, many real-world systems can be modeled using graphs, and corresponding problems can be reduced to graph computation problems.
|
||||
|
||||
<p align="center"> Table 9-1 Common graphs in real life </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | Vertices | Edges | Graph Computing Problem |
|
||||
| --------------- | ---------------- | --------------------------------------------- | -------------------------------- |
|
||||
| Social Networks | Users | Follow / Followed | Potential Following Recommendations |
|
||||
| Subway Lines | Stations | Connectivity Between Stations | Shortest Route Recommendations |
|
||||
| Solar System | Celestial Bodies | Gravitational Forces Between Celestial Bodies | Planetary Orbit Calculations |
|
||||
| | Vertices | Edges | Graph Computation Problem |
|
||||
| -------------- | --------------- | -------------------------------------- | ----------------------------- |
|
||||
| Social network | Users | Friend relationships | Potential friend recommendation |
|
||||
| Subway lines | Stations | Connectivity between stations | Shortest route recommendation |
|
||||
| Solar system | Celestial bodies | Gravitational forces between celestial bodies | Planetary orbit calculation |
|
||||
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -9,13 +9,13 @@ icon: material/graphql
|
||||
|
||||
!!! abstract
|
||||
|
||||
In the journey of life, each of us is a node, connected by countless invisible edges.
|
||||
|
||||
Each encounter and parting leaves a unique imprint on this vast graph of life.
|
||||
In the journey of life, we are like nodes, connected by countless invisible edges.
|
||||
|
||||
Each encounter and parting leaves a unique mark on this vast network graph.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [9.1 Graph](graph.md)
|
||||
- [9.2 Basic graph operations](graph_operations.md)
|
||||
- [9.3 Graph traversal](graph_traversal.md)
|
||||
- [9.2 Basic Operations on Graphs](graph_operations.md)
|
||||
- [9.3 Graph Traversal](graph_traversal.md)
|
||||
- [9.4 Summary](summary.md)
|
||||
|
||||
@@ -4,32 +4,32 @@ comments: true
|
||||
|
||||
# 9.4 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
- A graph is made up of vertices and edges. It can be described as a set of vertices and a set of edges.
|
||||
- Compared to linear relationships (like linked lists) and hierarchical relationships (like trees), network relationships (graphs) offer greater flexibility, making them more complex.
|
||||
- In a directed graph, edges have directions. In a connected graph, any vertex can be reached from any other vertex. In a weighted graph, each edge has an associated weight variable.
|
||||
- An adjacency matrix is a way to represent a graph using matrix (2D array). The rows and columns represent the vertices. The matrix element value indicates whether there is an edge between two vertices, using $1$ for an edge or $0$ for no edge. Adjacency matrices are highly efficient for operations like adding, deleting, or checking edges, but they require more space.
|
||||
- An adjacency list is another common way to represent a graph using a collection of linked lists. Each vertex in the graph has a list that contains all its adjacent vertices. The $i^{th}$ list represents vertex $i$. Adjacency lists use less space compared to adjacency matrices. However, since it requires traversing the list to find edges, the time efficiency is lower.
|
||||
- When the linked lists in an adjacency list are long enough, they can be converted into red-black trees or hash tables to improve lookup efficiency.
|
||||
- From the perspective of algorithmic design, an adjacency matrix reflects the concept of "trading space for time", whereas an adjacency list reflects "trading time for space".
|
||||
- Graphs can be used to model various real-world systems, such as social networks, subway routes.
|
||||
- A tree is a special case of a graph, and tree traversal is also a special case of graph traversal.
|
||||
- Breadth-first traversal of a graph is a search method that expands layer by layer from near to far, typically using a queue.
|
||||
- Depth-first traversal of a graph is a search method that prioritizes reaching the end before backtracking when no further path is available. It is often implemented using recursion.
|
||||
- Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges.
|
||||
- Compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex.
|
||||
- Directed graphs have edges with directionality, connected graphs have all vertices reachable from any vertex, and weighted graphs have edges that each contain a weight variable.
|
||||
- Adjacency matrices use matrices to represent graphs, where each row (column) represents a vertex, and matrix elements represent edges, using $1$ or $0$ to indicate whether two vertices have an edge or not. Adjacency matrices are highly efficient for addition, deletion, lookup, and modification operations, but consume significant space.
|
||||
- Adjacency lists use multiple linked lists to represent graphs, where the $i$-th linked list corresponds to vertex $i$ and stores all adjacent vertices of that vertex. Adjacency lists are more space-efficient than adjacency matrices, but have lower time efficiency because they require traversing linked lists to find edges.
|
||||
- When linked lists in adjacency lists become too long, they can be converted to red-black trees or hash tables, thereby improving lookup efficiency.
|
||||
- From an algorithmic perspective, adjacency matrices embody "trading space for time", while adjacency lists embody "trading time for space".
|
||||
- Graphs can be used to model various real-world systems, such as social networks and subway lines.
|
||||
- Trees are a special case of graphs, and tree traversal is a special case of graph traversal.
|
||||
- Breadth-first search of graphs is a near-to-far, layer-by-layer expansion search method, typically implemented using a queue.
|
||||
- Depth-first search of graphs is a search method that prioritizes going as far as possible and backtracks when no path remains, commonly implemented using recursion.
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**: Is a path defined as a sequence of vertices or a sequence of edges?
|
||||
|
||||
In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.
|
||||
The definitions in different language versions of Wikipedia are inconsistent: the English version states "a path is a sequence of edges", while the Chinese version states "a path is a sequence of vertices". The following is the original English text: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices.
|
||||
|
||||
In this document, a path is considered a sequence of edges, rather than a sequence of vertices. This is because there might be multiple edges connecting two vertices, in which case each edge corresponds to a path.
|
||||
In this text, a path is viewed as a sequence of edges, not a sequence of vertices. This is because there may be multiple edges connecting two vertices, in which case each edge corresponds to a path.
|
||||
|
||||
**Q**: In a disconnected graph, are there points that cannot be traversed?
|
||||
**Q**: In a disconnected graph, will there be unreachable vertices?
|
||||
|
||||
In a disconnected graph, there is at least one vertex that cannot be reached from a specific point. To traverse a disconnected graph, you need to set multiple starting points to traverse all the connected components of the graph.
|
||||
In a disconnected graph, starting from a certain vertex, at least one vertex cannot be reached. Traversing a disconnected graph requires setting multiple starting points to traverse all connected components of the graph.
|
||||
|
||||
**Q**: In an adjacency list, does the order of "all vertices connected to that vertex" matter?
|
||||
**Q**: In an adjacency list, is there a requirement for the order of "all vertices connected to that vertex"?
|
||||
|
||||
It can be in any order. However, in real-world applications, it might be necessary to sort them according to certain rules, such as the order in which vertices are added, or the order of vertex values. This can help find vertices quickly with certain extreme values.
|
||||
It can be in any order. However, in practical applications, it may be necessary to sort according to specified rules, such as the order in which vertices were added, or the order of vertex values, which helps quickly find vertices "with certain extreme values".
|
||||
|
||||
@@ -2,42 +2,42 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.2 Fractional knapsack problem
|
||||
# 15.2 Fractional Knapsack Problem
|
||||
|
||||
!!! question
|
||||
|
||||
Given $n$ items, the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with a capacity of $cap$. Each item can be chosen only once, **but a part of the item can be selected, with its value calculated based on the proportion of the weight chosen**, what is the maximum value of the items in the knapsack under the limited capacity? An example is shown in Figure 15-3.
|
||||
Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can be selected only once, **but a portion of an item can be selected, with the value calculated based on the proportion of weight selected**, what is the maximum value of items in the knapsack under the limited capacity? An example is shown in Figure 15-3.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-3 Example data of the fractional knapsack problem </p>
|
||||
<p align="center"> Figure 15-3 Example data for the fractional knapsack problem </p>
|
||||
|
||||
The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, involving the current item $i$ and capacity $c$, aiming to maximize the value within the limited capacity of the knapsack.
|
||||
The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item $i$ and capacity $c$, and the goal being to maximize value under the limited knapsack capacity.
|
||||
|
||||
The difference is that, in this problem, only a part of an item can be chosen. As shown in Figure 15-4, **we can arbitrarily split the items and calculate the corresponding value based on the weight proportion**.
|
||||
The difference is that this problem allows selecting only a portion of an item. As shown in Figure 15-4, **we can arbitrarily split items and calculate the corresponding value based on the weight proportion**.
|
||||
|
||||
1. For item $i$, its value per unit weight is $val[i-1] / wgt[i-1]$, referred to as the unit value.
|
||||
2. Suppose we put a part of item $i$ with weight $w$ into the knapsack, then the value added to the knapsack is $w \times val[i-1] / wgt[i-1]$.
|
||||
1. For item $i$, its value per unit weight is $val[i-1] / wgt[i-1]$, referred to as unit value.
|
||||
2. Suppose we put a portion of item $i$ with weight $w$ into the knapsack, then the value added to the knapsack is $w \times val[i-1] / wgt[i-1]$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-4 Value per unit weight of the item </p>
|
||||
<p align="center"> Figure 15-4 Value of items per unit weight </p>
|
||||
|
||||
### 1. Greedy strategy determination
|
||||
### 1. Greedy Strategy Determination
|
||||
|
||||
Maximizing the total value of the items in the knapsack **essentially means maximizing the value per unit weight**. From this, the greedy strategy shown in Figure 15-5 can be deduced.
|
||||
Maximizing the total value of items in the knapsack **is essentially maximizing the value per unit weight of items**. From this, we can derive the greedy strategy shown in Figure 15-5.
|
||||
|
||||
1. Sort the items by their unit value from high to low.
|
||||
2. Iterate over all items, **greedily choosing the item with the highest unit value in each round**.
|
||||
3. If the remaining capacity of the knapsack is insufficient, use part of the current item to fill the knapsack.
|
||||
1. Sort items by unit value from high to low.
|
||||
2. Iterate through all items, **greedily selecting the item with the highest unit value in each round**.
|
||||
3. If the remaining knapsack capacity is insufficient, use a portion of the current item to fill the knapsack.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-5 Greedy strategy of the fractional knapsack problem </p>
|
||||
<p align="center"> Figure 15-5 Greedy strategy for the fractional knapsack problem </p>
|
||||
|
||||
### 2. Code implementation
|
||||
### 2. Code Implementation
|
||||
|
||||
We have created an `Item` class in order to sort the items by their unit value. We loop and make greedy choices until the knapsack is full, then exit and return the solution:
|
||||
We created an `Item` class to facilitate sorting items by unit value. We loop to make greedy selections, breaking when the knapsack is full and returning the solution:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -50,8 +50,8 @@ We have created an `Item` class in order to sort the items by their unit value.
|
||||
self.v = v # Item value
|
||||
|
||||
def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int:
|
||||
"""Fractional knapsack: Greedy"""
|
||||
# Create an item list, containing two properties: weight, value
|
||||
"""Fractional knapsack: Greedy algorithm"""
|
||||
# Create item list with two attributes: weight, value
|
||||
items = [Item(w, v) for w, v in zip(wgt, val)]
|
||||
# Sort by unit value item.v / item.w from high to low
|
||||
items.sort(key=lambda item: item.v / item.w, reverse=True)
|
||||
@@ -59,13 +59,13 @@ We have created an `Item` class in order to sort the items by their unit value.
|
||||
res = 0
|
||||
for item in items:
|
||||
if item.w <= cap:
|
||||
# If the remaining capacity is sufficient, put the entire item into the knapsack
|
||||
# If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v
|
||||
cap -= item.w
|
||||
else:
|
||||
# If the remaining capacity is insufficient, put part of the item into the knapsack
|
||||
# If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += (item.v / item.w) * cap
|
||||
# No remaining capacity left, thus break the loop
|
||||
# No remaining capacity, so break out of the loop
|
||||
break
|
||||
return res
|
||||
```
|
||||
@@ -83,9 +83,9 @@ We have created an `Item` class in order to sort the items by their unit value.
|
||||
}
|
||||
};
|
||||
|
||||
/* Fractional knapsack: Greedy */
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
double fractionalKnapsack(vector<int> &wgt, vector<int> &val, int cap) {
|
||||
// Create an item list, containing two properties: weight, value
|
||||
// Create item list with two attributes: weight, value
|
||||
vector<Item> items;
|
||||
for (int i = 0; i < wgt.size(); i++) {
|
||||
items.push_back(Item(wgt[i], val[i]));
|
||||
@@ -96,13 +96,13 @@ We have created an `Item` class in order to sort the items by their unit value.
|
||||
double res = 0;
|
||||
for (auto &item : items) {
|
||||
if (item.w <= cap) {
|
||||
// If the remaining capacity is sufficient, put the entire item into the knapsack
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// If the remaining capacity is insufficient, put part of the item into the knapsack
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += (double)item.v / item.w * cap;
|
||||
// No remaining capacity left, thus break the loop
|
||||
// No remaining capacity, so break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -124,9 +124,9 @@ We have created an `Item` class in order to sort the items by their unit value.
|
||||
}
|
||||
}
|
||||
|
||||
/* Fractional knapsack: Greedy */
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
double fractionalKnapsack(int[] wgt, int[] val, int cap) {
|
||||
// Create an item list, containing two properties: weight, value
|
||||
// Create item list with two attributes: weight, value
|
||||
Item[] items = new Item[wgt.length];
|
||||
for (int i = 0; i < wgt.length; i++) {
|
||||
items[i] = new Item(wgt[i], val[i]);
|
||||
@@ -137,13 +137,13 @@ We have created an `Item` class in order to sort the items by their unit value.
|
||||
double res = 0;
|
||||
for (Item item : items) {
|
||||
if (item.w <= cap) {
|
||||
// If the remaining capacity is sufficient, put the entire item into the knapsack
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// If the remaining capacity is insufficient, put part of the item into the knapsack
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += (double) item.v / item.w * cap;
|
||||
// No remaining capacity left, thus break the loop
|
||||
// No remaining capacity, so break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -154,104 +154,398 @@ We have created an `Item` class in order to sort the items by their unit value.
|
||||
=== "C#"
|
||||
|
||||
```csharp title="fractional_knapsack.cs"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
class Item(int w, int v) {
|
||||
public int w = w; // Item weight
|
||||
public int v = v; // Item value
|
||||
}
|
||||
|
||||
[class]{fractional_knapsack}-[func]{FractionalKnapsack}
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
double FractionalKnapsack(int[] wgt, int[] val, int cap) {
|
||||
// Create item list with two attributes: weight, value
|
||||
Item[] items = new Item[wgt.Length];
|
||||
for (int i = 0; i < wgt.Length; i++) {
|
||||
items[i] = new Item(wgt[i], val[i]);
|
||||
}
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w));
|
||||
// Loop for greedy selection
|
||||
double res = 0;
|
||||
foreach (Item item in items) {
|
||||
if (item.w <= cap) {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += (double)item.v / item.w * cap;
|
||||
// No remaining capacity, so break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="fractional_knapsack.go"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
type Item struct {
|
||||
w int // Item weight
|
||||
v int // Item value
|
||||
}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
func fractionalKnapsack(wgt []int, val []int, cap int) float64 {
|
||||
// Create item list with two attributes: weight, value
|
||||
items := make([]Item, len(wgt))
|
||||
for i := 0; i < len(wgt); i++ {
|
||||
items[i] = Item{wgt[i], val[i]}
|
||||
}
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return float64(items[i].v)/float64(items[i].w) > float64(items[j].v)/float64(items[j].w)
|
||||
})
|
||||
// Loop for greedy selection
|
||||
res := 0.0
|
||||
for _, item := range items {
|
||||
if item.w <= cap {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += float64(item.v)
|
||||
cap -= item.w
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += float64(item.v) / float64(item.w) * float64(cap)
|
||||
// No remaining capacity, so break out of the loop
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="fractional_knapsack.swift"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
class Item {
|
||||
var w: Int // Item weight
|
||||
var v: Int // Item value
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
init(w: Int, v: Int) {
|
||||
self.w = w
|
||||
self.v = v
|
||||
}
|
||||
}
|
||||
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
func fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double {
|
||||
// Create item list with two attributes: weight, value
|
||||
var items = zip(wgt, val).map { Item(w: $0, v: $1) }
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) }
|
||||
// Loop for greedy selection
|
||||
var res = 0.0
|
||||
var cap = cap
|
||||
for item in items {
|
||||
if item.w <= cap {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += Double(item.v)
|
||||
cap -= item.w
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += Double(item.v) / Double(item.w) * Double(cap)
|
||||
// No remaining capacity, so break out of the loop
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="fractional_knapsack.js"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
class Item {
|
||||
constructor(w, v) {
|
||||
this.w = w; // Item weight
|
||||
this.v = v; // Item value
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
function fractionalKnapsack(wgt, val, cap) {
|
||||
// Create item list with two attributes: weight, value
|
||||
const items = wgt.map((w, i) => new Item(w, val[i]));
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
items.sort((a, b) => b.v / b.w - a.v / a.w);
|
||||
// Loop for greedy selection
|
||||
let res = 0;
|
||||
for (const item of items) {
|
||||
if (item.w <= cap) {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += (item.v / item.w) * cap;
|
||||
// No remaining capacity, so break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="fractional_knapsack.ts"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
class Item {
|
||||
w: number; // Item weight
|
||||
v: number; // Item value
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
constructor(w: number, v: number) {
|
||||
this.w = w;
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
function fractionalKnapsack(wgt: number[], val: number[], cap: number): number {
|
||||
// Create item list with two attributes: weight, value
|
||||
const items: Item[] = wgt.map((w, i) => new Item(w, val[i]));
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
items.sort((a, b) => b.v / b.w - a.v / a.w);
|
||||
// Loop for greedy selection
|
||||
let res = 0;
|
||||
for (const item of items) {
|
||||
if (item.w <= cap) {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += (item.v / item.w) * cap;
|
||||
// No remaining capacity, so break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="fractional_knapsack.dart"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
class Item {
|
||||
int w; // Item weight
|
||||
int v; // Item value
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
Item(this.w, this.v);
|
||||
}
|
||||
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
double fractionalKnapsack(List<int> wgt, List<int> val, int cap) {
|
||||
// Create item list with two attributes: weight, value
|
||||
List<Item> items = List.generate(wgt.length, (i) => Item(wgt[i], val[i]));
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w));
|
||||
// Loop for greedy selection
|
||||
double res = 0;
|
||||
for (Item item in items) {
|
||||
if (item.w <= cap) {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += item.v / item.w * cap;
|
||||
// No remaining capacity, so break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="fractional_knapsack.rs"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
struct Item {
|
||||
w: i32, // Item weight
|
||||
v: i32, // Item value
|
||||
}
|
||||
|
||||
[class]{}-[func]{fractional_knapsack}
|
||||
impl Item {
|
||||
fn new(w: i32, v: i32) -> Self {
|
||||
Self { w, v }
|
||||
}
|
||||
}
|
||||
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
fn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 {
|
||||
// Create item list with two attributes: weight, value
|
||||
let mut items = wgt
|
||||
.iter()
|
||||
.zip(val.iter())
|
||||
.map(|(&w, &v)| Item::new(w, v))
|
||||
.collect::<Vec<Item>>();
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
items.sort_by(|a, b| {
|
||||
(b.v as f64 / b.w as f64)
|
||||
.partial_cmp(&(a.v as f64 / a.w as f64))
|
||||
.unwrap()
|
||||
});
|
||||
// Loop for greedy selection
|
||||
let mut res = 0.0;
|
||||
for item in &items {
|
||||
if item.w <= cap {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v as f64;
|
||||
cap -= item.w;
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += item.v as f64 / item.w as f64 * cap as f64;
|
||||
// No remaining capacity, so break out of the loop
|
||||
break;
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="fractional_knapsack.c"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
typedef struct {
|
||||
int w; // Item weight
|
||||
int v; // Item value
|
||||
} Item;
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
float fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) {
|
||||
// Create item list with two attributes: weight, value
|
||||
Item *items = malloc(sizeof(Item) * itemCount);
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
items[i] = (Item){.w = wgt[i], .v = val[i]};
|
||||
}
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity);
|
||||
// Loop for greedy selection
|
||||
float res = 0.0;
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
if (items[i].w <= cap) {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += items[i].v;
|
||||
cap -= items[i].w;
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += (float)cap / items[i].w * items[i].v;
|
||||
cap = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
free(items);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="fractional_knapsack.kt"
|
||||
[class]{Item}-[func]{}
|
||||
/* Item */
|
||||
class Item(
|
||||
val w: Int, // Item
|
||||
val v: Int // Item value
|
||||
)
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
/* Fractional knapsack: Greedy algorithm */
|
||||
fun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double {
|
||||
// Create item list with two attributes: weight, value
|
||||
var cap = c
|
||||
val items = arrayOfNulls<Item>(wgt.size)
|
||||
for (i in wgt.indices) {
|
||||
items[i] = Item(wgt[i], _val[i])
|
||||
}
|
||||
// Sort by unit value item.v / item.w from high to low
|
||||
items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) }
|
||||
// Loop for greedy selection
|
||||
var res = 0.0
|
||||
for (item in items) {
|
||||
if (item!!.w <= cap) {
|
||||
// If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v
|
||||
cap -= item.w
|
||||
} else {
|
||||
// If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += item.v.toDouble() / item.w * cap
|
||||
// No remaining capacity, so break out of the loop
|
||||
break
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="fractional_knapsack.rb"
|
||||
[class]{Item}-[func]{}
|
||||
### Item ###
|
||||
class Item
|
||||
attr_accessor :w # Item weight
|
||||
attr_accessor :v # Item value
|
||||
|
||||
[class]{}-[func]{fractional_knapsack}
|
||||
def initialize(w, v)
|
||||
@w = w
|
||||
@v = v
|
||||
end
|
||||
end
|
||||
|
||||
### Fractional knapsack: greedy ###
|
||||
def fractional_knapsack(wgt, val, cap)
|
||||
# Create item list with two attributes: weight, value
|
||||
items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) }
|
||||
# Sort by unit value item.v / item.w from high to low
|
||||
items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) }
|
||||
# Loop for greedy selection
|
||||
res = 0
|
||||
for item in items
|
||||
if item.w <= cap
|
||||
# If remaining capacity is sufficient, put the entire current item into the knapsack
|
||||
res += item.v
|
||||
cap -= item.w
|
||||
else
|
||||
# If remaining capacity is insufficient, put part of the current item into the knapsack
|
||||
res += (item.v.to_f / item.w) * cap
|
||||
# No remaining capacity, so break out of the loop
|
||||
break
|
||||
end
|
||||
end
|
||||
res
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
The time complexity of built-in sorting algorithms is usually $O(\log n)$, and the space complexity is usually $O(\log n)$ or $O(n)$, depending on the specific implementation of the programming language.
|
||||
|
||||
```zig title="fractional_knapsack.zig"
|
||||
[class]{Item}-[func]{}
|
||||
|
||||
[class]{}-[func]{fractionalKnapsack}
|
||||
```
|
||||
|
||||
Apart from sorting, in the worst case, the entire list of items needs to be traversed, **hence the time complexity is $O(n)$**, where $n$ is the number of items.
|
||||
Apart from sorting, in the worst case the entire item list needs to be traversed, **therefore the time complexity is $O(n)$**, where $n$ is the number of items.
|
||||
|
||||
Since an `Item` object list is initialized, **the space complexity is $O(n)$**.
|
||||
|
||||
### 3. Correctness proof
|
||||
### 3. Correctness Proof
|
||||
|
||||
Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value `res`, but the solution does not include item $x$.
|
||||
Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value of `res`, but this solution does not include item $x$.
|
||||
|
||||
Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item $x$. Since the unit value of item $x$ is the highest, the total value after replacement will definitely be greater than `res`. **This contradicts the assumption that `res` is the optimal solution, proving that the optimal solution must include item $x$**.
|
||||
Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item $x$. Since item $x$ has the highest unit value, the total value after replacement will definitely be greater than `res`. **This contradicts the assumption that `res` is the optimal solution, proving that the optimal solution must include item $x$**.
|
||||
|
||||
For other items in this solution, we can also construct the above contradiction. Overall, **items with greater unit value are always better choices**, proving that the greedy strategy is effective.
|
||||
For other items in this solution, we can also construct the above contradiction. In summary, **items with greater unit value are always better choices**, which proves that the greedy strategy is effective.
|
||||
|
||||
As shown in Figure 15-6, if the item weight and unit value are viewed as the horizontal and vertical axes of a two-dimensional chart respectively, the fractional knapsack problem can be transformed into "seeking the largest area enclosed within a limited horizontal axis range". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective.
|
||||
As shown in Figure 15-6, if we view item weight and item unit value as the horizontal and vertical axes of a two-dimensional chart respectively, then the fractional knapsack problem can be transformed into "finding the maximum area enclosed within a limited horizontal axis range". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
|
||||
@@ -2,22 +2,22 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.1 Greedy algorithms
|
||||
# 15.1 Greedy Algorithm
|
||||
|
||||
<u>Greedy algorithm</u> is a common algorithm for solving optimization problems, which fundamentally involves making the seemingly best choice at each decision-making stage of the problem, i.e., greedily making locally optimal decisions in hopes of finding a globally optimal solution. Greedy algorithms are concise and efficient, and are widely used in many practical problems.
|
||||
<u>Greedy algorithm</u> is a common algorithm for solving optimization problems. Its basic idea is to make the seemingly best choice at each decision stage of the problem, that is, to greedily make locally optimal decisions in hopes of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely applied in many practical problems.
|
||||
|
||||
Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as relying on the property of optimal substructure, but they operate differently.
|
||||
Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently.
|
||||
|
||||
- Dynamic programming considers all previous decisions at the current decision stage and uses solutions to past subproblems to construct solutions for the current subproblem.
|
||||
- Greedy algorithms do not consider past decisions; instead, they proceed with greedy choices, continually narrowing the scope of the problem until it is solved.
|
||||
- Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem.
|
||||
- Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved.
|
||||
|
||||
Let's first understand the working principle of the greedy algorithm through the example of "coin change," which has been introduced in the "Complete Knapsack Problem" chapter. I believe you are already familiar with it.
|
||||
We will first understand how greedy algorithms work through the example problem "coin change". This problem has already been introduced in the "Complete Knapsack Problem" chapter, so I believe you are not unfamiliar with it.
|
||||
|
||||
!!! question
|
||||
|
||||
Given $n$ types of coins, where the denomination of the $i$th type of coin is $coins[i - 1]$, and the target amount is $amt$, with each type of coin available indefinitely, what is the minimum number of coins needed to make up the target amount? If it is not possible to make up the target amount, return $-1$.
|
||||
Given $n$ types of coins, where the denomination of the $i$-th type of coin is $coins[i - 1]$, and the target amount is $amt$, with each type of coin available for repeated selection, what is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return $-1$.
|
||||
|
||||
The greedy strategy adopted in this problem is shown in Figure 15-1. Given the target amount, **we greedily choose the coin that is closest to and not greater than it**, repeatedly following this step until the target amount is met.
|
||||
The greedy strategy adopted for this problem is shown in Figure 15-1. Given a target amount, **we greedily select the coin that is not greater than and closest to it**, and continuously repeat this step until the target amount is reached.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -29,13 +29,13 @@ The implementation code is as follows:
|
||||
|
||||
```python title="coin_change_greedy.py"
|
||||
def coin_change_greedy(coins: list[int], amt: int) -> int:
|
||||
"""Coin change: Greedy"""
|
||||
# Assume coins list is ordered
|
||||
"""Coin change: Greedy algorithm"""
|
||||
# Assume coins list is sorted
|
||||
i = len(coins) - 1
|
||||
count = 0
|
||||
# Loop for greedy selection until no remaining amount
|
||||
# Loop to make greedy choices until no remaining amount
|
||||
while amt > 0:
|
||||
# Find the smallest coin close to and less than the remaining amount
|
||||
# Find the coin that is less than and closest to the remaining amount
|
||||
while i > 0 and coins[i] > amt:
|
||||
i -= 1
|
||||
# Choose coins[i]
|
||||
@@ -48,14 +48,14 @@ The implementation code is as follows:
|
||||
=== "C++"
|
||||
|
||||
```cpp title="coin_change_greedy.cpp"
|
||||
/* Coin change: Greedy */
|
||||
/* Coin change: Greedy algorithm */
|
||||
int coinChangeGreedy(vector<int> &coins, int amt) {
|
||||
// Assume coins list is ordered
|
||||
// Assume coins list is sorted
|
||||
int i = coins.size() - 1;
|
||||
int count = 0;
|
||||
// Loop for greedy selection until no remaining amount
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while (amt > 0) {
|
||||
// Find the smallest coin close to and less than the remaining amount
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
@@ -71,14 +71,14 @@ The implementation code is as follows:
|
||||
=== "Java"
|
||||
|
||||
```java title="coin_change_greedy.java"
|
||||
/* Coin change: Greedy */
|
||||
/* Coin change: Greedy algorithm */
|
||||
int coinChangeGreedy(int[] coins, int amt) {
|
||||
// Assume coins list is ordered
|
||||
// Assume coins list is sorted
|
||||
int i = coins.length - 1;
|
||||
int count = 0;
|
||||
// Loop for greedy selection until no remaining amount
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while (amt > 0) {
|
||||
// Find the smallest coin close to and less than the remaining amount
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
@@ -94,137 +94,310 @@ The implementation code is as follows:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="coin_change_greedy.cs"
|
||||
[class]{coin_change_greedy}-[func]{CoinChangeGreedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
int CoinChangeGreedy(int[] coins, int amt) {
|
||||
// Assume coins list is sorted
|
||||
int i = coins.Length - 1;
|
||||
int count = 0;
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while (amt > 0) {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// Choose coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="coin_change_greedy.go"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
func coinChangeGreedy(coins []int, amt int) int {
|
||||
// Assume coins list is sorted
|
||||
i := len(coins) - 1
|
||||
count := 0
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
for amt > 0 {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
for i > 0 && coins[i] > amt {
|
||||
i--
|
||||
}
|
||||
// Choose coins[i]
|
||||
amt -= coins[i]
|
||||
count++
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
if amt != 0 {
|
||||
return -1
|
||||
}
|
||||
return count
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="coin_change_greedy.swift"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
func coinChangeGreedy(coins: [Int], amt: Int) -> Int {
|
||||
// Assume coins list is sorted
|
||||
var i = coins.count - 1
|
||||
var count = 0
|
||||
var amt = amt
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while amt > 0 {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while i > 0 && coins[i] > amt {
|
||||
i -= 1
|
||||
}
|
||||
// Choose coins[i]
|
||||
amt -= coins[i]
|
||||
count += 1
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
return amt == 0 ? count : -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="coin_change_greedy.js"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
function coinChangeGreedy(coins, amt) {
|
||||
// Assume coins array is sorted
|
||||
let i = coins.length - 1;
|
||||
let count = 0;
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while (amt > 0) {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// Choose coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
return amt === 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="coin_change_greedy.ts"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
function coinChangeGreedy(coins: number[], amt: number): number {
|
||||
// Assume coins array is sorted
|
||||
let i = coins.length - 1;
|
||||
let count = 0;
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while (amt > 0) {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// Choose coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
return amt === 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="coin_change_greedy.dart"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
int coinChangeGreedy(List<int> coins, int amt) {
|
||||
// Assume coins list is sorted
|
||||
int i = coins.length - 1;
|
||||
int count = 0;
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while (amt > 0) {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// Choose coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="coin_change_greedy.rs"
|
||||
[class]{}-[func]{coin_change_greedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 {
|
||||
// Assume coins list is sorted
|
||||
let mut i = coins.len() - 1;
|
||||
let mut count = 0;
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while amt > 0 {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while i > 0 && coins[i] > amt {
|
||||
i -= 1;
|
||||
}
|
||||
// Choose coins[i]
|
||||
amt -= coins[i];
|
||||
count += 1;
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
if amt == 0 {
|
||||
count
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="coin_change_greedy.c"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
int coinChangeGreedy(int *coins, int size, int amt) {
|
||||
// Assume coins list is sorted
|
||||
int i = size - 1;
|
||||
int count = 0;
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while (amt > 0) {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while (i > 0 && coins[i] > amt) {
|
||||
i--;
|
||||
}
|
||||
// Choose coins[i]
|
||||
amt -= coins[i];
|
||||
count++;
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
return amt == 0 ? count : -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="coin_change_greedy.kt"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
/* Coin change: Greedy algorithm */
|
||||
fun coinChangeGreedy(coins: IntArray, amt: Int): Int {
|
||||
// Assume coins list is sorted
|
||||
var am = amt
|
||||
var i = coins.size - 1
|
||||
var count = 0
|
||||
// Loop to make greedy choices until no remaining amount
|
||||
while (am > 0) {
|
||||
// Find the coin that is less than and closest to the remaining amount
|
||||
while (i > 0 && coins[i] > am) {
|
||||
i--
|
||||
}
|
||||
// Choose coins[i]
|
||||
am -= coins[i]
|
||||
count++
|
||||
}
|
||||
// If no feasible solution is found, return -1
|
||||
return if (am == 0) count else -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="coin_change_greedy.rb"
|
||||
[class]{}-[func]{coin_change_greedy}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="coin_change_greedy.zig"
|
||||
[class]{}-[func]{coinChangeGreedy}
|
||||
### Coin change: greedy ###
|
||||
def coin_change_greedy(coins, amt)
|
||||
# Assume coins list is sorted
|
||||
i = coins.length - 1
|
||||
count = 0
|
||||
# Loop to make greedy choices until no remaining amount
|
||||
while amt > 0
|
||||
# Find the coin that is less than and closest to the remaining amount
|
||||
while i > 0 && coins[i] > amt
|
||||
i -= 1
|
||||
end
|
||||
# Choose coins[i]
|
||||
amt -= coins[i]
|
||||
count += 1
|
||||
end
|
||||
# Return -1 if no solution found
|
||||
amt == 0 ? count : -1
|
||||
end
|
||||
```
|
||||
|
||||
You might exclaim: So clean! The greedy algorithm solves the coin change problem in about ten lines of code.
|
||||
|
||||
## 15.1.1 Advantages and limitations of greedy algorithms
|
||||
## 15.1.1 Advantages and Limitations of Greedy Algorithms
|
||||
|
||||
**Greedy algorithms are not only straightforward and simple to implement, but they are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution, which is $O(n \times amt)$.
|
||||
**Greedy algorithms are not only straightforward and simple to implement, but are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution $O(n \times amt)$.
|
||||
|
||||
However, **for some combinations of coin denominations, greedy algorithms cannot find the optimal solution**. Figure 15-2 provides two examples.
|
||||
However, **for certain coin denomination combinations, greedy algorithms cannot find the optimal solution**. Figure 15-2 provides two examples.
|
||||
|
||||
- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: In this coin combination, given any $amt$, the greedy algorithm can find the optimal solution.
|
||||
- **Negative example $coins = [1, 20, 50]$**: Suppose $amt = 60$, the greedy algorithm can only find the combination $50 + 1 \times 10$, totaling 11 coins, but dynamic programming can find the optimal solution of $20 + 20 + 20$, needing only 3 coins.
|
||||
- **Negative example $coins = [1, 49, 50]$**: Suppose $amt = 98$, the greedy algorithm can only find the combination $50 + 1 \times 48$, totaling 49 coins, but dynamic programming can find the optimal solution of $49 + 49$, needing only 2 coins.
|
||||
- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: With this coin combination, given any $amt$, the greedy algorithm can find the optimal solution.
|
||||
- **Negative example $coins = [1, 20, 50]$**: Suppose $amt = 60$, the greedy algorithm can only find the combination $50 + 1 \times 10$, totaling $11$ coins, but dynamic programming can find the optimal solution $20 + 20 + 20$, requiring only $3$ coins.
|
||||
- **Negative example $coins = [1, 49, 50]$**: Suppose $amt = 98$, the greedy algorithm can only find the combination $50 + 1 \times 48$, totaling $49$ coins, but dynamic programming can find the optimal solution $49 + 49$, requiring only $2$ coins.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-2 Examples where greedy algorithms do not find the optimal solution </p>
|
||||
<p align="center"> Figure 15-2 Examples where greedy algorithms cannot find the optimal solution </p>
|
||||
|
||||
This means that for the coin change problem, greedy algorithms cannot guarantee finding the globally optimal solution, and they might find a very poor solution. They are better suited for dynamic programming.
|
||||
In other words, for the coin change problem, greedy algorithms cannot guarantee finding the global optimal solution, and may even find very poor solutions. It is better suited for solving with dynamic programming.
|
||||
|
||||
Generally, the suitability of greedy algorithms falls into two categories.
|
||||
Generally, the applicability of greedy algorithms falls into the following two situations.
|
||||
|
||||
1. **Guaranteed to find the optimal solution**: In these cases, greedy algorithms are often the best choice, as they tend to be more efficient than backtracking or dynamic programming.
|
||||
2. **Can find a near-optimal solution**: Greedy algorithms are also applicable here. For many complex problems, finding the global optimal solution is very challenging, and being able to find a high-efficiency suboptimal solution is also very commendable.
|
||||
1. **Can guarantee finding the optimal solution**: In this situation, greedy algorithms are often the best choice, because they tend to be more efficient than backtracking and dynamic programming.
|
||||
2. **Can find an approximate optimal solution**: Greedy algorithms are also applicable in this situation. For many complex problems, finding the global optimal solution is very difficult, and being able to find a suboptimal solution with high efficiency is also very good.
|
||||
|
||||
## 15.1.2 Characteristics of greedy algorithms
|
||||
## 15.1.2 Characteristics of Greedy Algorithms
|
||||
|
||||
So, what kind of problems are suitable for solving with greedy algorithms? Or rather, under what conditions can greedy algorithms guarantee to find the optimal solution?
|
||||
So the question arises: what kind of problems are suitable for solving with greedy algorithms? Or in other words, under what conditions can greedy algorithms guarantee finding the optimal solution?
|
||||
|
||||
Compared to dynamic programming, greedy algorithms have stricter usage conditions, focusing mainly on two properties of the problem.
|
||||
Compared to dynamic programming, the conditions for using greedy algorithms are stricter, mainly focusing on two properties of the problem.
|
||||
|
||||
- **Greedy choice property**: Only when the locally optimal choice can always lead to a globally optimal solution can greedy algorithms guarantee to obtain the optimal solution.
|
||||
- **Optimal substructure**: The optimal solution to the original problem contains the optimal solutions to its subproblems.
|
||||
- **Greedy choice property**: Only when locally optimal choices can always lead to a globally optimal solution can greedy algorithms guarantee obtaining the optimal solution.
|
||||
- **Optimal substructure**: The optimal solution to the original problem contains the optimal solutions to subproblems.
|
||||
|
||||
Optimal substructure has already been introduced in the "Dynamic Programming" chapter, so it is not discussed further here. It's important to note that some problems do not have an obvious optimal substructure, but can still be solved using greedy algorithms.
|
||||
Optimal substructure has already been introduced in the "Dynamic Programming" chapter, so we won't elaborate on it here. It's worth noting that the optimal substructure of some problems is not obvious, but they can still be solved using greedy algorithms.
|
||||
|
||||
We mainly explore the method for determining the greedy choice property. Although its description seems simple, **in practice, proving the greedy choice property for many problems is not easy**.
|
||||
We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, **in practice, for many problems, proving the greedy choice property is not easy**.
|
||||
|
||||
For example, in the coin change problem, although we can easily cite counterexamples to disprove the greedy choice property, proving it is much more challenging. If asked, **what conditions must a coin combination meet to be solvable using a greedy algorithm**? We often have to rely on intuition or examples to provide an ambiguous answer, as it is difficult to provide a rigorous mathematical proof.
|
||||
For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving it is quite difficult. If asked: **what conditions must a coin combination satisfy to be solvable using a greedy algorithm**? We often can only rely on intuition or examples to give an ambiguous answer, and find it difficult to provide a rigorous mathematical proof.
|
||||
|
||||
!!! quote
|
||||
|
||||
A paper presents an algorithm with a time complexity of $O(n^3)$ for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount.
|
||||
There is a paper that presents an algorithm with $O(n^3)$ time complexity for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount.
|
||||
|
||||
Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234.
|
||||
|
||||
## 15.1.3 Steps for solving problems with greedy algorithms
|
||||
## 15.1.3 Steps for Solving Problems with Greedy Algorithms
|
||||
|
||||
The problem-solving process for greedy problems can generally be divided into the following three steps.
|
||||
|
||||
1. **Problem analysis**: Sort out and understand the characteristics of the problem, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming.
|
||||
2. **Determine the greedy strategy**: Determine how to make a greedy choice at each step. This strategy can reduce the scale of the problem at each step and eventually solve the entire problem.
|
||||
3. **Proof of correctness**: It is usually necessary to prove that the problem has both a greedy choice property and optimal substructure. This step may require mathematical proofs, such as induction or reductio ad absurdum.
|
||||
1. **Problem analysis**: Sort out and understand the problem characteristics, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming.
|
||||
2. **Determine the greedy strategy**: Determine how to make greedy choices at each step. This strategy should be able to reduce the problem size at each step, ultimately solving the entire problem.
|
||||
3. **Correctness proof**: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical proofs, such as mathematical induction or proof by contradiction.
|
||||
|
||||
Determining the greedy strategy is the core step in solving the problem, but it may not be easy to implement, mainly for the following reasons.
|
||||
|
||||
- **Greedy strategies vary greatly between different problems**. For many problems, the greedy strategy is fairly straightforward, and we can come up with it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which is a real test of individual problem-solving experience and algorithmic capability.
|
||||
- **Some greedy strategies are quite misleading**. When we confidently design a greedy strategy, write the code, and submit it for testing, it is quite possible that some test cases will not pass. This is because the designed greedy strategy is only "partially correct," as described above with the coin change example.
|
||||
- **Greedy strategies differ greatly between different problems**. For many problems, the greedy strategy is relatively straightforward, and we can derive it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which really tests one's problem-solving experience and algorithmic ability.
|
||||
- **Some greedy strategies are highly misleading**. When we confidently design a greedy strategy, write the solution code and submit it for testing, we may find that some test cases cannot pass. This is because the designed greedy strategy is only "partially correct", as exemplified by the coin change problem discussed above.
|
||||
|
||||
To ensure accuracy, we should provide rigorous mathematical proofs for the greedy strategy, **usually involving reductio ad absurdum or mathematical induction**.
|
||||
To ensure correctness, we should rigorously mathematically prove the greedy strategy, **usually using proof by contradiction or mathematical induction**.
|
||||
|
||||
However, proving correctness may not be an easy task. If we are at a loss, we usually choose to debug the code based on test cases, modifying and verifying the greedy strategy step by step.
|
||||
However, correctness proofs may also not be easy. If we have no clue, we usually choose to debug the code based on test cases, step by step modifying and verifying the greedy strategy.
|
||||
|
||||
## 15.1.4 Typical problems solved by greedy algorithms
|
||||
## 15.1.4 Typical Problems Solved by Greedy Algorithms
|
||||
|
||||
Greedy algorithms are often applied to optimization problems that satisfy the properties of greedy choice and optimal substructure. Below are some typical greedy algorithm problems.
|
||||
Greedy algorithms are often applied to optimization problems that satisfy greedy choice property and optimal substructure. Below are some typical greedy algorithm problems.
|
||||
|
||||
- **Coin change problem**: In some coin combinations, the greedy algorithm always provides the optimal solution.
|
||||
- **Interval scheduling problem**: Suppose you have several tasks, each of which takes place over a period of time. Your goal is to complete as many tasks as possible. If you always choose the task that ends the earliest, then the greedy algorithm can achieve the optimal solution.
|
||||
- **Fractional knapsack problem**: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), the greedy algorithm can achieve the optimal solution in some cases.
|
||||
- **Stock trading problem**: Given a set of historical stock prices, you can make multiple trades, but you cannot buy again until after you have sold if you already own stocks. The goal is to achieve the maximum profit.
|
||||
- **Huffman coding**: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree, it always merges the two nodes with the lowest frequency, resulting in a Huffman tree with the minimum weighted path length (coding length).
|
||||
- **Coin change problem**: With certain coin combinations, greedy algorithms can always obtain the optimal solution.
|
||||
- **Interval scheduling problem**: Suppose you have some tasks, each taking place during a period of time, and your goal is to complete as many tasks as possible. If you always choose the task that ends earliest, then the greedy algorithm can obtain the optimal solution.
|
||||
- **Fractional knapsack problem**: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), then the greedy algorithm can obtain the optimal solution in some cases.
|
||||
- **Stock trading problem**: Given a set of historical stock prices, you can make multiple trades, but if you already hold stocks, you cannot buy again before selling, and the goal is to obtain the maximum profit.
|
||||
- **Huffman coding**: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree and always merging the two nodes with the lowest frequency, the resulting Huffman tree has the minimum weighted path length (encoding length).
|
||||
- **Dijkstra's algorithm**: It is a greedy algorithm for solving the shortest path problem from a given source vertex to all other vertices.
|
||||
|
||||
@@ -9,14 +9,14 @@ icon: material/head-heart-outline
|
||||
|
||||
!!! abstract
|
||||
|
||||
Sunflowers turn towards the sun, always seeking the greatest possible growth for themselves.
|
||||
Sunflowers turn toward the sun, constantly pursuing the maximum potential for their own growth.
|
||||
|
||||
Greedy strategy guides to the best answer step by step through rounds of simple choices.
|
||||
Through rounds of simple choices, greedy strategies gradually lead to the best answer.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [15.1 Greedy algorithms](greedy_algorithm.md)
|
||||
- [15.2 Fractional knapsack problem](fractional_knapsack_problem.md)
|
||||
- [15.3 Maximum capacity problem](max_capacity_problem.md)
|
||||
- [15.4 Maximum product cutting problem](max_product_cutting_problem.md)
|
||||
- [15.1 Greedy Algorithm](greedy_algorithm.md)
|
||||
- [15.2 Fractional Knapsack Problem](fractional_knapsack_problem.md)
|
||||
- [15.3 Maximum Capacity Problem](max_capacity_problem.md)
|
||||
- [15.4 Maximum Product Cutting Problem](max_product_cutting_problem.md)
|
||||
- [15.5 Summary](summary.md)
|
||||
|
||||
@@ -2,63 +2,63 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.3 Maximum capacity problem
|
||||
# 15.3 Max Capacity Problem
|
||||
|
||||
!!! question
|
||||
|
||||
Input an array $ht$, where each element represents the height of a vertical partition. Any two partitions in the array, along with the space between them, can form a container.
|
||||
|
||||
The capacity of the container is the product of the height and the width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions.
|
||||
|
||||
Please select two partitions in the array that maximize the container's capacity and return this maximum capacity. An example is shown in Figure 15-7.
|
||||
|
||||
{ class="animation-figure" }
|
||||
The capacity of the container equals the product of height and width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions.
|
||||
|
||||
<p align="center"> Figure 15-7 Example data for the maximum capacity problem </p>
|
||||
Please select two partitions in the array such that the capacity of the formed container is maximized, and return the maximum capacity. An example is shown in Figure 15-7.
|
||||
|
||||
The container is formed by any two partitions, **therefore the state of this problem is represented by the indices of the two partitions, denoted as $[i, j]$**.
|
||||
{ class="animation-figure" }
|
||||
|
||||
According to the problem statement, the capacity equals the product of height and width, where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions. The formula for capacity $cap[i, j]$ is:
|
||||
<p align="center"> Figure 15-7 Example data for the max capacity problem </p>
|
||||
|
||||
The container is formed by any two partitions, **therefore the state of this problem is the indices of two partitions, denoted as $[i, j]$**.
|
||||
|
||||
According to the problem description, capacity equals height multiplied by width, where height is determined by the shorter partition, and width is the difference in array indices between the two partitions. Let the capacity be $cap[i, j]$, then the calculation formula is:
|
||||
|
||||
$$
|
||||
cap[i, j] = \min(ht[i], ht[j]) \times (j - i)
|
||||
$$
|
||||
|
||||
Assuming the length of the array is $n$, the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. The most straightforward approach is to **enumerate all possible states**, resulting in a time complexity of $O(n^2)$.
|
||||
Let the array length be $n$, then the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. Most directly, **we can exhaustively enumerate all states** to find the maximum capacity, with time complexity $O(n^2)$.
|
||||
|
||||
### 1. Determination of a greedy strategy
|
||||
### 1. Greedy Strategy Determination
|
||||
|
||||
There is a more efficient solution to this problem. As shown in Figure 15-8, we select a state $[i, j]$ where the indices $i < j$ and the height $ht[i] < ht[j]$, meaning $i$ is the shorter partition, and $j$ is the taller one.
|
||||
This problem has a more efficient solution. As shown in Figure 15-8, select a state $[i, j]$ where index $i < j$ and height $ht[i] < ht[j]$, meaning $i$ is the short partition and $j$ is the long partition.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-8 Initial state </p>
|
||||
|
||||
As shown in Figure 15-9, **if we move the taller partition $j$ closer to the shorter partition $i$, the capacity will definitely decrease**.
|
||||
As shown in Figure 15-9, **if we now move the long partition $j$ closer to the short partition $i$, the capacity will definitely decrease**.
|
||||
|
||||
This is because when moving the taller partition $j$, the width $j-i$ definitely decreases; and since the height is determined by the shorter partition, the height can only remain the same (if $i$ remains the shorter partition) or decrease (if the moved $j$ becomes the shorter partition).
|
||||
This is because after moving the long partition $j$, the width $j-i$ definitely decreases; and since height is determined by the short partition, the height can only remain unchanged ($i$ is still the short partition) or decrease (the moved $j$ becomes the short partition).
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-9 State after moving the taller partition inward </p>
|
||||
<p align="center"> Figure 15-9 State after moving the long partition inward </p>
|
||||
|
||||
Conversely, **we can only possibly increase the capacity by moving the shorter partition $i$ inward**. Although the width will definitely decrease, **the height may increase** (if the moved shorter partition $i$ becomes taller). For example, in Figure 15-10, the area increases after moving the shorter partition.
|
||||
Conversely, **we can only possibly increase capacity by contracting the short partition $i$ inward**. Because although width will definitely decrease, **height may increase** (the moved short partition $i$ may become taller). For example, in Figure 15-10, the area increases after moving the short partition.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-10 State after moving the shorter partition inward </p>
|
||||
<p align="center"> Figure 15-10 State after moving the short partition inward </p>
|
||||
|
||||
This leads us to the greedy strategy for this problem: initialize two pointers at the ends of the container, and in each round, move the pointer corresponding to the shorter partition inward until the two pointers meet.
|
||||
From this we can derive the greedy strategy for this problem: initialize two pointers at both ends of the container, and in each round contract the pointer corresponding to the short partition inward, until the two pointers meet.
|
||||
|
||||
Figure 15-11 illustrate the execution of the greedy strategy.
|
||||
Figure 15-11 shows the execution process of the greedy strategy.
|
||||
|
||||
1. Initially, the pointers $i$ and $j$ are positioned at the ends of the array.
|
||||
2. Calculate the current state's capacity $cap[i, j]$ and update the maximum capacity.
|
||||
3. Compare the heights of partitions $i$ and $j$, and move the shorter partition inward by one step.
|
||||
4. Repeat steps `2.` and `3.` until $i$ and $j$ meet.
|
||||
1. In the initial state, pointers $i$ and $j$ are at both ends of the array.
|
||||
2. Calculate the capacity of the current state $cap[i, j]$, and update the maximum capacity.
|
||||
3. Compare the heights of partition $i$ and partition $j$, and move the short partition inward by one position.
|
||||
4. Loop through steps `2.` and `3.` until $i$ and $j$ meet.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -84,26 +84,26 @@ Figure 15-11 illustrate the execution of the greedy strategy.
|
||||
=== "<9>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-11 The greedy process for maximum capacity problem </p>
|
||||
<p align="center"> Figure 15-11 Greedy process for the max capacity problem </p>
|
||||
|
||||
### 2. Implementation
|
||||
### 2. Code Implementation
|
||||
|
||||
The code loops at most $n$ times, **thus the time complexity is $O(n)$**.
|
||||
The code loops at most $n$ rounds, **therefore the time complexity is $O(n)$**.
|
||||
|
||||
The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus the space complexity is $O(1)$**.
|
||||
Variables $i$, $j$, and $res$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="max_capacity.py"
|
||||
def max_capacity(ht: list[int]) -> int:
|
||||
"""Maximum capacity: Greedy"""
|
||||
# Initialize i, j, making them split the array at both ends
|
||||
"""Max capacity: Greedy algorithm"""
|
||||
# Initialize i, j to be at both ends of the array
|
||||
i, j = 0, len(ht) - 1
|
||||
# Initial maximum capacity is 0
|
||||
# Initial max capacity is 0
|
||||
res = 0
|
||||
# Loop for greedy selection until the two boards meet
|
||||
while i < j:
|
||||
# Update maximum capacity
|
||||
# Update max capacity
|
||||
cap = min(ht[i], ht[j]) * (j - i)
|
||||
res = max(res, cap)
|
||||
# Move the shorter board inward
|
||||
@@ -117,15 +117,15 @@ The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus t
|
||||
=== "C++"
|
||||
|
||||
```cpp title="max_capacity.cpp"
|
||||
/* Maximum capacity: Greedy */
|
||||
/* Max capacity: Greedy algorithm */
|
||||
int maxCapacity(vector<int> &ht) {
|
||||
// Initialize i, j, making them split the array at both ends
|
||||
// Initialize i, j to be at both ends of the array
|
||||
int i = 0, j = ht.size() - 1;
|
||||
// Initial maximum capacity is 0
|
||||
// Initial max capacity is 0
|
||||
int res = 0;
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while (i < j) {
|
||||
// Update maximum capacity
|
||||
// Update max capacity
|
||||
int cap = min(ht[i], ht[j]) * (j - i);
|
||||
res = max(res, cap);
|
||||
// Move the shorter board inward
|
||||
@@ -142,15 +142,15 @@ The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus t
|
||||
=== "Java"
|
||||
|
||||
```java title="max_capacity.java"
|
||||
/* Maximum capacity: Greedy */
|
||||
/* Max capacity: Greedy algorithm */
|
||||
int maxCapacity(int[] ht) {
|
||||
// Initialize i, j, making them split the array at both ends
|
||||
// Initialize i, j to be at both ends of the array
|
||||
int i = 0, j = ht.length - 1;
|
||||
// Initial maximum capacity is 0
|
||||
// Initial max capacity is 0
|
||||
int res = 0;
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while (i < j) {
|
||||
// Update maximum capacity
|
||||
// Update max capacity
|
||||
int cap = Math.min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.max(res, cap);
|
||||
// Move the shorter board inward
|
||||
@@ -167,83 +167,274 @@ The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus t
|
||||
=== "C#"
|
||||
|
||||
```csharp title="max_capacity.cs"
|
||||
[class]{max_capacity}-[func]{MaxCapacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
int MaxCapacity(int[] ht) {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
int i = 0, j = ht.Length - 1;
|
||||
// Initial max capacity is 0
|
||||
int res = 0;
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while (i < j) {
|
||||
// Update max capacity
|
||||
int cap = Math.Min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.Max(res, cap);
|
||||
// Move the shorter board inward
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="max_capacity.go"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
func maxCapacity(ht []int) int {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
i, j := 0, len(ht)-1
|
||||
// Initial max capacity is 0
|
||||
res := 0
|
||||
// Loop for greedy selection until the two boards meet
|
||||
for i < j {
|
||||
// Update max capacity
|
||||
capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i)
|
||||
res = int(math.Max(float64(res), float64(capacity)))
|
||||
// Move the shorter board inward
|
||||
if ht[i] < ht[j] {
|
||||
i++
|
||||
} else {
|
||||
j--
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="max_capacity.swift"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
func maxCapacity(ht: [Int]) -> Int {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
var i = ht.startIndex, j = ht.endIndex - 1
|
||||
// Initial max capacity is 0
|
||||
var res = 0
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while i < j {
|
||||
// Update max capacity
|
||||
let cap = min(ht[i], ht[j]) * (j - i)
|
||||
res = max(res, cap)
|
||||
// Move the shorter board inward
|
||||
if ht[i] < ht[j] {
|
||||
i += 1
|
||||
} else {
|
||||
j -= 1
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="max_capacity.js"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
function maxCapacity(ht) {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
let i = 0,
|
||||
j = ht.length - 1;
|
||||
// Initial max capacity is 0
|
||||
let res = 0;
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while (i < j) {
|
||||
// Update max capacity
|
||||
const cap = Math.min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.max(res, cap);
|
||||
// Move the shorter board inward
|
||||
if (ht[i] < ht[j]) {
|
||||
i += 1;
|
||||
} else {
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="max_capacity.ts"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
function maxCapacity(ht: number[]): number {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
let i = 0,
|
||||
j = ht.length - 1;
|
||||
// Initial max capacity is 0
|
||||
let res = 0;
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while (i < j) {
|
||||
// Update max capacity
|
||||
const cap: number = Math.min(ht[i], ht[j]) * (j - i);
|
||||
res = Math.max(res, cap);
|
||||
// Move the shorter board inward
|
||||
if (ht[i] < ht[j]) {
|
||||
i += 1;
|
||||
} else {
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="max_capacity.dart"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
int maxCapacity(List<int> ht) {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
int i = 0, j = ht.length - 1;
|
||||
// Initial max capacity is 0
|
||||
int res = 0;
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while (i < j) {
|
||||
// Update max capacity
|
||||
int cap = min(ht[i], ht[j]) * (j - i);
|
||||
res = max(res, cap);
|
||||
// Move the shorter board inward
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="max_capacity.rs"
|
||||
[class]{}-[func]{max_capacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
fn max_capacity(ht: &[i32]) -> i32 {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
let mut i = 0;
|
||||
let mut j = ht.len() - 1;
|
||||
// Initial max capacity is 0
|
||||
let mut res = 0;
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while i < j {
|
||||
// Update max capacity
|
||||
let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32;
|
||||
res = std::cmp::max(res, cap);
|
||||
// Move the shorter board inward
|
||||
if ht[i] < ht[j] {
|
||||
i += 1;
|
||||
} else {
|
||||
j -= 1;
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="max_capacity.c"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
int maxCapacity(int ht[], int htLength) {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
int i = 0;
|
||||
int j = htLength - 1;
|
||||
// Initial max capacity is 0
|
||||
int res = 0;
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while (i < j) {
|
||||
// Update max capacity
|
||||
int capacity = myMin(ht[i], ht[j]) * (j - i);
|
||||
res = myMax(res, capacity);
|
||||
// Move the shorter board inward
|
||||
if (ht[i] < ht[j]) {
|
||||
i++;
|
||||
} else {
|
||||
j--;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="max_capacity.kt"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
/* Max capacity: Greedy algorithm */
|
||||
fun maxCapacity(ht: IntArray): Int {
|
||||
// Initialize i, j to be at both ends of the array
|
||||
var i = 0
|
||||
var j = ht.size - 1
|
||||
// Initial max capacity is 0
|
||||
var res = 0
|
||||
// Loop for greedy selection until the two boards meet
|
||||
while (i < j) {
|
||||
// Update max capacity
|
||||
val cap = min(ht[i], ht[j]) * (j - i)
|
||||
res = max(res, cap)
|
||||
// Move the shorter board inward
|
||||
if (ht[i] < ht[j]) {
|
||||
i++
|
||||
} else {
|
||||
j--
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="max_capacity.rb"
|
||||
[class]{}-[func]{max_capacity}
|
||||
### Maximum capacity: greedy ###
|
||||
def max_capacity(ht)
|
||||
# Initialize i, j to be at both ends of the array
|
||||
i, j = 0, ht.length - 1
|
||||
# Initial max capacity is 0
|
||||
res = 0
|
||||
|
||||
# Loop for greedy selection until the two boards meet
|
||||
while i < j
|
||||
# Update max capacity
|
||||
cap = [ht[i], ht[j]].min * (j - i)
|
||||
res = [res, cap].max
|
||||
# Move the shorter board inward
|
||||
if ht[i] < ht[j]
|
||||
i += 1
|
||||
else
|
||||
j -= 1
|
||||
end
|
||||
end
|
||||
|
||||
res
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
### 3. Correctness Proof
|
||||
|
||||
```zig title="max_capacity.zig"
|
||||
[class]{}-[func]{maxCapacity}
|
||||
```
|
||||
The reason greedy is faster than exhaustive enumeration is that each round of greedy selection "skips" some states.
|
||||
|
||||
### 3. Proof of correctness
|
||||
|
||||
The reason why the greedy method is faster than enumeration is that each round of greedy selection "skips" some states.
|
||||
|
||||
For example, under the state $cap[i, j]$ where $i$ is the shorter partition and $j$ is the taller partition, greedily moving the shorter partition $i$ inward by one step leads to the "skipped" states shown in Figure 15-12. **This means that these states' capacities cannot be verified later**.
|
||||
For example, in state $cap[i, j]$ where $i$ is the short partition and $j$ is the long partition, if we greedily move the short partition $i$ inward by one position, the states shown in Figure 15-12 will be "skipped". **This means that the capacities of these states cannot be verified later**.
|
||||
|
||||
$$
|
||||
cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1]
|
||||
$$
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-12 States skipped by moving the shorter partition </p>
|
||||
<p align="center"> Figure 15-12 States skipped by moving the short partition </p>
|
||||
|
||||
It is observed that **these skipped states are actually all states where the taller partition $j$ is moved inward**. We have already proven that moving the taller partition inward will definitely decrease the capacity. Therefore, the skipped states cannot possibly be the optimal solution, **and skipping them does not lead to missing the optimal solution**.
|
||||
Observing carefully, **these skipped states are actually all the states obtained by moving the long partition $j$ inward**. We have already proven that moving the long partition inward will definitely decrease capacity. That is, the skipped states cannot possibly be the optimal solution, **skipping them will not cause us to miss the optimal solution**.
|
||||
|
||||
The analysis shows that the operation of moving the shorter partition is "safe", and the greedy strategy is effective.
|
||||
The above analysis shows that the operation of moving the short partition is "safe", and the greedy strategy is effective.
|
||||
|
||||
@@ -2,33 +2,33 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 15.4 Maximum product cutting problem
|
||||
# 15.4 Max Product Cutting Problem
|
||||
|
||||
!!! question
|
||||
|
||||
Given a positive integer $n$, split it into at least two positive integers that sum up to $n$, and find the maximum product of these integers, as illustrated in Figure 15-13.
|
||||
Given a positive integer $n$, split it into the sum of at least two positive integers, and find the maximum product of all integers after splitting, as shown in Figure 15-13.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-13 Definition of the maximum product cutting problem </p>
|
||||
<p align="center"> Figure 15-13 Problem definition of max product cutting </p>
|
||||
|
||||
Assume we split $n$ into $m$ integer factors, where the $i$-th factor is denoted as $n_i$, that is,
|
||||
Suppose we split $n$ into $m$ integer factors, where the $i$-th factor is denoted as $n_i$, that is
|
||||
|
||||
$$
|
||||
n = \sum_{i=1}^{m}n_i
|
||||
$$
|
||||
|
||||
The goal of this problem is to find the maximum product of all integer factors, namely,
|
||||
The goal of this problem is to find the maximum product of all integer factors, namely
|
||||
|
||||
$$
|
||||
\max(\prod_{i=1}^{m}n_i)
|
||||
$$
|
||||
|
||||
We need to consider: How large should the number of splits $m$ be, and what should each $n_i$ be?
|
||||
We need to think about: how large should the splitting count $m$ be, and what should each $n_i$ be?
|
||||
|
||||
### 1. Greedy strategy determination
|
||||
### 1. Greedy Strategy Determination
|
||||
|
||||
Experience suggests that the product of two integers is often greater than their sum. Suppose we split a factor of $2$ from $n$, then their product is $2(n-2)$. Compare this product with $n$:
|
||||
Based on experience, the product of two integers is often greater than their sum. Suppose we split out a factor of $2$ from $n$, then their product is $2(n-2)$. We compare this product with $n$:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
@@ -38,53 +38,53 @@ n & \geq 4
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
As shown in Figure 15-14, when $n \geq 4$, splitting out a $2$ increases the product, **which indicates that integers greater than or equal to $4$ should be split**.
|
||||
As shown in Figure 15-14, when $n \geq 4$, splitting out a $2$ will increase the product, **which indicates that integers greater than or equal to $4$ should all be split**.
|
||||
|
||||
**Greedy strategy one**: If the splitting scheme includes factors $\geq 4$, they should be further split. The final split should only include factors $1$, $2$, and $3$.
|
||||
**Greedy strategy one**: If the splitting scheme includes factors $\geq 4$, then they should continue to be split. The final splitting scheme should only contain factors $1$, $2$, and $3$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-14 Product increase due to splitting </p>
|
||||
<p align="center"> Figure 15-14 Splitting causes product to increase </p>
|
||||
|
||||
Next, consider which factor is optimal. Among the factors $1$, $2$, and $3$, clearly $1$ is the worst, as $1 \times (n-1) < n$ always holds, meaning splitting out $1$ actually decreases the product.
|
||||
Next, consider which factor is optimal. Among the three factors $1$, $2$, and $3$, clearly $1$ is the worst, because $1 \times (n-1) < n$ always holds, meaning splitting out $1$ will actually decrease the product.
|
||||
|
||||
As shown in Figure 15-15, when $n = 6$, $3 \times 3 > 2 \times 2 \times 2$. **This means splitting out $3$ is better than splitting out $2$**.
|
||||
As shown in Figure 15-15, when $n = 6$, we have $3 \times 3 > 2 \times 2 \times 2$. **This means that splitting out $3$ is better than splitting out $2$**.
|
||||
|
||||
**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s. Because three $2$s can always be replaced by two $3$s to obtain a higher product.
|
||||
**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s. Because three $2$s can always be replaced by two $3$s to obtain a larger product.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 15-15 Optimal splitting factors </p>
|
||||
<p align="center"> Figure 15-15 Optimal splitting factor </p>
|
||||
|
||||
From the above, the following greedy strategies can be derived.
|
||||
In summary, the following greedy strategies can be derived.
|
||||
|
||||
1. Input integer $n$, continually split out factor $3$ until the remainder is $0$, $1$, or $2$.
|
||||
2. When the remainder is $0$, it means $n$ is a multiple of $3$, so no further action is taken.
|
||||
3. When the remainder is $2$, do not continue to split, keep it.
|
||||
1. Input integer $n$, continuously split out factor $3$ until the remainder is $0$, $1$, or $2$.
|
||||
2. When the remainder is $0$, it means $n$ is a multiple of $3$, so no further action is needed.
|
||||
3. When the remainder is $2$, do not continue splitting, keep it.
|
||||
4. When the remainder is $1$, since $2 \times 2 > 1 \times 3$, the last $3$ should be replaced with $2$.
|
||||
|
||||
### 2. Code implementation
|
||||
### 2. Code Implementation
|
||||
|
||||
As shown in Figure 15-16, we do not need to use loops to split the integer but can use the floor division operation to get the number of $3$s, $a$, and the modulo operation to get the remainder, $b$, thus:
|
||||
As shown in Figure 15-16, we don't need to use loops to split the integer, but can use integer division to get the count of $3$s as $a$, and modulo operation to get the remainder as $b$, at which point we have:
|
||||
|
||||
$$
|
||||
n = 3a + b
|
||||
n = 3 a + b
|
||||
$$
|
||||
|
||||
Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, with a product of $1 \times (n - 1)$.
|
||||
Please note that for the edge case of $n \leq 3$, a $1$ must be split out, with product $1 \times (n - 1)$.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="max_product_cutting.py"
|
||||
def max_product_cutting(n: int) -> int:
|
||||
"""Maximum product of cutting: Greedy"""
|
||||
"""Max product cutting: Greedy algorithm"""
|
||||
# When n <= 3, must cut out a 1
|
||||
if n <= 3:
|
||||
return 1 * (n - 1)
|
||||
# Greedy cut out 3s, a is the number of 3s, b is the remainder
|
||||
# Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
a, b = n // 3, n % 3
|
||||
if b == 1:
|
||||
# When the remainder is 1, convert a pair of 1 * 3 into 2 * 2
|
||||
# When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return int(math.pow(3, a - 1)) * 2 * 2
|
||||
if b == 2:
|
||||
# When the remainder is 2, do nothing
|
||||
@@ -96,17 +96,17 @@ Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, wi
|
||||
=== "C++"
|
||||
|
||||
```cpp title="max_product_cutting.cpp"
|
||||
/* Maximum product of cutting: Greedy */
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
int maxProductCutting(int n) {
|
||||
// When n <= 3, must cut out a 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// Greedy cut out 3s, a is the number of 3s, b is the remainder
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 into 2 * 2
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return (int)pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
@@ -121,17 +121,17 @@ Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, wi
|
||||
=== "Java"
|
||||
|
||||
```java title="max_product_cutting.java"
|
||||
/* Maximum product of cutting: Greedy */
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
int maxProductCutting(int n) {
|
||||
// When n <= 3, must cut out a 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// Greedy cut out 3s, a is the number of 3s, b is the remainder
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 into 2 * 2
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return (int) Math.pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
@@ -146,84 +146,261 @@ Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, wi
|
||||
=== "C#"
|
||||
|
||||
```csharp title="max_product_cutting.cs"
|
||||
[class]{max_product_cutting}-[func]{MaxProductCutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
int MaxProductCutting(int n) {
|
||||
// When n <= 3, must cut out a 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return (int)Math.Pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
// When the remainder is 2, do nothing
|
||||
return (int)Math.Pow(3, a) * 2;
|
||||
}
|
||||
// When the remainder is 0, do nothing
|
||||
return (int)Math.Pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="max_product_cutting.go"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
func maxProductCutting(n int) int {
|
||||
// When n <= 3, must cut out a 1
|
||||
if n <= 3 {
|
||||
return 1 * (n - 1)
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
a := n / 3
|
||||
b := n % 3
|
||||
if b == 1 {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return int(math.Pow(3, float64(a-1))) * 2 * 2
|
||||
}
|
||||
if b == 2 {
|
||||
// When the remainder is 2, do nothing
|
||||
return int(math.Pow(3, float64(a))) * 2
|
||||
}
|
||||
// When the remainder is 0, do nothing
|
||||
return int(math.Pow(3, float64(a)))
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="max_product_cutting.swift"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
func maxProductCutting(n: Int) -> Int {
|
||||
// When n <= 3, must cut out a 1
|
||||
if n <= 3 {
|
||||
return 1 * (n - 1)
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
let a = n / 3
|
||||
let b = n % 3
|
||||
if b == 1 {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return pow(3, a - 1) * 2 * 2
|
||||
}
|
||||
if b == 2 {
|
||||
// When the remainder is 2, do nothing
|
||||
return pow(3, a) * 2
|
||||
}
|
||||
// When the remainder is 0, do nothing
|
||||
return pow(3, a)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="max_product_cutting.js"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
function maxProductCutting(n) {
|
||||
// When n <= 3, must cut out a 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
let a = Math.floor(n / 3);
|
||||
let b = n % 3;
|
||||
if (b === 1) {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return Math.pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b === 2) {
|
||||
// When the remainder is 2, do nothing
|
||||
return Math.pow(3, a) * 2;
|
||||
}
|
||||
// When the remainder is 0, do nothing
|
||||
return Math.pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="max_product_cutting.ts"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
function maxProductCutting(n: number): number {
|
||||
// When n <= 3, must cut out a 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
let a: number = Math.floor(n / 3);
|
||||
let b: number = n % 3;
|
||||
if (b === 1) {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return Math.pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b === 2) {
|
||||
// When the remainder is 2, do nothing
|
||||
return Math.pow(3, a) * 2;
|
||||
}
|
||||
// When the remainder is 0, do nothing
|
||||
return Math.pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="max_product_cutting.dart"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
int maxProductCutting(int n) {
|
||||
// When n <= 3, must cut out a 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
int a = n ~/ 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return (pow(3, a - 1) * 2 * 2).toInt();
|
||||
}
|
||||
if (b == 2) {
|
||||
// When the remainder is 2, do nothing
|
||||
return (pow(3, a) * 2).toInt();
|
||||
}
|
||||
// When the remainder is 0, do nothing
|
||||
return pow(3, a).toInt();
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="max_product_cutting.rs"
|
||||
[class]{}-[func]{max_product_cutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
fn max_product_cutting(n: i32) -> i32 {
|
||||
// When n <= 3, must cut out a 1
|
||||
if n <= 3 {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
let a = n / 3;
|
||||
let b = n % 3;
|
||||
if b == 1 {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
3_i32.pow(a as u32 - 1) * 2 * 2
|
||||
} else if b == 2 {
|
||||
// When the remainder is 2, do nothing
|
||||
3_i32.pow(a as u32) * 2
|
||||
} else {
|
||||
// When the remainder is 0, do nothing
|
||||
3_i32.pow(a as u32)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="max_product_cutting.c"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
int maxProductCutting(int n) {
|
||||
// When n <= 3, must cut out a 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1);
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
int a = n / 3;
|
||||
int b = n % 3;
|
||||
if (b == 1) {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return pow(3, a - 1) * 2 * 2;
|
||||
}
|
||||
if (b == 2) {
|
||||
// When the remainder is 2, do nothing
|
||||
return pow(3, a) * 2;
|
||||
}
|
||||
// When the remainder is 0, do nothing
|
||||
return pow(3, a);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="max_product_cutting.kt"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
/* Max product cutting: Greedy algorithm */
|
||||
fun maxProductCutting(n: Int): Int {
|
||||
// When n <= 3, must cut out a 1
|
||||
if (n <= 3) {
|
||||
return 1 * (n - 1)
|
||||
}
|
||||
// Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
val a = n / 3
|
||||
val b = n % 3
|
||||
if (b == 1) {
|
||||
// When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return 3.0.pow((a - 1)).toInt() * 2 * 2
|
||||
}
|
||||
if (b == 2) {
|
||||
// When the remainder is 2, do nothing
|
||||
return 3.0.pow(a).toInt() * 2 * 2
|
||||
}
|
||||
// When the remainder is 0, do nothing
|
||||
return 3.0.pow(a).toInt()
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="max_product_cutting.rb"
|
||||
[class]{}-[func]{max_product_cutting}
|
||||
### Maximum cutting product: greedy ###
|
||||
def max_product_cutting(n)
|
||||
# When n <= 3, must cut out a 1
|
||||
return 1 * (n - 1) if n <= 3
|
||||
# Greedily cut out 3, a is the number of 3s, b is the remainder
|
||||
a, b = n / 3, n % 3
|
||||
# When the remainder is 1, convert a pair of 1 * 3 to 2 * 2
|
||||
return (3.pow(a - 1) * 2 * 2).to_i if b == 1
|
||||
# When the remainder is 2, do nothing
|
||||
return (3.pow(a) * 2).to_i if b == 2
|
||||
# When the remainder is 0, do nothing
|
||||
3.pow(a).to_i
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
{ class="animation-figure" }
|
||||
|
||||
```zig title="max_product_cutting.zig"
|
||||
[class]{}-[func]{maxProductCutting}
|
||||
```
|
||||
<p align="center"> Figure 15-16 Calculation method for max product cutting </p>
|
||||
|
||||
{ class="animation-figure" }
|
||||
**The time complexity depends on the implementation of the exponentiation operation in the programming language**. Taking Python as an example, there are three commonly used power calculation functions.
|
||||
|
||||
<p align="center"> Figure 15-16 Calculation method of the maximum product after cutting </p>
|
||||
- Both the operator `**` and the function `pow()` have time complexity $O(\log a)$.
|
||||
- The function `math.pow()` internally calls the C library's `pow()` function, which performs floating-point exponentiation, with time complexity $O(1)$.
|
||||
|
||||
**Time complexity depends on the implementation of the power operation in the programming language**. For Python, the commonly used power calculation functions are three types:
|
||||
Variables $a$ and $b$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**.
|
||||
|
||||
- Both the operator `**` and the function `pow()` have a time complexity of $O(\log a)$.
|
||||
- The `math.pow()` function internally calls the C language library's `pow()` function, performing floating-point exponentiation, with a time complexity of $O(1)$.
|
||||
### 3. Correctness Proof
|
||||
|
||||
Variables $a$ and $b$ use constant size of extra space, **hence the space complexity is $O(1)$**.
|
||||
Using proof by contradiction, only analyzing the case where $n \geq 4$.
|
||||
|
||||
### 3. Correctness proof
|
||||
|
||||
Using the proof by contradiction, only analyze cases where $n \geq 3$.
|
||||
|
||||
1. **All factors $\leq 3$**: Assume the optimal splitting scheme includes a factor $x \geq 4$, then it can definitely be further split into $2(x-2)$, obtaining a larger product. This contradicts the assumption.
|
||||
2. **The splitting scheme does not contain $1$**: Assume the optimal splitting scheme includes a factor of $1$, then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption.
|
||||
3. **The splitting scheme contains at most two $2$s**: Assume the optimal splitting scheme includes three $2$s, then they can definitely be replaced by two $3$s, achieving a higher product. This contradicts the assumption.
|
||||
1. **All factors $\leq 3$**: Suppose the optimal splitting scheme includes a factor $x \geq 4$, then it can definitely continue to be split into $2(x-2)$ to obtain a larger (or equal) product. This contradicts the assumption.
|
||||
2. **The splitting scheme does not contain $1$**: Suppose the optimal splitting scheme includes a factor of $1$, then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption.
|
||||
3. **The splitting scheme contains at most two $2$s**: Suppose the optimal splitting scheme includes three $2$s, then they can definitely be replaced by two $3$s for a larger product. This contradicts the assumption.
|
||||
|
||||
@@ -4,13 +4,15 @@ comments: true
|
||||
|
||||
# 15.5 Summary
|
||||
|
||||
- Greedy algorithms are often used to solve optimization problems, where the principle is to make locally optimal decisions at each decision stage in order to achieve a globally optimal solution.
|
||||
- Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller sub-problem with each round, until the problem is resolved.
|
||||
- Greedy algorithms are not only simple to implement but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms generally have a lower time complexity.
|
||||
- In the problem of coin change, greedy algorithms can guarantee the optimal solution for certain combinations of coins; for others, however, the greedy algorithm might find a very poor solution.
|
||||
- Problems suitable for greedy algorithm solutions possess two main properties: greedy-choice property and optimal substructure. The greedy-choice property represents the effectiveness of the greedy strategy.
|
||||
- For some complex problems, proving the greedy-choice property is not straightforward. Contrarily, proving the invalidity is often easier, such as with the coin change problem.
|
||||
- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and proving correctness. Among these, determining the greedy strategy is the key step, while proving correctness often poses the challenge.
|
||||
- The fractional knapsack problem builds on the 0-1 knapsack problem by allowing the selection of a part of the items, hence it can be solved using a greedy algorithm. The correctness of the greedy strategy can be proved by contradiction.
|
||||
- The maximum capacity problem can be solved using the exhaustive method, with a time complexity of $O(n^2)$. By designing a greedy strategy, each round moves inwardly shortening the board, optimizing the time complexity to $O(n)$.
|
||||
- In the problem of maximum product after cutting, we deduce two greedy strategies: integers $\geq 4$ should continue to be cut, with the optimal cutting factor being $3$. The code includes power operations, and the time complexity depends on the method of implementing power operations, generally being $O(1)$ or $O(\log n)$.
|
||||
### 1. Key Review
|
||||
|
||||
- Greedy algorithms are typically used to solve optimization problems. The principle is to make locally optimal decisions at each decision stage in hopes of obtaining a globally optimal solution.
|
||||
- Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller subproblem in each round, until the problem is solved.
|
||||
- Greedy algorithms are not only simple to implement, but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms typically have lower time complexity.
|
||||
- In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions.
|
||||
- Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy.
|
||||
- For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem.
|
||||
- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the difficult point.
|
||||
- The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting a portion of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction.
|
||||
- The max capacity problem can be solved using exhaustive enumeration with time complexity $O(n^2)$. By designing a greedy strategy to move the short partition inward in each round, the time complexity can be optimized to $O(n)$.
|
||||
- In the max product cutting problem, we successively derive two greedy strategies: integers $\geq 4$ should all continue to be split, and the optimal splitting factor is $3$. The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being $O(1)$ or $O(\log n)$.
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 6.3 Hash algorithms
|
||||
# 6.3 Hash Algorithm
|
||||
|
||||
The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and chaining can **only ensure that the hash table functions normally when collisions occur, but cannot reduce the frequency of hash collisions**.
|
||||
The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and separate chaining **can only ensure that the hash table functions normally when hash collisions occur, but cannot reduce the frequency of hash collisions**.
|
||||
|
||||
If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in Figure 6-8, for a chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to $O(n)$.
|
||||
If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in Figure 6-8, for a separate chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to $O(n)$.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 6-8 Ideal and worst cases of hash collisions </p>
|
||||
|
||||
**The distribution of key-value pairs is determined by the hash function**. Recalling the steps of calculating a hash function, first compute the hash value, then modulo it by the array length:
|
||||
**The distribution of key-value pairs is determined by the hash function**. Recalling the calculation steps of the hash function, first compute the hash value, then take the modulo by the array length:
|
||||
|
||||
```shell
|
||||
index = hash(key) % capacity
|
||||
@@ -22,7 +22,7 @@ Observing the above formula, when the hash table capacity `capacity` is fixed, *
|
||||
|
||||
This means that, to reduce the probability of hash collisions, we should focus on the design of the hash algorithm `hash()`.
|
||||
|
||||
## 6.3.1 Goals of hash algorithms
|
||||
## 6.3.1 Goals of Hash Algorithms
|
||||
|
||||
To achieve a "fast and stable" hash table data structure, hash algorithms should have the following characteristics:
|
||||
|
||||
@@ -41,9 +41,9 @@ For cryptographic applications, to prevent reverse engineering such as deducing
|
||||
- **Collision resistance**: It should be extremely difficult to find two different inputs that produce the same hash value.
|
||||
- **Avalanche effect**: Minor changes in the input should lead to significant and unpredictable changes in the output.
|
||||
|
||||
Note that **"Uniform Distribution" and "Collision Resistance" are two separate concepts**. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input `key`, the hash function `key % 100` can produce a uniformly distributed output. However, this hash algorithm is too simple, and all `key` with the same last two digits will have the same output, making it easy to deduce a usable `key` from the hash value, thereby cracking the password.
|
||||
Note that **"uniform distribution" and "collision resistance" are two independent concepts**. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input `key`, the hash function `key % 100` can produce a uniformly distributed output. However, this hash algorithm is too simple, and all `key` with the same last two digits will have the same output, making it easy to deduce a usable `key` from the hash value, thereby cracking the password.
|
||||
|
||||
## 6.3.2 Design of hash algorithms
|
||||
## 6.3.2 Design of Hash Algorithms
|
||||
|
||||
The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms.
|
||||
|
||||
@@ -179,133 +179,467 @@ The design of hash algorithms is a complex issue that requires consideration of
|
||||
=== "C#"
|
||||
|
||||
```csharp title="simple_hash.cs"
|
||||
[class]{simple_hash}-[func]{AddHash}
|
||||
/* Additive hash */
|
||||
int AddHash(string key) {
|
||||
long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
foreach (char c in key) {
|
||||
hash = (hash + c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
[class]{simple_hash}-[func]{MulHash}
|
||||
/* Multiplicative hash */
|
||||
int MulHash(string key) {
|
||||
long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
foreach (char c in key) {
|
||||
hash = (31 * hash + c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
[class]{simple_hash}-[func]{XorHash}
|
||||
/* XOR hash */
|
||||
int XorHash(string key) {
|
||||
int hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
foreach (char c in key) {
|
||||
hash ^= c;
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
[class]{simple_hash}-[func]{RotHash}
|
||||
/* Rotational hash */
|
||||
int RotHash(string key) {
|
||||
long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
foreach (char c in key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="simple_hash.go"
|
||||
[class]{}-[func]{addHash}
|
||||
/* Additive hash */
|
||||
func addHash(key string) int {
|
||||
var hash int64
|
||||
var modulus int64
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
modulus = 1000000007
|
||||
for _, b := range []byte(key) {
|
||||
hash = (hash + int64(b)) % modulus
|
||||
}
|
||||
return int(hash)
|
||||
}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
/* Multiplicative hash */
|
||||
func mulHash(key string) int {
|
||||
var hash int64
|
||||
var modulus int64
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
modulus = 1000000007
|
||||
for _, b := range []byte(key) {
|
||||
hash = (31*hash + int64(b)) % modulus
|
||||
}
|
||||
return int(hash)
|
||||
}
|
||||
|
||||
/* XOR hash */
|
||||
func xorHash(key string) int {
|
||||
hash := 0
|
||||
modulus := 1000000007
|
||||
for _, b := range []byte(key) {
|
||||
fmt.Println(int(b))
|
||||
hash ^= int(b)
|
||||
hash = (31*hash + int(b)) % modulus
|
||||
}
|
||||
return hash & modulus
|
||||
}
|
||||
|
||||
/* Rotational hash */
|
||||
func rotHash(key string) int {
|
||||
var hash int64
|
||||
var modulus int64
|
||||
|
||||
modulus = 1000000007
|
||||
for _, b := range []byte(key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus
|
||||
}
|
||||
return int(hash)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="simple_hash.swift"
|
||||
[class]{}-[func]{addHash}
|
||||
/* Additive hash */
|
||||
func addHash(key: String) -> Int {
|
||||
var hash = 0
|
||||
let MODULUS = 1_000_000_007
|
||||
for c in key {
|
||||
for scalar in c.unicodeScalars {
|
||||
hash = (hash + Int(scalar.value)) % MODULUS
|
||||
}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
/* Multiplicative hash */
|
||||
func mulHash(key: String) -> Int {
|
||||
var hash = 0
|
||||
let MODULUS = 1_000_000_007
|
||||
for c in key {
|
||||
for scalar in c.unicodeScalars {
|
||||
hash = (31 * hash + Int(scalar.value)) % MODULUS
|
||||
}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
/* XOR hash */
|
||||
func xorHash(key: String) -> Int {
|
||||
var hash = 0
|
||||
let MODULUS = 1_000_000_007
|
||||
for c in key {
|
||||
for scalar in c.unicodeScalars {
|
||||
hash ^= Int(scalar.value)
|
||||
}
|
||||
}
|
||||
return hash & MODULUS
|
||||
}
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
/* Rotational hash */
|
||||
func rotHash(key: String) -> Int {
|
||||
var hash = 0
|
||||
let MODULUS = 1_000_000_007
|
||||
for c in key {
|
||||
for scalar in c.unicodeScalars {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS
|
||||
}
|
||||
}
|
||||
return hash
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="simple_hash.js"
|
||||
[class]{}-[func]{addHash}
|
||||
/* Additive hash */
|
||||
function addHash(key) {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = (hash + c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
/* Multiplicative hash */
|
||||
function mulHash(key) {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = (31 * hash + c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
/* XOR hash */
|
||||
function xorHash(key) {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash ^= c.charCodeAt(0);
|
||||
}
|
||||
return hash % MODULUS;
|
||||
}
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
/* Rotational hash */
|
||||
function rotHash(key) {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="simple_hash.ts"
|
||||
[class]{}-[func]{addHash}
|
||||
/* Additive hash */
|
||||
function addHash(key: string): number {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = (hash + c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
/* Multiplicative hash */
|
||||
function mulHash(key: string): number {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = (31 * hash + c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
/* XOR hash */
|
||||
function xorHash(key: string): number {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash ^= c.charCodeAt(0);
|
||||
}
|
||||
return hash % MODULUS;
|
||||
}
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
/* Rotational hash */
|
||||
function rotHash(key: string): number {
|
||||
let hash = 0;
|
||||
const MODULUS = 1000000007;
|
||||
for (const c of key) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="simple_hash.dart"
|
||||
[class]{}-[func]{addHash}
|
||||
/* Additive hash */
|
||||
int addHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
hash = (hash + key.codeUnitAt(i)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
/* Multiplicative hash */
|
||||
int mulHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
hash = (31 * hash + key.codeUnitAt(i)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
/* XOR hash */
|
||||
int xorHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
hash ^= key.codeUnitAt(i);
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
/* Rotational hash */
|
||||
int rotHash(String key) {
|
||||
int hash = 0;
|
||||
final int MODULUS = 1000000007;
|
||||
for (int i = 0; i < key.length; i++) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="simple_hash.rs"
|
||||
[class]{}-[func]{add_hash}
|
||||
/* Additive hash */
|
||||
fn add_hash(key: &str) -> i32 {
|
||||
let mut hash = 0_i64;
|
||||
const MODULUS: i64 = 1000000007;
|
||||
|
||||
[class]{}-[func]{mul_hash}
|
||||
for c in key.chars() {
|
||||
hash = (hash + c as i64) % MODULUS;
|
||||
}
|
||||
|
||||
[class]{}-[func]{xor_hash}
|
||||
hash as i32
|
||||
}
|
||||
|
||||
[class]{}-[func]{rot_hash}
|
||||
/* Multiplicative hash */
|
||||
fn mul_hash(key: &str) -> i32 {
|
||||
let mut hash = 0_i64;
|
||||
const MODULUS: i64 = 1000000007;
|
||||
|
||||
for c in key.chars() {
|
||||
hash = (31 * hash + c as i64) % MODULUS;
|
||||
}
|
||||
|
||||
hash as i32
|
||||
}
|
||||
|
||||
/* XOR hash */
|
||||
fn xor_hash(key: &str) -> i32 {
|
||||
let mut hash = 0_i64;
|
||||
const MODULUS: i64 = 1000000007;
|
||||
|
||||
for c in key.chars() {
|
||||
hash ^= c as i64;
|
||||
}
|
||||
|
||||
(hash & MODULUS) as i32
|
||||
}
|
||||
|
||||
/* Rotational hash */
|
||||
fn rot_hash(key: &str) -> i32 {
|
||||
let mut hash = 0_i64;
|
||||
const MODULUS: i64 = 1000000007;
|
||||
|
||||
for c in key.chars() {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS;
|
||||
}
|
||||
|
||||
hash as i32
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="simple_hash.c"
|
||||
[class]{}-[func]{addHash}
|
||||
/* Additive hash */
|
||||
int addHash(char *key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (int i = 0; i < strlen(key); i++) {
|
||||
hash = (hash + (unsigned char)key[i]) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
/* Multiplicative hash */
|
||||
int mulHash(char *key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (int i = 0; i < strlen(key); i++) {
|
||||
hash = (31 * hash + (unsigned char)key[i]) % MODULUS;
|
||||
}
|
||||
return (int)hash;
|
||||
}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
/* XOR hash */
|
||||
int xorHash(char *key) {
|
||||
int hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
for (int i = 0; i < strlen(key); i++) {
|
||||
hash ^= (unsigned char)key[i];
|
||||
}
|
||||
return hash & MODULUS;
|
||||
}
|
||||
|
||||
/* Rotational hash */
|
||||
int rotHash(char *key) {
|
||||
long long hash = 0;
|
||||
const int MODULUS = 1000000007;
|
||||
for (int i = 0; i < strlen(key); i++) {
|
||||
hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS;
|
||||
}
|
||||
|
||||
return (int)hash;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="simple_hash.kt"
|
||||
[class]{}-[func]{addHash}
|
||||
/* Additive hash */
|
||||
fun addHash(key: String): Int {
|
||||
var hash = 0L
|
||||
val MODULUS = 1000000007
|
||||
for (c in key.toCharArray()) {
|
||||
hash = (hash + c.code) % MODULUS
|
||||
}
|
||||
return hash.toInt()
|
||||
}
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
/* Multiplicative hash */
|
||||
fun mulHash(key: String): Int {
|
||||
var hash = 0L
|
||||
val MODULUS = 1000000007
|
||||
for (c in key.toCharArray()) {
|
||||
hash = (31 * hash + c.code) % MODULUS
|
||||
}
|
||||
return hash.toInt()
|
||||
}
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
/* XOR hash */
|
||||
fun xorHash(key: String): Int {
|
||||
var hash = 0
|
||||
val MODULUS = 1000000007
|
||||
for (c in key.toCharArray()) {
|
||||
hash = hash xor c.code
|
||||
}
|
||||
return hash and MODULUS
|
||||
}
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
/* Rotational hash */
|
||||
fun rotHash(key: String): Int {
|
||||
var hash = 0L
|
||||
val MODULUS = 1000000007
|
||||
for (c in key.toCharArray()) {
|
||||
hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS
|
||||
}
|
||||
return hash.toInt()
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="simple_hash.rb"
|
||||
[class]{}-[func]{add_hash}
|
||||
### Additive hash ###
|
||||
def add_hash(key)
|
||||
hash = 0
|
||||
modulus = 1_000_000_007
|
||||
|
||||
[class]{}-[func]{mul_hash}
|
||||
key.each_char { |c| hash += c.ord }
|
||||
|
||||
[class]{}-[func]{xor_hash}
|
||||
hash % modulus
|
||||
end
|
||||
|
||||
[class]{}-[func]{rot_hash}
|
||||
```
|
||||
### Multiplicative hash ###
|
||||
def mul_hash(key)
|
||||
hash = 0
|
||||
modulus = 1_000_000_007
|
||||
|
||||
=== "Zig"
|
||||
key.each_char { |c| hash = 31 * hash + c.ord }
|
||||
|
||||
```zig title="simple_hash.zig"
|
||||
[class]{}-[func]{addHash}
|
||||
hash % modulus
|
||||
end
|
||||
|
||||
[class]{}-[func]{mulHash}
|
||||
### XOR hash ###
|
||||
def xor_hash(key)
|
||||
hash = 0
|
||||
modulus = 1_000_000_007
|
||||
|
||||
[class]{}-[func]{xorHash}
|
||||
key.each_char { |c| hash ^= c.ord }
|
||||
|
||||
[class]{}-[func]{rotHash}
|
||||
hash % modulus
|
||||
end
|
||||
|
||||
### Rotational hash ###
|
||||
def rot_hash(key)
|
||||
hash = 0
|
||||
modulus = 1_000_000_007
|
||||
|
||||
key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord }
|
||||
|
||||
hash % modulus
|
||||
end
|
||||
```
|
||||
|
||||
It is observed that the last step of each hash algorithm is to take the modulus of the large prime number $1000000007$ to ensure that the hash value is within an appropriate range. It is worth pondering why emphasis is placed on modulo a prime number, or what are the disadvantages of modulo a composite number? This is an interesting question.
|
||||
@@ -336,7 +670,7 @@ It is worth noting that if the `key` is guaranteed to be randomly and uniformly
|
||||
|
||||
In summary, we usually choose a prime number as the modulus, and this prime number should be large enough to eliminate periodic patterns as much as possible, enhancing the robustness of the hash algorithm.
|
||||
|
||||
## 6.3.3 Common hash algorithms
|
||||
## 6.3.3 Common Hash Algorithms
|
||||
|
||||
It is not hard to see that the simple hash algorithms mentioned above are quite "fragile" and far from reaching the design goals of hash algorithms. For example, since addition and XOR obey the commutative law, additive hash and XOR hash cannot distinguish strings with the same content but in different order, which may exacerbate hash collisions and cause security issues.
|
||||
|
||||
@@ -362,7 +696,7 @@ Over the past century, hash algorithms have been in a continuous process of upgr
|
||||
|
||||
</div>
|
||||
|
||||
# Hash values in data structures
|
||||
# Hash Values in Data Structures
|
||||
|
||||
We know that the keys in a hash table can be of various data types such as integers, decimals, or strings. Programming languages usually provide built-in hash algorithms for these data types to calculate the bucket indices in the hash table. Taking Python as an example, we can use the `hash()` function to compute the hash values for various data types.
|
||||
|
||||
@@ -608,19 +942,62 @@ We know that the keys in a hash table can be of various data types such as integ
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="built_in_hash.kt"
|
||||
val num = 3
|
||||
val hashNum = num.hashCode()
|
||||
// Hash value of integer 3 is 3
|
||||
|
||||
val bol = true
|
||||
val hashBol = bol.hashCode()
|
||||
// Hash value of boolean true is 1231
|
||||
|
||||
val dec = 3.14159
|
||||
val hashDec = dec.hashCode()
|
||||
// Hash value of decimal 3.14159 is -1340954729
|
||||
|
||||
val str = "Hello 算法"
|
||||
val hashStr = str.hashCode()
|
||||
// Hash value of string "Hello 算法" is -727081396
|
||||
|
||||
val arr = arrayOf<Any>(12836, "小哈")
|
||||
val hashTup = arr.hashCode()
|
||||
// Hash value of array [12836, 小哈] is 189568618
|
||||
|
||||
val obj = ListNode(0)
|
||||
val hashObj = obj.hashCode()
|
||||
// Hash value of ListNode object utils.ListNode@1d81eb93 is 495053715
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
=== "Ruby"
|
||||
|
||||
```zig title="built_in_hash.zig"
|
||||
```ruby title="built_in_hash.rb"
|
||||
num = 3
|
||||
hash_num = num.hash
|
||||
# Hash value of integer 3 is -4385856518450339636
|
||||
|
||||
bol = true
|
||||
hash_bol = bol.hash
|
||||
# Hash value of boolean true is -1617938112149317027
|
||||
|
||||
dec = 3.14159
|
||||
hash_dec = dec.hash
|
||||
# Hash value of decimal 3.14159 is -1479186995943067893
|
||||
|
||||
str = "Hello 算法"
|
||||
hash_str = str.hash
|
||||
# Hash value of string "Hello 算法" is -4075943250025831763
|
||||
|
||||
tup = [12836, '小哈']
|
||||
hash_tup = tup.hash
|
||||
# Hash value of tuple (12836, '小哈') is 1999544809202288822
|
||||
|
||||
obj = ListNode.new(0)
|
||||
hash_obj = obj.hash
|
||||
# Hash value of ListNode object #<ListNode:0x000078133140ab70> is 4302940560806366381
|
||||
```
|
||||
|
||||
??? pythontutor "Code Visualization"
|
||||
??? pythontutor "Visualized Execution"
|
||||
|
||||
<div style="height: 549px; width: 100%;"><iframe class="pythontutor-iframe" src="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&codeDivHeight=472&codeDivWidth=350&cumulative=false&curInstr=19&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe></div>
|
||||
<div style="margin-top: 5px;"><a href="https://pythontutor.com/iframe-embed.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&codeDivHeight=800&codeDivWidth=600&cumulative=false&curInstr=19&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false" target="_blank" rel="noopener noreferrer">Full Screen ></a></div>
|
||||
https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
In many programming languages, **only immutable objects can serve as the `key` in a hash table**. If we use a list (dynamic array) as a `key`, when the contents of the list change, its hash value also changes, and we would no longer be able to find the original `value` in the hash table.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1025
-156
File diff suppressed because it is too large
Load Diff
@@ -3,19 +3,19 @@ comments: true
|
||||
icon: material/table-search
|
||||
---
|
||||
|
||||
# Chapter 6. Hash table
|
||||
# Chapter 6. Hashing
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
In the world of computing, a hash table is akin to an intelligent librarian.
|
||||
|
||||
It understands how to compute index numbers, enabling swift retrieval of the desired book.
|
||||
In the world of computing, a hash table is like a clever librarian.
|
||||
|
||||
They know how to calculate call numbers, enabling them to quickly locate the target book.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [6.1 Hash table](hash_map.md)
|
||||
- [6.2 Hash collision](hash_collision.md)
|
||||
- [6.3 Hash algorithm](hash_algorithm.md)
|
||||
- [6.1 Hash Table](hash_map.md)
|
||||
- [6.2 Hash Collision](hash_collision.md)
|
||||
- [6.3 Hash Algorithm](hash_algorithm.md)
|
||||
- [6.4 Summary](summary.md)
|
||||
|
||||
@@ -4,19 +4,19 @@ comments: true
|
||||
|
||||
# 6.4 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
- Given an input `key`, a hash table can retrieve the corresponding `value` in $O(1)$ time, which is highly efficient.
|
||||
- Common hash table operations include querying, adding key-value pairs, deleting key-value pairs, and traversing the hash table.
|
||||
- The hash function maps a `key` to an array index, allowing access to the corresponding bucket and retrieval of the `value`.
|
||||
- Two different keys may end up with the same array index after hashing, leading to erroneous query results. This phenomenon is known as hash collision.
|
||||
- The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table resizing can mitigate hash collisions. Similar to array resizing, hash table resizing is costly.
|
||||
- The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table resizing.
|
||||
- Chaining addresses hash collisions by converting each element into a linked list, storing all colliding elements in the same list. However, excessively long lists can reduce query efficiency, which can be improved by converting the lists into red-black trees.
|
||||
- Open addressing handles hash collisions through multiple probes. Linear probing uses a fixed step size but it cannot delete elements and is prone to clustering. Multiple hashing uses several hash functions for probing which reduces clustering compared to linear probing but increases computational overhead.
|
||||
- Different programming languages adopt various hash table implementations. For example, Java's `HashMap` uses chaining, while Python's `dict` employs open addressing.
|
||||
- The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table expansion can mitigate hash collisions. Similar to array expansion, hash table expansion is costly.
|
||||
- The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table expansion.
|
||||
- Separate chaining addresses hash collisions by converting each element into a linked list, storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by converting the linked lists into red-black trees.
|
||||
- Open addressing handles hash collisions through multiple probing. Linear probing uses a fixed step size but cannot delete elements and is prone to clustering. Double hashing uses multiple hash functions for probing, which reduces clustering compared to linear probing but increases computational overhead.
|
||||
- Different programming languages adopt various hash table implementations. For example, Java's `HashMap` uses separate chaining, while Python's `dict` employs open addressing.
|
||||
- In hash tables, we desire hash algorithms with determinism, high efficiency, and uniform distribution. In cryptography, hash algorithms should also possess collision resistance and the avalanche effect.
|
||||
- Hash algorithms typically use large prime numbers as moduli to ensure uniform distribution of hash values and reduce hash collisions.
|
||||
- Hash algorithms typically use large prime numbers as moduli to maximize the uniform distribution of hash values and reduce hash collisions.
|
||||
- Common hash algorithms include MD5, SHA-1, SHA-2, and SHA-3. MD5 is often used for file integrity checks, while SHA-2 is commonly used in secure applications and protocols.
|
||||
- Programming languages usually provide built-in hash algorithms for data types to calculate bucket indices in hash tables. Generally, only immutable objects are hashable.
|
||||
|
||||
@@ -36,16 +36,16 @@ Firstly, hash tables have higher time efficiency but lower space efficiency. A s
|
||||
|
||||
Secondly, hash tables are only more time-efficient in specific use cases. If a feature can be implemented with the same time complexity using an array or a linked list, it's usually faster than using a hash table. This is because the computation of the hash function incurs overhead, making the constant factor in the time complexity larger.
|
||||
|
||||
Lastly, the time complexity of hash tables can degrade. For example, in chaining, we perform search operations in a linked list or red-black tree, which still risks degrading to $O(n)$ time.
|
||||
Lastly, the time complexity of hash tables can degrade. For example, in separate chaining, we perform search operations in a linked list or red-black tree, which still risks degrading to $O(n)$ time.
|
||||
|
||||
**Q**: Does multiple hashing also have the flaw of not being able to delete elements directly? Can space marked as deleted be reused?
|
||||
**Q**: Does double hashing also have the flaw of not being able to delete elements directly? Can space marked as deleted be reused?
|
||||
|
||||
Multiple hashing is a form of open addressing, and all open addressing methods have the drawback of not being able to delete elements directly; they require marking elements as deleted. Marked spaces can be reused. When inserting new elements into the hash table, and the hash function points to a position marked as deleted, that position can be used by the new element. This maintains the probing sequence of the hash table while ensuring efficient use of space.
|
||||
Double hashing is a form of open addressing, and all open addressing methods have the drawback of not being able to delete elements directly; they require marking elements as deleted. Marked spaces can be reused. When inserting new elements into the hash table, and the hash function points to a position marked as deleted, that position can be used by the new element. This maintains the probing sequence of the hash table while ensuring efficient use of space.
|
||||
|
||||
**Q**: Why do hash collisions occur during the search process in linear probing?
|
||||
|
||||
During the search process, the hash function points to the corresponding bucket and key-value pair. If the `key` doesn't match, it indicates a hash collision. Therefore, linear probing will search downwards at a predetermined step size until the correct key-value pair is found or the search fails.
|
||||
During the search process, the hash function points to the corresponding bucket and key-value pair. If the `key` doesn't match, it indicates a hash collision. Therefore, linear probing will search downward at a predetermined step size until the correct key-value pair is found or the search fails.
|
||||
|
||||
**Q**: Why can resizing a hash table alleviate hash collisions?
|
||||
**Q**: Why can expanding a hash table alleviate hash collisions?
|
||||
|
||||
The last step of a hash function often involves taking the modulo of the array length $n$, to keep the output within the array index range. When resizing, the array length $n$ changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after resizing, thereby mitigating hash collisions.
|
||||
The last step of a hash function often involves taking the modulo of the array length $n$, to keep the output within the array index range. When expanding, the array length $n$ changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after expansion, thereby mitigating hash collisions.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3,28 +3,28 @@ comments: true
|
||||
icon: material/rocket-launch-outline
|
||||
---
|
||||
|
||||
# Before starting
|
||||
# Before Starting
|
||||
|
||||
A few years ago, I shared the "Sword for Offer" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most common question I encountered was "how to get started with algorithms." Gradually, I developed a keen interest in this question.
|
||||
A few years ago, I shared the "Sword for Offer" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most frequently asked question I encountered was "how to get started with algorithms." Gradually, I developed a keen interest in this question.
|
||||
|
||||
Directly solving problems seems to be the most popular method — it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-study skills can navigate the pitfalls one by one, while those lacking a solid foundation may find themselves repeatedly stumbling and retreating in frustration. Reading through textbooks is also a common practice, but for job seekers, writing graduation thesis, submitting resumes, preparing for written tests and interviews have already consumed most of their energy, and reading thick books often becomes a daunting challenge.
|
||||
Diving straight into problem-solving seems to be the most popular approach—it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-learning abilities can successfully defuse the mines one by one, while those with insufficient foundations may end up bruised and battered, retreating step by step in frustration. Reading through textbooks is also a common practice, but for job seekers, graduation theses, resume submissions, and preparations for written tests and interviews have already consumed most of their energy, making working through thick books an arduous challenge.
|
||||
|
||||
If you're facing similar troubles, then this book is lucky to have found you. This book is my answer to the question. While it may not be the best solution, it is at least a positive attempt. Although this book is not enough to get you an offer directly, it will guide you to explore the "knowledge map" of data structures and algorithms, help you understand the shapes, sizes, and locations of different "mines", and enable you to master various "mine clearance methods". With these skills, I believe you can solve problems and read literature more comfortably, gradually building a knowledge system.
|
||||
If you're facing similar struggles, then it's fortunate that this book has "found" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you to explore the "knowledge map" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different "mines," and enable you to master various "mine-clearing methods." With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system.
|
||||
|
||||
I deeply agree with Professor Feynman's statement: "Knowledge isn't free. You have to pay attention." In this sense, this book is not entirely "free." In order to live up to your precious "attention" for this book, I will do my best and devote my greatest "attention" to write this book.
|
||||
I deeply agree with Professor Feynman's words: "Knowledge isn't free. You have to pay attention." In this sense, this book is not entirely "free." In order to live up to the precious "attention" you invest in this book, I will do my utmost and devote my greatest "attention" to completing this work.
|
||||
|
||||
Aware of my limitations, I recognize that despite the content of this book being refined over time, errors surely remain. I sincerely welcome critiques and corrections from both teachers and students.
|
||||
I'm acutely aware of my limited knowledge and shallow expertise. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students.
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
<div style="text-align: center;">
|
||||
<h2 style="margin-top: 0.8em; margin-bottom: 0.8em;">Hello, Algo!</h2>
|
||||
<h2 style="margin-top: 0.8em; margin-bottom: 0.8em;">Hello, Algorithms!</h2>
|
||||
</div>
|
||||
|
||||
The advent of computers has brought significant changes to the world. With their high-speed computing power and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics of video games, the intelligent decisions in autonomous driving, the brilliant Go games of AlphaGo, or the natural interactions of ChatGPT, these applications are all exquisite demonstrations of algorithms at work on computers.
|
||||
The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all exquisite interpretations of algorithms on computers.
|
||||
|
||||
In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms became more refined and complex. From the exquisite craftsmanship of artisans, to industrial products that liberate productive forces, to the scientific laws governing the universe, almost every ordinary or astonishing thing has behind it the ingenious thought of algorithms.
|
||||
In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms gradually became more refined and complex. From the ingenious craftsmanship of master artisans, to industrial products that liberate productive forces, to the scientific laws governing the operation of the universe, behind almost every ordinary or astonishing thing lies ingenious algorithmic thinking.
|
||||
|
||||
Similarly, data structures are everywhere: from social networks to subway lines, many systems can be modeled as "graphs"; from a country to a family, the main forms of social organization exhibit characteristics of "trees"; winter clothes are like a "stack", where the first item worn is the last to be taken off; a badminton shuttle tube resembles a "queue", with one end for insertion and the other for retrieval; a dictionary is like a "hash table", enabling quick search for target entries.
|
||||
Similarly, data structures are everywhere: from large-scale social networks to small subway systems, many systems can be modeled as "graphs"; from a nation to a family, the primary organizational forms of society exhibit characteristics of "trees"; winter clothing is like a "stack," where the first item put on is the last to be taken off; a badminton tube is like a "queue," with items inserted at one end and retrieved from the other; a dictionary is like a "hash table," enabling quick lookup of target entries.
|
||||
|
||||
This book aims to help readers understand the core concepts of algorithms and data structures through clear, easy-to-understand animated illustrations and runnable code examples, and to be able to implement them through programming. On this basis, this book strives to reveal the vivid manifestations of algorithms in the complex world, showcasing the beauty of algorithms. I hope this book can help you!
|
||||
This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them through programming. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you!
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 1.1 Algorithms are everywhere
|
||||
# 1.1 Algorithms Are Everywhere
|
||||
|
||||
When we hear the term "algorithm," we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives.
|
||||
|
||||
|
||||
@@ -3,18 +3,18 @@ comments: true
|
||||
icon: material/calculator-variant-outline
|
||||
---
|
||||
|
||||
# Chapter 1. Encounter with algorithms
|
||||
# Chapter 1. Encounter with Algorithms
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
A graceful maiden dances, intertwined with the data, her skirt swaying to the melody of algorithms.
|
||||
|
||||
She invites you to a dance, follow her steps, and enter the world of algorithms full of logic and beauty.
|
||||
A young girl dances gracefully, intertwined with data, her skirt flowing with the melody of algorithms.
|
||||
|
||||
She invites you to dance with her. Follow her steps closely and enter the world of algorithms, full of logic and beauty.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [1.1 Algorithms are everywhere](algorithms_are_everywhere.md)
|
||||
- [1.2 What is an algorithm](what_is_dsa.md)
|
||||
- [1.1 Algorithms Are Everywhere](algorithms_are_everywhere.md)
|
||||
- [1.2 What Is an Algorithm](what_is_dsa.md)
|
||||
- [1.3 Summary](summary.md)
|
||||
|
||||
@@ -4,23 +4,25 @@ comments: true
|
||||
|
||||
# 1.3 Summary
|
||||
|
||||
- Algorithms are ubiquitous in daily life and are not as inaccessible and complex as they might seem. In fact, we have already unconsciously learned many algorithms to solve various problems in life.
|
||||
- The principle of looking up a word in a dictionary is consistent with the binary search algorithm. The binary search algorithm embodies the important algorithmic concept of divide and conquer.
|
||||
- The process of organizing playing cards is very similar to the insertion sort algorithm. The insertion sort algorithm is suitable for sorting small datasets.
|
||||
- The steps of making change in currency essentially follow the greedy algorithm, where each step involves making the best possible choice at the moment.
|
||||
- An algorithm is a set of step-by-step instructions for solving a specific problem within a finite time, while a data structure defines how data is organized and stored in a computer.
|
||||
- Data structures and algorithms are closely linked. Data structures are the foundation of algorithms, and algorithms are the stage to utilize the functions of data structures.
|
||||
- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent data structures, and the steps of assembling the blocks correspond to algorithms.
|
||||
### 1. Key Review
|
||||
|
||||
### 1. Q & A
|
||||
- Algorithms are ubiquitous in daily life and are not distant, esoteric knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life.
|
||||
- The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer.
|
||||
- The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets.
|
||||
- The steps of making change are essentially a greedy algorithm, where the best choice is made at each step based on the current situation.
|
||||
- An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is the way computers organize and store data.
|
||||
- Data structures and algorithms are closely connected. Data structures are the foundation of algorithms, and algorithms breathe life into data structures.
|
||||
- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent the data structure, and the steps to assemble the blocks correspond to the algorithm.
|
||||
|
||||
**Q**:As a programmer, I’ve rarely needed to implement algorithms manually in my daily work. Most commonly used algorithms are already built into programming languages and libraries, ready to use. Does this suggest that the problems we encounter in our work haven’t yet reached the level of complexity that demands custom algorithm design?
|
||||
### 2. Q & A
|
||||
|
||||
If specific work skills are like the "moves" in martial arts, then fundamental subjects are more like "internal strength".
|
||||
**Q**: As a programmer, I have never used algorithms to solve problems in my daily work. Common algorithms are already encapsulated by programming languages and can be used directly. Does this mean that the problems in our work have not yet reached the level where algorithms are needed?
|
||||
|
||||
I believe the significance of learning algorithms (and other fundamental subjects) isn’t necessarily to implement them from scratch at work, but to enable more professional decision-making and problem-solving based on a solid understanding of the concepts. This, in turn, raises the overall quality of our work. For example, every programming language provides a built-in sorting function:
|
||||
If we compare specific work skills to "techniques" in martial arts, then fundamental subjects should be more like "internal skills".
|
||||
|
||||
- If we have not learned data structures and algorithms, then given any data, we might just give it to this sorting function. It runs smoothly, has good performance, and seems to have no problems.
|
||||
- However, if we’ve studied algorithms, we understand that the time complexity of a built-in sorting function is typically $O(n \log n)$. Moreover, if the data consists of integers with a fixed number of digits (such as student IDs), we can apply a more efficient approach like radix sort, reducing the time complexity to O(nk) , where k is the number of digits. When handling large volumes of data, the time saved can turn into significant value — lowering costs, improving user experience, and enhancing system performance.
|
||||
I believe the significance of learning algorithms (and other fundamental subjects) is not to implement them from scratch at work, but rather to be able to make professional reactions and judgments when solving problems based on the knowledge learned, thereby improving the overall quality of work. Here is a simple example. Every programming language has a built-in sorting function:
|
||||
|
||||
In engineering, many problems are difficult to solve optimally; most are addressed with ‘near-optimal’ solutions. The difficulty of a problem depends not only on its inherent complexity but also on the knowledge and experience of the person tackling it. The deeper one’s expertise and experience, the more thorough the analysis, and the more elegantly the problem can be solved.
|
||||
- If we have not studied data structures and algorithms, we might simply feed any given data to this sorting function. It runs smoothly with good performance, and there doesn't seem to be any problem.
|
||||
- But if we have studied algorithms, we would know that the time complexity of the built-in sorting function is $O(n \log n)$. However, if the given data consists of integers with a fixed number of digits (such as student IDs), we can use the more efficient "radix sort", reducing the time complexity to $O(nk)$, where $k$ is the number of digits. When the data volume is very large, the saved running time can create significant value (reduced costs, improved experience, etc.).
|
||||
|
||||
In the field of engineering, a large number of problems are difficult to reach optimal solutions, and many problems are only solved "approximately". The difficulty of a problem depends on one hand on the nature of the problem itself, and on the other hand on the knowledge reserve of the person observing the problem. The more complete a person's knowledge and the more experience they have, the deeper their analysis of the problem will be, and the more elegantly the problem can be solved.
|
||||
|
||||
@@ -2,42 +2,42 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 1.2 What is an algorithm
|
||||
# 1.2 What Is an Algorithm
|
||||
|
||||
## 1.2.1 Definition of an algorithm
|
||||
## 1.2.1 Algorithm Definition
|
||||
|
||||
An <u>algorithm</u> is a set of instructions or steps to solve a specific problem within a finite amount of time. It has the following characteristics:
|
||||
An <u>algorithm</u> is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics.
|
||||
|
||||
- The problem is clearly defined, including unambiguous definitions of input and output.
|
||||
- The algorithm is feasible, meaning it can be completed within a finite number of steps, time, and memory space.
|
||||
- Each step has a definitive meaning. The output is consistently the same under the same inputs and conditions.
|
||||
- The problem is well-defined, with clear input and output definitions.
|
||||
- It is feasible and can be completed within a finite number of steps, time, and memory space.
|
||||
- Each step has a definite meaning, and under the same input and operating conditions, the output is always the same.
|
||||
|
||||
## 1.2.2 Definition of a data structure
|
||||
## 1.2.2 Data Structure Definition
|
||||
|
||||
A <u>data structure</u> is a way of organizing and storing data in a computer, with the following design goals:
|
||||
A <u>data structure</u> is a way of organizing and storing data, covering the data content, relationships between data, and methods for data operations. It has the following design objectives.
|
||||
|
||||
- Minimize space occupancy to save computer memory.
|
||||
- Make data operations as fast as possible, covering data access, addition, deletion, updating, etc.
|
||||
- Provide concise data representation and logical information to enable efficient algorithm execution.
|
||||
- Occupy as little space as possible to save computer memory.
|
||||
- Data operations should be as fast as possible, covering data access, addition, deletion, update, etc.
|
||||
- Provide a concise data representation and logical information so that algorithms can run efficiently.
|
||||
|
||||
**Designing data structures is a balancing act, often requiring trade-offs**. If you want to improve in one aspect, you often need to compromise in another. Here are two examples:
|
||||
**Data structure design is a process full of trade-offs**. If we want to achieve improvements in one aspect, we often need to make compromises in another aspect. Here are two examples.
|
||||
|
||||
- Compared to arrays, linked lists offer more convenience in data addition and deletion but sacrifice data access speed.
|
||||
- Compared with linked lists, graphs provide richer logical information but require more memory space.
|
||||
- Compared to arrays, linked lists are more convenient for data addition and deletion operations but sacrifice data access speed.
|
||||
- Compared to linked lists, graphs provide richer logical information but require larger memory space.
|
||||
|
||||
## 1.2.3 Relationship between data structures and algorithms
|
||||
## 1.2.3 The Relationship Between Data Structures and Algorithms
|
||||
|
||||
As shown in Figure 1-4, data structures and algorithms are highly related and closely integrated, specifically in the following three aspects:
|
||||
As shown in Figure 1-4, data structures and algorithms are highly related and tightly coupled, specifically manifested in the following three aspects.
|
||||
|
||||
- Data structures are the foundation of algorithms. They provide structured data storage and methods for manipulating data for algorithms.
|
||||
- Algorithms inject vitality into data structures. The data structure alone only stores data information; it is through the application of algorithms that specific problems can be solved.
|
||||
- Algorithms can often be implemented based on different data structures, but their execution efficiency can vary greatly. Choosing the right data structure is key.
|
||||
- Data structures are the foundation of algorithms. Data structures provide algorithms with structured storage of data and methods for operating on data.
|
||||
- Algorithms breathe life into data structures. Data structures themselves only store data information; combined with algorithms, they can solve specific problems.
|
||||
- Algorithms can usually be implemented based on different data structures, but execution efficiency may vary greatly. Choosing the appropriate data structure is key.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 1-4 Relationship between data structures and algorithms </p>
|
||||
<p align="center"> Figure 1-4 The relationship between data structures and algorithms </p>
|
||||
|
||||
Data structures and algorithms can be likened to a set of building blocks, as illustrated in Figure 1-5. A building block set includes numerous pieces, accompanied by detailed assembly instructions. Following these instructions step by step allows us to construct an intricate block model.
|
||||
Data structures and algorithms are like assembling building blocks as shown in Figure 1-5. A set of building blocks, in addition to containing many parts, also comes with detailed assembly instructions. By following the instructions step by step, we can assemble an exquisite building block model.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
@@ -45,21 +45,21 @@ Data structures and algorithms can be likened to a set of building blocks, as il
|
||||
|
||||
The detailed correspondence between the two is shown in Table 1-1.
|
||||
|
||||
<p align="center"> Table 1-1 Comparing data structures and algorithms to building blocks </p>
|
||||
<p align="center"> Table 1-1 Comparing data structures and algorithms to assembling building blocks </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| Data Structures and Algorithms | Building Blocks |
|
||||
| ------------------------------ | --------------------------------------------------------------- |
|
||||
| Input data | Unassembled blocks |
|
||||
| Data structure | Organization of blocks, including shape, size, connections, etc |
|
||||
| Algorithm | A series of steps to assemble the blocks into the desired shape |
|
||||
| Output data | Completed Block model |
|
||||
| Data structures and algorithms | Assembling building blocks |
|
||||
| ------------------------------ | ------------------------------------------------------------------ |
|
||||
| Input data | Unassembled building blocks |
|
||||
| Data structure | Organization form of building blocks, including shape, size, connection method, etc. |
|
||||
| Algorithm | A series of operational steps to assemble the blocks into the target form |
|
||||
| Output data | Building block model |
|
||||
|
||||
</div>
|
||||
|
||||
It's worth noting that data structures and algorithms are independent of programming languages. For this reason, this book is able to provide implementations in multiple programming languages.
|
||||
It is worth noting that data structures and algorithms are independent of programming languages. For this reason, this book is able to provide implementations based on multiple programming languages.
|
||||
|
||||
!!! tip "Conventional Abbreviation"
|
||||
!!! tip "Conventional abbreviation"
|
||||
|
||||
In real-life discussions, we often refer to "Data Structures and Algorithms" simply as "Algorithms". For example, the well-known LeetCode algorithm questions actually test knowledge of both data structures and algorithms.
|
||||
In actual discussions, we usually abbreviate "data structures and algorithms" as "algorithms". For example, the well-known LeetCode algorithm problems actually examine knowledge of both data structures and algorithms.
|
||||
|
||||
@@ -2,57 +2,57 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 0.1 About this book
|
||||
# 0.1 About This Book
|
||||
|
||||
This open-source project aims to create a free, and beginner-friendly crash course on data structures and algorithms.
|
||||
This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms.
|
||||
|
||||
- Animated illustrations, easy-to-understand content, and a smooth learning curve help beginners explore the "knowledge map" of data structures and algorithms.
|
||||
- Run code with just one click, helping readers improve their programming skills and understand the working principle of algorithms and the underlying implementation of data structures.
|
||||
- Promoting learning by teaching, feel free to ask questions and share insights. Let's grow together through discussion.
|
||||
- The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners to explore the knowledge map of data structures and algorithms.
|
||||
- The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures.
|
||||
- We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange.
|
||||
|
||||
## 0.1.1 Target audience
|
||||
## 0.1.1 Target Audience
|
||||
|
||||
If you are new to algorithms with limited exposure, or you have accumulated some experience in algorithms, but you only have a vague understanding of data structures and algorithms, and you are constantly jumping between "yep" and "hmm", then this book is for you!
|
||||
If you are an algorithm beginner who has never been exposed to algorithms, or if you already have some problem-solving experience and have a vague understanding of data structures and algorithms, oscillating between knowing and not knowing, then this book is tailor-made for you!
|
||||
|
||||
If you have already accumulated a certain amount of problem-solving experience, and are familiar with most types of problems, then this book can help you review and organize your algorithm knowledge system. The repository's source code can be used as a "problem-solving toolkit" or an "algorithm cheat sheet".
|
||||
If you have already accumulated a certain amount of problem-solving experience and are familiar with most question types, this book can help you review and organize your algorithm knowledge system, and the repository's source code can be used as a "problem-solving toolkit" or "algorithm dictionary."
|
||||
|
||||
If you are an algorithm expert, we look forward to receiving your valuable suggestions, or [join us and collaborate](https://www.hello-algo.com/chapter_appendix/contribution/).
|
||||
If you are an algorithm "expert," we look forward to receiving your valuable suggestions, or [participating in creation together](https://www.hello-algo.com/chapter_appendix/contribution/).
|
||||
|
||||
!!! success "Prerequisites"
|
||||
|
||||
You should know how to write and read simple code in at least one programming language.
|
||||
You need to have at least a programming foundation in any language, and be able to read and write simple code.
|
||||
|
||||
## 0.1.2 Content structure
|
||||
## 0.1.2 Content Structure
|
||||
|
||||
The main content of the book is shown in Figure 0-1.
|
||||
The main content of this book is shown in Figure 0-1.
|
||||
|
||||
- **Complexity analysis**: explores aspects and methods for evaluating data structures and algorithms. Covers methods of deriving time complexity and space complexity, along with common types and examples.
|
||||
- **Data structures**: focuses on fundamental data types, classification methods, definitions, pros and cons, common operations, types, applications, and implementation methods of data structures such as array, linked list, stack, queue, hash table, tree, heap, graph, etc.
|
||||
- **Algorithms**: defines algorithms, discusses their pros and cons, efficiency, application scenarios, problem-solving steps, and includes sample questions for various algorithms such as search, sorting, divide and conquer, backtracking, dynamic programming, greedy algorithms, and more.
|
||||
- **Complexity analysis**: Evaluation dimensions and methods for data structures and algorithms. Methods for calculating time complexity and space complexity, common types, examples, etc.
|
||||
- **Data structures**: Classification methods for basic data types and data structures. The definition, advantages and disadvantages, common operations, common types, typical applications, implementation methods, etc. of data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs.
|
||||
- **Algorithms**: The definition, advantages and disadvantages, efficiency, application scenarios, problem-solving steps, and example problems of algorithms such as searching, sorting, divide and conquer, backtracking, dynamic programming, and greedy algorithms.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-1 Main content of the book </p>
|
||||
<p align="center"> Figure 0-1 Main content of this book </p>
|
||||
|
||||
## 0.1.3 Acknowledgements
|
||||
|
||||
This book is continuously improved with the joint efforts of many contributors from the open-source community. Thanks to each writer who invested their time and energy, listed in the order generated by GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, msk397, gvenusleo, khoaxuantu, RiverTwilight, rongyi, gyt95, zhuoqinyue, K3v123, Zuoxun, mingXta, hello-ikun, FangYuan33, GN-Yu, yuelinxin, longsizhuo, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, QiLOL, pengchzn, Guanngxu, L-Super, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, longranger2, cy-by-side, xiongsp, JeffersonHuang, Transmigration-zhou, magentaqin, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, Shyam-Chen, nanlei, hongyun-robot, Phoenix0415, MolDuM, Nigh, he-weilai, junminhong, mgisr, iron-irax, yd-j, XiaChuerwu, XC-Zero, seven1240, SamJin98, wodray, reeswell, NI-SW, Horbin-Magician, Enlightenus, xjr7670, YangXuanyi, DullSword, boloboloda, iStig, qq909244296, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, liuxjerry, lucaswangdev, lyl625760, hts0000, gledfish, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, luluxia, xb534, bitsmi, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, steventimes, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, ZhongGuanbin, shanghai-Jerry, JackYang-hellobobo, Javesun99, lipusheng, BlindTerran, ShiMaRing, FreddieLi, FloranceYeh, iFleey, fanchenggang, gltianwen, goerll, Dr-XYZ, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, fanenr, eagleanurag, LifeGoesOnionOnionOnion, 52coder, foursevenlove, KorsChen, hezhizhen, linzeyan, ZJKung, GaochaoZhu, hopkings2008, yang-le, Evilrabbit520, Turing-1024-Lee, thomasq0, Suremotoo, Allen-Scai, Risuntsy, Richard-Zhang1019, qingpeng9802, primexiao, nidhoggfgg, 1ch0, MwumLi, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai and 0130w.
|
||||
This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every writer who invested time and effort, they are (in the order automatically generated by GitHub): krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, msk397, gvenusleo, khoaxuantu, RiverTwilight, rongyi, gyt95, zhuoqinyue, K3v123, Zuoxun, mingXta, hello-ikun, FangYuan33, GN-Yu, yuelinxin, longsizhuo, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, QiLOL, pengchzn, Guanngxu, L-Super, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, longranger2, cy-by-side, xiongsp, JeffersonHuang, Transmigration-zhou, magentaqin, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, Shyam-Chen, nanlei, hongyun-robot, Phoenix0415, MolDuM, Nigh, he-weilai, junminhong, mgisr, iron-irax, yd-j, XiaChuerwu, XC-Zero, seven1240, SamJin98, wodray, reeswell, NI-SW, Horbin-Magician, Enlightenus, xjr7670, YangXuanyi, DullSword, boloboloda, iStig, qq909244296, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, liuxjerry, lucaswangdev, lyl625760, hts0000, gledfish, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, luluxia, xb534, bitsmi, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, steventimes, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, ZhongGuanbin, shanghai-Jerry, JackYang-hellobobo, Javesun99, lipusheng, BlindTerran, ShiMaRing, FreddieLi, FloranceYeh, iFleey, fanchenggang, gltianwen, goerll, Dr-XYZ, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, fanenr, eagleanurag, LifeGoesOnionOnionOnion, 52coder, foursevenlove, KorsChen, hezhizhen, linzeyan, ZJKung, GaochaoZhu, hopkings2008, yang-le, Evilrabbit520, Turing-1024-Lee, thomasq0, Suremotoo, Allen-Scai, Risuntsy, Richard-Zhang1019, qingpeng9802, primexiao, nidhoggfgg, 1ch0, MwumLi, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai and 0130w.
|
||||
|
||||
The code review work for this book was completed by coderonion, Gonglja, gvenusleo, hpstory, justin‐tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (listed in alphabetical order). Thanks to them for their time and effort, ensuring the standardization and uniformity of the code in various languages.
|
||||
The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in, it is they who ensure the standardization and unity of code in various languages.
|
||||
|
||||
The Traditional Chinese version of this book was reviewed by Shyam-Chen and Dr-XYZ, while the English version was reviewed by yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0, and magentaqin. It is thanks to their continuous contributions that this book can reach and serve a broader audience.
|
||||
The Traditional Chinese version of this book was reviewed by Shyam-Chen and Dr-XYZ, and the English version was reviewed by yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0 and magentaqin. It is because of their continuous contributions that this book can serve a wider readership, and we thank them.
|
||||
|
||||
Throughout the creation of this book, numerous individuals provided invaluable assistance, including but not limited to:
|
||||
During the creation of this book, I received help from many people.
|
||||
|
||||
- Thanks to my mentor at the company, Dr. Xi Li, who encouraged me in a conversation to "get moving fast," which solidified my determination to write this book;
|
||||
- Thanks to my girlfriend Bubble, as the first reader of this book, for offering many valuable suggestions from the perspective of a beginner in algorithms, making this book more suitable for newbies;
|
||||
- Thanks to my mentor at the company, Dr. Li Xi, who encouraged me to "take action quickly" during a conversation, strengthening my determination to write this book;
|
||||
- Thanks to my girlfriend Bubble as the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more suitable for novices to read;
|
||||
- Thanks to Tengbao, Qibao, and Feibao for coming up with a creative name for this book, evoking everyone's fond memories of writing their first line of code "Hello World!";
|
||||
- Thanks to Xiaoquan for providing professional help in intellectual property, which has played a significant role in the development of this open-source book;
|
||||
- Thanks to Sutong for designing a beautiful cover and logo for this book, and for patiently making multiple revisions under my insistence;
|
||||
- Thanks to @squidfunk for providing writing and typesetting suggestions, as well as his developed open-source documentation theme [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master).
|
||||
- Thanks to Xiaoquan for providing professional help in intellectual property rights, which played an important role in the improvement of this open-source book;
|
||||
- Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently making revisions multiple times driven by my obsessive-compulsive disorder;
|
||||
- Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master).
|
||||
|
||||
Throughout the writing journey, I delved into numerous textbooks and articles on data structures and algorithms. These works served as exemplary models, ensuring the accuracy and quality of this book's content. I extend my gratitude to all who preceded me for their invaluable contributions!
|
||||
During the writing process, I read many textbooks and articles on data structures and algorithms. These works provided excellent examples for this book and ensured the accuracy and quality of the book's content. I would like to thank all the teachers and predecessors for their outstanding contributions!
|
||||
|
||||
This book advocates a combination of hands-on and minds-on learning, inspired in this regard by ["Dive into Deep Learning"](https://github.com/d2l-ai/d2l-en). I highly recommend this excellent book to all readers.
|
||||
This book advocates a learning method that combines hands and brain, and in this regard I was deeply inspired by [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). I highly recommend this excellent work to all readers.
|
||||
|
||||
**Heartfelt thanks to my parents, whose ongoing support and encouragement have allowed me to do this interesting work**.
|
||||
**Heartfelt thanks to my parents, it is your support and encouragement that has given me the opportunity to do this interesting thing**.
|
||||
|
||||
@@ -9,12 +9,12 @@ icon: material/book-open-outline
|
||||
|
||||
!!! abstract
|
||||
|
||||
Algorithms are like a beautiful symphony, with each line of code flowing like a rhythm.
|
||||
|
||||
May this book ring softly in your mind, leaving a unique and profound melody.
|
||||
Algorithms are like a beautiful symphony, each line of code flows like a melody.
|
||||
|
||||
May this book gently resonate in your mind, leaving a unique and profound melody.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [0.1 About this book](about_the_book.md)
|
||||
- [0.2 How to read](suggestions.md)
|
||||
- [0.1 About This Book](about_the_book.md)
|
||||
- [0.2 How to Use This Book](suggestions.md)
|
||||
- [0.3 Summary](summary.md)
|
||||
|
||||
@@ -2,254 +2,258 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 0.2 How to read
|
||||
# 0.2 How to Use This Book
|
||||
|
||||
!!! tip
|
||||
|
||||
For the best reading experience, it is recommended that you read through this section.
|
||||
|
||||
## 0.2.1 Writing conventions
|
||||
## 0.2.1 Writing Style Conventions
|
||||
|
||||
- Chapters marked with '*' after the title are optional and contain relatively challenging content. If you are short on time, it is advisable to skip them.
|
||||
- Technical terms will be in boldface (in the print and PDF versions) or underlined (in the web version), for instance, <u>array</u>. It's advisable to familiarize yourself with these for better comprehension of technical texts.
|
||||
- **Bolded text** indicates key content or summary statements, which deserve special attention.
|
||||
- Words and phrases with specific meanings are indicated with “quotation marks” to avoid ambiguity.
|
||||
- When it comes to terms that are inconsistent between programming languages, this book follows Python, for example using `None` to mean `null`.
|
||||
- This book partially ignores the comment conventions for programming languages in exchange for a more compact layout of the content. The comments primarily consist of three types: title comments, content comments, and multi-line comments.
|
||||
- Titles marked with `*` are optional sections with relatively difficult content. If you have limited time, you can skip them first.
|
||||
- Technical terms will be in bold (in paper and PDF versions) or underlined (in web versions), such as <u>array</u>. It is recommended to memorize them for reading literature.
|
||||
- Key content and summary statements will be **bolded**, and such text deserves special attention.
|
||||
- Words and phrases with specific meanings will be marked with "quotation marks" to avoid ambiguity.
|
||||
- When it comes to nouns that are inconsistent between programming languages, this book uses Python as the standard, for example, using `None` to represent "null".
|
||||
- This book partially abandons the comment conventions of programming languages in favor of more compact content layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title=""
|
||||
"""Header comments for labeling functions, classes, test samples, etc"""
|
||||
|
||||
# Comments for explaining details
|
||||
|
||||
"""Title comment, used to label functions, classes, test cases, etc."""
|
||||
|
||||
# Content comment, used to explain code in detail
|
||||
|
||||
"""
|
||||
Multiline
|
||||
comments
|
||||
Multi-line
|
||||
comment
|
||||
"""
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "Java"
|
||||
|
||||
```java title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "C#"
|
||||
|
||||
```csharp title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* Header comments for labeling functions, classes, test samples, etc */
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
/* Title comment, used to label functions, classes, test cases, etc. */
|
||||
|
||||
// Content comment, used to explain code in detail
|
||||
|
||||
/**
|
||||
* Multiline
|
||||
* comments
|
||||
* Multi-line
|
||||
* comment
|
||||
*/
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
=== "Ruby"
|
||||
|
||||
```zig title=""
|
||||
// Header comments for labeling functions, classes, test samples, etc
|
||||
|
||||
// Comments for explaining details.
|
||||
|
||||
// Multiline
|
||||
// comments
|
||||
```ruby title=""
|
||||
### Title comment, used to label functions, classes, test cases, etc. ###
|
||||
|
||||
# Content comment, used to explain code in detail
|
||||
|
||||
# Multi-line
|
||||
# comment
|
||||
```
|
||||
|
||||
## 0.2.2 Efficient learning via animated illustrations
|
||||
## 0.2.2 Learning Efficiently with Animated Illustrations
|
||||
|
||||
Compared with text, videos and pictures have a higher density of information and are more structured, making them easier to understand. In this book, **key and difficult concepts are mainly presented through animations and illustrations**, with text serving as explanations and supplements.
|
||||
Compared to text, videos and images have higher information density and structural organization, making them easier to understand. In this book, **key and difficult knowledge will mainly be presented in the form of animated illustrations**, with text serving as explanation and supplement.
|
||||
|
||||
When encountering content with animations or illustrations as shown in Figure 0-2, **prioritize understanding the figure, with text as supplementary**, integrating both for a comprehensive understanding.
|
||||
If you find that a section of content provides animated illustrations as shown in Figure 0-2 while reading this book, **please focus on the illustrations first, with text as a supplement**, and combine the two to understand the content.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-2 Animated illustration example </p>
|
||||
<p align="center"> Figure 0-2 Example of animated illustrations </p>
|
||||
|
||||
## 0.2.3 Deepen understanding through coding practice
|
||||
## 0.2.3 Deepening Understanding Through Code Practice
|
||||
|
||||
The source code of this book is hosted on the [GitHub Repository](https://github.com/krahets/hello-algo). As shown in Figure 0-3, **the source code comes with test examples and can be executed with just a single click**.
|
||||
The accompanying code for this book is hosted in the [GitHub repository](https://github.com/krahets/hello-algo). As shown in Figure 0-3, **the source code comes with test cases and can be run with one click**.
|
||||
|
||||
If time permits, **it's recommended to type out the code yourself**. If pressed for time, at least read and run all the codes.
|
||||
If time permits, **it is recommended that you type out the code yourself**. If you have limited study time, please at least read through and run all the code.
|
||||
|
||||
Compared to just reading code, writing code often yields more learning. **Learning by doing is the real way to learn.**
|
||||
Compared to reading code, the process of writing code often brings more rewards. **Learning by doing is the real learning**.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-3 Running code example </p>
|
||||
<p align="center"> Figure 0-3 Example of running code </p>
|
||||
|
||||
Setting up to run the code involves three main steps.
|
||||
The preliminary work for running code is mainly divided into three steps.
|
||||
|
||||
**Step 1: Install a local programming environment**. Follow the [tutorial](https://www.hello-algo.com/chapter_appendix/installation/) in the appendix for installation, or skip this step if already installed.
|
||||
**Step 1: Install the local programming environment**. Please follow the [tutorial](https://www.hello-algo.com/chapter_appendix/installation/) shown in the appendix for installation. If already installed, you can skip this step.
|
||||
|
||||
**Step 2: Clone or download the code repository**. Visit the [GitHub Repository](https://github.com/krahets/hello-algo).
|
||||
|
||||
If [Git](https://git-scm.com/downloads) is installed, use the following command to clone the repository:
|
||||
**Step 2: Clone or download the code repository**. Visit the [GitHub repository](https://github.com/krahets/hello-algo). If you have already installed [Git](https://git-scm.com/downloads), you can clone this repository with the following command:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/krahets/hello-algo.git
|
||||
```
|
||||
|
||||
Alternatively, you can also click the "Download ZIP" button at the location shown in Figure 0-4 to directly download the code as a compressed ZIP file. Then, you can simply extract it locally.
|
||||
Of course, you can also click the "Download ZIP" button at the location shown in Figure 0-4 to directly download the code compressed package, and then extract it locally.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-4 Cloning repository and downloading code </p>
|
||||
<p align="center"> Figure 0-4 Clone repository and download code </p>
|
||||
|
||||
**Step 3: Run the source code**. As shown in Figure 0-5, for the code block labeled with the file name at the top, we can find the corresponding source code file in the `codes` folder of the repository. These files can be executed with a single click, which will help you save unnecessary debugging time and allow you to focus on learning.
|
||||
**Step 3: Run the source code**. As shown in Figure 0-5, for code blocks with file names at the top, we can find the corresponding source code files in the `codes` folder of the repository. The source code files can be run with one click, which will help you save unnecessary debugging time and allow you to focus on learning content.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-5 Code block and corresponding source code file </p>
|
||||
<p align="center"> Figure 0-5 Code blocks and corresponding source code files </p>
|
||||
|
||||
## 0.2.4 Learning together in discussion
|
||||
In addition to running code locally, **the web version also supports visual running of Python code** (implemented based on [pythontutor](https://pythontutor.com/)). As shown in Figure 0-6, you can click "Visual Run" below the code block to expand the view and observe the execution process of the algorithm code; you can also click "Full Screen View" for a better viewing experience.
|
||||
|
||||
While reading this book, please don't skip over the points that you didn't learn. **Feel free to post your questions in the comment section**. We will be happy to answer them and can usually respond within two days.
|
||||
{ class="animation-figure" }
|
||||
|
||||
As illustrated in Figure 0-6, each chapter features a comment section at the bottom. I encourage you to pay attention to these comments. They not only expose you to others' encountered problems, aiding in identifying knowledge gaps and sparking deeper contemplation, but also invite you to generously contribute by answering fellow readers' inquiries, sharing insights, and fostering mutual improvement.
|
||||
<p align="center"> Figure 0-6 Visual running of Python code </p>
|
||||
|
||||
{ class="animation-figure" }
|
||||
## 0.2.4 Growing Together Through Questions and Discussions
|
||||
|
||||
<p align="center"> Figure 0-6 Comment section example </p>
|
||||
When reading this book, please do not easily skip knowledge points that you have not learned well. **Feel free to ask your questions in the comments section**, and my friends and I will do our best to answer you, and generally reply within two days.
|
||||
|
||||
## 0.2.5 Algorithm learning path
|
||||
As shown in Figure 0-7, the web version has a comments section at the bottom of each chapter. I hope you will pay more attention to the content of the comments section. On the one hand, you can learn about the problems that everyone encounters, thus checking for omissions and stimulating deeper thinking. On the other hand, I hope you can generously answer other friends' questions, share your insights, and help others progress.
|
||||
|
||||
Overall, the journey of mastering data structures and algorithms can be divided into three stages:
|
||||
{ class="animation-figure" }
|
||||
|
||||
1. **Stage 1: Introduction to algorithms**. We need to familiarize ourselves with the characteristics and usage of various data structures and learn about the principles, processes, uses, and efficiency of different algorithms.
|
||||
2. **Stage 2: Practicing algorithm problems**. It is recommended to start from popular problems, such as [Sword for Offer](https://leetcode.cn/studyplan/coding-interviews/) and [LeetCode Hot 100](https://leetcode.cn/studyplan/top-100- liked/), and accumulate at least 100 questions to familiarize yourself with mainstream algorithmic problems. Forgetfulness can be a challenge when you start practicing, but rest assured that this is normal. We can follow the "Ebbinghaus Forgetting Curve" to review the questions, and usually after 3~5 rounds of repetitions, we will be able to memorize them.
|
||||
3. **Stage 3: Building the knowledge system**. In terms of learning, we can read algorithm column articles, solution frameworks, and algorithm textbooks to continuously enrich the knowledge system. In terms of practicing, we can try advanced strategies, such as categorizing by topic, multiple solutions for a single problem, and one solution for multiple problems, etc. Insights on these strategies can be found in various communities.
|
||||
<p align="center"> Figure 0-7 Example of comments section </p>
|
||||
|
||||
As shown in Figure 0-7, this book mainly covers “Stage 1,” aiming to help you more efficiently embark on Stages 2 and 3.
|
||||
## 0.2.5 Algorithm Learning Roadmap
|
||||
|
||||
{ class="animation-figure" }
|
||||
From an overall perspective, we can divide the process of learning data structures and algorithms into three stages.
|
||||
|
||||
<p align="center"> Figure 0-7 Algorithm learning path </p>
|
||||
1. **Stage 1: Algorithm introduction**. We need to familiarize ourselves with the characteristics and usage of various data structures, and learn the principles, processes, uses, and efficiency of different algorithms.
|
||||
2. **Stage 2: Practice algorithm problems**. It is recommended to start with popular problems, and accumulate at least 100 problems first, to familiarize yourself with mainstream algorithm problems. When first practicing problems, "knowledge forgetting" may be a challenge, but rest assured, this is very normal. We can review problems according to the "Ebbinghaus forgetting curve", and usually after 3-5 rounds of repetition, we can firmly remember them. For recommended problem lists and practice plans, please see this [GitHub repository](https://github.com/krahets/LeetCode-Book).
|
||||
3. **Stage 3: Building a knowledge system**. In terms of learning, we can read algorithm column articles, problem-solving frameworks, and algorithm textbooks to continuously enrich our knowledge system. In terms of practicing problems, we can try advanced problem-solving strategies, such as categorization by topic, one problem multiple solutions, one solution multiple problems, etc. Related problem-solving insights can be found in various communities.
|
||||
|
||||
As shown in Figure 0-8, the content of this book mainly covers "Stage 1", aiming to help you more efficiently carry out Stage 2 and Stage 3 learning.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 0-8 Algorithm learning roadmap </p>
|
||||
|
||||
@@ -4,9 +4,11 @@ comments: true
|
||||
|
||||
# 0.3 Summary
|
||||
|
||||
- The main audience of this book is beginners in algorithm. If you already have some basic knowledge, this book can help you systematically review your algorithm knowledge, and the source code in this book can also be used as a "Coding Toolkit".
|
||||
- The book consists of three main sections, Complexity Analysis, Data Structures, and Algorithms, covering most of the topics in the field.
|
||||
- For newcomers to algorithms, it is crucial to read an introductory book in the beginning stages to avoid many detours or common pitfalls.
|
||||
- Animations and figures within the book are usually used to introduce key points and difficult knowledge. These should be given more attention when reading the book.
|
||||
- Practice is the best way to learn programming. It is highly recommended that you run the source code and type in the code yourself.
|
||||
- Each chapter in the web version of this book features a discussion section, and you are welcome to share your questions and insights at any time.
|
||||
### 1. Key Review
|
||||
|
||||
- The main audience of this book is algorithm beginners. If you already have a certain foundation, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a "problem-solving toolkit."
|
||||
- The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field.
|
||||
- For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours.
|
||||
- The animated illustrations in the book are usually used to introduce key and difficult knowledge. When reading this book, you should pay more attention to these contents.
|
||||
- Practice is the best way to learn programming. It is strongly recommended to run the source code and type the code yourself.
|
||||
- The web version of this book has a comments section for each chapter, where you are welcome to share your questions and insights at any time.
|
||||
|
||||
@@ -16,7 +16,7 @@ icon: material/bookshelf
|
||||
|
||||
[6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition).
|
||||
|
||||
[7] Cheng Jie. Speaking of Data Structures.
|
||||
[7] Cheng Jie. Conversational Data Structures.
|
||||
|
||||
[8] Wang Zheng. The Beauty of Data Structures and Algorithms.
|
||||
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.1 Binary search
|
||||
# 10.1 Binary Search
|
||||
|
||||
<u>Binary search</u> is an efficient search algorithm that uses a divide-and-conquer strategy. It takes advantage of the sorted order of elements in an array by reducing the search interval by half in each iteration, continuing until either the target element is found or the search interval becomes empty.
|
||||
<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.
|
||||
|
||||
!!! question
|
||||
|
||||
Given an array `nums` of length $n$, where elements are arranged in ascending order without duplicates. Please find and return the index of element `target` in this array. If the array does not contain the element, return $-1$. An example is shown in Figure 10-1.
|
||||
Given an array `nums` of length $n$ with elements arranged in ascending order and no duplicates, search for and return the index of element `target` in the array. If the array does not contain the element, return $-1$. An example is shown in Figure 10-1.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 10-1 Binary search example data </p>
|
||||
|
||||
As shown in Figure 10-2, we firstly initialize pointers with $i = 0$ and $j = n - 1$, pointing to the first and last element of the array respectively. They also represent the whole search interval $[0, n - 1]$. Please note that square brackets indicate a closed interval, which includes the boundary values themselves.
|
||||
As shown in Figure 10-2, we first initialize pointers $i = 0$ and $j = n - 1$, pointing to the first and last elements of the array respectively, representing the search interval $[0, n - 1]$. Note that square brackets denote a closed interval, which includes the boundary values themselves.
|
||||
|
||||
And then the following two steps may be performed in a loop.
|
||||
Next, perform the following two steps in a loop:
|
||||
|
||||
1. Calculate the midpoint index $m = \lfloor {(i + j) / 2} \rfloor$, where $\lfloor \: \rfloor$ denotes the floor operation.
|
||||
2. Based on the comparison between the value of `nums[m]` and `target`, one of the following three cases will be chosen to execute.
|
||||
1. If `nums[m] < target`, it indicates that `target` is in the interval $[m + 1, j]$, thus set $i = m + 1$.
|
||||
2. If `nums[m] > target`, it indicates that `target` is in the interval $[i, m - 1]$, thus set $j = m - 1$.
|
||||
3. If `nums[m] = target`, it indicates that `target` is found, thus return index $m$.
|
||||
2. Compare `nums[m]` and `target`, which results in three cases:
|
||||
1. When `nums[m] < target`, it indicates that `target` is in the interval $[m + 1, j]$, so execute $i = m + 1$.
|
||||
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 reduce to empty, ending up returning $-1$.
|
||||
If the array does not contain the target element, the search interval will eventually shrink to empty. In this case, return $-1$.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
@@ -49,48 +49,48 @@ If the array does not contain the target element, the search interval will event
|
||||
|
||||
<p align="center"> Figure 10-2 Binary search process </p>
|
||||
|
||||
It's worth noting that as $i$ and $j$ are both of type `int`, **$i + j$ might exceed the range of `int` type**. To avoid large number overflow, we usually 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 large number overflow, we typically use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint.
|
||||
|
||||
The code is as follows:
|
||||
The code is shown below:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="binary_search.py"
|
||||
def binary_search(nums: list[int], target: int) -> int:
|
||||
"""Binary search (double closed interval)"""
|
||||
# Initialize double closed interval [0, n-1], i.e., i, j point to the first element and last element of the array respectively
|
||||
"""Binary search (closed interval)"""
|
||||
# Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
i, j = 0, len(nums) - 1
|
||||
# Loop until the search interval is empty (when i > j, it is empty)
|
||||
# Loop, exit when the search interval is empty (empty when i > j)
|
||||
while i <= j:
|
||||
# Theoretically, Python's numbers can be infinitely large (depending on memory size), so there is no need to consider large number overflow
|
||||
m = i + (j - i) // 2 # Calculate midpoint index m
|
||||
# In theory, Python numbers can be infinitely large (depending on memory size), no need to consider large number overflow
|
||||
m = (i + j) // 2 # Calculate midpoint index m
|
||||
if nums[m] < target:
|
||||
i = m + 1 # This situation indicates that target is in the interval [m+1, j]
|
||||
i = m + 1 # This means target is in the interval [m+1, j]
|
||||
elif nums[m] > target:
|
||||
j = m - 1 # This situation indicates that target is in the interval [i, m-1]
|
||||
j = m - 1 # This means target is in the interval [i, m-1]
|
||||
else:
|
||||
return m # Found the target element, thus return its index
|
||||
return -1 # Did not find the target element, thus return -1
|
||||
return m # Found the target element, return its index
|
||||
return -1 # Target element not found, return -1
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="binary_search.cpp"
|
||||
/* Binary search (double closed interval) */
|
||||
/* Binary search (closed interval on both sides) */
|
||||
int binarySearch(vector<int> &nums, int target) {
|
||||
// Initialize double closed interval [0, n-1], i.e., i, j point to the first element and last element of the array respectively
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
int i = 0, j = nums.size() - 1;
|
||||
// Loop until the search interval is empty (when i > j, it is empty)
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate midpoint index m
|
||||
if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j]
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j]
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // This situation indicates that target is in the interval [i, m-1]
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m-1]
|
||||
j = m - 1;
|
||||
else // Found the target element, thus return its index
|
||||
else // Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
// Did not find the target element, thus return -1
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
@@ -98,21 +98,21 @@ The code is as follows:
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_search.java"
|
||||
/* Binary search (double closed interval) */
|
||||
/* Binary search (closed interval on both sides) */
|
||||
int binarySearch(int[] nums, int target) {
|
||||
// Initialize double closed interval [0, n-1], i.e., i, j point to the first element and last element of the array respectively
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
int i = 0, j = nums.length - 1;
|
||||
// Loop until the search interval is empty (when i > j, it is empty)
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate midpoint index m
|
||||
if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j]
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j]
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // This situation indicates that target is in the interval [i, m-1]
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m-1]
|
||||
j = m - 1;
|
||||
else // Found the target element, thus return its index
|
||||
else // Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
// Did not find the target element, thus return -1
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
@@ -120,76 +120,255 @@ The code is as follows:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_search.cs"
|
||||
[class]{binary_search}-[func]{BinarySearch}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
int BinarySearch(int[] nums, int target) {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
int i = 0, j = nums.Length - 1;
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j]
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m-1]
|
||||
j = m - 1;
|
||||
else // Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search.go"
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
func binarySearch(nums []int, target int) int {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
i, j := 0, len(nums)-1
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
for i <= j {
|
||||
m := i + (j-i)/2 // Calculate the midpoint index m
|
||||
if nums[m] < target { // This means target is in the interval [m+1, j]
|
||||
i = m + 1
|
||||
} else if nums[m] > target { // This means target is in the interval [i, m-1]
|
||||
j = m - 1
|
||||
} else { // Found the target element, return its index
|
||||
return m
|
||||
}
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search.swift"
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
func binarySearch(nums: [Int], target: Int) -> Int {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
var i = nums.startIndex
|
||||
var j = nums.endIndex - 1
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while i <= j {
|
||||
let m = i + (j - i) / 2 // Calculate the midpoint index m
|
||||
if nums[m] < target { // This means target is in the interval [m+1, j]
|
||||
i = m + 1
|
||||
} else if nums[m] > target { // This means target is in the interval [i, m-1]
|
||||
j = m - 1
|
||||
} else { // Found the target element, return its index
|
||||
return m
|
||||
}
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_search.js"
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
function binarySearch(nums, target) {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
let i = 0,
|
||||
j = nums.length - 1;
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while (i <= j) {
|
||||
// Calculate midpoint index m, use parseInt() to round down
|
||||
const m = parseInt(i + (j - i) / 2);
|
||||
if (nums[m] < target)
|
||||
// This means target is in the interval [m+1, j]
|
||||
i = m + 1;
|
||||
else if (nums[m] > target)
|
||||
// This means target is in the interval [i, m-1]
|
||||
j = m - 1;
|
||||
else return m; // Found the target element, return its index
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_search.ts"
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
function binarySearch(nums: number[], target: number): number {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
let i = 0,
|
||||
j = nums.length - 1;
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while (i <= j) {
|
||||
// Calculate the midpoint index m
|
||||
const m = Math.floor(i + (j - i) / 2);
|
||||
if (nums[m] < target) {
|
||||
// This means target is in the interval [m+1, j]
|
||||
i = m + 1;
|
||||
} else if (nums[m] > target) {
|
||||
// This means target is in the interval [i, m-1]
|
||||
j = m - 1;
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return -1; // Target element not found, return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_search.dart"
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
int binarySearch(List<int> nums, int target) {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
int i = 0, j = nums.length - 1;
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) ~/ 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
// This means target is in the interval [m+1, j]
|
||||
i = m + 1;
|
||||
} else if (nums[m] > target) {
|
||||
// This means target is in the interval [i, m-1]
|
||||
j = m - 1;
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search.rs"
|
||||
[class]{}-[func]{binary_search}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
fn binary_search(nums: &[i32], target: i32) -> i32 {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
let mut i = 0;
|
||||
let mut j = nums.len() as i32 - 1;
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while i <= j {
|
||||
let m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if nums[m as usize] < target {
|
||||
// This means target is in the interval [m+1, j]
|
||||
i = m + 1;
|
||||
} else if nums[m as usize] > target {
|
||||
// This means target is in the interval [i, m-1]
|
||||
j = m - 1;
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_search.c"
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
int binarySearch(int *nums, int len, int target) {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
int i = 0, j = len - 1;
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j]
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m-1]
|
||||
j = m - 1;
|
||||
else // Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_search.kt"
|
||||
[class]{}-[func]{binarySearch}
|
||||
/* Binary search (closed interval on both sides) */
|
||||
fun binarySearch(nums: IntArray, target: Int): Int {
|
||||
// Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
var i = 0
|
||||
var j = nums.size - 1
|
||||
// Loop, exit when the search interval is empty (empty when i > j)
|
||||
while (i <= j) {
|
||||
val m = i + (j - i) / 2 // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j]
|
||||
i = m + 1
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m-1]
|
||||
j = m - 1
|
||||
else // Found the target element, return its index
|
||||
return m
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_search.rb"
|
||||
[class]{}-[func]{binary_search}
|
||||
### Binary search (closed interval) ###
|
||||
def binary_search(nums, target)
|
||||
# Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array
|
||||
i, j = 0, nums.length - 1
|
||||
|
||||
# Loop, exit when the search interval is empty (empty when i > j)
|
||||
while i <= j
|
||||
# In theory, Ruby numbers can be infinitely large (limited by memory), no need to consider overflow
|
||||
m = (i + j) / 2 # Calculate the midpoint index m
|
||||
|
||||
if nums[m] < target
|
||||
i = m + 1 # This means target is in the interval [m+1, j]
|
||||
elsif nums[m] > target
|
||||
j = m - 1 # This means target is in the interval [i, m-1]
|
||||
else
|
||||
return m # Found the target element, return its index
|
||||
end
|
||||
end
|
||||
|
||||
-1 # Target element not found, return -1
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
**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$.
|
||||
|
||||
```zig title="binary_search.zig"
|
||||
[class]{}-[func]{binarySearch}
|
||||
```
|
||||
**Space complexity is $O(1)$**: Pointers $i$ and $j$ use constant-size space.
|
||||
|
||||
**Time complexity is $O(\log n)$** : In the binary loop, the interval decreases by half each round, hence the number of iterations is $\log_2 n$.
|
||||
## 10.1.1 Interval Representation Methods
|
||||
|
||||
**Space complexity is $O(1)$** : Pointers $i$ and $j$ occupies constant size of space.
|
||||
|
||||
## 10.1.1 Interval representation methods
|
||||
|
||||
Besides the above closed interval, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, where the left boundary includes itself, and the right boundary does not. In 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 the left boundary includes itself while the right boundary does not. 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:
|
||||
|
||||
@@ -197,39 +376,39 @@ We can implement a binary search algorithm with the same functionality based on
|
||||
|
||||
```python title="binary_search.py"
|
||||
def binary_search_lcro(nums: list[int], target: int) -> int:
|
||||
"""Binary search (left closed right open interval)"""
|
||||
# Initialize left closed right open interval [0, n), i.e., i, j point to the first element and the last element +1 of the array respectively
|
||||
"""Binary search (left-closed right-open interval)"""
|
||||
# Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
i, j = 0, len(nums)
|
||||
# Loop until the search interval is empty (when i = j, it is empty)
|
||||
# Loop, exit when the search interval is empty (empty when i = j)
|
||||
while i < j:
|
||||
m = i + (j - i) // 2 # Calculate midpoint index m
|
||||
m = (i + j) // 2 # Calculate midpoint index m
|
||||
if nums[m] < target:
|
||||
i = m + 1 # This situation indicates that target is in the interval [m+1, j)
|
||||
i = m + 1 # This means target is in the interval [m+1, j)
|
||||
elif nums[m] > target:
|
||||
j = m # This situation indicates that target is in the interval [i, m)
|
||||
j = m # This means target is in the interval [i, m)
|
||||
else:
|
||||
return m # Found the target element, thus return its index
|
||||
return -1 # Did not find the target element, thus return -1
|
||||
return m # Found the target element, return its index
|
||||
return -1 # Target element not found, return -1
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="binary_search.cpp"
|
||||
/* Binary search (left closed right open interval) */
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
int binarySearchLCRO(vector<int> &nums, int target) {
|
||||
// Initialize left closed right open interval [0, n), i.e., i, j point to the first element and the last element +1 of the array respectively
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
int i = 0, j = nums.size();
|
||||
// Loop until the search interval is empty (when i = j, it is empty)
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while (i < j) {
|
||||
int m = i + (j - i) / 2; // Calculate midpoint index m
|
||||
if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j)
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j)
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // This situation indicates that target is in the interval [i, m)
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m)
|
||||
j = m;
|
||||
else // Found the target element, thus return its index
|
||||
else // Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
// Did not find the target element, thus return -1
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
@@ -237,21 +416,21 @@ We can implement a binary search algorithm with the same functionality based on
|
||||
=== "Java"
|
||||
|
||||
```java title="binary_search.java"
|
||||
/* Binary search (left closed right open interval) */
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
int binarySearchLCRO(int[] nums, int target) {
|
||||
// Initialize left closed right open interval [0, n), i.e., i, j point to the first element and the last element +1 of the array respectively
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
int i = 0, j = nums.length;
|
||||
// Loop until the search interval is empty (when i = j, it is empty)
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while (i < j) {
|
||||
int m = i + (j - i) / 2; // Calculate midpoint index m
|
||||
if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j)
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j)
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // This situation indicates that target is in the interval [i, m)
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m)
|
||||
j = m;
|
||||
else // Found the target element, thus return its index
|
||||
else // Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
// Did not find the target element, thus return -1
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
@@ -259,86 +438,266 @@ We can implement a binary search algorithm with the same functionality based on
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_search.cs"
|
||||
[class]{binary_search}-[func]{BinarySearchLCRO}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
int BinarySearchLCRO(int[] nums, int target) {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
int i = 0, j = nums.Length;
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while (i < j) {
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j)
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m)
|
||||
j = m;
|
||||
else // Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search.go"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
func binarySearchLCRO(nums []int, target int) int {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
i, j := 0, len(nums)
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
for i < j {
|
||||
m := i + (j-i)/2 // Calculate the midpoint index m
|
||||
if nums[m] < target { // This means target is in the interval [m+1, j)
|
||||
i = m + 1
|
||||
} else if nums[m] > target { // This means target is in the interval [i, m)
|
||||
j = m
|
||||
} else { // Found the target element, return its index
|
||||
return m
|
||||
}
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search.swift"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
func binarySearchLCRO(nums: [Int], target: Int) -> Int {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
var i = nums.startIndex
|
||||
var j = nums.endIndex
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while i < j {
|
||||
let m = i + (j - i) / 2 // Calculate the midpoint index m
|
||||
if nums[m] < target { // This means target is in the interval [m+1, j)
|
||||
i = m + 1
|
||||
} else if nums[m] > target { // This means target is in the interval [i, m)
|
||||
j = m
|
||||
} else { // Found the target element, return its index
|
||||
return m
|
||||
}
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_search.js"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
function binarySearchLCRO(nums, target) {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
let i = 0,
|
||||
j = nums.length;
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while (i < j) {
|
||||
// Calculate midpoint index m, use parseInt() to round down
|
||||
const m = parseInt(i + (j - i) / 2);
|
||||
if (nums[m] < target)
|
||||
// This means target is in the interval [m+1, j)
|
||||
i = m + 1;
|
||||
else if (nums[m] > target)
|
||||
// This means target is in the interval [i, m)
|
||||
j = m;
|
||||
// Found the target element, return its index
|
||||
else return m;
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_search.ts"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
function binarySearchLCRO(nums: number[], target: number): number {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
let i = 0,
|
||||
j = nums.length;
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while (i < j) {
|
||||
// Calculate the midpoint index m
|
||||
const m = Math.floor(i + (j - i) / 2);
|
||||
if (nums[m] < target) {
|
||||
// This means target is in the interval [m+1, j)
|
||||
i = m + 1;
|
||||
} else if (nums[m] > target) {
|
||||
// This means target is in the interval [i, m)
|
||||
j = m;
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return -1; // Target element not found, return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_search.dart"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
int binarySearchLCRO(List<int> nums, int target) {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
int i = 0, j = nums.length;
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while (i < j) {
|
||||
int m = i + (j - i) ~/ 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
// This means target is in the interval [m+1, j)
|
||||
i = m + 1;
|
||||
} else if (nums[m] > target) {
|
||||
// This means target is in the interval [i, m)
|
||||
j = m;
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search.rs"
|
||||
[class]{}-[func]{binary_search_lcro}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
fn binary_search_lcro(nums: &[i32], target: i32) -> i32 {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
let mut i = 0;
|
||||
let mut j = nums.len() as i32;
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while i < j {
|
||||
let m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if nums[m as usize] < target {
|
||||
// This means target is in the interval [m+1, j)
|
||||
i = m + 1;
|
||||
} else if nums[m as usize] > target {
|
||||
// This means target is in the interval [i, m)
|
||||
j = m;
|
||||
} else {
|
||||
// Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_search.c"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
int binarySearchLCRO(int *nums, int len, int target) {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
int i = 0, j = len;
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while (i < j) {
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j)
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m)
|
||||
j = m;
|
||||
else // Found the target element, return its index
|
||||
return m;
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_search.kt"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
/* Binary search (left-closed right-open interval) */
|
||||
fun binarySearchLCRO(nums: IntArray, target: Int): Int {
|
||||
// Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
var i = 0
|
||||
var j = nums.size
|
||||
// Loop, exit when the search interval is empty (empty when i = j)
|
||||
while (i < j) {
|
||||
val m = i + (j - i) / 2 // Calculate the midpoint index m
|
||||
if (nums[m] < target) // This means target is in the interval [m+1, j)
|
||||
i = m + 1
|
||||
else if (nums[m] > target) // This means target is in the interval [i, m)
|
||||
j = m
|
||||
else // Found the target element, return its index
|
||||
return m
|
||||
}
|
||||
// Target element not found, return -1
|
||||
return -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_search.rb"
|
||||
[class]{}-[func]{binary_search_lcro}
|
||||
### Binary search (left-closed right-open interval) ###
|
||||
def binary_search_lcro(nums, target)
|
||||
# Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1
|
||||
i, j = 0, nums.length
|
||||
|
||||
# Loop, exit when the search interval is empty (empty when i = j)
|
||||
while i < j
|
||||
# Calculate the midpoint index m
|
||||
m = (i + j) / 2
|
||||
|
||||
if nums[m] < target
|
||||
i = m + 1 # This means target is in the interval [m+1, j)
|
||||
elsif nums[m] > target
|
||||
j = m - 1 # This means target is in the interval [i, m)
|
||||
else
|
||||
return m # Found the target element, return its index
|
||||
end
|
||||
end
|
||||
|
||||
-1 # Target element not found, return -1
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
As shown in Figure 10-3, under the two interval representations, the initialization, loop condition, and interval narrowing operations of the binary search algorithm are all different.
|
||||
|
||||
```zig title="binary_search.zig"
|
||||
[class]{}-[func]{binarySearchLCRO}
|
||||
```
|
||||
Since both the left and right boundaries in the "closed interval" representation are defined as closed, the operations to narrow the interval through pointers $i$ and $j$ are also symmetric. This makes it less error-prone, **so the "closed interval" approach is generally recommended**.
|
||||
|
||||
As shown in Figure 10-3, under the two types of interval representations, the initialization, loop condition, and narrowing interval operation of the binary search algorithm differ.
|
||||
{ class="animation-figure" }
|
||||
|
||||
Since both boundaries in the "closed interval" representation are inclusive, the operations to narrow the interval through pointers $i$ and $j$ are also symmetrical. This makes it less prone to errors, **therefore, it is generally recommended to use the "closed interval" approach**.
|
||||
<p align="center"> Figure 10-3 Two interval definitions </p>
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 10-3 Two types of interval definitions </p>
|
||||
|
||||
## 10.1.2 Advantages and limitations
|
||||
## 10.1.2 Advantages and Limitations
|
||||
|
||||
Binary search performs well in both time and space aspects.
|
||||
|
||||
- Binary search is time-efficient. With large dataset, the logarithmic time complexity offers a major advantage. For instance, given a dataset with size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ iterations, while binary search only demands $\log_2 2^{20} = 20$ loops.
|
||||
- Binary search does not need extra space. Compared to search algorithms that rely on additional space (like hash search), binary search is more space-efficient.
|
||||
- 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 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 may not be suitable for all scenarios due to the following concerns.
|
||||
However, binary search is not suitable for all situations, mainly for the following reasons:
|
||||
|
||||
- Binary search can only be applied to sorted data. Unsorted data must be sorted before applying binary search, which may not be worthwhile as sorting algorithm typically has a time complexity of $O(n \log n)$. Such cost is even higher than linear search, not to mention binary search itself. For scenarios with frequent insertion, the cost of remaining the array in order is pretty high as the time complexity of inserting new elements into specific positions is $O(n)$.
|
||||
- Binary search may use array only. Binary search requires non-continuous (jumping) element access, which is inefficient in linked list. As a result, linked list or data structures based on linked list may not be suitable for this algorithm.
|
||||
- Linear search performs better on small dataset. In linear search, only 1 decision operation is required for each iteration; whereas in binary search, it involves 1 addition, 1 division, 1 to 3 decision operations, 1 addition (subtraction), totaling 4 to 6 operations. Therefore, if data size $n$ is small, linear search is faster than binary search.
|
||||
- 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.
|
||||
- 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,22 +2,22 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.3 Binary search boundaries
|
||||
# 10.3 Binary Search Edge Cases
|
||||
|
||||
## 10.3.1 Find the left boundary
|
||||
## 10.3.1 Finding the Left Boundary
|
||||
|
||||
!!! question
|
||||
|
||||
Given a sorted array `nums` of length $n$, which may contain duplicate elements, return the index of the leftmost element `target`. If the element is not present in the array, return $-1$.
|
||||
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$.
|
||||
|
||||
Recalling the method of binary search for an insertion point, after the search is completed, the index $i$ will point to the leftmost occurrence of `target`. Therefore, **searching for the insertion point is essentially the same as finding the index of the leftmost `target`**.
|
||||
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`**.
|
||||
|
||||
We can use the function for finding an insertion point to find the left boundary of `target`. Note that the array might not contain `target`, which could lead to the following two results:
|
||||
Consider implementing the left boundary search using the insertion point finding function. Note that the array may not contain `target`, which could result in the following two cases:
|
||||
|
||||
- The index $i$ of the insertion point is out of bounds.
|
||||
- The insertion point index $i$ is out of bounds.
|
||||
- The element `nums[i]` is not equal to `target`.
|
||||
|
||||
In these cases, simply return $-1$. The code is as follows:
|
||||
When either of these situations occurs, simply return $-1$. The code is shown below:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -26,7 +26,7 @@ In these cases, simply return $-1$. The code is as follows:
|
||||
"""Binary search for the leftmost target"""
|
||||
# Equivalent to finding the insertion point of target
|
||||
i = binary_search_insertion(nums, target)
|
||||
# Did not find target, thus return -1
|
||||
# Target not found, return -1
|
||||
if i == len(nums) or nums[i] != target:
|
||||
return -1
|
||||
# Found target, return index i
|
||||
@@ -40,7 +40,7 @@ In these cases, simply return $-1$. The code is as follows:
|
||||
int binarySearchLeftEdge(vector<int> &nums, int target) {
|
||||
// Equivalent to finding the insertion point of target
|
||||
int i = binarySearchInsertion(nums, target);
|
||||
// Did not find target, thus return -1
|
||||
// Target not found, return -1
|
||||
if (i == nums.size() || nums[i] != target) {
|
||||
return -1;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ In these cases, simply return $-1$. The code is as follows:
|
||||
int binarySearchLeftEdge(int[] nums, int target) {
|
||||
// Equivalent to finding the insertion point of target
|
||||
int i = binary_search_insertion.binarySearchInsertion(nums, target);
|
||||
// Did not find target, thus return -1
|
||||
// Target not found, return -1
|
||||
if (i == nums.length || nums[i] != target) {
|
||||
return -1;
|
||||
}
|
||||
@@ -68,86 +68,179 @@ In these cases, simply return $-1$. The code is as follows:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_search_edge.cs"
|
||||
[class]{binary_search_edge}-[func]{BinarySearchLeftEdge}
|
||||
/* Binary search for the leftmost target */
|
||||
int BinarySearchLeftEdge(int[] nums, int target) {
|
||||
// Equivalent to finding the insertion point of target
|
||||
int i = binary_search_insertion.BinarySearchInsertion(nums, target);
|
||||
// Target not found, return -1
|
||||
if (i == nums.Length || nums[i] != target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search_edge.go"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
/* Binary search for the leftmost target */
|
||||
func binarySearchLeftEdge(nums []int, target int) int {
|
||||
// Equivalent to finding the insertion point of target
|
||||
i := binarySearchInsertion(nums, target)
|
||||
// Target not found, return -1
|
||||
if i == len(nums) || nums[i] != target {
|
||||
return -1
|
||||
}
|
||||
// Found target, return index i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search_edge.swift"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
/* Binary search for the leftmost target */
|
||||
func binarySearchLeftEdge(nums: [Int], target: Int) -> Int {
|
||||
// Equivalent to finding the insertion point of target
|
||||
let i = binarySearchInsertion(nums: nums, target: target)
|
||||
// Target not found, return -1
|
||||
if i == nums.endIndex || nums[i] != target {
|
||||
return -1
|
||||
}
|
||||
// Found target, return index i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_search_edge.js"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
/* Binary search for the leftmost target */
|
||||
function binarySearchLeftEdge(nums, target) {
|
||||
// Equivalent to finding the insertion point of target
|
||||
const i = binarySearchInsertion(nums, target);
|
||||
// Target not found, return -1
|
||||
if (i === nums.length || nums[i] !== target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_search_edge.ts"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
/* Binary search for the leftmost target */
|
||||
function binarySearchLeftEdge(nums: Array<number>, target: number): number {
|
||||
// Equivalent to finding the insertion point of target
|
||||
const i = binarySearchInsertion(nums, target);
|
||||
// Target not found, return -1
|
||||
if (i === nums.length || nums[i] !== target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_search_edge.dart"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
/* Binary search for the leftmost target */
|
||||
int binarySearchLeftEdge(List<int> nums, int target) {
|
||||
// Equivalent to finding the insertion point of target
|
||||
int i = binarySearchInsertion(nums, target);
|
||||
// Target not found, return -1
|
||||
if (i == nums.length || nums[i] != target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search_edge.rs"
|
||||
[class]{}-[func]{binary_search_left_edge}
|
||||
/* Binary search for the leftmost target */
|
||||
fn binary_search_left_edge(nums: &[i32], target: i32) -> i32 {
|
||||
// Equivalent to finding the insertion point of target
|
||||
let i = binary_search_insertion(nums, target);
|
||||
// Target not found, return -1
|
||||
if i == nums.len() as i32 || nums[i as usize] != target {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index i
|
||||
i
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_search_edge.c"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
/* Binary search for the leftmost target */
|
||||
int binarySearchLeftEdge(int *nums, int numSize, int target) {
|
||||
// Equivalent to finding the insertion point of target
|
||||
int i = binarySearchInsertion(nums, numSize, target);
|
||||
// Target not found, return -1
|
||||
if (i == numSize || nums[i] != target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_search_edge.kt"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
/* Binary search for the leftmost target */
|
||||
fun binarySearchLeftEdge(nums: IntArray, target: Int): Int {
|
||||
// Equivalent to finding the insertion point of target
|
||||
val i = binarySearchInsertion(nums, target)
|
||||
// Target not found, return -1
|
||||
if (i == nums.size || nums[i] != target) {
|
||||
return -1
|
||||
}
|
||||
// Found target, return index i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_search_edge.rb"
|
||||
[class]{}-[func]{binary_search_left_edge}
|
||||
### Binary search leftmost target ###
|
||||
def binary_search_left_edge(nums, target)
|
||||
# Equivalent to finding the insertion point of target
|
||||
i = binary_search_insertion(nums, target)
|
||||
|
||||
# Target not found, return -1
|
||||
return -1 if i == nums.length || nums[i] != target
|
||||
|
||||
i # Found target, return index i
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 10.3.2 Finding the Right Boundary
|
||||
|
||||
```zig title="binary_search_edge.zig"
|
||||
[class]{}-[func]{binarySearchLeftEdge}
|
||||
```
|
||||
So how do we find the rightmost `target`? The most direct approach is to modify the code and replace the pointer shrinking operation in the `nums[m] == target` case. The code is omitted here; interested readers can implement it themselves.
|
||||
|
||||
## 10.3.2 Find the right boundary
|
||||
Below we introduce two more clever methods.
|
||||
|
||||
How do we find the rightmost occurrence of `target`? The most straightforward way is to modify the traditional binary search logic by changing how we adjust the search boundaries in the case of `nums[m] == target`. The code is omitted here. If you are interested, try to implement the code on your own.
|
||||
### 1. Reusing Left Boundary Search
|
||||
|
||||
Below we are going to introduce two more ingenious methods.
|
||||
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`**.
|
||||
|
||||
### 1. Reuse the left boundary search
|
||||
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$**.
|
||||
|
||||
To find the rightmost occurrence of `target`, we can reuse the function used for locating the leftmost `target`. Specifically, we transform the search for the rightmost target into a search for the leftmost target + 1.
|
||||
{ class="animation-figure" }
|
||||
|
||||
As shown in Figure 10-7, after the search is complete, pointer $i$ will point to the leftmost `target + 1` (if exists), while pointer $j$ will point to the rightmost occurrence of `target`. Therefore, returning $j$ will give us the right boundary.
|
||||
<p align="center"> Figure 10-7 Converting right boundary search to left boundary search </p>
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 10-7 Transforming the search for the right boundary into the search for the left boundary </p>
|
||||
|
||||
Note that the insertion point returned is $i$, therefore, it should be subtracted by $1$ to obtain $j$:
|
||||
Note that the returned insertion point is $i$, so we need to subtract $1$ from it to obtain $j$:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -158,7 +251,7 @@ Note that the insertion point returned is $i$, therefore, it should be subtracte
|
||||
i = binary_search_insertion(nums, target + 1)
|
||||
# j points to the rightmost target, i points to the first element greater than target
|
||||
j = i - 1
|
||||
# Did not find target, thus return -1
|
||||
# Target not found, return -1
|
||||
if j == -1 or nums[j] != target:
|
||||
return -1
|
||||
# Found target, return index j
|
||||
@@ -174,7 +267,7 @@ Note that the insertion point returned is $i$, therefore, it should be subtracte
|
||||
int i = binarySearchInsertion(nums, target + 1);
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
int j = i - 1;
|
||||
// Did not find target, thus return -1
|
||||
// Target not found, return -1
|
||||
if (j == -1 || nums[j] != target) {
|
||||
return -1;
|
||||
}
|
||||
@@ -192,7 +285,7 @@ Note that the insertion point returned is $i$, therefore, it should be subtracte
|
||||
int i = binary_search_insertion.binarySearchInsertion(nums, target + 1);
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
int j = i - 1;
|
||||
// Did not find target, thus return -1
|
||||
// Target not found, return -1
|
||||
if (j == -1 || nums[j] != target) {
|
||||
return -1;
|
||||
}
|
||||
@@ -204,83 +297,197 @@ Note that the insertion point returned is $i$, therefore, it should be subtracte
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_search_edge.cs"
|
||||
[class]{binary_search_edge}-[func]{BinarySearchRightEdge}
|
||||
/* Binary search for the rightmost target */
|
||||
int BinarySearchRightEdge(int[] nums, int target) {
|
||||
// Convert to finding the leftmost target + 1
|
||||
int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1);
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
int j = i - 1;
|
||||
// Target not found, return -1
|
||||
if (j == -1 || nums[j] != target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index j
|
||||
return j;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search_edge.go"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
/* Binary search for the rightmost target */
|
||||
func binarySearchRightEdge(nums []int, target int) int {
|
||||
// Convert to finding the leftmost target + 1
|
||||
i := binarySearchInsertion(nums, target+1)
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
j := i - 1
|
||||
// Target not found, return -1
|
||||
if j == -1 || nums[j] != target {
|
||||
return -1
|
||||
}
|
||||
// Found target, return index j
|
||||
return j
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search_edge.swift"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
/* Binary search for the rightmost target */
|
||||
func binarySearchRightEdge(nums: [Int], target: Int) -> Int {
|
||||
// Convert to finding the leftmost target + 1
|
||||
let i = binarySearchInsertion(nums: nums, target: target + 1)
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
let j = i - 1
|
||||
// Target not found, return -1
|
||||
if j == -1 || nums[j] != target {
|
||||
return -1
|
||||
}
|
||||
// Found target, return index j
|
||||
return j
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_search_edge.js"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
/* Binary search for the rightmost target */
|
||||
function binarySearchRightEdge(nums, target) {
|
||||
// Convert to finding the leftmost target + 1
|
||||
const i = binarySearchInsertion(nums, target + 1);
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
const j = i - 1;
|
||||
// Target not found, return -1
|
||||
if (j === -1 || nums[j] !== target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index j
|
||||
return j;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_search_edge.ts"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
/* Binary search for the rightmost target */
|
||||
function binarySearchRightEdge(nums: Array<number>, target: number): number {
|
||||
// Convert to finding the leftmost target + 1
|
||||
const i = binarySearchInsertion(nums, target + 1);
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
const j = i - 1;
|
||||
// Target not found, return -1
|
||||
if (j === -1 || nums[j] !== target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index j
|
||||
return j;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_search_edge.dart"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
/* Binary search for the rightmost target */
|
||||
int binarySearchRightEdge(List<int> nums, int target) {
|
||||
// Convert to finding the leftmost target + 1
|
||||
int i = binarySearchInsertion(nums, target + 1);
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
int j = i - 1;
|
||||
// Target not found, return -1
|
||||
if (j == -1 || nums[j] != target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index j
|
||||
return j;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search_edge.rs"
|
||||
[class]{}-[func]{binary_search_right_edge}
|
||||
/* Binary search for the rightmost target */
|
||||
fn binary_search_right_edge(nums: &[i32], target: i32) -> i32 {
|
||||
// Convert to finding the leftmost target + 1
|
||||
let i = binary_search_insertion(nums, target + 1);
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
let j = i - 1;
|
||||
// Target not found, return -1
|
||||
if j == -1 || nums[j as usize] != target {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index j
|
||||
j
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_search_edge.c"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
/* Binary search for the rightmost target */
|
||||
int binarySearchRightEdge(int *nums, int numSize, int target) {
|
||||
// Convert to finding the leftmost target + 1
|
||||
int i = binarySearchInsertion(nums, numSize, target + 1);
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
int j = i - 1;
|
||||
// Target not found, return -1
|
||||
if (j == -1 || nums[j] != target) {
|
||||
return -1;
|
||||
}
|
||||
// Found target, return index j
|
||||
return j;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_search_edge.kt"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
/* Binary search for the rightmost target */
|
||||
fun binarySearchRightEdge(nums: IntArray, target: Int): Int {
|
||||
// Convert to finding the leftmost target + 1
|
||||
val i = binarySearchInsertion(nums, target + 1)
|
||||
// j points to the rightmost target, i points to the first element greater than target
|
||||
val j = i - 1
|
||||
// Target not found, return -1
|
||||
if (j == -1 || nums[j] != target) {
|
||||
return -1
|
||||
}
|
||||
// Found target, return index j
|
||||
return j
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_search_edge.rb"
|
||||
[class]{}-[func]{binary_search_right_edge}
|
||||
### Binary search rightmost target ###
|
||||
def binary_search_right_edge(nums, target)
|
||||
# Convert to finding the leftmost target + 1
|
||||
i = binary_search_insertion(nums, target + 1)
|
||||
|
||||
# j points to the rightmost target, i points to the first element greater than target
|
||||
j = i - 1
|
||||
|
||||
# Target not found, return -1
|
||||
return -1 if j == -1 || nums[j] != target
|
||||
|
||||
j # Found target, return index j
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
### 2. Converting to Element Search
|
||||
|
||||
```zig title="binary_search_edge.zig"
|
||||
[class]{}-[func]{binarySearchRightEdge}
|
||||
```
|
||||
We know that when the array does not contain `target`, $i$ and $j$ will eventually point to the first elements greater than and less than `target`, respectively.
|
||||
|
||||
### 2. Transform into an element search
|
||||
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.
|
||||
|
||||
When the array does not contain `target`, $i$ and $j$ will eventually point to the first element greater and smaller than `target` respectively.
|
||||
- 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$.
|
||||
|
||||
Thus, as shown in Figure 10-8, we can construct an element that does not exist in the array, to search for the left and right boundaries.
|
||||
{ class="animation-figure" }
|
||||
|
||||
- To find the leftmost `target`: it can be transformed into searching for `target - 0.5`, and return the pointer $i$.
|
||||
- To find the rightmost `target`: it can be transformed into searching for `target + 0.5`, and return the pointer $j$.
|
||||
<p align="center"> Figure 10-8 Converting boundary search to element search </p>
|
||||
|
||||
{ class="animation-figure" }
|
||||
The code is omitted here, but the following two points are worth noting:
|
||||
|
||||
<p align="center"> Figure 10-8 Transforming the search for boundaries into the search for an element </p>
|
||||
|
||||
The code is omitted here, but here are two important points to note about this approach.
|
||||
|
||||
- The given array `nums` does not contain decimal, so handling equal cases is not a concern.
|
||||
- However, introducing decimals in this approach requires modifying the `target` variable to a floating-point type (no change needed in Python).
|
||||
- Since the given array does not contain decimals, we don't need to worry about how to handle equal cases.
|
||||
- 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).
|
||||
|
||||
@@ -2,47 +2,47 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.2 Binary search insertion
|
||||
# 10.2 Binary Search Insertion Point
|
||||
|
||||
Binary search is not only used to search for target elements but also to solve many variant problems, such as searching for the insertion position of target elements.
|
||||
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.
|
||||
|
||||
## 10.2.1 Case with no duplicate elements
|
||||
## 10.2.1 Case Without Duplicate Elements
|
||||
|
||||
!!! question
|
||||
|
||||
Given a sorted array `nums` of length $n$ with unique elements and an element `target`, insert `target` into `nums` while maintaining its sorted order. If `target` already exists in the array, insert it to the left of the existing element. Return the index of `target` in the array after insertion. See the example 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 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.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 10-4 Example data for binary search insertion point </p>
|
||||
<p align="center"> Figure 10-4 Binary search insertion point example data </p>
|
||||
|
||||
If you want to reuse the binary search code from the previous section, you need to answer the following two questions.
|
||||
If we want to reuse the binary search code from the previous section, we need to answer the following two questions.
|
||||
|
||||
**Question one**: If the array already contains `target`, would the insertion point be the index of existing element?
|
||||
**Question 1**: When the array contains `target`, is the insertion point index the same as that element's index?
|
||||
|
||||
The requirement to insert `target` to the left of equal elements means that the newly inserted `target` will replace the original `target` position. In other words, **when the array contains `target`, the insertion point is indeed the index of that `target`**.
|
||||
The problem requires inserting `target` to the left of equal elements, which means the newly inserted `target` replaces the position of the original `target`. In other words, **when the array contains `target`, the insertion point index is the index of that `target`**.
|
||||
|
||||
**Question two**: When the array does not contain `target`, at which index would it be inserted?
|
||||
**Question 2**: When the array does not contain `target`, what is the insertion point index?
|
||||
|
||||
Let's further consider the binary search process: when `nums[m] < target`, pointer $i$ moves, meaning that pointer $i$ is approaching an element greater than or equal to `target`. Similarly, pointer $j$ is always approaching an element less than or equal to `target`.
|
||||
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`.
|
||||
|
||||
Therefore, at the end of the binary, it is certain that: $i$ points to the first element greater than `target`, and $j$ points to the first element less than `target`. **It is easy to see that when the array does not contain `target`, the insertion point is $i$**. The code is as follows:
|
||||
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:
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="binary_search_insertion.py"
|
||||
def binary_search_insertion_simple(nums: list[int], target: int) -> int:
|
||||
"""Binary search for insertion point (no duplicate elements)"""
|
||||
i, j = 0, len(nums) - 1 # Initialize double closed interval [0, n-1]
|
||||
i, j = 0, len(nums) - 1 # Initialize closed interval [0, n-1]
|
||||
while i <= j:
|
||||
m = i + (j - i) // 2 # Calculate midpoint index m
|
||||
m = (i + j) // 2 # Calculate midpoint index m
|
||||
if nums[m] < target:
|
||||
i = m + 1 # Target is in interval [m+1, j]
|
||||
i = m + 1 # target is in the interval [m+1, j]
|
||||
elif nums[m] > target:
|
||||
j = m - 1 # Target is in interval [i, m-1]
|
||||
j = m - 1 # target is in the interval [i, m-1]
|
||||
else:
|
||||
return m # Found target, return insertion point m
|
||||
# Did not find target, return insertion point i
|
||||
# Target not found, return insertion point i
|
||||
return i
|
||||
```
|
||||
|
||||
@@ -51,18 +51,18 @@ Therefore, at the end of the binary, it is certain that: $i$ points to the first
|
||||
```cpp title="binary_search_insertion.cpp"
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
int binarySearchInsertionSimple(vector<int> &nums, int target) {
|
||||
int i = 0, j = nums.size() - 1; // Initialize double closed interval [0, n-1]
|
||||
int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate midpoint index m
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // Target is in interval [m+1, j]
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // Target is in interval [i, m-1]
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m; // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Did not find target, return insertion point i
|
||||
// Target not found, return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
@@ -72,18 +72,18 @@ Therefore, at the end of the binary, it is certain that: $i$ points to the first
|
||||
```java title="binary_search_insertion.java"
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
int binarySearchInsertionSimple(int[] nums, int target) {
|
||||
int i = 0, j = nums.length - 1; // Initialize double closed interval [0, n-1]
|
||||
int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate midpoint index m
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // Target is in interval [m+1, j]
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // Target is in interval [i, m-1]
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m; // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Did not find target, return insertion point i
|
||||
// Target not found, return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
@@ -91,94 +91,255 @@ Therefore, at the end of the binary, it is certain that: $i$ points to the first
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_search_insertion.cs"
|
||||
[class]{binary_search_insertion}-[func]{BinarySearchInsertionSimple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
int BinarySearchInsertionSimple(int[] nums, int target) {
|
||||
int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m; // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search_insertion.go"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
func binarySearchInsertionSimple(nums []int, target int) int {
|
||||
// Initialize closed interval [0, n-1]
|
||||
i, j := 0, len(nums)-1
|
||||
for i <= j {
|
||||
// Calculate the midpoint index m
|
||||
m := i + (j-i)/2
|
||||
if nums[m] < target {
|
||||
// target is in the interval [m+1, j]
|
||||
i = m + 1
|
||||
} else if nums[m] > target {
|
||||
// target is in the interval [i, m-1]
|
||||
j = m - 1
|
||||
} else {
|
||||
// Found target, return insertion point m
|
||||
return m
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search_insertion.swift"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
func binarySearchInsertionSimple(nums: [Int], target: Int) -> Int {
|
||||
// Initialize closed interval [0, n-1]
|
||||
var i = nums.startIndex
|
||||
var j = nums.endIndex - 1
|
||||
while i <= j {
|
||||
let m = i + (j - i) / 2 // Calculate the midpoint index m
|
||||
if nums[m] < target {
|
||||
i = m + 1 // target is in the interval [m+1, j]
|
||||
} else if nums[m] > target {
|
||||
j = m - 1 // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_search_insertion.js"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
function binarySearchInsertionSimple(nums, target) {
|
||||
let i = 0,
|
||||
j = nums.length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m; // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_search_insertion.ts"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
function binarySearchInsertionSimple(
|
||||
nums: Array<number>,
|
||||
target: number
|
||||
): number {
|
||||
let i = 0,
|
||||
j = nums.length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m; // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_search_insertion.dart"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
int binarySearchInsertionSimple(List<int> nums, int target) {
|
||||
int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) ~/ 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m; // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search_insertion.rs"
|
||||
[class]{}-[func]{binary_search_insertion_simple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
fn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 {
|
||||
let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]
|
||||
while i <= j {
|
||||
let m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if nums[m as usize] < target {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if nums[m as usize] > target {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m;
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
i
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_search_insertion.c"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
int binarySearchInsertionSimple(int *nums, int numSize, int target) {
|
||||
int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m; // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_search_insertion.kt"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
/* Binary search for insertion point (no duplicate elements) */
|
||||
fun binarySearchInsertionSimple(nums: IntArray, target: Int): Int {
|
||||
var i = 0
|
||||
var j = nums.size - 1 // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
val m = i + (j - i) / 2 // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1 // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1 // target is in the interval [i, m-1]
|
||||
} else {
|
||||
return m // Found target, return insertion point m
|
||||
}
|
||||
}
|
||||
// Target not found, return insertion point i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_search_insertion.rb"
|
||||
[class]{}-[func]{binary_search_insertion_simple}
|
||||
### Binary search insertion point (no duplicates) ###
|
||||
def binary_search_insertion_simple(nums, target)
|
||||
# Initialize closed interval [0, n-1]
|
||||
i, j = 0, nums.length - 1
|
||||
|
||||
while i <= j
|
||||
# Calculate the midpoint index m
|
||||
m = (i + j) / 2
|
||||
|
||||
if nums[m] < target
|
||||
i = m + 1 # target is in the interval [m+1, j]
|
||||
elsif nums[m] > target
|
||||
j = m - 1 # target is in the interval [i, m-1]
|
||||
else
|
||||
return m # Found target, return insertion point m
|
||||
end
|
||||
end
|
||||
|
||||
i # Target not found, return insertion point i
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="binary_search_insertion.zig"
|
||||
[class]{}-[func]{binarySearchInsertionSimple}
|
||||
```
|
||||
|
||||
## 10.2.2 Case with duplicate elements
|
||||
## 10.2.2 Case with Duplicate Elements
|
||||
|
||||
!!! question
|
||||
|
||||
Based on the previous question, assume the array may contain duplicate elements, all else remains the same.
|
||||
Based on the previous problem, assume the array may contain duplicate elements, with everything else remaining the same.
|
||||
|
||||
When there are multiple occurrences of `target` in the array, a regular binary search can only return the index of one occurrence of `target`, **and it cannot determine how many occurrences of `target` are to the left and right of that position**.
|
||||
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**. Initially, consider implementing this through the steps shown in Figure 10-5:
|
||||
|
||||
1. Perform a binary search to find any index of `target`, say $k$.
|
||||
2. Starting from index $k$, conduct a linear search to the left until the leftmost occurrence of `target` is found, then return this index.
|
||||
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.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 10-5 Linear search for the insertion point of duplicate elements </p>
|
||||
<p align="center"> Figure 10-5 Linear search for insertion point of duplicate elements </p>
|
||||
|
||||
Although this method is feasible, it includes linear search, so its time complexity is $O(n)$. This method is inefficient when the array contains many duplicate `target`s.
|
||||
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 the same. In each round, we first calculate the middle index $m$, then compare the value of `target` with `nums[m]`, leading to the following cases.
|
||||
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:
|
||||
|
||||
- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, thus use the normal binary search to narrow the search range, **bringing pointers $i$ and $j$ closer to `target`**.
|
||||
- When `nums[m] == target`, it indicates that the elements less than `target` are in the range $[i, m - 1]$, therefore use $j = m - 1$ to narrow the range, **thus bringing pointer $j$ closer to the elements less than `target`**.
|
||||
- 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`**.
|
||||
|
||||
After the loop, $i$ points to the leftmost `target`, and $j$ points to the first element less than `target`, **therefore index $i$ is the insertion point**.
|
||||
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**.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
@@ -206,24 +367,24 @@ After the loop, $i$ points to the leftmost `target`, and $j$ points to the first
|
||||
|
||||
<p align="center"> Figure 10-6 Steps for binary search insertion point of duplicate elements </p>
|
||||
|
||||
Observe the following code. The operations in the branches `nums[m] > target` and `nums[m] == target` are the same, so these two branches can be merged.
|
||||
Observe the following code: the operations for branches `nums[m] > target` and `nums[m] == target` are the same, so the two can be merged.
|
||||
|
||||
Even so, we can still keep the conditions expanded, as it makes the logic clearer and improves readability.
|
||||
Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="binary_search_insertion.py"
|
||||
def binary_search_insertion(nums: list[int], target: int) -> int:
|
||||
"""Binary search for insertion point (with duplicate elements)"""
|
||||
i, j = 0, len(nums) - 1 # Initialize double closed interval [0, n-1]
|
||||
i, j = 0, len(nums) - 1 # Initialize closed interval [0, n-1]
|
||||
while i <= j:
|
||||
m = i + (j - i) // 2 # Calculate midpoint index m
|
||||
m = (i + j) // 2 # Calculate midpoint index m
|
||||
if nums[m] < target:
|
||||
i = m + 1 # Target is in interval [m+1, j]
|
||||
i = m + 1 # target is in the interval [m+1, j]
|
||||
elif nums[m] > target:
|
||||
j = m - 1 # Target is in interval [i, m-1]
|
||||
j = m - 1 # target is in the interval [i, m-1]
|
||||
else:
|
||||
j = m - 1 # First element less than target is in interval [i, m-1]
|
||||
j = m - 1 # The first element less than target is in the interval [i, m-1]
|
||||
# Return insertion point i
|
||||
return i
|
||||
```
|
||||
@@ -233,15 +394,15 @@ Even so, we can still keep the conditions expanded, as it makes the logic cleare
|
||||
```cpp title="binary_search_insertion.cpp"
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
int binarySearchInsertion(vector<int> &nums, int target) {
|
||||
int i = 0, j = nums.size() - 1; // Initialize double closed interval [0, n-1]
|
||||
int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate midpoint index m
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // Target is in interval [m+1, j]
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // Target is in interval [i, m-1]
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1; // First element less than target is in interval [i, m-1]
|
||||
j = m - 1; // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
@@ -254,15 +415,15 @@ Even so, we can still keep the conditions expanded, as it makes the logic cleare
|
||||
```java title="binary_search_insertion.java"
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
int binarySearchInsertion(int[] nums, int target) {
|
||||
int i = 0, j = nums.length - 1; // Initialize double closed interval [0, n-1]
|
||||
int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate midpoint index m
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // Target is in interval [m+1, j]
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // Target is in interval [i, m-1]
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1; // First element less than target is in interval [i, m-1]
|
||||
j = m - 1; // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
@@ -273,73 +434,231 @@ Even so, we can still keep the conditions expanded, as it makes the logic cleare
|
||||
=== "C#"
|
||||
|
||||
```csharp title="binary_search_insertion.cs"
|
||||
[class]{binary_search_insertion}-[func]{BinarySearchInsertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
int BinarySearchInsertion(int[] nums, int target) {
|
||||
int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1; // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search_insertion.go"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
func binarySearchInsertion(nums []int, target int) int {
|
||||
// Initialize closed interval [0, n-1]
|
||||
i, j := 0, len(nums)-1
|
||||
for i <= j {
|
||||
// Calculate the midpoint index m
|
||||
m := i + (j-i)/2
|
||||
if nums[m] < target {
|
||||
// target is in the interval [m+1, j]
|
||||
i = m + 1
|
||||
} else if nums[m] > target {
|
||||
// target is in the interval [i, m-1]
|
||||
j = m - 1
|
||||
} else {
|
||||
// The first element less than target is in the interval [i, m-1]
|
||||
j = m - 1
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search_insertion.swift"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
func binarySearchInsertion(nums: [Int], target: Int) -> Int {
|
||||
// Initialize closed interval [0, n-1]
|
||||
var i = nums.startIndex
|
||||
var j = nums.endIndex - 1
|
||||
while i <= j {
|
||||
let m = i + (j - i) / 2 // Calculate the midpoint index m
|
||||
if nums[m] < target {
|
||||
i = m + 1 // target is in the interval [m+1, j]
|
||||
} else if nums[m] > target {
|
||||
j = m - 1 // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1 // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="binary_search_insertion.js"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
function binarySearchInsertion(nums, target) {
|
||||
let i = 0,
|
||||
j = nums.length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1; // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="binary_search_insertion.ts"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
function binarySearchInsertion(nums: Array<number>, target: number): number {
|
||||
let i = 0,
|
||||
j = nums.length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1; // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="binary_search_insertion.dart"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
int binarySearchInsertion(List<int> nums, int target) {
|
||||
int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) ~/ 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1; // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="binary_search_insertion.rs"
|
||||
[class]{}-[func]{binary_search_insertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
pub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 {
|
||||
let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1]
|
||||
while i <= j {
|
||||
let m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if nums[m as usize] < target {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if nums[m as usize] > target {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1; // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
i
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="binary_search_insertion.c"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
int binarySearchInsertion(int *nums, int numSize, int target) {
|
||||
int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
int m = i + (j - i) / 2; // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1; // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1; // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1; // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
return i;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="binary_search_insertion.kt"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
/* Binary search for insertion point (with duplicate elements) */
|
||||
fun binarySearchInsertion(nums: IntArray, target: Int): Int {
|
||||
var i = 0
|
||||
var j = nums.size - 1 // Initialize closed interval [0, n-1]
|
||||
while (i <= j) {
|
||||
val m = i + (j - i) / 2 // Calculate the midpoint index m
|
||||
if (nums[m] < target) {
|
||||
i = m + 1 // target is in the interval [m+1, j]
|
||||
} else if (nums[m] > target) {
|
||||
j = m - 1 // target is in the interval [i, m-1]
|
||||
} else {
|
||||
j = m - 1 // The first element less than target is in the interval [i, m-1]
|
||||
}
|
||||
}
|
||||
// Return insertion point i
|
||||
return i
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="binary_search_insertion.rb"
|
||||
[class]{}-[func]{binary_search_insertion}
|
||||
```
|
||||
### Binary search insertion point (with duplicates) ###
|
||||
def binary_search_insertion(nums, target)
|
||||
# Initialize closed interval [0, n-1]
|
||||
i, j = 0, nums.length - 1
|
||||
|
||||
=== "Zig"
|
||||
while i <= j
|
||||
# Calculate the midpoint index m
|
||||
m = (i + j) / 2
|
||||
|
||||
```zig title="binary_search_insertion.zig"
|
||||
[class]{}-[func]{binarySearchInsertion}
|
||||
if nums[m] < target
|
||||
i = m + 1 # target is in the interval [m+1, j]
|
||||
elsif nums[m] > target
|
||||
j = m - 1 # target is in the interval [i, m-1]
|
||||
else
|
||||
j = m - 1 # The first element less than target is in the interval [i, m-1]
|
||||
end
|
||||
end
|
||||
|
||||
i # Return insertion point i
|
||||
end
|
||||
```
|
||||
|
||||
!!! tip
|
||||
|
||||
The code in this section uses "closed interval". If you are interested in "left-closed, right-open", try to implement the code on your own.
|
||||
The code in this section all uses the "closed interval" approach. Interested readers can implement the "left-closed right-open" approach themselves.
|
||||
|
||||
In summary, binary search essentially involves setting search targets for pointers $i$ and $j$. These targets could be a specific element (like `target`) or a range of elements (such as those smaller than `target`).
|
||||
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`).
|
||||
|
||||
In the continuous loop of binary search, pointers $i$ and $j$ gradually approach the predefined target. Ultimately, they either find the answer or stop after crossing the boundary.
|
||||
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.
|
||||
|
||||
@@ -9,15 +9,15 @@ icon: material/text-search
|
||||
|
||||
!!! abstract
|
||||
|
||||
Searching is an adventure into the unknown; where we may need to traverse every corner of a mysterious space, or perhaps we’ll quickly locate our target.
|
||||
|
||||
On this journey of discovery, each exploration may end up with an unexpected answer.
|
||||
Searching is an adventure into the unknown, where we may need to traverse every corner of the mysterious space, or we may be able to quickly lock onto the target.
|
||||
|
||||
In this journey of discovery, each exploration may yield an unexpected answer.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [10.1 Binary search](binary_search.md)
|
||||
- [10.2 Binary search insertion](binary_search_insertion.md)
|
||||
- [10.3 Binary search boundaries](binary_search_edge.md)
|
||||
- [10.4 Hashing optimization strategies](replace_linear_by_hashing.md)
|
||||
- [10.5 Search algorithms revisited](searching_algorithm_revisited.md)
|
||||
- [10.1 Binary Search](binary_search.md)
|
||||
- [10.2 Binary Search Insertion](binary_search_insertion.md)
|
||||
- [10.3 Binary Search Edge Cases](binary_search_edge.md)
|
||||
- [10.4 Hash Optimization Strategy](replace_linear_by_hashing.md)
|
||||
- [10.5 Search Algorithms Revisited](searching_algorithm_revisited.md)
|
||||
- [10.6 Summary](summary.md)
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.4 Hash optimization strategies
|
||||
# 10.4 Hash Optimization Strategy
|
||||
|
||||
In algorithm problems, **we often reduce the time complexity of an algorithm by replacing a linear search with a hash-based search**. Let's use an algorithm problem to deepen the understanding.
|
||||
In algorithm problems, **we often reduce the time complexity of algorithms by replacing linear search with hash-based search**. Let's use an algorithm problem to deepen our understanding.
|
||||
|
||||
!!! question
|
||||
|
||||
Given an integer array `nums` and a target element `target`, please search for two elements in the array whose "sum" equals `target`, and return their array indices. Any solution is acceptable.
|
||||
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.
|
||||
|
||||
## 10.4.1 Linear search: trading time for space
|
||||
## 10.4.1 Linear Search: Trading Time for Space
|
||||
|
||||
Consider traversing through all possible combinations directly. As shown in Figure 10-9, we initiate a nested loop, and in each iteration, we determine whether the sum of the two integers equals `target`. If so, we return their indices.
|
||||
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.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 10-9 Linear search solution for two-sum problem </p>
|
||||
<p align="center"> Figure 10-9 Linear search solution for two sum </p>
|
||||
|
||||
The code is shown below:
|
||||
|
||||
@@ -24,8 +24,8 @@ The code is shown below:
|
||||
|
||||
```python title="two_sum.py"
|
||||
def two_sum_brute_force(nums: list[int], target: int) -> list[int]:
|
||||
"""Method one: Brute force enumeration"""
|
||||
# Two-layer loop, time complexity is O(n^2)
|
||||
"""Method 1: Brute force enumeration"""
|
||||
# Two nested loops, time complexity is O(n^2)
|
||||
for i in range(len(nums) - 1):
|
||||
for j in range(i + 1, len(nums)):
|
||||
if nums[i] + nums[j] == target:
|
||||
@@ -36,10 +36,10 @@ The code is shown below:
|
||||
=== "C++"
|
||||
|
||||
```cpp title="two_sum.cpp"
|
||||
/* Method one: Brute force enumeration */
|
||||
/* Method 1: Brute force enumeration */
|
||||
vector<int> twoSumBruteForce(vector<int> &nums, int target) {
|
||||
int size = nums.size();
|
||||
// Two-layer loop, time complexity is O(n^2)
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for (int i = 0; i < size - 1; i++) {
|
||||
for (int j = i + 1; j < size; j++) {
|
||||
if (nums[i] + nums[j] == target)
|
||||
@@ -53,10 +53,10 @@ The code is shown below:
|
||||
=== "Java"
|
||||
|
||||
```java title="two_sum.java"
|
||||
/* Method one: Brute force enumeration */
|
||||
/* Method 1: Brute force enumeration */
|
||||
int[] twoSumBruteForce(int[] nums, int target) {
|
||||
int size = nums.length;
|
||||
// Two-layer loop, time complexity is O(n^2)
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for (int i = 0; i < size - 1; i++) {
|
||||
for (int j = i + 1; j < size; j++) {
|
||||
if (nums[i] + nums[j] == target)
|
||||
@@ -70,80 +70,188 @@ The code is shown below:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="two_sum.cs"
|
||||
[class]{two_sum}-[func]{TwoSumBruteForce}
|
||||
/* Method 1: Brute force enumeration */
|
||||
int[] TwoSumBruteForce(int[] nums, int target) {
|
||||
int size = nums.Length;
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for (int i = 0; i < size - 1; i++) {
|
||||
for (int j = i + 1; j < size; j++) {
|
||||
if (nums[i] + nums[j] == target)
|
||||
return [i, j];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="two_sum.go"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
/* Method 1: Brute force enumeration */
|
||||
func twoSumBruteForce(nums []int, target int) []int {
|
||||
size := len(nums)
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for i := 0; i < size-1; i++ {
|
||||
for j := i + 1; j < size; j++ {
|
||||
if nums[i]+nums[j] == target {
|
||||
return []int{i, j}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="two_sum.swift"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
/* Method 1: Brute force enumeration */
|
||||
func twoSumBruteForce(nums: [Int], target: Int) -> [Int] {
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for i in nums.indices.dropLast() {
|
||||
for j in nums.indices.dropFirst(i + 1) {
|
||||
if nums[i] + nums[j] == target {
|
||||
return [i, j]
|
||||
}
|
||||
}
|
||||
}
|
||||
return [0]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="two_sum.js"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
/* Method 1: Brute force enumeration */
|
||||
function twoSumBruteForce(nums, target) {
|
||||
const n = nums.length;
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
if (nums[i] + nums[j] === target) {
|
||||
return [i, j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="two_sum.ts"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
/* Method 1: Brute force enumeration */
|
||||
function twoSumBruteForce(nums: number[], target: number): number[] {
|
||||
const n = nums.length;
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for (let i = 0; i < n; i++) {
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
if (nums[i] + nums[j] === target) {
|
||||
return [i, j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="two_sum.dart"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
/* Method 1: Brute force enumeration */
|
||||
List<int> twoSumBruteForce(List<int> nums, int target) {
|
||||
int size = nums.length;
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for (var i = 0; i < size - 1; i++) {
|
||||
for (var j = i + 1; j < size; j++) {
|
||||
if (nums[i] + nums[j] == target) return [i, j];
|
||||
}
|
||||
}
|
||||
return [0];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="two_sum.rs"
|
||||
[class]{}-[func]{two_sum_brute_force}
|
||||
/* Method 1: Brute force enumeration */
|
||||
pub fn two_sum_brute_force(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
|
||||
let size = nums.len();
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for i in 0..size - 1 {
|
||||
for j in i + 1..size {
|
||||
if nums[i] + nums[j] == target {
|
||||
return Some(vec![i as i32, j as i32]);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="two_sum.c"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
/* Method 1: Brute force enumeration */
|
||||
int *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) {
|
||||
for (int i = 0; i < numsSize; ++i) {
|
||||
for (int j = i + 1; j < numsSize; ++j) {
|
||||
if (nums[i] + nums[j] == target) {
|
||||
int *res = malloc(sizeof(int) * 2);
|
||||
res[0] = i, res[1] = j;
|
||||
*returnSize = 2;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
*returnSize = 0;
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="two_sum.kt"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
/* Method 1: Brute force enumeration */
|
||||
fun twoSumBruteForce(nums: IntArray, target: Int): IntArray {
|
||||
val size = nums.size
|
||||
// Two nested loops, time complexity is O(n^2)
|
||||
for (i in 0..<size - 1) {
|
||||
for (j in i + 1..<size) {
|
||||
if (nums[i] + nums[j] == target) return intArrayOf(i, j)
|
||||
}
|
||||
}
|
||||
return IntArray(0)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="two_sum.rb"
|
||||
[class]{}-[func]{two_sum_brute_force}
|
||||
### Method 1: Brute force enumeration ###
|
||||
def two_sum_brute_force(nums, target)
|
||||
# Two nested loops, time complexity is O(n^2)
|
||||
for i in 0...(nums.length - 1)
|
||||
for j in (i + 1)...nums.length
|
||||
return [i, j] if nums[i] + nums[j] == target
|
||||
end
|
||||
end
|
||||
|
||||
[]
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
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.
|
||||
|
||||
```zig title="two_sum.zig"
|
||||
[class]{}-[func]{twoSumBruteForce}
|
||||
```
|
||||
## 10.4.2 Hash-Based Search: Trading Space for Time
|
||||
|
||||
This method has a time complexity of $O(n^2)$ and a space complexity of $O(1)$, which can be very time-consuming with large data volumes.
|
||||
|
||||
## 10.4.2 Hash search: trading space for time
|
||||
|
||||
Consider using a hash table, where the key-value pairs are the array elements and their indices, respectively. Loop through the array, performing the steps shown in Figure 10-10 during each iteration.
|
||||
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:
|
||||
|
||||
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.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -151,7 +259,7 @@ Consider using a hash table, where the key-value pairs are the array elements an
|
||||
=== "<3>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 10-10 Help hash table solve two-sum </p>
|
||||
<p align="center"> Figure 10-10 Hash table solution for two sum </p>
|
||||
|
||||
The implementation code is shown below, requiring only a single loop:
|
||||
|
||||
@@ -159,10 +267,10 @@ The implementation code is shown below, requiring only a single loop:
|
||||
|
||||
```python title="two_sum.py"
|
||||
def two_sum_hash_table(nums: list[int], target: int) -> list[int]:
|
||||
"""Method two: Auxiliary hash table"""
|
||||
"""Method 2: Auxiliary hash table"""
|
||||
# Auxiliary hash table, space complexity is O(n)
|
||||
dic = {}
|
||||
# Single-layer loop, time complexity is O(n)
|
||||
# Single loop, time complexity is O(n)
|
||||
for i in range(len(nums)):
|
||||
if target - nums[i] in dic:
|
||||
return [dic[target - nums[i]], i]
|
||||
@@ -173,12 +281,12 @@ The implementation code is shown below, requiring only a single loop:
|
||||
=== "C++"
|
||||
|
||||
```cpp title="two_sum.cpp"
|
||||
/* Method two: Auxiliary hash table */
|
||||
/* Method 2: Auxiliary hash table */
|
||||
vector<int> twoSumHashTable(vector<int> &nums, int target) {
|
||||
int size = nums.size();
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
unordered_map<int, int> dic;
|
||||
// Single-layer loop, time complexity is O(n)
|
||||
// Single loop, time complexity is O(n)
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (dic.find(target - nums[i]) != dic.end()) {
|
||||
return {dic[target - nums[i]], i};
|
||||
@@ -192,12 +300,12 @@ The implementation code is shown below, requiring only a single loop:
|
||||
=== "Java"
|
||||
|
||||
```java title="two_sum.java"
|
||||
/* Method two: Auxiliary hash table */
|
||||
/* Method 2: Auxiliary hash table */
|
||||
int[] twoSumHashTable(int[] nums, int target) {
|
||||
int size = nums.length;
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
Map<Integer, Integer> dic = new HashMap<>();
|
||||
// Single-layer loop, time complexity is O(n)
|
||||
// Single loop, time complexity is O(n)
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (dic.containsKey(target - nums[i])) {
|
||||
return new int[] { dic.get(target - nums[i]), i };
|
||||
@@ -211,71 +319,218 @@ The implementation code is shown below, requiring only a single loop:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="two_sum.cs"
|
||||
[class]{two_sum}-[func]{TwoSumHashTable}
|
||||
/* Method 2: Auxiliary hash table */
|
||||
int[] TwoSumHashTable(int[] nums, int target) {
|
||||
int size = nums.Length;
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
Dictionary<int, int> dic = [];
|
||||
// Single loop, time complexity is O(n)
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (dic.ContainsKey(target - nums[i])) {
|
||||
return [dic[target - nums[i]], i];
|
||||
}
|
||||
dic.Add(nums[i], i);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="two_sum.go"
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
/* Method 2: Auxiliary hash table */
|
||||
func twoSumHashTable(nums []int, target int) []int {
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
hashTable := map[int]int{}
|
||||
// Single loop, time complexity is O(n)
|
||||
for idx, val := range nums {
|
||||
if preIdx, ok := hashTable[target-val]; ok {
|
||||
return []int{preIdx, idx}
|
||||
}
|
||||
hashTable[val] = idx
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="two_sum.swift"
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
/* Method 2: Auxiliary hash table */
|
||||
func twoSumHashTable(nums: [Int], target: Int) -> [Int] {
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
var dic: [Int: Int] = [:]
|
||||
// Single loop, time complexity is O(n)
|
||||
for i in nums.indices {
|
||||
if let j = dic[target - nums[i]] {
|
||||
return [j, i]
|
||||
}
|
||||
dic[nums[i]] = i
|
||||
}
|
||||
return [0]
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="two_sum.js"
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
/* Method 2: Auxiliary hash table */
|
||||
function twoSumHashTable(nums, target) {
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
let m = {};
|
||||
// Single loop, time complexity is O(n)
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
if (m[target - nums[i]] !== undefined) {
|
||||
return [m[target - nums[i]], i];
|
||||
} else {
|
||||
m[nums[i]] = i;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="two_sum.ts"
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
/* Method 2: Auxiliary hash table */
|
||||
function twoSumHashTable(nums: number[], target: number): number[] {
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
let m: Map<number, number> = new Map();
|
||||
// Single loop, time complexity is O(n)
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
let index = m.get(target - nums[i]);
|
||||
if (index !== undefined) {
|
||||
return [index, i];
|
||||
} else {
|
||||
m.set(nums[i], i);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="two_sum.dart"
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
/* Method 2: Auxiliary hash table */
|
||||
List<int> twoSumHashTable(List<int> nums, int target) {
|
||||
int size = nums.length;
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
Map<int, int> dic = HashMap();
|
||||
// Single loop, time complexity is O(n)
|
||||
for (var i = 0; i < size; i++) {
|
||||
if (dic.containsKey(target - nums[i])) {
|
||||
return [dic[target - nums[i]]!, i];
|
||||
}
|
||||
dic.putIfAbsent(nums[i], () => i);
|
||||
}
|
||||
return [0];
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="two_sum.rs"
|
||||
[class]{}-[func]{two_sum_hash_table}
|
||||
/* Method 2: Auxiliary hash table */
|
||||
pub fn two_sum_hash_table(nums: &Vec<i32>, target: i32) -> Option<Vec<i32>> {
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
let mut dic = HashMap::new();
|
||||
// Single loop, time complexity is O(n)
|
||||
for (i, num) in nums.iter().enumerate() {
|
||||
match dic.get(&(target - num)) {
|
||||
Some(v) => return Some(vec![*v as i32, i as i32]),
|
||||
None => dic.insert(num, i as i32),
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="two_sum.c"
|
||||
[class]{HashTable}-[func]{}
|
||||
/* Hash table */
|
||||
typedef struct {
|
||||
int key;
|
||||
int val;
|
||||
UT_hash_handle hh; // Implemented using uthash.h
|
||||
} HashTable;
|
||||
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
/* Hash table lookup */
|
||||
HashTable *find(HashTable *h, int key) {
|
||||
HashTable *tmp;
|
||||
HASH_FIND_INT(h, &key, tmp);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
/* Hash table element insertion */
|
||||
void insert(HashTable **h, int key, int val) {
|
||||
HashTable *t = find(*h, key);
|
||||
if (t == NULL) {
|
||||
HashTable *tmp = malloc(sizeof(HashTable));
|
||||
tmp->key = key, tmp->val = val;
|
||||
HASH_ADD_INT(*h, key, tmp);
|
||||
} else {
|
||||
t->val = val;
|
||||
}
|
||||
}
|
||||
|
||||
/* Method 2: Auxiliary hash table */
|
||||
int *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) {
|
||||
HashTable *hashtable = NULL;
|
||||
for (int i = 0; i < numsSize; i++) {
|
||||
HashTable *t = find(hashtable, target - nums[i]);
|
||||
if (t != NULL) {
|
||||
int *res = malloc(sizeof(int) * 2);
|
||||
res[0] = t->val, res[1] = i;
|
||||
*returnSize = 2;
|
||||
return res;
|
||||
}
|
||||
insert(&hashtable, nums[i], i);
|
||||
}
|
||||
*returnSize = 0;
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="two_sum.kt"
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
/* Method 2: Auxiliary hash table */
|
||||
fun twoSumHashTable(nums: IntArray, target: Int): IntArray {
|
||||
val size = nums.size
|
||||
// Auxiliary hash table, space complexity is O(n)
|
||||
val dic = HashMap<Int, Int>()
|
||||
// Single loop, time complexity is O(n)
|
||||
for (i in 0..<size) {
|
||||
if (dic.containsKey(target - nums[i])) {
|
||||
return intArrayOf(dic[target - nums[i]]!!, i)
|
||||
}
|
||||
dic[nums[i]] = i
|
||||
}
|
||||
return IntArray(0)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="two_sum.rb"
|
||||
[class]{}-[func]{two_sum_hash_table}
|
||||
### Method 2: Auxiliary hash table ###
|
||||
def two_sum_hash_table(nums, target)
|
||||
# Auxiliary hash table, space complexity is O(n)
|
||||
dic = {}
|
||||
# Single loop, time complexity is O(n)
|
||||
for i in 0...nums.length
|
||||
return [dic[target - nums[i]], i] if dic.has_key?(target - nums[i])
|
||||
|
||||
dic[nums[i]] = i
|
||||
end
|
||||
|
||||
[]
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
This method reduces the time complexity from $O(n^2)$ to $O(n)$ through hash-based search, greatly improving runtime efficiency.
|
||||
|
||||
```zig title="two_sum.zig"
|
||||
[class]{}-[func]{twoSumHashTable}
|
||||
```
|
||||
|
||||
This method reduces the time complexity from $O(n^2)$ to $O(n)$ by using hash search, significantly enhancing runtime efficiency.
|
||||
|
||||
As it requires maintaining an additional hash table, the space complexity is $O(n)$. **Nevertheless, this method has a more balanced time-space efficiency overall, 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 achieves a more balanced overall time-space efficiency, making it the optimal solution for this problem**.
|
||||
|
||||
@@ -2,93 +2,93 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 10.5 Search algorithms revisited
|
||||
# 10.5 Searching Algorithms Revisited
|
||||
|
||||
<u>Searching algorithms (search algorithms)</u> are used to retrieve one or more elements that meet specific criteria within data structures such as arrays, linked lists, trees, or graphs.
|
||||
<u>Searching algorithms</u> are used to search for one or a group of elements that meet specific conditions in data structures (such as arrays, linked lists, trees, or graphs).
|
||||
|
||||
Searching algorithms can be divided into the following two categories based on their approach.
|
||||
Searching algorithms can be divided into the following two categories based on their implementation approach:
|
||||
|
||||
- **Locating the target element by traversing the data structure**, such as traversals of arrays, linked lists, trees, and graphs, etc.
|
||||
- **Using the organizational structure of the data or existing data to achieve efficient element searches**, such as binary search, hash search, binary search tree search, etc.
|
||||
- **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.
|
||||
|
||||
These topics were introduced in previous chapters, so they are not unfamiliar to us. In this section, we will revisit searching algorithms from a more systematic perspective.
|
||||
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.
|
||||
|
||||
## 10.5.1 Brute-force search
|
||||
## 10.5.1 Brute-Force Search
|
||||
|
||||
A Brute-force search locates the target element by traversing every element of the data structure.
|
||||
Brute-force search locates target elements by traversing each element of the data structure.
|
||||
|
||||
- "Linear search" is suitable for linear data structures such as arrays and linked lists. It starts from one end of the data structure and accesses each element one by one until the target element is found or the other end is reached without finding the target element.
|
||||
- "Breadth-first search" and "Depth-first search" are two traversal strategies for graphs and trees. Breadth-first search starts from the initial node and searches layer by layer (left to right), accessing nodes from near to far. Depth-first search starts from the initial node, follows a path until the end (top to bottom), then backtracks and tries other paths until the entire data structure is traversed.
|
||||
- "Linear search" is applicable to linear data structures such as arrays and linked lists. It starts from one end of the data structure and accesses elements one by one until the target element is found or the other end is reached without finding the target element.
|
||||
- "Breadth-first search" and "depth-first search" are two traversal strategies for graphs and trees. Breadth-first search starts from the initial node and searches layer by layer, visiting nodes from near to far. Depth-first search starts from the initial node, follows a path to the end, then backtracks and tries other paths until the entire data structure is traversed.
|
||||
|
||||
The advantage of brute-force search is its simplicity and versatility, **no need for data preprocessing or the help of additional data structures**.
|
||||
The advantage of brute-force search is that it is simple and has good generality, **requiring no data preprocessing or additional data structures**.
|
||||
|
||||
However, **the time complexity of this type of algorithm is $O(n)$**, where $n$ is the number of elements, so the performance is poor with large data sets.
|
||||
However, **the time complexity of such algorithms is $O(n)$**, where $n$ is the number of elements, so performance is poor when dealing with large amounts of data.
|
||||
|
||||
## 10.5.2 Adaptive search
|
||||
## 10.5.2 Adaptive Search
|
||||
|
||||
An Adaptive search uses the unique properties of data (such as order) to optimize the search process, thereby locating the target element more efficiently.
|
||||
Adaptive search utilizes the unique properties of data (such as orderliness) to optimize the search process, thereby locating target elements more efficiently.
|
||||
|
||||
- "Binary search" uses the orderliness of data to achieve efficient searching, only suitable for arrays.
|
||||
- "Hash search" uses a hash table to establish a key-value mapping between search data and target data, thus implementing the query operation.
|
||||
- "Tree search" in a specific tree structure (such as a binary search tree), quickly eliminates nodes based on node value comparisons, thus locating the target element.
|
||||
- "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.
|
||||
|
||||
The advantage of these algorithms is high efficiency, **with time complexities reaching $O(\log n)$ or even $O(1)$**.
|
||||
The advantage of such algorithms is high efficiency, **with time complexity reaching $O(\log n)$ or even $O(1)$**.
|
||||
|
||||
However, **using these algorithms often requires data preprocessing**. For example, binary search requires sorting the array in advance, and hash search and tree search both require the help of additional data structures. Maintaining these structures also requires more overhead in terms of time and space.
|
||||
However, **using these algorithms often requires data preprocessing**. For example, binary search requires pre-sorting the array, while hash-based search and tree search both require additional data structures, and maintaining these data structures also requires extra time and space overhead.
|
||||
|
||||
!!! tip
|
||||
|
||||
Adaptive search algorithms are often referred to as search algorithms, **mainly used for quickly retrieving target elements in specific data structures**.
|
||||
Adaptive search algorithms are often called lookup algorithms, **mainly used to quickly retrieve target elements in specific data structures**.
|
||||
|
||||
## 10.5.3 Choosing a search method
|
||||
## 10.5.3 Search Method Selection
|
||||
|
||||
Given a set of data of size $n$, we can use a linear search, binary search, tree search, hash search, or other methods to retrieve the target element. The working principles of these methods are shown in Figure 10-11.
|
||||
Given a dataset of size $n$, we can use linear search, binary search, tree search, hash-based search, and other methods to search for the target element. The working principles of each method are shown in Figure 10-11.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 10-11 Various search strategies </p>
|
||||
<p align="center"> Figure 10-11 Multiple search strategies </p>
|
||||
|
||||
The characteristics and operational efficiency of the aforementioned methods are shown in the following table.
|
||||
The operational efficiency and characteristics of the above methods are as follows:
|
||||
|
||||
<p align="center"> Table 10-1 Comparison of search algorithm efficiency </p>
|
||||
|
||||
<div class="center-table" markdown>
|
||||
|
||||
| | Linear search | Binary search | Tree search | Hash search |
|
||||
| | Linear search | Binary search | Tree search | Hash-based search |
|
||||
| ------------------ | ------------- | --------------------- | --------------------------- | -------------------------- |
|
||||
| Search element | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| Insert element | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| Delete element | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ |
|
||||
| Extra space | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ |
|
||||
| Data preprocessing | / | Sorting $O(n \log n)$ | Building tree $O(n \log n)$ | Building hash table $O(n)$ |
|
||||
| Data orderliness | Unordered | Ordered | Ordered | Unordered |
|
||||
| Data preprocessing | / | Sorting $O(n \log n)$ | Tree building $O(n \log n)$ | Hash table building $O(n)$ |
|
||||
| Data ordered | Unordered | Ordered | Ordered | Unordered |
|
||||
|
||||
</div>
|
||||
|
||||
The choice of search algorithm also depends on the volume of data, search performance requirements, frequency of data queries and updates, etc.
|
||||
The choice of search algorithm also depends on data volume, search performance requirements, data query and update frequency, etc.
|
||||
|
||||
**Linear search**
|
||||
|
||||
- Good versatility, no need for any data preprocessing operations. If we only need to query the data once, then the time for data preprocessing in the other three methods would be longer than the time for a linear search.
|
||||
- Suitable for small volumes of data, where time complexity has a smaller impact on efficiency.
|
||||
- Suitable for scenarios with very frequent data updates, because this method does not require any additional maintenance of the data.
|
||||
- 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.
|
||||
- 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 larger data volumes, with stable performance and a worst-case time complexity of $O(\log n)$.
|
||||
- However, the data volume cannot be too large, because storing arrays requires contiguous memory space.
|
||||
- Not suitable for scenarios with frequent additions and deletions, because maintaining an ordered array incurs a lot of overhead.
|
||||
- Suitable for large data volumes with stable efficiency performance, 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 search**
|
||||
**Hash-based search**
|
||||
|
||||
- Suitable for scenarios where fast query performance is essential, with an average time complexity of $O(1)$.
|
||||
- Not suitable for scenarios needing ordered data or range searches, because hash tables cannot maintain data orderliness.
|
||||
- High dependency on hash functions and hash collision handling strategies, with significant performance degradation risks.
|
||||
- Not suitable for overly large data volumes, because hash tables need extra space to minimize collisions and provide good query performance.
|
||||
- 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.
|
||||
- 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, because tree nodes are stored scattered in memory.
|
||||
- Suitable for maintaining ordered data or range searches.
|
||||
- With the continuous addition and deletion of nodes, the binary search tree may become skewed, degrading the time complexity to $O(n)$.
|
||||
- If using AVL trees or red-black trees, operations can run stably at $O(\log n)$ efficiency, but the operation to maintain tree balance adds extra overhead.
|
||||
- Suitable for massive data, as tree nodes are stored dispersedly in memory.
|
||||
- Suitable for scenarios requiring maintained ordered data or 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.
|
||||
|
||||
@@ -4,9 +4,11 @@ comments: true
|
||||
|
||||
# 10.6 Summary
|
||||
|
||||
- Binary search depends on the order of data and performs the search by iteratively halving the search interval. It requires the input data to be sorted and is only applicable to arrays or array-based data structures.
|
||||
- Brute force search may be required to locate an entry in an unordered dataset. Different search algorithms can be applied based on the data structure: Linear search is suitable for arrays and linked lists, while breadth-first search (BFS) and depth-first search (DFS) are suitable for graphs and trees. These algorithms are highly versatile, requiring no preprocessing of data, but they have a higher time complexity of $O(n)$.
|
||||
- Hash search, tree search, and binary search are efficient search methods that can quickly locate target elements within specific data structures. These algorithms are highly efficient, with time complexities reaching $O(\log n)$ or even $O(1)$, but they usually require extra space to accommodate additional data structures.
|
||||
- In practice, we need to analyze factors such as data volume, search performance requirements, data query and update frequencies, etc., to choose an appropriate search method.
|
||||
- Linear search is ideal for small or frequently updated (volatile) data. Binary search works well for large and sorted data. Hash search is suitable for data that requires high query efficiency and does not need range queries. Tree search is best suited for large dynamic data that require maintaining order and need to support range queries.
|
||||
- Replacing linear search with hash search is a common strategy to optimize runtime performance, reducing the time complexity from $O(n)$ to $O(1)$.
|
||||
### 1. 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)$.
|
||||
- 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.
|
||||
- Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from $O(n)$ to $O(1)$.
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.3 Bubble sort
|
||||
# 11.3 Bubble Sort
|
||||
|
||||
<u>Bubble sort</u> works by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name "bubble sort."
|
||||
<u>Bubble sort (bubble sort)</u> achieves sorting by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name bubble sort.
|
||||
|
||||
As shown in Figure 11-4, the bubbling process can be simulated using element swaps: start from the leftmost end of the array and move right, comparing each pair of adjacent elements. If the left element is greater than the right element, swap them. After the traversal, the largest element will have bubbled up to the rightmost end of the array.
|
||||
As shown in Figure 11-4, the bubbling process can be simulated using element swap operations: starting from the leftmost end of the array and traversing to the right, compare the size of adjacent elements, and if "left element > right element", swap them. After completing the traversal, the largest element will be moved to the rightmost end of the array.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -29,20 +29,20 @@ As shown in Figure 11-4, the bubbling process can be simulated using element swa
|
||||
=== "<7>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-4 Simulating bubble process using element swap </p>
|
||||
<p align="center"> Figure 11-4 Simulating bubble using element swap operation </p>
|
||||
|
||||
## 11.3.1 Algorithm process
|
||||
## 11.3.1 Algorithm Flow
|
||||
|
||||
Assume the array has length $n$. The steps of bubble sort are shown in Figure 11-5:
|
||||
Assume the array has length $n$. The steps of bubble sort are shown in Figure 11-5.
|
||||
|
||||
1. First, perform one "bubble" pass on $n$ elements, **swapping the largest element to its correct position**.
|
||||
2. Next, perform a "bubble" pass on the remaining $n - 1$ elements, **swapping the second largest element to its correct position**.
|
||||
3. Continue in this manner; after $n - 1$ such passes, **the largest $n - 1$ elements will have been moved to their correct positions**.
|
||||
4. The only remaining element **must** be the smallest, so **no** further sorting is required. At this point, the array is sorted.
|
||||
1. First, perform "bubbling" on $n$ elements, **swapping the largest element of the array to its correct position**.
|
||||
2. Next, perform "bubbling" on the remaining $n - 1$ elements, **swapping the second largest element to its correct position**.
|
||||
3. And so on. After $n - 1$ rounds of "bubbling", **the largest $n - 1$ elements have all been swapped to their correct positions**.
|
||||
4. The only remaining element must be the smallest element, requiring no sorting, so the array sorting is complete.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-5 Bubble sort process </p>
|
||||
<p align="center"> Figure 11-5 Bubble sort flow </p>
|
||||
|
||||
Example code is as follows:
|
||||
|
||||
@@ -52,9 +52,9 @@ Example code is as follows:
|
||||
def bubble_sort(nums: list[int]):
|
||||
"""Bubble sort"""
|
||||
n = len(nums)
|
||||
# Outer loop: unsorted range is [0, i]
|
||||
# Outer loop: unsorted interval is [0, i]
|
||||
for i in range(n - 1, 0, -1):
|
||||
# Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
|
||||
# Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval
|
||||
for j in range(i):
|
||||
if nums[j] > nums[j + 1]:
|
||||
# Swap nums[j] and nums[j + 1]
|
||||
@@ -68,11 +68,11 @@ Example code is as follows:
|
||||
void bubbleSort(vector<int> &nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = nums.size() - 1; i > 0; i--) {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
// Here, the std
|
||||
// Using std::swap() function here
|
||||
swap(nums[j], nums[j + 1]);
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ Example code is as follows:
|
||||
void bubbleSort(int[] nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
@@ -103,113 +103,237 @@ Example code is as follows:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="bubble_sort.cs"
|
||||
[class]{bubble_sort}-[func]{BubbleSort}
|
||||
/* Bubble sort */
|
||||
void BubbleSort(int[] nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = nums.Length - 1; i > 0; i--) {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="bubble_sort.go"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
/* Bubble sort */
|
||||
func bubbleSort(nums []int) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for i := len(nums) - 1; i > 0; i-- {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for j := 0; j < i; j++ {
|
||||
if nums[j] > nums[j+1] {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
nums[j], nums[j+1] = nums[j+1], nums[j]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="bubble_sort.swift"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
/* Bubble sort */
|
||||
func bubbleSort(nums: inout [Int]) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for i in nums.indices.dropFirst().reversed() {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for j in 0 ..< i {
|
||||
if nums[j] > nums[j + 1] {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
nums.swapAt(j, j + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="bubble_sort.js"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
/* Bubble sort */
|
||||
function bubbleSort(nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
let tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="bubble_sort.ts"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
/* Bubble sort */
|
||||
function bubbleSort(nums: number[]): void {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
let tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="bubble_sort.dart"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
/* Bubble sort */
|
||||
void bubbleSort(List<int> nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
int tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="bubble_sort.rs"
|
||||
[class]{}-[func]{bubble_sort}
|
||||
/* Bubble sort */
|
||||
fn bubble_sort(nums: &mut [i32]) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for i in (1..nums.len()).rev() {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for j in 0..i {
|
||||
if nums[j] > nums[j + 1] {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
nums.swap(j, j + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="bubble_sort.c"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
/* Bubble sort */
|
||||
void bubbleSort(int nums[], int size) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = size - 1; i > 0; i--) {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
int temp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="bubble_sort.kt"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
/* Bubble sort */
|
||||
fun bubbleSort(nums: IntArray) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (i in nums.size - 1 downTo 1) {
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (j in 0..<i) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
val temp = nums[j]
|
||||
nums[j] = nums[j + 1]
|
||||
nums[j + 1] = temp
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="bubble_sort.rb"
|
||||
[class]{}-[func]{bubble_sort}
|
||||
### Bubble sort ###
|
||||
def bubble_sort(nums)
|
||||
n = nums.length
|
||||
# Outer loop: unsorted range is [0, i]
|
||||
for i in (n - 1).downto(1)
|
||||
# Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for j in 0...i
|
||||
if nums[j] > nums[j + 1]
|
||||
# Swap nums[j] and nums[j + 1]
|
||||
nums[j], nums[j + 1] = nums[j + 1], nums[j]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 11.3.2 Efficiency Optimization
|
||||
|
||||
```zig title="bubble_sort.zig"
|
||||
[class]{}-[func]{bubbleSort}
|
||||
```
|
||||
We notice that if no swap operations are performed during a certain round of "bubbling", it means the array has already completed sorting and can directly return the result. Therefore, we can add a flag `flag` to monitor this situation and return immediately once it occurs.
|
||||
|
||||
## 11.3.2 Efficiency optimization
|
||||
|
||||
If no swaps occur during a round of "bubbling," the array is already sorted, so we can return immediately. To detect this, we can add a `flag` variable; whenever no swaps are made in a pass, we set the flag and return early.
|
||||
|
||||
Even with this optimization, the worst time complexity and average time complexity of bubble sort remains $O(n^2)$. However, if the input array is already sorted, the best-case time complexity can be as low as $O(n)$.
|
||||
After optimization, the worst-case time complexity and average time complexity of bubble sort remain $O(n^2)$; but when the input array is completely ordered, the best-case time complexity can reach $O(n)$.
|
||||
|
||||
=== "Python"
|
||||
|
||||
```python title="bubble_sort.py"
|
||||
def bubble_sort_with_flag(nums: list[int]):
|
||||
"""Bubble sort (optimized with flag)"""
|
||||
"""Bubble sort (flag optimization)"""
|
||||
n = len(nums)
|
||||
# Outer loop: unsorted range is [0, i]
|
||||
# Outer loop: unsorted interval is [0, i]
|
||||
for i in range(n - 1, 0, -1):
|
||||
flag = False # Initialize flag
|
||||
# Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
|
||||
# Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval
|
||||
for j in range(i):
|
||||
if nums[j] > nums[j + 1]:
|
||||
# Swap nums[j] and nums[j + 1]
|
||||
nums[j], nums[j + 1] = nums[j + 1], nums[j]
|
||||
flag = True # Record swapped elements
|
||||
flag = True # Record element swap
|
||||
if not flag:
|
||||
break # If no elements were swapped in this round of "bubbling", exit
|
||||
break # No elements were swapped in this round of "bubbling", exit directly
|
||||
```
|
||||
|
||||
=== "C++"
|
||||
|
||||
```cpp title="bubble_sort.cpp"
|
||||
/* Bubble sort (optimized with flag)*/
|
||||
/* Bubble sort (flag optimization)*/
|
||||
void bubbleSortWithFlag(vector<int> &nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = nums.size() - 1; i > 0; i--) {
|
||||
bool flag = false; // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
// Here, the std
|
||||
// Using std::swap() function here
|
||||
swap(nums[j], nums[j + 1]);
|
||||
flag = true; // Record swapped elements
|
||||
flag = true; // Record element swap
|
||||
}
|
||||
}
|
||||
if (!flag)
|
||||
break; // If no elements were swapped in this round of "bubbling", exit
|
||||
break; // No elements were swapped in this round of "bubbling", exit directly
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -217,23 +341,23 @@ Even with this optimization, the worst time complexity and average time complexi
|
||||
=== "Java"
|
||||
|
||||
```java title="bubble_sort.java"
|
||||
/* Bubble sort (optimized with flag) */
|
||||
/* Bubble sort (flag optimization) */
|
||||
void bubbleSortWithFlag(int[] nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
boolean flag = false; // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
int tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
flag = true; // Record swapped elements
|
||||
flag = true; // Record element swap
|
||||
}
|
||||
}
|
||||
if (!flag)
|
||||
break; // If no elements were swapped in this round of "bubbling", exit
|
||||
break; // No elements were swapped in this round of "bubbling", exit directly
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -241,71 +365,233 @@ Even with this optimization, the worst time complexity and average time complexi
|
||||
=== "C#"
|
||||
|
||||
```csharp title="bubble_sort.cs"
|
||||
[class]{bubble_sort}-[func]{BubbleSortWithFlag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
void BubbleSortWithFlag(int[] nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = nums.Length - 1; i > 0; i--) {
|
||||
bool flag = false; // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
(nums[j + 1], nums[j]) = (nums[j], nums[j + 1]);
|
||||
flag = true; // Record element swap
|
||||
}
|
||||
}
|
||||
if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="bubble_sort.go"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
func bubbleSortWithFlag(nums []int) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for i := len(nums) - 1; i > 0; i-- {
|
||||
flag := false // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for j := 0; j < i; j++ {
|
||||
if nums[j] > nums[j+1] {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
nums[j], nums[j+1] = nums[j+1], nums[j]
|
||||
flag = true // Record element swap
|
||||
}
|
||||
}
|
||||
if flag == false { // No elements were swapped in this round of "bubbling", exit directly
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="bubble_sort.swift"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
func bubbleSortWithFlag(nums: inout [Int]) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for i in nums.indices.dropFirst().reversed() {
|
||||
var flag = false // Initialize flag
|
||||
for j in 0 ..< i {
|
||||
if nums[j] > nums[j + 1] {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
nums.swapAt(j, j + 1)
|
||||
flag = true // Record element swap
|
||||
}
|
||||
}
|
||||
if !flag { // No elements were swapped in this round of "bubbling", exit directly
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="bubble_sort.js"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
function bubbleSortWithFlag(nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
let flag = false; // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
let tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
flag = true; // Record element swap
|
||||
}
|
||||
}
|
||||
if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="bubble_sort.ts"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
function bubbleSortWithFlag(nums: number[]): void {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
let flag = false; // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
let tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
flag = true; // Record element swap
|
||||
}
|
||||
}
|
||||
if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="bubble_sort.dart"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
void bubbleSortWithFlag(List<int> nums) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
bool flag = false; // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
int tmp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = tmp;
|
||||
flag = true; // Record element swap
|
||||
}
|
||||
}
|
||||
if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="bubble_sort.rs"
|
||||
[class]{}-[func]{bubble_sort_with_flag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
fn bubble_sort_with_flag(nums: &mut [i32]) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for i in (1..nums.len()).rev() {
|
||||
let mut flag = false; // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for j in 0..i {
|
||||
if nums[j] > nums[j + 1] {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
nums.swap(j, j + 1);
|
||||
flag = true; // Record element swap
|
||||
}
|
||||
}
|
||||
if !flag {
|
||||
break; // No elements were swapped in this round of "bubbling", exit directly
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="bubble_sort.c"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
void bubbleSortWithFlag(int nums[], int size) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (int i = size - 1; i > 0; i--) {
|
||||
bool flag = false;
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
int temp = nums[j];
|
||||
nums[j] = nums[j + 1];
|
||||
nums[j + 1] = temp;
|
||||
flag = true;
|
||||
}
|
||||
}
|
||||
if (!flag)
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="bubble_sort.kt"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
/* Bubble sort (flag optimization) */
|
||||
fun bubbleSortWithFlag(nums: IntArray) {
|
||||
// Outer loop: unsorted range is [0, i]
|
||||
for (i in nums.size - 1 downTo 1) {
|
||||
var flag = false // Initialize flag
|
||||
// Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for (j in 0..<i) {
|
||||
if (nums[j] > nums[j + 1]) {
|
||||
// Swap nums[j] and nums[j + 1]
|
||||
val temp = nums[j]
|
||||
nums[j] = nums[j + 1]
|
||||
nums[j + 1] = temp
|
||||
flag = true // Record element swap
|
||||
}
|
||||
}
|
||||
if (!flag) break // No elements were swapped in this round of "bubbling", exit directly
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="bubble_sort.rb"
|
||||
[class]{}-[func]{bubble_sort_with_flag}
|
||||
### Bubble sort (flag optimization) ###
|
||||
def bubble_sort_with_flag(nums)
|
||||
n = nums.length
|
||||
# Outer loop: unsorted range is [0, i]
|
||||
for i in (n - 1).downto(1)
|
||||
flag = false # Initialize flag
|
||||
|
||||
# Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range
|
||||
for j in 0...i
|
||||
if nums[j] > nums[j + 1]
|
||||
# Swap nums[j] and nums[j + 1]
|
||||
nums[j], nums[j + 1] = nums[j + 1], nums[j]
|
||||
flag = true # Record element swap
|
||||
end
|
||||
end
|
||||
|
||||
break unless flag # No elements were swapped in this round of "bubbling", exit directly
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 11.3.3 Algorithm Characteristics
|
||||
|
||||
```zig title="bubble_sort.zig"
|
||||
[class]{}-[func]{bubbleSortWithFlag}
|
||||
```
|
||||
|
||||
## 11.3.3 Algorithm characteristics
|
||||
|
||||
- **Time complexity of $O(n^2)$, adaptive sorting.** Each round of "bubbling" traverses array segments of length $n - 1$, $n - 2$, $\dots$, $2$, $1$, which sums to $(n - 1) n / 2$. With a `flag` optimization, the best-case time complexity can reach $O(n)$ when the array is already sorted.
|
||||
- **Space complexity of $O(1)$, in-place sorting.** Only a constant amount of extra space is used by pointers $i$ and $j$.
|
||||
- **Stable sorting.** Because equal elements are not swapped during "bubbling," their original order is preserved, making this a stable sort.
|
||||
- **Time complexity of $O(n^2)$, adaptive sorting**: The array lengths traversed in each round of "bubbling" are $n - 1$, $n - 2$, $\dots$, $2$, $1$, totaling $(n - 1) n / 2$. After introducing the `flag` optimization, the best-case time complexity can reach $O(n)$.
|
||||
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
|
||||
- **Stable sorting**: Since equal elements are not swapped during "bubbling".
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.8 Bucket sort
|
||||
# 11.8 Bucket Sort
|
||||
|
||||
The previously mentioned sorting algorithms are all "comparison-based sorting algorithms," which sort elements by comparing their values. Such sorting algorithms cannot have better time complexity of $O(n \log n)$. Next, we will discuss several "non-comparison sorting algorithms" that could achieve linear time complexity.
|
||||
The several sorting algorithms mentioned earlier all belong to "comparison-based sorting algorithms", which achieve sorting by comparing the size of elements. The time complexity of such sorting algorithms cannot exceed $O(n \log n)$. Next, we will explore several "non-comparison sorting algorithms", whose time complexity can reach linear order.
|
||||
|
||||
<u>Bucket sort</u> is a typical application of the divide-and-conquer strategy. It works by setting up a series of ordered buckets, each containing a range of data, and distributing the input data evenly across these buckets. And then, the data in each bucket is sorted individually. Finally, the sorted data from all the buckets is merged in sequence to produce the final result.
|
||||
<u>Bucket sort (bucket sort)</u> is a typical application of the divide-and-conquer strategy. It works by setting up buckets with size order, each bucket corresponding to a data range, evenly distributing data to each bucket; then, sorting within each bucket separately; finally, merging all data in the order of the buckets.
|
||||
|
||||
## 11.8.1 Algorithm process
|
||||
## 11.8.1 Algorithm Flow
|
||||
|
||||
Consider an array of length $n$, with float numbers in the range $[0, 1)$. The bucket sort process is illustrated in Figure 11-13.
|
||||
Consider an array of length $n$, whose elements are floating-point numbers in the range $[0, 1)$. The flow of bucket sort is shown in Figure 11-13.
|
||||
|
||||
1. Initialize $k$ buckets and distribute $n$ elements into these $k$ buckets.
|
||||
2. Sort each bucket individually (using the built-in sorting function of the programming language).
|
||||
3. Merge the results in the order from the smallest to the largest bucket.
|
||||
1. Initialize $k$ buckets and distribute the $n$ elements into the $k$ buckets.
|
||||
2. Sort each bucket separately (here we use the built-in sorting function of the programming language).
|
||||
3. Merge the results in order from smallest to largest bucket.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-13 Bucket sort algorithm process </p>
|
||||
<p align="center"> Figure 11-13 Bucket sort algorithm flow </p>
|
||||
|
||||
The code is shown as follows:
|
||||
The code is as follows:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -60,7 +60,7 @@ The code is shown as follows:
|
||||
for (float num : nums) {
|
||||
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
int i = num * k;
|
||||
// Add number to bucket_idx
|
||||
// Add num to bucket bucket_idx
|
||||
buckets[i].push_back(num);
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
@@ -114,92 +114,352 @@ The code is shown as follows:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="bucket_sort.cs"
|
||||
[class]{bucket_sort}-[func]{BucketSort}
|
||||
/* Bucket sort */
|
||||
void BucketSort(float[] nums) {
|
||||
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
int k = nums.Length / 2;
|
||||
List<List<float>> buckets = [];
|
||||
for (int i = 0; i < k; i++) {
|
||||
buckets.Add([]);
|
||||
}
|
||||
// 1. Distribute array elements into various buckets
|
||||
foreach (float num in nums) {
|
||||
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
int i = (int)(num * k);
|
||||
// Add num to bucket i
|
||||
buckets[i].Add(num);
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
foreach (List<float> bucket in buckets) {
|
||||
// Use built-in sorting function, can also replace with other sorting algorithms
|
||||
bucket.Sort();
|
||||
}
|
||||
// 3. Traverse buckets to merge results
|
||||
int j = 0;
|
||||
foreach (List<float> bucket in buckets) {
|
||||
foreach (float num in bucket) {
|
||||
nums[j++] = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="bucket_sort.go"
|
||||
[class]{}-[func]{bucketSort}
|
||||
/* Bucket sort */
|
||||
func bucketSort(nums []float64) {
|
||||
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
k := len(nums) / 2
|
||||
buckets := make([][]float64, k)
|
||||
for i := 0; i < k; i++ {
|
||||
buckets[i] = make([]float64, 0)
|
||||
}
|
||||
// 1. Distribute array elements into various buckets
|
||||
for _, num := range nums {
|
||||
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
i := int(num * float64(k))
|
||||
// Add num to bucket i
|
||||
buckets[i] = append(buckets[i], num)
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
for i := 0; i < k; i++ {
|
||||
// Use built-in slice sorting function, can also be replaced with other sorting algorithms
|
||||
sort.Float64s(buckets[i])
|
||||
}
|
||||
// 3. Traverse buckets to merge results
|
||||
i := 0
|
||||
for _, bucket := range buckets {
|
||||
for _, num := range bucket {
|
||||
nums[i] = num
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="bucket_sort.swift"
|
||||
[class]{}-[func]{bucketSort}
|
||||
/* Bucket sort */
|
||||
func bucketSort(nums: inout [Double]) {
|
||||
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
let k = nums.count / 2
|
||||
var buckets = (0 ..< k).map { _ in [Double]() }
|
||||
// 1. Distribute array elements into various buckets
|
||||
for num in nums {
|
||||
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
let i = Int(num * Double(k))
|
||||
// Add num to bucket i
|
||||
buckets[i].append(num)
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
for i in buckets.indices {
|
||||
// Use built-in sorting function, can also replace with other sorting algorithms
|
||||
buckets[i].sort()
|
||||
}
|
||||
// 3. Traverse buckets to merge results
|
||||
var i = nums.startIndex
|
||||
for bucket in buckets {
|
||||
for num in bucket {
|
||||
nums[i] = num
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="bucket_sort.js"
|
||||
[class]{}-[func]{bucketSort}
|
||||
/* Bucket sort */
|
||||
function bucketSort(nums) {
|
||||
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
const k = nums.length / 2;
|
||||
const buckets = [];
|
||||
for (let i = 0; i < k; i++) {
|
||||
buckets.push([]);
|
||||
}
|
||||
// 1. Distribute array elements into various buckets
|
||||
for (const num of nums) {
|
||||
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
const i = Math.floor(num * k);
|
||||
// Add num to bucket i
|
||||
buckets[i].push(num);
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
for (const bucket of buckets) {
|
||||
// Use built-in sorting function, can also replace with other sorting algorithms
|
||||
bucket.sort((a, b) => a - b);
|
||||
}
|
||||
// 3. Traverse buckets to merge results
|
||||
let i = 0;
|
||||
for (const bucket of buckets) {
|
||||
for (const num of bucket) {
|
||||
nums[i++] = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="bucket_sort.ts"
|
||||
[class]{}-[func]{bucketSort}
|
||||
/* Bucket sort */
|
||||
function bucketSort(nums: number[]): void {
|
||||
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
const k = nums.length / 2;
|
||||
const buckets: number[][] = [];
|
||||
for (let i = 0; i < k; i++) {
|
||||
buckets.push([]);
|
||||
}
|
||||
// 1. Distribute array elements into various buckets
|
||||
for (const num of nums) {
|
||||
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
const i = Math.floor(num * k);
|
||||
// Add num to bucket i
|
||||
buckets[i].push(num);
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
for (const bucket of buckets) {
|
||||
// Use built-in sorting function, can also replace with other sorting algorithms
|
||||
bucket.sort((a, b) => a - b);
|
||||
}
|
||||
// 3. Traverse buckets to merge results
|
||||
let i = 0;
|
||||
for (const bucket of buckets) {
|
||||
for (const num of bucket) {
|
||||
nums[i++] = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="bucket_sort.dart"
|
||||
[class]{}-[func]{bucketSort}
|
||||
/* Bucket sort */
|
||||
void bucketSort(List<double> nums) {
|
||||
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
int k = nums.length ~/ 2;
|
||||
List<List<double>> buckets = List.generate(k, (index) => []);
|
||||
|
||||
// 1. Distribute array elements into various buckets
|
||||
for (double _num in nums) {
|
||||
// Input data range is [0, 1), use _num * k to map to index range [0, k-1]
|
||||
int i = (_num * k).toInt();
|
||||
// Add _num to bucket bucket_idx
|
||||
buckets[i].add(_num);
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
for (List<double> bucket in buckets) {
|
||||
bucket.sort();
|
||||
}
|
||||
// 3. Traverse buckets to merge results
|
||||
int i = 0;
|
||||
for (List<double> bucket in buckets) {
|
||||
for (double _num in bucket) {
|
||||
nums[i++] = _num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="bucket_sort.rs"
|
||||
[class]{}-[func]{bucket_sort}
|
||||
/* Bucket sort */
|
||||
fn bucket_sort(nums: &mut [f64]) {
|
||||
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
let k = nums.len() / 2;
|
||||
let mut buckets = vec![vec![]; k];
|
||||
// 1. Distribute array elements into various buckets
|
||||
for &num in nums.iter() {
|
||||
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
let i = (num * k as f64) as usize;
|
||||
// Add num to bucket i
|
||||
buckets[i].push(num);
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
for bucket in &mut buckets {
|
||||
// Use built-in sorting function, can also replace with other sorting algorithms
|
||||
bucket.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
}
|
||||
// 3. Traverse buckets to merge results
|
||||
let mut i = 0;
|
||||
for bucket in buckets.iter() {
|
||||
for &num in bucket.iter() {
|
||||
nums[i] = num;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="bucket_sort.c"
|
||||
[class]{}-[func]{bucketSort}
|
||||
/* Bucket sort */
|
||||
void bucketSort(float nums[], int n) {
|
||||
int k = n / 2; // Initialize k = n/2 buckets
|
||||
int *sizes = malloc(k * sizeof(int)); // Record each bucket's size
|
||||
float **buckets = malloc(k * sizeof(float *)); // Array of dynamic arrays (buckets)
|
||||
// Pre-allocate sufficient space for each bucket
|
||||
for (int i = 0; i < k; ++i) {
|
||||
buckets[i] = (float *)malloc(n * sizeof(float));
|
||||
sizes[i] = 0;
|
||||
}
|
||||
// 1. Distribute array elements into various buckets
|
||||
for (int i = 0; i < n; ++i) {
|
||||
int idx = (int)(nums[i] * k);
|
||||
buckets[idx][sizes[idx]++] = nums[i];
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
for (int i = 0; i < k; ++i) {
|
||||
qsort(buckets[i], sizes[i], sizeof(float), compare);
|
||||
}
|
||||
// 3. Merge sorted buckets
|
||||
int idx = 0;
|
||||
for (int i = 0; i < k; ++i) {
|
||||
for (int j = 0; j < sizes[i]; ++j) {
|
||||
nums[idx++] = buckets[i][j];
|
||||
}
|
||||
// Free memory
|
||||
free(buckets[i]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="bucket_sort.kt"
|
||||
[class]{}-[func]{bucketSort}
|
||||
/* Bucket sort */
|
||||
fun bucketSort(nums: FloatArray) {
|
||||
// Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
val k = nums.size / 2
|
||||
val buckets = mutableListOf<MutableList<Float>>()
|
||||
for (i in 0..<k) {
|
||||
buckets.add(mutableListOf())
|
||||
}
|
||||
// 1. Distribute array elements into various buckets
|
||||
for (num in nums) {
|
||||
// Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
val i = (num * k).toInt()
|
||||
// Add num to bucket i
|
||||
buckets[i].add(num)
|
||||
}
|
||||
// 2. Sort each bucket
|
||||
for (bucket in buckets) {
|
||||
// Use built-in sorting function, can also replace with other sorting algorithms
|
||||
bucket.sort()
|
||||
}
|
||||
// 3. Traverse buckets to merge results
|
||||
var i = 0
|
||||
for (bucket in buckets) {
|
||||
for (num in bucket) {
|
||||
nums[i++] = num
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="bucket_sort.rb"
|
||||
[class]{}-[func]{bucket_sort}
|
||||
### Bucket sort ###
|
||||
def bucket_sort(nums)
|
||||
# Initialize k = n/2 buckets, expected to allocate 2 elements per bucket
|
||||
k = nums.length / 2
|
||||
buckets = Array.new(k) { [] }
|
||||
|
||||
# 1. Distribute array elements into various buckets
|
||||
nums.each do |num|
|
||||
# Input data range is [0, 1), use num * k to map to index range [0, k-1]
|
||||
i = (num * k).to_i
|
||||
# Add num to bucket i
|
||||
buckets[i] << num
|
||||
end
|
||||
|
||||
# 2. Sort each bucket
|
||||
buckets.each do |bucket|
|
||||
# Use built-in sorting function, can also replace with other sorting algorithms
|
||||
bucket.sort!
|
||||
end
|
||||
|
||||
# 3. Traverse buckets to merge results
|
||||
i = 0
|
||||
buckets.each do |bucket|
|
||||
bucket.each do |num|
|
||||
nums[i] = num
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 11.8.2 Algorithm Characteristics
|
||||
|
||||
```zig title="bucket_sort.zig"
|
||||
[class]{}-[func]{bucketSort}
|
||||
```
|
||||
Bucket sort is suitable for processing very large data volumes. For example, if the input data contains 1 million elements and system memory cannot load all the data at once, the data can be divided into 1000 buckets, each bucket sorted separately, and then the results merged.
|
||||
|
||||
## 11.8.2 Algorithm characteristics
|
||||
- **Time complexity of $O(n + k)$**: Assuming the elements are evenly distributed among the buckets, then the number of elements in each bucket is $\frac{n}{k}$. Assuming sorting a single bucket uses $O(\frac{n}{k} \log\frac{n}{k})$ time, then sorting all buckets uses $O(n \log\frac{n}{k})$ time. **When the number of buckets $k$ is relatively large, the time complexity approaches $O(n)$**. Merging results requires traversing all buckets and elements, taking $O(n + k)$ time. In the worst case, all data is distributed into one bucket, and sorting that bucket uses $O(n^2)$ time.
|
||||
- **Space complexity of $O(n + k)$, non-in-place sorting**: Additional space is required for $k$ buckets and a total of $n$ elements.
|
||||
- Whether bucket sort is stable depends on whether the algorithm for sorting elements within buckets is stable.
|
||||
|
||||
Bucket sort is suitable for handling very large data sets. For example, if the input data includes 1 million elements, and system memory limitations prevent loading all the data at the same time, you can divide the data into 1,000 buckets and sort each bucket separately before merging the results.
|
||||
## 11.8.3 How to Achieve Even Distribution
|
||||
|
||||
- **Time complexity is $O(n + k)$**: Assuming the elements are evenly distributed across the buckets, the number of elements in each bucket is $n/k$. Assuming sorting a single bucket takes $O(n/k \log(n/k))$ time, sorting all buckets takes $O(n \log(n/k))$ time. **When the number of buckets $k$ is relatively large, the time complexity approaches $O(n)$**. Merging the results requires traversing all buckets and elements, taking $O(n + k)$ time. In the worst case, all data is distributed into a single bucket, and sorting that bucket takes $O(n^2)$ time.
|
||||
- **Space complexity is $O(n + k)$, non-in-place sorting**: It requires additional space for $k$ buckets and a total of $n$ elements.
|
||||
- Whether bucket sort is stable depends on whether the sorting algorithm used within each bucket is stable.
|
||||
Theoretically, bucket sort can achieve $O(n)$ time complexity. **The key is to evenly distribute elements to each bucket**, because real data is often not evenly distributed. For example, if we want to evenly distribute all products on Taobao into 10 buckets by price range, there may be very many products below 100 yuan and very few above 1000 yuan. If the price intervals are evenly divided into 10, the difference in the number of products in each bucket will be very large.
|
||||
|
||||
## 11.8.3 How to achieve even distribution
|
||||
To achieve even distribution, we can first set an approximate dividing line to roughly divide the data into 3 buckets. **After distribution is complete, continue dividing buckets with more products into 3 buckets until the number of elements in all buckets is roughly equal**.
|
||||
|
||||
The theoretical time complexity of bucket sort can reach $O(n)$. **The key is to evenly distribute the elements across all buckets** as real-world data is often not uniformly distributed. For example, we may want to evenly distribute all products on eBay by price range into 10 buckets. However, the distribution of product prices may not be even, with many under $100 and few over $500. If the price range is evenly divided into 10, the difference in the number of products in each bucket will be significant.
|
||||
As shown in Figure 11-14, this method essentially creates a recursion tree, with the goal of making the values of leaf nodes as even as possible. Of course, it is not necessary to divide the data into 3 buckets every round; the specific division method can be flexibly chosen according to data characteristics.
|
||||
|
||||
To achieve even distribution, we can initially set an approximate boundary to roughly divide the data into 3 buckets. **After the distribution is complete, the buckets with more items can be further divided into 3 buckets, until the number of elements in all buckets is roughly equal**.
|
||||
{ class="animation-figure" }
|
||||
|
||||
As shown in Figure 11-14, this method essentially constructs a recursive tree, aiming to ensure the element counts in leaf nodes are as even as possible. Of course, you don't have to divide the data into 3 buckets each round - the partitioning strategy can be adaptively tailored to the data's unique characteristics.
|
||||
<p align="center"> Figure 11-14 Recursively dividing buckets </p>
|
||||
|
||||
{ class="animation-figure" }
|
||||
If we know the probability distribution of product prices in advance, **we can set the price dividing line for each bucket based on the data probability distribution**. It is worth noting that the data distribution does not necessarily need to be specifically calculated, but can also be approximated using a certain probability model based on data characteristics.
|
||||
|
||||
<p align="center"> Figure 11-14 Recursive division of buckets </p>
|
||||
|
||||
If we know the probability distribution of product prices in advance, **we can set the price boundaries for each bucket based on the data probability distribution**. It is worth noting that it is not necessarily required to specifically calculate the data distribution; instead, it can be approximated based on data characteristics using a probability model.
|
||||
|
||||
As shown in Figure 11-15, assuming that product prices follow a normal distribution, we can define reasonable price intervals to balance the distribution of items across the buckets.
|
||||
As shown in Figure 11-15, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.9 Counting sort
|
||||
# 11.9 Counting Sort
|
||||
|
||||
<u>Counting sort</u> achieves sorting by counting the number of elements, usually applied to integer arrays.
|
||||
<u>Counting sort (counting sort)</u> achieves sorting by counting the number of elements, typically applied to integer arrays.
|
||||
|
||||
## 11.9.1 Simple implementation
|
||||
## 11.9.1 Simple Implementation
|
||||
|
||||
Let's start with a simple example. Given an array `nums` of length $n$, where all elements are "non-negative integers", the overall process of counting sort is shown in Figure 11-16.
|
||||
Let's start with a simple example. Given an array `nums` of length $n$, where the elements are all "non-negative integers", the overall flow of counting sort is shown in Figure 11-16.
|
||||
|
||||
1. Traverse the array to find the maximum number, denoted as $m$, then create an auxiliary array `counter` of length $m + 1$.
|
||||
2. **Use `counter` to count the occurrence of each number in `nums`**, where `counter[num]` corresponds to the occurrence of the number `num`. The counting method is simple, just traverse `nums` (suppose the current number is `num`), and increase `counter[num]` by $1$ each round.
|
||||
3. **Since the indices of `counter` are naturally ordered, all numbers are essentially sorted already**. Next, we traverse `counter`, and fill in `nums` in ascending order of occurrence.
|
||||
1. Traverse the array to find the largest number, denoted as $m$, and then create an auxiliary array `counter` of length $m + 1$.
|
||||
2. **Use `counter` to count the number of occurrences of each number in `nums`**, where `counter[num]` corresponds to the number of occurrences of the number `num`. The counting method is simple: just traverse `nums` (let the current number be `num`), and increase `counter[num]` by $1$ in each round.
|
||||
3. **Since each index of `counter` is naturally ordered, this is equivalent to all numbers being sorted**. Next, we traverse `counter` and fill in `nums` in ascending order based on the number of occurrences of each number.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-16 Counting sort process </p>
|
||||
<p align="center"> Figure 11-16 Counting sort flow </p>
|
||||
|
||||
The code is shown below:
|
||||
The code is as follows:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -30,7 +30,7 @@ The code is shown below:
|
||||
m = 0
|
||||
for num in nums:
|
||||
m = max(m, num)
|
||||
# 2. Count the occurrence of each digit
|
||||
# 2. Count the occurrence of each number
|
||||
# counter[num] represents the occurrence of num
|
||||
counter = [0] * (m + 1)
|
||||
for num in nums:
|
||||
@@ -54,7 +54,7 @@ The code is shown below:
|
||||
for (int num : nums) {
|
||||
m = max(m, num);
|
||||
}
|
||||
// 2. Count the occurrence of each digit
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
vector<int> counter(m + 1, 0);
|
||||
for (int num : nums) {
|
||||
@@ -81,7 +81,7 @@ The code is shown below:
|
||||
for (int num : nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
// 2. Count the occurrence of each digit
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
int[] counter = new int[m + 1];
|
||||
for (int num : nums) {
|
||||
@@ -100,92 +100,292 @@ The code is shown below:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="counting_sort.cs"
|
||||
[class]{counting_sort}-[func]{CountingSortNaive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
void CountingSortNaive(int[] nums) {
|
||||
// 1. Count the maximum element m in the array
|
||||
int m = 0;
|
||||
foreach (int num in nums) {
|
||||
m = Math.Max(m, num);
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
int[] counter = new int[m + 1];
|
||||
foreach (int num in nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
int i = 0;
|
||||
for (int num = 0; num < m + 1; num++) {
|
||||
for (int j = 0; j < counter[num]; j++, i++) {
|
||||
nums[i] = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="counting_sort.go"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
func countingSortNaive(nums []int) {
|
||||
// 1. Count the maximum element m in the array
|
||||
m := 0
|
||||
for _, num := range nums {
|
||||
if num > m {
|
||||
m = num
|
||||
}
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
counter := make([]int, m+1)
|
||||
for _, num := range nums {
|
||||
counter[num]++
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
for i, num := 0, 0; num < m+1; num++ {
|
||||
for j := 0; j < counter[num]; j++ {
|
||||
nums[i] = num
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="counting_sort.swift"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
func countingSortNaive(nums: inout [Int]) {
|
||||
// 1. Count the maximum element m in the array
|
||||
let m = nums.max()!
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
var counter = Array(repeating: 0, count: m + 1)
|
||||
for num in nums {
|
||||
counter[num] += 1
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
var i = 0
|
||||
for num in 0 ..< m + 1 {
|
||||
for _ in 0 ..< counter[num] {
|
||||
nums[i] = num
|
||||
i += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="counting_sort.js"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
function countingSortNaive(nums) {
|
||||
// 1. Count the maximum element m in the array
|
||||
let m = Math.max(...nums);
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
const counter = new Array(m + 1).fill(0);
|
||||
for (const num of nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
let i = 0;
|
||||
for (let num = 0; num < m + 1; num++) {
|
||||
for (let j = 0; j < counter[num]; j++, i++) {
|
||||
nums[i] = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="counting_sort.ts"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
function countingSortNaive(nums: number[]): void {
|
||||
// 1. Count the maximum element m in the array
|
||||
let m: number = Math.max(...nums);
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
const counter: number[] = new Array<number>(m + 1).fill(0);
|
||||
for (const num of nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
let i = 0;
|
||||
for (let num = 0; num < m + 1; num++) {
|
||||
for (let j = 0; j < counter[num]; j++, i++) {
|
||||
nums[i] = num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="counting_sort.dart"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
void countingSortNaive(List<int> nums) {
|
||||
// 1. Count the maximum element m in the array
|
||||
int m = 0;
|
||||
for (int _num in nums) {
|
||||
m = max(m, _num);
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[_num] represents occurrence count of _num
|
||||
List<int> counter = List.filled(m + 1, 0);
|
||||
for (int _num in nums) {
|
||||
counter[_num]++;
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
int i = 0;
|
||||
for (int _num = 0; _num < m + 1; _num++) {
|
||||
for (int j = 0; j < counter[_num]; j++, i++) {
|
||||
nums[i] = _num;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="counting_sort.rs"
|
||||
[class]{}-[func]{counting_sort_naive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
fn counting_sort_naive(nums: &mut [i32]) {
|
||||
// 1. Count the maximum element m in the array
|
||||
let m = *nums.iter().max().unwrap();
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
let mut counter = vec![0; m as usize + 1];
|
||||
for &num in nums.iter() {
|
||||
counter[num as usize] += 1;
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
let mut i = 0;
|
||||
for num in 0..m + 1 {
|
||||
for _ in 0..counter[num as usize] {
|
||||
nums[i] = num;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="counting_sort.c"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
void countingSortNaive(int nums[], int size) {
|
||||
// 1. Count the maximum element m in the array
|
||||
int m = 0;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (nums[i] > m) {
|
||||
m = nums[i];
|
||||
}
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
int *counter = calloc(m + 1, sizeof(int));
|
||||
for (int i = 0; i < size; i++) {
|
||||
counter[nums[i]]++;
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
int i = 0;
|
||||
for (int num = 0; num < m + 1; num++) {
|
||||
for (int j = 0; j < counter[num]; j++, i++) {
|
||||
nums[i] = num;
|
||||
}
|
||||
}
|
||||
// 4. Free memory
|
||||
free(counter);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="counting_sort.kt"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
/* Counting sort */
|
||||
// Simple implementation, cannot be used for sorting objects
|
||||
fun countingSortNaive(nums: IntArray) {
|
||||
// 1. Count the maximum element m in the array
|
||||
var m = 0
|
||||
for (num in nums) {
|
||||
m = max(m, num)
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
val counter = IntArray(m + 1)
|
||||
for (num in nums) {
|
||||
counter[num]++
|
||||
}
|
||||
// 3. Traverse counter, filling each element back into the original array nums
|
||||
var i = 0
|
||||
for (num in 0..<m + 1) {
|
||||
var j = 0
|
||||
while (j < counter[num]) {
|
||||
nums[i] = num
|
||||
j++
|
||||
i++
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="counting_sort.rb"
|
||||
[class]{}-[func]{counting_sort_naive}
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="counting_sort.zig"
|
||||
[class]{}-[func]{countingSortNaive}
|
||||
### Counting sort ###
|
||||
def counting_sort_naive(nums)
|
||||
# Simple implementation, cannot be used for sorting objects
|
||||
# 1. Count the maximum element m in the array
|
||||
m = 0
|
||||
nums.each { |num| m = [m, num].max }
|
||||
# 2. Count the occurrence of each number
|
||||
# counter[num] represents the occurrence of num
|
||||
counter = Array.new(m + 1, 0)
|
||||
nums.each { |num| counter[num] += 1 }
|
||||
# 3. Traverse counter, filling each element back into the original array nums
|
||||
i = 0
|
||||
for num in 0...(m + 1)
|
||||
(0...counter[num]).each do
|
||||
nums[i] = num
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
!!! note "Connection between counting sort and bucket sort"
|
||||
|
||||
From the perspective of bucket sort, we can consider each index of the counting array `counter` in counting sort as a bucket, and the process of counting as distributing elements into the corresponding buckets. Essentially, counting sort is a special case of bucket sort for integer data.
|
||||
From the perspective of bucket sort, we can regard each index of the counting array `counter` in counting sort as a bucket, and the process of counting quantities as distributing each element to the corresponding bucket. Essentially, counting sort is a special case of bucket sort for integer data.
|
||||
|
||||
## 11.9.2 Complete implementation
|
||||
## 11.9.2 Complete Implementation
|
||||
|
||||
Observant readers might notice, **if the input data is an object, the above step `3.` is invalid**. Suppose the input data is a product object, we want to sort the products by the price (a class member variable), but the above algorithm can only give the sorted price as the result.
|
||||
Observant readers may have noticed that **if the input data is objects, step `3.` above becomes invalid**. Suppose the input data is product objects, and we want to sort the products by price (a member variable of the class), but the above algorithm can only give the sorting result of prices.
|
||||
|
||||
So how can we get the sorting result for the original data? First, we calculate the "prefix sum" of `counter`. As the name suggests, the prefix sum at index `i`, `prefix[i]`, equals the sum of the first `i` elements of the array:
|
||||
So how can we obtain the sorting result of the original data? We first calculate the "prefix sum" of `counter`. As the name suggests, the prefix sum at index `i`, `prefix[i]`, equals the sum of the first `i` elements of the array:
|
||||
|
||||
$$
|
||||
\text{prefix}[i] = \sum_{j=0}^i \text{counter[j]}
|
||||
$$
|
||||
|
||||
**The prefix sum has a clear meaning, `prefix[num] - 1` represents the index of the last occurrence of element `num` in the result array `res`**. This information is crucial, as it tells us where each element should appear in the result array. Next, we traverse each element `num` of the original array `nums` in reverse order, performing the following two steps in each iteration.
|
||||
**The prefix sum has a clear meaning: `prefix[num] - 1` represents the index of the last occurrence of element `num` in the result array `res`**. This information is very critical because it tells us where each element should appear in the result array. Next, we traverse each element `num` of the original array `nums` in reverse order, performing the following two steps in each iteration.
|
||||
|
||||
1. Fill `num` into the array `res` at the index `prefix[num] - 1`.
|
||||
2. Decrease the prefix sum `prefix[num]` by $1$ to obtain the next index to place `num`.
|
||||
1. Fill `num` into the array `res` at index `prefix[num] - 1`.
|
||||
2. Decrease the prefix sum `prefix[num]` by $1$ to get the index for the next placement of `num`.
|
||||
|
||||
After the traversal, the array `res` contains the sorted result, and finally, `res` replaces the original array `nums`. The complete counting sort process is shown in Figure 11-17.
|
||||
After the traversal is complete, the array `res` contains the sorted result, and finally `res` is used to overwrite the original array `nums`. The complete counting sort flow is shown in Figure 11-17.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -208,9 +408,9 @@ After the traversal, the array `res` contains the sorted result, and finally, `r
|
||||
=== "<8>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-17 Counting sort process </p>
|
||||
<p align="center"> Figure 11-17 Counting sort steps </p>
|
||||
|
||||
The implementation code of counting sort is shown below:
|
||||
The implementation code of counting sort is as follows:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -220,7 +420,7 @@ The implementation code of counting sort is shown below:
|
||||
# Complete implementation, can sort objects and is a stable sort
|
||||
# 1. Count the maximum element m in the array
|
||||
m = max(nums)
|
||||
# 2. Count the occurrence of each digit
|
||||
# 2. Count the occurrence of each number
|
||||
# counter[num] represents the occurrence of num
|
||||
counter = [0] * (m + 1)
|
||||
for num in nums:
|
||||
@@ -253,7 +453,7 @@ The implementation code of counting sort is shown below:
|
||||
for (int num : nums) {
|
||||
m = max(m, num);
|
||||
}
|
||||
// 2. Count the occurrence of each digit
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
vector<int> counter(m + 1, 0);
|
||||
for (int num : nums) {
|
||||
@@ -289,7 +489,7 @@ The implementation code of counting sort is shown below:
|
||||
for (int num : nums) {
|
||||
m = Math.max(m, num);
|
||||
}
|
||||
// 2. Count the occurrence of each digit
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
int[] counter = new int[m + 1];
|
||||
for (int num : nums) {
|
||||
@@ -319,79 +519,371 @@ The implementation code of counting sort is shown below:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="counting_sort.cs"
|
||||
[class]{counting_sort}-[func]{CountingSort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
void CountingSort(int[] nums) {
|
||||
// 1. Count the maximum element m in the array
|
||||
int m = 0;
|
||||
foreach (int num in nums) {
|
||||
m = Math.Max(m, num);
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
int[] counter = new int[m + 1];
|
||||
foreach (int num in nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// counter[num]-1 is the last index where num appears in res
|
||||
for (int i = 0; i < m; i++) {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
int n = nums.Length;
|
||||
int[] res = new int[n];
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
int num = nums[i];
|
||||
res[counter[num] - 1] = num; // Place num at the corresponding index
|
||||
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
for (int i = 0; i < n; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="counting_sort.go"
|
||||
[class]{}-[func]{countingSort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
func countingSort(nums []int) {
|
||||
// 1. Count the maximum element m in the array
|
||||
m := 0
|
||||
for _, num := range nums {
|
||||
if num > m {
|
||||
m = num
|
||||
}
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
counter := make([]int, m+1)
|
||||
for _, num := range nums {
|
||||
counter[num]++
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// counter[num]-1 is the last index where num appears in res
|
||||
for i := 0; i < m; i++ {
|
||||
counter[i+1] += counter[i]
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
n := len(nums)
|
||||
res := make([]int, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
num := nums[i]
|
||||
// Place num at the corresponding index
|
||||
res[counter[num]-1] = num
|
||||
// Decrement the prefix sum by 1, getting the next index to place num
|
||||
counter[num]--
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
copy(nums, res)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="counting_sort.swift"
|
||||
[class]{}-[func]{countingSort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
func countingSort(nums: inout [Int]) {
|
||||
// 1. Count the maximum element m in the array
|
||||
let m = nums.max()!
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
var counter = Array(repeating: 0, count: m + 1)
|
||||
for num in nums {
|
||||
counter[num] += 1
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// counter[num]-1 is the last index where num appears in res
|
||||
for i in 0 ..< m {
|
||||
counter[i + 1] += counter[i]
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
var res = Array(repeating: 0, count: nums.count)
|
||||
for i in nums.indices.reversed() {
|
||||
let num = nums[i]
|
||||
res[counter[num] - 1] = num // Place num at the corresponding index
|
||||
counter[num] -= 1 // Decrement the prefix sum by 1, getting the next index to place num
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
for i in nums.indices {
|
||||
nums[i] = res[i]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="counting_sort.js"
|
||||
[class]{}-[func]{countingSort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
function countingSort(nums) {
|
||||
// 1. Count the maximum element m in the array
|
||||
let m = Math.max(...nums);
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
const counter = new Array(m + 1).fill(0);
|
||||
for (const num of nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// counter[num]-1 is the last index where num appears in res
|
||||
for (let i = 0; i < m; i++) {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
const n = nums.length;
|
||||
const res = new Array(n);
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const num = nums[i];
|
||||
res[counter[num] - 1] = num; // Place num at the corresponding index
|
||||
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
for (let i = 0; i < n; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="counting_sort.ts"
|
||||
[class]{}-[func]{countingSort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
function countingSort(nums: number[]): void {
|
||||
// 1. Count the maximum element m in the array
|
||||
let m: number = Math.max(...nums);
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
const counter: number[] = new Array<number>(m + 1).fill(0);
|
||||
for (const num of nums) {
|
||||
counter[num]++;
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// counter[num]-1 is the last index where num appears in res
|
||||
for (let i = 0; i < m; i++) {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
const n = nums.length;
|
||||
const res: number[] = new Array<number>(n);
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const num = nums[i];
|
||||
res[counter[num] - 1] = num; // Place num at the corresponding index
|
||||
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
for (let i = 0; i < n; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="counting_sort.dart"
|
||||
[class]{}-[func]{countingSort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
void countingSort(List<int> nums) {
|
||||
// 1. Count the maximum element m in the array
|
||||
int m = 0;
|
||||
for (int _num in nums) {
|
||||
m = max(m, _num);
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[_num] represents occurrence count of _num
|
||||
List<int> counter = List.filled(m + 1, 0);
|
||||
for (int _num in nums) {
|
||||
counter[_num]++;
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// That is, counter[_num]-1 is the last occurrence index of _num in res
|
||||
for (int i = 0; i < m; i++) {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
int n = nums.length;
|
||||
List<int> res = List.filled(n, 0);
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
int _num = nums[i];
|
||||
res[counter[_num] - 1] = _num; // Place _num at corresponding index
|
||||
counter[_num]--; // Decrement prefix sum by 1 to get next placement index for _num
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
nums.setAll(0, res);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="counting_sort.rs"
|
||||
[class]{}-[func]{counting_sort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
fn counting_sort(nums: &mut [i32]) {
|
||||
// 1. Count the maximum element m in the array
|
||||
let m = *nums.iter().max().unwrap() as usize;
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
let mut counter = vec![0; m + 1];
|
||||
for &num in nums.iter() {
|
||||
counter[num as usize] += 1;
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// counter[num]-1 is the last index where num appears in res
|
||||
for i in 0..m {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
let n = nums.len();
|
||||
let mut res = vec![0; n];
|
||||
for i in (0..n).rev() {
|
||||
let num = nums[i];
|
||||
res[counter[num as usize] - 1] = num; // Place num at the corresponding index
|
||||
counter[num as usize] -= 1; // Decrement the prefix sum by 1, getting the next index to place num
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
nums.copy_from_slice(&res)
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="counting_sort.c"
|
||||
[class]{}-[func]{countingSort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
void countingSort(int nums[], int size) {
|
||||
// 1. Count the maximum element m in the array
|
||||
int m = 0;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (nums[i] > m) {
|
||||
m = nums[i];
|
||||
}
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
int *counter = calloc(m, sizeof(int));
|
||||
for (int i = 0; i < size; i++) {
|
||||
counter[nums[i]]++;
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// counter[num]-1 is the last index where num appears in res
|
||||
for (int i = 0; i < m; i++) {
|
||||
counter[i + 1] += counter[i];
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
int *res = malloc(sizeof(int) * size);
|
||||
for (int i = size - 1; i >= 0; i--) {
|
||||
int num = nums[i];
|
||||
res[counter[num] - 1] = num; // Place num at the corresponding index
|
||||
counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
memcpy(nums, res, size * sizeof(int));
|
||||
// 5. Free memory
|
||||
free(res);
|
||||
free(counter);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="counting_sort.kt"
|
||||
[class]{}-[func]{countingSort}
|
||||
/* Counting sort */
|
||||
// Complete implementation, can sort objects and is a stable sort
|
||||
fun countingSort(nums: IntArray) {
|
||||
// 1. Count the maximum element m in the array
|
||||
var m = 0
|
||||
for (num in nums) {
|
||||
m = max(m, num)
|
||||
}
|
||||
// 2. Count the occurrence of each number
|
||||
// counter[num] represents the occurrence of num
|
||||
val counter = IntArray(m + 1)
|
||||
for (num in nums) {
|
||||
counter[num]++
|
||||
}
|
||||
// 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
// counter[num]-1 is the last index where num appears in res
|
||||
for (i in 0..<m) {
|
||||
counter[i + 1] += counter[i]
|
||||
}
|
||||
// 4. Traverse nums in reverse order, placing each element into the result array res
|
||||
// Initialize the array res to record results
|
||||
val n = nums.size
|
||||
val res = IntArray(n)
|
||||
for (i in n - 1 downTo 0) {
|
||||
val num = nums[i]
|
||||
res[counter[num] - 1] = num // Place num at the corresponding index
|
||||
counter[num]-- // Decrement the prefix sum by 1, getting the next index to place num
|
||||
}
|
||||
// Use result array res to overwrite the original array nums
|
||||
for (i in 0..<n) {
|
||||
nums[i] = res[i]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="counting_sort.rb"
|
||||
[class]{}-[func]{counting_sort}
|
||||
### Counting sort ###
|
||||
def counting_sort(nums)
|
||||
# Complete implementation, can sort objects and is a stable sort
|
||||
# 1. Count the maximum element m in the array
|
||||
m = nums.max
|
||||
# 2. Count the occurrence of each number
|
||||
# counter[num] represents the occurrence of num
|
||||
counter = Array.new(m + 1, 0)
|
||||
nums.each { |num| counter[num] += 1 }
|
||||
# 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index"
|
||||
# counter[num]-1 is the last index where num appears in res
|
||||
(0...m).each { |i| counter[i + 1] += counter[i] }
|
||||
# 4. Traverse nums in reverse, fill elements into result array res
|
||||
# Initialize the array res to record results
|
||||
n = nums.length
|
||||
res = Array.new(n, 0)
|
||||
(n - 1).downto(0).each do |i|
|
||||
num = nums[i]
|
||||
res[counter[num] - 1] = num # Place num at the corresponding index
|
||||
counter[num] -= 1 # Decrement the prefix sum by 1, getting the next index to place num
|
||||
end
|
||||
# Use result array res to overwrite the original array nums
|
||||
(0...n).each { |i| nums[i] = res[i] }
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 11.9.3 Algorithm Characteristics
|
||||
|
||||
```zig title="counting_sort.zig"
|
||||
[class]{}-[func]{countingSort}
|
||||
```
|
||||
|
||||
## 11.9.3 Algorithm characteristics
|
||||
|
||||
- **Time complexity is $O(n + m)$, non-adaptive sort**: It involves traversing `nums` and `counter`, both using linear time. Generally, $n \gg m$, and the time complexity tends towards $O(n)$.
|
||||
- **Space complexity is $O(n + m)$, non-in-place sort**: It uses array `res` of lengths $n$ and array `counter` of length $m$ respectively.
|
||||
- **Stable sort**: Since elements are filled into `res` in a "right-to-left" order, reversing the traversal of `nums` can prevent changing the relative position between equal elements, thereby achieving a stable sort. Actually, traversing `nums` in order can also produce the correct sorting result, but the outcome is unstable.
|
||||
- **Time complexity of $O(n + m)$, non-adaptive sorting**: Involves traversing `nums` and traversing `counter`, both using linear time. Generally, $n \gg m$, and time complexity tends toward $O(n)$.
|
||||
- **Space complexity of $O(n + m)$, non-in-place sorting**: Uses arrays `res` and `counter` of lengths $n$ and $m$ respectively.
|
||||
- **Stable sorting**: Since elements are filled into `res` in a "right-to-left" order, traversing `nums` in reverse can avoid changing the relative positions of equal elements, thereby achieving stable sorting. In fact, traversing `nums` in forward order can also yield correct sorting results, but the result would be unstable.
|
||||
|
||||
## 11.9.4 Limitations
|
||||
|
||||
By now, you might find counting sort very clever, as it can achieve efficient sorting merely by counting quantities. However, the prerequisites for using counting sort are relatively strict.
|
||||
By this point, you might think counting sort is very clever, as it can achieve efficient sorting just by counting quantities. However, the prerequisites for using counting sort are relatively strict.
|
||||
|
||||
**Counting sort is only suitable for non-negative integers**. If you want to apply it to other types of data, you need to ensure that these data can be converted to non-negative integers without changing the original order of the elements. For example, for an array containing negative integers, you can first add a constant to all numbers, converting them all to positive numbers, and then convert them back after sorting is complete.
|
||||
**Counting sort is only suitable for non-negative integers**. If you want to apply it to other types of data, you need to ensure that the data can be converted to non-negative integers without changing the relative size relationships between elements. For example, for an integer array containing negative numbers, you can first add a constant to all numbers to convert them all to positive numbers, and then convert them back after sorting is complete.
|
||||
|
||||
**Counting sort is suitable for large datasets with a small range of values**. For example, in the above example, $m$ should not be too large, otherwise, it will occupy too much space. And when $n \ll m$, counting sort uses $O(m)$ time, which may be slower than $O(n \log n)$ sorting algorithms.
|
||||
**Counting sort is suitable for situations where the data volume is large but the data range is small**. For example, in the above example, $m$ cannot be too large, otherwise it will occupy too much space. And when $n \ll m$, counting sort uses $O(m)$ time, which may be slower than $O(n \log n)$ sorting algorithms.
|
||||
|
||||
@@ -2,34 +2,34 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.7 Heap sort
|
||||
# 11.7 Heap Sort
|
||||
|
||||
!!! tip
|
||||
|
||||
Before reading this section, please ensure you have completed the "Heap" chapter.
|
||||
|
||||
<u>Heap sort</u> is an efficient sorting algorithm based on the heap data structure. We can implement heap sort using the "heap creation" and "element extraction" operations we have already learned.
|
||||
<u>Heap sort (heap sort)</u> is an efficient sorting algorithm based on the heap data structure. We can use the "build heap operation" and "element out-heap operation" that we have already learned to implement heap sort.
|
||||
|
||||
1. Input the array and construct a min-heap, where the smallest element is at the top of the heap.
|
||||
2. Continuously perform the extraction operation, record the extracted elements sequentially to obtain a sorted list from smallest to largest.
|
||||
1. Input the array and build a min-heap, at which point the smallest element is at the heap top.
|
||||
2. Continuously perform the out-heap operation, record the out-heap elements in sequence, and an ascending sorted sequence can be obtained.
|
||||
|
||||
Although the above method is feasible, it requires an additional array to store the popped elements, which is somewhat space-consuming. In practice, we usually use a more elegant implementation.
|
||||
Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method.
|
||||
|
||||
## 11.7.1 Algorithm flow
|
||||
## 11.7.1 Algorithm Flow
|
||||
|
||||
Suppose the array length is $n$, the heap sort process is as follows.
|
||||
Assume the array length is $n$. The flow of heap sort is shown in Figure 11-12.
|
||||
|
||||
1. Input the array and establish a max-heap. After this step, the largest element is positioned at the top of the heap.
|
||||
2. Swap the top element of the heap (the first element) with the heap's bottom element (the last element). Following this swap, reduce the heap's length by $1$ and increase the sorted elements count by $1$.
|
||||
3. Starting from the heap top, perform the sift-down operation from top to bottom. After the sift-down, the heap's property is restored.
|
||||
4. Repeat steps `2.` and `3.` Loop for $n - 1$ rounds to complete the sorting of the array.
|
||||
1. Input the array and build a max-heap. After completion, the largest element is at the heap top.
|
||||
2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by $1$ and increase the count of sorted elements by $1$.
|
||||
3. Starting from the heap top element, perform top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored.
|
||||
4. Loop through steps `2.` and `3.` After looping $n - 1$ rounds, the array sorting can be completed.
|
||||
|
||||
!!! tip
|
||||
|
||||
In fact, the element extraction operation also includes steps `2.` and `3.`, with an additional step to pop (remove) the extracted element from the heap.
|
||||
In fact, the element out-heap operation also includes steps `2.` and `3.`, with just an additional step to pop the element.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -64,9 +64,9 @@ Suppose the array length is $n$, the heap sort process is as follows.
|
||||
=== "<12>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-12 Heap sort process </p>
|
||||
<p align="center"> Figure 11-12 Heap sort steps </p>
|
||||
|
||||
In the code implementation, we used the sift-down function `sift_down()` from the "Heap" chapter. It is important to note that since the heap's length decreases as the maximum element is extracted, we need to add a length parameter $n$ to the `sift_down()` function to specify the current effective length of the heap. The code is shown below:
|
||||
In the code implementation, we use the same top-to-bottom heapify function `sift_down()` from the "Heap" chapter. It is worth noting that since the heap length will decrease as the largest element is extracted, we need to add a length parameter $n$ to the `sift_down()` function to specify the current effective length of the heap. The code is as follows:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -109,7 +109,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
void siftDown(vector<int> &nums, int n, int i) {
|
||||
while (true) {
|
||||
// Determine the largest node among i, l, r, noted as ma
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
int l = 2 * i + 1;
|
||||
int r = 2 * i + 2;
|
||||
int ma = i;
|
||||
@@ -117,7 +117,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
|
||||
ma = l;
|
||||
if (r < n && nums[r] > nums[ma])
|
||||
ma = r;
|
||||
// If node i is the largest or indices l, r are out of bounds, no further heapification needed, break
|
||||
// Swap two nodes
|
||||
if (ma == i) {
|
||||
break;
|
||||
}
|
||||
@@ -136,7 +136,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for (int i = nums.size() - 1; i > 0; --i) {
|
||||
// Swap the root node with the rightmost leaf node (swap the first element with the last element)
|
||||
// Delete node
|
||||
swap(nums[0], nums[i]);
|
||||
// Start heapifying the root node, from top to bottom
|
||||
siftDown(nums, i, 0);
|
||||
@@ -150,7 +150,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
void siftDown(int[] nums, int n, int i) {
|
||||
while (true) {
|
||||
// Determine the largest node among i, l, r, noted as ma
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
int l = 2 * i + 1;
|
||||
int r = 2 * i + 2;
|
||||
int ma = i;
|
||||
@@ -158,7 +158,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
|
||||
ma = l;
|
||||
if (r < n && nums[r] > nums[ma])
|
||||
ma = r;
|
||||
// If node i is the largest or indices l, r are out of bounds, no further heapification needed, break
|
||||
// Swap two nodes
|
||||
if (ma == i)
|
||||
break;
|
||||
// Swap two nodes
|
||||
@@ -178,7 +178,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
// Swap the root node with the rightmost leaf node (swap the first element with the last element)
|
||||
// Delete node
|
||||
int tmp = nums[0];
|
||||
nums[0] = nums[i];
|
||||
nums[i] = tmp;
|
||||
@@ -191,93 +191,429 @@ In the code implementation, we used the sift-down function `sift_down()` from th
|
||||
=== "C#"
|
||||
|
||||
```csharp title="heap_sort.cs"
|
||||
[class]{heap_sort}-[func]{SiftDown}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
void SiftDown(int[] nums, int n, int i) {
|
||||
while (true) {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
int l = 2 * i + 1;
|
||||
int r = 2 * i + 2;
|
||||
int ma = i;
|
||||
if (l < n && nums[l] > nums[ma])
|
||||
ma = l;
|
||||
if (r < n && nums[r] > nums[ma])
|
||||
ma = r;
|
||||
// Swap two nodes
|
||||
if (ma == i)
|
||||
break;
|
||||
// Swap two nodes
|
||||
(nums[ma], nums[i]) = (nums[i], nums[ma]);
|
||||
// Loop downwards heapification
|
||||
i = ma;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{heap_sort}-[func]{HeapSort}
|
||||
/* Heap sort */
|
||||
void HeapSort(int[] nums) {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for (int i = nums.Length / 2 - 1; i >= 0; i--) {
|
||||
SiftDown(nums, nums.Length, i);
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for (int i = nums.Length - 1; i > 0; i--) {
|
||||
// Delete node
|
||||
(nums[i], nums[0]) = (nums[0], nums[i]);
|
||||
// Start heapifying the root node, from top to bottom
|
||||
SiftDown(nums, i, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="heap_sort.go"
|
||||
[class]{}-[func]{siftDown}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
func siftDown(nums *[]int, n, i int) {
|
||||
for true {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
l := 2*i + 1
|
||||
r := 2*i + 2
|
||||
ma := i
|
||||
if l < n && (*nums)[l] > (*nums)[ma] {
|
||||
ma = l
|
||||
}
|
||||
if r < n && (*nums)[r] > (*nums)[ma] {
|
||||
ma = r
|
||||
}
|
||||
// Swap two nodes
|
||||
if ma == i {
|
||||
break
|
||||
}
|
||||
// Swap two nodes
|
||||
(*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i]
|
||||
// Loop downwards heapification
|
||||
i = ma
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
/* Heap sort */
|
||||
func heapSort(nums *[]int) {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for i := len(*nums)/2 - 1; i >= 0; i-- {
|
||||
siftDown(nums, len(*nums), i)
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for i := len(*nums) - 1; i > 0; i-- {
|
||||
// Delete node
|
||||
(*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0]
|
||||
// Start heapifying the root node, from top to bottom
|
||||
siftDown(nums, i, 0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="heap_sort.swift"
|
||||
[class]{}-[func]{siftDown}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
func siftDown(nums: inout [Int], n: Int, i: Int) {
|
||||
var i = i
|
||||
while true {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
let l = 2 * i + 1
|
||||
let r = 2 * i + 2
|
||||
var ma = i
|
||||
if l < n, nums[l] > nums[ma] {
|
||||
ma = l
|
||||
}
|
||||
if r < n, nums[r] > nums[ma] {
|
||||
ma = r
|
||||
}
|
||||
// Swap two nodes
|
||||
if ma == i {
|
||||
break
|
||||
}
|
||||
// Swap two nodes
|
||||
nums.swapAt(i, ma)
|
||||
// Loop downwards heapification
|
||||
i = ma
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
/* Heap sort */
|
||||
func heapSort(nums: inout [Int]) {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) {
|
||||
siftDown(nums: &nums, n: nums.count, i: i)
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for i in nums.indices.dropFirst().reversed() {
|
||||
// Delete node
|
||||
nums.swapAt(0, i)
|
||||
// Start heapifying the root node, from top to bottom
|
||||
siftDown(nums: &nums, n: i, i: 0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="heap_sort.js"
|
||||
[class]{}-[func]{siftDown}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
function siftDown(nums, n, i) {
|
||||
while (true) {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
let l = 2 * i + 1;
|
||||
let r = 2 * i + 2;
|
||||
let ma = i;
|
||||
if (l < n && nums[l] > nums[ma]) {
|
||||
ma = l;
|
||||
}
|
||||
if (r < n && nums[r] > nums[ma]) {
|
||||
ma = r;
|
||||
}
|
||||
// Swap two nodes
|
||||
if (ma === i) {
|
||||
break;
|
||||
}
|
||||
// Swap two nodes
|
||||
[nums[i], nums[ma]] = [nums[ma], nums[i]];
|
||||
// Loop downwards heapification
|
||||
i = ma;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
/* Heap sort */
|
||||
function heapSort(nums) {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {
|
||||
siftDown(nums, nums.length, i);
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
// Delete node
|
||||
[nums[0], nums[i]] = [nums[i], nums[0]];
|
||||
// Start heapifying the root node, from top to bottom
|
||||
siftDown(nums, i, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="heap_sort.ts"
|
||||
[class]{}-[func]{siftDown}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
function siftDown(nums: number[], n: number, i: number): void {
|
||||
while (true) {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
let l = 2 * i + 1;
|
||||
let r = 2 * i + 2;
|
||||
let ma = i;
|
||||
if (l < n && nums[l] > nums[ma]) {
|
||||
ma = l;
|
||||
}
|
||||
if (r < n && nums[r] > nums[ma]) {
|
||||
ma = r;
|
||||
}
|
||||
// Swap two nodes
|
||||
if (ma === i) {
|
||||
break;
|
||||
}
|
||||
// Swap two nodes
|
||||
[nums[i], nums[ma]] = [nums[ma], nums[i]];
|
||||
// Loop downwards heapification
|
||||
i = ma;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
/* Heap sort */
|
||||
function heapSort(nums: number[]): void {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) {
|
||||
siftDown(nums, nums.length, i);
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for (let i = nums.length - 1; i > 0; i--) {
|
||||
// Delete node
|
||||
[nums[0], nums[i]] = [nums[i], nums[0]];
|
||||
// Start heapifying the root node, from top to bottom
|
||||
siftDown(nums, i, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="heap_sort.dart"
|
||||
[class]{}-[func]{siftDown}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
void siftDown(List<int> nums, int n, int i) {
|
||||
while (true) {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
int l = 2 * i + 1;
|
||||
int r = 2 * i + 2;
|
||||
int ma = i;
|
||||
if (l < n && nums[l] > nums[ma]) ma = l;
|
||||
if (r < n && nums[r] > nums[ma]) ma = r;
|
||||
// Swap two nodes
|
||||
if (ma == i) break;
|
||||
// Swap two nodes
|
||||
int temp = nums[i];
|
||||
nums[i] = nums[ma];
|
||||
nums[ma] = temp;
|
||||
// Loop downwards heapification
|
||||
i = ma;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
/* Heap sort */
|
||||
void heapSort(List<int> nums) {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for (int i = nums.length ~/ 2 - 1; i >= 0; i--) {
|
||||
siftDown(nums, nums.length, i);
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for (int i = nums.length - 1; i > 0; i--) {
|
||||
// Delete node
|
||||
int tmp = nums[0];
|
||||
nums[0] = nums[i];
|
||||
nums[i] = tmp;
|
||||
// Start heapifying the root node, from top to bottom
|
||||
siftDown(nums, i, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="heap_sort.rs"
|
||||
[class]{}-[func]{sift_down}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
fn sift_down(nums: &mut [i32], n: usize, mut i: usize) {
|
||||
loop {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
let l = 2 * i + 1;
|
||||
let r = 2 * i + 2;
|
||||
let mut ma = i;
|
||||
if l < n && nums[l] > nums[ma] {
|
||||
ma = l;
|
||||
}
|
||||
if r < n && nums[r] > nums[ma] {
|
||||
ma = r;
|
||||
}
|
||||
// Swap two nodes
|
||||
if ma == i {
|
||||
break;
|
||||
}
|
||||
// Swap two nodes
|
||||
nums.swap(i, ma);
|
||||
// Loop downwards heapification
|
||||
i = ma;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{heap_sort}
|
||||
/* Heap sort */
|
||||
fn heap_sort(nums: &mut [i32]) {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for i in (0..nums.len() / 2).rev() {
|
||||
sift_down(nums, nums.len(), i);
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for i in (1..nums.len()).rev() {
|
||||
// Delete node
|
||||
nums.swap(0, i);
|
||||
// Start heapifying the root node, from top to bottom
|
||||
sift_down(nums, i, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="heap_sort.c"
|
||||
[class]{}-[func]{siftDown}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
void siftDown(int nums[], int n, int i) {
|
||||
while (1) {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
int l = 2 * i + 1;
|
||||
int r = 2 * i + 2;
|
||||
int ma = i;
|
||||
if (l < n && nums[l] > nums[ma])
|
||||
ma = l;
|
||||
if (r < n && nums[r] > nums[ma])
|
||||
ma = r;
|
||||
// Swap two nodes
|
||||
if (ma == i) {
|
||||
break;
|
||||
}
|
||||
// Swap two nodes
|
||||
int temp = nums[i];
|
||||
nums[i] = nums[ma];
|
||||
nums[ma] = temp;
|
||||
// Loop downwards heapification
|
||||
i = ma;
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
/* Heap sort */
|
||||
void heapSort(int nums[], int n) {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for (int i = n / 2 - 1; i >= 0; --i) {
|
||||
siftDown(nums, n, i);
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for (int i = n - 1; i > 0; --i) {
|
||||
// Delete node
|
||||
int tmp = nums[0];
|
||||
nums[0] = nums[i];
|
||||
nums[i] = tmp;
|
||||
// Start heapifying the root node, from top to bottom
|
||||
siftDown(nums, i, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="heap_sort.kt"
|
||||
[class]{}-[func]{siftDown}
|
||||
/* Heap length is n, start heapifying node i, from top to bottom */
|
||||
fun siftDown(nums: IntArray, n: Int, li: Int) {
|
||||
var i = li
|
||||
while (true) {
|
||||
// If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
val l = 2 * i + 1
|
||||
val r = 2 * i + 2
|
||||
var ma = i
|
||||
if (l < n && nums[l] > nums[ma])
|
||||
ma = l
|
||||
if (r < n && nums[r] > nums[ma])
|
||||
ma = r
|
||||
// Swap two nodes
|
||||
if (ma == i)
|
||||
break
|
||||
// Swap two nodes
|
||||
val temp = nums[i]
|
||||
nums[i] = nums[ma]
|
||||
nums[ma] = temp
|
||||
// Loop downwards heapification
|
||||
i = ma
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
/* Heap sort */
|
||||
fun heapSort(nums: IntArray) {
|
||||
// Build heap operation: heapify all nodes except leaves
|
||||
for (i in nums.size / 2 - 1 downTo 0) {
|
||||
siftDown(nums, nums.size, i)
|
||||
}
|
||||
// Extract the largest element from the heap and repeat for n-1 rounds
|
||||
for (i in nums.size - 1 downTo 1) {
|
||||
// Delete node
|
||||
val temp = nums[0]
|
||||
nums[0] = nums[i]
|
||||
nums[i] = temp
|
||||
// Start heapifying the root node, from top to bottom
|
||||
siftDown(nums, i, 0)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="heap_sort.rb"
|
||||
[class]{}-[func]{sift_down}
|
||||
### Heap length is n, heapify from node i, top to bottom ###
|
||||
def sift_down(nums, n, i)
|
||||
while true
|
||||
# If node i is largest or indices l, r are out of bounds, no need to continue heapify, break
|
||||
l = 2 * i + 1
|
||||
r = 2 * i + 2
|
||||
ma = i
|
||||
ma = l if l < n && nums[l] > nums[ma]
|
||||
ma = r if r < n && nums[r] > nums[ma]
|
||||
# Swap two nodes
|
||||
break if ma == i
|
||||
# Swap two nodes
|
||||
nums[i], nums[ma] = nums[ma], nums[i]
|
||||
# Loop downwards heapification
|
||||
i = ma
|
||||
end
|
||||
end
|
||||
|
||||
[class]{}-[func]{heap_sort}
|
||||
### Heap sort ###
|
||||
def heap_sort(nums)
|
||||
# Build heap operation: heapify all nodes except leaves
|
||||
(nums.length / 2 - 1).downto(0) do |i|
|
||||
sift_down(nums, nums.length, i)
|
||||
end
|
||||
# Extract the largest element from the heap and repeat for n-1 rounds
|
||||
(nums.length - 1).downto(1) do |i|
|
||||
# Delete node
|
||||
nums[0], nums[i] = nums[i], nums[0]
|
||||
# Start heapifying the root node, from top to bottom
|
||||
sift_down(nums, i, 0)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 11.7.2 Algorithm Characteristics
|
||||
|
||||
```zig title="heap_sort.zig"
|
||||
[class]{}-[func]{siftDown}
|
||||
|
||||
[class]{}-[func]{heapSort}
|
||||
```
|
||||
|
||||
## 11.7.2 Algorithm characteristics
|
||||
|
||||
- **Time complexity is $O(n \log n)$, non-adaptive sort**: The heap creation uses $O(n)$ time. Extracting the largest element from the heap takes $O(\log n)$ time, looping for $n - 1$ rounds.
|
||||
- **Space complexity is $O(1)$, in-place sort**: A few pointer variables use $O(1)$ space. The element swapping and heapifying operations are performed on the original array.
|
||||
- **Non-stable sort**: The relative positions of equal elements may change during the swapping of the heap's top and bottom elements.
|
||||
- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The build heap operation uses $O(n)$ time. Extracting the largest element from the heap has a time complexity of $O(\log n)$, looping a total of $n - 1$ rounds.
|
||||
- **Space complexity of $O(1)$, in-place sorting**: A few pointer variables use $O(1)$ space. Element swapping and heapify operations are both performed on the original array.
|
||||
- **Non-stable sorting**: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change.
|
||||
|
||||
@@ -9,20 +9,20 @@ icon: material/sort-ascending
|
||||
|
||||
!!! abstract
|
||||
|
||||
Sorting is like a magical key that turns chaos into order, enabling us to understand and handle data more efficiently.
|
||||
Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently.
|
||||
|
||||
Whether it's simple ascending order or complex categorical arrangements, sorting reveals the harmonious beauty of data.
|
||||
Whether it's simple ascending order or complex categorized arrangements, sorting demonstrates the harmonious beauty of data.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [11.1 Sorting algorithms](sorting_algorithm.md)
|
||||
- [11.2 Selection sort](selection_sort.md)
|
||||
- [11.3 Bubble sort](bubble_sort.md)
|
||||
- [11.4 Insertion sort](insertion_sort.md)
|
||||
- [11.5 Quick sort](quick_sort.md)
|
||||
- [11.6 Merge sort](merge_sort.md)
|
||||
- [11.7 Heap sort](heap_sort.md)
|
||||
- [11.8 Bucket sort](bucket_sort.md)
|
||||
- [11.9 Counting sort](counting_sort.md)
|
||||
- [11.10 Radix sort](radix_sort.md)
|
||||
- [11.1 Sorting Algorithms](sorting_algorithm.md)
|
||||
- [11.2 Selection Sort](selection_sort.md)
|
||||
- [11.3 Bubble Sort](bubble_sort.md)
|
||||
- [11.4 Insertion Sort](insertion_sort.md)
|
||||
- [11.5 Quick Sort](quick_sort.md)
|
||||
- [11.6 Merge Sort](merge_sort.md)
|
||||
- [11.7 Heap Sort](heap_sort.md)
|
||||
- [11.8 Bucket Sort](bucket_sort.md)
|
||||
- [11.9 Counting Sort](counting_sort.md)
|
||||
- [11.10 Radix Sort](radix_sort.md)
|
||||
- [11.11 Summary](summary.md)
|
||||
|
||||
@@ -2,30 +2,30 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.4 Insertion sort
|
||||
# 11.4 Insertion Sort
|
||||
|
||||
<u>Insertion sort</u> is a simple sorting algorithm that works very much like the process of manually sorting a deck of cards.
|
||||
<u>Insertion sort (insertion sort)</u> is a simple sorting algorithm that works very similarly to the process of manually organizing a deck of cards.
|
||||
|
||||
Specifically, we select a base element from the unsorted interval, compare it with the elements in the sorted interval to its left, and insert the element into the correct position.
|
||||
Specifically, we select a base element from the unsorted interval, compare the element with elements in the sorted interval to its left one by one, and insert the element into the correct position.
|
||||
|
||||
Figure 11-6 illustrates how an element is inserted into the array. Assuming the base element is `base`, we need to shift all elements from the target index up to `base` one position to the right, then assign `base` to the target index.
|
||||
Figure 11-6 shows the operation flow of inserting an element into the array. Let the base element be `base`. We need to move all elements from the target index to `base` one position to the right, and then assign `base` to the target index.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-6 Single insertion operation </p>
|
||||
|
||||
## 11.4.1 Algorithm process
|
||||
## 11.4.1 Algorithm Flow
|
||||
|
||||
The overall process of insertion sort is shown in Figure 11-7.
|
||||
The overall flow of insertion sort is shown in Figure 11-7.
|
||||
|
||||
1. Consider the first element of the array as sorted.
|
||||
2. Select the second element as `base`, insert it into its correct position, **leaving the first two elements sorted**.
|
||||
3. Select the third element as `base`, insert it into its correct position, **leaving the first three elements sorted**.
|
||||
4. Continuing in this manner, in the final iteration, the last element is taken as `base`, and after inserting it into the correct position, **all elements are sorted**.
|
||||
1. Initially, the first element of the array has completed sorting.
|
||||
2. Select the second element of the array as `base`, and after inserting it into the correct position, **the first 2 elements of the array are sorted**.
|
||||
3. Select the third element as `base`, and after inserting it into the correct position, **the first 3 elements of the array are sorted**.
|
||||
4. And so on. In the last round, select the last element as `base`, and after inserting it into the correct position, **all elements are sorted**.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-7 Insertion sort process </p>
|
||||
<p align="center"> Figure 11-7 Insertion sort flow </p>
|
||||
|
||||
Example code is as follows:
|
||||
|
||||
@@ -34,11 +34,11 @@ Example code is as follows:
|
||||
```python title="insertion_sort.py"
|
||||
def insertion_sort(nums: list[int]):
|
||||
"""Insertion sort"""
|
||||
# Outer loop: sorted range is [0, i-1]
|
||||
# Outer loop: sorted interval is [0, i-1]
|
||||
for i in range(1, len(nums)):
|
||||
base = nums[i]
|
||||
j = i - 1
|
||||
# Inner loop: insert base into the correct position within the sorted range [0, i-1]
|
||||
# Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while j >= 0 and nums[j] > base:
|
||||
nums[j + 1] = nums[j] # Move nums[j] to the right by one position
|
||||
j -= 1
|
||||
@@ -50,10 +50,10 @@ Example code is as follows:
|
||||
```cpp title="insertion_sort.cpp"
|
||||
/* Insertion sort */
|
||||
void insertionSort(vector<int> &nums) {
|
||||
// Outer loop: sorted range is [0, i-1]
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for (int i = 1; i < nums.size(); i++) {
|
||||
int base = nums[i], j = i - 1;
|
||||
// Inner loop: insert base into the correct position within the sorted range [0, i-1]
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
|
||||
j--;
|
||||
@@ -68,10 +68,10 @@ Example code is as follows:
|
||||
```java title="insertion_sort.java"
|
||||
/* Insertion sort */
|
||||
void insertionSort(int[] nums) {
|
||||
// Outer loop: sorted range is [0, i-1]
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for (int i = 1; i < nums.length; i++) {
|
||||
int base = nums[i], j = i - 1;
|
||||
// Inner loop: insert base into the correct position within the sorted range [0, i-1]
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
|
||||
j--;
|
||||
@@ -84,85 +84,208 @@ Example code is as follows:
|
||||
=== "C#"
|
||||
|
||||
```csharp title="insertion_sort.cs"
|
||||
[class]{insertion_sort}-[func]{InsertionSort}
|
||||
/* Insertion sort */
|
||||
void InsertionSort(int[] nums) {
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for (int i = 1; i < nums.Length; i++) {
|
||||
int bas = nums[i], j = i - 1;
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while (j >= 0 && nums[j] > bas) {
|
||||
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = bas; // Assign base to the correct position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="insertion_sort.go"
|
||||
[class]{}-[func]{insertionSort}
|
||||
/* Insertion sort */
|
||||
func insertionSort(nums []int) {
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for i := 1; i < len(nums); i++ {
|
||||
base := nums[i]
|
||||
j := i - 1
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
for j >= 0 && nums[j] > base {
|
||||
nums[j+1] = nums[j] // Move nums[j] to the right by one position
|
||||
j--
|
||||
}
|
||||
nums[j+1] = base // Assign base to the correct position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="insertion_sort.swift"
|
||||
[class]{}-[func]{insertionSort}
|
||||
/* Insertion sort */
|
||||
func insertionSort(nums: inout [Int]) {
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for i in nums.indices.dropFirst() {
|
||||
let base = nums[i]
|
||||
var j = i - 1
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while j >= 0, nums[j] > base {
|
||||
nums[j + 1] = nums[j] // Move nums[j] to the right by one position
|
||||
j -= 1
|
||||
}
|
||||
nums[j + 1] = base // Assign base to the correct position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="insertion_sort.js"
|
||||
[class]{}-[func]{insertionSort}
|
||||
/* Insertion sort */
|
||||
function insertionSort(nums) {
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
let base = nums[i],
|
||||
j = i - 1;
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = base; // Assign base to the correct position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="insertion_sort.ts"
|
||||
[class]{}-[func]{insertionSort}
|
||||
/* Insertion sort */
|
||||
function insertionSort(nums: number[]): void {
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for (let i = 1; i < nums.length; i++) {
|
||||
const base = nums[i];
|
||||
let j = i - 1;
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = base; // Assign base to the correct position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="insertion_sort.dart"
|
||||
[class]{}-[func]{insertionSort}
|
||||
/* Insertion sort */
|
||||
void insertionSort(List<int> nums) {
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for (int i = 1; i < nums.length; i++) {
|
||||
int base = nums[i], j = i - 1;
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j]; // Move nums[j] to the right by one position
|
||||
j--;
|
||||
}
|
||||
nums[j + 1] = base; // Assign base to the correct position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="insertion_sort.rs"
|
||||
[class]{}-[func]{insertion_sort}
|
||||
/* Insertion sort */
|
||||
fn insertion_sort(nums: &mut [i32]) {
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for i in 1..nums.len() {
|
||||
let (base, mut j) = (nums[i], (i - 1) as i32);
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while j >= 0 && nums[j as usize] > base {
|
||||
nums[(j + 1) as usize] = nums[j as usize]; // Move nums[j] to the right by one position
|
||||
j -= 1;
|
||||
}
|
||||
nums[(j + 1) as usize] = base; // Assign base to the correct position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="insertion_sort.c"
|
||||
[class]{}-[func]{insertionSort}
|
||||
/* Insertion sort */
|
||||
void insertionSort(int nums[], int size) {
|
||||
// Outer loop: sorted interval is [0, i-1]
|
||||
for (int i = 1; i < size; i++) {
|
||||
int base = nums[i], j = i - 1;
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
// Move nums[j] to the right by one position
|
||||
nums[j + 1] = nums[j];
|
||||
j--;
|
||||
}
|
||||
// Assign base to the correct position
|
||||
nums[j + 1] = base;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="insertion_sort.kt"
|
||||
[class]{}-[func]{insertionSort}
|
||||
/* Insertion sort */
|
||||
fun insertionSort(nums: IntArray) {
|
||||
// Outer loop: sorted elements are 1, 2, ..., n
|
||||
for (i in nums.indices) {
|
||||
val base = nums[i]
|
||||
var j = i - 1
|
||||
// Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while (j >= 0 && nums[j] > base) {
|
||||
nums[j + 1] = nums[j] // Move nums[j] to the right by one position
|
||||
j--
|
||||
}
|
||||
nums[j + 1] = base // Assign base to the correct position
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="insertion_sort.rb"
|
||||
[class]{}-[func]{insertion_sort}
|
||||
### Insertion sort ###
|
||||
def insertion_sort(nums)
|
||||
n = nums.length
|
||||
# Outer loop: sorted interval is [0, i-1]
|
||||
for i in 1...n
|
||||
base = nums[i]
|
||||
j = i - 1
|
||||
# Inner loop: insert base into the correct position within the sorted interval [0, i-1]
|
||||
while j >= 0 && nums[j] > base
|
||||
nums[j + 1] = nums[j] # Move nums[j] to the right by one position
|
||||
j -= 1
|
||||
end
|
||||
nums[j + 1] = base # Assign base to the correct position
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 11.4.2 Algorithm Characteristics
|
||||
|
||||
```zig title="insertion_sort.zig"
|
||||
[class]{}-[func]{insertionSort}
|
||||
```
|
||||
- **Time complexity of $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires loops of $n - 1$, $n-2$, $\dots$, $2$, $1$, summing to $(n - 1) n / 2$, so the time complexity is $O(n^2)$. When encountering ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best-case time complexity of $O(n)$.
|
||||
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
|
||||
- **Stable sorting**: During the insertion operation process, we insert elements to the right of equal elements, without changing their order.
|
||||
|
||||
## 11.4.2 Algorithm characteristics
|
||||
## 11.4.3 Advantages of Insertion Sort
|
||||
|
||||
- **Time complexity is $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires $n - 1$, $n-2$, ..., $2$, $1$ loops, summing up to $(n - 1) n / 2$, thus the time complexity is $O(n^2)$. In the case of ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best time complexity of $O(n)$.
|
||||
- **Space complexity is $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
|
||||
- **Stable sorting**: During the insertion operation, we insert elements to the right of equal elements, not changing their order.
|
||||
The time complexity of insertion sort is $O(n^2)$, while the time complexity of quick sort, which we will learn about next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **insertion sort is usually faster for smaller data volumes**.
|
||||
|
||||
## 11.4.3 Advantages of insertion sort
|
||||
This conclusion is similar to the applicable situations of linear search and binary search. Algorithms like quick sort with $O(n \log n)$ complexity are sorting algorithms based on divide-and-conquer strategy and often contain more unit computation operations. When the data volume is small, $n^2$ and $n \log n$ are numerically close, and complexity does not dominate; the number of unit operations per round plays a decisive role.
|
||||
|
||||
The time complexity of insertion sort is $O(n^2)$, while the time complexity of quicksort, which we will study next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **it is usually faster in small input sizes**.
|
||||
In fact, the built-in sorting functions in many programming languages (such as Java) adopt insertion sort. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategy, such as quick sort; for short arrays, directly use insertion sort.
|
||||
|
||||
This conclusion is similar to that for linear and binary search. Algorithms like quicksort that have a time complexity of $O(n \log n)$ and are based on the divide-and-conquer strategy often involve more unit operations. For small input sizes, the numerical values of $n^2$ and $n \log n$ are close, and complexity does not dominate, with the number of unit operations per round playing a decisive role.
|
||||
Although bubble sort, selection sort, and insertion sort all have a time complexity of $O(n^2)$, in actual situations, **insertion sort is used significantly more frequently than bubble sort and selection sort**, mainly for the following reasons.
|
||||
|
||||
In fact, many programming languages (such as Java) use insertion sort within their built-in sorting functions. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategies, such as quicksort; for short arrays, use insertion sort directly.
|
||||
|
||||
Although bubble sort, selection sort, and insertion sort all have a time complexity of $O(n^2)$, in practice, **insertion sort is commonly used than bubble sort and selection sort**, mainly for the following reasons.
|
||||
|
||||
- Bubble sort is based on element swapping, which requires the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, **the computational overhead of bubble sort is generally higher than that of insertion sort**.
|
||||
- The time complexity of selection sort is always $O(n^2)$. **Given a set of partially ordered data, insertion sort is usually more efficient than selection sort**.
|
||||
- Bubble sort is based on element swapping, requiring the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, **the computational overhead of bubble sort is usually higher than that of insertion sort**.
|
||||
- Selection sort has a time complexity of $O(n^2)$ in any case. **If given a set of partially ordered data, insertion sort is usually more efficient than selection sort**.
|
||||
- Selection sort is unstable and cannot be applied to multi-level sorting.
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.6 Merge sort
|
||||
# 11.6 Merge Sort
|
||||
|
||||
<u>Merge sort</u> is a sorting algorithm based on the divide-and-conquer strategy, involving the "divide" and "merge" phases shown in Figure 11-10.
|
||||
<u>Merge sort (merge sort)</u> is a sorting algorithm based on the divide-and-conquer strategy, which includes the "divide" and "merge" phases shown in Figure 11-10.
|
||||
|
||||
1. **Divide phase**: Recursively split the array from the midpoint, transforming the sorting problem of a long array into shorter arrays.
|
||||
2. **Merge phase**: Stop dividing when the length of the sub-array is 1, and then begin merging. The two shorter sorted arrays are continuously merged into a longer sorted array until the process is complete.
|
||||
1. **Divide phase**: Recursively split the array from the midpoint, transforming the sorting problem of a long array into the sorting problems of shorter arrays.
|
||||
2. **Merge phase**: When the sub-array length is 1, terminate the division and start merging, continuously merging two shorter sorted arrays into one longer sorted array until the process is complete.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-10 The divide and merge phases of merge sort </p>
|
||||
<p align="center"> Figure 11-10 Divide and merge phases of merge sort </p>
|
||||
|
||||
## 11.6.1 Algorithm workflow
|
||||
## 11.6.1 Algorithm Flow
|
||||
|
||||
As shown in Figure 11-11, the "divide phase" recursively splits the array from the midpoint into two sub-arrays from top to bottom.
|
||||
|
||||
1. Calculate the midpoint `mid`, recursively divide the left sub-array (interval `[left, mid]`) and the right sub-array (interval `[mid + 1, right]`).
|
||||
2. Continue with step `1.` recursively until sub-array length becomes 1, then stops.
|
||||
1. Calculate the array midpoint `mid`, recursively divide the left sub-array (interval `[left, mid]`) and right sub-array (interval `[mid + 1, right]`).
|
||||
2. Recursively execute step `1.` until the sub-array interval length is 1, then terminate.
|
||||
|
||||
The "merge phase" combines the left and right sub-arrays into a sorted array from bottom to top. It is important to note that, merging starts with sub-arrays of length 1, and each sub-array is sorted during the merge phase.
|
||||
The "merge phase" merges the left sub-array and right sub-array into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, and each sub-array in the merge phase is sorted.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -52,14 +52,14 @@ The "merge phase" combines the left and right sub-arrays into a sorted array fro
|
||||
=== "<10>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-11 Merge sort process </p>
|
||||
<p align="center"> Figure 11-11 Merge sort steps </p>
|
||||
|
||||
It can be observed that the order of recursion in merge sort is consistent with the post-order traversal of a binary tree.
|
||||
It can be observed that the recursive order of merge sort is consistent with the post-order traversal of a binary tree.
|
||||
|
||||
- **Post-order traversal**: First recursively traverse the left subtree, then the right subtree, and finally process the root node.
|
||||
- **Merge sort**: First recursively process the left sub-array, then the right sub-array, and finally perform the merge.
|
||||
- **Post-order traversal**: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node.
|
||||
- **Merge sort**: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge.
|
||||
|
||||
The implementation of merge sort is shown in the following code. Note that the interval to be merged in `nums` is `[left, right]`, while the corresponding interval in `tmp` is `[0, right - left]`.
|
||||
The implementation of merge sort is shown in the code below. Note that the interval to be merged in `nums` is `[left, right]`, while the corresponding interval in `tmp` is `[0, right - left]`.
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -98,8 +98,8 @@ The implementation of merge sort is shown in the following code. Note that the i
|
||||
# Termination condition
|
||||
if left >= right:
|
||||
return # Terminate recursion when subarray length is 1
|
||||
# Partition stage
|
||||
mid = left + (right - left) // 2 # Calculate midpoint
|
||||
# Divide and conquer stage
|
||||
mid = (left + right) // 2 # Calculate midpoint
|
||||
merge_sort(nums, left, mid) # Recursively process the left subarray
|
||||
merge_sort(nums, mid + 1, right) # Recursively process the right subarray
|
||||
# Merge stage
|
||||
@@ -141,7 +141,7 @@ The implementation of merge sort is shown in the following code. Note that the i
|
||||
// Termination condition
|
||||
if (left >= right)
|
||||
return; // Terminate recursion when subarray length is 1
|
||||
// Partition stage
|
||||
// Divide and conquer stage
|
||||
int mid = left + (right - left) / 2; // Calculate midpoint
|
||||
mergeSort(nums, left, mid); // Recursively process the left subarray
|
||||
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
|
||||
@@ -185,7 +185,7 @@ The implementation of merge sort is shown in the following code. Note that the i
|
||||
// Termination condition
|
||||
if (left >= right)
|
||||
return; // Terminate recursion when subarray length is 1
|
||||
// Partition stage
|
||||
// Divide and conquer stage
|
||||
int mid = left + (right - left) / 2; // Calculate midpoint
|
||||
mergeSort(nums, left, mid); // Recursively process the left subarray
|
||||
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
|
||||
@@ -197,102 +197,499 @@ The implementation of merge sort is shown in the following code. Note that the i
|
||||
=== "C#"
|
||||
|
||||
```csharp title="merge_sort.cs"
|
||||
[class]{merge_sort}-[func]{Merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
void Merge(int[] nums, int left, int mid, int right) {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
int[] tmp = new int[right - left + 1];
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
int i = left, j = mid + 1, k = 0;
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while (i <= mid && j <= right) {
|
||||
if (nums[i] <= nums[j])
|
||||
tmp[k++] = nums[i++];
|
||||
else
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while (i <= mid) {
|
||||
tmp[k++] = nums[i++];
|
||||
}
|
||||
while (j <= right) {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for (k = 0; k < tmp.Length; ++k) {
|
||||
nums[left + k] = tmp[k];
|
||||
}
|
||||
}
|
||||
|
||||
[class]{merge_sort}-[func]{MergeSort}
|
||||
/* Merge sort */
|
||||
void MergeSort(int[] nums, int left, int right) {
|
||||
// Termination condition
|
||||
if (left >= right) return; // Terminate recursion when subarray length is 1
|
||||
// Divide and conquer stage
|
||||
int mid = left + (right - left) / 2; // Calculate midpoint
|
||||
MergeSort(nums, left, mid); // Recursively process the left subarray
|
||||
MergeSort(nums, mid + 1, right); // Recursively process the right subarray
|
||||
// Merge stage
|
||||
Merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="merge_sort.go"
|
||||
[class]{}-[func]{merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
func merge(nums []int, left, mid, right int) {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
tmp := make([]int, right-left+1)
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
i, j, k := left, mid+1, 0
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
for i <= mid && j <= right {
|
||||
if nums[i] <= nums[j] {
|
||||
tmp[k] = nums[i]
|
||||
i++
|
||||
} else {
|
||||
tmp[k] = nums[j]
|
||||
j++
|
||||
}
|
||||
k++
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
for i <= mid {
|
||||
tmp[k] = nums[i]
|
||||
i++
|
||||
k++
|
||||
}
|
||||
for j <= right {
|
||||
tmp[k] = nums[j]
|
||||
j++
|
||||
k++
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for k := 0; k < len(tmp); k++ {
|
||||
nums[left+k] = tmp[k]
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
/* Merge sort */
|
||||
func mergeSort(nums []int, left, right int) {
|
||||
// Termination condition
|
||||
if left >= right {
|
||||
return
|
||||
}
|
||||
// Divide and conquer stage
|
||||
mid := left + (right - left) / 2
|
||||
mergeSort(nums, left, mid)
|
||||
mergeSort(nums, mid+1, right)
|
||||
// Merge stage
|
||||
merge(nums, left, mid, right)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="merge_sort.swift"
|
||||
[class]{}-[func]{merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
func merge(nums: inout [Int], left: Int, mid: Int, right: Int) {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
var tmp = Array(repeating: 0, count: right - left + 1)
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
var i = left, j = mid + 1, k = 0
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while i <= mid, j <= right {
|
||||
if nums[i] <= nums[j] {
|
||||
tmp[k] = nums[i]
|
||||
i += 1
|
||||
} else {
|
||||
tmp[k] = nums[j]
|
||||
j += 1
|
||||
}
|
||||
k += 1
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while i <= mid {
|
||||
tmp[k] = nums[i]
|
||||
i += 1
|
||||
k += 1
|
||||
}
|
||||
while j <= right {
|
||||
tmp[k] = nums[j]
|
||||
j += 1
|
||||
k += 1
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for k in tmp.indices {
|
||||
nums[left + k] = tmp[k]
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
/* Merge sort */
|
||||
func mergeSort(nums: inout [Int], left: Int, right: Int) {
|
||||
// Termination condition
|
||||
if left >= right { // Terminate recursion when subarray length is 1
|
||||
return
|
||||
}
|
||||
// Divide and conquer stage
|
||||
let mid = left + (right - left) / 2 // Calculate midpoint
|
||||
mergeSort(nums: &nums, left: left, right: mid) // Recursively process the left subarray
|
||||
mergeSort(nums: &nums, left: mid + 1, right: right) // Recursively process the right subarray
|
||||
// Merge stage
|
||||
merge(nums: &nums, left: left, mid: mid, right: right)
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="merge_sort.js"
|
||||
[class]{}-[func]{merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
function merge(nums, left, mid, right) {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
const tmp = new Array(right - left + 1);
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
let i = left,
|
||||
j = mid + 1,
|
||||
k = 0;
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while (i <= mid && j <= right) {
|
||||
if (nums[i] <= nums[j]) {
|
||||
tmp[k++] = nums[i++];
|
||||
} else {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while (i <= mid) {
|
||||
tmp[k++] = nums[i++];
|
||||
}
|
||||
while (j <= right) {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for (k = 0; k < tmp.length; k++) {
|
||||
nums[left + k] = tmp[k];
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
/* Merge sort */
|
||||
function mergeSort(nums, left, right) {
|
||||
// Termination condition
|
||||
if (left >= right) return; // Terminate recursion when subarray length is 1
|
||||
// Divide and conquer stage
|
||||
let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint
|
||||
mergeSort(nums, left, mid); // Recursively process the left subarray
|
||||
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
|
||||
// Merge stage
|
||||
merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="merge_sort.ts"
|
||||
[class]{}-[func]{merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
function merge(nums: number[], left: number, mid: number, right: number): void {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
const tmp = new Array(right - left + 1);
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
let i = left,
|
||||
j = mid + 1,
|
||||
k = 0;
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while (i <= mid && j <= right) {
|
||||
if (nums[i] <= nums[j]) {
|
||||
tmp[k++] = nums[i++];
|
||||
} else {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while (i <= mid) {
|
||||
tmp[k++] = nums[i++];
|
||||
}
|
||||
while (j <= right) {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for (k = 0; k < tmp.length; k++) {
|
||||
nums[left + k] = tmp[k];
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
/* Merge sort */
|
||||
function mergeSort(nums: number[], left: number, right: number): void {
|
||||
// Termination condition
|
||||
if (left >= right) return; // Terminate recursion when subarray length is 1
|
||||
// Divide and conquer stage
|
||||
let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint
|
||||
mergeSort(nums, left, mid); // Recursively process the left subarray
|
||||
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
|
||||
// Merge stage
|
||||
merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="merge_sort.dart"
|
||||
[class]{}-[func]{merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
void merge(List<int> nums, int left, int mid, int right) {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
List<int> tmp = List.filled(right - left + 1, 0);
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
int i = left, j = mid + 1, k = 0;
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while (i <= mid && j <= right) {
|
||||
if (nums[i] <= nums[j])
|
||||
tmp[k++] = nums[i++];
|
||||
else
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while (i <= mid) {
|
||||
tmp[k++] = nums[i++];
|
||||
}
|
||||
while (j <= right) {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for (k = 0; k < tmp.length; k++) {
|
||||
nums[left + k] = tmp[k];
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
/* Merge sort */
|
||||
void mergeSort(List<int> nums, int left, int right) {
|
||||
// Termination condition
|
||||
if (left >= right) return; // Terminate recursion when subarray length is 1
|
||||
// Divide and conquer stage
|
||||
int mid = left + (right - left) ~/ 2; // Calculate midpoint
|
||||
mergeSort(nums, left, mid); // Recursively process the left subarray
|
||||
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
|
||||
// Merge stage
|
||||
merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="merge_sort.rs"
|
||||
[class]{}-[func]{merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
let tmp_size = right - left + 1;
|
||||
let mut tmp = vec![0; tmp_size];
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
let (mut i, mut j, mut k) = (left, mid + 1, 0);
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while i <= mid && j <= right {
|
||||
if nums[i] <= nums[j] {
|
||||
tmp[k] = nums[i];
|
||||
i += 1;
|
||||
} else {
|
||||
tmp[k] = nums[j];
|
||||
j += 1;
|
||||
}
|
||||
k += 1;
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while i <= mid {
|
||||
tmp[k] = nums[i];
|
||||
k += 1;
|
||||
i += 1;
|
||||
}
|
||||
while j <= right {
|
||||
tmp[k] = nums[j];
|
||||
k += 1;
|
||||
j += 1;
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for k in 0..tmp_size {
|
||||
nums[left + k] = tmp[k];
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{merge_sort}
|
||||
/* Merge sort */
|
||||
fn merge_sort(nums: &mut [i32], left: usize, right: usize) {
|
||||
// Termination condition
|
||||
if left >= right {
|
||||
return; // Terminate recursion when subarray length is 1
|
||||
}
|
||||
|
||||
// Divide and conquer stage
|
||||
let mid = left + (right - left) / 2; // Calculate midpoint
|
||||
merge_sort(nums, left, mid); // Recursively process the left subarray
|
||||
merge_sort(nums, mid + 1, right); // Recursively process the right subarray
|
||||
|
||||
// Merge stage
|
||||
merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="merge_sort.c"
|
||||
[class]{}-[func]{merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
void merge(int *nums, int left, int mid, int right) {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
int tmpSize = right - left + 1;
|
||||
int *tmp = (int *)malloc(tmpSize * sizeof(int));
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
int i = left, j = mid + 1, k = 0;
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while (i <= mid && j <= right) {
|
||||
if (nums[i] <= nums[j]) {
|
||||
tmp[k++] = nums[i++];
|
||||
} else {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while (i <= mid) {
|
||||
tmp[k++] = nums[i++];
|
||||
}
|
||||
while (j <= right) {
|
||||
tmp[k++] = nums[j++];
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for (k = 0; k < tmpSize; ++k) {
|
||||
nums[left + k] = tmp[k];
|
||||
}
|
||||
// Free memory
|
||||
free(tmp);
|
||||
}
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
/* Merge sort */
|
||||
void mergeSort(int *nums, int left, int right) {
|
||||
// Termination condition
|
||||
if (left >= right)
|
||||
return; // Terminate recursion when subarray length is 1
|
||||
// Divide and conquer stage
|
||||
int mid = left + (right - left) / 2; // Calculate midpoint
|
||||
mergeSort(nums, left, mid); // Recursively process the left subarray
|
||||
mergeSort(nums, mid + 1, right); // Recursively process the right subarray
|
||||
// Merge stage
|
||||
merge(nums, left, mid, right);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="merge_sort.kt"
|
||||
[class]{}-[func]{merge}
|
||||
/* Merge left subarray and right subarray */
|
||||
fun merge(nums: IntArray, left: Int, mid: Int, right: Int) {
|
||||
// Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
// Create a temporary array tmp to store the merged results
|
||||
val tmp = IntArray(right - left + 1)
|
||||
// Initialize the start indices of the left and right subarrays
|
||||
var i = left
|
||||
var j = mid + 1
|
||||
var k = 0
|
||||
// While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while (i <= mid && j <= right) {
|
||||
if (nums[i] <= nums[j])
|
||||
tmp[k++] = nums[i++]
|
||||
else
|
||||
tmp[k++] = nums[j++]
|
||||
}
|
||||
// Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while (i <= mid) {
|
||||
tmp[k++] = nums[i++]
|
||||
}
|
||||
while (j <= right) {
|
||||
tmp[k++] = nums[j++]
|
||||
}
|
||||
// Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
for (l in tmp.indices) {
|
||||
nums[left + l] = tmp[l]
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
/* Merge sort */
|
||||
fun mergeSort(nums: IntArray, left: Int, right: Int) {
|
||||
// Termination condition
|
||||
if (left >= right) return // Terminate recursion when subarray length is 1
|
||||
// Divide and conquer stage
|
||||
val mid = left + (right - left) / 2 // Calculate midpoint
|
||||
mergeSort(nums, left, mid) // Recursively process the left subarray
|
||||
mergeSort(nums, mid + 1, right) // Recursively process the right subarray
|
||||
// Merge stage
|
||||
merge(nums, left, mid, right)
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="merge_sort.rb"
|
||||
[class]{}-[func]{merge}
|
||||
### Merge left and right subarrays ###
|
||||
def merge(nums, left, mid, right)
|
||||
# Left subarray interval is [left, mid], right subarray interval is [mid+1, right]
|
||||
# Create temporary array tmp to store merged result
|
||||
tmp = Array.new(right - left + 1, 0)
|
||||
# Initialize the start indices of the left and right subarrays
|
||||
i, j, k = left, mid + 1, 0
|
||||
# While both subarrays still have elements, compare and copy the smaller element into the temporary array
|
||||
while i <= mid && j <= right
|
||||
if nums[i] <= nums[j]
|
||||
tmp[k] = nums[i]
|
||||
i += 1
|
||||
else
|
||||
tmp[k] = nums[j]
|
||||
j += 1
|
||||
end
|
||||
k += 1
|
||||
end
|
||||
# Copy the remaining elements of the left and right subarrays into the temporary array
|
||||
while i <= mid
|
||||
tmp[k] = nums[i]
|
||||
i += 1
|
||||
k += 1
|
||||
end
|
||||
while j <= right
|
||||
tmp[k] = nums[j]
|
||||
j += 1
|
||||
k += 1
|
||||
end
|
||||
# Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval
|
||||
(0...tmp.length).each do |k|
|
||||
nums[left + k] = tmp[k]
|
||||
end
|
||||
end
|
||||
|
||||
[class]{}-[func]{merge_sort}
|
||||
### Merge sort ###
|
||||
def merge_sort(nums, left, right)
|
||||
# Termination condition
|
||||
# Terminate recursion when subarray length is 1
|
||||
return if left >= right
|
||||
# Divide and conquer stage
|
||||
mid = left + (right - left) / 2 # Calculate midpoint
|
||||
merge_sort(nums, left, mid) # Recursively process the left subarray
|
||||
merge_sort(nums, mid + 1, right) # Recursively process the right subarray
|
||||
# Merge stage
|
||||
merge(nums, left, mid, right)
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 11.6.2 Algorithm Characteristics
|
||||
|
||||
```zig title="merge_sort.zig"
|
||||
[class]{}-[func]{merge}
|
||||
- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The division produces a recursion tree of height $\log n$, and the total number of merge operations at each level is $n$, so the overall time complexity is $O(n \log n)$.
|
||||
- **Space complexity of $O(n)$, non-in-place sorting**: The recursion depth is $\log n$, using $O(\log n)$ size of stack frame space. The merge operation requires the aid of an auxiliary array, using $O(n)$ size of additional space.
|
||||
- **Stable sorting**: In the merge process, the order of equal elements remains unchanged.
|
||||
|
||||
[class]{}-[func]{mergeSort}
|
||||
```
|
||||
## 11.6.3 Linked List Sorting
|
||||
|
||||
## 11.6.2 Algorithm characteristics
|
||||
For linked lists, merge sort has significant advantages over other sorting algorithms, **and can optimize the space complexity of linked list sorting tasks to $O(1)$**.
|
||||
|
||||
- **Time complexity of $O(n \log n)$, non-adaptive sort**: The division creates a recursion tree of height $\log n$, with each layer merging a total of $n$ operations, resulting in an overall time complexity of $O(n \log n)$.
|
||||
- **Space complexity of $O(n)$, non-in-place sort**: The recursion depth is $\log n$, using $O(\log n)$ stack frame space. The merging operation requires auxiliary arrays, using an additional space of $O(n)$.
|
||||
- **Stable sort**: During the merging process, the order of equal elements remains unchanged.
|
||||
- **Divide phase**: "Iteration" can be used instead of "recursion" to implement linked list division work, thus saving the stack frame space used by recursion.
|
||||
- **Merge phase**: In linked lists, node insertion and deletion operations can be achieved by just changing references (pointers), so there is no need to create additional linked lists during the merge phase (merging two short ordered linked lists into one long ordered linked list).
|
||||
|
||||
## 11.6.3 Linked List sorting
|
||||
|
||||
For linked lists, merge sort has significant advantages over other sorting algorithms. **It can optimize the space complexity of the linked list sorting task to $O(1)$**.
|
||||
|
||||
- **Divide phase**: "Iteration" can be used instead of "recursion" to perform the linked list division work, thus saving the stack frame space used by recursion.
|
||||
- **Merge phase**: In linked lists, node insertion and deletion operations can be achieved by changing references (pointers), so no extra lists need to be created during the merge phase (combining two short ordered lists into one long ordered list).
|
||||
|
||||
The implementation details are relatively complex, and interested readers can consult related materials for learning.
|
||||
The specific implementation details are quite complex, and interested readers can consult related materials for learning.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,33 +2,33 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.10 Radix sort
|
||||
# 11.10 Radix Sort
|
||||
|
||||
The previous section introduced counting sort, which is suitable for scenarios where the data size $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, where each ID is an $8$-digit number. This means the data range $m = 10^8$ is very large. Using counting sort in this case would require significant memory space. Radix sort can avoid this situation.
|
||||
The previous section introduced counting sort, which is suitable for situations where the data volume $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, and the student ID is an 8-digit number, which means the data range $m = 10^8$ is very large. Using counting sort would require allocating a large amount of memory space, whereas radix sort can avoid this situation.
|
||||
|
||||
<u>Radix sort</u> shares the same core concept as counting sort, which also sorts by counting the frequency of elements. Meanwhile, radix sort builds upon this by utilizing the progressive relationship between the digits of numbers. It processes and sorts the digits one at a time, achieving the final sorted order.
|
||||
<u>Radix sort (radix sort)</u> has a core idea consistent with counting sort, which also achieves sorting by counting quantities. Building on this, radix sort utilizes the progressive relationship between the digits of numbers, sorting each digit in turn to obtain the final sorting result.
|
||||
|
||||
## 11.10.1 Algorithm process
|
||||
## 11.10.1 Algorithm Flow
|
||||
|
||||
Taking the student ID data as an example, assume the least significant digit is the $1^{st}$ and the most significant is the $8^{th}$, the radix sort process is illustrated in Figure 11-18.
|
||||
Taking student ID data as an example, assume the lowest digit is the $1$st digit and the highest digit is the $8$th digit. The flow of radix sort is shown in Figure 11-18.
|
||||
|
||||
1. Initialize digit $k = 1$.
|
||||
2. Perform "counting sort" on the $k^{th}$ digit of the student IDs. After completion, the data will be sorted from smallest to largest based on the $k^{th}$ digit.
|
||||
3. Increment $k$ by $1$, then return to step `2.` and continue iterating until all digits have been sorted, at which point the process ends.
|
||||
1. Initialize the digit $k = 1$.
|
||||
2. Perform "counting sort" on the $k$th digit of the student IDs. After completion, the data will be sorted from smallest to largest according to the $k$th digit.
|
||||
3. Increase $k$ by $1$, then return to step `2.` and continue iterating until all digits are sorted, at which point the process ends.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-18 Radix sort algorithm process </p>
|
||||
<p align="center"> Figure 11-18 Radix sort algorithm flow </p>
|
||||
|
||||
Below we dissect the code implementation. For a number $x$ in base $d$, to obtain its $k^{th}$ digit $x_k$, the following calculation formula can be used:
|
||||
Below we analyze the code implementation. For a $d$-base number $x$, to get its $k$th digit $x_k$, the following calculation formula can be used:
|
||||
|
||||
$$
|
||||
x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d
|
||||
$$
|
||||
|
||||
Where $\lfloor a \rfloor$ denotes rounding down the floating point number $a$, and $\bmod \: d$ denotes taking the modulus of $d$. For student ID data, $d = 10$ and $k \in [1, 8]$.
|
||||
Where $\lfloor a \rfloor$ denotes rounding down the floating-point number $a$, and $\bmod \: d$ denotes taking the modulo (remainder) with respect to $d$. For student ID data, $d = 10$ and $k \in [1, 8]$.
|
||||
|
||||
Additionally, we need to slightly modify the counting sort code to allow sorting based on the $k^{th}$ digit:
|
||||
Additionally, we need to slightly modify the counting sort code to make it sort based on the $k$th digit of the number:
|
||||
|
||||
=== "Python"
|
||||
|
||||
@@ -183,121 +183,547 @@ Additionally, we need to slightly modify the counting sort code to allow sorting
|
||||
=== "C#"
|
||||
|
||||
```csharp title="radix_sort.cs"
|
||||
[class]{radix_sort}-[func]{Digit}
|
||||
/* Get the k-th digit of element num, where exp = 10^(k-1) */
|
||||
int Digit(int num, int exp) {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
return (num / exp) % 10;
|
||||
}
|
||||
|
||||
[class]{radix_sort}-[func]{CountingSortDigit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
void CountingSortDigit(int[] nums, int exp) {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
int[] counter = new int[10];
|
||||
int n = nums.Length;
|
||||
// Count the occurrence of digits 0~9
|
||||
for (int i = 0; i < n; i++) {
|
||||
int d = Digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
|
||||
counter[d]++; // Count the occurrence of digit d
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for (int i = 1; i < 10; i++) {
|
||||
counter[i] += counter[i - 1];
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
int[] res = new int[n];
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
int d = Digit(nums[i], exp);
|
||||
int j = counter[d] - 1; // Get the index j for d in the array
|
||||
res[j] = nums[i]; // Place the current element at index j
|
||||
counter[d]--; // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
for (int i = 0; i < n; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
|
||||
[class]{radix_sort}-[func]{RadixSort}
|
||||
/* Radix sort */
|
||||
void RadixSort(int[] nums) {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
int m = int.MinValue;
|
||||
foreach (int num in nums) {
|
||||
if (num > m) m = num;
|
||||
}
|
||||
// Traverse from the lowest to the highest digit
|
||||
for (int exp = 1; exp <= m; exp *= 10) {
|
||||
// Perform counting sort on the k-th digit of array elements
|
||||
// k = 1 -> exp = 1
|
||||
// k = 2 -> exp = 10
|
||||
// i.e., exp = 10^(k-1)
|
||||
CountingSortDigit(nums, exp);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="radix_sort.go"
|
||||
[class]{}-[func]{digit}
|
||||
/* Get the k-th digit of element num, where exp = 10^(k-1) */
|
||||
func digit(num, exp int) int {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
return (num / exp) % 10
|
||||
}
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
func countingSortDigit(nums []int, exp int) {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
counter := make([]int, 10)
|
||||
n := len(nums)
|
||||
// Count the occurrence of digits 0~9
|
||||
for i := 0; i < n; i++ {
|
||||
d := digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d
|
||||
counter[d]++ // Count the occurrence of digit d
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for i := 1; i < 10; i++ {
|
||||
counter[i] += counter[i-1]
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
res := make([]int, n)
|
||||
for i := n - 1; i >= 0; i-- {
|
||||
d := digit(nums[i], exp)
|
||||
j := counter[d] - 1 // Get the index j for d in the array
|
||||
res[j] = nums[i] // Place the current element at index j
|
||||
counter[d]-- // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
for i := 0; i < n; i++ {
|
||||
nums[i] = res[i]
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
/* Radix sort */
|
||||
func radixSort(nums []int) {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
max := math.MinInt
|
||||
for _, num := range nums {
|
||||
if num > max {
|
||||
max = num
|
||||
}
|
||||
}
|
||||
// Traverse from the lowest to the highest digit
|
||||
for exp := 1; max >= exp; exp *= 10 {
|
||||
// Perform counting sort on the k-th digit of array elements
|
||||
// k = 1 -> exp = 1
|
||||
// k = 2 -> exp = 10
|
||||
// i.e., exp = 10^(k-1)
|
||||
countingSortDigit(nums, exp)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="radix_sort.swift"
|
||||
[class]{}-[func]{digit}
|
||||
/* Get the k-th digit of element num, where exp = 10^(k-1) */
|
||||
func digit(num: Int, exp: Int) -> Int {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
(num / exp) % 10
|
||||
}
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
func countingSortDigit(nums: inout [Int], exp: Int) {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
var counter = Array(repeating: 0, count: 10)
|
||||
// Count the occurrence of digits 0~9
|
||||
for i in nums.indices {
|
||||
let d = digit(num: nums[i], exp: exp) // Get the k-th digit of nums[i], noted as d
|
||||
counter[d] += 1 // Count the occurrence of digit d
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for i in 1 ..< 10 {
|
||||
counter[i] += counter[i - 1]
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
var res = Array(repeating: 0, count: nums.count)
|
||||
for i in nums.indices.reversed() {
|
||||
let d = digit(num: nums[i], exp: exp)
|
||||
let j = counter[d] - 1 // Get the index j for d in the array
|
||||
res[j] = nums[i] // Place the current element at index j
|
||||
counter[d] -= 1 // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
for i in nums.indices {
|
||||
nums[i] = res[i]
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
/* Radix sort */
|
||||
func radixSort(nums: inout [Int]) {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
var m = Int.min
|
||||
for num in nums {
|
||||
if num > m {
|
||||
m = num
|
||||
}
|
||||
}
|
||||
// Traverse from the lowest to the highest digit
|
||||
for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) {
|
||||
// Perform counting sort on the k-th digit of array elements
|
||||
// k = 1 -> exp = 1
|
||||
// k = 2 -> exp = 10
|
||||
// i.e., exp = 10^(k-1)
|
||||
countingSortDigit(nums: &nums, exp: exp)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="radix_sort.js"
|
||||
[class]{}-[func]{digit}
|
||||
/* Get the k-th digit of element num, where exp = 10^(k-1) */
|
||||
function digit(num, exp) {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
return Math.floor(num / exp) % 10;
|
||||
}
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
function countingSortDigit(nums, exp) {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
const counter = new Array(10).fill(0);
|
||||
const n = nums.length;
|
||||
// Count the occurrence of digits 0~9
|
||||
for (let i = 0; i < n; i++) {
|
||||
const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
|
||||
counter[d]++; // Count the occurrence of digit d
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for (let i = 1; i < 10; i++) {
|
||||
counter[i] += counter[i - 1];
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
const res = new Array(n).fill(0);
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const d = digit(nums[i], exp);
|
||||
const j = counter[d] - 1; // Get the index j for d in the array
|
||||
res[j] = nums[i]; // Place the current element at index j
|
||||
counter[d]--; // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
for (let i = 0; i < n; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
/* Radix sort */
|
||||
function radixSort(nums) {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
let m = Math.max(... nums);
|
||||
// Traverse from the lowest to the highest digit
|
||||
for (let exp = 1; exp <= m; exp *= 10) {
|
||||
// Perform counting sort on the k-th digit of array elements
|
||||
// k = 1 -> exp = 1
|
||||
// k = 2 -> exp = 10
|
||||
// i.e., exp = 10^(k-1)
|
||||
countingSortDigit(nums, exp);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="radix_sort.ts"
|
||||
[class]{}-[func]{digit}
|
||||
/* Get the k-th digit of element num, where exp = 10^(k-1) */
|
||||
function digit(num: number, exp: number): number {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
return Math.floor(num / exp) % 10;
|
||||
}
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
function countingSortDigit(nums: number[], exp: number): void {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
const counter = new Array(10).fill(0);
|
||||
const n = nums.length;
|
||||
// Count the occurrence of digits 0~9
|
||||
for (let i = 0; i < n; i++) {
|
||||
const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
|
||||
counter[d]++; // Count the occurrence of digit d
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for (let i = 1; i < 10; i++) {
|
||||
counter[i] += counter[i - 1];
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
const res = new Array(n).fill(0);
|
||||
for (let i = n - 1; i >= 0; i--) {
|
||||
const d = digit(nums[i], exp);
|
||||
const j = counter[d] - 1; // Get the index j for d in the array
|
||||
res[j] = nums[i]; // Place the current element at index j
|
||||
counter[d]--; // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
for (let i = 0; i < n; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
}
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
/* Radix sort */
|
||||
function radixSort(nums: number[]): void {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
let m: number = Math.max(... nums);
|
||||
// Traverse from the lowest to the highest digit
|
||||
for (let exp = 1; exp <= m; exp *= 10) {
|
||||
// Perform counting sort on the k-th digit of array elements
|
||||
// k = 1 -> exp = 1
|
||||
// k = 2 -> exp = 10
|
||||
// i.e., exp = 10^(k-1)
|
||||
countingSortDigit(nums, exp);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="radix_sort.dart"
|
||||
[class]{}-[func]{digit}
|
||||
/* Get k-th digit of element _num, where exp = 10^(k-1) */
|
||||
int digit(int _num, int exp) {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
return (_num ~/ exp) % 10;
|
||||
}
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
void countingSortDigit(List<int> nums, int exp) {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
List<int> counter = List<int>.filled(10, 0);
|
||||
int n = nums.length;
|
||||
// Count the occurrence of digits 0~9
|
||||
for (int i = 0; i < n; i++) {
|
||||
int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
|
||||
counter[d]++; // Count the occurrence of digit d
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for (int i = 1; i < 10; i++) {
|
||||
counter[i] += counter[i - 1];
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
List<int> res = List<int>.filled(n, 0);
|
||||
for (int i = n - 1; i >= 0; i--) {
|
||||
int d = digit(nums[i], exp);
|
||||
int j = counter[d] - 1; // Get the index j for d in the array
|
||||
res[j] = nums[i]; // Place the current element at index j
|
||||
counter[d]--; // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
for (int i = 0; i < n; i++) nums[i] = res[i];
|
||||
}
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
/* Radix sort */
|
||||
void radixSort(List<int> nums) {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
// In Dart, int length is 64 bits
|
||||
int m = -1 << 63;
|
||||
for (int _num in nums) if (_num > m) m = _num;
|
||||
// Traverse from the lowest to the highest digit
|
||||
for (int exp = 1; exp <= m; exp *= 10)
|
||||
// Perform counting sort on the k-th digit of array elements
|
||||
// k = 1 -> exp = 1
|
||||
// k = 2 -> exp = 10
|
||||
// i.e., exp = 10^(k-1)
|
||||
countingSortDigit(nums, exp);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="radix_sort.rs"
|
||||
[class]{}-[func]{digit}
|
||||
/* Get the k-th digit of element num, where exp = 10^(k-1) */
|
||||
fn digit(num: i32, exp: i32) -> usize {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
return ((num / exp) % 10) as usize;
|
||||
}
|
||||
|
||||
[class]{}-[func]{counting_sort_digit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
fn counting_sort_digit(nums: &mut [i32], exp: i32) {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
let mut counter = [0; 10];
|
||||
let n = nums.len();
|
||||
// Count the occurrence of digits 0~9
|
||||
for i in 0..n {
|
||||
let d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d
|
||||
counter[d] += 1; // Count the occurrence of digit d
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for i in 1..10 {
|
||||
counter[i] += counter[i - 1];
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
let mut res = vec![0; n];
|
||||
for i in (0..n).rev() {
|
||||
let d = digit(nums[i], exp);
|
||||
let j = counter[d] - 1; // Get the index j for d in the array
|
||||
res[j] = nums[i]; // Place the current element at index j
|
||||
counter[d] -= 1; // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
nums.copy_from_slice(&res);
|
||||
}
|
||||
|
||||
[class]{}-[func]{radix_sort}
|
||||
/* Radix sort */
|
||||
fn radix_sort(nums: &mut [i32]) {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
let m = *nums.into_iter().max().unwrap();
|
||||
// Traverse from the lowest to the highest digit
|
||||
let mut exp = 1;
|
||||
while exp <= m {
|
||||
counting_sort_digit(nums, exp);
|
||||
exp *= 10;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="radix_sort.c"
|
||||
[class]{}-[func]{digit}
|
||||
/* Get the k-th digit of element num, where exp = 10^(k-1) */
|
||||
int digit(int num, int exp) {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
return (num / exp) % 10;
|
||||
}
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
void countingSortDigit(int nums[], int size, int exp) {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
int *counter = (int *)malloc((sizeof(int) * 10));
|
||||
memset(counter, 0, sizeof(int) * 10); // Initialize to 0 to support subsequent memory release
|
||||
// Count the occurrence of digits 0~9
|
||||
for (int i = 0; i < size; i++) {
|
||||
// Get the k-th digit of nums[i], noted as d
|
||||
int d = digit(nums[i], exp);
|
||||
// Count the occurrence of digit d
|
||||
counter[d]++;
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for (int i = 1; i < 10; i++) {
|
||||
counter[i] += counter[i - 1];
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
int *res = (int *)malloc(sizeof(int) * size);
|
||||
for (int i = size - 1; i >= 0; i--) {
|
||||
int d = digit(nums[i], exp);
|
||||
int j = counter[d] - 1; // Get the index j for d in the array
|
||||
res[j] = nums[i]; // Place the current element at index j
|
||||
counter[d]--; // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
for (int i = 0; i < size; i++) {
|
||||
nums[i] = res[i];
|
||||
}
|
||||
// Free memory
|
||||
free(res);
|
||||
free(counter);
|
||||
}
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
/* Radix sort */
|
||||
void radixSort(int nums[], int size) {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
int max = INT32_MIN;
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (nums[i] > max) {
|
||||
max = nums[i];
|
||||
}
|
||||
}
|
||||
// Traverse from the lowest to the highest digit
|
||||
for (int exp = 1; max >= exp; exp *= 10)
|
||||
// Perform counting sort on the k-th digit of array elements
|
||||
// k = 1 -> exp = 1
|
||||
// k = 2 -> exp = 10
|
||||
// i.e., exp = 10^(k-1)
|
||||
countingSortDigit(nums, size, exp);
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="radix_sort.kt"
|
||||
[class]{}-[func]{digit}
|
||||
/* Get the k-th digit of element num, where exp = 10^(k-1) */
|
||||
fun digit(num: Int, exp: Int): Int {
|
||||
// Passing exp instead of k can avoid repeated expensive exponentiation here
|
||||
return (num / exp) % 10
|
||||
}
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
/* Counting sort (based on nums k-th digit) */
|
||||
fun countingSortDigit(nums: IntArray, exp: Int) {
|
||||
// Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
val counter = IntArray(10)
|
||||
val n = nums.size
|
||||
// Count the occurrence of digits 0~9
|
||||
for (i in 0..<n) {
|
||||
val d = digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d
|
||||
counter[d]++ // Count the occurrence of digit d
|
||||
}
|
||||
// Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
for (i in 1..9) {
|
||||
counter[i] += counter[i - 1]
|
||||
}
|
||||
// Traverse in reverse, based on bucket statistics, place each element into res
|
||||
val res = IntArray(n)
|
||||
for (i in n - 1 downTo 0) {
|
||||
val d = digit(nums[i], exp)
|
||||
val j = counter[d] - 1 // Get the index j for d in the array
|
||||
res[j] = nums[i] // Place the current element at index j
|
||||
counter[d]-- // Decrease the count of d by 1
|
||||
}
|
||||
// Use result to overwrite the original array nums
|
||||
for (i in 0..<n)
|
||||
nums[i] = res[i]
|
||||
}
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
/* Radix sort */
|
||||
fun radixSort(nums: IntArray) {
|
||||
// Get the maximum element of the array, used to determine the maximum number of digits
|
||||
var m = Int.MIN_VALUE
|
||||
for (num in nums) if (num > m) m = num
|
||||
var exp = 1
|
||||
// Traverse from the lowest to the highest digit
|
||||
while (exp <= m) {
|
||||
// Perform counting sort on the k-th digit of array elements
|
||||
// k = 1 -> exp = 1
|
||||
// k = 2 -> exp = 10
|
||||
// i.e., exp = 10^(k-1)
|
||||
countingSortDigit(nums, exp)
|
||||
exp *= 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="radix_sort.rb"
|
||||
[class]{}-[func]{digit}
|
||||
### Get k-th digit of element num, where exp = 10^(k-1) ###
|
||||
def digit(num, exp)
|
||||
# Passing exp instead of k avoids expensive exponentiation calculations
|
||||
(num / exp) % 10
|
||||
end
|
||||
|
||||
[class]{}-[func]{counting_sort_digit}
|
||||
### Counting sort (sort by k-th digit of nums) ###
|
||||
def counting_sort_digit(nums, exp)
|
||||
# Decimal digit range is 0~9, therefore need a bucket array of length 10
|
||||
counter = Array.new(10, 0)
|
||||
n = nums.length
|
||||
# Count the occurrence of digits 0~9
|
||||
for i in 0...n
|
||||
d = digit(nums[i], exp) # Get the k-th digit of nums[i], noted as d
|
||||
counter[d] += 1 # Count the occurrence of digit d
|
||||
end
|
||||
# Calculate prefix sum, converting "occurrence count" into "array index"
|
||||
(1...10).each { |i| counter[i] += counter[i - 1] }
|
||||
# Traverse in reverse, based on bucket statistics, place each element into res
|
||||
res = Array.new(n, 0)
|
||||
for i in (n - 1).downto(0)
|
||||
d = digit(nums[i], exp)
|
||||
j = counter[d] - 1 # Get the index j for d in the array
|
||||
res[j] = nums[i] # Place the current element at index j
|
||||
counter[d] -= 1 # Decrease the count of d by 1
|
||||
end
|
||||
# Use result to overwrite the original array nums
|
||||
(0...n).each { |i| nums[i] = res[i] }
|
||||
end
|
||||
|
||||
[class]{}-[func]{radix_sort}
|
||||
### Radix sort ###
|
||||
def radix_sort(nums)
|
||||
# Get the maximum element of the array, used to determine the maximum number of digits
|
||||
m = nums.max
|
||||
# Traverse from the lowest to the highest digit
|
||||
exp = 1
|
||||
while exp <= m
|
||||
# Perform counting sort on the k-th digit of array elements
|
||||
# k = 1 -> exp = 1
|
||||
# k = 2 -> exp = 10
|
||||
# i.e., exp = 10^(k-1)
|
||||
counting_sort_digit(nums, exp)
|
||||
exp *= 10
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
!!! question "Why start sorting from the lowest digit?"
|
||||
|
||||
```zig title="radix_sort.zig"
|
||||
[class]{}-[func]{digit}
|
||||
In successive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the first round result is $a < b$, while the second round result is $a > b$, then the second round's result will replace the first round's result. Since higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then sort the higher digits.
|
||||
|
||||
[class]{}-[func]{countingSortDigit}
|
||||
## 11.10.2 Algorithm Characteristics
|
||||
|
||||
[class]{}-[func]{radixSort}
|
||||
```
|
||||
Compared to counting sort, radix sort is suitable for larger numerical ranges, **but the prerequisite is that the data must be representable in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are not suitable for radix sort because their number of digits $k$ may be too large, potentially leading to time complexity $O(nk) \gg O(n^2)$.
|
||||
|
||||
!!! question "Why start sorting from the least significant digit?"
|
||||
|
||||
In consecutive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the result of the first round is $a < b$ and the second round is $a > b$, the second round's result will replace the first round's result. Since higher-order digits take precedence over lower-order digits, it makes sense to sort the lower digits before the higher digits.
|
||||
|
||||
## 11.10.2 Algorithm characteristics
|
||||
|
||||
Compared to counting sort, radix sort is suitable for larger numerical ranges, **but it assumes that the data can be represented in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are unsuitable for radix sort, as their digit count $k$ may be large, potentially leading to a time complexity $O(nk) \gg O(n^2)$.
|
||||
|
||||
- **Time complexity is $O(nk)$, non-adaptive sorting**: Assuming the data size is $n$, the data is in base $d$, and the maximum number of digits is $k$, then sorting a single digit takes $O(n + d)$ time, and sorting all $k$ digits takes $O((n + d)k)$ time. Generally, both $d$ and $k$ are relatively small, leading to a time complexity approaching $O(n)$.
|
||||
- **Space complexity is $O(n + d)$, non-in-place sorting**: Like counting sort, radix sort relies on arrays `res` and `counter` of lengths $n$ and $d$ respectively.
|
||||
- **Stable sorting**: When counting sort is stable, radix sort is also stable; if counting sort is unstable, radix sort cannot ensure a correct sorting order.
|
||||
- **Time complexity of $O(nk)$, non-adaptive sorting**: Let the data volume be $n$, the data be in base $d$, and the maximum number of digits be $k$. Then performing counting sort on a certain digit uses $O(n + d)$ time, and sorting all $k$ digits uses $O((n + d)k)$ time. Typically, both $d$ and $k$ are relatively small, and the time complexity approaches $O(n)$.
|
||||
- **Space complexity of $O(n + d)$, non-in-place sorting**: Same as counting sort, radix sort requires auxiliary arrays `res` and `counter` of lengths $n$ and $d$.
|
||||
- **Stable sorting**: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee obtaining correct sorting results.
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.2 Selection sort
|
||||
# 11.2 Selection Sort
|
||||
|
||||
<u>Selection sort</u> works on a very simple principle: it uses a loop where each iteration selects the smallest element from the unsorted interval and moves it to the end of the sorted section.
|
||||
<u>Selection sort (selection sort)</u> works very simply: it opens a loop, and in each round, selects the smallest element from the unsorted interval and places it at the end of the sorted interval.
|
||||
|
||||
Suppose the length of the array is $n$, the steps of selection sort is shown in Figure 11-2.
|
||||
Assume the array has length $n$. The algorithm flow of selection sort is shown in Figure 11-2.
|
||||
|
||||
1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is $[0, n-1]$.
|
||||
2. Select the smallest element in the interval $[0, n-1]$ and swap it with the element at index $0$. After this, the first element of the array is sorted.
|
||||
3. Select the smallest element in the interval $[1, n-1]$ and swap it with the element at index $1$. After this, the first two elements of the array are sorted.
|
||||
4. Continue in this manner. After $n - 1$ rounds of selection and swapping, the first $n - 1$ elements are sorted.
|
||||
5. The only remaining element is subsequently the largest element and does not need sorting, thus the array is sorted.
|
||||
2. Select the smallest element in the interval $[0, n-1]$ and swap it with the element at index $0$. After completion, the first element of the array is sorted.
|
||||
3. Select the smallest element in the interval $[1, n-1]$ and swap it with the element at index $1$. After completion, the first 2 elements of the array are sorted.
|
||||
4. And so on. After $n - 1$ rounds of selection and swapping, the first $n - 1$ elements of the array are sorted.
|
||||
5. The only remaining element must be the largest element, requiring no sorting, so the array sorting is complete.
|
||||
|
||||
=== "<1>"
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
=== "<2>"
|
||||
{ class="animation-figure" }
|
||||
@@ -47,7 +47,7 @@ Suppose the length of the array is $n$, the steps of selection sort is shown in
|
||||
=== "<11>"
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-2 Selection sort process </p>
|
||||
<p align="center"> Figure 11-2 Selection sort steps </p>
|
||||
|
||||
In the code, we use $k$ to record the smallest element within the unsorted interval:
|
||||
|
||||
@@ -57,14 +57,14 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
|
||||
def selection_sort(nums: list[int]):
|
||||
"""Selection sort"""
|
||||
n = len(nums)
|
||||
# Outer loop: unsorted range is [i, n-1]
|
||||
# Outer loop: unsorted interval is [i, n-1]
|
||||
for i in range(n - 1):
|
||||
# Inner loop: find the smallest element within the unsorted range
|
||||
# Inner loop: find the smallest element within the unsorted interval
|
||||
k = i
|
||||
for j in range(i + 1, n):
|
||||
if nums[j] < nums[k]:
|
||||
k = j # Record the index of the smallest element
|
||||
# Swap the smallest element with the first element of the unsorted range
|
||||
# Swap the smallest element with the first element of the unsorted interval
|
||||
nums[i], nums[k] = nums[k], nums[i]
|
||||
```
|
||||
|
||||
@@ -74,15 +74,15 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
|
||||
/* Selection sort */
|
||||
void selectionSort(vector<int> &nums) {
|
||||
int n = nums.size();
|
||||
// Outer loop: unsorted range is [i, n-1]
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
// Inner loop: find the smallest element within the unsorted range
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
int k = i;
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k])
|
||||
k = j; // Record the index of the smallest element
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted range
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
swap(nums[i], nums[k]);
|
||||
}
|
||||
}
|
||||
@@ -94,15 +94,15 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
|
||||
/* Selection sort */
|
||||
void selectionSort(int[] nums) {
|
||||
int n = nums.length;
|
||||
// Outer loop: unsorted range is [i, n-1]
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
// Inner loop: find the smallest element within the unsorted range
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
int k = i;
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k])
|
||||
k = j; // Record the index of the smallest element
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted range
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
int temp = nums[i];
|
||||
nums[i] = nums[k];
|
||||
nums[k] = temp;
|
||||
@@ -113,75 +113,223 @@ In the code, we use $k$ to record the smallest element within the unsorted inter
|
||||
=== "C#"
|
||||
|
||||
```csharp title="selection_sort.cs"
|
||||
[class]{selection_sort}-[func]{SelectionSort}
|
||||
/* Selection sort */
|
||||
void SelectionSort(int[] nums) {
|
||||
int n = nums.Length;
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
int k = i;
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k])
|
||||
k = j; // Record the index of the smallest element
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
(nums[k], nums[i]) = (nums[i], nums[k]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Go"
|
||||
|
||||
```go title="selection_sort.go"
|
||||
[class]{}-[func]{selectionSort}
|
||||
/* Selection sort */
|
||||
func selectionSort(nums []int) {
|
||||
n := len(nums)
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for i := 0; i < n-1; i++ {
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
k := i
|
||||
for j := i + 1; j < n; j++ {
|
||||
if nums[j] < nums[k] {
|
||||
// Record the index of the smallest element
|
||||
k = j
|
||||
}
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
nums[i], nums[k] = nums[k], nums[i]
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="selection_sort.swift"
|
||||
[class]{}-[func]{selectionSort}
|
||||
/* Selection sort */
|
||||
func selectionSort(nums: inout [Int]) {
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for i in nums.indices.dropLast() {
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
var k = i
|
||||
for j in nums.indices.dropFirst(i + 1) {
|
||||
if nums[j] < nums[k] {
|
||||
k = j // Record the index of the smallest element
|
||||
}
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
nums.swapAt(i, k)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "JS"
|
||||
|
||||
```javascript title="selection_sort.js"
|
||||
[class]{}-[func]{selectionSort}
|
||||
/* Selection sort */
|
||||
function selectionSort(nums) {
|
||||
let n = nums.length;
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
let k = i;
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k]) {
|
||||
k = j; // Record the index of the smallest element
|
||||
}
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
[nums[i], nums[k]] = [nums[k], nums[i]];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "TS"
|
||||
|
||||
```typescript title="selection_sort.ts"
|
||||
[class]{}-[func]{selectionSort}
|
||||
/* Selection sort */
|
||||
function selectionSort(nums: number[]): void {
|
||||
let n = nums.length;
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
let k = i;
|
||||
for (let j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k]) {
|
||||
k = j; // Record the index of the smallest element
|
||||
}
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
[nums[i], nums[k]] = [nums[k], nums[i]];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Dart"
|
||||
|
||||
```dart title="selection_sort.dart"
|
||||
[class]{}-[func]{selectionSort}
|
||||
/* Selection sort */
|
||||
void selectionSort(List<int> nums) {
|
||||
int n = nums.length;
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
int k = i;
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k]) k = j; // Record the index of the smallest element
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
int temp = nums[i];
|
||||
nums[i] = nums[k];
|
||||
nums[k] = temp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Rust"
|
||||
|
||||
```rust title="selection_sort.rs"
|
||||
[class]{}-[func]{selection_sort}
|
||||
/* Selection sort */
|
||||
fn selection_sort(nums: &mut [i32]) {
|
||||
if nums.is_empty() {
|
||||
return;
|
||||
}
|
||||
let n = nums.len();
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for i in 0..n - 1 {
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
let mut k = i;
|
||||
for j in i + 1..n {
|
||||
if nums[j] < nums[k] {
|
||||
k = j; // Record the index of the smallest element
|
||||
}
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
nums.swap(i, k);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
|
||||
```c title="selection_sort.c"
|
||||
[class]{}-[func]{selectionSort}
|
||||
/* Selection sort */
|
||||
void selectionSort(int nums[], int n) {
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
int k = i;
|
||||
for (int j = i + 1; j < n; j++) {
|
||||
if (nums[j] < nums[k])
|
||||
k = j; // Record the index of the smallest element
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
int temp = nums[i];
|
||||
nums[i] = nums[k];
|
||||
nums[k] = temp;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="selection_sort.kt"
|
||||
[class]{}-[func]{selectionSort}
|
||||
/* Selection sort */
|
||||
fun selectionSort(nums: IntArray) {
|
||||
val n = nums.size
|
||||
// Outer loop: unsorted interval is [i, n-1]
|
||||
for (i in 0..<n - 1) {
|
||||
var k = i
|
||||
// Inner loop: find the smallest element within the unsorted interval
|
||||
for (j in i + 1..<n) {
|
||||
if (nums[j] < nums[k])
|
||||
k = j // Record the index of the smallest element
|
||||
}
|
||||
// Swap the smallest element with the first element of the unsorted interval
|
||||
val temp = nums[i]
|
||||
nums[i] = nums[k]
|
||||
nums[k] = temp
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="selection_sort.rb"
|
||||
[class]{}-[func]{selection_sort}
|
||||
### Selection sort ###
|
||||
def selection_sort(nums)
|
||||
n = nums.length
|
||||
# Outer loop: unsorted interval is [i, n-1]
|
||||
for i in 0...(n - 1)
|
||||
# Inner loop: find the smallest element within the unsorted interval
|
||||
k = i
|
||||
for j in (i + 1)...n
|
||||
if nums[j] < nums[k]
|
||||
k = j # Record the index of the smallest element
|
||||
end
|
||||
end
|
||||
# Swap the smallest element with the first element of the unsorted interval
|
||||
nums[i], nums[k] = nums[k], nums[i]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
## 11.2.1 Algorithm Characteristics
|
||||
|
||||
```zig title="selection_sort.zig"
|
||||
[class]{}-[func]{selectionSort}
|
||||
```
|
||||
- **Time complexity of $O(n^2)$, non-adaptive sorting**: The outer loop has $n - 1$ rounds in total. The length of the unsorted interval in the first round is $n$, and the length of the unsorted interval in the last round is $2$. That is, each round of the outer loop contains $n$, $n - 1$, $\dots$, $3$, $2$ inner loop iterations, summing to $\frac{(n - 1)(n + 2)}{2}$.
|
||||
- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space.
|
||||
- **Non-stable sorting**: As shown in Figure 11-3, element `nums[i]` may be swapped to the right of an element equal to it, causing a change in their relative order.
|
||||
|
||||
## 11.2.1 Algorithm characteristics
|
||||
{ class="animation-figure" }
|
||||
|
||||
- **Time complexity of $O(n^2)$, non-adaptive sort**: There are $n - 1$ iterations in the outer loop, with the length of the unsorted section starting at $n$ in the first iteration and decreasing to $2$ in the last iteration, i.e., each outer loop iterations contain $n$, $n - 1$, $\dots$, $3$, $2$ inner loop iterations respectively, summing up to $\frac{(n - 1)(n + 2)}{2}$.
|
||||
- **Space complexity of $O(1)$, in-place sort**: Uses constant extra space with pointers $i$ and $j$.
|
||||
- **Non-stable sort**: As shown in Figure 11-3, an element `nums[i]` may be swapped to the right of an equal element, causing their relative order to change.
|
||||
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-3 Selection sort instability example </p>
|
||||
<p align="center"> Figure 11-3 Selection sort non-stability example </p>
|
||||
|
||||
@@ -2,28 +2,28 @@
|
||||
comments: true
|
||||
---
|
||||
|
||||
# 11.1 Sorting algorithms
|
||||
# 11.1 Sorting Algorithm
|
||||
|
||||
<u>Sorting algorithms</u> are used to arrange a set of data in a specific order. Sorting algorithms have a wide range of applications because ordered data can usually be searched, analyzed, and processed more efficiently.
|
||||
<u>Sorting algorithm (sorting algorithm)</u> is used to arrange a group of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently.
|
||||
|
||||
As shown in Figure 11-1, the data types in sorting algorithms can be integers, floating point numbers, characters, or strings, etc. Sorting criterion can be set according to needs, such as numerical size, character ASCII order, or custom criterion.
|
||||
As shown in Figure 11-1, data types in sorting algorithms can be integers, floating-point numbers, characters, or strings, etc. The sorting criterion can be set according to requirements, such as numerical size, character ASCII code order, or custom rules.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-1 Data types and comparator examples </p>
|
||||
<p align="center"> Figure 11-1 Data type and criterion examples </p>
|
||||
|
||||
## 11.1.1 Evaluation dimensions
|
||||
## 11.1.1 Evaluation Dimensions
|
||||
|
||||
**Execution efficiency**: We expect the time complexity of sorting algorithms to be as low as possible, as well as a lower number of overall operations (lowering the constant term of time complexity). For large data volumes, execution efficiency is particularly important.
|
||||
**Execution efficiency**: We expect the time complexity of sorting algorithms to be as low as possible, with a smaller total number of operations (reducing the constant factor in time complexity). For large data volumes, execution efficiency is particularly important.
|
||||
|
||||
**In-place property**: As the name implies, <u>in-place sorting</u> is achieved by directly manipulating the original array, without the need for additional helper arrays, thus saving memory. Generally, in-place sorting involves fewer data moving operations and is faster.
|
||||
**In-place property**: As the name implies, <u>in-place sorting</u> achieves sorting by operating directly on the original array without requiring additional auxiliary arrays, thus saving memory. Typically, in-place sorting involves fewer data movement operations and runs faster.
|
||||
|
||||
**Stability**: <u>Stable sorting</u> ensures that the relative order of equal elements in the array does not change after sorting.
|
||||
**Stability**: <u>Stable sorting</u> ensures that the relative order of equal elements in the array does not change after sorting is completed.
|
||||
|
||||
Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose we have a table storing student information, with the first and second columns being name and age, respectively. In this case, <u>unstable sorting</u> might lead to a loss of order in the input data:
|
||||
Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, <u>unstable sorting</u> may cause the ordered nature of the input data to be lost:
|
||||
|
||||
```shell
|
||||
# Input data is sorted by name
|
||||
# Input Data Is Sorted by Name
|
||||
# (name, age)
|
||||
('A', 19)
|
||||
('B', 18)
|
||||
@@ -31,9 +31,9 @@ Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose
|
||||
('D', 19)
|
||||
('E', 23)
|
||||
|
||||
# Assuming an unstable sorting algorithm is used to sort the list by age,
|
||||
# the result changes the relative position of ('D', 19) and ('A', 19),
|
||||
# and the property of the input data being sorted by name is lost
|
||||
# Assuming We Use an Unstable Sorting Algorithm to Sort the List by Age,
|
||||
# In the Result, the Relative Positions of ('D', 19) and ('A', 19) Are Changed,
|
||||
# And the Property That the Input Data Is Sorted by Name Is Lost
|
||||
('B', 18)
|
||||
('D', 19)
|
||||
('A', 19)
|
||||
@@ -41,12 +41,12 @@ Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose
|
||||
('E', 23)
|
||||
```
|
||||
|
||||
**Adaptability**: <u>Adaptive sorting</u> leverages existing order information within the input data to reduce computational effort, achieving more optimal time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than their average-case time complexity.
|
||||
**Adaptability**: <u>Adaptive sorting</u> can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity.
|
||||
|
||||
**Comparison or non-comparison-based**: <u>Comparison-based sorting</u> relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements and thus sort the entire array, with the theoretical optimal time complexity being $O(n \log n)$. Meanwhile, <u>non-comparison sorting</u> does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively poor.
|
||||
**Comparison-based or not**: <u>Comparison-based sorting</u> relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of $O(n \log n)$. <u>Non-comparison sorting</u> does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively limited.
|
||||
|
||||
## 11.1.2 Ideal sorting algorithm
|
||||
## 11.1.2 Ideal Sorting Algorithm
|
||||
|
||||
**Fast execution, in-place, stable, adaptive, and versatile**. Clearly, no sorting algorithm that combines all these features has been found to date. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.
|
||||
**Fast execution, in-place, stable, adaptive, good versatility**. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem.
|
||||
|
||||
Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each based on the above evaluation dimensions.
|
||||
Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each sorting algorithm based on the above evaluation dimensions.
|
||||
|
||||
@@ -4,50 +4,50 @@ comments: true
|
||||
|
||||
# 11.11 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
- Bubble sort works by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$.
|
||||
- Insertion sort sorts each round by inserting elements from the unsorted interval into the correct position in the sorted interval. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in sorting small amounts of data due to relatively fewer operations per unit.
|
||||
- Quick sort is based on sentinel partitioning operations. In sentinel partitioning, it's possible to always pick the worst pivot, leading to a time complexity degradation to $O(n^2)$. Introducing median or random pivots can reduce the probability of such degradation. Tail recursion effectively reduce the recursion depth, optimizing the space complexity to $O(\log n)$.
|
||||
- Merge sort includes dividing and merging two phases, typically embodying the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, resulting in a space complexity of $O(n)$; however, the space complexity for sorting a list can be optimized to $O(1)$.
|
||||
- Bucket sort consists of three steps: distributing data into buckets, sorting within each bucket, and merging results in bucket order. It also embodies the divide-and-conquer strategy, suitable for very large datasets. The key to bucket sort is the even distribution of data.
|
||||
- Counting sort is a variant of bucket sort, which sorts by counting the occurrences of each data point. Counting sort is suitable for large datasets with a limited range of data and requires data conversion to positive integers.
|
||||
- Radix sort processes data by sorting it digit by digit, requiring data to be represented as fixed-length numbers.
|
||||
- Overall, we seek sorting algorithm that has high efficiency, stability, in-place operation, and adaptability. However, like other data structures and algorithms, no sorting algorithm can meet all these conditions simultaneously. In practical applications, we need to choose the appropriate sorting algorithm based on the characteristics of the data.
|
||||
- Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place nature, and adaptability.
|
||||
- Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$.
|
||||
- Insertion sort completes sorting by inserting elements from the unsorted interval into the correct position in the sorted interval each round. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in small data volume sorting tasks because it involves relatively few unit operations.
|
||||
- Quick sort is implemented based on sentinel partitioning operations. In sentinel partitioning, it is possible to select the worst pivot every time, causing the time complexity to degrade to $O(n^2)$. Introducing median pivot or random pivot can reduce the probability of such degradation. By preferentially recursing on the shorter sub-interval, the recursion depth can be effectively reduced, optimizing the space complexity to $O(\log n)$.
|
||||
- Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of $O(n)$; however, the space complexity of sorting a linked list can be optimized to $O(1)$.
|
||||
- Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly.
|
||||
- Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers.
|
||||
- Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers.
|
||||
- Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive, with good versatility. However, just like other data structures and algorithms, no sorting algorithm has been found so far that simultaneously possesses all these characteristics. In practical applications, we need to select the appropriate sorting algorithm based on the specific characteristics of the data.
|
||||
- Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability.
|
||||
|
||||
{ class="animation-figure" }
|
||||
{ class="animation-figure" }
|
||||
|
||||
<p align="center"> Figure 11-19 Sorting Algorithm Comparison </p>
|
||||
<p align="center"> Figure 11-19 Sorting algorithm comparison </p>
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**: When is the stability of sorting algorithms necessary?
|
||||
**Q**: In what situations is the stability of sorting algorithms necessary?
|
||||
|
||||
In reality, we might sort based on one attribute of an object. For example, students have names and heights as attributes, and we aim to implement multi-level sorting: first by name to get `(A, 180) (B, 185) (C, 170) (D, 170)`; then by height. Because the sorting algorithm is unstable, we might end up with `(D, 170) (C, 170) (A, 180) (B, 185)`.
|
||||
In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get `(A, 180) (B, 185) (C, 170) (D, 170)`; then sort by height. Because the sorting algorithm is unstable, we may get `(D, 170) (C, 170) (A, 180) (B, 185)`.
|
||||
|
||||
It can be seen that the positions of students D and C have been swapped, disrupting the orderliness of the names, which is undesirable.
|
||||
It can be seen that the positions of students D and C have been swapped, and the orderliness of names has been disrupted, which is something we don't want to see.
|
||||
|
||||
**Q**: Can the order of "searching from right to left" and "searching from left to right" in sentinel partitioning be swapped?
|
||||
|
||||
No, when using the leftmost element as the pivot, we must first "search from right to left" then "search from left to right". This conclusion is somewhat counterintuitive, so let's analyze the reason.
|
||||
No. When we use the leftmost element as the pivot, we must first "search from right to left" and then "search from left to right". This conclusion is somewhat counterintuitive; let's analyze the reason.
|
||||
|
||||
The last step of the sentinel partition `partition()` is to swap `nums[left]` and `nums[i]`. After the swap, the elements to the left of the pivot are all `<=` the pivot, **which requires that `nums[left] >= nums[i]` must hold before the last swap**. Suppose we "search from left to right" first, and if no element larger than the pivot is found, **we will exit the loop when `i == j`, possibly with `nums[j] == nums[i] > nums[left]`**. In other words, the final swap operation will exchange an element larger than the pivot to the left end of the array, causing the sentinel partition to fail.
|
||||
The last step of sentinel partitioning `partition()` is to swap `nums[left]` and `nums[i]`. After the swap is complete, the elements to the left of the pivot are all `<=` the pivot, **which requires that `nums[left] >= nums[i]` must hold before the last swap**. Suppose we first "search from left to right", then if we cannot find an element larger than the pivot, **we will exit the loop when `i == j`, at which point it may be that `nums[j] == nums[i] > nums[left]`**. In other words, the last swap operation will swap an element larger than the pivot to the leftmost end of the array, causing sentinel partitioning to fail.
|
||||
|
||||
For example, given the array `[0, 0, 0, 0, 1]`, if we first "search from left to right", the array after the sentinel partition is `[1, 0, 0, 0, 0]`, which is incorrect.
|
||||
For example, given the array `[0, 0, 0, 0, 1]`, if we first "search from left to right", the array after sentinel partitioning is `[1, 0, 0, 0, 0]`, which is incorrect.
|
||||
|
||||
Upon further consideration, if we choose `nums[right]` as the pivot, then exactly the opposite, we must first "search from left to right".
|
||||
Thinking deeper, if we select `nums[right]` as the pivot, then it's exactly the opposite - we must first "search from left to right".
|
||||
|
||||
**Q**: Regarding tail recursion optimization, why does choosing the shorter array ensure that the recursion depth does not exceed $\log n$?
|
||||
**Q**: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed $\log n$?
|
||||
|
||||
The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partition divides the original array into two subarrays. With tail recursion optimization, the length of the subarray to be recursively followed is at most half of the original array length. Assuming the worst case always halves the length, the final recursion depth will be $\log n$.
|
||||
The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partitioning divides the original array into two sub-arrays. After recursion depth optimization, the length of the sub-array to be recursively processed is at most half of the original array length. Assuming the worst case is always half the length, the final recursion depth will be $\log n$.
|
||||
|
||||
Reviewing the original quicksort, we might continuously recursively process larger arrays, in the worst case from $n$, $n - 1$, ..., $2$, $1$, with a recursion depth of $n$. Tail recursion optimization can avoid this scenario.
|
||||
Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be $n$, $n - 1$, $\dots$, $2$, $1$, with a recursion depth of $n$. Recursion depth optimization can avoid this situation.
|
||||
|
||||
**Q**: When all elements in the array are equal, is the time complexity of quicksort $O(n^2)$? How should this degenerate case be handled?
|
||||
**Q**: When all elements in the array are equal, is the time complexity of quick sort $O(n^2)$? How should this degenerate case be handled?
|
||||
|
||||
Yes. For this situation, consider using sentinel partitioning to divide the array into three parts: less than, equal to, and greater than the pivot. Only recursively proceed with the less than and greater than parts. In this method, an array where all input elements are equal can be sorted in just one round of sentinel partitioning.
|
||||
Yes. For this situation, consider partitioning the array into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. Only recursively process the less than and greater than parts. Under this method, an array where all input elements are equal can complete sorting in just one round of sentinel partitioning.
|
||||
|
||||
**Q**: Why is the worst-case time complexity of bucket sort $O(n^2)$?
|
||||
|
||||
In the worst case, all elements are placed in the same bucket. If we use an $O(n^2)$ algorithm to sort these elements, the time complexity will be $O(n^2)$.
|
||||
In the worst case, all elements are distributed into the same bucket. If we use an $O(n^2)$ algorithm to sort these elements, the time complexity will be $O(n^2)$.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,19 +3,19 @@ comments: true
|
||||
icon: material/stack-overflow
|
||||
---
|
||||
|
||||
# Chapter 5. Stack and queue
|
||||
# Chapter 5. Stack and Queue
|
||||
|
||||
{ class="cover-image" }
|
||||
{ class="cover-image" }
|
||||
|
||||
!!! abstract
|
||||
|
||||
A stack is like cats placed on top of each other, while a queue is like cats lined up one by one.
|
||||
|
||||
They represent the logical relationships of Last-In-First-Out (LIFO) and First-In-First-Out (FIFO), respectively.
|
||||
Stacks are like stacking cats, while queues are like cats lining up.
|
||||
|
||||
They represent LIFO (Last In First Out) and FIFO (First In First Out) logic, respectively.
|
||||
|
||||
## Chapter contents
|
||||
|
||||
- [5.1 Stack](stack.md)
|
||||
- [5.2 Queue](queue.md)
|
||||
- [5.3 Double-ended queue](deque.md)
|
||||
- [5.3 Double-Ended Queue](deque.md)
|
||||
- [5.4 Summary](summary.md)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,32 +4,32 @@ comments: true
|
||||
|
||||
# 5.4 Summary
|
||||
|
||||
### 1. Key review
|
||||
### 1. Key Review
|
||||
|
||||
- Stack is a data structure that follows the Last-In-First-Out (LIFO) principle and can be implemented using arrays or linked lists.
|
||||
- In terms of time efficiency, the array implementation of the stack has a higher average efficiency. However, during expansion, the time complexity for a single push operation can degrade to $O(n)$. In contrast, the linked list implementation of a stack offers more stable efficiency.
|
||||
- Regarding space efficiency, the array implementation of the stack may lead to a certain degree of space wastage. However, it's important to note that the memory space occupied by nodes in a linked list is generally larger than that for elements in an array.
|
||||
- A queue is a data structure that follows the First-In-First-Out (FIFO) principle, and it can also be implemented using arrays or linked lists. The conclusions regarding time and space efficiency for queues are similar to those for stacks.
|
||||
- A double-ended queue (deque) is a more flexible type of queue that allows adding and removing elements at both ends.
|
||||
- A stack is a data structure that follows the LIFO principle and can be implemented using arrays or linked lists.
|
||||
- In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to $O(n)$. In contrast, the linked list implementation of a stack provides more stable efficiency performance.
|
||||
- In terms of space efficiency, the array implementation of a stack may lead to some degree of space wastage. However, it should be noted that the memory space occupied by linked list nodes is larger than that of array elements.
|
||||
- A queue is a data structure that follows the FIFO principle and can also be implemented using arrays or linked lists. The conclusions regarding time efficiency and space efficiency comparisons for queues are similar to those for stacks mentioned above.
|
||||
- A deque is a queue with greater flexibility that allows adding and removing elements at both ends.
|
||||
|
||||
### 2. Q & A
|
||||
|
||||
**Q**: Is the browser's forward and backward functionality implemented with a doubly linked list?
|
||||
|
||||
A browser's forward and backward navigation is essentially a manifestation of the "stack" concept. When a user visits a new page, the page is added to the top of the stack; when they click the back button, the page is popped from the top of the stack. A double-ended queue (deque) can conveniently implement some additional operations, as mentioned in the "Double-Ended Queue" section.
|
||||
The forward and backward functionality of a browser is essentially a manifestation of a "stack." When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. Using a deque can conveniently implement some additional operations, as mentioned in the "Deque" section.
|
||||
|
||||
**Q**: After popping from a stack, is it necessary to free the memory of the popped node?
|
||||
**Q**: After popping from the stack, do we need to free the memory of the popped node?
|
||||
|
||||
If the popped node will still be used later, it's not necessary to free its memory. In languages like Java and Python that have automatic garbage collection, manual memory release is not necessary; in C and C++, manual memory release is required.
|
||||
If the popped node will still be needed later, then memory does not need to be freed. If it won't be used afterward, languages like Java and Python have automatic garbage collection, so manual memory deallocation is not required; in C and C++, manual memory deallocation is necessary.
|
||||
|
||||
**Q**: A double-ended queue seems like two stacks joined together. What are its uses?
|
||||
**Q**: A deque seems like two stacks joined together. What is its purpose?
|
||||
|
||||
A double-ended queue, which is a combination of a stack and a queue or two stacks joined together, exhibits both stack and queue logic. Thus, it can implement all applications of stacks and queues while offering more flexibility.
|
||||
A deque is like a combination of a stack and a queue, or two stacks joined together. It exhibits the logic of both stack and queue, so it can implement all applications of stacks and queues, and is more flexible.
|
||||
|
||||
**Q**: How exactly are undo and redo implemented?
|
||||
**Q**: How are undo and redo specifically implemented?
|
||||
|
||||
Undo and redo operations are implemented using two stacks: Stack `A` for undo and Stack `B` for redo.
|
||||
Use two stacks: stack `A` for undo and stack `B` for redo.
|
||||
|
||||
1. Each time a user performs an operation, it is pushed onto Stack `A`, and Stack `B` is cleared.
|
||||
2. When the user executes an "undo", the most recent operation is popped from Stack `A` and pushed onto Stack `B`.
|
||||
3. When the user executes a "redo", the most recent operation is popped from Stack `B` and pushed back onto Stack `A`.
|
||||
1. Whenever the user performs an operation, push this operation onto stack `A` and clear stack `B`.
|
||||
2. When the user performs "undo," pop the most recent operation from stack `A` and push it onto stack `B`.
|
||||
3. When the user performs "redo," pop the most recent operation from stack `B` and push it onto stack `A`.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1514
-243
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user