leetcode系列-双指针

数组篇

使用双指针法才展现出效率的优势:通过两个指针在一个for循环下完成两个for循环的工作。

27- 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

class Solution {
    public int removeElement(int[] nums, int val) {
        int i = 0;
        for (int j = 0; j <nums.length ; j++) {
            if (nums[j] != val){
                nums[i] = nums[j];
                i++;
            }
        }
        return i;
    }
}

复杂度分析

  • 时间复杂度:O(n)
    假设数组总共有 n个元素,i 和 j至少遍历 2n步。
  • 空间复杂度:O(1)

字符串篇

使用双指针法,**定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。**时间复杂度是O(n)。

344-反转字符串

**编写一个函数,其作用是将输入的字符串反转过来。**输入字符串以字符数组 char[] 的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。

如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。

class Solution {
    public void reverseString(char[] s) {
        int n = s.length;
        for (int left = 0,right = n-1; left < right ; left++,right--) {

            char tmp = s[left];
            s[left] = s[right];
            s[right] = tmp;
        }
    }
}

复杂度分析

时间复杂度:O(N),其中 N 为字符数组的长度。一共执行了 N/2 次的交换。
空间复杂度:O(1)。只使用了常数空间来存放若干变量。

双指针法在数组,链表和字符串中很常用。其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。

题目:剑指Offer 05.替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:
输入:s = “We are happy.”
输出:“We%20are%20happy.”

/**
 * 方法一:字符数组
 * 由于每次替换从 1 个字符变成 3 个字符,使用字符数组可方便地进行替换。
 * 建立字符数组地长度为 s 的长度的 3 倍,这样可保证字符数组可以容纳所有替换后的字符。
 *
 */
class Solution {
    public String replaceSpace(String s) {
        int length = s.length();
        char[] array = new char[length * 3];
        int size = 0;
        for (int i = 0; i < length; i++) {
            char c = s.charAt(i);
            if (c == ' ') {
                array[size++] = '%';
                array[size++] = '2';
                array[size++] = '0';
            } else {
                array[size++] = c;
            }
        }
        String newStr = new String(array, 0, size);
        return newStr;
    }
}

复杂性分析

  • 时间复杂度:O(n)。遍历字符串 s 一遍。
  • 空间复杂度:O(n)。额外创建字符数组,长度为 s 的长度的 3 倍。

151-翻转字符串里的单词

给定一个字符串,逐个翻转字符串中的每个单词。

示例 1:
输入: “the sky is blue”
输出: “blue is sky the”

这道题目通过 先整体反转再局部反转,实现了反转字符串里的单词。

class Solution {
    public String reverseWords(String s) {
        
        StringBuilder sb = trimSpaces(s);

        // 翻转字符串
        reverse(sb, 0, sb.length() - 1);

        // 翻转每个单词
        reverseEachWord(sb);

        return sb.toString();
    }

    public StringBuilder trimSpaces(String s) {
        int left = 0, right = s.length() - 1;
        // 去掉字符串开头的空白字符
        while (left <= right && s.charAt(left) == ' ') {
            ++left;
        }

        // 去掉字符串末尾的空白字符
        while (left <= right && s.charAt(right) == ' ') {
            --right;
        }

        // 将字符串间多余的空白字符去除
        StringBuilder sb = new StringBuilder();
        while (left <= right) {
            char c = s.charAt(left);

            if (c != ' ') {
                sb.append(c);
            } else if (sb.charAt(sb.length() - 1) != ' ') {
                sb.append(c);
            }

            ++left;
        }
        return sb;
    }

    public void reverse(StringBuilder sb, int left, int right) {
        while (left < right) {
            char tmp = sb.charAt(left);
            sb.setCharAt(left++, sb.charAt(right));
            sb.setCharAt(right--, tmp);
        }
    }

    public void reverseEachWord(StringBuilder sb) {
        int n = sb.length();
        int start = 0, end = 0;

        while (start < n) {
            // 循环至单词的末尾
            while (end < n && sb.charAt(end) != ' ') {
                ++end;
            }
            // 翻转单词
            reverse(sb, start, end - 1);
            // 更新start,去找下一个单词
            start = end + 1;
            ++end;
        }
    }
}

