【Java】【数据结构与算法】【华为】leetcode刷题记录--数组、双指针和滑动窗口

本篇博客适用于对于数据结构和算法有一定基础,懂得计算机基础概念(例如内存分配)的人阅读和学习,且本文内容的语言仅有java,如果有需求可以再加上python版本。

数组

数组是最基础的数据结构,其特点的支持随即访问,在内存中是连续存储的。在java中,可以使用nums.length得到数组的长度。

java是面向对象的,数据结构也是讲继承的。List这个接口就衍生出新的数据结构,例如ArrayList或者LinkedList,其功能都是很丰富的。那这样看来,什么都没有的数组岂不是很差劲?没有丰富的方法,在内存中连续存储也注定其插入和删除要麻烦一些,那数组还有什么存在意义?

数组的优点大概有以下几点:

  1. 性能:对于随机访问操作,数组的性能通常优于ArrayList,因为数组访问不涉及额外的函数调用开销。
  2. 内存占用:相比于ArrayList,数组通常占用更少的内存,因为ArrayList对象有额外的内部开销。
  3. 基本类型:数组可以直接存储基本类型,如int、char、boolean等,而ArrayList不能直接存储基本类型,必须使用其包装类,如Integer、Character、Boolean等。
  4. 多维数组:Java支持多维数组,这在处理某些问题(如矩阵运算)时非常有用。而ArrayList的多维使用并不直观。

704 二分查找

二分查找是在大学时期都学过的简单算法,对数组进行查找,能够执行二分查找的前提条件就是需要数组是有序的,且元素无重复元素(否则会找到不同的下标)。
时间复杂度为O(logn),分析时间复杂度的方法类似于画二叉树的方法,树高为logn。空间复杂度为O(1).

class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length-1;
        while(left<= right){
            int mid = (left + right) / 2;
            if (nums[mid]==target){
                return mid;
            }
            else if(nums[mid] < target){
                left = mid+1;
            }
            else{
                right = mid-1;
            }
        }
        return -1;
    }
}

27 移除元素

题目如下:
在这里插入图片描述
对于数组的移动、删除、插入,都可以使用双指针来处理。数组不像是链表或者以链表为底层设计的数据结构,可以从中间移除元素(例如python的list或者cpp的vector),因此如果每次都要使用暴力解法(双重for循环,每次把后面的元素进行前移),那就会导致时间复杂度为O(n^2),非常低效。而双指针(或者叫快慢指针)可以有效的利用一快一慢的特点,用快指针去”探路“,查看是否需要更改内容,而慢指针则用来配合快指针进行数组的修改,非常好用。
对于本题,我们的目标是将给定的val进行覆盖(同上,在内存中连续的数据结构无法直接删除,除非是链表或者底层逻辑是链表的数据结构,那就不属于数组的范畴咯),因此可以快慢指针,快指针和慢指针开始都在同一位置,如果没有需要val,则两个指针同时前进。而如果遇到了需要覆盖的元素,就将快指针后移,直到其指向的值不为val,也就可以在下一次循环中覆盖慢指针的值,也就是val。

