Content
题目一:704. 二分查找
1. 题目描述
2. 解题思路
看到题目第一反应index
函数,原谅早已把数据结构算法抛之脑后也没系统学过
- 该题目核心确定
left
和right
边界指示变量,while
循环即可 - 前置条件:数组有序+无重复元素(有重复元素也可返回下标,不一定是该元素第一次在数组中出现的下标)
- 左闭右闭其实和左闭右开核心都一样,只不过左闭右开中右边界为虚拟边界
3. 代码
3.1 左闭右闭
class Solution:
def search(self, nums: List[int], target: int) -> int:
list_len = len(nums)
left = 0
right = list_len - 1
while left <= right:
middle = (left + right) // 2
if target > nums[middle]:
left = middle + 1
elif target < nums[middle]:
right = middle - 1
else:
return middle
return -1
3.2 左闭右开
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)
while left < right:
middle = (left + right) // 2
if target > nums[middle]:
left = middle + 1
elif target < nums[middle]:
right = middle
else:
return middle
return -1
题目二:35. 搜索插入位置
1. 题目描述
2. 题目分析
该题目类似于# 704,但是返回条件略微复杂
- 前提:数组无重复元素
- 复杂之处在于要针对
while
循环退出时的情况进行精确分析(这里不分析找到元素的情况,因为该情况和# 704相同)
2.1 左闭右开
2.1.1 解题思路
- 左闭右开的优点在于
while
循环的退出条件是固定的,即left==right
首先确定left
和right
如何变化,left
由middle+1
得到,right
则直接等于middle
, 首先证明left
移动时永远不会大于right
, 即middle+1<=right恒成立
证明:
∵
m
i
d
d
l
e
=
⌊
(
l
e
f
t
+
r
i
g
h
t
)
/
2
⌋
∴
m
i
d
d
l
e
+
1
=
⌊
(
l
e
f
t
+
r
i
g
h
t
)
/
2
⌋
+
1
∵
l
e
f
t
移动之前
l
e
f
t
<
r
i
g
h
t
恒成立
∴
l
e
f
t
≤
r
i
g
h
t
−
1
恒成立
∴
(
m
i
d
d
l
e
+
1
)
∗
2
=
⌊
(
l
e
f
t
+
r
i
g
h
t
)
/
2
⌋
∗
2
+
2
≤
⌊
(
r
i
g
h
t
−
1
+
r
i
g
h
t
)
/
2
⌋
∗
2
+
2
≤
(
r
i
g
h
t
−
1
)
∗
2
+
2
≤
2
∗
r
i
g
h
t
∴
m
i
d
d
l
e
+
1
≤
r
i
g
h
t
\begin{aligned} &\because middle = \lfloor(left + right) / 2\rfloor \\ &\therefore middle + 1 = \lfloor(left + right) / 2\rfloor+1\\ &\because left移动之前left\lt{right}恒成立\\ &\therefore{left}\le{right-1}恒成立\\ &\begin{aligned} \therefore (middle + 1)*2 &= \lfloor(left+right)/2\rfloor*2+2 \\ &\le{\lfloor(right-1+right)/2\rfloor*2+2} \\ &\le{(right-1)*2+2} \\ &\le{2*right} \end{aligned}\\ &\therefore middle+1\le{right} \end{aligned}
∵middle=⌊(left+right)/2⌋∴middle+1=⌊(left+right)/2⌋+1∵left移动之前left<right恒成立∴left≤right−1恒成立∴(middle+1)∗2=⌊(left+right)/2⌋∗2+2≤⌊(right−1+right)/2⌋∗2+2≤(right−1)∗2+2≤2∗right∴middle+1≤right
关键在于middle
由向下取整获得,其实默想一下向下取整再加1永远不会超出右边界right
还需证right
移动时永远不会小于left
, 即left<=middle恒成立,显然成立
因此退出while
循环时,left==right
- 退出时
left==right
,这里取left
进一步分析,此时存在两种情况0<=left<len(nums)
,此时小于目标值target<nums[left]
返回left
即可,大于则返回left+1
left==len(nums)
,该情况也是容易遗漏的一点,left
不断向right
靠拢最后等于right
,如果right
一直等于数组长度(虚拟边界),此时nums[left]
不存在,直接返回left
即可
2.1.2 代码
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
list_len = len(nums)
left = 0
right = list_len
while left < right:
middle = (left + right) // 2
if nums[middle] < target:
left = middle + 1
elif nums[middle] == target:
return middle
else:
right = middle
if left == len(nums) or target < nums[left]:
return left
else:
return left + 1
2.2 左闭右闭
2.2.1 解题思路
- 左闭右闭的优点在于
left
不会超出数组边界 - 与左闭右开不同的区别在于
right=middle-1
会使得存在right<left
的情况,导致while
循环退出时不一定left==right
注意到这些不同,针对代码局部修改即可
2.2.2 代码
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
list_len = len(nums)
left = 0
right = list_len - 1
while left < right:
middle = (left + right) // 2
if nums[middle] < target:
left = middle + 1
elif nums[middle] == target:
return middle
else:
right = middle - 1
if target < nums[left] or left == right and nums[left] == target:
return left
else:
return left + 1
题目三:27. 移除元素
1. 题目描述
简单描述为删除指定列表中值等于目标值的所有元素,返回新列表的长度,新列表元素顺序可打乱。
2. 题目分析
2.1 暴力解法
2.1.1 解题思路
该题目需要先对列表元素逐个进行排查,如果该元素等于目标值val
,则从该元素往后找到非val
的值与该元素进行替换。即:
- 找到
val
值时,for
循环或者while
循环向后依次遍历直到找到非val
值 - 替换
val
值和非val
值
注:如果第二层for
循环或者while
循环一直遍历到最后一个元素才找到非val
值或一直遍历到最后一个元素都没找到val
值,则说明整个列表已经遍历完毕(即没有元素再需要被替换),此时可以直接结束程序
2.1.2 代码
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
total_len = 0
for i in range(len(nums)):
if nums[i] != val:
total_len += 1
continue
else:
j = i + 1
while j < len(nums):
if nums[j] != val:
nums[i], nums[j] = nums[j], nums[i]
total_len += 1
break
j += 1
if j >= len(nums) - 1:
return total_len
return total_len
2.2 双指针法
2.2.1 解题思路
双指针法指的是用一个后指针从列表尾部向前检索,直到找到非val
值,以此替换暴力解法中的第二层for
循环或者while
循环,时间复杂度为O(n)
2.2.2 代码
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
left = 0
right = len(nums) - 1
total_len = 0
while left <= right:
if nums[left] != val:
total_len += 1
left += 1
else:
if nums[right] != val:
nums[left], nums[right] = nums[right], nums[left]
total_len += 1
left += 1
right -= 1
return total_len
2.2.3 扩展 (代码训练营官方代码)
该方法使用右指针前后探索,左指针每次只移动一步。
优点
- 时间复杂度同样为
O(n)
- 不改变相对元素位置。
- 只用右指针作为循环条件会保证找到的每一个非
val
值都会被移到最前面,所以左指针可以随意覆盖(不用判断是否等于val
)
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
# 快指针遍历元素
fast = 0
# 慢指针记录位置
slow = 0
for fast in range(len(nums)):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
return slow
2.3 总结
- 双指针优点在于代码简便、时间复杂度低,缺点在于自定义的双指针代码会打乱原列表元素相对位置
- 暴力解法(加上注)优点在于不会打乱原列表元素的顺序,缺点在于时间复杂度略高
两种解法都需要考虑到特殊情况以及边界条件,特殊情况为
- [1], 1 (只有一个元素,即被删除元素)
- [1], 2 (只有一个元素,该元素不等于被删除元素)
- [], 1 (空列表)