/**
 * 方法一:使用语言特性
 *使用 split 将字符串按空格分割成字符串数组;
 *使用 reverse 将字符串数组进行反转;
 *使用 join 方法将字符串数组拼成一个字符串。
 */

class Solution {
    public String reverseWords(String s) {
        // 除去开头和末尾的空白字符
        s = s.trim();
        // 正则匹配连续的空白字符作为分隔符分割
        List<String> wordList = Arrays.asList(s.split("\\s+"));
        Collections.reverse(wordList);
        return String.join(" ", wordList);
    }
}

复杂度分析

时间复杂度:O(N),其中 N 为输入字符串的长度。

空间复杂度:Java 和 Python 的方法需要 O(N)的空间来存储字符串,

链表篇

翻转链表是现场面试,白纸写代码的好题,考察了候选者对链表以及指针的熟悉程度,而且代码也不长,适合在白纸上写。

链表:听说过两天反转链表又写不出来了?中,讲如何使用双指针法来翻转链表,只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。

206-反转一个单链表。

/**方法一:迭代 : 假设链表为 1→2→3→∅,我们想要把它改成 ∅←1←2←3。
 在遍历链表时,将当前节点的 next 指针改为指向前一个节点。
 由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。
 在更改引用之前,还需要存储后一个节点。最后返回新的头引用

 */
class Solution {
    public ListNode reverseList(ListNode head) {


        ListNode pre = null;
        ListNode curr = head;

        while (curr != null){

            ListNode temp = curr.next;
            curr.next = pre;
            pre = curr;
            curr = temp;
        }
        return pre;

    }
}

复杂度分析

  • 时间复杂度:O(n),其中 n是链表的长度。需要遍历链表一次。

  • 空间复杂度:O(1)。

142 -给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

在链表中求环,应该是双指针在链表里最经典的应用,在链表:环找到了,那入口呢?中讲解了如何通过双指针判断是否有环,而且还要找到环的入口。

使用快慢指针(双指针法),分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

/**
 *方法一:哈希表
 * 我们遍历链表中的每个节点,并将它记录下来;一旦遇到了此前遍历过的节点,就可以判定链表中存在环。
 *  * 时间复杂度 O(N)
 *  * 空间复杂度 O(N)
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {

        ListNode pos = head;
        Set<ListNode> set = new HashSet<ListNode>();

        while (pos != null){
            if (set.contains(pos)){
                return pos;
            }else{
                set.add(pos);
            }
            pos = pos.next;
        }
        return null;

    }
}
/**
 * 方法二:快慢指针
 *我们使用两个指针,fast 与 slow。它们起始都位于链表的头部。
 * 随后,slow 指针每次向后移动一个位置,而 fast 指针向后移动两个位置。
 * 如果链表中存在环,则 fast 指针最终将再次与 slow 指针在环中相遇。
 *
 * 需要构造两次相遇,第一次相遇是检查是否有环,第二次是找到环的入口。
 * 时间复杂度 O(N)
 * 空间复杂度 O(1)  我们只使用了 slow,fast,ptr 三个指针
 */

public class Solution {
    public ListNode detectCycle(ListNode head) {

        ListNode fast = head,slow = head;
        while (true){
            if (fast == null || fast.next == null) return null;

            fast.next = fast.next.next;
            slow = slow.next;

            if (fast == slow) break;
        }
        fast = head;
        while (slow != fast){
            slow = slow.next;
            fast = fast.next;
        }
        return fast;
    }
}

N数之和篇

哈希表:解决了两数之和,那么能解决三数之和么?中,讲到使用哈希法可以解决1.两数之和的问题

**15- 三数之和 **

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。

时间复杂度是O(n^2)

空间复杂度:O(log n)

示例:

给定数组 nums = [-1, 0, 1, 2, -1, -4],

满足要求的三元组集合为:[ [-1, 0, 1], [-1, -1, 2] ]

