算法题解 (双指针)

leetcode官网:https://leetcode-cn.com/problems
CS-Notes:https://www.cyc2018.xyz/


1、两数之和 II - 输入有序数组(7/21-167)

在这里插入图片描述
在这里插入图片描述


解题思路:

使用双指针,一个指针指向值较小的元素,一个指针指向值较大的元素。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。

如果两个指针指向元素的和 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;
    }
}

2、平方数之和(7/21-633)

在这里插入图片描述
在这里插入图片描述


解析:

可以看成是在元素为 0~target 的有序数组中查找两个数,使得这两个数的平方和为 target,如果能找到,则返回 true,表示 target 是两个整数的平方和。

本题和 167. Two Sum II - Input array is sorted 类似,只有一个明显区别:一个是和为 target,一个是平方和为 target。本题同样可以使用双指针得到两个数,使其平方和为 target。

本题的关键是右指针的初始化,实现剪枝,从而降低时间复杂度。设右指针为 x,左指针固定为 0,为了使 02 + x2 的值尽可能接近 target,我们可以将 x 取为 sqrt(target)。

因为最多只需要遍历一次 0~sqrt(target),所以时间复杂度为 O(sqrt(target))。又因为只使用了两个额外的变量,因此空间复杂度为 O(1)。


Java实现:

class Solution {
    public boolean judgeSquareSum(int c) {
        int left = 0, right = (int)Math.sqrt(c);
        while(left <= right) {
            int sum = left*left + right*right;
            if(sum == c) {
                return true;
            }else if(sum < c) {
                ++left;
            }else {
                --right;
            }
        }
        return false;
    }
}

在这里插入图片描述

3、反转字符串中的元音字母(7/21-345)

在这里插入图片描述


解析:

使用双指针,一个指针从头向尾遍历,一个指针从尾到头遍历,当两个指针都遍历到元音字符时,交换这两个元音字符。

为了快速判断一个字符是不是元音字符,我们将全部元音字符添加到集合 HashSet 中,从而以 O(1) 的时间复杂度进行该操作。

时间复杂度为 O(N):只需要遍历所有元素一次
空间复杂度 O(1):只需要使用两个额外变量

在这里插入图片描述
在这里插入图片描述


Java实现:

class Solution {
    //元音字母集合
    private final static HashSet<Character> vowels = new HashSet<>( //创建HashSet集合对象
        Arrays.asList('a','e','i','o','u','A','E','I','O','U')
    );

    public String reverseVowels(String s) {
        if(s == null) return null;
        int left = 0, right = s.length() -1;
        char[] result = new char[s.length()];
        while(left <= right) {
            char cl = s.charAt(left);  // charAt返回第i个位置的字符
            char cr = s.charAt(right);
            if(!vowels.contains(cl)) {
                result[left++] = cl;   //如果不包含就存如直接数组
            } else if(!vowels.contains(cr)) {
                result[right--] = cr;  //如果不包含就存如直接数组
            } else {
                result[left++] = cr;   //当左右两边都包含就先交换再存入数组
                result[right--] = cl;
            }
        }
        return new String(result); 
    }
}

在这里插入图片描述

4、验证回文字符串 Ⅱ(7/21-680)

在这里插入图片描述
在这里插入图片描述


解析:

所谓的回文字符串,是指具有左右对称特点的字符串,例如 “abcba” 就是一个回文字符串。

使用双指针可以很容易判断一个字符串是否是回文字符串:令一个指针从左到右遍历,一个指针从右到左遍历,这两个指针同时移动一个位置,每次都判断两个指针指向的字符是否相同,如果都相同,字符串才是具有左右对称性质的回文字符串。
在这里插入图片描述
本题的关键是处理删除一个字符。在使用双指针遍历字符串时,如果出现两个指针指向的字符不相等的情况,我们就试着删除一个字符,再判断删除完之后的字符串是否是回文字符串。

在判断是否为回文字符串时,我们不需要判断整个字符串,因为左指针左边和右指针右边的字符之前已经判断过具有对称性质,所以只需要判断中间的子字符串即可。

在试着删除字符时,我们既可以删除左指针指向的字符,也可以删除右指针指向的字符。
在这里插入图片描述


Java实现:

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)) {
                return isPalindrome(s, i, j - 1) || isPalindrome(s, i + 1, j);
            }
        }
        return true;
    }

    private boolean isPalindrome(String s, int i, int j) {
        while (i < j) {
            if (s.charAt(i++) != s.charAt(j--)) {
                return false;
            }
        }
        return true;
    }

}

在这里插入图片描述

5、合并两个有序数组(7/27-88)

在这里插入图片描述
在这里插入图片描述


解析:

需要从尾开始遍历,否则在 nums1 上归并得到的值会覆盖还未进行归并比较的值。


Java实现:

