————技巧点:对撞指针即左右开工
\quad \quad 对撞指针是双指针的一种。对撞指针是指在有序数组中,将指向最左侧的索引定义为左指针(left),最右侧的定义为右指针(right),然后从两头向中间进行数组遍历。对撞指针的终止条件是两个指针相遇。对撞指针常用于排序数组或者是两端交换元素位置。
1、两数之和II—输入有序数组(167)
题目描述:
【简单题】
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2。
说明:
- 返回的下标值(index1 和 index2)不是从零开始的。
- 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
思路分析:
要求:在有序数组中找到两值所处索引位置+1
约束:两数相加之和等于目标值
题解一:暴力法—两层遍历
\quad \quad 最直观解法,即暴力解法,遍历数组每个元素 i,并查找除此元素之外的其余元素是否存在一个值与 target - i相等的目标元素。
- 双层遍历:i从0到len-1,j从i+1到len-1
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
res=[]# 存放结果
for i in range(len(numbers)):
for j in range(i+1,len(numbers)):
if numbers[j]==target-numbers[i]:
res=[i+1,j+1]
return res
注:这种方法虽然可以解答,但leetCode通不过,会超出时间限制,故此题不建议用暴力法,需另辟它径。
-
时间复杂度: O ( n 2 ) O(n^2) O(n2)
对于每个元素,我们试图通过遍历数组的其余部分来寻找它所对应的目标元素,这将耗费 O(n)的时间。因此时间复杂度为 O ( n 2 ) O(n^2) O(n2)
-
空间复杂度: O ( 1 ) O(1) O(1)。
\quad \quad 显然,暴力解法没有充分利用原数组的性质–有序,谈到有序数组,首先必须想到什么?二分查找!于是第二种思路就可以按照二分搜索对时间复杂度进行优化。
题解二:二分查找
- 依次来遍历每一个元素i,
- 对于每一个i,都在剩余的有序数组中,使用二分查找的思路,寻找target-nums[i],找到即返回
[i+1,mid+1]
,否则继续遍历。
- 对于每一个i,都在剩余的有序数组中,使用二分查找的思路,寻找target-nums[i],找到即返回
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
for i in range(len(numbers)):
l=i+1
r=len(numbers)
while l<r:
mid=l+(r-l)//2 #中间位置
if numbers[mid]==target-numbers[i]:# 说明已找到另一值,则停止循环,返回结果
return [i+1,mid+1]#题目要求返回的下标值是从1开始的,而语言索引是从零开始的,故要在结果中加一
elif numbers[mid]<target-numbers[i]:
l=mid+1
else:
r=mid
- 时间复杂度:
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)
遍历为n,二分搜索为logn,总体的时间复杂度为:nlogn - 空间复杂度: O ( 1 ) O(1) O(1)
题解三:对撞指针(双指针的一个特殊组合)
\quad \quad
在前几个题目中,为了避免额外开辟新的数组空间,于是用了两个指针,来做交换删除之类的操作,同样这里,为了少一层循环,我们也可以考虑多用一个指针索引。寻找两个指针索引,两个索引代表的数字和是target,因为数组有序,故其一定是一左一右存在,那么初始时两个指针l,r
分别指向第一个元素位置和最后一个元素的位置。这样的双指针就可叫做对撞指针。
每次计算两个指针l,r
指向的两个元素之和,并和目标值比较。
当l<r
,执行以下操作:(因为不可以是重复元素,所以循环条件l < r)
-
如果两个元素之和等于目标值,则发现了唯一解
-
如果两个元素之和小于目标值,则将左侧指针右移一位。即让
l++
,由于整个数组有序,故这个位置会比原来位置的值更大; -
如果两个元素之和大于目标值,则将右侧指针左移一位即
r--
;由于有序,得到的结果会比上一次更小。 -
移动指针之后,重复上述操作,直到找到答案。
那么小套路–对撞指针,就是指的是用两个索引,向中间的方向不断前行,就能找到所给的答案。
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
l=0
r=len(numbers)-1
while l<r:
if numbers[l]+numbers[r]==target:
return [l+1,r+1]
elif numbers[l]+numbers[r]<target:
l+=1
else:#numbers[l]+numbers[r]>target
r-=1
- 时间复杂度:
O
(
n
)
O(n)
O(n)
其中 n 是数组的长度。两个指针移动的总次数最多为 n 次。 - 空间复杂度: O ( 1 ) O(1) O(1)
2、验证回文串(125)
题目描述:
【简单题】
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
题目链接
思路分析:
要求:判断是否是回文串?
什么是回文串?
\quad \quad 回文串就是指一串字符,无论是从前往后读或者从后往前读都是一样的。也就是说倒数第一与正数第一、倒数第二与正数第二,…是一样的。比如说“wow”,“level”,不会因为读的前后顺序而不同。
\quad \quad 对于回文串问题,即比较左右两端相对应位置的元素是否相同,就可以利用左右对撞指针的思想,设前后两个指针,一个指针从头进行,一个指针从尾部进行。依次判断两个指针的字符是否相等,只要有一个不相等,就返回False。当两个指针相遇的时候循环停止跳出。
字符串问题中常要考虑的几个问题是:
1.空字符串怎么处理?
本题目中已经将空字符串定义为有效的回文串,故不用特殊判断处理,默认循环完返回true就可以;
2.大小写问题
本题目中忽略字母的大小写,而忽略字母大小写的套路做法是:将对应的字母统一转化为大写或小写,再比较大写或小写的字母是否相同。
3.跳过非法字符
这里指除数字或字母的其他字符。
Python中有字符串方法isalnum(),用来检测字符串是否由字母和数字组成,是的话返回true,如果没有库的话,可以用判断字符的ascii码来实现。
isalnum()函数
描述:检测字符串是否由字母和数字组成。
语法:str.isalnum() -> bool 返回值为布尔类型(True,False)
str中至少有一个字符且所有字符都是字母或数字则返回 True,否则返回 False
class Solution:
def isPalindrome(self, s: str) -> bool:
l=0 #左指针索引
r=len(s)-1#右指针索引
while l<r:
# 每次while都需要判断下l<r,保证大前提正确
while l<r and not s[l].isalnum():#左指针遇到非法字符,加一
l+=1
while l<r and not s[r].isalnum():#右指针遇到非法字符,减一,寻找到字母或数字那一索引
r-=1
if s[l].upper()!=s[r].upper():#都转化为大写字母再比较,如果不相等,则返回False
return False
# while循环中要改变循环变量,每次比较一对,左指针右移1,右指针左移1
l+=1
r-=1
return True
- 时间复杂度: O ( n ) O(n) O(n),遍历了一遍数组
- 空间复杂度: O ( 1 ) O(1) O(1)
3、反转字符串(344)
题目描述:
【简单题】
编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[]
的形式给出。
不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。
题目链接
思路分析:
要求:反转即倒序输出
约束:原地修改
\quad \quad 同样,使用对撞指针的思路,一个指针从前到后,一个指针从后向前,然后两个指针相互交换,当两个指针相同时跳出循环。
class Solution:
def reverseString(self, s: List[str]) -> None:
"""
Do not return anything, modify s in-place instead.
"""
i,j=0,len(s)-1
while i<j:
s[i],s[j]=s[j],s[i]
i+=1
j-=1
- 时间复杂度: O ( N ) O ( N ) O(N),进行了 N / 2 N / 2 N/2次的交换
- 空间复杂度: O ( 1 ) O ( 1 ) O(1)
4、反转字符串中的元音字母(345)
题目描述:
【简单题】
编写一个函数,以字符串作为输入,反转该字符串中的元音字母
题目链接
思路分析:
要求:仅对元音字母进行反转
1、反转两字,同样可以使用对撞指针思想进行解决。从前到后,从后到前,遇到两个元音字母的元素进行交换。
这时要注意,传入的为字符串,属于不可变对象,故要将字符串转化为list,交换后,再将list拼接为字符串。
基础知识点:
1.str->list: list(str)
2.list->str:’’.join(list)
2、仅对元音字母进行反转,因此我们在对撞的时候还要判断当前两指针元素是否为元音字母:建立一个大写的元音字母表,若元素在其中,则为元音字母。
大致思路:
-
初始化左右指针,l,r=0,len-1
-
建立元音字母表
v=['A','E','I','O','U']
-
将字符串转化为列表形式
-
当l<r:执行以下操作
- 当l<r且左指针当前指向元素的大写形式不在元音字母表,l++
- 当l<r且右指针当前指向元素的大写形式不在元音字母表,r- -
- 交换指针元素
- l++,r- -
-
返回列表的字符串形式。
class Solution:
def reverseVowels(self, s: str) -> str:
l=0
r=len(s)-1
v=['A','E','I','O','U']#元音字母表
list_s=list(s)#将字符串转换为列表的形式
while l<r:
while l<r and list_s[l].upper() not in v:
l+=1
while l<r and list_s[r].upper() not in v:
r-=1
list_s[l],list_s[r]=list_s[r],list_s[l]
l+=1
r-=1
return ''.join(list_s)
- 时间复杂度: O ( N ) O ( N ) O(N)
- 空间复杂度: O ( n ) O ( n ) O(n)
5、盛最多水的容器(11)
题目描述:
【中等题】
给你 n 个非负整数 a1,a2,…,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。
题目链接
思路分析:
要求:最大矩阵容器可容纳水的面积
\quad \quad 容器的面积与两个因素有关:
- 容器的长度:两条垂直线的距离即输入数组所在索引的差值
- 容器的宽度:两条垂直线其中较短一条的长度即数组中所在位置的数值的最小值。
因此,要容器面积最大化,两条垂直线的距离越远越好,两条垂直线的最短长度也要越长越好。
我们设置两个指针 left 和 right,分别指向数组的最左端和最右端。此时,两条垂直线的距离是最远的,若要下一个矩阵面积比当前面积来得大,必须要把 height[left] 和 height[right] 中较短的垂直线往中间移动,看看是否可以找到更长的垂直线。
题解:对撞指针法
-
初始化指针:l=0,r=len-1
-
初始化为最大容器面积为其最小小值maxarea=0
-
当l<r:
- 计算当前容器面积
curarea=min(list[l],list[r])*(r-l)
【list为数组】 - 更新最大容器面积为当前容器面积与最大容器面积的最大值
maxarea=max(maxarea,curarea)
移动指针,找到更多可行解 - 如果
list[l]<list[r]
,则l++
- 否则,r- -
- 计算当前容器面积
-
返回maxarea
class Solution:
def maxArea(self, height: List[int]) -> int:
l,r=0,len(height)-1
maxarea=0
while l<r:
curarea=min(height[l],height[r])*(r-l)
maxarea=max(maxarea,curarea)
if height[l]<height[r]:
l+=1
else:
r-=1
return maxarea
- 时间复杂度: O ( n ) O(n) O(n)
- 空间复杂度: O ( 1 ) O(1) O(1)
6、平方数之和(633)
题目描述:
【中等题】
给定一个非负整数 c ,你要判断是否存在两个整数 a 和 b,使得
a
2
+
b
2
=
c
a^2 + b^2 = c
a2+b2=c 。
示例1
输入:c = 5
输出:true
解释:1 * 1 + 2 * 2 = 5
题目链接
思路分析:
1、本题和 167. 两数之和类似,只有一个明显区别:一个是和为 target,一个是平方和为 target。本题同样可以使用双指针得到两个数,使其平方和为 target。
2、本题的关键是右指针的初始化,实现剪枝,从而降低时间复杂度。设右指针为 x,左指针固定为 0,为了使 0 2 + x 2 0^2 + x^2 02+x2的值尽可能接近 target,我们可以将 x 取为 sqrt(target)。
class Solution:
def judgeSquareSum(self, c: int) -> bool:
i,j=0,int(c**0.5)
while i<=j:
if i*i+j*j==c:
return True
elif i*i+j*j>c:
j-=1
elif i*i+j*j<c:
i+=1
return False
- 时间复杂度: O ( c ) O(\sqrt c) O(c)
- 空间复杂度: O ( 1 ) O(1) O(1)