二分法和移除元素
二分注意点
1、建议理解后背熟一套模板,不要搞混
2、另外需要注意,二分的使用前提:有序数组
二分的最大优势是在于其时间复杂度是O(logn),因此看到有序数组都要第一时间反问自己是否可以使用二分。
3、混乱点:while(left<right),while(left<=right) ; right = mid-1,right=mid。
首先要对区间做明确定义:左闭右闭 ; 左闭右开
区间定义是循环中的不变量,在左闭右闭的前提条件下,while(left<=right)是合法的,因为left=right是合法的,相反,while(left<right)就是不对的,因为未包含left=right的情况。
If ( nums[mid] < target ) : right = mid -1 ;因为已经明确判断,mid所代表的值小于target,不应该包含在区间 [ left,right] 中。
704 二分查找
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
左闭右闭
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)-1
while left <= right :
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return -1
小记1 : 将mid的计算放在循环开始,而不是循环前一次+循环末尾,可以加速(力扣上执行速度从打败11%增长到65%)
左闭右开
class Solution:
def search(self, nums: List[int], target: int) -> int:
left = 0
right = len(nums)
while left < right :
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid
else:
return mid
return -1
35 搜索插入位置
给定一个排序(升序)数组(无重复元素)和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法。
左闭右闭
错误代码
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
left = 0
right = n-1
if nums[left] > target:
return 0
elif nums[right] < target:
return n
while left <= right :
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
if nums[mid] < target:
return mid+1
else:
return mid-1
正确代码
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
left = 0
right = n-1
if nums[0] > target:
return 0
elif nums[n-1] < target:
return n
while left <= right :
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return left
左闭右开
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
left = 0
right = n
if nums[0] > target:
return 0
elif nums[n-1] < target:
return n
while left < right :
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid
else:
return mid
return left
力扣示例代码(简洁)
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
left = 0
right = n
while left < right :
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
else:
right = mid
return left
小记:只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法,同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的。
代码随想录解答代码
两个版本。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
left = 0
right = n-1
while left <= right :
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid - 1
else:
return mid
return left
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
left = 0
right = n
while left < right :
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
elif nums[mid] > target:
right = mid
else:
return mid
return left
题目总结
一开始加了很多判断条件,判断target是否在数据大小内,后续发现这些判断是不必要的。同时,也要注意while退出的情况,据此判断应该return什么。
后面二分法代码不再区分,默认写左闭右闭。
34 排序数组查找元素位置区间
小记:这题一看就说明数组是有重复的。
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
标记:此题毫无思路,直接看的代码随想录的解答。
解答思路:
先把所有情况都讨论一下:
寻找target在数组里的左右边界,有如下三种情况:
情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}
情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
这三种情况都考虑到,说明就想的很清楚了。
接下来,在去寻找左边界,和右边界了。
采用二分法来去寻找左右边界,为了让代码清晰,分别写两个二分来寻找左边界和右边界。
刚刚接触二分搜索的同学不建议上来就想用一个二分来查找左右边界,很容易把自己绕进去,建议扎扎实实的写两个二分分别找左边界和右边界
解法一:
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
if n==0:
return [-1,-1]
left = 0
right = n-1
tl = -2
tr = -2
# 寻找右边界
while(left<=right):
mid = left + (right-left)//2
if nums[mid] > target:
right = mid - 1
else:
left = mid + 1
tr = left
# 寻找左边界
left = 0
right = n-1
while(left<=right):
mid = left + (right-left)//2
if nums[mid] < target:
left = mid + 1
else:
right = mid - 1
tl = right
if tl==-2 or tr==-2:
return [-1,-1]
elif tr-tl>1:
return [tl+1,tr-1]
else:
return [-1,-1]
小记:此题还有其他解法,比如先用二分法找到某一个元素,然后左右滑动指针。其他解法一刷未看,在此给出代码随想录网站的链接。
链接: 代码随想录34题
力扣给出的参考示例
class Solution:
def searchRange(self, nums, target):
if not nums:
return [-1, -1]
if len(nums) == 1:
if nums[0] == target:
return [0,0]
else:
return [-1,-1]
def findLeft(nums, target):
if nums[0] == target:
return 0
L, R = 1, len(nums) - 1
while L < R:
m = L + (R - L) // 2
if nums[m] > target:
R = m
elif nums[m] < target:
L = m + 1
elif nums[m] == target:
R = m
if nums[L] != target:
return -1
return L
def findRight(nums, target):
if nums[len(nums) - 1] == target:
return len(nums) - 1
L, R = 0, len(nums) - 2
while L < R:
m = L + (R - L) // 2
if nums[m] > target:
R = m
elif nums[m] < target:
L = m + 1
elif nums[m] == target:
if nums[m + 1] == target:
L = m + 1
else:
return m
return L
a = findLeft(nums,target)
if a == -1:
return [-1,-1]
b = findRight(nums,target)
return [a,b]
69 X的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
我的代码
class Solution:
def mySqrt(self, x: int) -> int:
if x == 1 or x == 0 :
return x
left = 1
right = x//2
while(left<=right):
mid = left + (right-left)//2
target = mid*mid
if target > x :
right = mid - 1
else :
left = mid + 1
return left - 1
小记:向二分法上靠拢,题目要求不许使用指数函数和开方函数,但没说不让使用 X*X 这种乘法运算啊,其实这种思路我一开始是不认可的,但又想不出其他的。
另外要注意的点就是,要把x=1的情况分类出来,自己代码第一次提交错误就在这里。
同时有一个自己认为的小聪明,直接取x//2作为最大,这是从数学上推理的结果。
力扣给出的参考示例
class Solution:
def mySqrt(self, x: int) -> int:
if x == 0:
return 0
ans = int(math.exp(0.5 * math.log(x)))
return ans + 1 if (ans + 1) ** 2 <= x else ans
记录:不是不让使用指数函数吗,你给的这是什么破玩意?
一位录友的解法
int mySqrt(int x)
{
if(x == 1)
return 1;
int min = 0;
int max = x;
while(max-min>1)
{
int m = (max+min)/2;
if(x/m<m)
max = m;
else
min = m;
}
return min;
}
小记:用x/m<m而不是m*m>x防止溢出。
367 有效的完全平方数
给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false 。
完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。
不能使用任何内置的库函数,如 sqrt 。
我的代码
class Solution:
def isPerfectSquare(self, num: int) -> bool:
if num == 1 :
return True
left = 1
right = num//2
while(left<=right):
mid = left + (right-left)//2
target = mid*mid
if target > num :
right = mid - 1
elif target < num :
left = mid + 1
else :
return True
return False
小记:牛逼!和力扣给出的参考示例一模一样。
移除元素注意点
1、数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。
2、双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。一定要弄懂快慢指针的定义。
快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
慢指针:指向新数组下标的位置
27 移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
我的代码
class Solution:
def removeElement(self, nums: List[int], val: int) -> int:
slow = 0
n = len(nums)
for fast in range(n):
if nums[fast] != val :
nums[slow] = nums[fast]
slow = slow + 1
return slow
力扣的示例代码
用了内置remove函数,离谱
26 删除有序数组中的重复项
给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。返回 k 。
我的代码
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
slow = 0
n = len(nums)
for fast in range(1,n):
temp = nums[slow]
if nums[fast] != temp :
slow = slow + 1
nums[slow] = nums[fast]
return slow+1
小记:这里按照我的编程逻辑,return的值,要有+1的,即必须为slow+1,因为我默认了第一个元素直接保留,在提交的时候,就是这一点没注意,看了一会才反应过来。
力扣的示例代码
class Solution:
def removeDuplicates(self, nums: List[int]) -> int:
if not nums:
return 0
i, j = 1, 1
while j < len(nums):
if nums[j] != nums[j-1]:
nums[i] = nums[j]
i += 1
j += 1
return i
小记:示例这里判断了空数组的情况。
283 移动零
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
我的代码
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
slow = 0
n = len(nums)
for fast in range(n):
if nums[fast] != 0 :
nums[slow] = nums[fast]
slow = slow + 1
if slow < n :
for i in range(slow,n):
nums[i] = 0
return nums
力扣的示例代码
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
non_zero_index = 0
for i in range(len(nums)):
if nums[i]:
nums[non_zero_index] = nums[i]
non_zero_index += 1
for i in range(non_zero_index, len(nums)):
nums[i] = 0
return
小记:我的代码和示例代码一致,均为双指针法。
844 比较含退格的字符串
给定 s 和 t 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true 。# 代表退格字符。
注意:如果对空文本输入退格字符,文本继续为空。
示例 1:
输入:s = “ab#c”, t = “ad#c”
输出:true
解释:s 和 t 都会变成 “ac”。
我的代码
class Solution:
def backspaceCompare(self, s: str, t: str) -> bool:
s = list(s)
t = list(t)
s_slow = 0
n = len(s)
for fast in range(n):
if s[fast] != '#' :
s[s_slow] = s[fast]
s_slow = s_slow + 1
else:
if s_slow > 0 :
s_slow = s_slow - 1
t_slow = 0
n = len(t)
for fast in range(n):
if t[fast] != '#' :
t[t_slow] = t[fast]
t_slow = t_slow + 1
else:
if t_slow > 0 :
t_slow = t_slow - 1
if s[0:s_slow] == t[0:t_slow]:
return True
else:
return False
小记:这里注意,会有一种情况,退格字符个数 >(大于) 之前的字符数,所以慢指针在退格的时候,要保证最小值为0,不能减为-1。在第一次提交代码时出错,看了报错输入才反应出来。
还有一个点,python中,字符串是可以索引的,但是是不可更改对象,所以此题要先将字符串转换为List。
力扣的示例代码
class Solution:
def backspaceCompare(self, s: str, t: str) -> bool:
s_skip, t_skip=0,0
i,j = len(s)-1, len(t)-1
while i >=0 or j>=0:
while i >= 0:
if s[i] == "#":
s_skip += 1
i -= 1
elif s_skip > 0:
s_skip -= 1
i -= 1
else:
break
while j >= 0:
if t[j] == "#":
t_skip += 1
j -= 1
elif t_skip > 0:
t_skip -= 1
j -= 1
else:
break
if i>= 0 and j >=0:
if s[i] != t[j]:
return False
elif i >= 0 or j >= 0:
return False
i -= 1
j -= 1
return True
小记:我自己写的代码,内存占用过多,把str转为了List做的,其实根本不需要这样做,力扣给出的示例代码也是双指针法的一种变种实现,从后向前遍历字符串,不需要额外内存!
值得学习!!!
977 有序数组的平方
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
此题没思路,不知道怎么用双指针,看了录友的解答。
某位录友的代码
class Solution {
public int[] sortedSquares(int[] nums) {
int i = nums.length;
int left = 0;
int right = i-1;
int[] res = new int[i];
while(left<=right){
i--;
if(nums[left]+nums[right]<0){
res[i] = nums[left]*nums[left];
left++;
}else{
res[i] = nums[right]*nums[right];
right--;
}
}
return res;
}
}
小记:从后向前比较,比较时不取平方。
我的代码
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
n = len(nums)
left = 0
right = n-1
res = nums.copy()
while left <= right :
n = n - 1
if nums[left] + nums[right] < 0 :
res[n] = nums[left] * nums[left]
left = left + 1
else:
res[n] = nums[right] * nums[right]
right = right - 1
return res
小记:此题用双指针的思想,也要重新申请一块存储空间,只在原数组上操作会造成很高的时间复杂度,当然这也算是空间换时间了。
如果是在可以使用原数组这一块空间,那么就是先算平方后排序了,涉及的就是多种排序算法,应该在训练营的后面会有涉及吧。
力扣的示例代码
class Solution:
def sortedSquares(self, nums: List[int]) -> List[int]:
return sorted(num * num for num in nums)
直接调用了排序函数,无语。
一段用于复制的标题
我的代码
力扣的示例代码
参考:
上述部分内容来自:力扣官网,代码随想录。