mirror of
https://github.com/krahets/hello-algo.git
synced 2026-08-24 09:07:14 +00:00
Merge branch 'master' into master
This commit is contained in:
@@ -9,7 +9,7 @@ comments: true
|
||||
使用二分查找有两个前置条件:
|
||||
|
||||
- **要求输入数据是有序的**,这样才能通过判断大小关系来排除一半的搜索区间;
|
||||
- **二分查找仅适用于数组** ,而在链表中使用效率很低,因为其在循环中需要跳跃式(非连续地)访问元素。
|
||||
- **二分查找仅适用于数组**,而在链表中使用效率很低,因为其在循环中需要跳跃式(非连续地)访问元素。
|
||||
|
||||
## 算法实现
|
||||
|
||||
@@ -116,17 +116,17 @@ $$
|
||||
=== "Go"
|
||||
|
||||
```go title="binary_search.go"
|
||||
/* 二分查找(左闭右开) */
|
||||
func binarySearch1(nums []int, target int) int {
|
||||
// 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1
|
||||
i, j := 0, len(nums)
|
||||
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
|
||||
for i < j {
|
||||
/* 二分查找(双闭区间) */
|
||||
func binarySearch(nums []int, target int) int {
|
||||
// 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
|
||||
i, j := 0, len(nums)-1
|
||||
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
|
||||
for i <= j {
|
||||
m := (i + j) / 2 // 计算中点索引 m
|
||||
if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中
|
||||
if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1
|
||||
} else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中
|
||||
j = m
|
||||
} else if nums[m] > target { // 此情况说明 target 在区间 [i, m-1] 中
|
||||
j = m - 1
|
||||
} else { // 找到目标元素,返回其索引
|
||||
return m
|
||||
}
|
||||
@@ -141,27 +141,43 @@ $$
|
||||
```js title="binary_search.js"
|
||||
/* 二分查找(双闭区间) */
|
||||
function binarySearch(nums, target) {
|
||||
// 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
|
||||
let i = 0, j = nums.length - 1;
|
||||
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
|
||||
while (i <= j) {
|
||||
let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中
|
||||
j = m - 1;
|
||||
else
|
||||
return m; // 找到目标元素,返回其索引
|
||||
// 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
|
||||
let i = 0, j = nums.length - 1;
|
||||
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
|
||||
while (i <= j) {
|
||||
let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m-1] 中
|
||||
j = m - 1;
|
||||
else
|
||||
return m; // 找到目标元素,返回其索引
|
||||
}
|
||||
// 未找到目标元素,返回 -1
|
||||
return -1;
|
||||
// 未找到目标元素,返回 -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="binary_search.ts"
|
||||
|
||||
/* 二分查找(双闭区间) */
|
||||
const binarySearch = function (nums: number[], target: number): number {
|
||||
// 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素
|
||||
let i = 0, j = nums.length - 1;
|
||||
// 循环,当搜索区间为空时跳出(当 i > j 时为空)
|
||||
while (i <= j) {
|
||||
const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m
|
||||
if (nums[m] < target) { // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1;
|
||||
} else if (nums[m] > target) { // 此情况说明 target 在区间 [i, m-1] 中
|
||||
j = m - 1;
|
||||
} else { // 找到目标元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return -1; // 未找到目标元素,返回 -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
@@ -194,6 +210,12 @@ $$
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search.swift"
|
||||
|
||||
```
|
||||
|
||||
### “左闭右开”实现
|
||||
|
||||
当然,我们也可以使用“左闭右开”的表示方法,写出相同功能的二分查找代码。
|
||||
@@ -208,9 +230,9 @@ $$
|
||||
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
|
||||
while (i < j) {
|
||||
int m = (i + j) / 2; // 计算中点索引 m
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m] 中
|
||||
j = m;
|
||||
else // 找到目标元素,返回其索引
|
||||
return m;
|
||||
@@ -230,9 +252,9 @@ $$
|
||||
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
|
||||
while (i < j) {
|
||||
int m = (i + j) / 2; // 计算中点索引 m
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m] 中
|
||||
j = m;
|
||||
else // 找到目标元素,返回其索引
|
||||
return m;
|
||||
@@ -252,9 +274,9 @@ $$
|
||||
# 循环,当搜索区间为空时跳出(当 i = j 时为空)
|
||||
while i < j:
|
||||
m = (i + j) // 2 # 计算中点索引 m
|
||||
if nums[m] < target: # 此情况说明 target 在区间 [m+1, j) 中
|
||||
if nums[m] < target: # 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1
|
||||
elif nums[m] > target: # 此情况说明 target 在区间 [i, m) 中
|
||||
elif nums[m] > target: # 此情况说明 target 在区间 [i, m] 中
|
||||
j = m
|
||||
else: # 找到目标元素,返回其索引
|
||||
return m
|
||||
@@ -271,9 +293,9 @@ $$
|
||||
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
|
||||
for i < j {
|
||||
m := (i + j) / 2 // 计算中点索引 m
|
||||
if nums[m] < target { // 此情况说明 target 在区间 [m+1, j) 中
|
||||
if nums[m] < target { // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1
|
||||
} else if nums[m] > target { // 此情况说明 target 在区间 [i, m) 中
|
||||
} else if nums[m] > target { // 此情况说明 target 在区间 [i, m] 中
|
||||
j = m
|
||||
} else { // 找到目标元素,返回其索引
|
||||
return m
|
||||
@@ -289,27 +311,43 @@ $$
|
||||
```js title="binary_search.js"
|
||||
/* 二分查找(左闭右开) */
|
||||
function binarySearch1(nums, target) {
|
||||
// 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1
|
||||
let i = 0, j = nums.length;
|
||||
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
|
||||
while (i < j) {
|
||||
let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中
|
||||
j = m;
|
||||
else // 找到目标元素,返回其索引
|
||||
return m;
|
||||
// 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1
|
||||
let i = 0, j = nums.length;
|
||||
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
|
||||
while (i < j) {
|
||||
let m = parseInt((i + j) / 2); // 计算中点索引 m ,在 JS 中需使用 parseInt 函数取整
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m] 中
|
||||
j = m;
|
||||
else // 找到目标元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
// 未找到目标元素,返回 -1
|
||||
return -1;
|
||||
// 未找到目标元素,返回 -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="binary_search.ts"
|
||||
|
||||
/* 二分查找(左闭右开) */
|
||||
const binarySearch1 = function (nums: number[], target: number): number {
|
||||
// 初始化左闭右开 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1
|
||||
let i = 0, j = nums.length;
|
||||
// 循环,当搜索区间为空时跳出(当 i = j 时为空)
|
||||
while (i < j) {
|
||||
const m = Math.floor(i + (j - i) / 2); // 计算中点索引 m
|
||||
if (nums[m] < target) { // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1;
|
||||
} else if (nums[m] > target) { // 此情况说明 target 在区间 [i, m] 中
|
||||
j = m;
|
||||
} else { // 找到目标元素,返回其索引
|
||||
return m;
|
||||
}
|
||||
}
|
||||
return -1; // 未找到目标元素,返回 -1
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
@@ -330,9 +368,9 @@ $$
|
||||
while (i < j)
|
||||
{
|
||||
int m = (i + j) / 2; // 计算中点索引 m
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j) 中
|
||||
if (nums[m] < target) // 此情况说明 target 在区间 [m+1, j] 中
|
||||
i = m + 1;
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m) 中
|
||||
else if (nums[m] > target) // 此情况说明 target 在区间 [i, m] 中
|
||||
j = m;
|
||||
else // 找到目标元素,返回其索引
|
||||
return m;
|
||||
@@ -342,6 +380,12 @@ $$
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="binary_search.swift"
|
||||
|
||||
```
|
||||
|
||||
### 两种表示对比
|
||||
|
||||
对比下来,两种表示的代码写法有以下不同点:
|
||||
@@ -407,7 +451,10 @@ $$
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title=""
|
||||
|
||||
// (i + j) 有可能超出 Number 的取值范围
|
||||
let m = Math.floor((i + j) / 2);
|
||||
// 更换为此写法则不会越界
|
||||
let m = Math.floor(i + (j - i) / 2);
|
||||
```
|
||||
|
||||
=== "C"
|
||||
@@ -425,21 +472,27 @@ $$
|
||||
int m = i + (j - i) / 2;
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title=""
|
||||
|
||||
```
|
||||
|
||||
## 复杂度分析
|
||||
|
||||
**时间复杂度 $O(\log n)$ :** 其中 $n$ 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 $\log_2 n$ ,使用 $O(\log n)$ 时间。
|
||||
**时间复杂度 $O(\log n)$** :其中 $n$ 为数组或链表长度;每轮排除一半的区间,因此循环轮数为 $\log_2 n$ ,使用 $O(\log n)$ 时间。
|
||||
|
||||
**空间复杂度 $O(1)$ :** 指针 `i` , `j` 使用常数大小空间。
|
||||
**空间复杂度 $O(1)$** :指针 `i` , `j` 使用常数大小空间。
|
||||
|
||||
## 优缺点
|
||||
## 优点与缺点
|
||||
|
||||
二分查找效率很高,体现在:
|
||||
|
||||
- **二分查找时间复杂度低。** 对数阶在数据量很大时具有巨大优势,例如,当数据大小 $n = 2^{20}$ 时,线性查找需要 $2^{20} = 1048576$ 轮循环,而二分查找仅需要 $\log_2 2^{20} = 20$ 轮循环。
|
||||
- **二分查找不需要额外空间。** 相对于借助额外数据结构来实现查找的算法来说,其更加节约空间使用。
|
||||
- **二分查找时间复杂度低**。对数阶在数据量很大时具有巨大优势,例如,当数据大小 $n = 2^{20}$ 时,线性查找需要 $2^{20} = 1048576$ 轮循环,而二分查找仅需要 $\log_2 2^{20} = 20$ 轮循环。
|
||||
- **二分查找不需要额外空间**。相对于借助额外数据结构来实现查找的算法来说,其更加节约空间使用。
|
||||
|
||||
但并不意味着所有情况下都应使用二分查找,这是因为:
|
||||
|
||||
- **二分查找仅适用于有序数据。** 如果输入数据是无序的,为了使用二分查找而专门执行数据排序,那么是得不偿失的,因为排序算法的时间复杂度一般为 $O(n \log n)$ ,比线性查找和二分查找都更差。再例如,对于频繁插入元素的场景,为了保持数组的有序性,需要将元素插入到特定位置,时间复杂度为 $O(n)$ ,也是非常昂贵的。
|
||||
- **二分查找仅适用于数组。** 由于在二分查找中,访问索引是 ”非连续“ 的,因此链表或者基于链表实现的数据结构都无法使用。
|
||||
- **在小数据量下,线性查找的性能更好。** 在线性查找中,每轮只需要 1 次判断操作;而在二分查找中,需要 1 次加法、1 次除法、1 ~ 3 次判断操作、1 次加法(减法),共 4 ~ 6 个单元操作;因此,在数据量 $n$ 较小时,线性查找反而比二分查找更快。
|
||||
- **二分查找仅适用于有序数据**。如果输入数据是无序的,为了使用二分查找而专门执行数据排序,那么是得不偿失的,因为排序算法的时间复杂度一般为 $O(n \log n)$ ,比线性查找和二分查找都更差。再例如,对于频繁插入元素的场景,为了保持数组的有序性,需要将元素插入到特定位置,时间复杂度为 $O(n)$ ,也是非常昂贵的。
|
||||
- **二分查找仅适用于数组**。由于在二分查找中,访问索引是 ”非连续“ 的,因此链表或者基于链表实现的数据结构都无法使用。
|
||||
- **在小数据量下,线性查找的性能更好**。在线性查找中,每轮只需要 1 次判断操作;而在二分查找中,需要 1 次加法、1 次除法、1 ~ 3 次判断操作、1 次加法(减法),共 4 ~ 6 个单元操作;因此,在数据量 $n$ 较小时,线性查找反而比二分查找更快。
|
||||
|
||||
@@ -105,6 +105,12 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="hashing_search.swift"
|
||||
|
||||
```
|
||||
|
||||
再比如,如果我们想要给定一个目标结点值 `target` ,获取对应的链表结点对象,那么也可以使用哈希查找实现。
|
||||
|
||||

|
||||
@@ -199,13 +205,19 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="hashing_search.swift"
|
||||
|
||||
```
|
||||
|
||||
## 复杂度分析
|
||||
|
||||
**时间复杂度:** $O(1)$ ,哈希表的查找操作使用 $O(1)$ 时间。
|
||||
**时间复杂度 $O(1)$** :哈希表的查找操作使用 $O(1)$ 时间。
|
||||
|
||||
**空间复杂度:** $O(n)$ ,其中 $n$ 为数组或链表长度。
|
||||
**空间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。
|
||||
|
||||
## 优缺点
|
||||
## 优点与缺点
|
||||
|
||||
在哈希表中,**查找、插入、删除操作的平均时间复杂度都为 $O(1)$** ,这意味着无论是高频增删还是高频查找场景,哈希查找的性能表现都非常好。当然,一切的前提是保证哈希表未退化。
|
||||
|
||||
|
||||
@@ -94,7 +94,18 @@ comments: true
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="linear_search.ts"
|
||||
|
||||
/* 线性查找(数组)*/
|
||||
function linearSearchArray(nums: number[], target: number): number {
|
||||
// 遍历数组
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
// 找到目标元素,返回其索引
|
||||
if (nums[i] === target) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// 未找到目标元素,返回 -1
|
||||
return -1;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
@@ -122,6 +133,12 @@ comments: true
|
||||
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="linear_search.swift"
|
||||
|
||||
```
|
||||
|
||||
再比如,我们想要在给定一个目标结点值 `target` ,返回此结点对象,也可以在链表中进行线性查找。
|
||||
|
||||
=== "Java"
|
||||
@@ -210,7 +227,19 @@ comments: true
|
||||
=== "TypeScript"
|
||||
|
||||
```typescript title="linear_search.ts"
|
||||
|
||||
/* 线性查找(链表)*/
|
||||
function linearSearchLinkedList(head: ListNode | null, target: number): ListNode | null {
|
||||
// 遍历链表
|
||||
while (head) {
|
||||
// 找到目标结点,返回之
|
||||
if (head.val === target) {
|
||||
return head;
|
||||
}
|
||||
head = head.next;
|
||||
}
|
||||
// 未找到目标结点,返回 null
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
=== "C"
|
||||
@@ -238,14 +267,20 @@ comments: true
|
||||
}
|
||||
```
|
||||
|
||||
=== "Swift"
|
||||
|
||||
```swift title="linear_search.swift"
|
||||
|
||||
```
|
||||
|
||||
## 复杂度分析
|
||||
|
||||
**时间复杂度 $O(n)$ :** 其中 $n$ 为数组或链表长度。
|
||||
**时间复杂度 $O(n)$** :其中 $n$ 为数组或链表长度。
|
||||
|
||||
**空间复杂度 $O(1)$ :** 无需使用额外空间。
|
||||
**空间复杂度 $O(1)$** :无需使用额外空间。
|
||||
|
||||
## 优缺点
|
||||
## 优点与缺点
|
||||
|
||||
**线性查找的通用性极佳。** 由于线性查找是依次访问元素的,即没有跳跃访问元素,因此数组或链表皆适用。
|
||||
**线性查找的通用性极佳**。由于线性查找是依次访问元素的,即没有跳跃访问元素,因此数组或链表皆适用。
|
||||
|
||||
**线性查找的时间复杂度太高。** 在数据量 $n$ 很大时,查找效率很低。
|
||||
**线性查找的时间复杂度太高**。在数据量 $n$ 很大时,查找效率很低。
|
||||
|
||||
Reference in New Issue
Block a user