class Solution {
    public int removeElement(int[] nums, int val) {
        int fast=0,slow=0;
        int len = nums.length;

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

977有序数组的平方

题目如下:
在这里插入图片描述
很明显,我们可以暴力解决,也就是平方+快排,这样的时间复杂度是O(nlogn),其实也比较优秀了。但前面说到,对于数组的操作,可以多考虑双指针,那么这题有没有可能呢?

当然可以,这个题目算是坑的点就是负数平方后可能大于正数了,也就需要考虑负数的绝对值大小。如果我们新建一个同等大小的数组,指针指向原数组的两端,不断比较大小并且收缩指针夹住的范围,那就可以解决这个问题。

class Solution {
    public int[] sortedSquares(int[] nums) {
        int left = 0,right = nums.length-1;
        int[] res = new int[nums.length];
        int ptr = nums.length-1;

        for(; ptr>=0; ptr--){
            if(nums[left] * nums[left] > nums[right]*nums[right]){
                res[ptr] = nums[left]*nums[left];
                left++;
            }
            else{
                res[ptr] = nums[right]*nums[right];
                right--;
            }
        }

        return res;

    }
}

209,长度最小的子数组

在这里插入图片描述
在这题中,我们要使用一种和快慢指针相似但不同的方法:滑动窗口。快慢指针在乎的是指针指向的值,而滑动窗口是不断滑动,看重的是指针之间的值。学习过计算机网络中的滑动窗口协议就很好理解,这是一个不断更新的过程。

对于本题,我们可以设置一个滑动窗口,当窗口中的值大于等于(看清楚是大于等于,我自己做的时候就以为是等于,坑惨了)target的时候,就可以存进res里。我们的res变量存的就是最小长度。

首先,左右指针相同(也就是窗口大小为0),这时候每次增加窗口大小直到窗口内的值大于target,这时候就可以存储res并且缩小窗口大小了。

class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int left=0,right=0;
        int sum;
        int res=Integer.MAX_VALUE;
        for(sum=0; right<nums.length;right++){
            sum += nums[right];
            while(sum>=target){
                res = Math.min(res, right - left + 1);
                sum -= nums[left++];
            }
        }
        return res == Integer.MAX_VALUE ? 0 : res;
    }
}

59 螺旋矩阵

在这里插入图片描述
不难看出,就这是一道很简单的模拟题,但还是有练习的必要。代码如下:

class Solution {
    public int[][] generateMatrix(int n) {
        int row=1;// 如果是1就按行遍历,0按列
        int right=1; // 如果是1就向右,0向左
        int down =1; // 如果是1就向下,0向上
        int[][] tmp = new int[n][n];
        int i = 0;
        int j = 0;
        int cnt = 1;

        while(cnt<=n*n){
            if(row==1&&right==1){
                while(j<n&&tmp[i][j]==0){
                    tmp[i][j++] = cnt;
                    cnt++;
                    System.out.println(i+ " " +j);
                }
                j--;
                i++;
                row=0;
                right=0;
            }
            if(row==0&&down==1){
                while(i<n&&tmp[i][j]==0){
                    tmp[i++][j] = cnt;
                    cnt++;
                }
                i--;
                j--;
                row=1;
                down=0;
            }
            if(row==1&&right==0){
                while(j>=0&&tmp[i][j]==0){
                    tmp[i][j--] = cnt;
                    cnt++;
                }
                j++;
                i--;
                row=0;
                right=1;
            }
            if(row==0&&down==0){
                while(i>0&&tmp[i][j]==0){
                    tmp[i--][j] = cnt;
                    cnt++;
                    System.out.println(i+ " " +j);
                }
                i++;
                j++;
                row=1;
                down=1;
            }
        }
        return tmp;       
    }
}

双指针

双指针也一般称为快慢指针,主要用于处理链表和数组等线性数据结构。这种技巧主要涉及到两个指针,一个快指针(通常每次移动两步)和一个慢指针(通常每次移动一步)。快指针可以起到’探路‘的作用,给慢指针修改。
适用范围:

  1. 一般适用于字符串/数组。
  2. 对数组/字符串进行更改(反转、删除、增添)。

344 反转字符串

在这里插入图片描述
本题就是双指针比较简单的用法,一头一尾进行交换即可。左右指针不断缩紧直到左指针大于等于右指针为主。

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

        while(left<right){
            char tmp = s[left];
            s[left++] = s[right];
            s[right--] = tmp;
        }
    }
}

151 反转字符串中的单词

在这里插入图片描述
看到这个题我的第一想法就是用split方法进行分割,将原字符串分割为多个字符,然后再双指针逆转(如果是python就更简单了),示例代码如下:

class Solution {
    public String reverseWords(String s) {
        String[] res = s.trim().split("\\s+");
        
        for(int left=0,right=res.length-1; left<right; left++,right--){
            String tmp = res[left];
            res[left] = res[right];
            res[right] = tmp;
        }

        return String.join(" ",res);
    }
}

