leetcode佛系刷题(无序)-第二天

1.盛最多水的容器

题目:给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。说明:你不能倾斜容器。

leetcode佛系刷题(无序)-第二天_第1张图片

思路分析: 我么可以定义左右指针来代表容器的左右边界。初始时,左指针指向第一个元素,右指针指向最后一个元素。容器的容量由左右指针指向的两个元素的较小值和两个元素之间的距离共同决定。为什么时较小值呢?是因为面积的高度是由较小值所决定的,否则,就不能围成一个封闭的区域。因此,我们计算当前容器的容量,并更新最大容量。然后移动左右指针中较小的那个,以寻找可能的更大容器。这是因为移动较大的指针不会导致容器的容量增加,而移动较小的指针可能会导致容器的容量增加。
当左右指针相遇时,算法结束。此时,最大容量即为结果。
时间复杂度:O(N),其中 N 是数组的长度。指针移动的次数最多为 N-1。
空间复杂度:O(1)。

代码如下:

class Solution {
    public int maxArea(int[] height) {
        int maxArea = 0; // 初始化最大面积为0
        int left = 0; // 左指针指向第一个元素
        int right = height.length - 1;// 右指针指向最后一个元素
        // 当左右指针相遇时,循环结束
        while (left < right) {
            // 当前的面积,这里肯定是min,因为你要是取大的,就不是封闭的容器
            int nowArea = Math.min(height[left], height[right]) * (right - left);
            // 更新最大面积
            maxArea = Math.max(maxArea, nowArea);
            // 移动较小的指针可能会使得面积增加
            if (height[left] < height[right]) {
                left++; // 左指针右移
            } else {
                right--;// 右指针左移
            }
        }
        return maxArea; // 返回最大面积
    }
}

2.合并K个升序链表

题目:

给你一个链数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序表链表中,返回合并后的链表。

输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]

题目分析:

此题可以使用分治法来解决,将n个有序链表两两合并,直到只剩下一个链表为止。
具体实现方式如下:
将n个有序链表平分成两半,分别递归地对左半部分和右半部分进行合并,得到两个有序链表。
将上一步得到的两个有序链表合并成一个有序链表,返回合并后的链表。
重复执行上述步骤,直到只剩下一个有序链表为止。

两个有序链表的合并操作可以参考第一篇文章第4题。
实现过程可以参考下图:

leetcode佛系刷题(无序)-第二天_第2张图片

 代码如下:

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null; // 如果链表数组为空则返回null
        }
        // 使用分治法,将n个有序链表两两合并直到只剩下一个链表为止
        int n = lists.length;
        while (n > 1) {
            int k = (n + 1) / 2;
            for (int i = 0; i < n / 2; i++) {
                lists[i] = mergeTwoLists(lists[i], lists[i + k]); // 合并两个链表
            }
            n = k; // 更新链表数量
        }
        return lists[0]; // 返回合并后的链表
    }

    private ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if (list1 == null)
            return list2;
        if (list2 == null)
            return list1;
        ListNode res = list1.val < list2.val ? list1 : list2;
        res.next = mergeTwoLists(res.next, list1.val >= list2.val ? list1 : list2);
        return res;
    }
}

这里的合并两个升序链表使用的是第一篇文章第4题的第二种递归的方法。

3.k个一组反转链表

题目:

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

leetcode佛系刷题(无序)-第二天_第3张图片

思路分析:

首先,这道题他要求翻转,那么我们要写一个函数来实现翻转功能。其次,要求k个一组进行翻转,那么好,如果节点个数小于k,无法翻转,或者剩余的节点个数小于k个也是无法翻转的,这是需要注意的地方。

因为节点个数不确定,所以本题可以使用递归实现。每次递归中先判断剩余节点数量是否小于k,若是则返回头节点head。否则,先翻转前k个节点,然后将子链表的头结点和尾结点指向递归处理后的子链表,最后返回新的头结点。在翻转前k个节点时,可以使用迭代的方式实现。

代码如下:

class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        // 创建一个虚拟节点,并将其指向该链表的头节点
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        // 定义前一个子链表的为节点和当前子链表的尾节点
        ListNode pre = dummy;
        ListNode end = dummy;
        // 遍历整个链表
        while (end.next != null) {
            // 找到当前子链表的尾节点
            for (int i = 0; i < k && end != null; i++) {
                end = end.next;
            }
            // 如果当前子链表的长度不足k个,则遍历结束
            if (end == null) {
                break;
            }
            // 记录当前子链表的头节点和尾节点
            ListNode start = pre.next;
            ListNode next = end.next;
            // 断开当前子链表和后续子链表的链接
            end.next = null;
            // 翻转当前子链表,并将翻转后的子链表连接到前一个子链表的尾节点
            pre.next = reverseList(start);
            // 将当前子链表的头节点和后续子链表连接起来
            start.next = next;
            // 更新前一个子链表的尾节点和当前子链表的尾节点
            pre = start;
            end = start;
        }
        // 返回虚拟头节点的下一个节点,即反转后的链表的头节点
        return dummy.next;
    }

    private ListNode reverseList(ListNode head) {
        ListNode pre = null, cur = head; // 定义一个前驱节点和当前节点
        // 遍历整个链表,当当前节点不为空时,一直循环
        while (cur != null) {
            ListNode curNext = cur.next; // 记录当前节点的后继节点
            cur.next = pre;
            pre = cur; // 更新前一个节点和当前节点
            cur = curNext;
        }
        return pre; // 返回翻转后的链表的头节点

    }
}

