文章链接:代码随想录 (programmercarl.com)
数组是存放在连续内存空间上的相同类型数据的集合。“连续”体现在以下2个方面:
(1)内存空间地址的连续性。这里需要注意不同的编程语言对于返回地址的表示方式不同。文章中以二维数组为例,C++可以返回相差4位的连续地址;但是对于Java则是虚拟机处理之后的结果,看起来是“离散的”,所以Java可能的存储方式是使用连续的一维数组(行),而行索引则是链表的形式(图源:代码随想录)。
(2)对于数组的增删引起的整体平移,元素不能删除,只能被覆盖。以删除为例,数组在删除元素的同时,会把后面所有的元素往前平移,造成一定程度上的内存开销。
视频链接:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili
简单的二分查找,需要注意两种不同写法的边界值处理。
左闭右开[left, right)和左闭右闭[left, right]的写法不同:对于闭的位置处,即right是否取。
1、如果right可以取到,即[left, right]:首先在声明的时候就 right = len(nums)-1 保证是合法的数组索引;然后在循环处传入合法的区间,即while left <= right;在更新新的边界时,注意在nums[middle] > target时,已经排除了middle位置,故要保证接下来的循环中传入while判断的区间依然合法,故更新right = middle - 1。
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
# 双边闭区间写法
left, right = 0, len(nums) - 1 # 注意right可以取到
while left <= right: # 传入闭区间
middle = left + (right - left) // 2 #索引从0开始的表示方式
if nums[middle] > target:
right = middle - 1 # 保证下次传入while的还是合法区间(right取到)
elif nums[middle] < target: # 注意!if之后的其他互斥条件用elif,不然会跳过到else
left = middle + 1
else:
return middle # 其他的情况是middle == target
return -1 # 没找到,返回异常值
2、如果right取不到,即[left, right):首先声明时需要 right = len(nums),此时right的索引是nums最后一个的后一位,是开区间边界;所以循环判断需要改为 while lef < right,保证是合法区间;更新right时需要注意right = middle。
class Solution(object):
def search(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: int
"""
# 左闭右开区间写法
left, right = 0, len(nums) # 注意right不能取到
while left < right: # 传入开区间
middle = left + (right - left) // 2
if nums[middle] > target:
right = middle # 此处已经判断middle不是target,所以不用-1
elif nums[middle] < target:
left = middle + 1
else:
return middle
return -1
文章链接:代码随想录 (programmercarl.com)
本题思路和上文一致,找到第一个比自己小的就返回他的索引,用二分查找即可。代码和上一题一致(也是两个版本),只需要把最后的return 换成right + 1(比每一个都大的情况下返回最大索引+1)
文章链接:代码随想录 (programmercarl.com)
情况 | 和左边界 | 和右边界 | 是否存在 | 输出 |
---|---|---|---|---|
1 | 小于 | 大于 | 否 | [-1, -1] |
2 | 大于等于 | 小于等于 | 否 | [-1, -1] |
3 | 大于等于 | 小于等于 | 是 | [ : ] |
三种情况如上,排除异常情况下,所以只需要获取左右边界值对应的索引输出即可,我们分两步来实现。每一步都是在704二分法的基础上进行修改或合并(本题采取左右闭区间方法)。
右边界:
# 二分法找右边界(最后一个)
def getRightBorder(nums, target):
left, right = 0, len(nums) - 1
rightborder = -2
while left <= right:
middle = left + (right - left) // 2
if target < nums[middle]:
right = middle -1
else: # 相当于把 target == num[middle] 和 target > nums[middle]合并成一个情况,都需要更新left,来找到最后一个target出现的索引
left = middle + 1
rightborder = left
return rightborder
左边界:
#二分法找左边界(第一个)
def getLeftBorder(nums, target):
left, right = 0, len(nums) - 1
leftborder = -2
while left <= right:
middle = left + (right - left) // 2
if nums[middle] < target:
left = middle + 1
else: # 同理可以做出合并
right = middle -1
leftborder = right
return leftborder
然后分别对三种情况进行计算,完整代码如下:
class Solution(object):
def searchRange(self, nums, target):
"""
:type nums: List[int]
:type target: int
:rtype: List[int]
"""
# 二分法找右边界(最后一个)
def getRightBorder(nums, target):
left, right = 0, len(nums) - 1
rightborder = -2
while left <= right:
middle = left + (right - left) // 2
if target < nums[middle]:
right = middle -1
else: # 相当于把 target == num[middle] 和 target > nums[middle]合并成一个情况,都需要更新left,来找到最后一个target出现的索引
left = middle + 1
rightborder = left
return rightborder
#二分法找左边界(第一个)
def getLeftBorder(nums, target):
left, right = 0, len(nums) - 1
leftborder = -2
while left <= right:
middle = left + (right - left) // 2
if nums[middle] < target:
left = middle + 1
else: # 同理可以做出合并
right = middle -1
leftborder = right
return leftborder
rightborder = getRightBorder(nums, target)
leftborder = getLeftBorder(nums, target)
# 接下来分三种情况完成输出
# 情况一
if leftborder == -2 or rightborder == -2: # 说明没找到
return [-1,-1]
# 情况三
if rightborder - leftborder > 1:
return [leftborder + 1, rightborder - 1] # 注意!
# 情况二
return [-1,-1]
这题放在这里是有一种解法可以用二分法迭代找平方根
由于x的完全平方根肯定在[0,x]内,所以只需要在这个闭区间内使用二分查找,对704的二分查找做修改,由于现在是求出整数部分,所以相当于是找一个最小值的上界,那么对于以上代码进行修改:
class Solution(object):
def mySqrt(self, x):
"""
:type x: int
:rtype: int
"""
left, right = 0, x # 平方根闭区间设定为[0, x]
ans = -1 # 初始化
while left <= right:
middle = left + (right - left) // 2
if middle * middle <= x:
left = middle + 1
ans = middle # 相当于是找一个上界,可以和上面的getrightBorder对比
else:
right = middle - 1
return ans
这题输入输出上有一个细节:输出是修改后数组的长度n,根据官方的解释,后台会检查数组前n个是否正确。
所以[2, 2, 0, 0]和[2, 2, 3, 3]在n=2输出上没有差别(雾)
1、暴力求解,嵌套循环。只要找到了一个需要删除的就把后面的全部往前移动一格。时间复杂度。这里需要注意,第一次写的时候用了range函数,无法AC的原因是i -= 1这一步根本不会被执行!
class Solution(object):
def removeElement(self, nums, val):
"""
:type nums: List[int]
:type val: int
:rtype: int
"""
n = len(nums)
i = 0
while i < n:
# 注意!!这里不能用库函数“for i in range(n)”
# 因为range是封装好的,每次i++,不会执行i -= 1的操作!!!
if nums[i] == val:
for j in range(i+1, n):
nums[j-1] = nums[j]
# 当前这个位置已经是新的i-1了,所以数组的长度和当前的索引位置都需要更改
i -= 1
n -= 1
i += 1
return n
2、双指针(这几道题都有涉及到“原地”操作,今后看到原地操作数组可以考虑双指针)。本质上是一个fast用来遍历数组的value,负责“查找”和“判断”,另外一个slow用来“跟进”和“保存”position。其关键是在fast识别到需要删除的元素之后,保持slow不变,也就是索引位置不变,这样fast进入到下一个值的时候就可以正常实现更新。由于过程实现在原始数组上,是一个“原地”in_place操作,所以需要时间复杂度
class Solution(object):
def removeElement(self, nums, val):
"""
:type nums: List[int]
:type val: int
:rtype: int
"""
# 双指针
n = len(nums)
fast, slow = 0, 0
for fast in range(n):
if nums[fast] != val:
nums[slow] = nums[fast]
slow += 1
n -= 1
return slow # 最后slow指向的就是数组的最后一个索引+1,也就是数组长度
我们可以总结出双指针的基本框架:
n = len(nums)
fast, slow = 0, 0 # 初始化位置可以修改(一端同向、双端相向)
for fast in range(n):
if nums[fast] != val: # 不满足删除条件的保留在原数组
nums[slow] = nums[fast]
slow += 1
n -= 1
本题和上一题一致的解法,只需要注意把判断条件更换为fast当前值和前一个值的比较。
class Solution(object):
def removeDuplicates(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
n = len(nums)
fast, slow = 1, 1 # 初始化trick:第一个值一定是unique的,所以 0 不用考虑
while fast < n:
if nums[fast - 1] != nums[fast]: # 此处修改为判断前一个和现在这一个fast索引的值是否相等即可
nums[slow] = nums[fast]
slow += 1
fast += 1
return slow
1、本人偷懒的思路:两个for循环,一个从头开始,执行删除,一个从slow+1处开始,赋值0,居然过了(喜)。
class Solution(object):
def moveZeroes(self, nums):
"""
:type nums: List[int]
:rtype: None Do not return anything, modify nums in-place instead.
"""
n = len(nums)
fast, slow = 0, 0
# Step 1: 完成0的删除
for fast in range(n):
if nums[fast] != 0:
nums[slow] = nums[fast]
slow += 1
# Step 2: 完成末尾0的复制
for i in range(slow, n):
nums[i] = 0
return n
2、双指针+python元素交换:常规做法,slow标记当前非0位置的末尾,fast查找下一个非0的位置,查询到之后,两个数字交换位置即可。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
n = len(nums)
slow = fast = 0
while fast < n:
if nums[fast] != 0:
nums[slow], nums[fast] = nums[fast], nums[slow] # python特有的交换规则
slow += 1
fast += 1
由于“#”在后,往前删除,所以从后往前遍历,依次比较没有被删掉的值,出现一个不一样的直接退出返回False。需要一个指针来查询,一个计数器保存当前“#”需要删除的个数。
class Solution(object):
def backspaceCompare(self, s, t):
"""
:type s: str
:type t: str
:rtype: bool
"""
s_len, t_len = len(s) - 1, len(t) - 1
skip_s, skip_t = 0, 0
# 在二者还存在一个没有遍历完成的时候
while s_len >= 0 or t_len >= 0:
# Step 1:查询现在需要删除的个数,返回没有被删除的索引
while s_len >= 0:
if s[s_len] == '#':
skip_s += 1 # 查询到“#”计数器 + 1
s_len -= 1。# 索引往前移动 1
elif skip_s > 0:
skip_s -= 1 # 使用一次“#”计数器 - 1
s_len -= 1 # 索引往前移动 1
else:
break
while t_len >= 0:
if t[t_len] == '#':
skip_t += 1
t_len -= 1
elif skip_t > 0:
skip_t -= 1
t_len -= 1
else:
break
# Step 2:比较索引位置数值
if s_len >= 0 and t_len >= 0:
if s[s_len] != t[t_len]:
return False
elif s_len >= 0 or t_len >= 0:
return False
# Step 3:比较正确,更新索引位置
s_len -= 1
t_len -= 1
return True
双指针+相向遍历:由于数组是非降序排列的,所以我们可以假设大的值一定是出现在两端(或其中一端)。所以构建两个指针,一个指向开头,一个指向结尾,每次比较后移动较大(或等于)的那一个,从后往前保存到ans。
class Solution(object):
def sortedSquares(self, nums):
"""
:type nums: List[int]
:rtype: List[int]
"""
n = len(nums)
ans = [0] * n # 初始化保存答案的数组
i, j, ans_pos = 0, n-1, n-1 # pos索引是ans保存的位置
while i <= j: # 相向而行的结束判据
if nums[i] * nums[i] > nums[j] * nums[j]:
ans[ans_pos] = nums[i] * nums[i]
i += 1 # 只要移动较大的指针即可
else:
ans[ans_pos] = nums[j] * nums[j]
j -= 1
ans_pos -= 1 # 每次比较完pos 向前移动 1
return ans
1、二分查找可以用于搜索数组数值类,需要注意选取的区间对于判断条件取等号、是否需要-1等的影响;
2、双指针解决数组修改类,需要注意单向和双向两种思路;
3、注意一些循环和判据的细节(for range和while的区别)。
第一天结束