小白Java双指针思想及其应用——第三关白银挑战

在之前的笔记中说过数组里面的元素是紧紧靠在一起的,假如有空隙后面的元素就要整体向前移动。同样如果在中间位置插入元素,那么其后的元素都要整体向后移动。在后面可以看到很多算法题目都需要多轮、大量移动元素,这就导致执行效率低下。如何解决该问题是数组算法的一个重要的问题,其中一种非常好用的方式就是双指针思想。

第三关双指针思想以及应用
内容1.理解双指针思想是如何工作的(快慢型和对撞型)
2.理解双指针如何解决删除元素问题
3.理解双指针如何解决奇偶移动问题

4.理解双指针如何解决数组轮转问题的

5.理解双指针如何解决区间相关问题
6.理解双指针如何解决字符串替换问题

目录

 1.双指针思想

2.删除元素专题

2.1原地移除所有数值等于val的元素

2.2删除有序数组中的重复项

 3.元素奇偶移动专题

4.数组轮转问题

5.数组的区间专题

6.字符串替换空格问题


 1.双指针思想

这里介绍一种简单但是非常有效的方式——双指针。所谓的双指针其实就是两个变量,不一定真的是指针。双指针思想简单好用,在处理数组、字符串的场景下很常见。看个例子,从在面的序列中删除重复的元素[1,2,2,2,3,3,3,5,5,7,8],重复元素只保留一个。删除之后的结果应该为[1,2,3,5,7,8]。我们可以在删除第一个2时,是将其后面的元素整体向前移动一次,删除第二个2时再将其后面的元素整体向前移动一次,处理后面的3和5都是一样的操作,这就导致我们需要执行5次大量移动才能完成,效率太低了。如果使用双指针可以方便的解决这个问题,如图:

 首先我们定义两个指针slow、fast。slow表示当前位置之前的元素都是不重复的,而fast则一直向后找,直到找到与slow位置不一样的,找到之后就将slow向后移动一个位置,并将arr[fast]复制给arr[slow],之后fast继续向后找,循环执行。找完之后slow以及之前的元素就都是单一的了。这样就可以只用一轮移动解决问题。

上面这种一个在前面一个在后面的方式也称为快慢指针,有些场景需要从两端向中间走,这种就称为对撞型指针或者相向指针,很多题目也会用到,我们接下来会看到很多相关的算法题目。

还有一种比较少见的背向型,就是从中间向两边走。

2.删除元素专题

2.1原地移除所有数值等于val的元素

LeetCode27题链接

 第一种:快慢指针

整体思想就是2.1.1中介绍的双指针的图示方式,定义两个指针slow和fast,初始值都是0.

slow之前的位置都是有效部分,fast表示当前要访问的元素.

这样遍历的时候,fast不断向后移动:

  • 如果nums[fast]的值不为val,则将其移动到nums[slow++]处。
  • 如果nums[fast]的值为val,则fast继续向前移动,slow先等待。

如图所示:

这样,前半部分是有效部分,后半部分是无效部分。

 public static int removeElement(int[] nums, int val) {
        int slow = 0;
        //fast充当了快指针的角色
        for (int fast = 0; fast < nums.length; fast++) {
            if (nums[fast] != val) {
                nums[slow] = nums[fast];
                slow++;
            }
        }
        //最后剩余元素的数量
        return slow;
    }

 第二种:对撞双指针

对撞指针,有的地方叫做交换移除,核心思想是从右侧找到不是val的值来来顶替左侧的val的值。什么意思呢?我们看图,以nums=[0,1,2,2,3,0,4,2],val=2为例:

 上图完整描述了执行的思路,当left==right的时候,left以及左侧的就是删除2的所有元素来。实现代码:

   public static int removeElement2(int[] nums, int val) {
        int right = nums.length - 1;
        int left = 0;

        for (left = 0; left <= right; ) {
            if ((nums[left] == val) && (nums[right] != val)) {
                int tmp = nums[left];
                nums[left] = nums[right];
                nums[right] = tmp;

            }
            if (nums[left] != val) {
                left++;
            }
            if (nums[right] == val){
                right--;
            }

        }
        return left ;
    }

