雙指針主要用於遍歷數組,兩個指針指向不同的元素,從而協同完成任務。雙指針部分七道雖簡單但是經典的練習題如下:
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的子串
}
}