4.串联所有单词的子串

题目:

给定一个字符串 s 和一个字符串数组 words words 中所有字符串 长度相同

 s 中的 串联子串 是指一个包含  words 中所有字符串以任意顺序排列连接起来的子串。

  • 例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd""cdabef", "cdefab""efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。

返回所有串联字串在 s 中的开始索引。你可以以 任意顺序 返回答案。

输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]

思路分析:

需要在一个字符串中查找是否存在由给定字符串数组中所有字符串连接而成的子串,因此我们可以遍历字符串 s 中每一个长度等于 words 中所有字符串长度乘积的子串,并检查该子串是否包含 words 中所有字符串。

我们可以创建两个哈希表:wordCnt和strCnt,分别存储了给定单词列表中每个单词出现的次数以及当前子串中每个单词出现的次数。首先,在处理之前,将给定单词列表中每个单词及其出现次数存储在wordCnt中。然后,对于s中每个可能的子串,使用substring()方法获取当前子串中第j个单词的字符串t,并进行如下处理:

  1. 如果t不在wordCnt中,则退出内部循环。
  2. 如果t在wordCnt中,则将其存储在strCnt中,并更新其出现次数。
  3. 如果strCnt中t的出现次数已经超过了wordCnt中的出现次数,则退出内部循环。
  4. 如果strCnt中t的出现次数没有超过wordCnt中的出现次数,并且已经判断完当前子串中所有单词。
  5. 均存在于wordCnt中,则将当前子串的起始位置加入结果集res中。
  6. 最终,返回结果集res即可。

代码如下:

class Solution {
    public List findSubstring(String s, String[] words) {
        List res = new ArrayList<>();
        // 检查输入字符串和单词数组是否为空
        if (s == null || s.length() == 0 || words == null || words.length == 0) {
            return res; // 如果有一个为空,直接返回res
        }
        // 统计每个单词出现的次数,使用哈希表
        Map wordCount = new HashMap<>();
        for (String word : words) {
            // 如果不存在这个单词,默认次数为0
            wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
        }
        // 分别定义n为字符串的长度,m为单词数组的长度,k为单词长度
        int n = s.length(), m = words.length, k = words[0].length();
        // 这里使用枚举的方法,枚举每个子串
        // 这里为什么是n-m*k,是因为两个单词的长度为m*k,所以字串起始下标i要小于等于n-m*k
        for (int i = 0; i <= n - m * k; i++) {
            // 统计当前字串中每个单词出现的次数
            Map strCount = new HashMap<>();
            int j = 0;
            // 单词数组长度为m,j从0开始,所以跳出循环的条件是j等于m
            while (j < m) {
                // 截取当前单词
                String t = s.substring(i + j * k, i + (j + 1) * k);
                // 如果当前单词不在单词数组中,跳出循环
                // containsKey方法是哈希表自带的内置函数,用于判断关键字是否存在于哈希表中
                if (!wordCount.containsKey(t)) {
                    break;
                }
                // 统计当前单词出现次数,如果不存在,默认是0,因为wordCount存在t,所以+1
                strCount.put(t, strCount.getOrDefault(t, 0) + 1);
                // 如果当前单词出现的次数超过了单词数组中单词出现的次数,跳出循环
                if (strCount.get(t) > wordCount.get(t)) {
                    break;
                }
                j++;// 处理下一个单词
            }
            // 如果所有单词都被匹配成功,加入到结果列表
            if (j == m) {
                res.add(i);
            }
        }
        return res;
    }
}

模拟过程:

