Files
leetcode-master/problems/0035.搜索插入位置.md
2025-05-19 17:11:04 +08:00

550 lines
16 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

* [做项目多个C++、Java、Go、测开、前端项目](https://www.programmercarl.com/other/kstar.html)
* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html)
* [背八股40天挑战高频面试题](https://www.programmercarl.com/xunlian/bagu.html)
# 35.搜索插入位置
[力扣题目链接](https://leetcode.cn/problems/search-insert-position/)
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
* 输入: [1,3,5,6], 5
* 输出: 2
示例 2:
* 输入: [1,3,5,6], 2
* 输出: 1
示例 3:
* 输入: [1,3,5,6], 7
* 输出: 4
示例 4:
* 输入: [1,3,5,6], 0
* 输出: 0
## 思路
这道题目不难,但是为什么通过率相对来说并不高呢,我理解是大家对边界处理的判断有所失误导致的。
这道题目,要在数组中插入目标值,无非是这四种情况。
![35_搜索插入位置3](https://file1.kamacoder.com/i/algo/20201216232148471.png)
* 目标值在数组所有元素之前
* 目标值等于数组中某一个元素
* 目标值插入数组中的位置
* 目标值在数组所有元素之后
这四种情况确认清楚了,就可以尝试解题了。
接下来我将从暴力的解法和二分法来讲解此题,也借此好好讲一讲二分查找法。
### 暴力解法
暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。
C++代码
```CPP
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
for (int i = 0; i < nums.size(); i++) {
// 分别处理如下三种情况
// 目标值在数组所有元素之前
// 目标值等于数组中某一个元素
// 目标值插入数组中的位置
if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i]那么i就是我们要的结果
return i;
}
}
// 目标值在数组所有元素之后的情况
return nums.size(); // 如果target是最大的或者 nums为空则返回nums的长度
}
};
```
* 时间复杂度O(n)
* 空间复杂度O(1)
效率如下:
![35_搜索插入位置](https://file1.kamacoder.com/i/algo/20201216232127268.png)
### 二分法
既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。
![35_搜索插入位置4](https://file1.kamacoder.com/i/algo/202012162326354.png)
大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。
以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。**
同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的。
大体讲解一下二分法的思路这里来举一个例子例如在这个数组中使用二分法寻找元素为5的位置并返回其下标。
![35_搜索插入位置5](https://file1.kamacoder.com/i/algo/20201216232659199.png)
二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好。
相信很多同学对二分查找法中边界条件处理不好。
例如到底是 `while(left < right)` 还是 `while(left <= right)`,到底是`right = middle`呢,还是要`right = middle - 1`呢?
这里弄不清楚主要是因为**对区间的定义没有想清楚,这就是不变量**。
要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查)。
### 二分法第一种写法
以这道题目来举例,以下的代码中定义 target 是在一个在左闭右闭的区间里,**也就是[left, right] (这个很重要)**。
这就决定了这个二分法的代码如何去写,大家看如下代码:
**大家要仔细看注释思考为什么要写while(left <= right) 为什么要写right = middle - 1**
```CPP
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n - 1; // 定义target在左闭右闭的区间里[left, right]
while (left <= right) { // 当left==right区间[left, right]依然有效
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle;
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0, -1]
// 目标值等于数组中某一个元素 return middle;
// 目标值插入数组中的位置 [left, right]return right + 1
// 目标值在数组所有元素之后的情况 [left, right] 因为是右闭区间,所以 return right + 1
return right + 1;
}
};
```
* 时间复杂度O(log n)
* 空间复杂度O(1)
效率如下:
![35_搜索插入位置2](https://file1.kamacoder.com/i/algo/2020121623272877.png)
### 二分法第二种写法
如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) 。
那么二分法的边界处理方式则截然不同。
不变量是[left, right)的区间,如下代码可以看出是如何在循环中坚持不变量的。
**大家要仔细看注释思考为什么要写while (left < right) 为什么要写right = middle**
```CPP
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int n = nums.size();
int left = 0;
int right = n; // 定义target在左闭右开的区间里[left, right) target
while (left < right) { // 因为left == right的时候在[left, right)是无效的空间
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
// 分别处理如下四种情况
// 目标值在数组所有元素之前 [0,0)
// 目标值等于数组中某一个元素 return middle
// 目标值插入数组中的位置 [left, right) return right 即可
// 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right
return right;
}
};
```
* 时间复杂度O(log n)
* 空间复杂度O(1)
## 总结
希望通过这道题目,大家会发现平时写二分法,为什么总写不好,就是因为对区间定义不清楚。
确定要查找的区间到底是左闭右开[left, right),还是左闭又闭[left, right],这就是不变量。
然后在**二分查找的循环中,坚持循环不变量的原则**,很多细节问题,自然会知道如何处理了。
## 其他语言版本
### Java
```java
class Solution {
public int searchInsert(int[] nums, int target) {
int n = nums.length;
// 定义target在左闭右闭的区间[low, high]
int low = 0;
int high = n - 1;
while (low <= high) { // 当low==high区间[low, high]依然有效
int mid = low + (high - low) / 2; // 防止溢出
if (nums[mid] > target) {
high = mid - 1; // target 在左区间,所以[low, mid - 1]
} else if (nums[mid] < target) {
low = mid + 1; // target 在右区间,所以[mid + 1, high]
} else {
// 1. 目标值等于数组中某一个元素 return mid;
return mid;
}
}
// 2.目标值在数组所有元素之前 3.目标值插入数组中 4.目标值在数组所有元素之后 return right + 1;
return high + 1;
}
}
```
```java
//第二种二分法:左闭右开
public int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length;
while (left < right) { //左闭右开 [left, right)
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
// 目标值在数组所有元素之前 [0,0)
// 目标值插入数组中的位置 [left, right) return right 即可
// 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right
return right;
}
```
### C#
```go
public int SearchInsert(int[] nums, int target) {
var left = 0;
var right = nums.Length - 1;
while (left <= right) {
var curr = (left + right) / 2;
if (nums[curr] == target)
{
return curr;
}
if (target > nums[curr]) {
left = curr + 1;
}
else {
right = curr - 1;
}
}
return left;
}
```
### Golang
```go
// 第一种二分法
func searchInsert(nums []int, target int) int {
left, right := 0, len(nums)-1
for left <= right {
mid := left + (right-left)/2
if nums[mid] == target {
return mid
} else if nums[mid] > target {
right = mid - 1
} else {
left = mid + 1
}
}
return right+1
}
```
### Rust
```rust
impl Solution {
pub fn search_insert(nums: Vec<i32>, target: i32) -> i32 {
use std::cmp::Ordering::{Equal, Greater, Less};
let (mut left, mut right) = (0, nums.len() as i32 - 1);
while left <= right {
let mid = (left + right) / 2;
match nums[mid as usize].cmp(&target) {
Less => left = mid + 1,
Equal => return mid,
Greater => right = mid - 1,
}
}
right + 1
}
}
```
### Python
```python
# 第一种二分法: [left, right]左闭右闭区间
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
middle = (left + right) // 2
if nums[middle] < target:
left = middle + 1
elif nums[middle] > target:
right = middle - 1
else:
return middle
return right + 1
```
```python
# 第二种二分法: [left, right)左闭右开区间
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)
while (left < right):
middle = (left + right) // 2
if nums[middle] > target:
right = middle
elif nums[middle] < target:
left = middle + 1
else:
return middle
return right
```
### JavaScript
```js
var searchInsert = function (nums, target) {
let l = 0, r = nums.length - 1, ans = nums.length;
while (l <= r) {
const mid = l + Math.floor((r - l) >> 1);
if (target > nums[mid]) {
l = mid + 1;
} else {
ans = mid;
r = mid - 1;
}
}
return ans;
};
```
### TypeScript
```typescript
// 第一种二分法
function searchInsert(nums: number[], target: number): number {
const length: number = nums.length;
let left: number = 0,
right: number = length - 1;
while (left <= right) {
const mid: number = Math.floor((left + right) / 2);
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] === target) {
return mid;
} else {
right = mid - 1;
}
}
return right + 1;
};
```
### Swift
```swift
// 暴力法
func searchInsert(_ nums: [Int], _ target: Int) -> Int {
for i in 0..<nums.count {
if nums[i] >= target {
return i
}
}
return nums.count
}
// 二分法
func searchInsert(_ nums: [Int], _ target: Int) -> Int {
var left = 0
var right = nums.count - 1
while left <= right {
let middle = left + ((right - left) >> 1)
if nums[middle] > target {
right = middle - 1
}else if nums[middle] < target {
left = middle + 1
}else if nums[middle] == target {
return middle
}
}
return right + 1
}
```
### Scala
```scala
object Solution {
def searchInsert(nums: Array[Int], target: Int): Int = {
var left = 0
var right = nums.length - 1
while (left <= right) {
var mid = left + (right - left) / 2
if (target == nums(mid)) {
return mid
} else if (target > nums(mid)) {
left = mid + 1
} else {
right = mid - 1
}
}
right + 1
}
}
```
### PHP
```php
// 二分法(1)[左闭右闭]
function searchInsert($nums, $target)
{
$n = count($nums);
$l = 0;
$r = $n - 1;
while ($l <= $r) {
$mid = floor(($l + $r) / 2);
if ($nums[$mid] > $target) {
// 下次搜索在左区间:[$l,$mid-1]
$r = $mid - 1;
} else if ($nums[$mid] < $target) {
// 下次搜索在右区间:[$mid+1,$r]
$l = $mid + 1;
} else {
// 命中返回
return $mid;
}
}
return $r + 1;
}
```
### C
```c
//版本一 [left, right]左闭右闭区间
int searchInsert(int* nums, int numsSize, int target){
//左闭右开区间 [0 , numsSize-1]
int left =0;
int mid =0;
int right = numsSize - 1;
while(left <= right){//左闭右闭区间 所以可以 left == right
mid = left + (right - left) / 2;
if(target < nums[mid]){
//target 在左区间 [left, mid - 1]中原区间包含mid右区间边界可以向左内缩
right = mid -1;
}else if( target > nums[mid]){
//target 在右区间 [mid + 1, right]中,原区间包含mid左区间边界可以向右内缩
left = mid + 1;
}else {
// nums[mid] == target 顺利找到target直接返回mid
return mid;
}
}
//数组中未找到target元素
//target在数组所有元素之后[left, right]是右闭区间,需要返回 right +1
return right + 1;
}
```
```c
//版本二 [left, right]左闭右开区间
int searchInsert(int* nums, int numsSize, int target){
//左闭右开区间 [0 , numsSize)
int left =0;
int mid =0;
int right = numsSize;
while(left < right){//左闭右闭区间 所以 left < right
mid = left + (right - left) / 2;
if(target < nums[mid]){
//target 在左区间 [left, mid)中原区间没有包含mid右区间边界不能内缩
right = mid ;
}else if( target > nums[mid]){
// target 在右区间 [mid+1, right)中原区间包含mid左区间边界可以向右内缩
left = mid + 1;
}else {
// nums[mid] == target 顺利找到target直接返回mid
return mid;
}
}
//数组中未找到target元素
//target在数组所有元素之后[left, right)是右开区间, return right即可
return right;
}
```