或者:

class Solution:
    def reverseWords(self, s: str) -> str:
        # 删除前后空白
        s = s.strip()
        # 反转整个字符串
        s = s[::-1]
        # 将字符串拆分为单词,并反转每个单词
        s = ' '.join(word[::-1] for word in s.split())
        return s

这里要说明一下,在java里,字符串是不可变的,即无法修改。使用split方法可以将字符串转化为字符串数组,split是将字符串安装给的的规则进行正则匹配分割。

String str = "Hello, world!";
String[] words = str.split(" ");
System.out.println(words[0]);  // 输出: Hello,

join:join方法用于将多个字符串连接成一个新的字符串,字符串之间用一个指定的分隔符分隔。例如:

String[] words = {"Hello", "world"};
String sentence = String.join(" ", words);
System.out.println(sentence);  // 输出: Hello world

在这个例子中,String.join(" “, words)将字符串数组words中的字符串连接成一个新的字符串,字符串之间用空格” "分隔。

trim:trim方法用于去掉字符串开头和结尾的空格。如果字符串的开头或结尾有一个或多个空格,trim方法会返回一个新的字符串,这个字符串是原字符串去掉开头和结尾的空格后的结果。例如:

String str = "  Hello, world!  ";
String trimmed = str.trim();
System.out.println(trimmed);  // 输出: Hello, world!

双指针在数组里大致就是这样,剩余的内容放到链表中。

滑动窗口

适用范围
1、一般是字符串或者列表
2、一般是要求最值(最大长度,最短长度等等)或者子序列

167 招式拆解

在这里插入图片描述
这个滑动窗口策略的目标是找到字符串中最长的不重复字符子串。为了实现这个目标,我们使用两个指针:left 和 right,right 指针在每次循环中都会向右移动一位,表示我们正在考虑的当前字符。left 指针表示窗口的开始位置,它只在我们遇到重复字符时移动。

具体来说,当我们遇到一个新的字符(即这个字符不在 dic 中),我们只需要将这个字符和它的索引添加到 dic 中,然后更新 res 的值为 res 和 right - left + 1 的较大值。

当我们遇到一个已经存在于 dic 的字符时,我们需要更新 left 的值。我们将 left 的值设置为 dic.get(arr.charAt(right)) + 1 和 left 的较大值。这样可以确保 left 总是位于窗口内的重复字符的右侧,也就是说,我们将窗口的左边界移动到了这个字符上一次出现的位置的右侧,以确保窗口内的字符不重复。比如说’abba’,这时候left第一次会更新为2,第二次更新不能再更新回1的位置,而还是2,因此是要取最大值。

在每次循环中,无论我们遇到的是一个新的字符还是一个已经存在于 dic 的字符,我们都将当前字符和它的索引添加到 dic 中。这是因为我们需要记录每个字符最后一次出现的位置,以便在遇到重复字符时正确地更新 left

class Solution {
    public int dismantlingAction(String arr) {
        Map<Character, Integer> dic = new HashMap<>();
        int left=0,right=0;
        int res=0;

        for(; right<arr.length(); right++){
            if(dic.containsKey(arr.charAt(right))){
                left = Math.max(left, dic.get(arr.charAt(right)) + 1);
            }
            dic.put(arr.charAt(right), right);
            res = Math.max(res, right - left + 1);
        }

        return res;
    }
}

1438 绝对值不超过限制的最长连续子数组在这里插入图片描述

不难发现,如果用滑动窗口,但是每次都要寻找窗口内的最大/小值,那时间复杂度仍为O(n^2),还是很低效的。

因此可以使用一个双端队列(Deque)来存储窗口内的最大值和最小值。你需要维护两个双端队列,一个用于存储最大值,另一个用于存储最小值。在每次循环中,你需要做以下操作:

  1. 从最大值队列的尾部移除所有小于当前元素的值,然后将当前元素添加到最大值队列的尾部。
  2. 从最小值队列的尾部移除所有大于当前元素的值,然后将当前元素添加到最小值队列的尾部。

如果最大值队列的头部元素和最小值队列的头部元素之差大于 limit,则移动窗口的左边界,并从队列中移除对应的元素。

这样,最大值队列的头部元素总是窗口内的最大值,最小值队列的头部元素总是窗口内的最小值,你可以在 O(1) 的时间内获取它们。

import java.util.*;

class Solution {
    public int longestSubarray(int[] nums, int limit) {
        // 初始化两个双端队列,一个用于存储窗口内的最大值,一个用于存储窗口内的最小值
        Deque<Integer> maxDeque = new LinkedList<>();
        Deque<Integer> minDeque = new LinkedList<>();
        // 初始化窗口的左右边界和结果
        int left = 0, right = 0, res = 0;

        // 遍历数组
        while (right < nums.length) {
            // 如果最大值队列不为空,且队列尾部的元素小于当前元素,移除队列尾部的元素
            while (!maxDeque.isEmpty() && maxDeque.peekLast() < nums[right]) {
                maxDeque.pollLast();
            }
            // 如果最小值队列不为空,且队列尾部的元素大于当前元素,移除队列尾部的元素
            while (!minDeque.isEmpty() && minDeque.peekLast() > nums[right]) {
                minDeque.pollLast();
            }
            // 将当前元素添加到两个队列的尾部
            maxDeque.offerLast(nums[right]);
            minDeque.offerLast(nums[right]);

            // 如果当前的最大值和最小值之差大于 limit,移动窗口的左边界,并从队列中移除对应的元素
            while (!maxDeque.isEmpty() && !minDeque.isEmpty() && maxDeque.peekFirst() - minDeque.peekFirst() > limit) {
                if (nums[left] == minDeque.peekFirst()) {
                    minDeque.pollFirst();
                }
                if (nums[left] == maxDeque.peekFirst()) {
                    maxDeque.pollFirst();
                }
                left++;
            }

            // 更新结果
            res = Math.max(res, right - left + 1);
            // 移动窗口的右边界
            right++;
        }
        return res;
    }
}

用一个例子解释一下:

nums = [8, 2, 4, 7];
limit = 4;

初始时,双端队列 maxDequeminDeque 都是空的。我们的窗口也是空的,左右边界都在索引 0 的位置。

  • 首先,我们看到元素 8。我们将其添加到 maxDequeminDeque 中。此时,maxDeque = [8],minDeque = [8]。窗口中的最大值和最小值的差为 0,小于 limit,所以我们将窗口向右扩展。

  • 接下来,我们看到元素 2。我们将其添加到 maxDequeminDeque 中。但是,在添加到 maxDeque 之前,我们需要先移除队列尾部的所有小于 2 的元素。同理,在添加到 minDeque 之前,我们需要先移除队列尾部的所有大于 2 的元素。因此,maxDeque = [8, 2],minDeque = [2]。窗口中的最大值和最小值的差为 6,大于 limit,所以我们需要移动窗口的左边界,并从队列中移除对应的元素。此时,maxDeque = [2],minDeque = [2]。

  • 我们继续看到元素 4。我们将其添加到 maxDequeminDeque 中。此时,maxDeque = [4],minDeque = [2, 4]。窗口中的最大值和最小值的差为 2,小于 limit,所以我们将窗口向右扩展。

  • 最后,我们看到元素 7。我们将其添加到 maxDequeminDeque 中。此时,maxDeque = [7],minDeque = [2, 4, 7]。窗口中的最大值和最小值的差为 5,大于 limit,所以我们需要移动窗口的左边界,并从队列中移除对应的元素。此时,maxDeque = [7],minDeque = [4, 7]。

在遍历完数组后,我们发现满足条件的最长子数组的长度为 2,所以我们返回 2。

这就是这段代码的工作过程。在每次循环中,我们都保证了 maxDeque 的头部元素是窗口内的最大值,minDeque 的头部元素是窗口内的最小值。当窗口内的最大值和最小值的差大于 limit 时,我们移动窗口的左边界,并更新队列。

  • 9
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值