假设现在有一个字符串 s = "barfoothefoobarman",单词数组 words = ["foo","bar"],我们想要找出 s 中所有包含单词数组中所有单词的子串的起始位置。
首先,检查输入字符串和单词数组是否为空。由于这里 s 和 words 都不为空,所以继续执行代码。接着,代码会统计每个单词在单词数组中出现的次数,并存储在一个 Map 对象 wordCnt 中。具体地,单词 "foo" 出现了 1 次,单词 "bar" 出现了 1 次,所以 wordCnt 中的键值对为 { "foo": 1, "bar": 1 }
然后,代码会分别定义 n、m 和 k 三个变量。由于 s 的长度为 18,words 数组中有 2 个单词,每个单词长度为 3,所以有 n = 18、m = 2 和 k = 3。
接着,代码开始枚举每个子串。由于要找到包含单词数组中所有单词的子串,因此枚举的起点为 0,终点为 n - m * k,即 12。
第一次枚举时,起点为 0。代码会截取子串 barfoothe,并统计其中每个单词出现的次数。首先,截取的第一个单词为 "bar",在 wordCnt 中出现 1 次,因此在 strCnt 中也增加了键值对 { "bar": 1 }。然后,截取的第二个单词为 "foo",在 wordCnt 中出现 1 次,因此在 strCnt 中也增加了键值对 { "foo": 1 }。由于所有单词都被匹配成功,因此将 0 加入结果列表。
然后,代码会继续枚举下一个子串。这一次,起点为 1。代码会截取子串 arfoothef,并统计其中每个单词出现的次数。首先,截取的第一个单词为 "arf",不在 wordCnt 中,因此跳出循环。
代码会继续枚举下一个子串,直到枚举到起点为 9 的子串。这一次,代码会截取子串 oobarman,并统计其中每个单词出现的次数。首先,截取的第一个单词为 "oob",不在 wordCnt 中,因此跳出循环。最后,代码返回结果列表 [0, 9]。

再上述代码基础上,又进行了修改,代码如下:

class Solution {
    public List findSubstring(String s, String[] words) {
        List res = new ArrayList<>();
        // 如果s或words为空,则返回空列表
        if (s == null || s.length() == 0 || words == null || words.length == 0) {
            return res;
        }
        // 定义一个哈希表记录每个单词出现的次数
        Map wordCount = new HashMap<>();
        for (String word : words) {
            wordCount.put(word, wordCount.getOrDefault(word, 0) + 1);
        }
        int n = s.length(), m = words.length, k = words[0].length();
        // 计算所有单词总长度
        int letterLen = m * k;
        // 遍历字符串s,每次取出长度为letterLen的子串
        for (int i = 0; i <= n - letterLen; i++) {
            String str = s.substring(i, i + letterLen);
            // 记录字串当中每个单词出现的次数
            Map strCount = new HashMap<>();
            for (int j = 0; j < letterLen; j += k) {
                String t = str.substring(j, j + k);
                strCount.put(t, strCount.getOrDefault(t, 0) + 1);
            }
            // 如果单词次数哈希表和子串哈希表相同,说明匹配成功,将i添加到列表res中
            if (wordCount.equals(strCount)) {
                res.add(i);
            }
        }
        return res;
    }
}

相比第一个代码,第二个代码少了一些,理解起来更容易一点,但是因为遍历和比较都是线性的,所以时间复杂度:O(n^2)。

于是,有了第二种方法:滑动窗口。

简介:滑动窗口是在给定特定窗口大小的数组或者字符串上执行的操作,它可以将部分问题的嵌套循环转化成一个单循环,这样就能后减少时间复杂度,对代码进行优化,提高效率。为了更好的理解滑动窗口,下面我会以图解的方式展示。

比如,我们给定一个字符串“nnoasnas”,我们要找出“noas”所在起始点的索引。

leetcode佛系刷题(无序)-第二天_第4张图片

上图中,window是滑动串口,我们设定滑动窗口大小为4,当滑动窗口滑动一次,我们要判断当前滑动窗口的字符串是否和目标字符串相等。显然(2)时已符合要求,那么当前的其实索引为1,输出即可。通过这个简单的小示例,可能对滑动窗口有了初步的了解,那么接下来来说明并演示一下实现滑动窗口的大体流程。

滑动窗口,滑动窗口,顾名思义,它可以滑动,还可以调整窗口的大小,知道调整到满足所要求的条件,同时,这个窗口的大小也可以自定义。

滑动窗口算法的思想(这里以字符串为例):

  1. 构建滑动窗口:使用左右双指针来设置窗口,初始化左右指针left = right = 0, 那么【left,right】这就是一个区间,也就是一个窗口。
  2. 移动右指针并扩展窗口:右指针right不断向右扩大移动,区间变为【left,right】扩大窗口,直到窗口中的字符串符合要求。
  3. 移动左指针并缩小窗口:不再增加右指针right,不断增加左指针left从而缩小区间【left,right】缩小窗口,直到窗口中的字符串不符合要求。
  4. 记录并重置窗口:如果窗口口大于等于要求长度,则说明窗口内的字符串构成了一个符合要求的子串,将该子串的左端点即left保存在结果列表中,然后重置窗口,并从右指针的下一个位置开始继续滑动窗口,重复 2 和 3 ,直到右指针right到达字符串的尾部。