这样就是一个中规中矩的双指针解决办法。

第三种方法:“对撞双指针+覆盖”法。当nums[left]==val的时候,我们就将nums[right]位置的元素覆盖nums[left],继续循环,如果nums[left]==val就继续覆盖,否则才让left++,这也是双指针的方法,代码如下:

  public static int removeElement3(int[] nums, int val) {
        int right = nums.length-1;
        for (int left = 0; left < right; ) {
            if (nums[left] == val) {
                nums[left] = nums[right];
                right--;
            } else {
                left++;
            }
        }
        return right;
    }

2.2删除有序数组中的重复项

LeetCode26链接

class Solution {
     public static int removeDuplicates(int[] nums) {
        //slow表示可以放入新元素的位置,索引为0的元素不用管
        int slow = 1;
        //循环起到了快指针的作用
        for (int fast = 0; fast < nums.length; fast++) {
            if (nums[fast] != nums[slow-1]) {
                nums[slow] = nums[fast];
                slow++;

            }
        }
        return slow;
    }
}

也可以这样:

 public static int removeDuplicates(int[] nums) {
        //slow表示可以放入新元素的位置,索引为0的元素不用管
        int slow = 0;
        //循环起到了快指针的作用
        for (int fast = 0; fast < nums.length; fast++) {
            if (nums[fast] != nums[slow]) {
                slow++;
                nums[slow] = nums[fast];

            }
        }
        return slow+1;
    }

 3.元素奇偶移动专题

LeetCode905链接,按奇偶排序数组,给你一个非负整数数组A,返回一个数组,在该数组中,A的所有偶元素之后跟着所有奇数元素,你可以返回满足此条件的任何数组作为答案。

最直接的方式就是使用一个临时数组,第一遍查找并将所有的偶数复制到新数组的前部分,第二遍查找并复制所有的奇数到数组后部分,这种方式实现比较简单。但是一般就会问到“是否有空间复杂度为O(1)的”方法。

我们可以采用对撞双指针的方法,图示与2.5.2中的对撞型基本一致,只不过比较的对象是奇数还是偶数,如下图所示:

维护两个指针left=0和right=arr.length-1,left从0开始逐个检查每个位置是否为偶数,如果是则跳过,如果是奇数则停下来,然后right从右向左检查,如果是奇数则提哦啊过偶数则停下来,然后交换Array[left]和array[right]。之后再继续循环,直到left>=right。

public static int[] sortArrayByParity(int[] A) {
        int left = 0, right = A.length - 1;
        while (left < right) {
            if (A[left] % 2 > A[right] % 2) {
                int tmp = A[left];
                A[left] = A[right];
                A[right] = tmp;
            }

            if (A[left] % 2 == 0) left++;
            if (A[right] % 2 == 1) right--;
        }

        return A;
    }

4.数组轮转问题

LeetCode189链接

 方法如下:

  1. 首先对整个数字实行翻转,例如[1,2,3,4,5,6,7]我们先将其整体反转成[7,6,5,4,3,2,1]
  2. 从k处分隔成左右两个部分,这里就是根据k将其分成两组[7,6,5]和[4,3,2,1]
  3. 最后将两个再次翻转就得到了[5,6,7]和[1,2,3,4],最终结果就是[5,6,7,1,2,3,4,5]

代码如下:

