数组题目总结 -- 双指针

文章介绍了使用快慢指针解决数组和链表中的重复元素问题,以及如何应用双指针进行二分查找、求两数之和、反转字符串和判断回文串。还讨论了删除排序链表中的重复元素和移动零的问题,提供了不同的解题思路和代码实现。
摘要由CSDN通过智能技术生成

① 快慢指针:

一. 删除有序数组中的重复项

1. 思路和代码

I. 博主的做法:

  • slow充当指针,如果后面的值和slow指向的值不一样,那么就在slow的下一位置保存这个新的值(),同时更新slow指针。这样巧妙的节省了fast这个指针。
class Solution {
    public int removeDuplicates(int[] nums) {

        int slow = 0;

        int length = nums.length;

        for(int i = 1; i < nums.length; i++){
            if(nums[i] == nums[slow])
                length--;
            else
                nums[++slow] = nums[i];
        }
        return length;
    }
}
  • 这里的lenth完全可以不定义,直接返回slow+1就可以,也表示的是数组的长度。
  • 修改后代码为:
class Solution {
    public int removeDuplicates(int[] nums) {
        if(nums.length == 0)
            return 0; 
            
        int slow = 0;

        for(int i = 1; i < nums.length; i++)
            if(nums[i] != nums[slow])
                nums[++slow] = nums[i];

        return slow + 1;
        //return ++slow
    }
}
  • 这里不能return slow++,因为这样slow还没更新,就被返回了。也可以return ++slow。

II. 东哥的做法:

  • 我们让慢指针 slow 走在后面,快指针 fast 走在前面探路,找到一个不重复的元素就赋值给 slow 并让 slow 前进一步。这样,就保证了 nums[0…slow] 都是无重复的元素,当 fast 指针遍历完整个数组 nums 后,nums[0…slow] 就是整个数组去重之后的结果。
  • 这里,fast相当于我思路中循环中的i。
class Solution {
    public int removeDuplicates(int[] nums) {
        if(nums.length == 0)
            return 0; 

        int fast = 0;
        int slow = 0;

        while(fast < nums.length){
            if(nums[fast] != nums[slow])
                nums[++slow] = nums[fast];
            fast++;
        }
        return slow + 1;
    }

}

2. 总结

  • 简单的快慢指针问题,没什么可说的。

二. 删除排序链表中的重复元素(扩展)

1. 思路和代码

I. 博主的做法:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if(head == null)
            return null;
            
        ListNode slow = head;

        for(ListNode i = slow; i != null; i = i.next)
            if(slow.val != i.val){
                slow.next = i;
                slow = slow.next;
            }

        
        slow.next = null;
        return head;
    }
}

II. 东哥的做法:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
   public ListNode deleteDuplicates(ListNode head) {
        if (head == null) return null;
        ListNode slow = head, fast = head;
        while (fast != null) {
            if (fast.val != slow.val) {
                // nums[slow] = nums[fast];
                slow.next = fast;
                // slow++;
                slow = slow.next;
            }
            // fast++
            fast = fast.next;
        }
        // 断开与后面重复元素的连接
        slow.next = null;
        return head;
    }
}
  • 这里可能有读者会问,链表中那些重复的元素并没有被删掉,就让这些节点在链表上挂着,合适吗?

    • 这就要探讨不同语言的特性了,像 Java/Python 这类带有垃圾回收的语言,可以帮我们自动找到并回收这些「悬空」的链表节点的内存,而像 C++ 这类语言没有自动垃圾回收的机制,确实需要我们编写代码时手动释放掉这些节点的内存。

2. 总结

  • 思路和数组的差不多,不再赘述。

三. 移除元素

1. 思路和代码

I. 博主的做法:

  • 若寻找的元素和需要删除的值相等:
    • 并且这个元素是第一个与删除值相等的元素,那么用temp保存此元素下标;否则,直接跳过这个元素。
  • 不相等:
    • 用后面的元素依次覆盖前面的元素,达到删除的目的。
class Solution {
    public int removeElement(int[] nums, int val) {
        if(nums.length == 0)
            return 0;

        int temp = 0;
        boolean flag = false;

        for(int i = 0; i < nums.length; i++){
            if(nums[i] == val){
                if(flag == false){
                    temp = i;
                    flag = true;
                }
                else continue;
            }
            else
                nums[temp++] = nums[i];


        }

        return temp;
    }

}
  • else continue;这一行可以优化掉。
  • 想了想,flag也可以给它优化掉,直接用temp代替,本质上就是初值设定的比较离谱(只要是负数都可以),让temp充当flag的角色。修改后,代码如下:
class Solution {
    public int removeElement(int[] nums, int val) {
        if(nums.length == 0)
            return 0;

        int temp = -1;

        for(int i = 0; i < nums.length; i++){
            if(nums[i] == val && temp == -1)
                temp = i;
            else if(nums[i] != val)
                nums[temp++] = nums[i];
        }

        return temp;
    }

}

II. 东哥的做法:

  • 如果 fast 遇到值为 val 的元素,则直接跳过,否则就赋值给 slow 指针,并让 slow 前进一步。
class Solution {
    public int removeElement(int[] nums, int val) {
        if(nums.length == 0)
            return 0;

        int slow = 0;

        for(int i = 0; i < nums.length; i++){
            if(nums[i] != val)
                nums[slow++] = nums[i];
        }

        return slow;
    }

}
class Solution {
    public int removeElement(int[] nums, int val) {
        if(nums.length == 0)
            return 0;

        int slow = 0;
        int fast = 0;

        while(fast < nums.length){
            if(nums[fast] != val)
                nums[slow++] = nums[fast];
            fast++;
        }

        return slow;
    }

}
  • 注意这里和有序数组去重的解法有一个细节差异,我们这里是先给 nums[slow] 赋值然后再给 slow++,这样可以保证 nums[0…slow-1] 是不包含值为 val 的元素的,最后的结果数组长度就是 slow。

2. 总结

  • 这题和上面那个思路差不多,是我想复杂了。东哥的思路有点像是把原来的数组当成了一个新数组,用fast进行遍历,如果不等于val,就把这个元素添加到这个新的数组中。这样想,就完全不需要知道这个元素是不是第一个等于val的元素。牛逼plus!

四. 移动零

1. 思路和代码

I. 博主的做法:

  • 本质上就是删除0,然后在数组的末尾去补0。
class Solution {
    public void moveZeroes(int[] nums) {
        if(nums.length == 0)
            System.out.print(0);
        
        int slow = 0;

        for(int i = 0; i < nums.length; i++){
            if(nums[i] != 0)
                nums[slow++] = nums[i];
        }
        for(int i = slow; i < nums.length; i++)
            nums[i] = 0;
        
        for(int i = 0; i < nums.length; i++){
            if(i == nums.length - 1)
                System.out.print(nums[i]);
            else
                System.out.print(nums[i] + ",");
        }

    }
}
  • 本来想着,是不是可以不改变数组,直接通过打印AC这道题呢,写了如下代码:
 class Solution {
     public static void moveZeroes(int[] nums) {
        if(nums.length == 0)
            System.out.print(0);

        int count = 0;

        for(int i = 0; i < nums.length; i++){
            if(nums[i] != 0){
                count++;

                System.out.print(nums[i]);
                if(count != nums.length)
                    System.out.print(",");
            }

        }

        for(int i = count; i < nums.length; i++){
            System.out.print(0);

            if(i != nums.length - 1)
                System.out.print(",");
        }
    }
}

结果:在这里插入图片描述
看来leetcode的输出,是遍历了这个数组,氧化钙。

II. 东哥的做法:

class Solution {
    public static int removeElement(int[] nums, int val) {
        if(nums.length == 0)
            return 0;

        int slow = 0;
        int fast = 0;

        while(fast < nums.length){
            if(nums[fast] != val)
                nums[slow++] = nums[fast];
            fast++;
        }

        return slow;
    }
    public static void moveZeroes(int[] nums) {
         // 去除 nums 中的所有 0,返回不含 0 的数组长度
        int p = removeElement(nums, 0);
        // 将 nums[p..] 的元素赋值为 0
        for (; p < nums.length; p++) {
            nums[p] = 0;
        }

    }
}

  • 题目让我们将所有 0 移到最后,其实就相当于移除 nums 中的所有 0,然后再把后面的元素都赋值为 0 即可。

2. 总结

  • 东哥的思路和博主是一样的,我的代码中的count就相当于slow指针。要像东哥一样,擅于去进行题目的整合。

② 左右指针

一. 二分查找

  • 无。函数名为:public static int binarySearch(int[] nums, int target);

1. 思路和代码

int binarySearch(int[] nums, int target) {
    // 一左一右两个指针相向而行
    int left = 0, right = nums.length - 1;
    while(left <= right) {
        int mid = (right + left) / 2;
        if(nums[mid] == target)
            return mid; 
        else if (nums[mid] < target)
            left = mid + 1; 
        else if (nums[mid] > target)
            right = mid - 1;
    }
    return -1;
}

  • “=”存在的意义:如果数组元素只有一个的话,就存在left == right的情况

二. 两数之和 II - 输入有序数组

1. 思路和代码

I. 博主的做法:

  • 先暴力一波,有点像是握手问题。