下面,我们来画图更加深刻的理解一下滑动窗口,map 和 window我们用来记录字符出现次数和窗口中字符出现次数。

初始状态:左右指针left 和 right为 0。

leetcode佛系刷题(无序)-第二天_第5张图片

 移动右指针并扩展窗口,直到符合要求,【left,right】包含了“asn”:

leetcode佛系刷题(无序)-第二天_第6张图片

 然后移动左指针并缩小窗口【left,right】:

leetcode佛系刷题(无序)-第二天_第7张图片

 继续移动左指针并缩小窗口【left,right】直到窗口中的字符串不符合要求,此时:

leetcode佛系刷题(无序)-第二天_第8张图片

 然后再重复 2 和 3,直到右指针right到达字符串的尾部。

 OK!!!那么好,既然我们已经知道了滑动窗口的思路和流程,那么接下来我们就利用滑动窗口来解决这道题目。

思路分析:

首先需要将单词数组 words 中的单词以及它们出现的次数存储到哈希表 map 中。

构造滑动窗口
        接着从字符串 s 的第一个单词位置开始,滑动一个长度为 len * lenOfWord 的窗口(其中 len 表示单词数组 words 的长度,lenOfWord 表示单词的长度)。在滑动窗口的过程中,用一个哈希表 window 来记录当前窗口中每个单词出现的次数。
移动右指针并扩展窗口
        对于每个右指针位置 right,从 s 中取出一个长度为单词长度的子串 word,判断该子串是否在 words 中出现过。如果 word 出现过,则将其出现次数加 $1$,并在 window 中记录该单词的出现次数。然后将右指针 right 向右移动一个单词的长度,继续扩展窗口。
移动左指针并缩小窗口
        如果当前窗口中某个单词出现的次数超过了其在 words 中出现的次数,那么需要移动左指针 left 并缩小窗口。每次将左指针向右移动一个单词的长度,同时从 window 中减去左端点所在的单词的出现次数,直到窗口内每个单词的出现次数都不超过其在 words 中出现的次数为止。
记录结果并重置窗口
        如果当前窗口的大小等于 len * lenOfWord,则说明窗口内的所有单词构成了一个符合要求的子串,将该子串的左端点加入到结果列表 list 中。然后需要重置窗口,并从右指针的下一个位置开始继续滑动窗口。
最后,将所有符合要求的子串的左端点存储在列表 list 中,并返回该列表作为方法的结果。

直接上代码:

class Solution {
    public List findSubstring(String s, String[] words) {
        int lenOfWord = words[0].length(); // 获取单词的长度
        int len = words.length; // 获取单词数组的长度
        List res = new ArrayList<>(); // 用来存储结果
        Map wordMap = new HashMap<>(); // 用来记录单词出现的次数
        for (String a : words) { // 遍历单词数组
            wordMap.put(a, wordMap.containsKey(a) ? wordMap.get(a) + 1 : 1); // 如果单词已经出现过,将其出现次数加一;否则将其出现次数设置为 1。
        }
        for (int i = 0; i < lenOfWord; i++) { // 滑动窗口
            Map window = new HashMap<>(); // 用来记录窗口中单词出现的次数
            int left = i, right = i; // left 和 right 为窗口左右端点
            while (right <= s.length() - lenOfWord) { // 当窗口右端点还没有到字符串的末尾时
                String word = s.substring(right, right + lenOfWord); // 获取右端点单词
                right += lenOfWord; // 将右端点向右移动一个单词的长度
                if (wordMap.containsKey(word)) { // 如果单词在单词数组中出现过
                    window.put(word, window.containsKey(word) ? window.get(word) + 1 : 1); // 将其在窗口中的出现次数加一
                    while (window.get(word) > wordMap.get(word)) { // 如果窗口中某个单词的出现次数超过了其在单词数组中出现的次数
                        String head = s.substring(left, left + lenOfWord); // 获取窗口左端点的单词
                        left += lenOfWord; // 将窗口左端点向右移动一个单词的长度
                        window.put(head, window.get(head) - 1); // 将窗口左端点的单词在窗口中的出现次数减一
                    }
                } else { // 如果单词不在单词数组中
                    window.clear(); // 清空窗口中的单词
                    left = right; // 将窗口左端点设置为右端点
                }
                if (right - left == len * lenOfWord) { // 如果窗口大小等于单词数组的总长度
                    res.add(left); // 将窗口左端点的下标加入到结果中
                    window.clear(); // 清空窗口中的单词
                    left += lenOfWord; // 将窗口左端点向右移动一个单词的长度
                    right = left; // 将窗口右端点设置为左端点
                }
            }
        }
        return res; // 返回结果
    }
}

leetcode佛系刷题(无序)-第二天_第9张图片

你可能感兴趣的:(leetcode,算法)