public static void rotate(int[] nums, int k) {
        k %= nums.length;
        reverse(nums, 0, nums.length - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, nums.length - 1);
    }

    public static void reverse(int[] nums, int start, int end) {
        while (start < end) {
            int temp = nums[start];
            nums[start] = nums[end];
            nums[end] = temp;
            start += 1;
            end -= 1;
        }

5.数组的区间专题

LeetCode228 数组链接

这道题使用双指针可以非常方便的处理,慢指针指向每个区间的起始位置,快指针从慢指针位置开始向后遍历知道不满足连续递增(或快指针达到数组边界),则当前区间结束。

然后将slow指向更新为fast+1,作为下一个区间的开始位置,fast继续向后遍历找下一个区间的结束位置。如此循环,直到输入数组遍历完成。

    public static List<String> summaryRanges(int[] nums) {
        List<String> res = new ArrayList<>();
        // slow 初始指向第 1 个区间的起始位置
        int slow = 0;
        for (int fast = 0; fast < nums.length; fast++) {
            // fast 向后遍历,直到不满足连续递增(即 nums[fast] + 1 != nums[fast + 1])
            // 或者 fast 达到数组边界,则当前连续递增区间 [slow, fast] 遍历完毕,将其写入结果列表。
            if (fast + 1 == nums.length || nums[fast] + 1 != nums[fast + 1]) {
                // 将当前区间 [slow, fast] 写入结果列表
                StringBuilder sb = new StringBuilder();
                sb.append(nums[slow]);
                if (slow != fast) {
                    sb.append("->").append(nums[fast]);
                }
                res.add(sb.toString());
                // 将 slow 指向更新为 fast + 1,作为下一个区间的起始位置
                slow = fast + 1;
            }
        }
        return res;
    }

6.字符串替换空格问题

这是剑指offer中的题目,出现频率很高。

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.经过替换之后的字符串为We%20Are%20Happy。

首先要考虑用什么来存储字符串?

如果是长度不可变的char数组,那么必须新申请一个更大的空间;如果使用长度可变的空间来管理原始数组,或者原始数组申请的足够大,这个时候可能要求你不能申请O(n)大小的空间。

首先如果是长度不可变,我们必须新申请一个更大的空间,然后将原始数组中出现空格的位置直接替换成%20即可

public static String replaceSpace1(StringBuffer str) {
        String res = "";
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c == ' ')
                res += "%20";
            else
                res += c;
        }
        return res;
    }

对于第二种情况,我们首先想到的是从头到尾遍历整个字符串,遇到空格的时候就将其后面的元素向后移动2两个位置,但是这样的问题在前面说过就会导致后面的元素大量的移动,时间复杂度为O(n^2),执行的时候非常容易超时。

比较好的方式是可以先遍历一次字符串,这样可以统计出字符串中空格的总数,由此计算出替换之后字符串的长度,,每替换一个空格,长度增加2,即替换之后的字符串长度为:

新串长度=原来的长度+2*空格数目

接下来从字符串的尾部开始复制和替换 ,用两个指针fast和slow分别指向原始字符串和新字符串的末尾,然后:slow不动,向前移动fast:

  • 若指向的不是空格,则将其复制到slow位置,然后fast和slow同时向前一步
  • 若fast指向的是空格,则在slow位置插入一个20%,fast只移动一步
  • 循环指向上面两步,便可以完成替换。详细过程如下:

  

 实现代码如下:

   public static String replaceSpace2(StringBuffer str) {
        if (str == null)
            return null;
        int numOfblank = 0;//空格数量
        int len = str.length();
        for (int i = 0; i < len; i++) {  //计算空格数量
            if (str.charAt(i) == ' ')
                numOfblank++;
        }
        str.setLength(len + 2 * numOfblank); //设置长度
        int fast = len - 1;  //两个指针
        int slow = (len + 2 * numOfblank) - 1;

        while (fast >= 0 && slow > fast) {
            char c = str.charAt(fast);
            if (c == ' ') {
                fast--;
                str.setCharAt(slow--, '0');
                str.setCharAt(slow--, '2');
                str.setCharAt(slow--, '%');
            } else {
                str.setCharAt(slow, c);
                fast--;
                slow--;
            }
        }
        return str.toString();
    }

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值