class Solution {
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i1 = m-1, i2 = n-1;
        int i3 = m+n-1;
        while(i2 >= 0){
            if(i1 < 0){
                nums1[i3--] = nums2[i2--];
            }else if(i2 < 0){
                nums1[i3--] = nums1[i1--];
            }else if(nums1[i1] > nums2[i2]){
                nums1[i3--] = nums1[i1--];
            }else{
                nums1[i3--] = nums2[i2--];
            }
        } 
    }
}

在这里插入图片描述

6、环形链表(7/27-141)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


解析:

使用双指针,一个指针每次移动一个节点,一个指针每次移动两个节点,如果存在环,那么这两个指针一定会相遇。


Java实现:

/**
 * 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) {
        if(head == null){
            return false;
        }
        ListNode l1 = head, l2 = head.next;
        while(l1 != null && l2 != null && l2.next != null){
            if(l1 == l2){
                return true;
            }
            l1 = l1.next;
            l2 = l2.next.next;
        }
        return false;
    }
}

在这里插入图片描述

7、通过删除字母匹配到字典里最长单词(7/27-524)

在这里插入图片描述
在这里插入图片描述


解析:

通过删除字符串 s 中的一个字符能得到字符串 t,可以认为 t 是 s 的子序列,我们可以使用双指针来判断一个字符串是否为另一个字符串的子序列。


Java实现:

class Solution {
    public String findLongestWord(String s, List<String> dictionary) {
        String longsetWord = "";
        for(String targer : dictionary){
            int l1 = longsetWord.length(), l2 = targer.length();
            if(l1 > l2 || (l1 == l2 && longsetWord.compareTo(targer) < 0)){ //longsetWord.compareTo(targer)如果此字符串小于字符串参数,则返回一个小于 0 的值;
                continue;
            }
            if(isSubstr(s,targer)){
                longsetWord = targer;
            }
        }
        return longsetWord;
    }

    //判断两个字符串是否相等
    private boolean isSubstr(String s, String targer) {
        int i = 0, j = 0;
        while(i < s.length() && j < targer.length()) {
            if(s.charAt(i) == targer.charAt(j)) {  // s.charAt(i) 表示:获取s中下标为i的字符
                j++;
            } 
            i++;
        }
        return j == targer.length();
    }
}

在这里插入图片描述

*剑指 Offer 18. 删除链表的节点

剑指 Offer 18. 删除链表的节点

在这里插入图片描述


解析:设置一个虚拟头结点再进行删除操作。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode deleteNode(ListNode head, int val) {
        ListNode dummy  = new ListNode(-1);
        dummy.next = head;
        ListNode cur = dummy;
        while(cur.next != null){
            if(cur.next.val == val){ //如果相等,就跳到下一个,就是删除
                cur.next = cur.next.next;
            }else{
                cur = cur.next; //移动到下一个
            }
        }
        return dummy.next;
    }
}

在这里插入图片描述

*剑指 Offer 22. 链表中倒数第k个节点

剑指 Offer 22. 链表中倒数第k个节点

在这里插入图片描述


解析:先让快指针移动k个节点,再同时移动,最后返回慢指针即可。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode getKthFromEnd(ListNode head, int k) {
        ListNode fast = head; //快指针
        ListNode slow = head; //慢指针
        for(int i=0; i<k; i++){
            fast = fast.next; //先让快指针移动k个节点
        }
        while(fast != null){ //再同时移动,最后返回慢指针即可
            fast = fast.next;
            slow = slow.next;
        }
        return slow;
    }
}

在这里插入图片描述

*剑指 Offer 25. 合并两个排序的链表

剑指 Offer 25. 合并两个排序的链表
在这里插入图片描述


解析:双指针,比较两个链表的节点值,哪个小就把新链表指向它,最后把剩下的直接拼接到新链表末尾。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if(l1 == null) return l2;
        if(l2 == null) return l1;
        ListNode dummy = new ListNode(-1); //创建虚拟头节点
        ListNode res = dummy; 
        while(l1 != null && l2 != null){
            if(l1.val <= l2.val){ //如果l1的值小于等于l2的值,就添加l1
                res.next = l1; //赋值
                l1 = l1.next;   //添加元素后的链接移动
            }else{  //如果l1的值大于l2的值,就添加l2
                res.next = l2;
                l2 = l2.next;
            }
            res = res.next; //新链表移动
        }
        //剩下的直接拼接到新链表后面
        if(l1 == null) res.next = l2;
        if(l2 == null) res.next = l1;
        return dummy.next;

    }
}

在这里插入图片描述

剑指 Offer 52. 两个链表的第一个公共节点

剑指 Offer 52. 两个链表的第一个公共节点

在这里插入图片描述


解析:使用双指针,先计算出两个链表的长度,然后让长的链表先移动“差值”,然后再一起移动,一起移动时碰到两个节点相同就返回,否则就一起移动到下一个节点。

注意:是节点相等,而不是值相等。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null && headB == null) return null;
        ListNode curr1 = headA;
        ListNode curr2 = headB;
         //算出两个链表的长度
        int len1=0, len2=0;
        while(curr1 != null){
            ++len1;
            curr1 = curr1.next;
        }
        while(curr2 != null){
            ++len2;
            curr2 = curr2.next;
        }
        //重置头指针
        curr1 = headA;
        curr2 = headB;
        
        //如果链表1比链表2长
        if(len1 > len2){
            //链表1先移动
            for(int i=0; i<(len1-len2); i++){
                curr1 = curr1.next;
            }
            //再同时移动,如果发现结点相等,就返回
            for(int j=0; j<len2; j++){
                if(curr1 == curr2){
                    return curr1;
                }else{
                    curr1 = curr1.next;
                    curr2 = curr2.next;
                }
            }
        }else{
            //链表2先移动
            for(int i=0; i<(len2-len1); i++){
                curr2 = curr2.next;
            }
            //再同时移动,如果发现结点相等,就返回
            for(int j=0; j<len2; j++){
                if(curr2 == curr1){
                    return curr2;
                }else{
                    curr1 = curr1.next;
                    curr2 = curr2.next;
                }
            }            
        }
        return null; 
    }
}

在这里插入图片描述

*剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

剑指 Offer 21. 调整数组顺序使奇数位于偶数前面

在这里插入图片描述


解析:双指针,分别从左右两边开始,判断如果是 偶奇,交换后都移动;奇偶,都移动;奇奇,移动左;偶偶,移动右。

class Solution {
    public int[] exchange(int[] nums) {
        if(nums == null || nums.length <= 0) return nums;
        int left = 0;
        int right = nums.length-1;
        while(left < right){
            if(nums[left] % 2 == 0 && nums[right] % 2 != 0){  //偶奇,交换后都移动
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                left++;
                right--;
            }else if(nums[left] % 2 != 0 && nums[right] % 2 == 0){ //奇偶,都移动
                left++;
                right--;               
            }else if(nums[left] % 2 != 0 && nums[right] % 2 != 0){ //奇奇,移动左
                left++;             
            }else { //偶偶,移动右
                right--;               
            }
        }
        return nums;

    }
}

在这里插入图片描述

*剑指 Offer 57. 和为s的两个数字

剑指 Offer 57. 和为s的两个数字
在这里插入图片描述


解析:双指针

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int left = 0, right = nums.length-1;
        while(left < right){
            if(nums[left] + nums[right] == target){ //相等,直接返回
                return new int[]{nums[left],nums[right]};
            }else if(nums[left] + nums[right] < target){ //和 小于 目标值,移动左
                left++;
            }else{ //和 大于 目标值,移动右
                right--;
            }
        }
        return new int[]{};

    }
}

在这里插入图片描述

剑指 Offer 58 - I. 翻转单词顺序

剑指 Offer 58 - I. 翻转单词顺序
在这里插入图片描述在这里插入图片描述


解法一:分割+倒序

class Solution {
    public String reverseWords(String s) {
        String[] new_s = s.trim().split(" "); // 删除首尾空格,分割字符串
        StringBuilder res = new StringBuilder();
        //从后往前添加单词数组
        for(int i=new_s.length-1; i>=0; i--){
            if(new_s[i].equals("")) continue; //如果遇到空单词,就跳过
            if(i != 0){
                res.append(new_s[i] + " ");
            }else{
                res.append(new_s[i]); //添加最后一个时,不需要加空格
            }
        }
        return new String(res);
    }
}

在这里插入图片描述

解法二:双指针

class Solution {
    public String reverseWords(String s) {
        s = s.trim(); // 删除首尾空格
        int j = s.length() - 1, i = j;
        StringBuilder res = new StringBuilder();
        while(i >= 0) {
            while(i >= 0 && s.charAt(i) != ' ') i--; // 搜索首个空格
            res.append(s.substring(i + 1, j + 1) + " "); // 添加单词
            while(i >= 0 && s.charAt(i) == ' ') i--; // 跳过单词间空格
            j = i; // j 指向下个单词的尾字符
        }
        return res.toString().trim(); // 转化为字符串并返回
    }
}

在这里插入图片描述

NC41 最长无重复子数组 - 3.14

NC41 最长无重复子数组

在这里插入图片描述


解析:双指针,i遍历数组,start为不重复子数组的起始位置,如果当前元素已经存在,则更新 不重复子数组的起始位置。

import java.util.*;


public class Solution {
    /**
     * 
     * @param arr int整型一维数组 the array
     * @return int整型
     */
    public int maxLength (int[] arr) {
        if(arr.length == 0) return 0;
        HashMap<Integer, Integer> map = new HashMap<>();
        int max = 0;
        //双指针,i遍历数组,start为不重复子数组的起始位置
        for(int i=0,start=0; i<arr.length; i++){
            //如果当前元素已经存在,则更新 不重复子数组的起始位置
            if(map.containsKey(arr[i])){
                start = Math.max(start, map.get(arr[i])+1);
            }
            //添加元素
            map.put(arr[i], i);
            //更新最长值
            max = Math.max(max, i-start+1);
        }
        return max;
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值