双指针
图解双指针原理
双指针法的一个共同特征就是可以找到 一对下标,而且这对下面满足一个约束条件,那就是
- i 和 j 都是合法的下标,即 i < n, j < n
- i < j
然后我们希望从中找到满足题目的条件,即 A[i]与 A[j] 之间存在一个直接关系,返回下标 (i, j)(i,j)。
以 n = 8n=8 为例,这时候全部的搜索空间是:
每一个格子就代表这一种情况,如果使用暴力破解的话,那么整个正方形就是我们的搜索空间,那么时间复杂度就是O(n^2)。 而使用双指针的话,那么因为约束条件的限制,我们的搜索空间就成为了上三角部分,此时的时间复杂度仍然为O(n^2),要想得到 O(n)
的解法,我们就需要能够一次排除多个单元格。那么我们来看看,双指针解法是如何削减搜索空间的:
- 假设我们以
167. Two Sum II - Input array is sorted
为例子解释:
一开始,我们检查右上方单元格 (0, 7)
,即计算 A[0] 与A[7]
之间的关系 ,与 target
进行比较。如果不相等的话,则要么大于 target,要么小于 target
。
假设此时 A[0] + A[7] 小于 target
。这时候,我们应该去找和更大的两个数。由于 A[7] 已经是最大的数了,其他的数跟 A[0] 相加,和只会更小。也就是说 A[0] + A[6] 、A[0] + A[5]、……、A[0] + A[1] 也都小于 target,这些都是不合要求的解,可以一次排除。这相当于 i=0i 的情况全部被排除。对应用双指针解法的代码,就是 i++,对应于搜索空间,就是削减了一行的搜索空间,如下图所示。
排除掉了搜索空间中的一行之后,我们再看剩余的搜索空间,仍然是倒三角形状。我们检查右上方的单元格 (1,7)
,计算 A[1] + A[7] 与 target
进行比较。
假设此时 A[0] + A[7] 大于 target
。这时候,我们应该去找和更小的两个数。由于 A[1] 已经是当前搜索空间最小的数了,其他的数跟 A[7] 相加的话,和只会更大。也就是说 A[1] + A[7] 、A[2] + A[7]、……、A[6] + A[7] 也都大于 target,这些都是不合要求的解,可以一次排除。这相当于 j=0的情况全部被排除。对应用双指针解法的代码,就是 j++,对应于搜索空间,就是削减了一列的搜索空间,如下图所示。
可以看到,无论 A[i] + A[j] 的结果是大了还是小了,我们都可以排除掉一行或者一列的搜索空间。经过 n步以后,就能排除所有的搜索空间,检查完所有的可能性。
搜索空间的减小过程如下面动图所示:
167. Two Sum II - Input array is sorted
描述:
- Given an array of integers that is already sorted in ascending order,find two numbers such that they add up to a specific target number.
The function twoSum should return indices of the two numbers such that
they add up to the target, where index1 must be less than index2.
说明:
- Your returned answers (both index1 and index2) are not zero-based.
You may assume that each input would have exactly one solution and you may not use the same element twice.
例子:
Input: numbers = [2,7,11,15], target = 9
Output: [1,2]
Explanation: The sum of 2 and 7 is 9. Therefore index1 = 1, index2 = 2.
分析:
使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。
- 如果两个指针指向元素的和
sum == target
,那么得到要求的结果; - 如果
sum > target
,移动较大的元素,使sum
变小一些; - 如果
sum<target
,移动较小的元素,使sum
变大一些。
数组中的元素最多遍历一次,时间复杂度为O(N)。
只使用了两个额外变量,空间复杂度为 O(1)。
源码:
Java版本
class Solution {
public int[] twoSum(int[] numbers, int target) {
if (numbers == null) return null;
int i = 0, j = numbers.length - 1;
while (i < j) {
int sum = numbers[i] + numbers[j];
if (sum == target) {
return new int[]{i + 1, j + 1};
} else if (sum < target) {
i++;
} else {
j--;
}
}
return null;
}
}
Python版本
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
i = 0
j = len(numbers) - 1
while i < j:
if numbers[i] + numbers[j] == target:
return [i+1, j+1]
elif numbers[i] + numbers[j] > target:
j -= 1
else:
i += 1
633. Sum of Square Numbers
描述:
Given a non-negative integer c, your task is to decide whether there’re two integers a and b such that a2 + b2 = c.
例子:
Example 1:
Input: 5
Output: True
Explanation: 1 * 1 + 2 * 2 = 5
Example 2:
Input: 3
Output: False
分析:
源码:
Python版本
class Solution:
def judgeSquareSum(self, c: int) -> bool:
b = int(c**0.5)
a = 0
while(a<=b):
if a*a+b*b == c:
return True
elif a*a+b*b<c:
a = a+1
else:
b = b-1
return False
Java版本
class Solution {
public boolean judgeSquareSum(int c) {
if(c<0)return false;
else{
int j = (int)Math.sqrt(c);
int i = 0;
while(i<=j){
if(i*i + j*j == c) return true;
else if (i*i + j*j < c) i++;
else j--;
}
return false;
}
}
}
345. Reverse Vowels of a String
描述
Write a function that takes a string as input and reverse only the vowels of a string.
The vowels does not include the letter “y”.
例子
Example 1:
Input: "hello"
Output: "holle"
Example 2:
Example 1:
Input: "leetcode"
Output: "leotcede"
分析
Java源码
class Solution {
private HashSet<Character> vowels = new HashSet<Character>(){{
add('a');
add('e');
add('i');
add('o');
add('u');
add('A');
add('E');
add('I');
add('O');
add('U');
}};
public String reverseVowels(String s) {
int i = 0;
int j = s.length()-1;
char sArr[] = s.toCharArray();
while(i<j){
while(i<j && !vowels.contains(sArr[j])){
j--;
}
while(i<j && !vowels.contains(sArr[i])){
i++;
}
if (sArr[i] != sArr[j]) {
char temp = sArr[j];
sArr[j] = sArr[i];
sArr[i] = temp;
}
i++;
j--;
}
return String.valueOf(sArr);
}
}
680. 验证回文字符串 Ⅱ
描述
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
示例:
示例 1:
输入: "aba"
输出: True
示例 2:
输入: "abca"
输出: True
解释: 你可以删除c字符。
注意:字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。
分析:
我的思路有两种解法:
方法一:暴力破解法
暴力法在字符串中的大部分题目都是可以使用的,因为暴力,通用性大,但是效率及其的低下,因此也不建议使用。
首先思路就是对给定的字符串中的每个索引 i,然后每次删除一个元素之后再进行判断是否为回文数,那么总共就要判断n+1次,每次需要比较n个字符。只要有一次为回文数,那么我们将返回 true
,否则返回 false
。
时间复杂度:O(n^2)
空间复杂度:O(n)
方法二:双指针法
使用双指针法很容易判断一个字符串是否为回文数,因为一个指针从左往右遍历 ,一个从右往左遍历。如果两个指针指向用一个字符,那么两个指针同时向中间靠拢。
本题的难点在于可以删除一个字符,那么既可以删除左边的字符,也可以删除右遍的字符。
源码:
为改进版
class Solution {
public boolean validPalindrome(String s) {
//双指针循环找出不等于的字符索引
for(int i=0 ,j = s.length()-1;i<j;i++,j--){
if(s.charAt(i)!= s.charAt(j)){
// 左边和右边删除都做一次,只要有一次为true,最终的结果都为true
return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j);
}
}
return true;
}
// 验证是否为回文数
public boolean isPalindrome(String s,int i,int j){
while(i<j){
if(s.charAt(i)!= s.charAt(j)){
return false;
}else{
i++;
j--;
}
}
return true;
}
}
改进版
class Solution {
public boolean validPalindrome(String s) {
char[] chars = s.toCharArray();
int i = 0;
int j = chars.length - 1;
//双指针循环找出不等于的字符索引
while (i < j && chars[i] == chars[j]) {
i++;
j--;
}
//删除左边循环判断
if (isValid(chars,i + 1,j)) return true;
//删除右边循环判断
if (isValid(chars,i,j - 1)) return true;
//如果上面都是false,那么结果肯定是false
return false;
}
//验证是否是回文
private boolean isValid(char[] chars,int i,int j) {
while (i < j) {
if (chars[i++] != chars[j--]) {
return false;
}
}
return true;
}}
charAt(i)与char[i]的效率区别
@Test
public void test1(){
String s = "";
for (int i = 0; i < 100000; i++) {
s+= "*";
}
long start_time;
start_time = System.nanoTime();
System.out.println(s.charAt(6666));
System.out.println("遍历单个charAt为:"+(System.nanoTime()-start_time));
start_time = System.nanoTime();
for (int i = 0; i < s.length(); i++) {
char char1 = s.charAt(i);
}
System.out.println("循环遍历每一个charAt为:"+(System.nanoTime()-start_time));
char[] chars;
start_time = System.nanoTime();
chars = s.toCharArray();
System.out.println(chars[6666]);
System.out.println("遍历单个toCharArray为:"+(System.nanoTime()-start_time));
start_time = System.nanoTime();
chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
char char2 = chars[i];
}
System.out.println("循环遍历每一个toCharArray为:"+(System.nanoTime()-start_time));
}
通过实验可以发现,char[] chars = s.toCharArray();通过一个字符数组的形式来遍历的效率要比直接用字符串的方式调用charAt()方法要快。但是内存方面都需要在底层创建一个数组,所用的内存空间大致是相同的。