class Solution {
    public int[] twoSum(int[] numbers, int target) {

        int[] res = new int[2];

        for(int i = 0; i < numbers.length; i++){
            for(int j = i + 1; j < numbers.length; j++){
                if(numbers[i] + numbers[j] == target){
                    res[0] = i + 1;
                    res[1] = j + 1;

                    return res;
                }

            }
        }
        return res;
    }
}

II.东哥的做法:

  • 只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 就可以调整 sum 的大小。
class Solution {
    public int[] twoSum(int[] numbers, int target) {

        int left = 0;
        int right = numbers.length - 1;

        while(left < right){
            if(numbers[left] + numbers[right] == target)
                return new int[]{left + 1, right + 1};
            else if(numbers[left] + numbers[right] > target)
                right--;
            else 
                left++;
        }
        return new int[]{-1, -1};
    }
}
  • 题目给的是一个有序数组,如果两个元素的和 > target,明显是右边的元素太大了,也就是说要缩小一些;如果是两个元素的和 < target,那就是左边的元素太小了,要放大一些。

2. 总结

  • 只要数组有序,就应该想到双指针技巧!!!

三. 反转字符串

1. 思路和代码

I. 博主的做法:

  • 很简单嘛,就是整一个临时变量temp保存字符,然后互换位置。
class Solution {
    public void reverseString(char[] s) {
        int left = 0;
        int right = s.length - 1;
        char temp;

        while(left < right){
            temp = s[left];
            s[left] = s[right];
            s[right] = temp;

            left++;
            right--;
        }
    }
}

II.东哥的做法:

  • 和博主想的一样。

四. 回文串判断

  • 无。函数名为:public static boolean isPalindrome(String s);

1. 思路和代码

I. 博主的做法:

    public static boolean isPalindrome(String s){
        int left = 0;
        int right = s.length() - 1;

        while(left < right){
            if(s.charAt(left) != s.charAt(right))
                return false;
            left++;
            right--;
        }
        return true;
    }
  • 和上一个交换数组基本上一模一样,换汤不换药。

II.东哥的做法:

  • 和博主想的一样。

五. 最长回文子串

1. 思路和代码

I. 博主的做法:

  • 复用了上面判断是否为回文数的算法。本质上还是暴力,使用substring方法,从最长的长度开始构建字符串。
  • 先固定子字符串的长度,然后不断的向右移动字符串的起点,依次对得到的子字符串进行判断,若是回文串,直接进行输出。
class Solution {
    public static boolean isPalindrome(String s){
        int left = 0;
        int right = s.length() - 1;

        while(left < right){
            if(s.charAt(left) != s.charAt(right))
                return false;
            left++;
            right--;
        }
        return true;
    }
    public String longestPalindrome(String s) {
        int len = s.length();
        String str;

        while(len > 0){
            for(int i = 0; i < s.length(); i++){
                if(i + len <= s.length()){
                    str = s.substring(i, i + len);
                    if(isPalindrome(str) == true)
                        return str;
                }

            }
            len--;
        }
        return "-1";
    }
}

II.东哥的做法:

for 0 <= i < len(s):
    找到以 s[i] 为中心的回文串
    找到以 s[i] 和 s[i+1] 为中心的回文串
    更新答案

class Solution {
    public static String Palindrome(String s, int left, int right){
        while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
        // 双指针,向两边展开
            left--;
            right++;
        }

        return s.substring(left + 1, right);
    }
    public String longestPalindrome(String s) {
        String res = "";
        
        for(int i = 0; i < s.length(); i++){
        	//以 s[i] 为中心的最长回文子串
            String str1 = Palindrome(s, i, i);
            // 以 s[i] s[i+1] 为中心的最长回文子串
            String str2 = Palindrome(s, i, i + 1);

            res = res.length() < str1.length() ? str1 : res;
            res = res.length() < str2.length() ? str2 : res; 
        }

        return res;
    }
}
  • 先开始,我还以为代码有问题,就是Palindrome()这个函数的返回值为s.substring(left + 1, right);
  • eg:“ 121 ”
    • 以2为中心,扩大回文子串的时候,出了while循环,left < 0 && right == s.length,由于substring的特性,所以应该这么写,完全正确!
    • “ 12 ”这个子串不是回文序列,所以left+1,返回的串就是 “ 2 ”
    • “ 1 ”这个子串是回文序列,出了while,left < 0 越界了,left + 1,返回的子串就是“ 1 ”

2. 总结

  • 东哥这个思路真有意思,回文子串就是从中心向两边进行发散,这个思路可以积累。

参考:https://labuladong.github.io/algo/di-yi-zhan-da78c/shou-ba-sh-48c1d/shuang-zhi-fa4bd/

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值