双指针主要用于遍历数组,两个指针指向不同的元素,从而协同完成任务。双指针部分七道虽简单但是经典的练习题如下:
1. 有序数组的 Two Sum
Input: numbers={2, 7, 11, 15},
target=9 Output: index1=1, index2=2
题目描述:在有序数组中找出两个数,使它们的和为 target。
使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。
- 如果两个指针指向元素的和 sum == target,那么得到要求的结果;
- 如果 sum > target,移动较大的元素,使 sum 变小一些;
- 如果 sum < target,移动较小的元素,使 sum 变大一些;
class Solution {
public int[] twoSum(int[] numbers, int target) {
int i=0;
int j=numbers.length-1;
int sum=0;
while(i<j){
sum=numbers[i]+numbers[j];
if(sum==target){
return new int[]{i+1,j+1};
}else if(sum>target){
j--;
}else{
i++;
}
}
return null;
}
}
2. 两数平方和
Input: 5
Output: True
Explanation: 1 * 1 + 2 * 2 = 5
题目描述:判断一个数是否为两个数的平方和。
class Solution {
public boolean judgeSquareSum(int c) {
int i=0;
int j=(int)Math.sqrt(c);
int sum=0;
while(i<=j){
sum=i*i+j*j;
if(sum==c){
return true;
}else if(sum<c){
i++;
}else{
j--;
}
}
return false;
}
}
3. 反转字符串中的元音字符
示例 1:
输入: "hello"
输出: "holle"
示例 2:
输入: "leetcode"
输出: "leotcede"
使用双指针指向待反转的两个元音字符,一个指针从头向尾遍历,一个指针从尾到头遍历。
class Solution {
public String reverseVowels(String s) {
//1.先定义一个list存储元音字母
List<Character> list = new ArrayList<>();
char[] letter=new char[] {'a','e','i','o','u','A','E','I','O','U'};
for(int i=0;i<10;i++)
list.add(letter[i]);
//2.将string转换为字符数组方便交换其中的字符
char[] tempCharArray=s.toCharArray();
//3.定义两个哨兵l,r从两边往中间遍历
int l=0;
int r=s.length()-1;
while(l<r){
char char_l=s.charAt(l);
char char_r=s.charAt(r);
char char_temp;
if(list.contains(char_l)&&list.contains(char_r)){
//交换
char_temp=char_l;
tempCharArray[l]=tempCharArray[r];
tempCharArray[r]=char_temp;
l++;
r--;
}else if(list.contains(char_l)&&!list.contains(char_r)){
r--;
}else if(!list.contains(char_l)&&list.contains(char_r)){
l++;
}else{
l++;
r--;
}
}
//4.将字符数组转换为字符串返回
String result=new String(tempCharArray);
return result;
}
}
4. 回文字符串
Input: "abca"
Output: True
Explanation: You could delete the character 'c'.
题目描述:可以删除一个字符,判断是否能构成回文字符串。
思想:双指针,从左右两端开始验证是否是回文串,若左右两边的字符不相等的时候,选择跳过左边或者右边的一个字符,再去验证一遍。
class Solution {
public boolean validPalindrome(String s) {
char[] tempCharArray=s.toCharArray();
int left=0;
int right=s.length()-1;
while(left<right){
if(tempCharArray[left]!=tempCharArray[right]){
//如果不相等,判断字串[left,right-1]或者[left+1,right]是否是回文串,如果是则返回true
return(yesOrNo(tempCharArray,left,right-1)||yesOrNo(tempCharArray,left+1,right));
}else{
left++;
right--;
}
}
return true;
}
public boolean yesOrNo(char[] s,int start,int end){
while(start<end){
if(s[start]!=s[end]){
return false;
}else{
start++;
end--;
}
}
return true;
}
}
5. 归并两个有序数组
Input:
nums1 = [1,2,3,0,0,0], m = 3
nums2 = [2,5,6], n = 3
Output: [1,2,2,3,5,6]
题目描述:把归并结果存到第一个数组上。
法一:新开辟一个m大小的数组nums1_copy存取nums1的数据,然后nums1_copy与nums2做比较,将小的值存在nums1中,再将剩余值存进去。时间开销为O(m+n),空间开销为O(M)
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int[] nums1_copy=new int[m];
System.arraycopy(nums1,0,nums1_copy,0,m);
int i=0;
int j=0;
int p=0;
while(i<m&&j<n){
nums1[p++]=nums1_copy[i]<nums2[j]?nums1_copy[i++]:nums2[j++];
}
if(i<m)//System.arraycopy(源数组,源数组起始位置,目标数组,目标数组起始位置,拷贝的长度)
System.arraycopy(nums1_copy,i,nums1,i+j,m-i);
if(j<n)
System.arraycopy(nums2,j,nums1,i+j,n-j);
}
}
法二:需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值。时间开销为O(m+n),空间开销为0即不需要开辟新的数组。
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
int index1 = m - 1, index2 = n - 1;
int indexMerge = m + n - 1;
while (index1 >= 0 || index2 >= 0) {
if(index1 < 0)
nums1[indexMerge--] = nums2[index2--];
else if(index2 < 0)
nums1[indexMerge--] = nums1[index1--];
else
nums1[indexMerge--]=nums1[index1] > nums2[index2]?nums1[index1--]:nums2[index2--];
}
}
}
6. 判断链表是否存在环
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
方法一:哈希表
思路
我们可以通过检查一个结点此前是否被访问过来判断链表是否为环形链表。常用的方法是使用哈希表(不存在重复值)。
算法
我们遍历所有结点并在哈希表中存储每个结点的引用(或内存地址)。如果当前结点为空结点 null(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,并且该链表不是环形链表。如果当前结点的引用已经存在于哈希表中,那么返回 true(即该链表为环形链表)。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
Set<ListNode> hasNode=new HashSet<>();
while(head!=null){
if(hasNode.contains(head))
return true;
else{
hasNode.add(head);
head=head.next;
}
}
return false;
}
}
复杂度分析
时间复杂度:O(n),对于含有 n 个元素的链表,我们访问每个元素最多一次。添加一个结点到哈希表中只需要花费 O(1) 的时间。空间复杂度:O(n),空间取决于添加到哈希表中的元素数目,最多可以添加 n 个元素。
方法二:双指针
思路
想象一下,两名运动员以不同的速度在环形赛道上跑步会发生什么?
算法
通过使用具有 不同速度 的快、慢两个指针遍历链表,空间复杂度可以被降低至 O(1)O(1)。慢指针每次移动一步,而快指针每次移动两步。如果列表中不存在环,最终快指针将会最先到达尾部,此时我们可以返回 false。
现在考虑一个环形链表,把慢指针和快指针想象成两个在环形赛道上跑步的运动员(分别称之为慢跑者与快跑者)。而快跑者最终一定会追上慢跑者。这是为什么呢?考虑下面这种情况(记作情况 A)- 假如快跑者只落后慢跑者一步,在下一次迭代中,它们就会分别跑了一步或两步并相遇。其他情况又会怎样呢?例如,我们没有考虑快跑者在慢跑者之后两步或三步的情况。但其实不难想到,因为在下一次或者下下次迭代后,又会变成上面提到的情况 A。总结:快指针总是比慢指针快一步,如果存在环,总会追上慢指针,二者相同则退出循环,返回true.如果不存在环,则快指针早早到达终点为null
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
// 法一:使用HashSet
// public boolean hasCycle(ListNode head) {
// Set<ListNode> hasNode=new HashSet<>();
// while(head!=null){
// if(hasNode.contains(head))
// return true;
// else{
// hasNode.add(head);
// head=head.next;
// }
// }
// return false;
// }
// 法二:使用快慢双指针
public boolean hasCycle(ListNode head) {
if(head==null||head.next==null)
return false;
ListNode slow=head;
ListNode fast=head.next;
while(slow!=fast){
if(fast==null||fast.next==null)
return false;
else{
fast=fast.next.next;//走两步拉开距离
slow=slow.next;
}
}
return true;
}
}
7. 最长子序列
Input:
s = "abpcplea", d = ["ale","apple","monkey","plea"]
Output:
"apple"
题目描述:删除 s 中的一些字符,使得它构成字符串列表 d 中的一个字符串,找出能构成的最长字符串。如果有多个相同长度的结果,返回字典序的最小字符串。
通过删除字符串 s 中的一个字符能得到字符串 t,可以认为 t 是 s 的子序列,我们可以使用双指针来判断一个字符串是否为另一个字符串的子序列。
class Solution {
public String findLongestWord(String s, List<String> d) {
String longestStr="";//存取当前字典中最长的串。或者相同长度但是字典序靠前的串儿
for(String target:d){
if((longestStr.length()>target.length())||(longestStr.length()==target.length()&&longestStr.compareTo(target)<0))
continue;//结束当前循环
//字典中的串是子串,且比longestStr长或者一样长但是字典序靠前
if(isSubStr(s,target)){
longestStr=target;//串赋值
}
}
return longestStr;
}
public boolean isSubStr(String s,String target){
int j=0;
for(int i=0;i<s.length()&&j<target.length();i++){
if(s.charAt(i)==target.charAt(j))
j++;
}
return j==target.length();//target串遍历完了,说明target是s的子串
}
}