/**
 * 排序 + 双指针
 * 1 特判,对于数组长度 n,如果数组为 null或者数组长度小于 3,返回[]。
 * 2 对数组进行排序。
 * 3 遍历排序后数组
 *    1 若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于0,直接返回结果
 *    2 对于重复元素:跳过,避免出现重复解
 *    3 令左指针 L=i+1,右指针 R=n-1,当 L<R 时,执行循环:
 *        1 当 nums[i]+nums[L]+nums[R]==0,执行循环,判断左界和右界是否和下一位置重复,去除重复解。
 *        并同时将 L,R移到下一位置,寻找新的解
 *        2 若和大于 0,说明 nums[R]太大,R左移
 *        3 若和小于 0,说明 nums[L] 太小,L右移
 */
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> lists = new ArrayList<>();
        Arrays.sort(nums);
        int len = nums.length;
        for (int i = 0; i < len; ++i) {
            if (nums[i] > 0) {
                return lists;
            }
            if (i > 0 && nums[i] == nums[i-1]) continue;
            int curr = nums[i];
            int L = i + 1, R = len - 1;
            while (L < R){
                int tmp = curr + nums[L] + nums[R];
                if (tmp == 0){
                    List<Integer> list = new ArrayList<>();
                    list.add(curr);
                    list.add(nums[L]);
                    list.add(nums[R]);
                    lists.add(list);
                    while ( L < R && nums[L+1] == nums[L]) ++L;
                    while ( L < R && nums[R-1] == nums[R]) --R;
                    ++L;
                    --R;
                }else if (tmp < 0){
                    ++L;
                }else{
                    --R;
                }
            }
        }
        return lists;
    }
}

双指针法:一样的道理,能解决四数之和中,讲到了四数之和,其实思路是一样的,在三数之和的基础上再套一层for循环,依然是使用双指针法。

对于三数之和使用双指针法就是将原本暴力O(n3)的解法,降为O(n2)的解法,四数之和的双指针解法就是将原本暴力O(n4)的解法,降为O(n3)的解法。

同样的道理,五数之和,n数之和都是在这个基础上累加。

18-四数之和

题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。

复杂度是O(n^3)

空间复杂度:O(log n)

/**
 * 与三数之和类似,
 * 排序 + 双指针
 *  * 1 特判,对于数组长度 n,如果数组为 null或者数组长度小于 4,返回[]。
 *  * 2 对数组进行排序。
 *  * 3 遍历排序后数组
 *  *    1  需要有两个for循环,来表示A+B的和,两两组合,如果有重复的元素,需要跳过
 *  *    2 对于重复元素:跳过,避免出现重复解
 *  *    3 令左指针 L=j+1,右指针 R=n-1,当 L<R 时,执行循环:
 *  *        1 当 nums[i]+nums[j]== target-nums[left] + nums[right],执行循环,判断左界和右界是否和下一位置重复,去除重复解。
 *  *        并同时将 L,R移到下一位置,寻找新的解
 *  *        2 若和大于 0,说明 nums[R]太大,R左移
 *  *        3 若和小于 0,说明 nums[L] 太小,L右移
 */
class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> res = new ArrayList<>();
        if(nums==null || nums.length<=3){
            return res;
        }
        //排序+双指针
        Arrays.sort(nums);
        int len = nums.length;
        for (int i = 0; i < len ; i++) {
            if (i - 1 >= 0 && nums[i] == nums[i-1]) continue;

            for (int j = i + 1;j < len;j++) {
                if (j - 1 >= i + 1 && nums[j - 1] == nums[j]) continue;

                int tr = target - nums[i] - nums[j];
                int left = j + 1, right = len - 1;
                while (left < right) {
                    if (nums[left] + nums[right] == tr) {
                        List<Integer> tp = new ArrayList<>();
                        tp.add(nums[i]);
                        tp.add(nums[j]);
                        tp.add(nums[left]);
                        tp.add(nums[right]);
                        res.add(tp);
                        while (left < right && nums[left + 1] == nums[left]) ++left;
                        while (left < right && nums[right - 1] == nums[right]) --right;
                        ++left;
                        --right;
                    } else if (nums[left] + nums[right] > tr) {
                        right--;
                    } else {
                        left++;
                    }
                }
            }
        }
    return res;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值