diff --git "a/ graduationSummary/\346\257\225\344\270\232\346\200\273\347\273\223.md" "b/ graduationSummary/\346\257\225\344\270\232\346\200\273\347\273\223.md" new file mode 100644 index 00000000..e5d98a5f --- /dev/null +++ "b/ graduationSummary/\346\257\225\344\270\232\346\200\273\347\273\223.md" @@ -0,0 +1,26 @@ +### 算法训练营毕业总结 + +**一. 我为什么来到算法训练营**? + + 1. 因为平时工作都是一些接口对接的工作,重要的任务都是由厂商进行支撑,接触不到核心的技术,所以想要跳槽去到大公司,见识更多的牛人,更好的技术。而算法是进入大厂的必要条件。 + +2. 对于算法的理解也就是停留在大学阶段所学的那些零碎的知识,知识都是点,并没有很好的串成线,构成面,不成体系。太过于碎片化,好像什么算法都听过,但是又好像什么都不会写,所以就想着系统的学习一下数据结构以及算法。 +3. 感觉自己在平时工作和学习中,缺少一些方法的指导,经常会做出事倍功半的事情,所以也是想要借助算法训练营这个平台结交更多的朋友,学到更多的知识,从老师那里也能够获取一些经验和方法。 + + + +**二. 我收获了什么?** + +1. 最大的收获当然是老师平时常常介绍的方法论,也就是所谓的五毒神掌,刻意练习等观点。其实大家从小都知道温故而知新的道理,但是就是很少有人能够去践行他并且制定一套可行的理论指导。而覃超老师做到了。 +2. 其次就是助教老师对我代码的建议让我了解了自己写代码上面一些非常不好的习惯。如用中文作为类名,不写注释,不记录题目链接,不写总结等等等等。这些都是我逆袭路上的助推剂。同时助教老师的认真批改也让我感觉到了陪伴。助教老师也在陪伴着我们一周一周的成长。这种感觉很微妙,无法用言语形容。 +3. 我还收获了一群朋友,他们来自五湖四海,来自中国的各个角落,来自不同的岗位,有着不同的经历,从同学们身上我也学到了很多的东西。 +4. 我收获了我最初来到训练营想要得到的算法能力。不是说我就有去大厂面试的能力了。而是说我现在有了一套方法论的支持,有了刷题的方向以及套路,学会了总结,做的题目也越来越多,越来越得心应手。这一点很令自己欣慰。 +5. 由于我同时参加了两个训练营,一个算法,一个java,所以时间上特别的紧张,所以这一段时间我也学会了很多时间管理的窍门,比如番茄工作法,建立每日的toDO List等等。这让我对于自己的时间安排,时间管理也有了长足的进步。 + + + +**三. 总结** + +​ 就像覃超老师在最后一堂课说的,我们从算法训练营毕业了,但这不是终点,而是另一个起点。有一句话叫做靡不有初,鲜克有终。所以即便毕业了,我也一定会继续认真的刷题,直到达成自己的目标。 + +​ 最后,谢谢所有的人。谢谢覃超老师,谢谢助教老师,谢谢班主任老师,谢谢所有的同学们。我的成长路上,因为有你们,我并不孤单。 \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..73f69e09 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/algorithm018.iml b/.idea/algorithm018.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/algorithm018.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..6560a989 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,36 @@ + + + + \ No newline at end of file diff --git a/.idea/leetcode/editor.xml b/.idea/leetcode/editor.xml new file mode 100644 index 00000000..c60401c3 --- /dev/null +++ b/.idea/leetcode/editor.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..d4b854b8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..5915613b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/week01/NOTE.md b/week01/NOTE.md index 50de3041..638830f0 100644 --- a/week01/NOTE.md +++ b/week01/NOTE.md @@ -1 +1,6 @@ -学习笔记 \ No newline at end of file +学习笔记 +1. 初探五毒神掌,我就尝到了甜头,不仅仅在算法方面,而且我发现只要是 +学习,任何方面都可以运用无毒神掌的技巧,最近也在学习极客大学的java进阶训练营,第一周也是学了 +一些知识,我发现我看不懂的,只要我反复看,多看几遍,总能看懂。算法也是,我不会的,只要我多看几遍题解, +多写一遍,我的理解就会更深。 +2. 真心的感觉到老师的认真负责,群里只要有同学有问题都会很快回答,相信自己在这门课一定能够实现能力的提升! \ No newline at end of file diff --git "a/week01/[189]\346\227\213\350\275\254\346\225\260\347\273\204.java" "b/week01/[189]\346\227\213\350\275\254\346\225\260\347\273\204.java" new file mode 100644 index 00000000..d55726d4 --- /dev/null +++ "b/week01/[189]\346\227\213\350\275\254\346\225\260\347\273\204.java" @@ -0,0 +1,93 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。 +// +// 示例 1: +// +// 输入: [1,2,3,4,5,6,7] 和 k = 3 +//输出: [5,6,7,1,2,3,4] +//解释: +//向右旋转 1 步: [7,1,2,3,4,5,6] +//向右旋转 2 步: [6,7,1,2,3,4,5] +//向右旋转 3 步: [5,6,7,1,2,3,4] +// +// +// 示例 2: +// +// 输入: [-1,-100,3,99] 和 k = 2 +//输出: [3,99,-1,-100] +//解释: +//向右旋转 1 步: [99,-1,-100,3] +//向右旋转 2 步: [3,99,-1,-100] +// +// 说明: +// +// +// 尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。 +// 要求使用空间复杂度为 O(1) 的 原地 算法。 +// +// Related Topics 数组 +// 👍 720 👎 0 + + +import netscape.security.UserTarget; + +import java.util.HashMap; + +//leetcode submit region begin(Prohibit modification and deletion) +class 旋转数组 { + public void rotate(int[] nums, int k) { + // 方法一:暴力法 +// for (int i = 0; i < k; i++) { +// int pre = nums[nums.length - 1]; +// for (int j = 0; j < nums.length; j++) { +// int temp = nums[j]; +// nums[j] = pre; +// pre = temp; +// } +// } + + // 方法二:使用额外空间复杂度的数组 +// int[] help = new int[nums.length]; +// for (int i = 0; i < nums.length; i++) { +// help[(i + k) % nums.length] = nums[i]; +// } +// +// for (int i = 0; i < nums.length; i++) { +// nums[i] = help[i]; +// } + + // 方法三:环形替换 +// k = k % nums.length; +// int count = 0; +// +// for (int start = 0; count < nums.length; start++) { +// int cur = start; +// int pre = start; +// +// do { +// int next = (cur + k) % nums.length; +// int temp = nums[next]; +// nums[next] = nums[pre]; +// nums[pre] = temp; +// cur = next; +// count++; +// } while (start != cur); +// } + + // 方法四:使用反转 + k = k % nums.length; + nums = reverse(0, nums.length - 1, nums); + nums = reverse(0, k - 1, nums); + nums = reverse(k, nums.length - 1, nums); + } + + public int[] reverse(int start, int end, int[] nums) { + while (start < end) { + int temp = nums[end]; + nums[end--] = nums[start]; + nums[start++] = temp; + } + return nums; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week01/[1]\344\270\244\346\225\260\344\271\213\345\222\214.java" "b/week01/[1]\344\270\244\346\225\260\344\271\213\345\222\214.java" new file mode 100644 index 00000000..1e9f68ab --- /dev/null +++ "b/week01/[1]\344\270\244\346\225\260\344\271\213\345\222\214.java" @@ -0,0 +1,53 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 +// +// 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 +// +// +// +// 示例: +// +// 给定 nums = [2, 7, 11, 15], target = 9 +// +//因为 nums[0] + nums[1] = 2 + 7 = 9 +//所以返回 [0, 1] +// +// Related Topics 数组 哈希表 +// 👍 9408 👎 0 + + +import java.util.HashMap; +import java.util.HashSet; + +//leetcode submit region begin(Prohibit modification and deletion) +class 两数之和 { + public int[] twoSum(int[] nums, int target) { + // 暴力法 +// int[] res = new int[2]; +// +// for (int i = 0; i <= nums.length - 2; i++) { +// for (int j = i + 1; j <= nums.length - 1; j++) { +// if (nums[i] + nums[j] == target) { +// res[0] = i; +// res[1] = j; +// return res; +// } +// } +// } +// +// return res; + + // 利用hash加速,一层for循环 + HashMap map = new HashMap<>(); + + for (int i = 0; i < nums.length; i++) { + if (map.containsKey(nums[i])) { + return new int[] {map.get(nums[i]), i}; + } + map.put(target - nums[i], i); + } + + return null; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week01/[21]\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\351\223\276\350\241\250.java" "b/week01/[21]\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\351\223\276\350\241\250.java" new file mode 100644 index 00000000..b2ae0c91 --- /dev/null +++ "b/week01/[21]\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\351\223\276\350\241\250.java" @@ -0,0 +1,62 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 +// +// +// +// 示例: +// +// 输入:1->2->4, 1->3->4 +//输出:1->1->2->3->4->4 +// +// Related Topics 链表 +// 👍 1174 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +/** + * 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 合并两个有序链表 { + // 时间复杂度, n 或者 m, 空间复杂度1. 反正时间复杂度就是n和m中比较小的那一个。 + public ListNode mergeTwoLists(ListNode l1, ListNode l2) { + // 迭代 + ListNode soldier = new ListNode(0); + ListNode p = soldier; + + while (l1 != null && l2 != null) { + if (l1.val < l2.val) { + p.next = l1; + l1 = l1.next; + } else { + p.next = l2; + l2 = l2.next; + } + p = p.next; + } + + p.next = l1 == null ? l2 : l1; + + return soldier.next; + + // 递归 + if (l1 == null) { + return l2; + } else if (l2 == null) { + return l1; + } else if (l1.val < l2.val) { + l1.next = mergeTwoLists(l1.next, l2); + return l1; + } else { + l2.next = mergeTwoLists(l2, l2.next); + return l2; + } + } +} +// leetcode submit region end(Prohibit modification and deletion) diff --git "a/week01/[26]\345\210\240\351\231\244\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.java" "b/week01/[26]\345\210\240\351\231\244\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.java" new file mode 100644 index 00000000..24b230d6 --- /dev/null +++ "b/week01/[26]\345\210\240\351\231\244\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.java" @@ -0,0 +1,81 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。 +// +// 不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 +// +// +// +// 示例 1: +// +// 给定数组 nums = [1,1,2], +// +//函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。 +// +//你不需要考虑数组中超出新长度后面的元素。 +// +// 示例 2: +// +// 给定 nums = [0,0,1,1,1,2,2,3,3,4], +// +//函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。 +// +//你不需要考虑数组中超出新长度后面的元素。 +// +// +// +// +// 说明: +// +// 为什么返回数值是整数,但输出的答案是数组呢? +// +// 请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。 +// +// 你可以想象内部操作如下: +// +// // nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝 +//int len = removeDuplicates(nums); +// +//// 在函数里修改输入数组对于调用者是可见的。 +//// 根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。 +//for (int i = 0; i < len; i++) { +//    print(nums[i]); +//} +// +// Related Topics 数组 双指针 +// 👍 1551 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class 删除排序数组中的重复元素 { + public int removeDuplicates(int[] nums) { +// int i = 0,j = 1; +// int length = nums.length; +// +// for (i = 0; i < length - 1; i++) { +// while (j < length && nums[i] == nums[j]) { +// j++; +// } +// if (j < length) { +// nums[i + 1] = nums[j]; +// } else { +// nums[i + 1] = nums[j - 1]; +// break; +// } +// +// } +// return i + 1; + + // 官方解 + int i = 0; + int length = nums.length; + + for (int j = 1; j < length; j++) { + if (nums[i] != nums[j]) { + i++; + nums[i] = nums[j]; + } + } + return i + 1; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week01/[283]\347\247\273\345\212\250\351\233\266.java" "b/week01/[283]\347\247\273\345\212\250\351\233\266.java" new file mode 100644 index 00000000..628a23a6 --- /dev/null +++ "b/week01/[283]\347\247\273\345\212\250\351\233\266.java" @@ -0,0 +1,77 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 +// +// 示例: +// +// 输入: [0,1,0,3,12] +//输出: [1,3,12,0,0] +// +// 说明: +// +// +// 必须在原数组上操作,不能拷贝额外的数组。 +// 尽量减少操作次数。 +// +// Related Topics 数组 双指针 +// 👍 732 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class 移动零 { + public void moveZeroes(int[] nums) { + // 申请一个数组的方法 + int[] b = new int[nums.length]; + int i = 0, j = nums.length - 1; + + for (int k = 0; k < nums.length; k++) { + if (nums[k] != 0) { + b[i] = nums[k]; + i++; + } else { + b[j] = 0; + j--; + } + } + + // 双指针法,定义一个j指针变量,指向的是当前可以放置非0元素的位置 + int j = 0; + + for (int i = 0; i < nums.length; i++) { + if (nums[i] != 0) { + nums[j] = nums[i]; + + if (i != j) { + nums[i] = 0; + } + + j++; + } + } + + // 双指针法,先把非0元素全移到前面,然后再在末尾补0 + int j = 0; + + for (int i = 0; i < nums.length; i++) { + if (nums[i] != 0) { + nums[j++] = nums[i]; + } + } + + for (int i = j; i < nums.length; i++) { + nums[i] = 0; + } + + // 双指针交换法:一直交换,同第一种双指针方法的思路感觉是类似的 + int j = 0; + + for (int i = 0; i < nums.length; i++) { + if (nums[i] != 0) { + int temp = nums[j]; + nums[j] = nums[i]; + nums[i] = temp; + j++; + } + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week01/[42]\346\216\245\351\233\250\346\260\264.java" "b/week01/[42]\346\216\245\351\233\250\346\260\264.java" new file mode 100644 index 00000000..9226e0d1 --- /dev/null +++ "b/week01/[42]\346\216\245\351\233\250\346\260\264.java" @@ -0,0 +1,65 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 +// +// +// +// 示例 1: +// +// +// +// +//输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] +//输出:6 +//解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 +// +// +// 示例 2: +// +// +//输入:height = [4,2,0,3,2,5] +//输出:9 +// +// +// +// +// 提示: +// +// +// n == height.length +// 0 <= n <= 3 * 104 +// 0 <= height[i] <= 105 +// +// Related Topics 栈 数组 双指针 +// 👍 1770 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class 接雨水 { + public int trap(int[] height) { + if (height == null || height.length == 0) { + return 0; + } + + int size = height.length; + int[] leftMax = new int[size]; + int[] rightMax = new int[size]; + int ans = 0; + + leftMax[0] = height[0]; + for (int i = 1; i < size; i++) { + leftMax[i] = Math.max(height[i], leftMax[i - 1]); + } + + rightMax[size - 1] = height[size - 1]; + for (int i = size - 2; i >= 0; i--) { + rightMax[i] = Math.max(height[i], rightMax[i + 1]); + } + + for (int i = 0; i < size; i++) { + ans += Math.min(leftMax[i], rightMax[i]) - height[i]; + } + + return ans; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week01/[641]\350\256\276\350\256\241\345\276\252\347\216\257\345\217\214\347\253\257\351\230\237\345\210\227.java" "b/week01/[641]\350\256\276\350\256\241\345\276\252\347\216\257\345\217\214\347\253\257\351\230\237\345\210\227.java" new file mode 100644 index 00000000..ab457e0e --- /dev/null +++ "b/week01/[641]\350\256\276\350\256\241\345\276\252\347\216\257\345\217\214\347\253\257\351\230\237\345\210\227.java" @@ -0,0 +1,146 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//设计实现双端队列。 +//你的实现需要支持以下操作: +// +// +// MyCircularDeque(k):构造函数,双端队列的大小为k。 +// insertFront():将一个元素添加到双端队列头部。 如果操作成功返回 true。 +// insertLast():将一个元素添加到双端队列尾部。如果操作成功返回 true。 +// deleteFront():从双端队列头部删除一个元素。 如果操作成功返回 true。 +// deleteLast():从双端队列尾部删除一个元素。如果操作成功返回 true。 +// getFront():从双端队列头部获得一个元素。如果双端队列为空,返回 -1。 +// getRear():获得双端队列的最后一个元素。 如果双端队列为空,返回 -1。 +// isEmpty():检查双端队列是否为空。 +// isFull():检查双端队列是否满了。 +// +// +// 示例: +// +// MyCircularDeque circularDeque = new MycircularDeque(3); // 设置容量大小为3 +//circularDeque.insertLast(1); // 返回 true +//circularDeque.insertLast(2); // 返回 true +//circularDeque.insertFront(3); // 返回 true +//circularDeque.insertFront(4); // 已经满了,返回 false +//circularDeque.getRear(); // 返回 2 +//circularDeque.isFull(); // 返回 true +//circularDeque.deleteLast(); // 返回 true +//circularDeque.insertFront(4); // 返回 true +//circularDeque.getFront(); // 返回 4 +//  +// +// +// +// 提示: +// +// +// 所有值的范围为 [1, 1000] +// 操作次数的范围为 [1, 1000] +// 请不要使用内置的双端队列库。 +// +// Related Topics 设计 队列 +// 👍 60 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class MyCircularDeque { + // 容量 + private int capacity; + // 使用数组进行模拟 + private int[] arr; + // 头指针 + private int front; + // 尾指针 + private int rear; + + /** Initialize your data structure here. Set the size of the deque to be k. */ + public MyCircularDeque(int k) { + capacity = k + 1; + arr = new int[capacity]; + front = 0; + rear = 0; + } + + /** Adds an item at the front of Deque. Return true if the operation is successful. */ + public boolean insertFront(int value) { + if (isFull()) { + return false; + } + + front = (front - 1 + capacity) % capacity; + arr[front] = value; + return true; + } + + /** Adds an item at the rear of Deque. Return true if the operation is successful. */ + public boolean insertLast(int value) { + if (isFull()) { + return false; + } + + arr[rear] = value; + rear = (rear + 1) % capacity; + return true; + } + + /** Deletes an item from the front of Deque. Return true if the operation is successful. */ + public boolean deleteFront() { + if (isEmpty()) { + return false; + } + + front = (front + 1) % capacity; + return true; + } + + /** Deletes an item from the rear of Deque. Return true if the operation is successful. */ + public boolean deleteLast() { + if (isEmpty()) { + return false; + } + + rear = (rear - 1 + capacity) % capacity; + return true; + } + + /** Get the front item from the deque. */ + public int getFront() { + if (isEmpty()) { + return -1; + } + + return arr[front]; + } + + /** Get the last item from the deque. */ + public int getRear() { + if (isEmpty()) { + return -1; + } + + return arr[(rear - 1 + capacity) % capacity]; + } + + /** Checks whether the circular deque is empty or not. */ + public boolean isEmpty() { + return front == rear; + } + + /** Checks whether the circular deque is full or not. */ + public boolean isFull() { + return (rear + 1) % capacity == front; + } +} + +/** + * Your MyCircularDeque object will be instantiated and called as such: + * MyCircularDeque obj = new MyCircularDeque(k); + * boolean param_1 = obj.insertFront(value); + * boolean param_2 = obj.insertLast(value); + * boolean param_3 = obj.deleteFront(); + * boolean param_4 = obj.deleteLast(); + * int param_5 = obj.getFront(); + * int param_6 = obj.getRear(); + * boolean param_7 = obj.isEmpty(); + * boolean param_8 = obj.isFull(); + */ +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week01/[66]\345\212\240\344\270\200.java" "b/week01/[66]\345\212\240\344\270\200.java" new file mode 100644 index 00000000..03932608 --- /dev/null +++ "b/week01/[66]\345\212\240\344\270\200.java" @@ -0,0 +1,62 @@ +//给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。 +// +// 最高位数字存放在数组的首位, 数组中每个元素只存储单个数字。 +// +// 你可以假设除了整数 0 之外,这个整数不会以零开头。 +// +// 示例 1: +// +// 输入: [1,2,3] +//输出: [1,2,4] +//解释: 输入数组表示数字 123。 +// +// +// 示例 2: +// +// 输入: [4,3,2,1] +//输出: [4,3,2,2] +//解释: 输入数组表示数字 4321。 +// +// Related Topics 数组 +// 👍 528 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class 加一 { + public int[] plusOne(int[] digits) { + // 递归解法 +// return plus(digits, digits.length - 1); + + // 进位解法 + for (int i = digits.length - 1; i >= 0 ; i--) { + digits[i] += 1; + digits[i] = digits[i] % 10; + if (digits[i]%10 != 0) { + return digits; + } + } + + digits = new int[digits.length + 1]; + digits[0] = 1; + return digits; + } + + private int[] plus(int[] digits, int length) { + if (length < 0) { + int[] newDigits = new int[digits.length + 1]; + newDigits[0] = 1; + System.arraycopy(digits, 0, newDigits, 1, newDigits.length - 1); + return newDigits; + } + + if (digits[length] + 1 == 10) { + digits[length] = 0; + digits = plus(digits, length - 1); + } else { + digits[length] += 1; + } + + return digits; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week01/[88]\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.java" "b/week01/[88]\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.java" new file mode 100644 index 00000000..67cf338f --- /dev/null +++ "b/week01/[88]\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.java" @@ -0,0 +1,48 @@ +package Algorithm.leetcode.leetcode.editor.cn;//给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。 +// +// +// +// 说明: +// +// +// 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 +// 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。 +// +// +// +// +// 示例: +// +// 输入: +//nums1 = [1,2,3,0,0,0], m = 3 +//nums2 = [2,5,6], n = 3 +// +//输出: [1,2,2,3,5,6] +// Related Topics 数组 双指针 +// 👍 598 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class 合并两个有序数组 { + public void merge(int[] nums1, int m, int[] nums2, int n) { + int tag = m + n - 1; + int p1 = m - 1; + int p2 = n - 1; + + while (p1 >= 0 && p2 >= 0){ + if (nums1[p1] <= nums2[p2]) { + nums1[tag] = nums2[p2]; + p2--; + } else { + nums1[tag] = nums1[p1]; + p1--; + } + tag--; + } + + while (p2 >= 0) { + nums1[tag--] = nums2[p2--]; + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week02/NOTE.md b/week02/NOTE.md deleted file mode 100644 index 50de3041..00000000 --- a/week02/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git "a/week02/[144]\344\272\214\345\217\211\346\240\221\347\232\204\345\211\215\345\272\217\351\201\215\345\216\206.java" "b/week02/[144]\344\272\214\345\217\211\346\240\221\347\232\204\345\211\215\345\272\217\351\201\215\345\216\206.java" new file mode 100644 index 00000000..3a9be770 --- /dev/null +++ "b/week02/[144]\344\272\214\345\217\211\346\240\221\347\232\204\345\211\215\345\272\217\351\201\215\345\216\206.java" @@ -0,0 +1,130 @@ +package Algorithm.leetcode.leetcode.editor.cn;//给定一个二叉树,返回它的 前序 遍历。 +// +// 示例: +// +// 输入: [1,null,2,3] +// 1 +// \ +// 2 +// / +// 3 +// +//输出: [1,2,3] +// +// +// 进阶: 递归算法很简单,你可以通过迭代算法完成吗? +// Related Topics 栈 树 +// 👍 367 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode(int x) { val = x; } + * } + */ +class 二叉树的前序遍历 { + List result = new ArrayList(); + + public List preorderTraversal(TreeNode root) { + // 方法一: 递归 + // 判空 + if (root == null) { + return res; + } + + // 将根节点加入结果集中 + res.add(root.val); + // 递归遍历根节点的左子节点 + preorderTraversal(root.left); + // 递归遍历根节点的右子节点 + preorderTraversal(root.right); + + return res; + + + // 方法二: 迭代 + // 判空 + if (root == null) { + return res; + } + + Stack s = new Stack<>(); + s.push(root); + + // 当栈不为空时 + while (!s.isEmpty()) { + TreeNode node = s.pop(); + // 当前出栈的元素加入到结果集中 + res.add(node.val); + // 我们为了模拟根左右的遍历顺序,先要将右子节点加入栈中,左子节点后加入。这样下次出栈左节点就会先出栈,才符合我们的需求 + if (node.right != null) { + s.push(node.right); + } + if (node.left != null) { + s.push(node.left); + } + } + + return res; + + // 迭代二 + if (root == null) { + return res; + } + + Stack s = new Stack<>(); + TreeNode node = root; + + // 这边做了一个很巧妙的改变,也就是说如果左子节点不为空的话。 + // 我这边将它push进栈,下一次循环立马就要出栈,中间这个操作可不可以省 + // 掉呢,答案是可以的,我们将while循环的结束条件改为node不为空, + // 第一次是根节点肯定不为空,后面就是直接将右子节点push,然后将node.left赋值 + // 给node,如果node为空,再从栈中pop一个出来。可以想下,当node.left和栈都为空的时候 + // 实际上所有的节点我们都遍历到了 + while (node != null) { + res.add(node.val); + if (node.right != null) { + s.push(node.right); + } + + node = node.left; + if (node == null && !s.isEmpty()) { + node = s.pop(); + } + } + + return res; + + + // 迭代三 + if (root == null) { + return res; + } + + TreeNode p = root; + + while (p != null || !s.isEmpty()) { + if (p != null) { + result.add(p.val); + s.push(p); + p = p.left; + } else { + p = s.pop().right; + } + } + + return result; + } + +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week02/[242]\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.java" "b/week02/[242]\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.java" new file mode 100644 index 00000000..8d09034d --- /dev/null +++ "b/week02/[242]\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.java" @@ -0,0 +1,64 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 +// +// 示例 1: +// +// 输入: s = "anagram", t = "nagaram" +//输出: true +// +// +// 示例 2: +// +// 输入: s = "rat", t = "car" +//输出: false +// +// 说明: +//你可以假设字符串只包含小写字母。 +// +// 进阶: +//如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况? +// Related Topics 排序 哈希表 +// 👍 267 👎 0 + + +import java.lang.reflect.Array; +import java.util.Arrays; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + public boolean isAnagram(String s, String t) { + // 方法1: 数组标记的方式(其实也可以用hashmap,不过这个题目的场景我们用数组即可) + if (s.length() != t.length()) { + return false; + } + + int[] tag = new int[26]; + + for (char c : s.toCharArray()) { + tag[c - 'a']++; + } + + for (char c : t.toCharArray()) { + tag[c - 'a']--; + if (tag[c - 'a'] < 0) { + return false; + } + } + + return true; + + // 方法2: 排序 + if (s.length() != t.length()) { + return false; + } + + char[] s1 = s.toCharArray(); + char[] s2 = t.toCharArray(); + + Arrays.sort(s1); + Arrays.sort(s2); + + return Arrays.equals(s1, s2); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week02/[264]\344\270\221\346\225\260 II.java" "b/week02/[264]\344\270\221\346\225\260 II.java" new file mode 100644 index 00000000..48ff6c04 --- /dev/null +++ "b/week02/[264]\344\270\221\346\225\260 II.java" @@ -0,0 +1,70 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//编写一个程序,找出第 n 个丑数。 +// +// 丑数就是质因数只包含 2, 3, 5 的正整数。 +// +// 示例: +// +// 输入: n = 10 +//输出: 12 +//解释: 1, 2, 3, 4, 5, 6, 8, 9, 10, 12 是前 10 个丑数。 +// +// 说明: +// +// +// 1 是丑数。 +// n 不超过1690。 +// +// Related Topics 堆 数学 动态规划 +// 👍 417 👎 0 + + +import java.util.HashSet; +import java.util.PriorityQueue; + +//leetcode submit region begin(Prohibit modification and deletion) +class 丑数 { + // 题目链接:https://leetcode-cn.com/problems/ugly-number-ii/ + public static Ugly ugly = new Ugly(); + public int nthUglyNumber(int n) { + return ugly.nums[n - 1]; + } +} + +class Ugly { + // 用于存储结果的数组 + public int[] nums = new int[1690]; + + public Ugly() { + // 用hashSet存储已经计算过的丑数,避免重复计算带来的消耗 + HashSet hashSet = new HashSet<>(); + // 申请一个基于优先队列的小顶堆 + PriorityQueue queue = new PriorityQueue<>(); + // 定义一个因子数组,用于之后将堆中元素弹出然后*因子数组中的每个元素,目的是为了找之后的丑数 + int[] element = new int[] {2, 3, 5}; + // 定义两个变量用作存储新旧丑数 + long oldNum = 1, newNum = 0; + // 将第一个丑数加入到hashset以及优先队列中 + hashSet.add(oldNum); + queue.offer(oldNum); + + for (int i = 0; i < 1690; i++) { + // 当前堆中poll出来的一定是当前最小的丑数,也是第i大的丑数(因为是小顶堆) + oldNum = queue.poll(); + // 将第i大的丑数赋值给nums[i] + nums[i] = (int) oldNum; + + for (int j = 0; j < element.length; j++) { + // 计算新的丑数 + newNum = oldNum * element[j]; + // 如果之前没有计算过 + if (!hashSet.contains(newNum)) { + // 则将其加入优先队列以及哈希表 + hashSet.add(newNum); + queue.offer(newNum); + } + } + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week02/[347]\345\211\215 K \344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.java" "b/week02/[347]\345\211\215 K \344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.java" new file mode 100644 index 00000000..8480f087 --- /dev/null +++ "b/week02/[347]\345\211\215 K \344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.java" @@ -0,0 +1,88 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 +// +// +// +// 示例 1: +// +// 输入: nums = [1,1,1,2,2,3], k = 2 +//输出: [1,2] +// +// +// 示例 2: +// +// 输入: nums = [1], k = 1 +//输出: [1] +// +// +// +// 提示: +// +// +// 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。 +// 你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。 +// 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。 +// 你可以按任意顺序返回答案。 +// +// Related Topics 堆 哈希表 +// 👍 556 👎 0 + + +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.PriorityQueue; + +//leetcode submit region begin(Prohibit modification and deletion) +class 前k个高频元素 { + // 题目地址: https://leetcode-cn.com/problems/top-k-frequent-elements/solution/ + public int[] topKFrequent(int[] nums, int k) { + // 先用hashmap存储nums里面每个元素出现的频率。 + Map hashMap = new HashMap<>(); + + // 此循环走完之后,每个元素的次数都被统计完了 + for (int i : nums) { + hashMap.put(i, hashMap.getOrDefault(i, 0) + 1); + } + + // 申请一个基于堆实现的优先队列(默认小顶堆) + PriorityQueue queue = new PriorityQueue<>(new Comparator() { + @Override + public int compare(int[] m, int[] n) { + return m[1] - n[1]; + } + }); + + // 遍历hashmap + for (Map.Entry entry : hashMap.entrySet()) { + // 得到map中每一对键值对中对应的数字和出现次数 + int num = entry.getKey(); + int time = entry.getValue(); + // 如果优先队列满了 + if (queue.size() == k) { + // 则判断堆顶元素的次数是否小于当前元素的次数 + if (queue.peek()[1] < time) { + // 将小的元素从堆中删除 + queue.poll(); + // 再加入新的元素 + queue.offer(new int[] {num, time}); + } + } else { + // 优先队列没有满,那直接往堆中加即可 + queue.offer(new int[] {num, time}); + } + } + // 至此,优先队列中剩余的值就是前k个高频元素,因为是小顶堆,堆顶元素是最小的,比他更小的都在比较重被淘汰了,剩余的自然就是前k个高频元素了 + + // 结果集 + int[] res = new int[k]; + + // 依次遍历将优先队列中的数组中的高频元素拿出来 + for (int i = 0; i < k; i++) { + res[i] = queue.poll()[0]; + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week02/[429]N\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.java" "b/week02/[429]N\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.java" new file mode 100644 index 00000000..8be6dc64 --- /dev/null +++ "b/week02/[429]N\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.java" @@ -0,0 +1,97 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。 +// +// 例如,给定一个 3叉树 : +// +// +// +// +// +// +// +// 返回其层序遍历: +// +// [ +// [1], +// [3,2,4], +// [5,6] +//] +// +// +// +// +// 说明: +// +// +// 树的深度不会超过 1000。 +// 树的节点总数不会超过 5000。 +// Related Topics 树 广度优先搜索 +// 👍 110 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +/* +// Definition for a Node. +class Node { + public int val; + public List children; + + public Node() {} + + public Node(int _val) { + val = _val; + } + + public Node(int _val, List _children) { + val = _val; + children = _children; + } +}; +*/ + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +// 题目地址 +class N叉树的层序遍历 { + // 题目地址:https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/ + + // 结果集 + List> res = new ArrayList<>(); + public List> levelOrder(Node root) { + // 判空 + if (root == null) { + return res; + } + + // 用队列进行层序遍历 + Queue queue = new LinkedList<>(); + queue.add(root); + + // 如果队列非空,证明还有节点没有遍历完 + while (!queue.isEmpty()) { + // 用于存储每一层的结果集 + List temp = new ArrayList<>(); + // 当前层次的节点个数 + int size = queue.size(); + // 当当前层次还没有遍历完,则继续遍历 + while (size != 0) { + // 出队 + Node node = queue.poll(); + // 加入当前层的结果集 + temp.add(node.val); + // 当前层的size - 1 + size--; + + // 将当前节点的所有儿子节点加入队列中(这也就是下一层的节点了) + queue.addAll(node.children); + } + // 将刚刚遍历的那一层的结果集加入到最终的结果集 + res.add(temp); + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week02/[49]\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.java" "b/week02/[49]\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.java" new file mode 100644 index 00000000..e4443354 --- /dev/null +++ "b/week02/[49]\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.java" @@ -0,0 +1,88 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。 +// +// 示例: +// +// 输入: ["eat", "tea", "tan", "ate", "nat", "bat"] +//输出: +//[ +// ["ate","eat","tea"], +// ["nat","tan"], +// ["bat"] +//] +// +// 说明: +// +// +// 所有输入均为小写字母。 +// 不考虑答案输出的顺序。 +// +// Related Topics 哈希表 字符串 +// 👍 505 👎 0 + + +import scala.Int; + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class 字母异位词分组 { + // 题目地址:https://leetcode-cn.com/problems/group-anagrams/ + public List> groupAnagrams(String[] strs) { + // 方法一: 排序法 + // 判空 + if (strs == null || strs.length == 0) { + return new ArrayList<>(); + } + + // 用hashmap来存储,异位词排序后肯定是一样的,可以作为键值 + Map> map = new HashMap<>(); + + for (String s : strs) { + char[] temp = s.toCharArray(); + // 对字符数组排序 + Arrays.sort(temp); + // 将排序好的字符串作为hashmap的key + String key = String.valueOf(temp); + // 如果map中还没有这个键值 + if (!map.containsKey(key)) { + // 那么将这个键值写进去 + map.put(key, new ArrayList<>()); + } + // 将这个字符串加到它所对应的键值的列表里面 + map.get(key).add(s); + } + + return new ArrayList<>(map.values()); + + + // 方法二: 计数法 + // 判空 + if (strs.length == 0 || strs == null) { + return new ArrayList<>(); + } + + // 用hashmap来存储,异位词如果映射为数字最终加起来的值肯定是一样的,可以作为键值 + Map> map = new HashMap<>(); + + for (String s : strs) { + char[] temp = new char[26]; + + for (char c : s.toCharArray()) { + temp[c - 'a'] ++; + } + // 此for循环结束之后,temp所代表的信息已经是和他的字母异位词一样的! + + // 将这个信息作为key + String key = String.valueOf(temp); + if (!map.containsKey(key)) { + map.put(key, new ArrayList<>()); + } + + map.get(key).add(s); + } + + return new ArrayList<>(map.values()); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week02/[589]N\345\217\211\346\240\221\347\232\204\345\211\215\345\272\217\351\201\215\345\216\206.java" "b/week02/[589]N\345\217\211\346\240\221\347\232\204\345\211\215\345\272\217\351\201\215\345\216\206.java" new file mode 100644 index 00000000..a336c742 --- /dev/null +++ "b/week02/[589]N\345\217\211\346\240\221\347\232\204\345\211\215\345\272\217\351\201\215\345\216\206.java" @@ -0,0 +1,78 @@ +package Algorithm.leetcode.leetcode.editor.cn;//给定一个 N 叉树,返回其节点值的前序遍历。 +// +// 例如,给定一个 3叉树 : +// +// +// +// +// +// +// +// 返回其前序遍历: [1,3,5,6,2,4]。 +// +// +// +// 说明: 递归法很简单,你可以使用迭代法完成此题吗? Related Topics 树 +// 👍 103 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +/* +// Definition for a Node. +class Node { + public int val; + public List children; + + public Node() {} + + public Node(int _val) { + val = _val; + } + + public Node(int _val, List _children) { + val = _val; + children = _children; + } +}; +*/ + +import java.util.*; + +class N叉树的前序遍历 { + List result = new ArrayList<>(); + public List preorder(Node root) { + // 递归解法(深度优先dfs) + if (root == null) { + return result; + } + + + result.add(root.val); + + List ln = root.children; + for (int i = 0; i < ln.size(); i++) { + result = preorder(ln.get(i)); + } + + return result; + + // 迭代解法(广度优先bfs) + if (root == null) { + return result; + } + + Stack s = new Stack(); + s.push(root); + + while (!s.isEmpty()) { + root = s.pop(); + result.add(root.val); + + for (int i = root.children.size() - 1; i >= 0; i--) { + s.push(root.children.get(i)); + } + } + return result; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week02/[94]\344\272\214\345\217\211\346\240\221\347\232\204\344\270\255\345\272\217\351\201\215\345\216\206.java" "b/week02/[94]\344\272\214\345\217\211\346\240\221\347\232\204\344\270\255\345\272\217\351\201\215\345\216\206.java" new file mode 100644 index 00000000..c7abd387 --- /dev/null +++ "b/week02/[94]\344\272\214\345\217\211\346\240\221\347\232\204\344\270\255\345\272\217\351\201\215\345\216\206.java" @@ -0,0 +1,93 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个二叉树,返回它的中序 遍历。 +// +// 示例: +// +// 输入: [1,null,2,3] +// 1 +// \ +// 2 +// / +// 3 +// +//输出: [1,3,2] +// +// 进阶: 递归算法很简单,你可以通过迭代算法完成吗? +// Related Topics 栈 树 哈希表 +// 👍 716 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) + +import java.util.ArrayList; +import java.util.List; +import java.util.Stack; + +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode() {} + * TreeNode(int val) { this.val = val; } + * TreeNode(int val, TreeNode left, TreeNode right) { + * this.val = val; + * this.left = left; + * this.right = right; + * } + * } + */ +class 二叉树的中序遍历 { + // 题目URL: https://leetcode-cn.com/problems/binary-tree-inorder-traversal/submissions/ + List res = new ArrayList<>(); + + public List inorderTraversal(TreeNode root) { + // 方法一: 递归 + // 判空 + if (root == null) { + return res; + } + + // 递归遍历左子树 + inorderTraversal(root.left); + // 访问根节点 + res.add(root.val); + // 递归遍历右子树 + inorderTraversal(root.right); + // 返回结果 + return res; + + + + // 方法二: 迭代 + // 判空 + if (root == null) { + return res; + } + + Stack s = new Stack<>(); + TreeNode node = root; + + // node不为空代表上一个出栈元素的右孩子不为空,s非空代表栈非空。如果这两个都空,证明我们已经遍历完整棵树了 + while (node != null || !s.isEmpty()) { + // 往左边找到最左边的节点 + while (node != null) { + // 因为栈是先进后出结构,所以每次找到都将其入栈,这样就能实现后面的左节点会先出栈 + s.push(node); + // 将当前指针指向它的左节点循环迭代 + node = node.left; + } + + // 这时候已经找到了最左边的节点,也就是栈顶元素,我们将其出栈 + node = s.pop(); + // 再将这个元素加入到res列表里面 + res.add(node.val); + // 再访问当前节点的右节点(由左,根,右访问顺序决定) + node = node.right; + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week02/images/1.png b/week02/images/1.png new file mode 100644 index 00000000..8ba021f0 Binary files /dev/null and b/week02/images/1.png differ diff --git a/week02/images/2.png b/week02/images/2.png new file mode 100644 index 00000000..03f6e346 Binary files /dev/null and b/week02/images/2.png differ diff --git "a/week02/\344\270\244\346\225\260\344\271\213\345\222\214.java" "b/week02/\344\270\244\346\225\260\344\271\213\345\222\214.java" new file mode 100644 index 00000000..a2e40854 --- /dev/null +++ "b/week02/\344\270\244\346\225\260\344\271\213\345\222\214.java" @@ -0,0 +1,15 @@ +class Solution { + public int[] twoSum(int[] nums, int target) { + Map hashMap = new HashMap<>(); + + for (int i = 0; i < nums.length; i++) { + if (hashMap.containsKey(nums[i])) { + return new int[] {hashMap.get(nums[i]), i}; + } else { + hashMap.put(target - nums[i], i); + } + } + + return null; + } +} \ No newline at end of file diff --git "a/week02/\350\207\252\345\255\246\345\240\206\346\216\222\345\272\217.md" "b/week02/\350\207\252\345\255\246\345\240\206\346\216\222\345\272\217.md" new file mode 100644 index 00000000..def235a3 --- /dev/null +++ "b/week02/\350\207\252\345\255\246\345\240\206\346\216\222\345\272\217.md" @@ -0,0 +1,100 @@ +### 一. 学习笔记 + 这周的学习明显感觉题目的难度和理解的难度都明显在往 +上走,所以也是花了更多的时间在算法上,中心思想还是通过超哥所说 +的五毒神掌,不断反复看视频,不断反复看题目,写题目,来让自己 +对某一个思想,某一个算法的理解不断加深。 + +### 二. 自学堆排序 +堆排序分为两步: +* 构建堆 +* 排序 + +1. 构建堆 + + 构建堆的思路是从堆中第一个非叶子节点开始,从上往下进行构建, + 以大顶堆举例,第一个非叶子节点会依次和他的子节点比较,子节点又会不断 + 和子节点的子节点进行比较,直到从这个节点出发,都满足大顶堆的要求。然后再循环往复去比较 + 第二个非叶子节点,第三个非叶子节点...如图所示: + + ![avatar](./images/1.png) + ![avatar](./images/2.png) + + 代码如下所示: + ``` + /** + * 构建堆 + */ + public void buildHeap() { + for (int i = (heap.length >> 1) - 1; i >= 0; i--) { + upToDownBigHeap(i); + } + } + +/** + * 自顶向下堆化(大顶堆) + * @param index 自顶向下开始堆化的下标 + */ + public void upToDownBigHeap(int index) { + int child; + int temp = heap[index]; + while (child(index, 1) < capacity) { + child = maxChild(index); + if (temp > heap[child]) { + break; + } + heap[index] = heap[child]; + index = child; + } + + heap[index] = temp; + } + ``` +2.排序 + + 排序的思路很简单,就是将堆顶元素和堆尾元素不断进行交换即可 + ``` +/** + * 堆排序 + */ + public void sort() { + if (heap.length <= 1) { + return; + } + + // 1. 将数组构建成一个堆(从第一个非叶子节点开始构建) + buildHeap(); + + // 2. 排序 + int k = capacity - 1; + while (k > 0) { + int temp = heap[k]; + heap[k--] = heap[0]; + heap[0] = temp; + capacity--; + heapIndex--; + upToDownBigHeap(0); + } +// System.out.println(Arrays.toString(heap)); + } + +/** + * 自顶向下堆化(大顶堆) + * @param index 自顶向下开始堆化的下标 + */ + public void upToDownBigHeap(int index) { + int child; + int temp = heap[index]; + while (child(index, 1) < capacity) { + child = maxChild(index); + if (temp > heap[child]) { + break; + } + heap[index] = heap[child]; + index = child; + } + + heap[index] = temp; + } + ``` + + \ No newline at end of file diff --git a/week03/NOTE.md b/week03/NOTE.md index 50de3041..45f667d5 100644 --- a/week03/NOTE.md +++ b/week03/NOTE.md @@ -1 +1,21 @@ -学习笔记 \ No newline at end of file +学习笔记 + +一. 在学习递归时发现有一些难点并没有能够很好的去突破,以前一直是处于超哥说的那种 +傻递归,一直手动递归。而在这周所做的大量的练习里面我发现两点 +1. 递归最重要的就是你要 +知道为什么需要递归,递归究竟在解决什么问题,递归函数你怎么设计。包括递归函数的含义以及参数的设置。 +2. 递归的时候要避免混乱逻辑,怎么避免呢,就是当前层只管当前层,不要去管下面一层的事情或者上面一层的事情,否则你会把自己绕进去的。 + +二. 在做全排列二这道题目的时候,我发现官方题解其实有点不好理解,如果我们是基于全排列1那道题目去加一个剪枝的操作,这样就会好理解很多。 +也就是visit数组不以i作为下标,而是以nums[i]作为下标,这样当前层我就可以很好的 +知道当前层哪一个数是我选了的,剩下的数里面即便有和他一样的也不会去访问到。 +剪枝的代码也就一句而已: + +```java +if (visited[nums[i]]) { + continue; +} +``` + +三. 在做从前序中序遍历构造二叉树这道题目中对于二叉树的前中后序遍历序列又有了更加深入的理解 +。 diff --git a/week03/combinations.java b/week03/combinations.java new file mode 100644 index 00000000..e09bf39e --- /dev/null +++ b/week03/combinations.java @@ -0,0 +1,80 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。 +// +// 示例: +// +// 输入: n = 4, k = 2 +//输出: +//[ +// [2,4], +// [3,4], +// [2,3], +// [1,2], +// [1,3], +// [1,4], +//] +// Related Topics 回溯算法 +// 👍 432 👎 0 + + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +//leetcode submit region begin(Prohibit modification and deletion) +class combinations { + // 题目地址:https://leetcode-cn.com/problems/combinations/ + + // 存储每一个可能的结果 + List temp = new ArrayList<>(); + // 所有结果的可能的结果集 + List> res = new ArrayList>(); + public List> combine(int n, int k) { + /** 方法一:递归 + * + * 时间复杂度:为 O({n \choose k} * k),前一个组合枚举的复杂度,由于每次记录答案的复杂度为 O(k),故这里的时间复杂度为 O({n \choose k} *k k) + * 空间复杂度:temp为O(K), 递归开辟的栈为O(N), 所以最终为O(N + K) = O(N) + */ + + // 调用自己的递归函数 + dfs(1, n, k); + return res; + } + + /** + * 深度优先遍历 + * + * @param cur 当前遍历到第几个数字了 + * @param n 数字一共多少个 + * @param k 需要从n个中选出k个 + */ + public void dfs(int cur, int n, int k) { + // 递归出口,也是一个剪枝操作 + // 如果说当前的这种决策导致即便剩下的数字全选也打不到k个数,那么我们可以直接在这一层就放弃这种选择 + if (temp.size() + n - cur + 1 < k) { + return; + } + + // 递归出口 + // 如果temp的size等于k,就证明我们找到了k个数,将temp加到结果集中,然后直接返回 + if (temp.size() == k) { + // 这里为什么要new一个,因为temp是个全局变量,你加同一个全局变量到结果集里面有可能会发生覆盖问题。 + res.add(new ArrayList<>(temp)); + return; + } + + // 选择当前的这个数 + temp.add(cur); + // 继续深度遍历 + dfs(cur + 1, n, k); + + // 不选则当前的这个数,为什么只用删除这一个呢,会不会想temp是个全局变量,下面层 + // 如果做了大量的add操作,temp里面会有好多值,实际上不会,地下层返回来一定都经历过remove这句话了。所以返回到你 + // 当前这一层你就只需要关注你当前层加进去的元素啦。 + temp.remove(temp.size() - 1); + // 继续深度遍历 + dfs(cur + 1, n, k); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week03/construct-binary-tree-from-preorder-and-inorder-traversal.java b/week03/construct-binary-tree-from-preorder-and-inorder-traversal.java new file mode 100644 index 00000000..bc8e744b --- /dev/null +++ b/week03/construct-binary-tree-from-preorder-and-inorder-traversal.java @@ -0,0 +1,100 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//根据一棵树的前序遍历与中序遍历构造二叉树。 +// +// 注意: +//你可以假设树中没有重复的元素。 +// +// 例如,给出 +// +// 前序遍历 preorder = [3,9,20,15,7] +//中序遍历 inorder = [9,3,15,20,7] +// +// 返回如下的二叉树: +// +// 3 +// / \ +// 9 20 +// / \ +// 15 7 +// Related Topics 树 深度优先搜索 数组 +// 👍 745 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) + +import java.util.HashMap; +import java.util.Map; + +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode(int x) { val = x; } + * } + */ +class construct-binary-tree-from-preorder-and-inorder-traversal { + // 题目链接: https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/solution/ + + Map findRoot = new HashMap(); + public TreeNode buildTree(int[] preorder, int[] inorder) { + /** + * 方法一: 递归 + * + * 时间复杂度:O(N), N就是树的节点个数 + * 空间复杂度:O(N), 除去返回的答案需要的 O(n) 空间之外,我们还需要使用 O(n) 的空间存储哈希映射,以及 O(h)(其中 h 是树的高度)的空间表示递归时栈空间。这里 h preorderRight) { + return null; + } + + // 2. 处理当前层逻辑 + + // 拿到根节点,前序遍历第一个元素就是根节点 + int rootValue = preorder[preorderLeft]; + // 找出根节点在中序遍历中的位置,这样我们就知道了左子树的节点数目和右子树的节点数目 + int rootInorderIndex = findRoot.get(rootValue); + // 计算出左子树的节点个数 + int leftNodeNum = rootInorderIndex - inorderLeft; + // 先构造出当前根节点 + TreeNode root = new TreeNode(rootValue); + + // 3. 递归调用 + + // 递归的得到左子树 + // 对于前序序列而言,左子树就是当前的left边界加一,到left边界加左子树的数目; + // 对于中序序列而言,左子树就是当前的left边界到中序序列对应的根节点的位置的前一个节点 + root.left = myBuildTree(preorder, inorder, preorderLeft + 1, preorderLeft + leftNodeNum, inorderLeft, rootInorderIndex - 1); + + // 递归的得到右子树 + // 对于前序序列而言,右子树的起点就是除掉根节点以及左子树的之后的下标, 终点就是当前层的preorderRight + // 对于中序序列而言,右子树的起点就是根节点所在下标加一,终点就是当前层的inorderRight + root.right = myBuildTree(preorder, inorder, preorderLeft + leftNodeNum + 1, preorderRight, rootInorderIndex + 1, inorderRight); + return root; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week03/lowest-common-ancestor-of-a-binary-tree.java b/week03/lowest-common-ancestor-of-a-binary-tree.java new file mode 100644 index 00000000..741a4765 --- /dev/null +++ b/week03/lowest-common-ancestor-of-a-binary-tree.java @@ -0,0 +1,140 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 +// +// 百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大( +//一个节点也可以是它自己的祖先)。” +// +// 例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4] +// +// +// +// +// +// 示例 1: +// +// 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 +//输出: 3 +//解释: 节点 5 和节点 1 的最近公共祖先是节点 3。 +// +// +// 示例 2: +// +// 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 +//输出: 5 +//解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。 +// +// +// +// +// 说明: +// +// +// 所有节点的值都是唯一的。 +// p、q 为不同节点且均存在于给定的二叉树中。 +// +// Related Topics 树 +// 👍 815 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; + +/** + * Definition for a binary tree node. + * public class TreeNode { + * int val; + * TreeNode left; + * TreeNode right; + * TreeNode(int x) { val = x; } + * } + */ +// lowest-common-ancestor-of-a-binary-tree +class lowest-common-ancestor-of-a-binary-tree { + // 题目链接: https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/ + + Map map = new HashMap<>(); + Set set = new HashSet<>(); + + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { +// 方法一:递归 + + /** + * 时间复杂的: 每个树节点都被访问且仅被访问N次(N为树的节点个数)所以时间复杂度是 O(n) + * 空间复杂度: 由于进行了递归,递归的最大深度本来是树的深度,但是如果树极端情况下退化成链表,所以空间复杂度是O(n) + */ + +// 如果root为空返回很容易理解,root 等于 q, p中的一个,那么root就一定是最近的公共祖先 + if (root == null || root == p || root == q) { + return root; + } + + // 到下层左边去进行寻找 + TreeNode left = lowestCommonAncestor(root.left, p, q); + // 到下层右边去寻找 + TreeNode right = lowestCommonAncestor(root.right, p, q); + + // 如果左边没有找到并且右边也没有找到,证明就是没有,返回空 + if (left == null && right == null) { + return null; + } + + // 如果左边没有找到,那证明两个节点都在右边。 + if (left == null) { + return right; + } + + // 如果右边没有找到,那证明两个节点都在左边 + if (right == null) { + return left; + } + + // 最后一种情况就是左边也不是null, 右边也不是null,则最近公共祖先是root + return root; + + + + // 方法二: 将父节点存储下来,最终判断两个人有没有第一次访问过同一个节点,就找到了他们的最近公共父节点了。 + + /** + * 时间复杂度:O(N),其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,从 p 和 q 节点往上跳经过的祖先节点个数不会超过 NN,因此总的时间复杂度为 O(N)O(N)。 + * O(N) ,其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 NN,因此空间复杂度为 O(N)O(N),哈希表存储每个节点的父节点也需要 O(N)O(N) 的空间复杂度,因此最后总的空间复杂度为 O(N)O(N) + */ + + dfs(root); + + // 先从p节点开始从下往上遍历,每遍历一个都加入到set集合中,然后获取其父节点直到为空 + while (p != null) { + set.add(p.val); + p = map.get(p.val); + } + + // 再从q节点从下往上遍历 + while (q != null) { + // 如果集合中包含这个节点,证明刚刚p遍历过了,这就是他们的最近公共父节点 + if (set.contains(q.val)) { + return q; + } + // 否则继续往上遍历 + q = map.get(q.val); + } + + // 循环结束还没找到那就是不存在了 + return null; + } + + public void dfs(TreeNode root) { + if (root.left != null) { + map.put(root.left.val, root); + dfs(root.left); + } + + if (root.right != null) { + map.put(root.right.val, root); + dfs(root.right); + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week03/permutations-ii.java b/week03/permutations-ii.java new file mode 100644 index 00000000..2b0beaa2 --- /dev/null +++ b/week03/permutations-ii.java @@ -0,0 +1,100 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。 +// +// +// +// 示例 1: +// +// +//输入:nums = [1,1,2] +//输出: +//[[1,1,2], +// [1,2,1], +// [2,1,1]] +// +// +// 示例 2: +// +// +//输入:nums = [1,2,3] +//输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] +// +// +// +// +// 提示: +// +// +// 1 <= nums.length <= 8 +// -10 <= nums[i] <= 10 +// +// Related Topics 回溯算法 +// 👍 518 👎 0 + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +//leetcode submit region begin(Prohibit modification and deletion) +class permutations-ii { + // 题目链接:https://leetcode-cn.com/problems/permutations-ii/ + + // 定义结果集 + List> res = new ArrayList<>(); + // 定义每一个符合题意的结果 + List temp = new ArrayList<>(); + // 定义visit数组 + boolean[] visit; + public List> permuteUnique(int[] nums) { + /** + * 方法一:递归回溯 + * + * 时间复杂度: O(N * N!), N!是全排列的时间复杂度,N是每一次递归调用结果都要复制一份再加入到res结果集中 + * 空间复杂度:O(N), 存储结果集的空间复杂度为O(N), 递归调用的深度也是O(N) + */ + + // 初始化visit数组,长度为nums数组的长度即可 + visit = new boolean[nums.length]; + // 对数组进行排序,这样的好处是我们能自定义规则说前面的还没访问不能访问后面的(针对有相同数字的数组而言),这样能有效避免重复结果 + Arrays.sort(nums); + // 递归调用 + dfs(nums, 0); + return res; + } + + /** + * 深度优先遍历 + * + * @param nums 待全排列的数组 + * @param index 全排列已经选了多少个元素了 + */ + public void dfs(int[] nums, int index) { + // 递归出口 + // 如果index已经等于数组的长度,证明某一个全排列的结果已经得到了,加入到结果集,返回即可 + if (index == nums.length) { + res.add(new ArrayList<>(temp)); + return; + } + + // 对数组进行循环,循环里面进行递归调用 + for (int i = 0; i < nums.length; i++) { + // 如果这个元素已经visit过,肯定是在上面的层次访问过了。也就是已经加入过temp结果中,那么直接跳过这次循环 + // 如果当前元素和前一个元素是一样的,并且前一个没有访问过,那这个也不能让他访问,否则结果集中就会出现重复的结果 + if (visit[i] || (i > 0 && nums[i] == nums[i - 1] && !visit[i - 1])) { + continue; + } + + // 当前元素要加入进来,即将它加入到结果中,并且置标记数组为true + temp.add(nums[i]); + visit[i] = true; + // 递归调用 + dfs(nums, index + 1); + + // 当前元素不加入进来,即将它remove掉,并且置标记数组为false + visit[i] = false; + temp.remove(index); + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week03/permutations.java b/week03/permutations.java new file mode 100644 index 00000000..506f433f --- /dev/null +++ b/week03/permutations.java @@ -0,0 +1,79 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个 没有重复 数字的序列,返回其所有可能的全排列。 +// +// 示例: +// +// 输入: [1,2,3] +//输出: +//[ +// [1,2,3], +// [1,3,2], +// [2,1,3], +// [2,3,1], +// [3,1,2], +// [3,2,1] +//] +// Related Topics 回溯算法 +// 👍 982 👎 0 + + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +//leetcode submit region begin(Prohibit modification and deletion) +class permutations { + // 题目链接:https://leetcode-cn.com/problems/permutations/ + + // 最终的结果集 + List> res = new ArrayList>(); + // 存储每一个结果 + List temp = new ArrayList<>(); + public List> permute(int[] nums) { + /** + * 方法一:递归回溯 + * + * 时间复杂度: O(N * N!), N!是全排列的时间复杂度,N是每一次递归调用结果都要复制一份再加入到res结果集中 + * 空间复杂度:O(N), 存储结果集的空间复杂度为O(N), 递归调用的深度也是O(N) + */ + + // 将int数组的值存储到ArrayList里面,方便我们后面做交换操作 + for (int num : nums) { + temp.add(num); + } + + // 得到数组的长度 + int length = nums.length; + // 递归调用 + dfs(length, 0); + + return res; + } + + /** + * 深度遍历 + * + * @param length 数组的长度 + * @param start 当前层的未遍历边界 + */ + public void dfs(int length, int start) { + // 递归出口 + // 如果当前的未遍历边界已经和数组长度一样,那就证明没有未遍历的元素了,我们都遍历过了,则返回即可。 + if (start == length) { + res.add(new ArrayList<>(temp)); + return; + } + + // 处理当前层逻辑 + for (int i = start; i < length; i++) { + // 我们要当前层的这个元素 + Collections.swap(temp, start, i); + // 递归调用 + dfs(length, start + 1); + // 当前层的元素不要,那么我们要将状态恢复一下,把刚刚交换过的元素交换回来 + Collections.swap(temp, start, i); + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/NOTE.md b/week04/NOTE.md deleted file mode 100644 index 50de3041..00000000 --- a/week04/NOTE.md +++ /dev/null @@ -1 +0,0 @@ -学习笔记 \ No newline at end of file diff --git a/week04/best-time-to-buy-and-sell-stock-ii.java b/week04/best-time-to-buy-and-sell-stock-ii.java new file mode 100644 index 00000000..559a42b0 --- /dev/null +++ b/week04/best-time-to-buy-and-sell-stock-ii.java @@ -0,0 +1,93 @@ +package Algorithm.leetcode.leetcode.editor.cn;//给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 +// +// 设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 +// +// 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 +// +// +// +// 示例 1: +// +// 输入: [7,1,5,3,6,4] +//输出: 7 +//解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 +//  随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 +// +// +// 示例 2: +// +// 输入: [1,2,3,4,5] +//输出: 4 +//解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。 +//  注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。 +//  因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 +// +// +// 示例 3: +// +// 输入: [7,6,4,3,1] +//输出: 0 +//解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 +// +// +// +// 提示: +// +// +// 1 <= prices.length <= 3 * 10 ^ 4 +// 0 <= prices[i] <= 10 ^ 4 +// +// Related Topics 贪心算法 数组 +// 👍 827 👎 0 + + +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +//leetcode submit region begin(Prohibit modification and deletion) +class best-time-to-buy-and-sell-stock-ii { + // 题目链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/ + public int maxProfit(int[] prices) { + /** + * 方法二:贪心算法 + * + * 时间复杂度:O(N), 一次遍历 + * 空间复杂度:O(1),没有用到额外空间 + */ + + // 定义一个差值变量,只要后一天的价格比前一天高,我们就贪心的把这个差值加入到总利润里 + int diff = 0; + // 定义返回结果 + int max = 0; + for (int i = 0; i < prices.length - 1; i++) { + // 计算后一天和当前天的差值 + diff = prices[i + 1] - prices[i]; + + // 如果差值大于0,证明可以贪心 + if (diff > 0) { + max += diff; + } + } + return max; + +// 自己的方法(逻辑比较混乱就不详细解释了) + int sumMax = 0; + int max = 0; + int minValue = Integer.MAX_VALUE; + + for (int i = 0; i < prices.length; i++) { + minValue = Math.min(minValue, prices[i]); + + max = Math.max(max, prices[i] - minValue); + + if (prices[i] - minValue < max || ((prices[i] - minValue >= max)&&i == prices.length - 1)) { + sumMax += max; + minValue = prices[i]; + max = 0; + } + } + return sumMax; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/find-minimum-in-rotated-sorted-array.java b/week04/find-minimum-in-rotated-sorted-array.java new file mode 100644 index 00000000..a4d60f7a --- /dev/null +++ b/week04/find-minimum-in-rotated-sorted-array.java @@ -0,0 +1,92 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//假设按照升序排序的数组在预先未知的某个点上进行了旋转。例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] 。 +// +// 请找出其中最小的元素。 +// +// +// +// 示例 1: +// +// +//输入:nums = [3,4,5,1,2] +//输出:1 +// +// +// 示例 2: +// +// +//输入:nums = [4,5,6,7,0,1,2] +//输出:0 +// +// +// 示例 3: +// +// +//输入:nums = [1] +//输出:1 +// +// +// +// +// 提示: +// +// +// 1 <= nums.length <= 5000 +// -5000 <= nums[i] <= 5000 +// nums 中的所有整数都是 唯一 的 +// nums 原来是一个升序排序的数组,但在预先未知的某个点上进行了旋转 +// +// Related Topics 数组 二分查找 +// 👍 295 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class find-minimum-in-rotated-sorted-array { + // 题目链接:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/ + public int findMin(int[] nums) { + /** + * 方法一:二分法 + * + * 时间复杂度:O(logN), N即为数组元素的个数 + * 空间复杂度:O(1) + */ + + if (nums.length == 1) { + return nums[0]; + } + + // 定义左边界,有边界,初始条件下当然就为0,和nums.length-1,也就是数组的初始位置和终止位置 + int left = 0, right = nums.length - 1, len = right; + + // 筛选出数组本身有序的情况 + if (nums[right] > nums[left]) { + // 数组本身有序的情况下,第一个元素即为数组的最小元素 + return nums[left]; + } + + // 当左边界小于有边界时继续循环(这就是二分模板记住即可) + while (left <= right) { + // 算出中间节点的下标,这里用变成left + (right - left) / 2是为了防止left+right超出范围 + // 用移位操作是因为计算机对于位运算更友好,更快 + int mid = left + ((right - left) >> 1); + + // 这就是边界点, 边界点就是数组的值不随下标的增大而增大的那两个点 + if (nums[mid] > nums[mid + 1]) { + return nums[mid + 1]; + } + if (nums[mid] < nums[mid - 1]) { + return nums[mid]; + } + + // 如果左边是有序的 + if (nums[mid] > nums[0]) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return -1; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/jump-game-ii.java b/week04/jump-game-ii.java new file mode 100644 index 00000000..c5773703 --- /dev/null +++ b/week04/jump-game-ii.java @@ -0,0 +1,91 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个非负整数数组,你最初位于数组的第一个位置。 +// +// 数组中的每个元素代表你在该位置可以跳跃的最大长度。 +// +// 你的目标是使用最少的跳跃次数到达数组的最后一个位置。 +// +// 示例: +// +// 输入: [2,3,1,1,4] +//输出: 2 +//解释: 跳到最后一个位置的最小跳跃数是 2。 +//  从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 +// +// +// 说明: +// +// 假设你总是可以到达数组的最后一个位置。 +// Related Topics 贪心算法 数组 +// 👍 749 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class jump-game-ii { + // 题目链接:https://leetcode-cn.com/problems/jump-game-ii/ + public int jump(int[] nums) { + /** + * 方法一:从后往前 + * + * 思路:最后一个endpoints,我们往前找最远的跳一步能够到他这里的点,以这个点的下标作为新的endpoints,以此类推 + * 时间复杂度:O(N^2), 两层循环 + * 空间复杂度:O(1), 没有用到额外空间 + */ + + // 第一个endpoints就是数组最后一个元素 + int endpoints = nums.length - 1; + // 用一个变量表示步数 + int steps = 0; + + // 当endpoints走到下标为0的位置时,结束循环了 + while (endpoints > 0) { + // 遍历的目的是为了找到离endpoints最远的点的下标 + for (int i = 0; i < nums.length; i++) { + // 如果当前元素的值加上当前的下标就能够到达endpoints + if (nums[i] + i >= endpoints) { + // 那么更新endpoints + endpoints = i; + // 更新步数 + ++steps; + // 结束此次循环 + break; + } + } + } + + return steps; + + + /** + * 方法二:从前往后 + * + * 思路:既然要贪心,那么当我在当前位置每个可跳的位置里面选一个位置,我怎么选呢,我选择我跳过去之后,他能够跳更远的的那个点。也就是nums[i]最大的那个点 + * 时间复杂度:O(N),一次遍历数组即可 + * 空间复杂度:O(1),并没有用到额外的数组空间 + */ + + // 用一个变量表示步数 + int steps = 0; + // 当前的边界 + int endpoints = 0; + // 元素所能够访问的最大位置 + int maxPosition = 0; + + // 这儿为什么不访问最后一个元素? + // 在遍历数组时,我们不访问最后一个元素,这是因为在访问最后一个元素之前,我们的边界一定大于等于最后一个位置,否则就无法跳到最后一个位置了。(题目明确说明总是可以跳到最后一个位置) + for (int i = 0; i < nums.length - 1; i++) { + // 更新元素所能够访问的最大位置 + maxPosition = Math.max(maxPosition, nums[i] + i); + + // 如果到达了一个边界点,那么我们就要更新这个边界点了(也就是要往下继续跳了),并且步数加一 + if (i == endpoints) { + endpoints = maxPosition; + // 跳跃的步数加一 + ++steps; + } + } + + return steps; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/jump-game.java b/week04/jump-game.java new file mode 100644 index 00000000..b2379ab2 --- /dev/null +++ b/week04/jump-game.java @@ -0,0 +1,53 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个非负整数数组,你最初位于数组的第一个位置。 +// +// 数组中的每个元素代表你在该位置可以跳跃的最大长度。 +// +// 判断你是否能够到达最后一个位置。 +// +// 示例 1: +// +// 输入: [2,3,1,1,4] +//输出: true +//解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 +// +// +// 示例 2: +// +// 输入: [3,2,1,0,4] +//输出: false +//解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。 +// +// Related Topics 贪心算法 数组 +// 👍 919 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class jump-game { + // 题目链接:https://leetcode-cn.com/problems/jump-game/ + public boolean canJump(int[] nums) { + /** + * 方法一:贪心算法 + * + * 时间复杂度:O(N),数组遍历一遍即可 + * 空间复杂度:O(1),并没有使用额外空间 + */ + + // 定义当前需要跳到的元素下标,初始当然就是最后一个元素的下标 + int endpoints = nums.length - 1; + // 循环遍历,从后往前 + for (int i = nums.length - 1; i >= 0; i--) { + // 如果当前下标所在元素的值+下标大于等于目标下标,证明只要前面的元素能够跳到当前位置,那么就一定能够跳到最后位置 + if (nums[i] + i >= endpoints) { + // 所以将endpoint更新为i + endpoints = i; + } + } + // 假设我从后往前一共有三个endpoints + // 如果最后endpoints为0,证明第三个也就是最后一个endp可以由第二个跳过去,第二个endp可以由第一个跳过去, + // 那第一个就能跳到最后一个endp。第一个endp一定要为0,也就是起始位置 + // 反之,则不行 + return endpoints == 0; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/lemonade-change.java b/week04/lemonade-change.java new file mode 100644 index 00000000..6be03aa4 --- /dev/null +++ b/week04/lemonade-change.java @@ -0,0 +1,112 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//在柠檬水摊上,每一杯柠檬水的售价为 5 美元。 +// +// 顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。 +// +// 每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。 +// +// 注意,一开始你手头没有任何零钱。 +// +// 如果你能给每位顾客正确找零,返回 true ,否则返回 false 。 +// +// 示例 1: +// +// 输入:[5,5,5,10,20] +//输出:true +//解释: +//前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 +//第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 +//第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 +//由于所有客户都得到了正确的找零,所以我们输出 true。 +// +// +// 示例 2: +// +// 输入:[5,5,10] +//输出:true +// +// +// 示例 3: +// +// 输入:[10,10] +//输出:false +// +// +// 示例 4: +// +// 输入:[5,5,10,10,20] +//输出:false +//解释: +//前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 +//对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 +//对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 +//由于不是每位顾客都得到了正确的找零,所以答案是 false。 +// +// +// +// +// 提示: +// +// +// 0 <= bills.length <= 10000 +// bills[i] 不是 5 就是 10 或是 20 +// +// Related Topics 贪心算法 +// 👍 135 👎 0 + + +import java.util.PriorityQueue; + +//leetcode submit region begin(Prohibit modification and deletion) +class lemonade-change { + // 题目链接:https://leetcode-cn.com/problems/lemonade-change/description/ + public boolean lemonadeChange(int[] bills) { + /** + * 方法一:贪心算法 + * + * 时间复杂度:O(N), 只有一层for循环。 + * 空间复杂度:O(1), 没有用到额外空间。 + */ + + /** + * 为什么这个题目而言是可以用贪心的? + * 当您收到5或10的账单时,您只有一个选项:要么不返回(当收到5)要么返回5(当收到10)的账单。 + * 但是当你收到20的账单时,你有两个选择:退还一个5元账单和一个10元账单,或者只返回3个 5元账单。 + * 正确的想法是前者更好,但为什么呢? 答案是10元账单只有在你收到20元账单时才能在退还时使用, + * 但是当你收到20张账单和10张账单时,你都可以退还5元账单。所以说, 在某种程度上, 5元账单比15元账单更“珍贵” 。 + */ + + // 用five记录当前我们拥有的5元的数量,ten记录我们当前拥有的10元的数量 + int five = 0, ten = 0; + + // 遍历即可 + for (int bill : bills) { + + // 如果当前客户给我们的是5元面值的钱,那么我们不用找零,将拥有的5元的钱的数量加一即可 + // 如果当前客户给我们的是10元面值的钱,那么我们要找他5元,所以如果我们拥有的5元的钱的数量为0的,结果就为false。 + // 如果当前客户给我们的是20元面值的钱,那么我们有两种方案,第一,找给他一张10元,一张5元。第二,找给他三张5元。如果都没有直接返回false + if (bill == 5) { + five++; + } else if (bill == 10) { + if (five == 0) { + return false; + } + five--; + ten++; + } else { + if (five != 0 && ten != 0) { + five--; + ten--; + } else if (five >= 3) { + five -= 3; + } else { + return false; + } + } + } + + // 遍历完之后还没有问题,返回true即可 + return true; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/minesweeper.java b/week04/minesweeper.java new file mode 100644 index 00000000..71e2a99c --- /dev/null +++ b/week04/minesweeper.java @@ -0,0 +1,249 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//让我们一起来玩扫雷游戏! +// +// 给定一个代表游戏板的二维字符矩阵。 'M' 代表一个未挖出的地雷,'E' 代表一个未挖出的空方块,'B' 代表没有相邻(上,下,左,右,和所有4个对角线) +//地雷的已挖出的空白方块,数字('1' 到 '8')表示有多少地雷与这块已挖出的方块相邻,'X' 则表示一个已挖出的地雷。 +// +// 现在给出在所有未挖出的方块中('M'或者'E')的下一个点击位置(行和列索引),根据以下规则,返回相应位置被点击后对应的面板: +// +// +// 如果一个地雷('M')被挖出,游戏就结束了- 把它改为 'X'。 +// 如果一个没有相邻地雷的空方块('E')被挖出,修改它为('B'),并且所有和其相邻的未挖出方块都应该被递归地揭露。 +// 如果一个至少与一个地雷相邻的空方块('E')被挖出,修改它为数字('1'到'8'),表示相邻地雷的数量。 +// 如果在此次点击中,若无更多方块可被揭露,则返回面板。 +// +// +// +// +// 示例 1: +// +// 输入: +// +//[['E', 'E', 'E', 'E', 'E'], +// ['E', 'E', 'M', 'E', 'E'], +// ['E', 'E', 'E', 'E', 'E'], +// ['E', 'E', 'E', 'E', 'E']] +// +//Click : [3,0] +// +//输出: +// +//[['B', '1', 'E', '1', 'B'], +// ['B', '1', 'M', '1', 'B'], +// ['B', '1', '1', '1', 'B'], +// ['B', 'B', 'B', 'B', 'B']] +// +//解释: +// +// +// +// 示例 2: +// +// 输入: +// +//[['B', '1', 'E', '1', 'B'], +// ['B', '1', 'M', '1', 'B'], +// ['B', '1', '1', '1', 'B'], +// ['B', 'B', 'B', 'B', 'B']] +// +//Click : [1,2] +// +//输出: +// +//[['B', '1', 'E', '1', 'B'], +// ['B', '1', 'X', '1', 'B'], +// ['B', '1', '1', '1', 'B'], +// ['B', 'B', 'B', 'B', 'B']] +// +//解释: +// +// +// +// +// +// 注意: +// +// +// 输入矩阵的宽和高的范围为 [1,50]。 +// 点击的位置只能是未被挖出的方块 ('M' 或者 'E'),这也意味着面板至少包含一个可点击的方块。 +// 输入面板不会是游戏结束的状态(即有地雷已被挖出)。 +// 简单起见,未提及的规则在这个问题中可被忽略。例如,当游戏结束时你不需要挖出所有地雷,考虑所有你可能赢得游戏或标记方块的情况。 +// +// Related Topics 深度优先搜索 广度优先搜索 +// 👍 194 👎 0 + + +import java.util.LinkedList; +import java.util.Queue; + +//leetcode submit region begin(Prohibit modification and deletion) +class minesweeper { + // 题目链接:https://leetcode-cn.com/problems/minesweeper/description/ + + // 因为题目涉及到8个方向,所以分别定义长度为8的x,y坐标数组,表示8个方向(上下左右,左上,右上,左下,右下) + int[] directX = {0, 0, -1, 1, -1, 1, -1, 1}; + int[] directY = {1, -1, 0, 0, -1, -1, 1, 1}; + + public char[][] updateBoard(char[][] board, int[] click) { + /** + * 方法一:深度优先遍历 + * + * 时间复杂度:O(MN), M,N为面板的长和宽。因为每个面板数据只有可能被访问一次 + * 空间复杂度:O(MN), M,N为面板的长和宽。递归的调用深度最大程度就是M * N + */ + + // 拿出点击序列 + int row = click[0], col = click[1]; + + // 如果当前点击的是雷,那么把它改成X,游戏结束 + if (board[row][col] == 'M') { + // 规则1 + board[row][col] = 'X'; + } else { + dfs(row, col, board); + } + + return board; + + + /** + * 方法二:广度优先搜索 + * + * 时间复杂度:O(MN), M,N为面板的长和宽。因为每个面板数据只有可能被访问一次 + * 空间复杂度:O(MN), M,N为面板的长和宽。递归的调用深度最大程度就是M * N + */ + + // 拿出点击序列 + int row = click[0], col = click[1]; + // 如果当前点击的是雷,那么把它改成X,游戏结束 + if (board[row][col] == 'M') { + // 规则 1 + board[row][col] = 'X'; + } else{ + bfs(row, col, board); + } + return board; + } + + + /** + * 深度优先搜索 + * + * @param row 当前搜索的行 + * @param col 当前搜索的列 + * @param board 当前的搜索面板 + */ + public void dfs(int row, int col, char[][] board) { + // 定义地雷的数量 + int countLandmine = 0; + // 8 个方向,遍历8次寻找 + for (int i = 0; i < 8; i++) { + // 得到新的坐标 + int nrow = row + directY[i]; + int ncol = col + directX[i]; + + // 如果新坐标不在我们的面板范围内,此次循环跳过 + if (nrow < 0 || nrow >= board.length || ncol < 0 || ncol >= board[0].length) { + continue; + } + + // 如果新的行新的列所在面板位置是雷,那么将雷的计数器加一 + if (board[nrow][ncol] == 'M') { + countLandmine++; + } + } + + // 规则3 + // 如果雷计数器大于0,证明与当前空方块(一定是空方块,否则不可能进入深度递归中)周围的确有雷,更新面板为当前空方块周围的雷的数量 + if (countLandmine > 0) { + board[row][col] = (char) (countLandmine + '0'); + } else { // 否则,证明当前空方块附近没有雷,我们需要将面板上对应位置改为B,然后递归的揭露和其相邻的未挖出的方块 + // 规则2 + board[row][col] = 'B'; + // 利用8个方向去遍历 + for (int i = 0; i < 8; i++) { + // 得到新的坐标 + int nrow = row + directY[i]; + int ncol = col + directX[i]; + + // 首先如果行数列数任何一个不符合面板要求,此次循环直接continue即可。 + // 其次,如果得到的新坐标所在面板数据不为E,我们也跳过,因为规则2说的是所有和其相邻的未挖出方块被递归揭露。那么未挖出包含M和E,这里为什么只判断E呢,因为M在前面已经被判断并且计算过了 + // 也就是说,有M,程序走不到这里来,实际上这里的规则3所要表达的就是E这样的未挖出空方块 + if (nrow < 0 || nrow >= board.length || ncol < 0 || ncol >= board[0].length || board[nrow][ncol] != 'E') { + continue; + } + + // 符合要求的坐标,我们就递归揭露 + dfs(nrow, ncol, board); + } + } + } + + + /** + * 广度优先搜索 + * @param row 当前搜索的行 + * @param col 当前搜索的列 + * @param board 当前的搜索面板 + */ + public void bfs(int row, int col, char[][] board) { + // 广度优先搜索需要用队列进行模拟 + Queue queue = new LinkedList(); + // 用布尔数组记录哪些元素是被访问过的 + boolean[][] vis = new boolean[board.length][board[0].length]; + // 将点击坐标放到队列中 + queue.offer(new int[]{row, col}); + vis[row][col] = true; + // 当队列不为空时循环 + while (!queue.isEmpty()) { + // 出队 + int[] pos = queue.poll(); + // 定义地雷的数量 + int countLandmine = 0; + int x = pos[0], y = pos[1]; + + // 8 个方向,遍历8次寻找 + for (int i = 0; i < 8; ++i) { + // 得到新坐标 + int nx = x + directY[i]; + int ny = y + directX[i]; + + // 如果新坐标不在我们的面板范围内,此次循环跳过 + if (nx < 0 || nx >= board.length || ny < 0 || ny >= board[0].length) { + continue; + } + // 如果新的行新的列所在面板位置是雷,那么将雷的计数器加一 + if (board[nx][ny] == 'M') { + ++countLandmine; + } + } + + // 规则3 + // 如果雷计数器大于0,证明与当前空方块(一定是空方块,否则不可能进入广度搜索中)周围的确有雷,更新面板为当前空方块周围的雷的数量 + if (countLandmine > 0) { + // 规则 3 + board[x][y] = (char) (countLandmine + '0'); + } else { // 否则,证明当前空方块附近没有雷,我们需要将面板上对应位置改为B,然后将符合面板的坐标加入队列中,揭露和其相邻的未挖出的方块 + // 规则 2 + board[x][y] = 'B'; + for (int i = 0; i < 8; ++i) { + int nx = x + directY[i]; + int ny = y + directX[i]; + + // 这里不需要在存在 B 的时候继续扩展,因为 B 之前被点击的时候已经被扩展过了 + // 首先如果行数列数任何一个不符合面板要求,此次循环直接continue即可。 + // 其次,如果得到的新坐标所在面板数据不为E,我们也跳过,因为规则2说的是所有和其相邻的未挖出方块被递归揭露。那么未挖出包含M和E,这里为什么只判断E呢,因为M在前面已经被判断并且计算过了 + // 也就是说,有M,程序走不到这里来,实际上这里的规则3所要表达的就是E这样的未挖出空方块 + if (nx < 0 || nx >= board.length || ny < 0 || ny >= board[0].length || board[nx][ny] != 'E' || vis[nx][ny]) { + continue; + } + + // 符合面板要求和题目要求的坐标加入到队列中 + queue.offer(new int[]{nx, ny}); + vis[nx][ny] = true; + } + } + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/number-of-islands.java b/week04/number-of-islands.java new file mode 100644 index 00000000..234811e4 --- /dev/null +++ b/week04/number-of-islands.java @@ -0,0 +1,106 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 +// +// 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 +// +// 此外,你可以假设该网格的四条边均被水包围。 +// +// +// +// 示例 1: +// +// +//输入:grid = [ +// ["1","1","1","1","0"], +// ["1","1","0","1","0"], +// ["1","1","0","0","0"], +// ["0","0","0","0","0"] +//] +//输出:1 +// +// +// 示例 2: +// +// +//输入:grid = [ +// ["1","1","0","0","0"], +// ["1","1","0","0","0"], +// ["0","0","1","0","0"], +// ["0","0","0","1","1"] +//] +//输出:3 +// +// +// +// +// 提示: +// +// +// m == grid.length +// n == grid[i].length +// 1 <= m, n <= 300 +// grid[i][j] 的值为 '0' 或 '1' +// +// Related Topics 深度优先搜索 广度优先搜索 并查集 +// 👍 852 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class number-of-islands { + // 题目链接:https://leetcode-cn.com/problems/number-of-islands/ + + // 定义返回结果 + int res = 0; + public int numIslands(char[][] grid) { + /** + * 方法一:递归 + * + * 时间复杂度:O(MN),也就是网格的数量 + * 空间复杂度:O(MN),递归遍历的深度也就是网格数量 + */ + + // 判断边界条件 + if (grid == null || grid.length == 0) { + return res; + } + + // 用m, n记录二维数组的行数和列数,便于后面的遍历 + int m = grid.length; + int n = grid[0].length; + + // 遍历二维数组 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == '1') { + ++res; + // 进行递归遍历将整个岛屿从1变为0,以免重复的发现相同的岛屿 + dfs(i, j, grid); + } + } + } + + return res; + } + + public void dfs(int i, int j, char[][] grid) { + // dfs里面也需要拿到二维数组的行数和列数用以判断递归出口 + int m = grid.length; + int n = grid[0].length; + // 递归出口,如果行数比0小;行数大于等于数组行数;列数比0小,列数大于等于数组列数;或者当前位置的字符是0,直接return + if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') { + return; + } + + // 程序走到这里证明grid[i][j]一定是‘1’,所以我们先将这个值改成0,然后再进行递归 + grid[i][j] = '0'; + // 递归遍历他的上面 + dfs(i, j - 1, grid); + // 递归遍历他的下面 + dfs(i, j + 1, grid); + // 递归遍历他的左边 + dfs(i - 1, j, grid); + // 递归遍历他的右边 + dfs(i + 1, j, grid); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/search-a-2d-matrix.java b/week04/search-a-2d-matrix.java new file mode 100644 index 00000000..d18c302b --- /dev/null +++ b/week04/search-a-2d-matrix.java @@ -0,0 +1,95 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性: +// +// +// 每行中的整数从左到右按升序排列。 +// 每行的第一个整数大于前一行的最后一个整数。 +// +// +// +// +// 示例 1: +// +// +//输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 3 +//输出:true +// +// +// 示例 2: +// +// +//输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,50]], target = 13 +//输出:false +// +// +// 示例 3: +// +// +//输入:matrix = [], target = 0 +//输出:false +// +// +// +// +// 提示: +// +// +// m == matrix.length +// n == matrix[i].length +// 0 <= m, n <= 100 +// -104 <= matrix[i][j], target <= 104 +// +// Related Topics 数组 二分查找 +// 👍 274 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class search-a-2d-matrix { + // 题目链接:https://leetcode-cn.com/problems/search-a-2d-matrix/ + public boolean searchMatrix(int[][] matrix, int target) { + /** + * 方法一:二分法 + * + * 时间复杂度:O(log(M*N)), M是行数,N是列数 + * 空间复杂度:O(1), 没有用到额外空间 + */ + + // 边界条件判断 + if (matrix.length == 0) { + return false; + } + + // 得到二维数组的行数 + int m = matrix.length; + // 得到二维数组的列数 + int n = matrix[0].length; + + // 定义左边界,有边界, 将整个二维数组拉长,看成一个一维数组,这个问题就非常好解决了。 + // 拉长之后,初始的left就是0,right就是 行数 * 列数 - 1 + int left = 0, right = m * n - 1; + + while (left <= right) { + // 算出中间节点的下标,这里用变成left + (right - left) / 2是为了防止left+right超出范围 + // 用移位操作是因为计算机对于位运算更友好,更快 + int mid = left + ((right - left) >> 1); + // 算出mid在二维数组中对应的行号和列号 + int row = mid / n; + int col = mid % n; + + // 如果当前对应的元素正好等于target,直接返回true + // 如果大于target,证明我们要到左边进行寻找,right = mid - 1 + // 如果小于target,证明我们要到右边进行寻找,left = mid + 1 + if (matrix[row][col] == target) { + return true; + } else if (matrix[row][col] > target) { + right = mid - 1; + } else { + left = mid + 1; + } + } + + // 循环结束还没有返回,那就是没找到 + return false; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/search-in-rotated-sorted-array.java b/week04/search-in-rotated-sorted-array.java new file mode 100644 index 00000000..a78318b5 --- /dev/null +++ b/week04/search-in-rotated-sorted-array.java @@ -0,0 +1,107 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给你一个整数数组 nums ,和一个整数 target 。 +// +// 该整数数组原本是按升序排列,但输入时在预先未知的某个点上进行了旋转。(例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] +// )。 +// +// 请你在数组中搜索 target ,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。 +// +// +// 示例 1: +// +// +//输入:nums = [4,5,6,7,0,1,2], target = 0 +//输出:4 +// +// +// 示例 2: +// +// +//输入:nums = [4,5,6,7,0,1,2], target = 3 +//输出:-1 +// +// 示例 3: +// +// +//输入:nums = [1], target = 0 +//输出:-1 +// +// +// +// +// 提示: +// +// +// 1 <= nums.length <= 5000 +// -10^4 <= nums[i] <= 10^4 +// nums 中的每个值都 独一无二 +// nums 肯定会在某个点上旋转 +// -10^4 <= target <= 10^4 +// +// Related Topics 数组 二分查找 +// 👍 1062 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class search-in-rotated-sorted-array { + // 题目链接:https://leetcode-cn.com/problems/search-in-rotated-sorted-array/ + + public int search(int[] nums, int target) { + /** + * 方法一:二分法 + * + * 时间复杂度:O(logN) + * 空间复杂度:O(1),没有用到额外空间 + */ + + // 得到数组的长度 + int len = nums.length; + // 特殊条件判断 + if (len == 0) { + return -1; + } + + // 特殊条件判断 + if (len == 1) { + return nums[0] == target ? 0 : -1; + } + + // 定义左边界,有边界,初始条件下当然就为0,和nums.length-1,也就是数组的初始位置和终止位置 + int left = 0, right = len - 1; + + // 当左边界小于有边界时继续循环(这就是二分模板记住即可) + while (left <= right) { + // 算出中间节点的下标,这里用变成left + (right - left) / 2是为了防止left+right超出范围 + // 用移位操作是因为计算机对于位运算更友好,更快 + int mid = left + ((right - left) >> 1); + // 如果中间元素刚好等于目标值,直接返回下标mid + if (nums[mid] == target) { + return mid; + } + + // 否则,我们要做其他判断,由于这个数组是一个旋转排序数组,所以我们要确定哪边是有序的 + // 如果左边是有序的 + if (nums[0] <= nums[mid]) { + // 并且target正好在nums[0]---nums[mid]之间,那么我们应该在[0, mid - 1)区间去继续寻找。更新right即可 + // 否则,我们就只能到右边的区间[mid + 1, right)去寻找 + if (nums[mid] > target && nums[0] <= target) { + right = mid - 1; + } else { + left = mid + 1; + } + } else { // 如果左边不是有序的,那么右边就是有序的 + // 如果目标值正好在(mid, len - 1]区间上, 那么我们要去右边继续寻找 + // 否则,我们要去左边找 + if (nums[mid] < target && target <= nums[len - 1]) { + left = mid + 1; + } else { + right = mid - 1; + } + } + } + + // 走到这里还没返回,证明就是没找到 + return -1; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/ssign-cookies.java b/week04/ssign-cookies.java new file mode 100644 index 00000000..d0431bfb --- /dev/null +++ b/week04/ssign-cookies.java @@ -0,0 +1,82 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 +// +// 对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i +//],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 +// +// +// 示例 1: +// +// +//输入: g = [1,2,3], s = [1,1] +//输出: 1 +//解释: +//你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。 +//虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。 +//所以你应该输出1。 +// +// +// 示例 2: +// +// +//输入: g = [1,2], s = [1,2,3] +//输出: 2 +//解释: +//你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 +//你拥有的饼干数量和尺寸都足以让所有孩子满足。 +//所以你应该输出2. +// +// +// +// +// 提示: +// +// +// 1 <= g.length <= 3 * 104 +// 0 <= s.length <= 3 * 104 +// 1 <= g[i], s[j] <= 231 - 1 +// +// Related Topics 贪心算法 +// 👍 216 👎 0 + + +import java.util.Arrays; + +//leetcode submit region begin(Prohibit modification and deletion) +class ssign-cookies { + // 题目链接:https://leetcode-cn.com/problems/assign-cookies/description/ + public int findContentChildren(int[] g, int[] s) { + /** + * 方法一:先排序加贪心 + * + * 时间复杂度:O(glogG + slogS),复杂度还是在O(NlogN)这个级别 + * 空间复杂度:O(logG + logS),主要是排序产生的复杂度。还是在O(logN)这个级别。 + */ + + // 先对两个数组进行排序 + Arrays.sort(g); + Arrays.sort(s); + // 定义结果变量 + int result = 0; + + // 拿到两个数组的最后一个元素的下标,因为我们是贪心,所以从后往前,只要当前饼干能够满足,就将result+1 + int gIndex = g.length - 1; + int sIndex = s.length - 1; + + while (gIndex >= 0 && sIndex >= 0) { + // 如果s[i] 比 g[gLen]大,证明这块饼干能够满足当前胃口最大的孩子(因为已经排好序了) + if (s[sIndex] >= g[gIndex]) { + // 直接将result + 1, 并且glen -- + result += 1; + sIndex --; + } + // 无论当前的饼干能不能满足当前小孩,小孩的下标都要往前移动一位 + // 能满足,往前移动很好理解 + // 不能满足,证明当前最大块的饼干都不能满足你,那我剩下的所有饼干也都不能满足你,那我就不考虑你了,去找其他能够满足的。 + gIndex --; + } + + return result; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/walking-robot-simulation.java b/week04/walking-robot-simulation.java new file mode 100644 index 00000000..8d1bf3b5 --- /dev/null +++ b/week04/walking-robot-simulation.java @@ -0,0 +1,110 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//机器人在一个无限大小的网格上行走,从点 (0, 0) 处开始出发,面向北方。该机器人可以接收以下三种类型的命令: +// +// +// -2:向左转 90 度 +// -1:向右转 90 度 +// 1 <= x <= 9:向前移动 x 个单位长度 +// +// +// 在网格上有一些格子被视为障碍物。 +// +// 第 i 个障碍物位于网格点 (obstacles[i][0], obstacles[i][1]) +// +// 机器人无法走到障碍物上,它将会停留在障碍物的前一个网格方块上,但仍然可以继续该路线的其余部分。 +// +// 返回从原点到机器人所有经过的路径点(坐标为整数)的最大欧式距离的平方。 +// +// +// +// 示例 1: +// +// 输入: commands = [4,-1,3], obstacles = [] +//输出: 25 +//解释: 机器人将会到达 (3, 4) +// +// +// 示例 2: +// +// 输入: commands = [4,-1,4,-2,4], obstacles = [[2,4]] +//输出: 65 +//解释: 机器人在左转走到 (1, 8) 之前将被困在 (1, 4) 处 +// +// +// +// +// 提示: +// +// +// 0 <= commands.length <= 10000 +// 0 <= obstacles.length <= 10000 +// -30000 <= obstacle[i][0] <= 30000 +// -30000 <= obstacle[i][1] <= 30000 +// 答案保证小于 2 ^ 31 +// +// Related Topics 贪心算法 +// 👍 112 👎 0 + + +import java.util.HashSet; +import java.util.Set; + +//leetcode submit region begin(Prohibit modification and deletion) +class walking-robot-simulation { + // 题目链接:https://leetcode-cn.com/problems/walking-robot-simulation/ + public int robotSim(int[] commands, int[][] obstacles) { + /** + * 暴力法 + * + * 时间复杂度:O(N+K),N是commands的长度,K是obstacles的长度 + * 空间复杂度:O(K), 用以存储障碍物所花掉的set的空间 + */ + + // 用两个数组来模拟方向所以初始值如下(题目要求朝着北面) + // 初始状态directX和directY中的第一个元素拿出来作为方向(0,1),因为朝北,所以X移动为零,Y的移动为1(每一次不管走多少步,都是一步一步走) + int[] directX = {0, 1, 0, -1}; + int[] directY = {1, 0, -1, 0}; + // di就是用于知道当前的方向的,向右转90度就是di + 1,向左转90度就是(di + 3) % 4 + int di = 0; + // 定义最大值 + int maxRes = 0; + + // 用HashSet来存储障碍物,否则只能强行暴力去遍历很浪费时间 + Set set = new HashSet(); + // 将障碍物一个一个加入到hashset中 + for (int[] obstacle : obstacles) { + long ox = (long) obstacle[0] + 30000; + long oy = (long) obstacle[1] + 30000; + set.add((ox << 16) + oy); + } + // 定义机器人的坐标,初始值为(0,0) + int x = 0, y = 0; + for (int command : commands) { + // 向右转 + if (command == -1) { + // di + 1之后对应的diretX和directY就是新的方向 + di = (di + 1) % 4; + } else if (command == -2) { + di = (di + 3) % 4; + } else { + // 每次走一步 + for (int i = 0; i < command; i++) { + int newx = x + directX[di]; + int newy = y + directY[di]; + + // 看看会不会遇到障碍物,如果不会,那么更新最大值;否则,直接break即可。因为她已经被障碍物挡着了,后面的操作也不用再做了。 + if (! set.contains((((long) newx + 30000) << 16) + ((long) newy + 30000))) { + x = newx; + y = newy; + maxRes = Math.max(maxRes, x * x + y * y); + } else { + break; + } + } + } + } + + return maxRes; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/word-ladder-ii.java b/week04/word-ladder-ii.java new file mode 100644 index 00000000..50859c5b --- /dev/null +++ b/week04/word-ladder-ii.java @@ -0,0 +1,159 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定两个单词(beginWord 和 endWord)和一个字典 wordList,找出所有从 beginWord 到 endWord 的最短转换序列。转换 +//需遵循如下规则: +// +// +// 每次转换只能改变一个字母。 +// 转换后得到的单词必须是字典中的单词。 +// +// +// 说明: +// +// +// 如果不存在这样的转换序列,返回一个空列表。 +// 所有单词具有相同的长度。 +// 所有单词只由小写字母组成。 +// 字典中不存在重复的单词。 +// 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。 +// +// +// 示例 1: +// +// 输入: +//beginWord = "hit", +//endWord = "cog", +//wordList = ["hot","dot","dog","lot","log","cog"] +// +//输出: +//[ +// ["hit","hot","dot","dog","cog"], +//  ["hit","hot","lot","log","cog"] +//] +// +// +// 示例 2: +// +// 输入: +//beginWord = "hit" +//endWord = "cog" +//wordList = ["hot","dot","dog","lot","log"] +// +//输出: [] +// +//解释: endWord "cog" 不在字典中,所以不存在符合要求的转换序列。 +// Related Topics 广度优先搜索 数组 字符串 回溯算法 +// 👍 359 👎 0 + + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class word-ladder-ii { + // 题目链接:https://leetcode-cn.com/problems/word-ladder-ii/ + public List> findLadders(String beginWord, String endWord, List wordList) { + /** + * 方法一:广度优先搜索 + 回溯dfs + * + * 时间复杂度:这个复杂度应该是指数级的复杂度了 + * 空间复杂度:O(N * ways) + */ + + // 先将 wordList 放到哈希表里,便于判断某个单词是否在 wordList 里 + Set wordSet = new HashSet<>(wordList); + // 定义结果集 + List> res = new ArrayList<>(); + // 边界条件判断 + if (wordSet.size() == 0 || !wordSet.contains(endWord)) { + return res; + } + + // 第 1 步:使用广度优先遍历得到后继结点列表 successors + // key:字符串,value:广度优先遍历过程中 key 的后继结点列表,也就是每遍历到的一个节点,都有其后继节点列表 + Map> successors = new HashMap<>(); + boolean found = bfs(beginWord, endWord, wordSet, successors); + if (!found) { + return res; + } + + // 第 2 步:基于后继结点列表 successors ,使用回溯算法得到所有最短路径列表 + Deque path = new ArrayDeque<>(); + path.addLast(beginWord); + dfs(beginWord, endWord, successors, path, res); + return res; + } + + private boolean bfs(String beginWord, String endWord, Set wordSet, Map> successors) { + Queue queue = new LinkedList<>(); + queue.offer(beginWord); + + // 标准写法,记录方法问过的单词 + Set visited = new HashSet<>(); + visited.add(beginWord); + + boolean found = false; + int wordLen = beginWord.length(); + // 当前层访问过的结点,当前层全部遍历完成以后,再添加到总的 visited 集合里 + Set nextLevelVisited = new HashSet<>(); + while (!queue.isEmpty()) { + int currentSize = queue.size(); + for (int i = 0; i < currentSize; i++) { + String currentWord = queue.poll(); + char[] charArray = currentWord.toCharArray(); + for (int j = 0; j < wordLen; j++) { + char originChar = charArray[j]; + for (char k = 'a'; k <= 'z'; k++) { + if (charArray[j] == k) { + continue; + } + charArray[j] = k; + String nextWord = new String(charArray); + if (wordSet.contains(nextWord)) { + if (!visited.contains(nextWord)) { + if (nextWord.equals(endWord)) { + found = true; + } + + // 避免下层元素重复加入队列,这里感谢 https://leetcode-cn.com/u/zhao-da-ming/ 优化了这个逻辑 + if (!nextLevelVisited.contains(nextWord)) { + queue.offer(nextWord); + nextLevelVisited.add(nextWord); + } + + // 维护 successors 的定义 + successors.computeIfAbsent(currentWord, a -> new HashSet<>()); + successors.get(currentWord).add(nextWord); + } + } + } + charArray[j] = originChar; + } + } + + if (found) { + break; + } + visited.addAll(nextLevelVisited); + nextLevelVisited.clear(); + } + return found; + } + + private void dfs(String beginWord, String endWord, Map> successors, Deque path, List> res) { + if (beginWord.equals(endWord)) { + res.add(new ArrayList<>(path)); + return; + } + + if (!successors.containsKey(beginWord)) { + return; + } + + Set successorWords = successors.get(beginWord); + for (String nextWord : successorWords) { + path.addLast(nextWord); + dfs(nextWord, endWord, successors, path, res); + path.removeLast(); + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week04/word-ladder.java b/week04/word-ladder.java new file mode 100644 index 00000000..c863c9fc --- /dev/null +++ b/week04/word-ladder.java @@ -0,0 +1,258 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则: +// +// +// +// 每次转换只能改变一个字母。 +// 转换过程中的中间单词必须是字典中的单词。 +// +// +// 说明: +// +// +// 如果不存在这样的转换序列,返回 0。 +// 所有单词具有相同的长度。 +// 所有单词只由小写字母组成。 +// 字典中不存在重复的单词。 +// 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。 +// +// +// 示例 1: +// +// 输入: +//beginWord = "hit", +//endWord = "cog", +//wordList = ["hot","dot","dog","lot","log","cog"] +// +//输出: 5 +// +//解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", +// 返回它的长度 5。 +// +// +// 示例 2: +// +// 输入: +//beginWord = "hit" +//endWord = "cog" +//wordList = ["hot","dot","dog","lot","log"] +// +//输出: 0 +// +//解释: endWord "cog" 不在字典中,所以无法进行转换。 +// Related Topics 广度优先搜索 +// 👍 639 👎 0 + + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; + +//leetcode submit region begin(Prohibit modification and deletion) +class word-ladder { + // 题目链接:https://leetcode-cn.com/problems/word-ladder/ + public int ladderLength(String beginWord, String endWord, List wordList) { + /** + * 方法一:单向广度优先遍历 + * + * 时间复杂度:O(N * C * 26), N代表字典中元素个数,C代表单词的平均长度,26就是26个字母 + * 空间复杂度:O(N), wordSet, queue, visited都用到了额外空间 + */ + + // 首先将单词字典放到hashSet中,方便我们通过O(1)的时间查找 + Set wordSet = new HashSet<>(wordList); + + // 边界条件判断,如果字典大小为0或者字典中不包含endWord,直接返回0 + if (wordSet.size() == 0 || !wordSet.contains(endWord)) { + return 0; + } + + // 图的广度优先遍历必须使用队列和记录是否访问过的visited哈希表 + Queue queue = new LinkedList<>(); + queue.offer(beginWord); + Set visited = new HashSet<>(); + visited.add(beginWord); + + // 初始步数为1,题目要求包含起点 + int steps = 1; + // 开始广度优先遍历 + while (!queue.isEmpty()) { + // 得到当前队列中的元素个数,因为广度优先的时候每一次要将当前层的所有元素出队 + int currentSize = queue.size(); + + for (int i = 0; i < currentSize; i++) { + // 开始访问队列当前层次的元素 + String currentWord = queue.poll(); + + // 如果当前的单词能够通过修改其中某一个字母而得到endWord,则找到了这条路径 + if (findPath(currentWord, endWord, queue, visited, wordSet)) { + // 直接返回steps+1 + return steps + 1; + } + } + + // 当前层没有找到,将steps + 1即可 + ++steps; + } + + // 如果while循环结束还没有得到结果,那么证明无法到达 + return 0; + + + + + /** + * 方法二:双向广度优先遍历 + * + * 时间复杂度:O(N * C * 26), N代表字典中元素个数,C代表单词的平均长度,26就是26个字母 + * 空间复杂度:O(N), wordSet, visited等set都用到了额外空间 + */ + + // 首先将单词字典放到hashSet中,方便我们通过O(1)的时间查找 + Set wordSet = new HashSet<>(wordList); + + // 边界条件判断,如果字典大小为0或者字典中不包含endWord,直接返回0 + if (wordSet.size() == 0 || !wordSet.contains(endWord)) { + return 0; + } + + // 已经访问过的 word 添加到 visited 哈希表里 + Set visited = new HashSet<>(); + // 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用 + Set beginVisited = new HashSet<>(); + beginVisited.add(beginWord); + Set endVisited = new HashSet<>(); + endVisited.add(endWord); + + // 初始步数为1,题目要求包含起点 + int steps = 1; + // 开始双向BFS,左右交替扩散的步数之和为所求。什么时候停止呢,当两个集合有交集的时候即可停止 + while (!beginVisited.isEmpty() && !endVisited.isEmpty()) { + // 优先选择小的哈希表进行扩散,这样更快。哈希表小,代表当前层需要扩散的元素少,那么比起扩散元素多的,也就是哈希表大的,肯定是要快一点 + if (beginVisited.size() > endVisited.size()) { + // 将两个set交换,这样的目的是让我们的处理逻辑一致,我们永远处理的较小的set都是beginVisited + Set temp = beginVisited; + beginVisited = endVisited; + endVisited = temp; + } + + // 程序走到这里,能够保证我们拿到的beginVisited就是较小的集合,扩散完之后,会将扩散后的集合赋值给beginVisited + Set nextLevelVisited = new HashSet<>(); + for (String currentWord : beginVisited) { + // 如果当前的单词能够通过修改其中某一个字母而得到endWord,则找到了这条路径 + if (findPath2(currentWord, endVisited, visited, wordSet, nextLevelVisited)) { + return steps + 1; + } + } + + // 原来的 beginVisited 废弃,从 nextLevelVisited 开始新的双向 BFS + beginVisited = nextLevelVisited; + // 当前双向遍历没有找到,将steps + 1即可 + ++steps; + } + + return 0; + } + + /** + * 当前单词经过替换后能否到达endWord + * + * @param currentWord 当前的单词 + * @param endVisited 目标集合 + * @param visited 用于存储是否访问过某个单词的哈希表 + * @param wordSet 存储单词字典的哈希表 + * @param nextLevelVisited 当前层扩散之后的集合 + * @return + */ + private boolean findPath2(String currentWord, Set endVisited, Set visited, Set wordSet, Set nextLevelVisited) { + // 将字符串转换成字符数组,用于遍历 + char[] currentWordArray = currentWord.toCharArray(); + + // 遍历字符数组进行字母替换,看能不能到达endWord + for (int i = 0; i < currentWordArray.length; i++) { + // 记录当前字符,之后如果替换当前位置的字符找不到,要还原进行下一个字符的寻找的。(这样可以满足题目要求,只允许更改一个字符) + char remember = currentWordArray[i]; + // 遍历26个字母对其进行替换 + for (char k = 'a'; k <= 'z'; k++) { + // 如果遍历到的字母就是原先的字母,直接跳过此次循环 + if (k == remember) { + continue; + } + + // 将当前的字母赋值给字符数组对应的位置得到更改一个字母后的新字符数组 + currentWordArray[i] = k; + // 将其转化为新的字符串 + String newWord = String.valueOf(currentWordArray); + + // 如果新字符串就等于endWord证明我们已经找到了这条路径直接返回true即可 + if (endVisited.contains(newWord)) { + return true; + } + // 如果单词表里包含这个词并且我们没有访问过这个词 + if (wordSet.contains(newWord) && !visited.contains(newWord)) { + // 将其加入到队列中,并且更新visite哈希表 + nextLevelVisited.add(newWord); + visited.add(newWord); + } + } + + // 恢复位置的字母,要进入下一轮循环了 + currentWordArray[i] = remember; + } + + return false; + } + + /** + * 当前单词经过替换后能否到达endWord + * + * @param currentWord 当前的单词 + * @param endWord 目标单词 + * @param queue 队列 + * @param visited 用于存储是否访问过某个单词的哈希表 + * @param wordSet 存储单词字典的哈希表 + * @return + */ + public boolean findPath(String currentWord, String endWord, + Queue queue, Set visited, Set wordSet) { + // 将字符串转换成字符数组,用于遍历 + char[] currentWordArray = currentWord.toCharArray(); + + // 遍历字符数组进行字母替换,看能不能到达endWord + for (int i = 0; i < currentWordArray.length; i++) { + // 记录当前字符,之后如果替换当前位置的字符找不到,要还原进行下一个字符的寻找的。(这样可以满足题目要求,只允许更改一个字符) + char remember = currentWordArray[i]; + // 遍历26个字母对其进行替换 + for (char k = 'a'; k <= 'z'; k++) { + // 如果遍历到的字母就是原先的字母,直接跳过此次循环 + if (k == remember) { + continue; + } + + // 将当前的字母赋值给字符数组对应的位置得到更改一个字母后的新字符数组 + currentWordArray[i] = k; + // 将其转化为新的字符串 + String newWord = String.valueOf(currentWordArray); + + // 如果新字符串就等于endWord证明我们已经找到了这条路径直接返回true即可 + if (newWord.equals(endWord)) { + return true; + } + // 如果单词表里包含这个词并且我们没有访问过这个词 + if (wordSet.contains(newWord) && !visited.contains(newWord)) { + // 将其加入到队列中,并且更新visite哈希表 + queue.offer(newWord); + visited.add(newWord); + } + } + + // 恢复位置的字母,要进入下一轮循环了 + currentWordArray[i] = remember; + } + + // 程序走到这里还没返回,就证明我们没有找到endWord + return false; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/week04/\346\234\254\345\221\250\345\255\246\344\271\240\347\254\224\350\256\260.md" "b/week04/\346\234\254\345\221\250\345\255\246\344\271\240\347\254\224\350\256\260.md" new file mode 100644 index 00000000..6a96a65b --- /dev/null +++ "b/week04/\346\234\254\345\221\250\345\255\246\344\271\240\347\254\224\350\256\260.md" @@ -0,0 +1,139 @@ +学习笔记第四周 + +### 一. 总结作业题 + +#### 1. 题目描述 + +​ 使用二分查找,寻找一个半有序数组 [4, 5, 6, 7, 0, 1, 2] 中间无序的地方 + + + +#### 2. 思路描述 + +​ 这样一个半有序数组其实是有规律的,抓住了最核心的规律这个问题就不难解决了。我们可以看到,虽然他是半有序的,但是他分别有序的两个区间都是递增(或者递减,看题目要求了)的。而偏偏在中间点也就是无序的边界点,他会变成递减的。也就是7 > 0, 或者 0 < 7, 翻译成代码就是如下所示的两句核心代码: + +```java +nums[i] > nums[i + 1]; +nums[i] < nums[i - 1]; +``` + +​ 所以我们思路再清晰一点就是如下: + +​ (1). 利用二分,每次计算出mid下标 + +​ (2). 看nums[mid] > nums[0]是否成立,如果成立,代表左边这一段是有序的,那么我就要到右边去寻找边界点。如果不成立,代表右边是有序的,那么我就要到左边去寻找边界点。 + + (3) . 重复以上操作直到找到为止 + +​ 代码如下所示: + +```java + /** + * 方法一:二分法 + * + * 时间复杂度:O(logN), N即为数组元素的个数 + * 空间复杂度:O(1) + */ + + if (nums.length == 1) { + return nums[0]; + } + + // 定义左边界,有边界,初始条件下当然就为0,和nums.length-1,也就是数组的初始位置和终止位置 + int left = 0, right = nums.length - 1, len = right; + + // 筛选出数组本身有序的情况 + if (nums[right] > nums[left]) { + // 数组本身有序的情况下,第一个元素即为数组的最小元素 + return nums[left]; + } + + // 当左边界小于有边界时继续循环(这就是二分模板记住即可) + while (left <= right) { + // 算出中间节点的下标,这里用变成left + (right - left) / 2是为了防止left+right超出范围 + // 用移位操作是因为计算机对于位运算更友好,更快 + int mid = left + ((right - left) >> 1); + + // 这就是边界点, 边界点就是数组的值不随下标的增大而增大的那两个点 + if (nums[mid] > nums[mid + 1]) { + return nums[mid + 1]; + } + if (nums[mid] < nums[mid - 1]) { + return nums[mid]; + } + + // 如果左边是有序的 + if (nums[mid] > nums[0]) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return -1; +``` + +​ 其中代码里我注释都写的非常详细,唯有一个地方需要单独拿出来讲一下: + +``` + if (nums[mid] > nums[mid + 1]) { + return nums[mid + 1]; + } + if (nums[mid] < nums[mid - 1]) { + return nums[mid]; + } +``` + +​ 这里的两句if是**不能更换位置**的。 + +​ 为什么? + +​ 因为java的除法是**向下取整**的,假如我们的序列现在是如下序列: + +​ 1 6 5 4 3 2 + +​ 那么最后的mid就会收敛到下标为0的这个位置,如果你后一句if写前面,那就会报数组下标越界的错误。 + + + +#### 二. 作业题感想 + +1. 单词接龙 + + 这道题目个人认为官方的解法不是很优,因为他每次都是比对单词,这样的话时间复杂度就是O(N^2), 实际上我们只需要将旧单词的每一位从a更换到z,看看在不在set集合里面即可。这样复杂度可以降为O(N * 26)。具体代码如下: + + ```java + // 遍历字符数组进行字母替换,看能不能到达endWord + for (int i = 0; i < currentWordArray.length; i++) { + // 记录当前字符,之后如果替换当前位置的字符找不到,要还原进行下一个字符的寻找的。(这样可以满足题目要求,只允许更改一个字符) + char remember = currentWordArray[i]; + // 遍历26个字母对其进行替换 + for (char k = 'a'; k <= 'z'; k++) { + // 如果遍历到的字母就是原先的字母,直接跳过此次循环 + if (k == remember) { + continue; + } + + // 将当前的字母赋值给字符数组对应的位置得到更改一个字母后的新字符数组 + currentWordArray[i] = k; + // 将其转化为新的字符串 + String newWord = String.valueOf(currentWordArray); + + // 如果新字符串就等于endWord证明我们已经找到了这条路径直接返回true即可 + if (newWord.equals(endWord)) { + return true; + } + // 如果单词表里包含这个词并且我们没有访问过这个词 + if (wordSet.contains(newWord) && !visited.contains(newWord)) { + // 将其加入到队列中,并且更新visite哈希表 + queue.offer(newWord); + visited.add(newWord); + } + } + + // 恢复位置的字母,要进入下一轮循环了 + currentWordArray[i] = remember; + } + ``` + + \ No newline at end of file diff --git a/week06/NOTE.md b/week06/NOTE.md index 50de3041..7dfc8e60 100644 --- a/week06/NOTE.md +++ b/week06/NOTE.md @@ -1 +1,6 @@ -学习笔记 \ No newline at end of file +学习笔记 +这两个周可真是开启了地狱模式,hard的DP实在是让人分分钟崩溃 +,看题解花2个小时,还有可能看不懂。好在真的是坚持下来了,看一遍看不懂, +看两遍。。。三遍。。。直到看懂,然后将其实现。已经真正能够感受到自己的 +进步,所有的进步都是在这种被虐的过程中煎熬出来的。最近工作也比较忙,java训练营那边 +也进入了难点的阶段。但是我相信自己会坚持下来,加油。以后会越来越努力。 \ No newline at end of file diff --git a/week06/burst-balloons.java b/week06/burst-balloons.java new file mode 100644 index 00000000..d3d7e88a --- /dev/null +++ b/week06/burst-balloons.java @@ -0,0 +1,50 @@ + +//有 n 个气球,编号为0 到 n-1,每个气球上都标有一个数字,这些数字存在数组 nums 中。 +// +// 现在要求你戳破所有的气球。如果你戳破气球 i ,就可以获得 nums[left] * nums[i] * nums[right] 个硬币。 这里的 lef +//t 和 right 代表和 i 相邻的两个气球的序号。注意当你戳破了气球 i 后,气球 left 和气球 right 就变成了相邻的气球。 +// +// 求所能获得硬币的最大数量。 +// +// 说明: +// +// +// 你可以假设 nums[-1] = nums[n] = 1,但注意它们不是真实存在的所以并不能被戳破。 +// 0 ≤ n ≤ 500, 0 ≤ nums[i] ≤ 100 +// +// +// 示例: +// +// 输入: [3,1,5,8] +//输出: 167 +//解释: nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> [] +//  coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167 +// +// Related Topics 分治算法 动态规划 +// 👍 581 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/burst-balloons/ + public int maxCoins(int[] nums) { + int n = nums.length; + int[][] rec = new int[n + 2][n + 2]; + int[] val = new int[n + 2]; + val[0] = val[n + 1] = 1; + for (int i = 1; i <= n; i++) { + val[i] = nums[i - 1]; + } + for (int i = n - 1; i >= 0; i--) { + for (int j = i + 2; j <= n + 1; j++) { + for (int k = i + 1; k < j; k++) { + int sum = val[i] * val[k] * val[j]; + sum += rec[i][k] + rec[k][j]; + rec[i][j] = Math.max(rec[i][j], sum); + } + } + } + return rec[0][n + 1]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/coin-change.java b/week06/coin-change.java new file mode 100644 index 00000000..6082bb69 --- /dev/null +++ b/week06/coin-change.java @@ -0,0 +1,103 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 +// -1。 +// +// 你可以认为每种硬币的数量是无限的。 +// +// +// +// 示例 1: +// +// +//输入:coins = [1, 2, 5], amount = 11 +//输出:3 +//解释:11 = 5 + 5 + 1 +// +// 示例 2: +// +// +//输入:coins = [2], amount = 3 +//输出:-1 +// +// 示例 3: +// +// +//输入:coins = [1], amount = 0 +//输出:0 +// +// +// 示例 4: +// +// +//输入:coins = [1], amount = 1 +//输出:1 +// +// +// 示例 5: +// +// +//输入:coins = [1], amount = 2 +//输出:2 +// +// +// +// +// 提示: +// +// +// 1 <= coins.length <= 12 +// 1 <= coins[i] <= 231 - 1 +// 0 <= amount <= 104 +// +// Related Topics 动态规划 +// 👍 931 👎 0 + + +import java.util.Arrays; + +//leetcode submit region begin(Prohibit modification and deletion) +class coin-change { + // 题目链接:https://leetcode-cn.com/problems/coin-change/ + public int count = -1; + public int sum = 0; + public int coinChange(int[] coins, int amount) { + + int[] dp = new int[amount + 1]; + Arrays.fill(dp, amount + 1); + dp[0] = 0; + + for (int i = 1; i <= amount; i++) { + for (int j = 0; j < coins.length; j++) { + if (coins[j] <= i) { + dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1); + } + } + } + + return dp[amount] == amount + 1 ? -1 : dp[amount]; +// sum = amount; +// dfs(coins, sum); +// +// return count == 0 ? -1 : count; + } + + public void dfs(int[] coins, int sum) { + for (int i = 0; i < coins.length; i++) { + if (sum > 0) { + count ++; + } + + if (sum == 0) { + return; + } + + if (sum < 0) { + return; + } + + dfs(coins, sum - coins[i]); + + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/decode-ways.java b/week06/decode-ways.java new file mode 100644 index 00000000..0dd9ce5a --- /dev/null +++ b/week06/decode-ways.java @@ -0,0 +1,130 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//一条包含字母 A-Z 的消息通过以下方式进行了编码: +// +// +//'A' -> 1 +//'B' -> 2 +//... +//'Z' -> 26 +// +// +// 给定一个只包含数字的非空字符串,请计算解码方法的总数。 +// +// 题目数据保证答案肯定是一个 32 位的整数。 +// +// +// +// 示例 1: +// +// +//输入:s = "12" +//输出:2 +//解释:它可以解码为 "AB"(1 2)或者 "L"(12)。 +// +// +// 示例 2: +// +// +//输入:s = "226" +//输出:3 +//解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。 +// +// +// 示例 3: +// +// +//输入:s = "0" +//输出:0 +// +// +// 示例 4: +// +// +//输入:s = "1" +//输出:1 +// +// +// 示例 5: +// +// +//输入:s = "2" +//输出:1 +// +// +// +// +// 提示: +// +// +// 1 <= s.length <= 100 +// s 只包含数字,并且可能包含前导零。 +// +// Related Topics 字符串 动态规划 +// 👍 564 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/decode-ways/ + public int numDecodings(String s) { + // 时间复杂度:O(N),一层遍历 + // 空间复杂度:O(N),额外开了一个一维数组 + return func1(s); + } + + /** + * 思路一:一维DP + * + * @param s 原始字符串 + * @return 最多编码数 + */ + private int func1(String s) { + // 1. 找重复性。当前字符串的解码方式分为几种情况。第一种当前s[i]为0, 如果s[i - 1]为1,或者2,证明最后两位可以拼成一个字母。那么以当前字符结尾的字符串的编码方式就 + // 是dp[i - 2].否则就是0。第二种如果当前s[i]不为0,那么至少dp[i] = dp[i - 1],也就是最后一位自己单独组。然后如果倒数第二位和倒数第一位组合的数字在[11, 26]之间, + // 证明最后两位也可以组成一组成为一个字母,这种情况下dp[i]要再加一个dp[i - 2] + // 2. 定义状态数组:dp[i] ,其中dp[i]表示以s[i]结尾的字符串有多少种编码方式 + // 3. DP方程:if s[i] = 0 && s[i-1] = 1 || 2, dp[i] = dp[i - 2]. + // if s[i] != 0 dp[i] = dp[i - 1], if 11 <= s[i - 1]s[i] <= 26, dp[i] += dp[i - 2] + // 实际上上面的思路还可以合并,具体在代码里体现 + + // 边界条件 + if (s.charAt(0) == '0') { + return 0; + } + + // 将字符串转化为字符数组 + char[] sArray = s.toCharArray(); + // 得到字符串的长度 + int length = s.length(); + + // 申请dp数组 + int[] dp = new int[length]; + // 初始化dp数组 + dp[0] = 1; + + // 循环dp + for (int i = 1; i < sArray.length; i++) { + // 如果最后一个字符不是0,铁定的可以单独解码出一个字母,这种情况dp[i]至少也是dp[i - 1] + if (sArray[i] != '0') { + dp[i] = dp[i - 1]; + } + + // 算出最后两个数的数值 + int num = 10 * (sArray[i - 1] - '0') + (sArray[i] - '0'); + + // 如果最后两个数在10 - 26之间,那么证明最后两个数是可以合并在一起解码一个字母的,那么dp[i]还要加上dp[i - 2]。 + // 这里其实就把最后一个字符是0的情况包含进去了。即便你是0,也只有10,20是符合要求的,而10,20都在10-26区间 + if (num >= 10 && num <= 26) { + // 这里如果i是1下标会越界,所以我们做一个特殊判断 + if (i == 1) { + dp[i] += 1; + } else { + dp[i] += dp[i - 2]; + } + } + } + + return dp[length - 1]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/edit-distance.java b/week06/edit-distance.java new file mode 100644 index 00000000..4d82bb7f --- /dev/null +++ b/week06/edit-distance.java @@ -0,0 +1,115 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 +// +// 你可以对一个单词进行如下三种操作: +// +// +// 插入一个字符 +// 删除一个字符 +// 替换一个字符 +// +// +// +// +// 示例 1: +// +// +//输入:word1 = "horse", word2 = "ros" +//输出:3 +//解释: +//horse -> rorse (将 'h' 替换为 'r') +//rorse -> rose (删除 'r') +//rose -> ros (删除 'e') +// +// +// 示例 2: +// +// +//输入:word1 = "intention", word2 = "execution" +//输出:5 +//解释: +//intention -> inention (删除 't') +//inention -> enention (将 'i' 替换为 'e') +//enention -> exention (将 'n' 替换为 'x') +//exention -> exection (将 'n' 替换为 'c') +//exection -> execution (插入 'u') +// +// +// +// +// 提示: +// +// +// 0 <= word1.length, word2.length <= 500 +// word1 和 word2 由小写英文字母组成 +// +// Related Topics 字符串 动态规划 +// 👍 1271 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/edit-distance/ + public int minDistance(String word1, String word2) { + // 时间复杂度:O(MN),两层for循环 + // 空间复杂度:O(MN),即为两个字符串长度的乘积 + return func1(word1, word2); + } + + /** + * 思路一:动态规划 + * + * @param word1 字符串1 + * @param word2 字符串2 + * @return 最小编辑距离 + */ + private int func1(String word1, String word2) { + // 1. 找重复性。其实重复性很好找。可以借鉴最长公共子序列这个题目的思路(1143)。如果当前下标指向的Word1,Word2所在字符相等。 + // 那么前一个的编辑距离就是当前的编辑距离。否则,按替换,插入,删除三种操作来看 + // 2. 定义状态数组:dp[i][j],表示字符串1从[0,i)转化成字符串[0,j)所需要的的编辑距离 + // 3. DP方程:当word1[i] == word2[j]。当前的dp[i][j] = dp[i - 1][j - 1]; + // 当word1[i] != word2[j]。可以考虑三种情况。增加,删除,替换。 + // 如果将word1增加一个word2当前的word2[j]字符,那么他们最后一个字符就相等了,那么dp[i][j] = dp[i][j - 1] + 1 + // 如果将word1最后一个字符删除,那么dp[i][j] = dp[i - 1][j] + 1 + // 如果将word1最后一个字符替换成word2最后一个字符,dp[i][j] = dp[i - 1][j - 1] + 1 + + // 得到两个字符串的长度 + int len1 = word1.length(); + int len2 = word2.length(); + + // 申请dp数组,多开一行是为了保存边界条件,防止不必要的麻烦,动态规划中很常见 + int[][] dp = new int[len1 + 1][len2 + 1]; + + // 初始化dp数组,考虑到空字符串的情况,其实空字符串与另一个字符串的编辑距离就是另一个字符串的长度 + // 当word2的长度为0时,将word1的字符全部删除即可 + for (int i = 1; i <= len1; i++) { + // 有多少个字符就删多少个字符 + dp[i][0] = i; + } + + // 当word1的长度为0时,插入所有的word2字符即可 + for (int i = 1; i <= len2; i++) { + // 有多少个字符就插入多少个字符 + dp[0][i] = i; + } + + // 将字符串转化为字符数组 + char[] c1 = word1.toCharArray(); + char[] c2 = word2.toCharArray(); + + // 循环进行DP + for (int i = 1; i <= c1.length; i++) { + for (int j = 1; j <= c2.length; j++) { + // 如果当前i和j所标识的两个字符串的最后一个相等,那么编辑距离就是前一个的距离即可 + if (c1[i - 1] == c2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min(dp[i][j - 1] + 1, Math.min(dp[i - 1][j] + 1, dp[i - 1][j - 1] + 1)); + } + } + } + + return dp[len1][len2]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/frog-jump.java b/week06/frog-jump.java new file mode 100644 index 00000000..e7eaee89 --- /dev/null +++ b/week06/frog-jump.java @@ -0,0 +1,100 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//一只青蛙想要过河。 假定河流被等分为 x 个单元格,并且在每一个单元格内都有可能放有一石子(也有可能没有)。 青蛙可以跳上石头,但是不可以跳入水中。 +// +// 给定石子的位置列表(用单元格序号升序表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一个石子上)。 开始时, 青蛙默认已站在第一个石子上,并可以 +//假定它第一步只能跳跃一个单位(即只能从单元格1跳至单元格2)。 +// +// 如果青蛙上一步跳跃了 k 个单位,那么它接下来的跳跃距离只能选择为 k - 1、k 或 k + 1个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。 +// +// +// 请注意: +// +// +// 石子的数量 ≥ 2 且 < 1100; +// 每一个石子的位置序号都是一个非负整数,且其 < 231; +// 第一个石子的位置永远是0。 +// +// +// 示例 1: +// +// +//[0,1,3,5,6,8,12,17] +// +//总共有8个石子。 +//第一个石子处于序号为0的单元格的位置, 第二个石子处于序号为1的单元格的位置, +//第三个石子在序号为3的单元格的位置, 以此定义整个数组... +//最后一个石子处于序号为17的单元格的位置。 +// +//返回 true。即青蛙可以成功过河,按照如下方案跳跃: +//跳1个单位到第2块石子, 然后跳2个单位到第3块石子, 接着 +//跳2个单位到第4块石子, 然后跳3个单位到第6块石子, +//跳4个单位到第7块石子, 最后,跳5个单位到第8个石子(即最后一块石子)。 +// +// +// 示例 2: +// +// +//[0,1,2,3,4,8,9,11] +// +//返回 false。青蛙没有办法过河。 +//这是因为第5和第6个石子之间的间距太大,没有可选的方案供青蛙跳跃过去。 +// +// Related Topics 动态规划 +// 👍 127 👎 0 + + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/frog-jump/ + public boolean canCross(int[] stones) { + // 时间复杂度:O(N^2) + // 空间复杂度:O(N^2), hashmap最大为O(N^2),平均应该就是O(N) + return func1(stones); + } + + /** + * 思路一:动态规划 + * + * @param stones 石头位置数组 + * @return 结果 + */ + private boolean func1(int[] stones) { + // 1. 找重复性:每个位置能够跳跃的size只有三种,每次跳跃看能不能跳到石头上(将石头位置存储到hashmap中即可) + // 如果能跳到石头上,就更新这块石头对应的跳跃size。hash的key即为石头位置,value即为当前石头位置能够跳跃的 + // 步数的集合。最终看最后一个位置的石头对应的value是否为空,就可以知道是否能够跳过去 + + // 如果第二个石头的位置不是1,那肯定跳不过去,因为第一个石头只能跳1步 + if (stones[1] != 1) { + return false; + } + + // 申请一个hashmap + Map> map = new HashMap(); + // 初始化map + for (int i = 1; i < stones.length; i++) { + map.put(stones[i], new HashSet<>()); + } + map.get(1).add(1); + + // 开始循环看是否能够跳跃到最后一个石头 + int len = stones.length; + for (int i = 1; i < stones.length; i++) { + // 获取当前位置能够跳跃的步数集合 + Set jumpSize = map.get(stones[i]); + // 遍历进行跳跃 + for (int jump : jumpSize) { + // 对于每个jump步数,都可以有三种选择方式进行跳跃 + for (int step = jump - 1; step <= jump + 1; step++) { + if (step > 0 && map.containsKey(stones[i] + step)) { + map.get(stones[i] + step).add(step); + } + } + } + } + + return map.get(stones[len - 1]).size() > 0; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/house-robber-ii.java b/week06/house-robber-ii.java new file mode 100644 index 00000000..7e53dfd0 --- /dev/null +++ b/week06/house-robber-ii.java @@ -0,0 +1,111 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的 +//房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。 +// +// 给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。 +// +// +// +// 示例 1: +// +// +//输入:nums = [2,3,2] +//输出:3 +//解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。 +// +// +// 示例 2: +// +// +//输入:nums = [1,2,3,1] +//输出:4 +//解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 +//  偷窃到的最高金额 = 1 + 3 = 4 。 +// +// 示例 3: +// +// +//输入:nums = [0] +//输出:0 +// +// +// +// +// 提示: +// +// +// 1 <= nums.length <= 100 +// 0 <= nums[i] <= 1000 +// +// Related Topics 动态规划 +// 👍 426 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class house-robber-ii { + // 题目链接:https://leetcode-cn.com/problems/house-robber-ii/ + public int rob(int[] nums) { + // 时间复杂度:O(N),一层for循环即可 + // 空间复杂度:O(2N),N为房子的数量,2是代表每个房子有可能偷有可能不偷 + return func1(nums); + } + + /** + * 思路一:二维DP + * + * @param nums 原始数据 + * @return 能够偷到的最大值 + */ + private int func1(int[] nums) { + // 1. 找重复性,也就是子问题。对于这个问题而言,关键是看前一个房子的他偷还是没偷。如果前一个房子偷了,那么当前的最大值也就是前一个房子偷到的值。 + // 如果前一个房子没偷,那么最大值就是第n - 2个房子偷到的钱加上当前房子能偷到的钱 + // 2. 定义状态数组:dp[i][0],dp[i][1], 前者代表第i个房子他没偷,后者代表第i个房子他偷了 + // 3. DP方程:dp[i][1] = dp[i - 1][0] + nums[i], dp[i][0] = Max(dp[i - 1][1], dp[i - 1][0]) + + // 以上描述与打家劫舍1没有任何差别。唯一有点不一样的就是这道题是一个闭环。那么我们可以采用的方式就是强行砸开一个口子 + // 要么第一个我不偷,要么最后一个我不偷,这两种情形分别用打家劫舍1的方式算出来,取最大值即可。 + + // 边界条件判断 + if (nums.length == 0) { + return 0; + } + + if (nums.length == 1) { + return nums[0]; + } + + // 获取数组长度 + int length = nums.length; + // 申请dp数组 + int[][] dp = new int[length][2]; + + // 最后一个不要 + dp[0][0] = 0; + dp[0][1] = nums[0]; + + // 循环dp + for (int i = 1; i < length - 1; i++) { + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]); + dp[i][1] = dp[i - 1][0] + nums[i]; + } + + // 得到第一种情况的最大值 + int firstMax = Math.max(dp[length - 2][0], dp[length - 2][1]); + + // 第一个元素不要 + dp[1][0] = 0; + dp[1][1] = nums[1]; + + // 循环dp + for (int i = 2; i < length; i++) { + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]); + dp[i][1] = dp[i - 1][0] + nums[i]; + } + + // 得到第二种情况的最大值 + int secondMax = Math.max(dp[length - 1][0], dp[length - 1][1]); + + return Math.max(firstMax, secondMax); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/house-robber.java b/week06/house-robber.java new file mode 100644 index 00000000..014d266c --- /dev/null +++ b/week06/house-robber.java @@ -0,0 +1,78 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上 +//被小偷闯入,系统会自动报警。 +// +// 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 +// +// +// +// 示例 1: +// +// 输入:[1,2,3,1] +//输出:4 +//解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 +//  偷窃到的最高金额 = 1 + 3 = 4 。 +// +// 示例 2: +// +// 输入:[2,7,9,3,1] +//输出:12 +//解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 +//  偷窃到的最高金额 = 2 + 9 + 1 = 12 。 +// +// +// +// +// 提示: +// +// +// 0 <= nums.length <= 100 +// 0 <= nums[i] <= 400 +// +// Related Topics 动态规划 +// 👍 1178 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class 打家劫舍 { + // 题目链接:https://leetcode-cn.com/problems/house-robber/ + public int rob(int[] nums) { + return func1(nums); + } + + /** + * 思路一:二维DP + * + * @param nums 原始数组 + * @return 能偷到的钱最大值 + */ + private int func1(int[] nums) { + // 1. 找重复性,也就是子问题。对于这个问题而言,关键是看前一个房子的他偷还是没偷。如果前一个房子偷了,那么当前的最大值也就是前一个房子偷到的值。 + // 如果前一个房子没偷,那么最大值就是第n - 2个房子偷到的钱加上当前房子能偷到的钱 + // 2. 定义状态数组:dp[i][0],dp[i][1], 前者代表第i个房子他没偷,后者代表第i个房子他偷了 + // 3. DP方程:dp[i][1] = dp[i - 1][0] + nums[i], dp[i][0] = Max(dp[i - 1][1], dp[i - 1][0]) + + // 边界条件判断 + if (nums.length == 0 || nums == null) { + return 0; + } + + // 得到数组的长度 + int length = nums.length; + // 申请dp数组 + int[][] dp = new int[length][2]; + + // 初始化 + dp[0][0] = 0; + dp[0][1] = nums[0]; + + // 循环dp + for (int i = 1; i < length; i++) { + dp[i][0] = Math.max(dp[i - 1][1], dp[i - 1][0]); + dp[i][1] = dp[i - 1][0] + nums[i]; + } + + return Math.max(dp[length - 1][0], dp[length - 1][1]); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/longest-common-subsequence.java b/week06/longest-common-subsequence.java new file mode 100644 index 00000000..f287b836 --- /dev/null +++ b/week06/longest-common-subsequence.java @@ -0,0 +1,150 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。 +// +// 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 +//例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。 +// +// +// 若这两个字符串没有公共子序列,则返回 0。 +// +// +// +// 示例 1: +// +// 输入:text1 = "abcde", text2 = "ace" +//输出:3 +//解释:最长公共子序列是 "ace",它的长度为 3。 +// +// +// 示例 2: +// +// 输入:text1 = "abc", text2 = "abc" +//输出:3 +//解释:最长公共子序列是 "abc",它的长度为 3。 +// +// +// 示例 3: +// +// 输入:text1 = "abc", text2 = "def" +//输出:0 +//解释:两个字符串没有公共子序列,返回 0。 +// +// +// +// +// 提示: +// +// +// 1 <= text1.length <= 1000 +// 1 <= text2.length <= 1000 +// 输入的字符串只含有小写英文字符。 +// +// Related Topics 动态规划 +// 👍 284 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class longest-common-subsequence { + // 题目链接:https://leetcode-cn.com/problems/longest-common-subsequence/ + public int longestCommonSubsequence(String text1, String text2) { + return func2(text1, text2); + } + + /** + * 思路二:动态规划简单版本 + * @param text1 第一个字符串 + * @param text2 第二个字符串 + * @return 最长公共子序列的长度 + */ + private int func2(String text1, String text2) { + // 将两个字符串转化为数组,便于后面的遍历 + char[] t1 = text1.toCharArray(); + char[] t2 = text2.toCharArray(); + + // 得到两个字符串的长度 + int length1 = text1.length() + 1; + int length2 = text2.length() + 1; + + // 定义动态规划状态集 + int[][] dp = new int[length1][length2]; + + for (int i = 1; i < length1; i++) { + for (int j = 1; j < length2; j++) { + // 1. + // 如果当前两个字符串目前为止最后一个字符相等,那么我只需要知道他们两个的子序列的公共长度,再加1即可 + // 比如 ****A 和 %%%%%A,A和A相等了,那么只需要看****和%%%%的最长公共子序列是多少然后加上1即可 + // 2. + // 否则,加入现在是****A,%%%%B,就要看****A和%%%%的公共子序列长度与****和%%%%B的最大值是多少 + if (t1[i - 1] == t2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + return dp[length1 - 1][length2 - 1]; + } + + /** + * 思路一:动态规划复杂版本 + * @param text1 第一个字符串 + * @param text2 第二个字符串 + * @return 最长公共子序列的长度 + */ + private int func1(String text1, String text2) { + // 将两个字符串转化为数组,便于后面的遍历 + char[] t1 = text1.toCharArray(); + char[] t2 = text2.toCharArray(); + + // 得到两个字符串的长度 + int length1 = text1.length(); + int length2 = text2.length(); + + // 用一个二维数组来存储结果 + int[][] res = new int[length1][length2]; + + // 初始化res的第一行 + for (int i = 0; i < length2; i++) { + if (i > 0 && res[0][i - 1] == 1) { + res[0][i] = 1; + continue; + } + + if (t2[i] == t1[0]) { + res[0][i] = 1; + } + } + + // 初始化res的第一列 + for (int i = 0; i < length1; i++) { + if (i > 0 && res[i - 1][0] == 1) { + res[i][0] = 1; + continue; + } + + if (t1[i] == t2[0]) { + res[i][0] = 1; + } + } + + // 开始完善结果集 + for (int i = 1; i < length1; i++) { + for (int j = 1; j < length2; j++) { + // 1. + // 如果当前两个字符串目前为止最后一个字符相等,那么我只需要知道他们两个的子序列的公共长度,再加1即可 + // 比如 ****A 和 %%%%%A,A和A相等了,那么只需要看****和%%%%的最长公共子序列是多少然后加上1即可 + // 2. + // 否则,加入现在是****A,%%%%B,就要看****A和%%%%的公共子序列长度与****和%%%%B的最大值是多少 + if (t1[i] == t2[j]) { + res[i][j] = res[i - 1][j - 1] + 1; + } else { + res[i][j] = Math.max(res[i][j - 1], res[i - 1][j]); + } + } + } + + return res[length1 - 1][length2 - 1]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/longest-valid-parentheses.java b/week06/longest-valid-parentheses.java new file mode 100644 index 00000000..a2f10245 --- /dev/null +++ b/week06/longest-valid-parentheses.java @@ -0,0 +1,118 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。 +// +// 示例 1: +// +// 输入: "(()" +//输出: 2 +//解释: 最长有效括号子串为 "()" +// +// +// 示例 2: +// +// 输入: ")()())" +//输出: 4 +//解释: 最长有效括号子串为 "()()" +// +// Related Topics 字符串 动态规划 +// 👍 1085 👎 0 + + +import java.util.Deque; +import java.util.LinkedList; +import java.util.Stack; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/longest-valid-parentheses/ + public int longestValidParentheses(String s) { + // 时间复杂度:O(N),一层for循环 + // 空间复杂度:O(N),用了一个栈 +// return func1(s); + + // 时间复杂度:O(N),一层for循环 + // 空间复杂度:O(N),用了一个栈 + return func2(s); + + } + + /** + * 思路二:动态规划 + * + * @param s 原始字符串 + * @return 最大有效括号数 + */ + private int func2(String s) { + // 1. 找重复性。 + // 2. 定义状态数组:dp[i]表示以i结尾的字符串的最大有效括号数量 + // 3. 如果当前下标表示的字符是')',并且他的前一个是左括号。那么dp[i] = dp[i - 2] + 2 + // 反之,如果他的前一个不是左括号,而是右括号。在这个基础上 + // 如果s[i - dp[i - 1] - 1]代表的字符是左括号。如****(()())这种情况。 + // dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2。其中dp[i - 1]指的是()()这两个括号 + // 的长度,可以看到的(()())最外面的两个括号就是 2, 而***代表的就是dp[i-dp[i-1]-2] + + // 获取字符串长度 + int length = s.length(); + // 申请dp数组 + int[] dp = new int[length]; + // 定义最大值 + int max = 0; + + // 循环dp + for (int i = 1; i < length; i++) { + if (s.charAt(i) == ')') { + if (s.charAt(i - 1) == '(') { + dp[i] = dp[Math.max(i - 2, 0)] + 2; + } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = dp[i - 1] + dp[Math.max(i - dp[i - 1] - 2, 0)] + 2; + } + max = Math.max(dp[i], max); + } + } + + return max; + } + + /** + * 思路一:栈 + * + * @param s 原字符串 + * @return 最大有效括号数 + */ + private int func1(String s) { + // 用栈去模拟 + // 将字符串转化为字符数组 + char[] sA = s.toCharArray(); + // 定义返回结果 + int res = 0; + // 获取字符串长度 + int length = s.length(); + + // 申请一个栈 + Deque stack = new LinkedList<>(); + // 防止第一个放进去的有括号的情况 + stack.push(-1); + + // 遍历字符串 + for (int i = 0; i < length; i++) { + // 如果遇到左括号,将它的下标入栈即可 + if (sA[i] == '(') { + stack.push(i); + } else { + // 如果遇到右括号 + // 1. 先出栈一个元素 + stack.pop(); + // 2. 如果此时栈为空,证明出掉的是我们最开始push进去的-1,那么我们将当前右括号的下标入栈 + // 3. 如果栈不为空,证明当前右括号被匹配了。那么我们计算比较一下以前的最大值以及当前右括号下标和栈顶的差值,得出最大值即可 + if (stack.isEmpty()) { + stack.push(i); + } else { + res = Math.max(res, i - stack.peek()); + } + } + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/max-sum-of-rectangle-no-larger-than-k.java b/week06/max-sum-of-rectangle-no-larger-than-k.java new file mode 100644 index 00000000..82569a2a --- /dev/null +++ b/week06/max-sum-of-rectangle-no-larger-than-k.java @@ -0,0 +1,56 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个非空二维矩阵 matrix 和一个整数 k,找到这个矩阵内部不大于 k 的最大矩形和。 +// +// 示例: +// +// 输入: matrix = [[1,0,1],[0,-2,3]], k = 2 +//输出: 2 +//解释: 矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。 +// +// +// 说明: +// +// +// 矩阵内的矩形区域面积必须大于 0。 +// 如果行数远大于列数,你将如何解答呢? +// +// Related Topics 队列 二分查找 动态规划 +// 👍 137 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k/ + public int maxSumSubmatrix(int[][] matrix, int k) { + // 找重复性:你会发现,以[i][j]作为右下角元素的最大矩形的数字和与[i-1][j],[i][j-1],[i-1][j-1]相关 + // 定义状态数组:dp[i][j],代表以[i][j]这个坐标所代表的元素作为右下角的最大矩形的数字和 + // DP方程:dp[i][j] = dp[i-1][j]+dp[i,j-1]-dp[i-1][j-1] + matrix[i][j] + + + // 得到矩阵的行数 + int row = matrix.length; + // 得到矩阵的列数 + int col = matrix[0].length; + + // 定义最大值 + int max = Integer.MIN_VALUE; + + // 循环dp + for (int p = 1; p <= row; p++) { + for (int q = 1; q <= col; q++) { + // 申请dp数组 + int[][] dp = new int[row + 1][col + 1]; + for (int i = p; i <= row; i++) { + for (int j = q; j <= col; j++) { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1] + matrix[i - 1][j - 1]; + if (dp[i][j] <= k && dp[i][j] > max) { + max = dp[i][j]; + } + } + } + } + } + return max; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/maximal-square.java b/week06/maximal-square.java new file mode 100644 index 00000000..0b33b8cf --- /dev/null +++ b/week06/maximal-square.java @@ -0,0 +1,128 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。 +// +// +// +// 示例: +// +// +//输入: +//matrix = [["1","0","1","0","0"], +// ["1","0","1","1","1"], +// ["1","1","1","1","1"], +// ["1","0","0","1","0"]] +// +//输出:4 +// Related Topics 动态规划 +// 👍 622 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/maximal-square/ + public int maximalSquare(char[][] matrix) { + // 时间复杂度:O(N^2),两层for循环 + // 空间复杂度,O(N^2),开了二维数组进行dp +// return func1(matrix); + + // 时间复杂度:O(N^2),两层for循环 + // 空间复杂度,O(N) + return func2(matrix); + + } + + /** + * 思路二:对思路一进行空间优化以及代码优化 + * + * @param matrix 原始数组 + * @return 最大正方形面积 + */ + private int func2(char[][] matrix) { + // 1. 找重复性。可以发现,矩阵中每一个元素的最大正方形都和他的长宽有关,而长宽直接和他左边的元素,上面的元素,斜对角上的元素相关 + // 2. 定义状态数组dp[i][j],代表以第i行第j列元素为右下角的能够形成的最大正方形的边的长度。 + // 3. DP方程:dp[i][j] = Min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]} + 1,这里的dp可以不用申请,直接用原始的matrix进行模拟 + + // 边界条件判断 + if (matrix.length == 0) { + return 0; + } + + // 获取二维矩阵的长宽。 + int row = matrix.length; + int col = matrix[0].length; + + // 记录最大值 + int max = 0; + // 根据第一行第一列初始化max的值 + for (int i = 0; i < row; i++) { + if (matrix[i][0] == '1') { + max = 1; + break; + } + } + for (int i = 0; i < col; i++) { + if (matrix[0][i] == '1') { + max = 1; + break; + } + } + + // 循环dp + for (int i = 1; i < row; i++) { + for (int j = 1; j < col; j++) { + // 如果当前这个元素不为0 + if (matrix[i][j] == '1') { + int minValue = Math.min(matrix[i - 1][j] - '0', Math.min(matrix[i][j - 1] - '0', matrix[i - 1][j - 1] - '0')) + 1; + matrix[i][j] = (char) (minValue + '0'); + max = Math.max(minValue, max); + } + } + } + + return max * max; + } + + /** + * 思路一:二维dp + * + * @param matrix 原始矩阵 + * @return 最大正方形面积 + */ + private int func1(char[][] matrix) { + // 1. 找重复性。可以发现,矩阵中每一个元素的最大正方形都和他的长宽有关,而长宽直接和他左边的元素,上面的元素,斜对角上的元素相关 + // 2. 定义状态数组dp[i][j],代表以第i行第j列元素为右下角的能够形成的最大正方形的边的长度。 + // 3. DP方程:dp[i][j] = Min{dp[i-1][j], dp[i][j-1], dp[i-1][j-1]} + 1 + + // 边界条件判断 + if (matrix.length == 0) { + return 0; + } + + // 获取二维矩阵的长宽。 + int row = matrix.length; + int col = matrix[0].length; + + // 申请DP数组 + int[][] dp = new int[row + 1][col + 1]; + + // 记录最大值 + int max = 0; + + // 循环dp + for (int i = 1; i <= row; i++) { + for (int j = 1; j <= col; j++) { + // 如果当前这个元素为0 + if (matrix[i - 1][j - 1] == '0') { + // 则以他为右下角所能围成的正方形最大面积就是0,将dp数组置为0 + dp[i][j] = 0; + } else { // 否则,用dp方程进行求解即可 + dp[i][j] = Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1])) + 1; + max = Math.max(dp[i][j], max); + } + } + } + + return max * max; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/maximum-product-subarray.java b/week06/maximum-product-subarray.java new file mode 100644 index 00000000..4646f7f3 --- /dev/null +++ b/week06/maximum-product-subarray.java @@ -0,0 +1,116 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。 +// +// +// +// 示例 1: +// +// 输入: [2,3,-2,4] +//输出: 6 +//解释: 子数组 [2,3] 有最大乘积 6。 +// +// +// 示例 2: +// +// 输入: [-2,0,-1] +//输出: 0 +//解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。 +// Related Topics 数组 动态规划 +// 👍 849 👎 0 + + +import scala.Int; + +//leetcode submit region begin(Prohibit modification and deletion) +class maximum-product-subarray { + // 题目链接:https://leetcode-cn.com/problems/maximum-product-subarray/ + public int maxProduct(int[] nums) { + // 时间复杂度:O(N),一层for循环 + // 空间复杂度:O(N),开了两个dp数组 +// return func1(nums); + + // 时间复杂度:O(N),一层for循环 + // 空间复杂度:O(1),只是使用了几个变量,所以是O(1)的复杂度。 + return func2(nums); + + } + + private int func2(int[] nums) { + // 判空,边界条件 + if (nums.length == 0) { + return 0; + } + + // 获取数组长度 + int length = nums.length; + + // 方法一用的数组,其实我们想想,完全可以不用数组,用变量即可做这件事情了 + int preMax = nums[0]; + int preMin = nums[0]; + int curMax = nums[0]; + int curMin = nums[0]; + int res = nums[0]; + + // 循环dp + for (int i = 1; i < length; i++) { + curMax = Math.max(preMax * nums[i], Math.max(nums[i], preMin * nums[i])); + curMin = Math.min(preMin * nums[i], Math.min(nums[i], preMax * nums[i])); + res = Math.max(res, curMax); + + // 当前一次循环结束后,当前的最大值,对于下一轮而言就是preMax了 + preMax = curMax; + // 当前一次循环结束后,当前的最小值,对于下一轮而言就是preMin了 + preMin = curMin; + } + + return res; + } + + /** + * 思路一:一维DP + * + * @param nums 原始数组 + * @return 最大乘积 + */ + private int func1(int[] nums) { + // 找重复性,当前的最大乘积只有两种情况,一种是前面连乘的我要保留和我当前的值乘一起,一种是我不要。而是当前的值自立门户 + // 定义状态数组:dp[i],当前位置的最大乘积值 + // DP方程:dp[i] = Max{dp[i - 1] * nums[i], nums[i]} + // 上面的思路是错的,因为如果当数组中只有偶数个负数的时候,最大值应该是所有的元素相乘。 + // 考虑当前位置如果是一个负数的话,那么我们希望以它前一个位置结尾的某个段的积也是个负数,这样就可以负负得正, + // 并且我们希望这个积尽可能「负得更多」,即尽可能小。如果当前位置是一个正数的话,我们更希望以它前一个位置结尾的 + // 某个段的积也是个正数,并且希望它尽可能地大。 + + // 重新定义DP方程:dpMax = Max{dpMax[i - 1] * nums[i], nums[i], dpMin[i - 1] * nums[i]} + // dpMin = Min{dpMax[i - 1] * nums[i], nums[i], dpMin[i - 1] * nums[i]} + + // 判空,边界条件 + if (nums.length == 0) { + return 0; + } + + // 得到数组的长度 + int length = nums.length; + // 申请dp数组 + int[] dpMax = new int[length]; + int[] dpMin = new int[length]; + + // 初始化dp[0] + dpMax[0] = nums[0]; + dpMin[0] = nums[0]; + // 定义记录最大值的变量 + int max = dpMax[0]; + + // 循环进行dp + for (int i = 1; i < length; i++) { + // 记录每一次的最大值最小值,这样我们就可以在最后的真正得到想要的最大值,也就是前面的负数才有可能会被后面的负数知道 + dpMax[i] = Math.max(dpMax[i - 1] * nums[i], Math.max(dpMin[i - 1] * nums[i], nums[i])); + dpMin[i] = Math.min(dpMin[i - 1] * nums[i], Math.min(dpMax[i - 1] * nums[i], nums[i])); + // 每一轮当前的最大值都与整体最大值比较下,这样遍历完我们选出的就是总体最大值 + max = Math.max(dpMax[i], Math.max(dpMin[i], max)); + } + + return max; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/maximum-subarray.java b/week06/maximum-subarray.java new file mode 100644 index 00000000..0f502a3c --- /dev/null +++ b/week06/maximum-subarray.java @@ -0,0 +1,102 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 +// +// 示例: +// +// 输入: [-2,1,-3,4,-1,2,1,-5,4] +//输出: 6 +//解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 +// +// +// 进阶: +// +// 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。 +// Related Topics 数组 分治算法 动态规划 +// 👍 2327 👎 0 最大子序和 + + +//leetcode submit region begin(Prohibit modification and deletion) +class maximum-subarray { + // 题目链接:https://leetcode-cn.com/problems/maximum-subarray/ + public int maxSubArray(int[] nums) { + // 暴力法,时间复杂度n +// int tmp = nums[0]; +// int max = tmp; +// +// for (int i = 1; i < nums.length; i++) { +// if (tmp + nums[i] > nums[i]) { +// max = Math.max(max, tmp + nums[i]); +// tmp += nums[i]; +// } else { +// max = Math.max(max, nums[i]); +// max = Math.max(max, tmp); +// tmp = nums[i]; +// } +// } +// +// return max; + + // 动态规划 +// int pre = 0; +// int max = nums[0]; +// +// for (int every : nums) { +// pre = Math.max(every, pre + every); +// max = Math.max(max, pre); +// } +// +// return max; + + // 1. 找重复性,不难发现,每遍历到一个地方,只有两种可能,一种就是当前元素和之前的截断,一种就是不截断 + // 2. 定义状态数组。dp[i],dp[i]表示到第i个位置为止的最大子序和 + // 3. DP方程:dp[i] = MAX{dp[i - 1], 0} + nums[i],这里如果dp[i-1]是小于0的,那他无论如何都会拖我当前nums[i]的后腿,我干嘛要加上他对不对 + int[] dp = new int[nums.length]; + dp[0] = nums[0]; + int max = nums[0]; + + for (int i = 1; i < nums.length; i++) { + dp[i] = Math.max(dp[i - 1], 0) + nums[i]; + max = Math.max(dp[i], max); + } + + return max; + + // 分治法 +// return maxSubArrayDivideWithBorder(nums, 0, nums.length-1); + } + + private int maxSubArrayDivideWithBorder(int[] nums, int start, int end) { + if (start == end) { + return nums[start]; + } + + int mid = start + (end - start)/2; + // 计算左边序列的最大值 + int leftMax = maxSubArrayDivideWithBorder(nums, 0, mid); + // 计算右边序列的最大值 + int rightMax = maxSubArrayDivideWithBorder(nums, mid + 1, end); + + // 计算最大值横跨左边和右边序列情况的最大值 + + // 先计算横跨左边和右边序列中左边的最大值 + int leftCrossMax = Integer.MIN_VALUE; + int leftCrossSum = 0; + for (int i = mid; i >= 0; i--) { + leftCrossSum += nums[i]; + leftCrossMax = Math.max(leftCrossMax, leftCrossSum); + } + + // 再计算横跨左边和右边序列中右边的最大值 + int rightCrossMax = Integer.MIN_VALUE; + int rightCrossSum = 0; + for (int i = mid + 1; i <= end; i++) { + rightCrossSum += nums[i]; + rightCrossMax = Math.max(rightCrossMax, rightCrossSum); + } + + // 这个时候就可以计算横跨情况下的最大值了 + int crossMax = leftCrossMax + rightCrossMax; + return Math.max(crossMax, Math.max(leftMax, rightMax)); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/minimum-path-sum.java b/week06/minimum-path-sum.java new file mode 100644 index 00000000..04b7cb0c --- /dev/null +++ b/week06/minimum-path-sum.java @@ -0,0 +1,116 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。 +// +// 说明:每次只能向下或者向右移动一步。 +// +// +// +// 示例 1: +// +// +//输入:grid = [[1,3,1],[1,5,1],[4,2,1]] +//输出:7 +//解释:因为路径 1→3→1→1→1 的总和最小。 +// +// +// 示例 2: +// +// +//输入:grid = [[1,2,3],[4,5,6]] +//输出:12 +// +// +// +// +// 提示: +// +// +// m == grid.length +// n == grid[i].length +// 1 <= m, n <= 200 +// 0 <= grid[i][j] <= 100 +// +// Related Topics 数组 动态规划 +// 👍 721 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class 最小路径和 { + // 题目链接:https://leetcode-cn.com/problems/minimum-path-sum/solution/zui-xiao-lu-jing-he-by-leetcode-solution/ + public int minPathSum(int[][] grid) { +// return func1(grid); + return func2(grid); + } + + /** + * 方法二:一维dp + * + * @param grid 原始二维矩阵 + * @return 返回最小路径和 + */ + private int func2(int[][] grid) { + // 拿到的矩阵的行数 + int row = grid.length; + // 拿到矩阵的列数 + int col = grid[0].length; + + // 开dp数组 + int[] dp = new int[col]; + + // 初始化dp + dp[0] = grid[0][0]; + for (int i = 1; i < col; i++) { + dp[i] = dp[i - 1] + grid[0][i]; + } + + // 循环进行dp + for (int i = 1; i < row; i++) { + // 每次先初始化第一列,因为第一列一定是前一行的第一列加上当前行的第一列 + dp[0] += grid[i][0]; + for (int j = 1; j < col; j++) { + // 现在的dp[j - 1]就是真正的当前行的前一列的最小路径和,而dp[j]现在表示的是上一行当前列的最小路径和,更新过后才变成当前行当前列的最小路径和。 + dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; + } + } + + return dp[col - 1]; + } + + /** + * 方法一:二维dp + * + * @param grid 原始二维矩阵 + * @return 返回最小路径 + */ + private int func1(int[][] grid) { + // 拿到的矩阵的行数 + int row = grid.length; + // 拿到矩阵的列数 + int col = grid[0].length; + + // 开dp数组 + int[][] dp = new int[row][col]; + + // 循环进行dp + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + // 1. 如果i,j都是0,证明是左上角第一个元素,那它的最小路径和就是他自己所在格子的数字本身 + // 2. 如果i为0,j不为0,证明在第一行,而第一行非左上角元素的最小路径和只取决于这一行的前一个元素(因为第一行的他只能从他左边的元素走过来) + // 3. 如果i不为0,j为0,证明在第一列,而第一列非左上角元素的最小路径和只取决于这一列的前一个元素。 + // 4. 如果i,j都不为0,那么最小路径和就是左边的格子和上面的格子的最小值加上当前格子的数值 + if (i == 0 && j == 0) { + dp[i][j] = grid[i][j]; + } else if (i == 0) { + dp[i][j] = dp[i][j - 1] + grid[i][j]; + } else if (j == 0) { + dp[i][j] = dp[i - 1][j] + grid[i][j]; + } else { + dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]; + } + } + } + + return dp[row - 1][col - 1]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/minimum-window-substring.java b/week06/minimum-window-substring.java new file mode 100644 index 00000000..5ceb2a32 --- /dev/null +++ b/week06/minimum-window-substring.java @@ -0,0 +1,83 @@ +//给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。 +// +// 注意:如果 s 中存在这样的子串,我们保证它是唯一的答案。 +// +// +// +// 示例 1: +// +// +//输入:s = "ADOBECODEBANC", t = "ABC" +//输出:"BANC" +// +// +// 示例 2: +// +// +//输入:s = "a", t = "a" +//输出:"a" +// +// +// +// +// 提示: +// +// +// 1 <= s.length, t.length <= 105 +// s 和 t 由英文字母组成 +// +// +// +//进阶:你能设计一个在 o(n) 时间内解决此问题的算法吗? Related Topics 哈希表 双指针 字符串 Sliding Window +// 👍 843 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/minimum-window-substring/ + Map ori = new HashMap(); + Map cnt = new HashMap(); + + public String minWindow(String s, String t) { + int tLen = t.length(); + for (int i = 0; i < tLen; i++) { + char c = t.charAt(i); + ori.put(c, ori.getOrDefault(c, 0) + 1); + } + int l = 0, r = -1; + int len = Integer.MAX_VALUE, ansL = -1, ansR = -1; + int sLen = s.length(); + while (r < sLen) { + ++r; + if (r < sLen && ori.containsKey(s.charAt(r))) { + cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1); + } + while (check() && l <= r) { + if (r - l + 1 < len) { + len = r - l + 1; + ansL = l; + ansR = l + len; + } + if (ori.containsKey(s.charAt(l))) { + cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1); + } + ++l; + } + } + return ansL == -1 ? "" : s.substring(ansL, ansR); + } + + public boolean check() { + Iterator iter = ori.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = (Map.Entry) iter.next(); + Character key = (Character) entry.getKey(); + Integer val = (Integer) entry.getValue(); + if (cnt.getOrDefault(key, 0) < val) { + return false; + } + } + return true; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/palindromic-substrings.java b/week06/palindromic-substrings.java new file mode 100644 index 00000000..7b66aec3 --- /dev/null +++ b/week06/palindromic-substrings.java @@ -0,0 +1,120 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。 +// +// 具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。 +// +// +// +// 示例 1: +// +// 输入:"abc" +//输出:3 +//解释:三个回文子串: "a", "b", "c" +// +// +// 示例 2: +// +// 输入:"aaa" +//输出:6 +//解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa" +// +// +// +// 提示: +// +// +// 输入的字符串长度不会超过 1000 。 +// +// Related Topics 字符串 动态规划 +// 👍 441 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { +// public static void main(String[] args) { +// Solution2 s= new Solution2(); +// s.countSubstrings("aaa"); +// } + // 题目链接:https://leetcode-cn.com/problems/palindromic-substrings/ + public int countSubstrings(String s) { + // 时间复杂度:O(N^2),两层循环 + // 空间复杂度:O(1),只用了常数级的空间 +// return func1(s); + + // 时间复杂度:O(N^2),虽然有三层循环,但实际上第二层循环最多遍历2次,所以实际上还是两层循环 + // 空间复杂度:O(1),只用了常数级的空间 + return func2(s); + + } + + /** + * 思路二:思路一的变种,更好理解 + * + * @param s 原字符串 + * @return 最大回文子串数 + */ + private int func2(String s) { + // 定义结果 + int res = 0; + // 获取字符串长度 + int len = s.length(); + + int temp = 2; + for (int i = 0; i < len; i++) { + int right; + + if (i == len - 1) { + temp = 1; + } + // 回文中心只有可能是奇数个或者偶数个 + // 如果是奇数个,那么就是一个字符作为回文中心 + // 如果是偶数个,那么就是两个字符作为回文中心 + for (int j = 0; j < temp; j++) { + int left = i; + right = i + j; + + while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) { + --left; + ++right; + ++res; + } + } + } + + return res; + } + + /** + * 思路一:找回文中心 + * + * @param s 原字符串 + * @return 最大回文子串数量 + */ + private int func1(String s) { + // 记录结果集 + int res = 0; + // 记录长度 + int len = s.length(); + // 遍历找出所有的回文中心点 + // 这里为什么是i<2*len-1,因为你想想,你的回文子串的回文中心点最大的可能性就是在len-1位置 + // 也就是最后一个字符自己作为子串他是一个回文的。(len - 1) * 2刚好是2len - 2。*2是因为我们要 + // 筛选出奇数情况的回文中心以及偶数情况的回文中心 + for (int i = 0; i < 2 * len - 1; i++) { + // 得出回文左中心 + int left = i / 2; + // 得出回文右中心 + int right = left + i % 2; + + // 如果满足回文要求 + while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) { + // 左指针往左移动,右指针往右移动,结果加一 + --left; + ++right; + ++res; + } + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/split-array-largest-sum.java b/week06/split-array-largest-sum.java new file mode 100644 index 00000000..211b5690 --- /dev/null +++ b/week06/split-array-largest-sum.java @@ -0,0 +1,85 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。 +// +// 注意: +//数组长度 n 满足以下条件: +// +// +// 1 ≤ n ≤ 1000 +// 1 ≤ m ≤ min(50, n) +// +// +// 示例: +// +// +//输入: +//nums = [7,2,5,10,8] +//m = 2 +// +//输出: +//18 +// +//解释: +//一共有四种方法将nums分割为2个子数组。 +//其中最好的方式是将其分为[7,2,5] 和 [10,8], +//因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。 +// +// Related Topics 二分查找 动态规划 +// 👍 382 👎 0 + + +import java.util.Arrays; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/split-array-largest-sum/ + public int splitArray(int[] nums, int m) { + // 时间复杂度:O(N^2 * M),3层for循环 + // 空间复杂度:O(N^2),二维dp数组 + return func1(nums, m); + } + + private int func1(int[] nums, int m) { + // 1:找重复性 + // 2. 定义状态数组:f[i][j],表示将数组的前i个数分割为j段所能够得到的最大连续子数组和的最小值 + // 3. DP方程:f[i][j] = min{max{f[k][j-1], sub[k+1][i]}},0<=k<=i-1 + + // 得到数组的长度 + int len = nums.length; + // 定义一个前缀和,preSum[i]=sum[0...i) + int[] preSum = new int[len + 1]; + // 初始化前缀和数组 + preSum[0] = 0; + for (int i = 0; i < len; i++) { + preSum[i + 1] = preSum[i] + nums[i]; + } + + // 申请dp数组 + int[][] dp = new int[len][m + 1]; + // 初始化dp数组,因为要找最小值,所以初值赋值一个很大的值即可 + for (int i = 0; i < len; i++) { + Arrays.fill(dp[i], Integer.MAX_VALUE); + } + + // 分割数为1的情况,即不分割的情况,所有的前缀和就是依次的状态值 + for (int i = 0; i < len; i++) { + dp[i][1] = preSum[i + 1]; + } + + // 从分割数为2开始递推 + for (int k = 2; k <= m; k++) { + // i是为了固定有多少个数字参与这件事情,层层网上累。分割成k段,至少要有k个元素吧。而i的下标从0开始, + // 0就是表示有一个元素的情况,1当然就是代表右两个元素的情况 + for (int i = k - 1; i < len; i++) { + // j表示第k - 1个区间的最后一个元素的下标,最小值是k-2,因为极端最小就是让前面的每一个数组占一段。最大值是len - 2,因为最后一个位置len-1极端情况你就让他作为第k段。前面的所有为k - 1段。 + // 比如,k=2时,第k-1(第1个)个区间的最后一个元素的下标是0,k = 3时,第k-1个(第2)个区间的最后一个元素是1,也就是nums[0],nums[1]分别为两段 + for (int j = k - 2; j < i; j++) { + dp[i][k] = Math.min(dp[i][k], Math.max(dp[j][k - 1], preSum[i + 1] - preSum[j + 1])); + } + } + } + + return dp[len - 1][m]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/student-attendance-record-ii.java b/week06/student-attendance-record-ii.java new file mode 100644 index 00000000..9f809436 --- /dev/null +++ b/week06/student-attendance-record-ii.java @@ -0,0 +1,58 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个正整数 n,返回长度为 n 的所有可被视为可奖励的出勤记录的数量。 答案可能非常大,你只需返回结果mod 109 + 7的值。 +// +// 学生出勤记录是只包含以下三个字符的字符串: +// +// +// 'A' : Absent,缺勤 +// 'L' : Late,迟到 +// 'P' : Present,到场 +// +// +// 如果记录不包含多于一个'A'(缺勤)或超过两个连续的'L'(迟到),则该记录被视为可奖励的。 +// +// 示例 1: +// +// +//输入: n = 2 +//输出: 8 +//解释: +//有8个长度为2的记录将被视为可奖励: +//"PP" , "AP", "PA", "LP", "PL", "AL", "LA", "LL" +//只有"AA"不会被视为可奖励,因为缺勤次数超过一次。 +// +// 注意:n 的值不会超过100000。 +// Related Topics 动态规划 +// 👍 111 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + int count,M=1000000007; + // 题目链接:https://leetcode-cn.com/problems/student-attendance-record-ii/ + public int checkRecord(int n) { + count = 0; + gen("", n); + return count; + } + + public void gen(String s, int n) { + if (n == 0 && checkRecord(s)) { + count=(count+1)%M; + } else if (n > 0) { + gen(s + "A", n - 1); + gen(s + "P", n - 1); + gen(s + "L", n - 1); + } + } + public boolean checkRecord(String s) { + int count = 0; + for (int i = 0; i < s.length() && count < 2; i++) { + if (s.charAt(i) == 'A') { + count++; + } + } + return s.length() > 0 && count < 2 && s.indexOf("LLL") < 0; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/task-scheduler.java b/week06/task-scheduler.java new file mode 100644 index 00000000..bb3dcce6 --- /dev/null +++ b/week06/task-scheduler.java @@ -0,0 +1,110 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个用字符数组表示的 CPU 需要执行的任务列表。其中包含使用大写的 A - Z 字母表示的26 种不同种类的任务。任务可以以任意顺序执行,并且每个任务 +//都可以在 1 个单位时间内执行完。CPU 在任何一个单位时间内都可以执行一个任务,或者在待命状态。 +// +// 然而,两个相同种类的任务之间必须有长度为 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。 +// +// 你需要计算完成所有任务所需要的最短时间。 +// +// +// +// 示例 : +// +// 输入:tasks = ["A","A","A","B","B","B"], n = 2 +//输出:8 +//解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B. +// 在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。 +// +// +// +// 提示: +// +// +// 任务的总个数为 [1, 10000]。 +// n 的取值范围为 [0, 100]。 +// +// Related Topics 贪心算法 队列 数组 +// 👍 429 👎 0 + + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/task-scheduler/ + public int leastInterval(char[] tasks, int n) { + // 时间复杂度:O(time),因为每个任务都安排了时间,所以最终的答案就是时间复杂度 + // 空间复杂度:O(N), 申请了一个优先队列 + return func1(tasks, n); + } + + /** + * 思路一:优先队列 + * + * @param tasks 任务数组 + * @param n 冷却时间 + * @return 执行时间 + */ + private int func1(char[] tasks, int n) { + // 直观分析,应该让数量多的任务先执行,我们用一个优先队列来模拟 + + // 因为任务用字母表示,而因为字母只有26个,所以申请一个26大小的数组即可,将字母映射为数字 + int[] map = new int[26]; + // 遍历tasks,初始化map数组 + for (char c : tasks) { + map[c - 'A']++; + } + + // 申请一个优先队列, 并且是大根堆,因为之前分析了,数量多的任务应该先执行 + PriorityQueue queue = new PriorityQueue<>(26, Collections.reverseOrder()); + + // 遍历map数组,将映射为数字的任务加入到优先队列中 + for (int i : map) { + if (i > 0) { + queue.offer(i); + } + } + + // 初始化完成任务所需要的的时间 + int time = 0; + + // 当队列非空的时候,证明任务没做完,循环去做任务 + while (! queue.isEmpty()) { + // 定义一个变量,用于控制冷却时间 + int i = 0; + // 定义一个列表用以记录每个任务还剩下的次数。每次拿出一个数字,我们都将其减一。这样用来代表每个任务还剩下的次数 + List taskTimes = new ArrayList<>(); + // 当i小于冷却时间时,继续进入以下循环 + while (i <= n) { + // 如果队列不为空 + if (!queue.isEmpty()) { + // 如果队头元素大于1 + if (queue.peek() > 1) { + // 将其减一再加入临时的一个任务列表里,代表这个任务数量被减一了 + taskTimes.add(queue.poll() - 1); + } else { + queue.poll(); + } + } + + // 执行任务的时间加一 + ++time; + // 当前的冷却时间也加一 + ++i; + + // 如果队列为空,并且临时任务列表也为空了,那么代表所有的任务都做完了。 + if (queue.isEmpty() && taskTimes.isEmpty()) { + break; + } + } + + // 一轮冷却时间过了,要重新新一轮的任务,将刚刚保存到临时任务列表的值再次加入到优先队列中 + for (int value : taskTimes) { + queue.offer(value); + } + } + + return time; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/triangle.java b/week06/triangle.java new file mode 100644 index 00000000..4af14bac --- /dev/null +++ b/week06/triangle.java @@ -0,0 +1,90 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 +// +// 相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。 +// +// +// +// 例如,给定三角形: +// +// [ +// [2], +// [3,4], +// [6,5,7], +// [4,1,8,3] +//] +// +// +// 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。 +// +// +// +// 说明: +// +// 如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分。 +// Related Topics 数组 动态规划 +// 👍 647 👎 0 + + +import java.util.Arrays; +import java.util.List; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/triangle/description/ + public int minimumTotal(List> triangle) { + return func1(triangle); + + + } + + /** + * 思路一:一维dp + * + * @param triangle 三角形列表 + * @return 最小路径和 + */ + private int func1(List> triangle) { + // 1. 分治子问题,找重复性。每一层除了第一个节点和最后一个节点,其他节点都是可以由上一层的第i个和第i-1个走过来。这就是重复性 + // 2. 定义状态数组 + // 3. DP方程:dp[i] = MIN {dp[i - 1], dp[i]} + triangle[j][i] + + /** + * 时间复杂度: O(N^2),双层for循环,所以空间复杂度为O(N^2) + * 空间复杂度:O(N), dp一维数组所占空间为三角形的行数 + */ + + // 获取三角形的行数, 行数和最大的列数其实是相等的 + int row = triangle.size(); + // 申请dp数组 + int[] dp = new int[row]; + + // 第一层只有一个元素,直接初始化dp[0]即可 + dp[0] = triangle.get(0).get(0); + + // 从第二层开始遍历 + for (int i = 1; i < row; i++) { +// int col = triangle.get(i).size(); + // 这里需要注意必须从后往前遍历,否则我们前面的先更新了,就会影响后面的值 + // 特殊处理三角形每一层最后一个 + dp[i] = dp[i - 1] + triangle.get(i).get(i); + for (int j = i - 1; j > 0; j--) { + // 非三角形最后一个和第一个的情况,最小路径和是取决于上一层的第j-1个和第j个 + // 这里的dp[j - 1]就是上一层的第j-1个位置的数,dp[j]就是上一层的第j个位置的数 + dp[j] = Math.min(dp[j - 1], dp[j]) + triangle.get(i).get(j); + } + // 特殊处理三角形每一层的第一个 + dp[0] += triangle.get(i).get(0); + } + +// Arrays.sort(dp); + + // 下面这段代码是为了找出最小值 + int minNum = dp[0]; + for (int i = 1; i < row; i++) { + minNum = Math.min(minNum, dp[i]); + } + return minNum; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/unique-paths-ii.java b/week06/unique-paths-ii.java new file mode 100644 index 00000000..c68a511f --- /dev/null +++ b/week06/unique-paths-ii.java @@ -0,0 +1,137 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 +// +// 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 +// +// 现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? +// +// +// +// 网格中的障碍物和空位置分别用 1 和 0 来表示。 +// +// +// +// 示例 1: +// +// +//输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] +//输出:2 +//解释: +//3x3 网格的正中间有一个障碍物。 +//从左上角到右下角一共有 2 条不同的路径: +//1. 向右 -> 向右 -> 向下 -> 向下 +//2. 向下 -> 向下 -> 向右 -> 向右 +// +// +// 示例 2: +// +// +//输入:obstacleGrid = [[0,1],[0,0]] +//输出:1 +// +// +// +// +// 提示: +// +// +// m == obstacleGrid.length +// n == obstacleGrid[i].length +// 1 <= m, n <= 100 +// obstacleGrid[i][j] 为 0 或 1 +// +// Related Topics 数组 动态规划 +// 👍 447 👎 0 + + +import java.util.Arrays; + +//leetcode submit region begin(Prohibit modification and deletion) +class unique-paths-ii { + // 题目链接:https://leetcode-cn.com/problems/unique-paths-ii/ + public int uniquePathsWithObstacles(int[][] obstacleGrid) { +// return func1(obstacleGrid); + return func2(obstacleGrid); + } + + /** + * 思路二:二维DP + * + * @param obstacleGrid 障碍物矩阵 + * @return 不同路径数 + */ + private int func2(int[][] obstacleGrid) { + // 1. 分治子问题:每一个格子实际上是由他上面的格子和左边的格子决定,障碍物的格子设置为0即可 + // 2. 定义状态数组,dp[i], 表示当前层的第i列的不同路径数 + // 3. DP方程:空地 dp[i] += dp[i - 1] 障碍物 dp[i] = 0 + + // 获取矩阵的行 + int row = obstacleGrid.length; + // 获取矩阵的列 + int col = obstacleGrid[0].length; + + // 申请dp数组 + int[] dp = new int[col]; + + // 初始化第一个数据 + dp[0] = 1; + + // 循环dp + // 由于第0行刚刚已经初始化了,所以从第一行开始遍历 + for (int i = 0; i < row; i++) { + // 当前层的dp[0]有可能是石头,所以要判断一下,如果是0,将其赋值成0 +// if (obstacleGrid[i][0] == 1) { +// dp[0] = 0; +// } + for (int j = 0; j < col; j++) { + if (obstacleGrid[i][j] == 1) { + dp[j] = 0; + } else if (j > 0){ // 当前格子不是障碍物,j也不大于0,证明是第一列非障碍物,这种格子我们不用做任何改变 + // 这里的dp[j - 1]就相当于左边的不同路径,这里右边的dp[j]就相当于上一层第j列的不同路径 + dp[j] = dp[j] + dp[j - 1]; + } + } + } + + return dp[col - 1]; + } + + /** + * 思路一:二维dp + * + * @param obstacleGrid 障碍物详情的二维矩阵 + * @return 不同路径数 + */ + private int func1(int[][] obstacleGrid) { + // 1. 分治子问题,每一个格子实际上还是只由他上面的格子和他下面的格子决定,只不过多了障碍物,那有障碍物的格子他的不同路径数就是0即可 + // 2. 定义状态数组:dp[i][j],表示第i行第j列的不同路径数 + // 3. DP方程: 空地:dp[i][j] = dp[i][j - 1] + dp[i - 1][j]。 障碍物:dp[i][j] = 0 + + // 获取矩阵的行 + int row = obstacleGrid.length; + // 获取矩阵的列 + int col = obstacleGrid[0].length; + + // 申请dp数组 + int[][] dp = new int[row + 1][col + 1]; + + // 初始化, 因为dp数组故意多申请了一行一列,多的一行一列就是为了来适配第一行第一列没有左没有上的情况的 + dp[0][1] = 1; + + // 循环dp + for (int i = 1; i <= row; i++) { + for (int j = 1; j <= col; j++) { + // 如果有障碍物,当前格子的不同路径数为0,因为不可能有一种走法能走到石头上 + // 否则,就代表是空地,利用我们的dp方程即可 + if (obstacleGrid[i - 1][j - 1] == 1) { + dp[i][j] = 0; + } else { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + } + + return dp[row][col]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week06/unique-paths.java b/week06/unique-paths.java new file mode 100644 index 00000000..e5a6a273 --- /dev/null +++ b/week06/unique-paths.java @@ -0,0 +1,120 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 +// +// 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 +// +// 问总共有多少条不同的路径? +// +// +// +// 例如,上图是一个7 x 3 的网格。有多少可能的路径? +// +// +// +// 示例 1: +// +// 输入: m = 3, n = 2 +//输出: 3 +//解释: +//从左上角开始,总共有 3 条路径可以到达右下角。 +//1. 向右 -> 向右 -> 向下 +//2. 向右 -> 向下 -> 向右 +//3. 向下 -> 向右 -> 向右 +// +// +// 示例 2: +// +// 输入: m = 7, n = 3 +//输出: 28 +// +// +// +// 提示: +// +// +// 1 <= m, n <= 100 +// 题目数据保证答案小于等于 2 * 10 ^ 9 +// +// Related Topics 数组 动态规划 +// 👍 754 👎 0 + + +import java.util.Arrays; + +//leetcode submit region begin(Prohibit modification and deletion) +class unique-paths { + // 题目链接:https://leetcode-cn.com/problems/unique-paths/ + public int uniquePaths(int m, int n) { +// return func1(m, n); + return func2(m, n); + } + + /** + * 思路二:一维dp + * + * @param m 行数 + * @param n 列数 + * @return 不同路径数 + */ + private int func2(int m, int n) { + // 1. 分治子问题:每一个格子实际上能够有的路径数就是他左边的路径数加上他上面的路径数,因为他只有可能从左边或者上边走下来。 + // 2. 定义状态数组:dp[i],表示当前行第i列的格子的不同路径数目 + // 3. DP方程:dp[i] = dp[i - 1] + dp[i] + + // 申请dp数组 + int[] dp = new int[n]; + + // 将第一行的每一列初始化为1 +// Arrays.fill(dp, 1); + + // 将第一行的第一列初始化即可 + dp[0] = 1; + + // 循环dp + for (int i = 0; i < m; i++) { + // dp[0]永远表示的第一列的值,所以不用改变 + for (int j = 1; j < n; j++) { + // 这里的dp[j - 1]就相当于左边的不同路径,这里右边的dp[j]就相当于上一层第j列的不同路径 + dp[j] = dp[j - 1] + dp[j]; + } + } + + return dp[n - 1]; + } + + /** + * 思路一:二维dp + * + * @param m 行数 + * @param n 列数 + * @return 不同路径数 + */ + private int func1(int m, int n) { + // 1. 分治子问题:每一个格子实际上能够有的路径数就是他左边的路径数加上他上面的路径数,因为他只有可能从左边或者上边走下来。 + // 2. 定义状态数组:dp[i][j],表示第i行第j列的格子的不同路径数目 + // 3. DP方程:dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + + // 申请dp数组 + int[][] dp = new int[m][n]; + + // 初始化第一行,第一行所有的格子都只有一条路径 + for (int i = 0; i < n; i++) { + dp[0][i] = 1; + } + + // 初始化第一列,第一列所有的格子也只有一条路径 + for (int i = 1; i < m; i++) { + dp[i][0] = 1; + } + + // 循环进行dp + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + + return dp[m - 1][n - 1]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/NOTE.md b/week07/NOTE.md index 50de3041..e69d198a 100644 --- a/week07/NOTE.md +++ b/week07/NOTE.md @@ -1 +1,141 @@ -学习笔记 \ No newline at end of file +学习笔记 + +### 一. 字典树模板代码 + +```java +class TrieNode { + private TrieNode[] trieNode; + + private final int len = 26; + + private boolean isEnd = false; + + public TrieNode() { + trieNode = new TrieNode[len]; + } + + public boolean containsKey(char ch) { + if (trieNode[ch - 'a'] == null) { + return false; + } + return true; + } + + public void put(char ch, TrieNode node) { + trieNode[ch - 'a'] = node; + } + + public TrieNode get(char ch) { + return trieNode[ch - 'a']; + } + + public void setEnd() { + isEnd = true; + } + + public boolean getEnd() { + return isEnd; + } +} +``` + +```java +class Trie { + public TrieNode root; + + /** Initialize your data structure here. */ + public Trie() { + root = new TrieNode(); + } + + private void insertWord(String word) { + TrieNode node = root; + char[] wordArray = word.toCharArray(); + for (int i = 0; i < wordArray.length; i++) { + char currentWord = wordArray[i]; + if (!node.containsKey(currentWord)) { + node.put(currentWord, new TrieNode()); + } + node = node.get(currentWord); + } + node.setEnd(); + } + + private boolean search(String word) { + TrieNode node = searchPrefix(word); + return node != null && node.isEnd(); + } + + private boolean startsWith(String word) { + TrieNode node = searchPrefix(word); + return node != null; + } + + private TrieNode searchPrefix(String word) { + TrieNode node = root; + char[] wordArray = word.toCharArray(); + for (int i = 0; i < wordArray.length; i++) { + char currentWord = wordArray[i]; + if (node.containsKey(currentWord)) { + node = node.get(currentWord); + } else { + return null; + } + } + + return node; + } +} +``` + + + +### 二. 并查集代码模板 + +```java +class UnionFind { + int count; + int[] parent; + + public UnionFind(int n) { + count = n; + parent = new int[n]; + + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + public int find(int n) { + int p = n; + while (p != parent[p]) { + parent[p] = parent[parent[p]]; + p = parent[p]; + } + // 路径压缩 + while (n != parent[n]) { + int temp = parent[n]; + parent[n] = p; + n = temp; + } + return p; + } + + public void union(int x, int y) { + int rootx = find(x); + int rooty = find(y); + + if (rootx == rooty) { + return; + } + parent[rootx] = rooty; + count--; + } + + public int getCount() { + return count; + } +} +``` + +这两个周真的感觉自己收获特别多,做了很多题目,的的确确发现过遍数之后对于一些算法,题目的理解不知道清晰了多少倍,而且这两周都是一些高阶的算法和数据结构,这种成长的喜悦是其他任何事情都无可替代的,谢谢超哥和助教老师一直以来的帮忙和辛劳。真的谢谢。 \ No newline at end of file diff --git a/week07/climbing-stairs.java b/week07/climbing-stairs.java new file mode 100644 index 00000000..c1e6d561 --- /dev/null +++ b/week07/climbing-stairs.java @@ -0,0 +1,136 @@ +package Algorithm.leetcode.leetcode.editor.cn;//假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 +// +// 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? +// +// 注意:给定 n 是一个正整数。 +// +// 示例 1: +// +// 输入: 2 +//输出: 2 +//解释: 有两种方法可以爬到楼顶。 +//1. 1 阶 + 1 阶 +//2. 2 阶 +// +// 示例 2: +// +// 输入: 3 +//输出: 3 +//解释: 有三种方法可以爬到楼顶。 +//1. 1 阶 + 1 阶 + 1 阶 +//2. 1 阶 + 2 阶 +//3. 2 阶 + 1 阶 +// +// Related Topics 动态规划 +// 👍 1201 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class climbing-stairs { + // 题目链接:https://leetcode-cn.com/problems/climbing-stairs/ + public int climbStairs(int n) { +// return func1(n); + + + // 递归避免重复记录 +// return func3(n); + + // 自底向上 +// return func2(n); + + // 动态规划 + return func4(n); + } + + /** + * 思路四:动态规划 + * + * 时间复杂度:O(N) + * 空间复杂度:O(1) + * @param n 楼梯数量 + * @return 返回有多少种跳法 + */ + private int func4(int n) { + int p = 0, q = 0, r = 1; + for (int i = 1; i <= n; i++) { + p = q; + q = r; + r = p + q; + } + + return r; + } + + /** + * 思路三:记录每次递归的时候得到的值,避免重复计算 + * + * 时间复杂度:O(N) + * 空间复杂度:O(N) + * @param n 楼梯数量 + * @return 返回有多少种跳法 + */ + private int func3(int n) { + int[] a = new int[n + 1]; + return climb(n, a); + } + + /** + * 思路二:自底向上 + * + * 时间复杂度:(n) + * 空间复杂度:O(N) + * @param n 楼梯数量 + * @return 返回有多少种跳法 + */ + private int func2(int n) { + int[] array = new int[n + 1]; + if (n == 1) { + return 1; + } + + if (n == 2) { + return 2; + } + array[1] = 1; + array[2] = 2; + for (int i = 3; i < n + 1; i++) { + array[i] = array[i - 1] + array[i - 2]; + } + return array[n]; + } + + /** + * 思路一:傻递归 + * + * 时间复杂度:指数级别(2^n) + * 空间复杂度:和递归的深度有关:logN + */ + private int func1(int n) { +// 递归 + if (n == 1) { + return 1; + } + + if (n == 2) { + return 2; + } + + return climbStairs(n - 1) + climbStairs(n - 2); + } + + public int climb(int n, int[] a) { + if (n == 0) { + return 1; + } + if (n == 1) { + return 1; + } + if (a[n] > 0) { + return a[n]; + } + + a[n] = climb(n - 1, a) + climb(n - 2, a); + return a[n]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/friend-circles.java b/week07/friend-circles.java new file mode 100644 index 00000000..94ce70ef --- /dev/null +++ b/week07/friend-circles.java @@ -0,0 +1,129 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C +//的朋友。所谓的朋友圈,是指所有朋友的集合。 +// +// 给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你 +//必须输出所有学生中的已知的朋友圈总数。 +// +// +// +// 示例 1: +// +// 输入: +//[[1,1,0], +// [1,1,0], +// [0,0,1]] +//输出:2 +//解释:已知学生 0 和学生 1 互为朋友,他们在一个朋友圈。 +//第2个学生自己在一个朋友圈。所以返回 2 。 +// +// +// 示例 2: +// +// 输入: +//[[1,1,0], +// [1,1,1], +// [0,1,1]] +//输出:1 +//解释:已知学生 0 和学生 1 互为朋友,学生 1 和学生 2 互为朋友,所以学生 0 和学生 2 也是朋友,所以他们三个在一个朋友圈,返回 1 。 +// +// +// +// +// 提示: +// +// +// 1 <= N <= 200 +// M[i][i] == 1 +// M[i][j] == M[j][i] +// +// Related Topics 深度优先搜索 并查集 +// 👍 379 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class UnionFind { + int count; + int[] parent; + + public UnionFind(int n) { + count = n; + parent = new int[n]; + + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + public int find(int n) { + int p = n; + while (p != parent[p]) { + parent[p] = parent[parent[p]]; + p = parent[p]; + } + // 路径压缩 + while (n != parent[n]) { + int temp = parent[n]; + parent[n] = p; + n = temp; + } + return p; + } + + public void union(int x, int y) { + int rootx = find(x); + int rooty = find(y); + + if (rootx == rooty) { + return; + } + parent[rootx] = rooty; + count--; + } + + public int getCount() { + return count; + } +} + +class Solution { + // 题目链接:https://leetcode-cn.com/problems/friend-circles/ + int rows = 0; + int cols = 0; + public int findCircleNum(int[][] M) { + return func1(M); + } + + /** + * 思路一:并查集 + * + * 时间复杂度:O(N^2),两层for循环 + * 空间复杂度:O(N),parent数组的大小 + * @param M 朋友圈矩阵 + * @return 朋友圈数量 + */ + private int func1(int[][] M) { + rows = M.length; + if (rows == 0) { + return 0; + } + cols = M[0].length; + + // 这里要特别注意,不是row*cols,因为人只有那么多个人。这里并查集里的容量代表的是人的数量 + UnionFind unionFind = new UnionFind(rows); + + int[][] directions = {{1, 0}, {0, 1}}; + + for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + // 如果M[i][j]==1,代表第i个人和第j个人是朋友(并且i!=j,i==j代表他和他自己是朋友,这当然不用union了),那么我们就union,将其加入到朋友圈即可。 + if (M[i][j] == 1 && i != j) { + unionFind.union(i, j); + } + } + } + + return unionFind.getCount(); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/generate-parentheses.java b/week07/generate-parentheses.java new file mode 100644 index 00000000..a69d7f26 --- /dev/null +++ b/week07/generate-parentheses.java @@ -0,0 +1,67 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 +// +// +// +// 示例: +// +// 输入:n = 3 +//输出:[ +// "((()))", +// "(()())", +// "(())()", +// "()(())", +// "()()()" +// ] +// +// Related Topics 字符串 回溯算法 +// 👍 1325 👎 0 + + +import java.util.ArrayList; +import java.util.List; + +//leetcode submit region begin(Prohibit modification and deletion) +class generate-parentheses { + // 题目链接:https://leetcode-cn.com/problems/generate-parentheses/ + + public List result = new ArrayList<>(); + public List generateParenthesis(int n) { + helper2(0, 0, n, ""); + return result; + } + + public void helper2(int left, int right, int n, String s) { + if (left == right) { + result.add(s); + return; + } + + if (left < n) { + helper2(left + 1, right, n, s + "("); + } + + if (right < left) { + helper2(left, right + 1, n, s + ")"); + } + } + + + + + public void helper(int left, int right, int n, String str) { + if (left == n && right == n) { + result.add(str); + return; + } + + if (left < n) { + helper(left + 1, right, n, str + "("); + } + + if (right < left) { + helper(left, right + 1, n, str + ")"); + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/implement-trie-prefix-tree.java b/week07/implement-trie-prefix-tree.java new file mode 100644 index 00000000..4874afc3 --- /dev/null +++ b/week07/implement-trie-prefix-tree.java @@ -0,0 +1,146 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。 +// +// 示例: +// +// Trie trie = new Trie(); +// +//trie.insert("apple"); +//trie.search("apple"); // 返回 true +//trie.search("app"); // 返回 false +//trie.startsWith("app"); // 返回 true +//trie.insert("app"); +//trie.search("app"); // 返回 true +// +// 说明: +// +// +// 你可以假设所有的输入都是由小写字母 a-z 构成的。 +// 保证所有输入均为非空字符串。 +// +// Related Topics 设计 字典树 +// 👍 472 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Trie { + // 题目链接:https://leetcode-cn.com/problems/implement-trie-prefix-tree/ + + private TrieNode root; + + /** Initialize your data structure here. */ + public Trie() { + root = new TrieNode(); + } + + /** Inserts a word into the trie. */ + public void insert(String word) { + // 拿到树的根节点 + TrieNode node = root; + // 将字符串转化为字符数组,方便遍历 + char[] wordArray = word.toCharArray(); + for (int i = 0; i < word.length(); i++) { + char currentChar = wordArray[i]; + // 如果当前node节点也就是数组中没有这个字符,那么我们加入tries树即可 + if (! node.containsKey(currentChar)) { + node.put(currentChar, new TrieNode()); + } + + // 因为是插入,无论找得到还是找不到,都要往下面一层去。 + // 当前层找得到这个字符,往下面层去看后面的字符是否能插入进去 + // 当前层找不到这个字符,将它加入到当前层,再下探到下一层插入后面的字符 + node = node.get(currentChar); + } + // 插入完了,最后要将node的是否是最后一层的标志位 置为true + node.setEnd(); + } + + /** Returns if the word is in the trie. */ + public boolean search(String word) { + // 拿到树的根节点 + TrieNode node = root; + // 将字符串转化为字符数组,方便遍历 + char[] wordArray = word.toCharArray(); + + for (int i = 0; i < word.length(); i++) { + char currentWord = wordArray[i]; + // 如果当前层包含所对应的单词 + if (node.containsKey(currentWord)) { + // 则下探到下一层,也就是获取当前字符的儿子节点 + node = node.get(currentWord); + } else { + // 否则当前层不包含对应的字符,那么后面的也不用看了,当前tries树里不存在这个单词。 + return false; + } + } + + return node.isEnd(); + } + + /** Returns if there is any word in the trie that starts with the given prefix. */ + public boolean startsWith(String prefix) { + // 拿到树的根节点 + TrieNode node = root; + // 将字符串转化为字符数组,方便遍历 + char[] prefixArray = prefix.toCharArray(); + + for (int i = 0; i < prefixArray.length; i++) { + char currentWord = prefixArray[i]; + // 如果当前层有这个字符 + if (node.containsKey(currentWord)) { + // 下探到下一层 + node = node.get(currentWord); + } else { + // 否则,只要有一个在某一层找不到,他都不是前缀 + return false; + } + } + + return true; + } +} + +class TrieNode { + // 当前节点的所有儿子 + private TrieNode[] children; + // 26个字母,所以当前场景下,children数组的最大长度为26 + private final int R = 26; + + private boolean isEnd; + + public TrieNode() { + children = new TrieNode[R]; + } + + // 当前层是否包含字符 + public boolean containsKey(char ch) { + return children[ch - 'a'] != null; + } + + // 得到下一层的所有儿子节点 + public TrieNode get(char ch) { + return children[ch - 'a']; + } + + // 插入操作 + public void put(char ch, TrieNode node) { + children[ch - 'a'] = node; + } + + public void setEnd() { + isEnd = true; + } + + public boolean isEnd() { + return isEnd; + } +} + +/** + * Your Trie object will be instantiated and called as such: + * Trie obj = new Trie(); + * obj.insert(word); + * boolean param_2 = obj.search(word); + * boolean param_3 = obj.startsWith(prefix); + */ +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/minimum-genetic-mutation.java b/week07/minimum-genetic-mutation.java new file mode 100644 index 00000000..f9b3e8b7 --- /dev/null +++ b/week07/minimum-genetic-mutation.java @@ -0,0 +1,113 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//一条基因序列由一个带有8个字符的字符串表示,其中每个字符都属于 "A", "C", "G", "T"中的任意一个。 +// +// 假设我们要调查一个基因序列的变化。一次基因变化意味着这个基因序列中的一个字符发生了变化。 +// +// 例如,基因序列由"AACCGGTT" 变化至 "AACCGGTA" 即发生了一次基因变化。 +// +// 与此同时,每一次基因变化的结果,都需要是一个合法的基因串,即该结果属于一个基因库。 +// +// 现在给定3个参数 — start, end, bank,分别代表起始基因序列,目标基因序列及基因库,请找出能够使起始基因序列变化为目标基因序列所需的最少变 +//化次数。如果无法实现目标变化,请返回 -1。 +// +// 注意: +// +// +// 起始基因序列默认是合法的,但是它并不一定会出现在基因库中。 +// 所有的目标基因序列必须是合法的。 +// 假定起始基因序列与目标基因序列是不一样的。 +// +// +// 示例 1: +// +// +//start: "AACCGGTT" +//end: "AACCGGTA" +//bank: ["AACCGGTA"] +// +//返回值: 1 +// +// +// 示例 2: +// +// +//start: "AACCGGTT" +//end: "AAACGGTA" +//bank: ["AACCGGTA", "AACCGCTA", "AAACGGTA"] +// +//返回值: 2 +// +// +// 示例 3: +// +// +//start: "AAAAACCC" +//end: "AACCCCCC" +//bank: ["AAAACCCC", "AAACCCCC", "AACCCCCC"] +// +//返回值: 3 +// +// 👍 63 👎 0 + + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/minimum-genetic-mutation/ + public int minMutation(String start, String end, String[] bank) { + if (bank.length == 0 || !Arrays.asList(bank).contains(end)) { + return -1; + } + + // 初始化基因库的set,方便我们后面的比较 + Set bankSet = new HashSet<>(Arrays.asList(bank)); + + Queue queue = new LinkedList(); + queue.offer(start); + // 广度优先必须要设置一个visited来存储哪些节点是我们访问过的,不要重复访问 + Set visited = new HashSet<>(); + visited.add(start); + + int step = 0; + + while (!queue.isEmpty()) { + int size = queue.size(); + + for (int i = 0; i < size; i++) { + String currentGene = queue.poll(); + if (findPath(currentGene, end, bankSet, visited, queue)) { + return step + 1; + } + } + + ++step; + } + + return -1; + } + + public boolean findPath(String gene, String end, Set bankSet, Set visited, Queue queue) { + char[] geneArray = gene.toCharArray(); + + for (int i = 0; i < geneArray.length; i++) { + char currentGene = geneArray[i]; + for (char c = 'A'; c <= 'Z'; c++) { + geneArray[i] = c; + String newGene = new String(geneArray); + + if (newGene.equals(end)) { + return true; + } + + if (bankSet.contains(newGene) && !visited.contains(newGene)) { + queue.offer(newGene); + visited.add(newGene); + } + } + geneArray[i] = currentGene; + } + return false; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/n-queens.java b/week07/n-queens.java new file mode 100644 index 00000000..856a05e8 --- /dev/null +++ b/week07/n-queens.java @@ -0,0 +1,134 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 +// +// +// +// 上图为 8 皇后问题的一种解法。 +// +// 给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。 +// +// 每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 +// +// +// +// 示例: +// +// 输入:4 +//输出:[ +// [".Q..", // 解法 1 +// "...Q", +// "Q...", +// "..Q."], +// +// ["..Q.", // 解法 2 +// "Q...", +// "...Q", +// ".Q.."] +//] +//解释: 4 皇后问题存在两个不同的解法。 +// +// +// +// +// 提示: +// +// +// 皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。 +// +// Related Topics 回溯算法 +// 👍 685 👎 0 + + +import scala.Array; + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/n-queens/ + + // 皇后列的set + Set col = new HashSet<>(); + // 皇后右斜上的set + Set pie = new HashSet<>(); + // 皇后左斜上的set + Set na = new HashSet<>(); + // 定义结果集 + List> res = new ArrayList<>(); + public List> solveNQueens(int n) { + // 申请一个char棋盘 + char[][] chess= new char[n][n]; + // 初始化棋盘 + for (char[] everyRow : chess) { + // 初始状态棋盘每个地方都是. + Arrays.fill(everyRow, '.'); + } + + // 开始回溯遍历 + // 从第一行开始,如果符合条件则往下一层走(逻辑上即为下一行) + for (int i = 0; i < n; i++) { + // 第一层怎么放都是符合要求的,所以不用判断 + chess[0][i] = 'Q'; + // 将其加入到各个set集合中 + col.add(i); + pie.add(0 + i); + na.add(0 - i); + + dfs(chess, 1); + + // 将状态回退 + na.remove(0 - i); + pie.remove(0 + i); + col.remove(i); + chess[0][i] = '.'; + } + + return res; + } + + public void dfs(char[][] chess, int row) { + // 递归出口,也就是到达最后一行还有效的时候 + // 实际上是row在n-1的时候就到最后一层了,因为下标从0开始 + // 但是要最后一层也是满足条件的才行,所以最后一层满足条件进入的下一层 + // 就是第n层,也就是row == n的时候,代表我们找到了一组解 + if (row == chess.length) { + // 将当前chess加入到结果集中 + res.add(transfer(chess)); + return; + } + + // 如果没到最后一层,处理当前层逻辑,判断是否能够走到下一层 + // 当前层(当前行)的每一个字符进行遍历 + for (int i = 0; i < chess.length; i++) { + // 如果是有效的,也就是当前列的set,撇的set,呐的set都没有对应的元素 + if (!col.contains(i) && !pie.contains(row + i) && !na.contains(row - i)) { + // 将当前行,当前列的字符设置成皇后 + chess[row][i] = 'Q'; + // 更新col,pie,na这三个set + col.add(i); + pie.add(row + i); + na.add(row - i); + + // 递归遍历到下一层 + dfs(chess, row + 1); + + // 当前行的当前列递归回溯完了之后,要将状态回溯 + na.remove(row - i); + pie.remove(row + i); + col.remove(i); + chess[row][i] = '.'; + } + } + + // 这一层遍历完了,return到上一层,这里也不用我们手动去return,程序执行完自己就回到上一层了。 + } + + public List transfer(char[][] chess) { + List cString = new ArrayList<>(); + for (char[] every : chess) { + cString.add(new String(every)); + } + return cString; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/number-of-islands.java b/week07/number-of-islands.java new file mode 100644 index 00000000..0e446016 --- /dev/null +++ b/week07/number-of-islands.java @@ -0,0 +1,213 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 +// +// 岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 +// +// 此外,你可以假设该网格的四条边均被水包围。 +// +// +// +// 示例 1: +// +// +//输入:grid = [ +// ["1","1","1","1","0"], +// ["1","1","0","1","0"], +// ["1","1","0","0","0"], +// ["0","0","0","0","0"] +//] +//输出:1 +// +// +// 示例 2: +// +// +//输入:grid = [ +// ["1","1","0","0","0"], +// ["1","1","0","0","0"], +// ["0","0","1","0","0"], +// ["0","0","0","1","1"] +//] +//输出:3 +// +// +// +// +// 提示: +// +// +// m == grid.length +// n == grid[i].length +// 1 <= m, n <= 300 +// grid[i][j] 的值为 '0' 或 '1' +// +// Related Topics 深度优先搜索 广度优先搜索 并查集 +// 👍 852 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) + + + +class Solution { + // 自定义并查集 + class UnionFind { + // 连通分量的个数 + private int count; + // parent数组 + private int[] parent; + + public int getCount() { + return count; + } + + // 构造函数,用于初始化并查集,初始状态连通分量个数(count)就是传进来的n的值 + public UnionFind(int n) { + this.count = n; + parent = new int[n]; + // 初始化将每个元素的parent指向自己即可 + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + /** + * 找到某个元素的领头元素 + * @param n 要找n的领头元素 + * @return 返回领导元素值 + */ + public int find(int n) { + int p = n; + while (p != parent[p]) { + parent[p] = parent[parent[p]]; + p = parent[p]; + } + // 下面是状态压缩 + while (n != parent[n]) { + int temp = parent[n]; + parent[n] = p; + n = temp; + } + return p; + } + + /** + * 合并连通分量 + * @param x 找到x的领头元素 + * @param y 找到y的领头元素 + */ + public void union(int x, int y) { + int rootx = find(x); + int rooty = find(y); + + if (rootx == rooty) { + return; + } + + parent[rootx] = rooty; + // 合并之后连通分量-1 + count--; + } + } + // 题目链接:https://leetcode-cn.com/problems/number-of-islands/ + + // 定义返回结果 + int res = 0; + private int row; + private int col; + public int numIslands(char[][] grid) { +// return func1(grid); + // 边界条件 + if (grid[0].length == 0) { + return 0; + } + + row = grid.length; + col = grid[0].length; + + // 记录空地的数量 + int spaces = 0; + UnionFind unionFind = new UnionFind(row * col); + // 这里的方向变成了两个方向,其实我们只用从左上扫描到右下,也就是每次遍历只扫描右边元素和下面元素即可。 + // 因为假如左边元素或者上面元素和他是一个岛屿,我们是不会错过的,我们一定在上一次或者上一行遍历就已经将当前 + // 的元素与之加入到一个集合中去了。 + int[][] directions = {{1, 0}, {0, 1}}; + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + // 如果是空地 + if (grid[i][j] == '0') { + // 则空地数量加一 + spaces++; + } else { + // 否则,证明当前行列代表的是岛屿,那么我们就要向右向下探测是否同样是岛屿 + for (int[] direction : directions) { + int newx = i + direction[0]; + int newy = j + direction[1]; + // 如果满足条件并且是岛屿,将其和之前的岛屿加入到一个集合中 + if (newx < row && newy < col && grid[newx][newy] == '1') { + unionFind.union(getIndex(i, j), getIndex(newx, newy)); + } + } + } + } + } + return unionFind.getCount() - spaces; + } + + private int getIndex(int i, int j) { + return i * col + j; + } + + private int func1(char[][] grid) { + /** + * 方法一:递归 + * + * 时间复杂度: + * 空间复杂度: + */ + + // 判断边界条件 + if (grid == null || grid.length == 0) { + return res; + } + + // 用m, n记录二维数组的行数和列数,便于后面的遍历 + int m = grid.length; + int n = grid[0].length; + + // 遍历二维数组 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == '1') { + ++res; + // 进行递归遍历将整个岛屿从1变为0,以免重复的发现相同的岛屿 + dfs(i, j, grid); + } + } + } + + return res; + } + + public void dfs(int i, int j, char[][] grid) { + // dfs里面也需要拿到二维数组的行数和列数用以判断递归出口 + int m = grid.length; + int n = grid[0].length; + // 递归出口,如果行数比0小;行数大于等于数组行数;列数比0小,列数大于等于数组列数;或者当前位置的字符是0,直接return + if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] == '0') { + return; + } + + // 程序走到这里证明grid[i][j]一定是‘1’,所以我们先将这个值改成0,然后再进行递归 + grid[i][j] = '0'; + // 递归遍历他的上面 + dfs(i, j - 1, grid); + // 递归遍历他的下面 + dfs(i, j + 1, grid); + // 递归遍历他的左边 + dfs(i - 1, j, grid); + // 递归遍历他的右边 + dfs(i + 1, j, grid); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/sudoku-solver.java b/week07/sudoku-solver.java new file mode 100644 index 00000000..4062440c --- /dev/null +++ b/week07/sudoku-solver.java @@ -0,0 +1,184 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//编写一个程序,通过填充空格来解决数独问题。 +// +// 一个数独的解法需遵循如下规则: +// +// +// 数字 1-9 在每一行只能出现一次。 +// 数字 1-9 在每一列只能出现一次。 +// 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 +// +// +// 空白格用 '.' 表示。 +// +// +// +// 一个数独。 +// +// +// +// 答案被标成红色。 +// +// 提示: +// +// +// 给定的数独序列只包含数字 1-9 和字符 '.' 。 +// 你可以假设给定的数独只有唯一解。 +// 给定数独永远是 9x9 形式的。 +// +// Related Topics 哈希表 回溯算法 +// 👍 709 👎 0 + + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/sudoku-solver/#/description + // 记录当前行剩余可用数字 + Set[] rows = new HashSet[9]; + // 记录当前列剩余可用数字 + Set[] cols = new HashSet[9]; + // 记录当前块剩余可用数字 + Set[] blocks = new HashSet[9]; + // 定义一个需要填数字的列表,里面装了当前所有空位置的坐标 + List empty = new ArrayList<>(); + public void solveSudoku(char[][] board) { + // 方法一:超时 +// func1(board); + + // 初始化set + for (int i = 0; i < 9; i++) { + rows[i] = new HashSet<>(); + rows[i].addAll(Arrays.asList('1', '2', '3', '4', '5', '6', '7', '8', '9')); + + cols[i] = new HashSet<>(); + cols[i].addAll(Arrays.asList('1', '2', '3', '4', '5', '6', '7', '8', '9')); + + blocks[i] = new HashSet<>(); + blocks[i].addAll(Arrays.asList('1', '2', '3', '4', '5', '6', '7', '8', '9')); + } + + // 预扫描,这样就可以扫描出哪些位置是可以放数字的以及每个格子对应的还可以放的数字是哪些 + for (int i = 0; i < 9; i++) { + for (int j = 0; j < 9; j++) { + // 如果当前格子有数字 + if (board[i][j] != '.') { + char value = board[i][j]; + // 对应的集合里将这个数字去除掉 + rows[i].remove(value); + cols[j].remove(value); + blocks[3 * (i / 3) + j / 3].remove(value); + } else { + // 否则当前格子是空的,将其坐标加入到empty列表中 + empty.add(new int[]{i, j}); + } + } + } + + // 预处理完了之后开始递归回溯 + backtrack(0, board); + } + + /** + * 回溯 + * @param num 代表当前填的数字个数,初始状态当然是0 + * @return 是否是合理数独 + */ + public boolean backtrack(int num, char[][] board) { + // 递归出口,就是当前填的数字的个数和empty的size是一样的,就证明所有数字都填完了。 + // 这里注意实际上是填的个数和empty的size-1的时候就填满了,因为下标num从0开始填,但是当第num - 1个 + // 填进去之后如果是合理的才往下走,这个时候num-1变成num,才能证明所有的数字都是符合数独要求的 + if (num == empty.size()) { + return true; + } + + // 否则还没填完,要继续去填 + int[] index = empty.get(num); + int row = index[0]; + int col = index[1]; + int b = (row / 3) * 3 + col / 3; + + // 定义一个set,求几个set的交集也就是当前行,列,块中都还剩下的那些元素才是可以填过来的 + Set result = new HashSet<>(); + result.addAll(rows[row]); + result.retainAll(cols[col]); + result.retainAll(blocks[b]); + + // 遍历set,进行回溯,当前这个放进去看能不能填圆满,不能继续填下一个 + for (char c : result) { + // 当前元素要填进去的话,就要在对应的行,列,块中删除掉 + rows[row].remove(c); + cols[col].remove(c); + blocks[b].remove(c); + board[row][col] = c; + // 如果就这样下去是可以满足条件的 + if (backtrack(num + 1, board)) { + // 直接返回true + return true; + } + // 否则,证明当前这个格子不能填这个元素,要换其他的试试,这个时候要回退当前层状态 + rows[row].add(c); + cols[col].add(c); + blocks[b].add(c); + } + + // 如果for循环完了都没有得到一个true,证明都是不可以的返回false即可 + return false; + } + + private void func1(char[][] board) { + // 边界条件 + if (board.length == 0) { + return; + } + + solve(board); + } + + public boolean solve(char[][] board) { + for (int i = 0; i < board.length; i++) { + for (int j = 0; j < board[0].length; j++) { + // 如果当前棋盘的这个格子没有数字,证明是我们可以放格子的地方 + if (board[i][j] == '.') { + // 有数字1-9可以放 + for (char c = '1'; c <= '9'; c++) { + if (isValid(board, i, j, c)) { + board[i][j] = c; + if (solve(board)) { + return true; + } else { + board[i][j] = '.'; + } + } + } + } + } + } + return false; + } + + public boolean isValid(char[][] board, int row, int col, char c) { + for (int i = 0; i < 9; i++) { + // 检查行 + if (i != col && board[row][i] == c) { + return false; + } + + // 检查列 + if (i != row && board[i][col] == c) { + return false; + } + + // 检查block + if (board[3 * (row / 3) + i / 3][3 * (col / 3) + i % 3] != '.' + && board[3 * (row / 3) + i / 3][3 * (col / 3) + i % 3] == c) { + return false; + } + } + + return true; + } + +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/surrounded-regions.java b/week07/surrounded-regions.java new file mode 100644 index 00000000..5aa57787 --- /dev/null +++ b/week07/surrounded-regions.java @@ -0,0 +1,114 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。 +// +// 找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 +// +// 示例: +// +// X X X X +//X O O X +//X X O X +//X O X X +// +// +// 运行你的函数后,矩阵变为: +// +// X X X X +//X X X X +//X X X X +//X O X X +// +// +// 解释: +// +// 被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被 +//填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 +// Related Topics 深度优先搜索 广度优先搜索 并查集 +// 👍 426 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/surrounded-regions/ + public void solve(char[][] board) { + int row = board.length; + if (board.length == 0) { + return; + } + int col = board[0].length; + + UnionFind unionFind = new UnionFind(row * col + 1); + int dummyNode = row * col; + + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + if (board[i][j] == 'O') { + // 把边界上的O和dummyNode合成一个连通区域 + if (i == 0 || i == row - 1 || j == 0 || j == col - 1) { + unionFind.union(i * col + j, dummyNode); + } else { + // 这里就将它上面的和左边的O合并成一个连通区域即可,为什么不用上下左右? + // 因为一个格子,只有可能由他上面的格子走过来或者他左边的格子走过来,这样可以减少很多无用功 + if (i > 0 && board[i - 1][j] == 'O') { + unionFind.union(i * col + j, (i - 1) * col + j); + } + if (j > 0 && board[i][j - 1] == 'O') { + unionFind.union(i * col + j, i * col + j - 1); + } + if (i < row - 1 && board[i + 1][j] == 'O') { + unionFind.union(i * col + j, (i + 1) * col + j); + } + if (j < col - 1 && board[i][j + 1] == 'O') { + unionFind.union(i * col + j, i * col + j + 1); + } + } + } + } + } + + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + if (board[i][j] == 'O') { + if (unionFind.find(i * col + j) != unionFind.find(dummyNode)) { + board[i][j] = 'X'; + } + } + } + } + } +} + +class UnionFind { + int[] parent; + + public UnionFind(int n) { + parent = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + } + } + + public int find(int n) { + int p = n; + while (p != parent[p]) { + parent[p] = parent[parent[p]]; + p = parent[p]; + } + // 路径压缩 + while (n != parent[n]) { + int temp = parent[n]; + parent[n] = p; + n = temp; + } + return p; + } + + public void union(int x, int y) { + int rootx = find(x); + int rooty = find(y); + if (rootx != rooty) { + parent[rootx] = rooty; + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/valid-sudoku.java b/week07/valid-sudoku.java new file mode 100644 index 00000000..8cbe553f --- /dev/null +++ b/week07/valid-sudoku.java @@ -0,0 +1,124 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//判断一个 9x9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可。 +// +// +// 数字 1-9 在每一行只能出现一次。 +// 数字 1-9 在每一列只能出现一次。 +// 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 +// +// +// +// +// 上图是一个部分填充的有效的数独。 +// +// 数独部分空格内已填入了数字,空白格用 '.' 表示。 +// +// 示例 1: +// +// 输入: +//[ +// ["5","3",".",".","7",".",".",".","."], +// ["6",".",".","1","9","5",".",".","."], +// [".","9","8",".",".",".",".","6","."], +// ["8",".",".",".","6",".",".",".","3"], +// ["4",".",".","8",".","3",".",".","1"], +// ["7",".",".",".","2",".",".",".","6"], +// [".","6",".",".",".",".","2","8","."], +// [".",".",".","4","1","9",".",".","5"], +// [".",".",".",".","8",".",".","7","9"] +//] +//输出: true +// +// +// 示例 2: +// +// 输入: +//[ +//  ["8","3",".",".","7",".",".",".","."], +//  ["6",".",".","1","9","5",".",".","."], +//  [".","9","8",".",".",".",".","6","."], +//  ["8",".",".",".","6",".",".",".","3"], +//  ["4",".",".","8",".","3",".",".","1"], +//  ["7",".",".",".","2",".",".",".","6"], +//  [".","6",".",".",".",".","2","8","."], +//  [".",".",".","4","1","9",".",".","5"], +//  [".",".",".",".","8",".",".","7","9"] +//] +//输出: false +//解释: 除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 +// 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。 +// +// 说明: +// +// +// 一个有效的数独(部分已被填充)不一定是可解的。 +// 只需要根据以上规则,验证已经填入的数字是否有效即可。 +// 给定数独序列只包含数字 1-9 和字符 '.' 。 +// 给定数独永远是 9x9 形式的。 +// +// Related Topics 哈希表 +// 👍 447 👎 0 + + +import java.util.HashSet; +import java.util.Set; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/valid-sudoku/ + public boolean isValidSudoku(char[][] board) { + // 和N皇后那个题目类似,N皇后那个题目是定义列,撇,呐的set来进行判重。 + // 这个是定义row, col, block来进行判重。 + + // 边界条件 + if (board.length == 0) { + return false; + } + + // 一共是9行9列9个块,所以分别set数量都应该是9,也就是我们需要容量为9的数组 + Set[] row = new HashSet[9]; + Set[] col = new HashSet[9]; + Set[] block = new HashSet[9]; + + int m = board.length; + int n = board[0].length; + + // 初始化行,列,块数组中的每个hashset + for (int i = 0; i < m; i++) { + row[i] = new HashSet<>(); + col[i] = new HashSet<>(); + block[i] = new HashSet<>(); + } + + // 遍历数独表 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + // 如果当前格子的元素不是点的时候,也就是有数字的时候 + if (board[i][j] != '.') { + // 判断行是否重复 + // 如果当前行还能增加成功,证明还没有重复元素,是合法的数独 + // 反之,如果不能增加成功,证明当前行之前已经有过这个数字,肯定是不合法的数独了。 + if (!row[i].add(board[i][j])) { + return false; + } + + // 判断列是否重复 + // 同样的,如果不能再增加进去,证明已经重复了。 + if (!col[j].add(board[i][j])) { + return false; + } + + // 判断块是否有重复 + int blockNum = (i / 3) * 3 + j / 3; + // 同上 + if (!block[blockNum].add(board[i][j])) { + return false; + } + } + } + } + + return true; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/word-ladder.java b/week07/word-ladder.java new file mode 100644 index 00000000..da01f635 --- /dev/null +++ b/week07/word-ladder.java @@ -0,0 +1,261 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则: +// +// +// +// 每次转换只能改变一个字母。 +// 转换过程中的中间单词必须是字典中的单词。 +// +// +// 说明: +// +// +// 如果不存在这样的转换序列,返回 0。 +// 所有单词具有相同的长度。 +// 所有单词只由小写字母组成。 +// 字典中不存在重复的单词。 +// 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。 +// +// +// 示例 1: +// +// 输入: +//beginWord = "hit", +//endWord = "cog", +//wordList = ["hot","dot","dog","lot","log","cog"] +// +//输出: 5 +// +//解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", +// 返回它的长度 5。 +// +// +// 示例 2: +// +// 输入: +//beginWord = "hit" +//endWord = "cog" +//wordList = ["hot","dot","dog","lot","log"] +// +//输出: 0 +// +//解释: endWord "cog" 不在字典中,所以无法进行转换。 +// Related Topics 广度优先搜索 +// 👍 639 👎 0 + + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class 单词接龙 { + // 题目链接:https://leetcode-cn.com/problems/word-ladder/ + public int ladderLength(String beginWord, String endWord, List wordList) { + /** + * 方法一:单向广度优先遍历 + * + * 时间复杂度:O(N * C * 26), N代表字典中元素个数,C代表单词的平均长度,26就是26个字母 + * 空间复杂度:O(N), wordSet, queue, visited都用到了额外空间 + */ + return func1(beginWord, endWord, wordList); + + + /** + * 方法二:双向广度优先遍历 + * + * 时间复杂度:O(N * C * 26), N代表字典中元素个数,C代表单词的平均长度,26就是26个字母 + * 空间复杂度:O(N), wordSet, visited等set都用到了额外空间 + */ +// return func2(beginWord, endWord, wordList); + + } + + private int func2(String beginWord, String endWord, List wordList) { + // 首先将单词字典放到hashSet中,方便我们通过O(1)的时间查找 + Set wordSet = new HashSet<>(wordList); + + // 边界条件判断,如果字典大小为0或者字典中不包含endWord,直接返回0 + if (wordSet.size() == 0 || !wordSet.contains(endWord)) { + return 0; + } + + // 已经访问过的 word 添加到 visited 哈希表里 + Set visited = new HashSet<>(); + // 分别用左边和右边扩散的哈希表代替单向 BFS 里的队列,它们在双向 BFS 的过程中交替使用 + Set beginVisited = new HashSet<>(); + beginVisited.add(beginWord); + Set endVisited = new HashSet<>(); + endVisited.add(endWord); + + // 初始步数为1,题目要求包含起点 + int steps = 1; + // 开始双向BFS,左右交替扩散的步数之和为所求。什么时候停止呢,当两个集合有交集的时候即可停止 + while (!beginVisited.isEmpty() && !endVisited.isEmpty()) { + // 优先选择小的哈希表进行扩散,这样更快。哈希表小,代表当前层需要扩散的元素少,那么比起扩散元素多的,也就是哈希表大的,肯定是要快一点 + if (beginVisited.size() > endVisited.size()) { + // 将两个set交换,这样的目的是让我们的处理逻辑一致,我们永远处理的较小的set都是beginVisited + Set temp = beginVisited; + beginVisited = endVisited; + endVisited = temp; + } + + // 程序走到这里,能够保证我们拿到的beginVisited就是较小的集合,扩散完之后,会将扩散后的集合赋值给beginVisited + Set nextLevelVisited = new HashSet<>(); + for (String currentWord : beginVisited) { + // 如果当前的单词能够通过修改其中某一个字母而得到endWord,则找到了这条路径 + if (findPath2(currentWord, endVisited, visited, wordSet, nextLevelVisited)) { + return steps + 1; + } + } + + // 原来的 beginVisited 废弃,从 nextLevelVisited 开始新的双向 BFS + beginVisited = nextLevelVisited; + // 当前双向遍历没有找到,将steps + 1即可 + ++steps; + } + + return 0; + } + + private int func1(String beginWord, String endWord, List wordList) { + // 首先将单词字典放到hashSet中,方便我们通过O(1)的时间查找 + Set wordSet = new HashSet<>(wordList); + + // 边界条件判断,如果字典大小为0或者字典中不包含endWord,直接返回0 + if (wordSet.size() == 0 || !wordSet.contains(endWord)) { + return 0; + } + + // 图的广度优先遍历必须使用队列和记录是否访问过的visited哈希表 + Queue queue = new LinkedList<>(); + queue.offer(beginWord); + Set visited = new HashSet<>(); + visited.add(beginWord); + + // 初始步数为1,题目要求包含起点 + int steps = 1; + // 开始广度优先遍历 + while (!queue.isEmpty()) { + // 得到当前队列中的元素个数,因为广度优先的时候每一次要将当前层的所有元素出队 + int currentSize = queue.size(); + + for (int i = 0; i < currentSize; i++) { + // 开始访问队列当前层次的元素 + String currentWord = queue.poll(); + + // 如果当前的单词能够通过修改其中某一个字母而得到endWord,则找到了这条路径 + if (findPath(currentWord, endWord, queue, visited, wordSet)) { + // 直接返回steps+1 + return steps + 1; + } + } + + // 当前层没有找到,将steps + 1即可 + ++steps; + } + + // 如果while循环结束还没有得到结果,那么证明无法到达 + return 0; + } + + /** + * 当前单词经过替换后能否到达endWord + * + * @param currentWord 当前的单词 + * @param endVisited 目标集合 + * @param visited 用于存储是否访问过某个单词的哈希表 + * @param wordSet 存储单词字典的哈希表 + * @param nextLevelVisited 当前层扩散之后的集合 + * @return + */ + private boolean findPath2(String currentWord, Set endVisited, Set visited, Set wordSet, Set nextLevelVisited) { + // 将字符串转换成字符数组,用于遍历 + char[] currentWordArray = currentWord.toCharArray(); + + // 遍历字符数组进行字母替换,看能不能到达endWord + for (int i = 0; i < currentWordArray.length; i++) { + // 记录当前字符,之后如果替换当前位置的字符找不到,要还原进行下一个字符的寻找的。(这样可以满足题目要求,只允许更改一个字符) + char remember = currentWordArray[i]; + // 遍历26个字母对其进行替换 + for (char k = 'a'; k <= 'z'; k++) { + // 如果遍历到的字母就是原先的字母,直接跳过此次循环 + if (k == remember) { + continue; + } + + // 将当前的字母赋值给字符数组对应的位置得到更改一个字母后的新字符数组 + currentWordArray[i] = k; + // 将其转化为新的字符串 + String newWord = String.valueOf(currentWordArray); + + // 如果新字符串就等于endWord证明我们已经找到了这条路径直接返回true即可 + if (endVisited.contains(newWord)) { + return true; + } + // 如果单词表里包含这个词并且我们没有访问过这个词 + if (wordSet.contains(newWord) && !visited.contains(newWord)) { + // 将其加入到队列中,并且更新visite哈希表 + nextLevelVisited.add(newWord); + visited.add(newWord); + } + } + + // 恢复位置的字母,要进入下一轮循环了 + currentWordArray[i] = remember; + } + + return false; + } + + /** + * 当前单词经过替换后能否到达endWord + * + * @param currentWord 当前的单词 + * @param endWord 目标单词 + * @param queue 队列 + * @param visited 用于存储是否访问过某个单词的哈希表 + * @param wordSet 存储单词字典的哈希表 + * @return + */ + public boolean findPath(String currentWord, String endWord, + Queue queue, Set visited, Set wordSet) { + // 将字符串转换成字符数组,用于遍历 + char[] currentWordArray = currentWord.toCharArray(); + + // 遍历字符数组进行字母替换,看能不能到达endWord + for (int i = 0; i < currentWordArray.length; i++) { + // 记录当前字符,之后如果替换当前位置的字符找不到,要还原进行下一个字符的寻找的。(这样可以满足题目要求,只允许更改一个字符) + char remember = currentWordArray[i]; + // 遍历26个字母对其进行替换 + for (char k = 'a'; k <= 'z'; k++) { + // 如果遍历到的字母就是原先的字母,直接跳过此次循环 + if (k == remember) { + continue; + } + + // 将当前的字母赋值给字符数组对应的位置得到更改一个字母后的新字符数组 + currentWordArray[i] = k; + // 将其转化为新的字符串 + String newWord = String.valueOf(currentWordArray); + + // 如果新字符串就等于endWord证明我们已经找到了这条路径直接返回true即可 + if (newWord.equals(endWord)) { + return true; + } + // 如果单词表里包含这个词并且我们没有访问过这个词 + if (wordSet.contains(newWord) && !visited.contains(newWord)) { + // 将其加入到队列中,并且更新visite哈希表。这里不用担心他会把同一层次的节点加进来,因为同一层次的节点在上一层次的遍历肯定全部都加进来了,所以肯定通不过!visited + // 为什么不能加入同一层次的节点?因为和同一层次的连一条边没有意义,这样就并不是最短的路径了。 + queue.offer(newWord); + visited.add(newWord); + } + } + + // 恢复位置的字母,要进入下一轮循环了 + currentWordArray[i] = remember; + } + + // 程序走到这里还没返回,就证明我们没有找到endWord + return false; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week07/word-search-ii.java b/week07/word-search-ii.java new file mode 100644 index 00000000..73c4bf27 --- /dev/null +++ b/week07/word-search-ii.java @@ -0,0 +1,345 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。 +// +// 单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使 +//用。 +// +// +// +// 示例 1: +// +// +//输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l" +//,"v"]], words = ["oath","pea","eat","rain"] +//输出:["eat","oath"] +// +// +// 示例 2: +// +// +//输入:board = [["a","b"],["c","d"]], words = ["abcb"] +//输出:[] +// +// +// +// +// 提示: +// +// +// m == board.length +// n == board[i].length +// 1 <= m, n <= 12 +// board[i][j] 是一个小写英文字母 +// 1 <= words.length <= 3 * 104 +// 1 <= words[i].length <= 10 +// words[i] 由小写英文字母组成 +// words 中的所有字符串互不相同 +// +// Related Topics 字典树 回溯算法 +// 👍 289 👎 0 + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +//leetcode submit region begin(Prohibit modification and deletion) +//class TrieNode { +// // 当前节点的所有儿子 +// private Map children; +// +// private boolean isEnd; +// +// public TrieNode() { +// children = new HashMap<>(); +// } +// +// // 当前层是否包含字符 +// public boolean containsKey(char ch) { +// return children.containsKey(ch); +// } +// +// // 得到下一层的所有儿子节点 +// public TrieNode get(char ch) { +// return children.get(ch); +// } +// +// // 插入操作 +// public void put(char ch, TrieNode node) { +// children.put(ch, node); +// } +// +// public void setEnd() { +// isEnd = true; +// } +// +// public boolean isEnd() { +// return isEnd; +// } +//} + +class TrieNode { + HashMap children = new HashMap<>(); + String word = null; + + public TrieNode() { + } +} + +class Solution { + // 题目链接:https://leetcode-cn.com/problems/word-search-ii/ + public TrieNode root; + public List res; + public int[] directX = {0, 0, -1, 1}; + public int[] directY = {-1, 1, 0, 0}; + char[][] myBoard; + + public List findWords(char[][] board, String[] words) { + // 思路应该是正确的,但是这个方法超时 +// return func1(board, words); + + // 思路二: + // 边界条件 + // 时间复杂度:O(M*N*(3^(L-1)). M,N是格子的行数和列数。3是代表每个格子除了刚刚来过的地方还有三个方向可以走,走多久呢?单词有多长就会走多久。所以L是单词的平均长度。 + // 空间复杂度:额外开了一个二维数组。所以空间复杂度是O(M*N) + return func2(board, words); + } + + private List func2(char[][] board, String[] words) { + if (board.length == 0) { + return res; + } + + res = new ArrayList<>(); + // 申请一个字典树 + root = new TrieNode(); + // 将每个单词加入到字典树中 + for (String word : words) { + TrieNode node = root; + char[] currentWords = word.toCharArray(); + for (char currentWord : currentWords) { + // 如果当前层不包含这个key + if (!node.children.containsKey(currentWord)) { + // 将其加入到字典树中 + node.children.put(currentWord, new TrieNode()); + } + // 无论怎样,我都要往下面一层走 + node = node.children.get(currentWord); + } + // 每个字符加入完,最后的这个节点存放整个单词的值 + node.word = word; + } + + // 得到面板的行和列 + int row = board.length; + int col = board[0].length; + + myBoard = board; + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + // 申请一个标记数组用于在回溯过程中标记哪些格子是访问过的 + boolean[][] mark = new boolean[row][col]; + // 如果当前root的字典里包括当前遍历到的字符,递归回溯进行寻找单词的工作 + if (root.children.containsKey(board[i][j])) { + dfs(i, j, root); + } + } + } + + return res; + } + + /** + * 思路二:递归回溯剪枝,trieNode简单 + * + * @param row 当前遍历的行 + * @param col 当前遍历的列 + * @param node 当前字典树的节点 + */ + private void dfs(int row, int col, TrieNode node) { + // 首先能够进来dfs的,一定是当前行和列对应的字符在字典树中的,这个是由上层函数以及当前函数后面的判断语句决定的 + // 得到当前递归到的字符 + char currentWord = myBoard[row][col]; + // 得到这一层字符对应的新node + TrieNode currNode = node.children.get(currentWord); + + // 如果已经找到单词了 + if (currNode.word != null) { + // 将其加入结果集 + res.add(currNode.word); + // 将其赋值为空,以防重复加入到结果集,这里也可以用另外一种方式,判断列表中是否存在这个单词,我们就不用去修改这棵字典树了。 + currNode.word = null; + } + + // 将当前层的格子对应的值修改为#,以免递归重复访问 + myBoard[row][col] = '#'; + // 这上面都是在处理当前层逻辑 + + // 循环遍历四个方向(上下左右)进行递归回溯 + for (int i = 0; i < 4; i++) { + int newRow = row + directX[i]; + int newCol = col + directY[i]; + + if (newRow < 0 || newRow >= myBoard.length || newCol < 0 || newCol >= myBoard[0].length) { + continue; + } + if (currNode.children.containsKey(myBoard[newRow][newCol])) { + dfs(newRow, newCol, currNode); + } + } + // 这里为止是递归调用 + + // 这一层结束了,需要将这一层所代表的的格子的值重新赋值回去,这是将当前层状态回退 + myBoard[row][col] = currentWord; + + // 如果已经遍历到最后的叶子节点,将叶子节点删掉,也可以避免之后花多余的时间去遍历他 + // 这里也可以叫剪枝 + if (currNode.children.isEmpty()) { + node.children.remove(currentWord); + } + } + + private List func1(char[][] board, String[] words) { + // 边界条件判断 + if (board.length == 0) { + return res; + } + + // 初始化结果集 + res = new ArrayList<>(); + + // 申请一棵前缀树(字典树) + root = new TrieNode(); + // 遍历单词数组,将所有单词加入到字典树中 + for (int i = 0; i < words.length; i++) { +// insertWord(words[i]); + } + + // 得到矩阵面板的行数和列数 + int row = board.length; + int col = board[0].length; + + // 开始进行回溯 + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + // 定义一个二维数组用于标记哪些地方走过了,走过的地方我们就不用再走了 + boolean[][] mark = new boolean[row][col]; + StringBuilder sb = new StringBuilder(); +// findHelper(mark, board, i, j, res, sb); + } + } + + return res; + } + + /** + * 回溯函数 + * + * @param mark 标记数组,用于标记哪些格子已经访问过了 + * @param row 当前格子行数 + * @param col 当前格子列数 + * @param res 当前结果集 + * @param sb 回溯迭代到的当前单词 + */ +// private void findHelper(boolean[][] mark, char[][] board, int row, int col, List res, StringBuilder sb) { +// // 如果以前访问过这个格子 +// if (mark[row][col]) { +// // 直接返回 +// return; +// } +// +// // 否则,将当前字符加入到当前单词中 +// sb.append(board[row][col]); +// // 如果字典树中找不到这个前缀,我们再往下走也不可能找得到我们要的单词 +// if (!startsWith(sb.toString())) { +// // 将sb中刚刚加进来的字符去掉 +// sb.deleteCharAt(sb.length() - 1); +// // 然后返回 +// return; +// } +// +// // 否则证明字典树中是有这个前缀的 +// // 将当前格子标记为true,也就是我们访问过了 +// mark[row][col] = true; +// // 通过sb得到当前的单词 +// String currentWord = sb.toString(); +// +// // 至此,前面2个if其实都是递归出口 +// +// +// // 下面就是处理当前层逻辑 +// // 如果字典树能够找到这个单词(注:找到单词有两个条件,一个是单词的所有字符在字典树的路径上,二是最后得到的node的isEnd为true) +// if (search(currentWord)) { +// // 如果结果集不包含这个结果 +// if (!res.contains(currentWord)) { +// // 将这个单词加入到结果集中 +// res.add(currentWord); +// } +// +// // 为了以防出现dog,dogs两个单词的情况,我们还要往下去遍历。 +// } +// +// // 这下面就是递归调用进行回溯 +// for (int i = 0; i < 4; i++) { +// // 得到新的格子的坐标 +// int newRow = row + directX[i]; +// int newCol = col + directY[i]; +// +// // 如果格子坐标是满足要求的 +// if (newRow >= 0 && newRow < board.length && newCol >= 0 && newCol < board[0].length) { +// // 则递归进行回溯运算 +// findHelper(mark, board, newRow, newCol, res, sb); +// } +// } +// +// // 下面的操作就是将当前层状态回退 +// sb.deleteCharAt(sb.length() - 1); +// mark[row][col] = false; +// } + + /** + * 往字典树中插入单词 + * + * @param word 要插入的单词 + */ +// private void insertWord(String word) { +// TrieNode node = root; +// char[] wordArray = word.toCharArray(); +// for (int i = 0; i < wordArray.length; i++) { +// char currentWord = wordArray[i]; +// if (!node.containsKey(currentWord)) { +// node.put(currentWord, new TrieNode()); +// } +// node = node.get(currentWord); +// } +// node.setEnd(); +// } + +// private boolean search(String word) { +// TrieNode node = searchPrefix(word); +// return node != null && node.isEnd(); +// } + +// private boolean startsWith(String word) { +// TrieNode node = searchPrefix(word); +// return node != null; +// } + +// private TrieNode searchPrefix(String word) { +// TrieNode node = root; +// char[] wordArray = word.toCharArray(); +// for (int i = 0; i < wordArray.length; i++) { +// char currentWord = wordArray[i]; +// if (node.containsKey(currentWord)) { +// node = node.get(currentWord); +// } else { +// return null; +// } +// } +// +// return node; +// } +} + +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/LRUCache.java b/week08/LRUCache.java new file mode 100644 index 00000000..39f5450a --- /dev/null +++ b/week08/LRUCache.java @@ -0,0 +1,154 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。 +// +// +// +// 实现 LRUCache 类: +// +// +// LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存 +// int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 +// void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上 +//限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。 +// +// +// +// +// +// +// 进阶:你是否可以在 O(1) 时间复杂度内完成这两种操作? +// +// +// +// 示例: +// +// +//输入 +//["LRUCache", "put", "put", "get", "put", "get", "put", "get", "get", "get"] +//[[2], [1, 1], [2, 2], [1], [3, 3], [2], [4, 4], [1], [3], [4]] +//输出 +//[null, null, null, 1, null, -1, null, -1, 3, 4] +// +//解释 +//LRUCache lRUCache = new LRUCache(2); +//lRUCache.put(1, 1); // 缓存是 {1=1} +//lRUCache.put(2, 2); // 缓存是 {1=1, 2=2} +//lRUCache.get(1); // 返回 1 +//lRUCache.put(3, 3); // 该操作会使得关键字 2 作废,缓存是 {1=1, 3=3} +//lRUCache.get(2); // 返回 -1 (未找到) +//lRUCache.put(4, 4); // 该操作会使得关键字 1 作废,缓存是 {4=4, 3=3} +//lRUCache.get(1); // 返回 -1 (未找到) +//lRUCache.get(3); // 返回 3 +//lRUCache.get(4); // 返回 4 +// +// +// +// +// 提示: +// +// +// 1 <= capacity <= 3000 +// 0 <= key <= 3000 +// 0 <= value <= 104 +// 最多调用 3 * 104 次 get 和 put +// +// Related Topics 设计 +// 👍 1039 👎 0 + + +import java.util.HashMap; +import java.util.Map; + +//leetcode submit region begin(Prohibit modification and deletion) +class LRUCache { + // 题目链接:https://leetcode-cn.com/problems/lru-cache/ + class DLinkedNode { + int key; + int value; + DLinkedNode prev; + DLinkedNode next; + public DLinkedNode() {} + public DLinkedNode(int _key, int _value) {key = _key; value = _value;} + } + + private Map cache = new HashMap(); + private int size; + private int capacity; + private DLinkedNode head, tail; + + public LRUCache(int capacity) { + this.size = 0; + this.capacity = capacity; + // 使用伪头部和伪尾部节点 + head = new DLinkedNode(); + tail = new DLinkedNode(); + head.next = tail; + tail.prev = head; + } + + public int get(int key) { + DLinkedNode node = cache.get(key); + if (node == null) { + return -1; + } + // 如果 key 存在,先通过哈希表定位,再移到头部 + moveToHead(node); + return node.value; + } + + public void put(int key, int value) { + DLinkedNode node = cache.get(key); + if (node == null) { + // 如果 key 不存在,创建一个新的节点 + DLinkedNode newNode = new DLinkedNode(key, value); + // 添加进哈希表 + cache.put(key, newNode); + // 添加至双向链表的头部 + addToHead(newNode); + ++size; + if (size > capacity) { + // 如果超出容量,删除双向链表的尾部节点 + DLinkedNode tail = removeTail(); + // 删除哈希表中对应的项 + cache.remove(tail.key); + --size; + } + } + else { + // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部 + node.value = value; + moveToHead(node); + } + } + + private void addToHead(DLinkedNode node) { + node.prev = head; + node.next = head.next; + head.next.prev = node; + head.next = node; + } + + private void removeNode(DLinkedNode node) { + node.prev.next = node.next; + node.next.prev = node.prev; + } + + private void moveToHead(DLinkedNode node) { + removeNode(node); + addToHead(node); + } + + private DLinkedNode removeTail() { + DLinkedNode res = tail.prev; + removeNode(res); + return res; + } +} + +/** + * Your LRUCache object will be instantiated and called as such: + * LRUCache obj = new LRUCache(capacity); + * int param_1 = obj.get(key); + * obj.put(key,value); + */ +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/NOTE.md b/week08/NOTE.md index 50de3041..24c9e492 100644 --- a/week08/NOTE.md +++ b/week08/NOTE.md @@ -1 +1,108 @@ -学习笔记 \ No newline at end of file +### 一. 冒泡排序模板 + +```java + +// 冒泡排序,a表示数组,n表示数组大小 +public void bubbleSort(int[] a, int n) { + if (n <= 1) return; + + for (int i = 0; i < n; ++i) { + // 提前退出冒泡循环的标志位 + boolean flag = false; + for (int j = 0; j < n - i - 1; ++j) { + if (a[j] > a[j+1]) { // 交换 + int tmp = a[j]; + a[j] = a[j+1]; + a[j+1] = tmp; + flag = true; // 表示有数据交换 + } + } + if (!flag) break; // 没有数据交换,提前退出 + } +} +``` + + + +### 二. 插入排序模板 + +```java + +// 插入排序,a表示数组,n表示数组大小 +public void insertionSort(int[] a, int n) { + if (n <= 1) return; + + for (int i = 1; i < n; ++i) { + int value = a[i]; + int j = i - 1; + // 查找插入的位置 + for (; j >= 0; --j) { + if (a[j] > value) { + a[j+1] = a[j]; // 数据移动 + } else { + break; + } + } + a[j+1] = value; // 插入数据 + } +} +``` + + + +### 三. 希尔排序模板 + +```java +private static void shellSort(int[] arr) { + int len = arr.length; + if (len == 1) return; + + int step = len / 2; + while (step >= 1) { + for (int i = step; i < len; i++) { + int value = arr[i]; + int j = i - step; + for (; j >= 0; j -= step) { + if (value < arr[j]) { + arr[j+step] = arr[j]; + } else { + break; + } + } + arr[j+step] = value; + } + + step = step / 2; + } + } +``` + + + +### 四. 选择排序模板 + +```java +// 选择排序,a表示数组,n表示数组大小 + public static void selectionSort(int[] a, int n) { + if (n <= 1) return; + + for (int i = 0; i < n - 1; ++i) { + // 查找最小值 + int minIndex = i; + for (int j = i + 1; j < n; ++j) { + if (a[j] < a[minIndex]) { + minIndex = j; + } + } + + // 交换 + int tmp = a[i]; + a[i] = a[minIndex]; + a[minIndex] = tmp; + } + } +``` + + + +学习体会:训练营第8周,无论是老题还是新题,解题的速度都比以前更快了,而且我觉得在这个过程中自己的理解能力,学习能力有了很大的提升。人生最有意义的事情莫过于知道自己的位置,然后想要去什么位置,最后付诸努力。继续加油,向目标进军。 \ No newline at end of file diff --git "a/week08/[1122]\346\225\260\347\273\204\347\232\204\347\233\270\345\257\271\346\216\222\345\272\217.java" "b/week08/[1122]\346\225\260\347\273\204\347\232\204\347\233\270\345\257\271\346\216\222\345\272\217.java" new file mode 100644 index 00000000..3fc035eb --- /dev/null +++ "b/week08/[1122]\346\225\260\347\273\204\347\232\204\347\233\270\345\257\271\346\216\222\345\272\217.java" @@ -0,0 +1,81 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给你两个数组,arr1 和 arr2, +// +// +// arr2 中的元素各不相同 +// arr2 中的每个元素都出现在 arr1 中 +// +// +// 对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末 +//尾。 +// +// +// +// 示例: +// +// +//输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6] +//输出:[2,2,2,1,4,3,3,9,6,7,19] +// +// +// +// +// 提示: +// +// +// 1 <= arr1.length, arr2.length <= 1000 +// 0 <= arr1[i], arr2[i] <= 1000 +// arr2 中的元素 arr2[i] 各不相同 +// arr2 中的每个元素 arr2[i] 都出现在 arr1 中 +// +// Related Topics 排序 数组 +// 👍 147 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class relative-sort-array { + // 题目链接:https://leetcode-cn.com/problems/relative-sort-array/ + public int[] relativeSortArray(int[] arr1, int[] arr2) { + // 计数排序 + return func1(arr1, arr2); + } + + private int[] func1(int[] arr1, int[] arr2) { + // 结果集 + int[] res = new int[arr1.length]; + + int maxNum = 0; + // 选出最大值 + for (int every : arr1) { + maxNum = Math.max(maxNum, every); + } + + // 申请一个数组,长度为刚刚选出来的最大值 + int[] mark = new int[maxNum + 1]; + + // 记录数组里的每个数有多少个 + for (int every : arr1) { + mark[every] += 1; + } + + // 遍历arr2,将数组2中有的数按照顺序放到结果集中 + int j = 0; + for (int every : arr2) { + for (int i = 0; i < mark[every]; i++) { + res[j++] = every; + } + // 这个元素加完了之后将mark数组清0 + mark[every] = 0; + } + + // 此时数组mark中只剩下那些下标没在arr2中出现过的 + // 也就是现在只有arr2中没出现过的元素对应到mark的下标对应的值是大于0的。 + for (int i = 0; i < mark.length; i++) { + for (int k = 0; k < mark[i]; k++) { + res[j++] = i; + } + } + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/merge-intervals.java b/week08/merge-intervals.java new file mode 100644 index 00000000..58fc560f --- /dev/null +++ b/week08/merge-intervals.java @@ -0,0 +1,73 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给出一个区间的集合,请合并所有重叠的区间。 +// +// +// +// 示例 1: +// +// 输入: intervals = [[1,3],[2,6],[8,10],[15,18]] +//输出: [[1,6],[8,10],[15,18]] +//解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. +// +// +// 示例 2: +// +// 输入: intervals = [[1,4],[4,5]] +//输出: [[1,5]] +//解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。 +// +// 注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。 +// +// +// +// 提示: +// +// +// intervals[i][0] <= intervals[i][1] +// +// Related Topics 排序 数组 +// 👍 725 👎 0 + + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/merge-intervals/ + public int[][] merge(int[][] intervals) { + // 方法一:先按照每个区间的左边元素排序,这样就可以知道哪些是可以合并的区间 + if (intervals.length == 0) { + return new int[0][2]; + } + // 按照每个区间左边元素进行排序 + Arrays.sort(intervals, new Comparator(){ + @Override + public int compare(int[] o1, int[] o2) { + return o1[0] - o2[0]; + } + }); + + List merged = new ArrayList<>(); + for (int i = 0; i < intervals.length; i++) { + // 得到每个区间的第一个元素 + int first = intervals[i][0]; + // 得到每个区间的第二个元素 + int second = intervals[i][1]; + // 如果合并的列表长度为0或者合并列表的最后一个区间的右区间比当前区间的左区间小 + // 证明他们不可能有交集,直接在结果集中增加一个区间即可。如(3,5),(6,7) + if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < first) { + merged.add(new int[]{first, second}); + } else { + // 否则,就要合并区间,如(3,5),(4,6)或者(3,7)(4,5) + merged.get(merged.size() - 1)[1] = Math.max(second, merged.get(merged.size() - 1)[1]); + } + } + + // 将列表转化为数组后返回 + return merged.toArray(new int[merged.size()][]); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/n-queens-ii.java b/week08/n-queens-ii.java new file mode 100644 index 00000000..80d52f81 --- /dev/null +++ b/week08/n-queens-ii.java @@ -0,0 +1,100 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 +// +// +// +// 上图为 8 皇后问题的一种解法。 +// +// 给定一个整数 n,返回 n 皇后不同的解决方案的数量。 +// +// 示例: +// +// 输入: 4 +//输出: 2 +//解释: 4 皇后问题存在如下两个不同的解法。 +//[ +// [".Q..",  // 解法 1 +//  "...Q", +//  "Q...", +//  "..Q."], +// +// ["..Q.",  // 解法 2 +//  "Q...", +//  "...Q", +//  ".Q.."] +//] +// +// +// +// +// 提示: +// +// +// 皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一或 N +//-1 步,可进可退。(引用自 百度百科 - 皇后 ) +// +// Related Topics 回溯算法 +// 👍 215 👎 0 + + +import java.util.HashSet; +import java.util.Set; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution29 { + // 题目链接:https://leetcode-cn.com/problems/n-queens-ii/description/ + + // n皇后解法的数量 + public int count = 0; + public int totalNQueens(int n) { + // 边界条件 + if (n < 1) { + return 0; + } + + dfs(n, 0, 0, 0, 0); + return count; + } + + public void dfs(int n, int row, int col, int pie, int na) { + // 递归终止条件, 每一次能够走到最后一行都对应N皇后的一种解法 + if (row >= n) { + count+=1; + return; + } + + // 这一步是关键,col | pie | na就是二进制位对应中第1位到第n位中有多少个1,也就是有多少个位子是已经被占了的 + // 取一个~就是说有多少个位子是没有被占的, 那为什么又要与上右边那一坨呢? + // 这是因为我是N皇后,我只需要一个int数字的N位即可,假如是8皇后,我只需要8位,其他的位是不需要的所以要将其置为0 + // 比如现在 col | pie | na 等于000000000000000000000000 10101001,代表现在8皇后有这些位置都被占着 + // 那我取一个~ 就代表没有被占着的位子,但是取非之后会把高位那些没用的位全部变成1,如下: + // 111111111111111111111111 01010110 ,而由于我们判断一个位子可不可用,循环的时候用的位子上是1这样一个标准来判断 + // 这样取反之后就会导致我们能用的1的个数变多,本来是N皇后,变成了N + 24皇后(高位24位) + // 所以我们要把高位的24位干掉。我们来看(1 << n),还是以8皇后为例:000000000000000000000001 00000000 + // 再减一之后 000000000000000000000000 11111111 + // 再与之前的 111111111111111111111111 01010110 相与,这样我就得到了我们要的那8位或者是N位,如下: + // 000000000000000000000000 01010110 + int bits = (~(col | pie | na)) & ((1 << n) - 1); + + // 循环开始判断 + while (bits != 0) { + // 取到最低的那一位 + int p = bits & -bits; + bits = bits & (bits - 1); + // 既然决定将p这个位置放上皇后,那bits中自然要去掉他 + + // 到这里位置都是在处理当前层逻辑 + + // dfs 到下一层 + // 这里的pie | p要左移一位的原因是对于下一层而言,在撇这条线上的位子不能用他感知不到,实际上我们举个例子看就知道了 + // 加入当前层取到的p的位子是00001000 + // 那么对于下一行而言,列就是这个列使我们不能用的,pie的话大家想想 + // 00001000当前从左到右第五个格子被一个皇后占着了,对于撇而言,下一行皇后是不是不能放在第四个格子地方,而对于na而言 + // 就是不能放在第六个格子的地方。所以撇要左移一位,na要右移一位 + dfs(n, row + 1, col | p, (pie | p) << 1, (na | p) >> 1); + + // 不需要回退lie, pie, na的状态,因为我是去与,并没有真正改掉他们的值。 + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/n-queens.java b/week08/n-queens.java new file mode 100644 index 00000000..3594892c --- /dev/null +++ b/week08/n-queens.java @@ -0,0 +1,191 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 +// +// +// +// 上图为 8 皇后问题的一种解法。 +// +// 给定一个整数 n,返回所有不同的 n 皇后问题的解决方案。 +// +// 每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 +// +// +// +// 示例: +// +// 输入:4 +//输出:[ +// [".Q..", // 解法 1 +// "...Q", +// "Q...", +// "..Q."], +// +// ["..Q.", // 解法 2 +// "Q...", +// "...Q", +// ".Q.."] +//] +//解释: 4 皇后问题存在两个不同的解法。 +// +// +// +// +// 提示: +// +// +// 皇后彼此不能相互攻击,也就是说:任何两个皇后都不能处于同一条横行、纵行或斜线上。 +// +// Related Topics 回溯算法 +// 👍 685 👎 0 + + +import scala.Array; + +import java.util.*; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/n-queens/ + + // 皇后列的set + Set col = new HashSet<>(); + // 皇后右斜上的set + Set pie = new HashSet<>(); + // 皇后左斜上的set + Set na = new HashSet<>(); + // 定义结果集 + List> res = new ArrayList<>(); + public List> solveNQueens(int n) { + // 回溯 +// return func1(n); + + // 位运算 + // 边界条件 + if (n < 1) { + return res; + } + + // 申请一个char棋盘 + char[][] chess = new char[n][n]; + // 初始化棋盘 + for (char[] everyRow : chess) { + Arrays.fill(everyRow, '.'); + } + + // 开始回溯 + dfs2(n, 0, 0, 0, 0, chess); + return res; + } + + public static void main(String[] args) { +// Solution23 solution = new Solution23(); + double index = Math.log(4)/Math.log(2); + System.out.println(index); +// solution.solveNQueens(4); + } + + public void dfs2(int n, int row, int lie, int pie, int na, char[][] chess) { + if (row >= n) { + res.add(transfer(chess)); + return; + } + + // 得到当前层的bits + int bits = (~(lie | pie | na)) & ((1 << n) - 1); + + int count = 0; + while (bits != 0) { + int p = bits & -bits; + int index = (int) (Math.log(p) / Math.log(2)); + chess[row][n - index - 1] = 'Q'; + bits = bits & (bits - 1); + + dfs2(n, row + 1, lie | p, (pie | p) << 1, (na | p) >> 1, chess); + chess[row][n - index - 1] = '.'; + } + } + + /** + * 思路一:递归回溯 + * 时间复杂度:指数级 + * 空间复杂度:O(N) + * @param n + * @return + */ + private List> func1(int n) { + // 申请一个char棋盘 + char[][] chess= new char[n][n]; + // 初始化棋盘 + for (char[] everyRow : chess) { + // 初始状态棋盘每个地方都是. + Arrays.fill(everyRow, '.'); + } + + // 开始回溯遍历 + // 从第一行开始,如果符合条件则往下一层走(逻辑上即为下一行) + for (int i = 0; i < n; i++) { + // 第一层怎么放都是符合要求的,所以不用判断 + chess[0][i] = 'Q'; + // 将其加入到各个set集合中 + col.add(i); + pie.add(0 + i); + na.add(0 - i); + + dfs(chess, 1); + + // 将状态回退 + na.remove(0 - i); + pie.remove(0 + i); + col.remove(i); + chess[0][i] = '.'; + } + + return res; + } + + public void dfs(char[][] chess, int row) { + // 递归出口,也就是到达最后一行还有效的时候 + // 实际上是row在n-1的时候就到最后一层了,因为下标从0开始 + // 但是要最后一层也是满足条件的才行,所以最后一层满足条件进入的下一层 + // 就是第n层,也就是row == n的时候,代表我们找到了一组解 + if (row == chess.length) { + // 将当前chess加入到结果集中 + res.add(transfer(chess)); + return; + } + + // 如果没到最后一层,处理当前层逻辑,判断是否能够走到下一层 + // 当前层(当前行)的每一个字符进行遍历 + for (int i = 0; i < chess.length; i++) { + // 如果是有效的,也就是当前列的set,撇的set,呐的set都没有对应的元素 + if (!col.contains(i) && !pie.contains(row + i) && !na.contains(row - i)) { + // 将当前行,当前列的字符设置成皇后 + chess[row][i] = 'Q'; + // 更新col,pie,na这三个set + col.add(i); + pie.add(row + i); + na.add(row - i); + + // 递归遍历到下一层 + dfs(chess, row + 1); + + // 当前行的当前列递归回溯完了之后,要将状态回溯 + na.remove(row - i); + pie.remove(row + i); + col.remove(i); + chess[row][i] = '.'; + } + } + + // 这一层遍历完了,return到上一层,这里也不用我们手动去return,程序执行完自己就回到上一层了。 + } + + public List transfer(char[][] chess) { + List cString = new ArrayList<>(); + for (char[] every : chess) { + cString.add(new String(every)); + } + return cString; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/number-of-1-bits.java b/week08/number-of-1-bits.java new file mode 100644 index 00000000..290cab76 --- /dev/null +++ b/week08/number-of-1-bits.java @@ -0,0 +1,79 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。 +// +// +// +// 示例 1: +// +// 输入:00000000000000000000000000001011 +//输出:3 +//解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。 +// +// +// 示例 2: +// +// 输入:00000000000000000000000010000000 +//输出:1 +//解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。 +// +// +// 示例 3: +// +// 输入:11111111111111111111111111111101 +//输出:31 +//解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。 +// +// +// +// 提示: +// +// +// 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的 +//还是无符号的,其内部的二进制表示形式都是相同的。 +// 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。 +// +// +// +// +// 进阶: +//如果多次调用这个函数,你将如何优化你的算法? +// Related Topics 位运算 +// 👍 225 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/number-of-1-bits/ + public int hammingWeight(int n) { + // 暴力法 + int count = 0; + int mask = 1; + for (int i = 0; i < 32; i++) { + if ((n & mask) != 0) { + count++; + } + + mask <<= 1; + } + + // 暴力 + int count = 0; + for (int i = 0; i < 32; i++) { + if ((n & 1) == 1) { + count++; + } + n >>= 1; + } + return count; + + // 位运算 + int count = 0; + while (n != 0) { + count++; + n &= (n - 1); + } + + return count; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/power-of-two.java b/week08/power-of-two.java new file mode 100644 index 00000000..b047da8d --- /dev/null +++ b/week08/power-of-two.java @@ -0,0 +1,54 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个整数,编写一个函数来判断它是否是 2 的幂次方。 +// +// 示例 1: +// +// 输入: 1 +//输出: true +//解释: 20 = 1 +// +// 示例 2: +// +// 输入: 16 +//输出: true +//解释: 24 = 16 +// +// 示例 3: +// +// 输入: 218 +//输出: false +// Related Topics 位运算 数学 +// 👍 263 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class power-of-two { + // 题目链接:https://leetcode-cn.com/problems/power-of-two/ + public boolean isPowerOfTwo(int n) { + // 暴力法 + // 如果一个数要是2的幂,那注定他的二进制1的个数注定只能是一个 + while ((n & 1) == 0) { + n >>= 1; + } + return n == 1; + + // 更好的思路,既然有且仅有一个1,那我们就把他唯一的那个1干掉 + if (n == 0) { + return false; + } + + long x = n; + + x &= (x - 1); + + return x == 0; + + // 得到最低位的1即可 + if (n == 0) { + return false; + } + long x = n; + return (x & (-x)) == x; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/reverse-bits.java b/week08/reverse-bits.java new file mode 100644 index 00000000..7594f65c --- /dev/null +++ b/week08/reverse-bits.java @@ -0,0 +1,59 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//颠倒给定的 32 位无符号整数的二进制位。 +// +// +// +// 示例 1: +// +// 输入: 00000010100101000001111010011100 +//输出: 00111001011110000010100101000000 +//解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596, +// 因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。 +// +// 示例 2: +// +// 输入:11111111111111111111111111111101 +//输出:10111111111111111111111111111111 +//解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293, +//  因此返回 3221225471 其二进制表示形式为 10111111111111111111111111111111 。 +// +// +// +// 提示: +// +// +// 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的 +//还是无符号的,其内部的二进制表示形式都是相同的。 +// 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -10737418 +//25。 +// +// +// +// +// 进阶: +//如果多次调用这个函数,你将如何优化你的算法? +// Related Topics 位运算 +// 👍 219 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class reverse-bits { + // 题目链接:https://leetcode-cn.com/problems/reverse-bits/ + public int reverseBits(int n) { + int result = 0, power = 31; +// +// while (n != 0) { +// result += (n & 1) << power; +// n = n >> 1; +// power -= 1; +// } + + for (int i = 0; i < 32; i++) { + result <<= 1; + result += (n & 1); + n >>= 1; + } + return result; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/reverse-pairs.java b/week08/reverse-pairs.java new file mode 100644 index 00000000..0eccedbd --- /dev/null +++ b/week08/reverse-pairs.java @@ -0,0 +1,93 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对。 +// +// 你需要返回给定数组中的重要翻转对的数量。 +// +// 示例 1: +// +// +//输入: [1,3,2,3,1] +//输出: 2 +// +// +// 示例 2: +// +// +//输入: [2,4,3,5,1] +//输出: 3 +// +// +// 注意: +// +// +// 给定数组的长度不会超过50000。 +// 输入数组中的所有数字都在32位整数的表示范围内。 +// +// Related Topics 排序 树状数组 线段树 二分查找 分治算法 +// 👍 241 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/reverse-pairs/ + public int reversePairs(int[] nums) { + if (nums.length == 0 || nums == null) { + return 0; + } + + return mergeSort(nums, 0, nums.length - 1); + } + + private int mergeSort(int[] nums, int start, int end) { + // 递归终止条件 + if (start >= end) { + return 0; + } + + // 计算出中间节点 + int mid = (start + end) >> 1; + // 当前层翻转对的数量, 初始化为0 + // 但是逻辑上,他等于,左半边翻转对的数量 + 右半边翻转对的数量 + 左右合并起来的翻转对的数量 + // 举个例子说:2 1 3 4 7 8 5 6反转对的数量就等于 + //(2 1 3 4)翻转对的数量 + (7 8 5 6)翻转对的数量 + (2 1 3 4)与(7 8 5 6)之间翻转对的数量 + int cnt = 0; + + cnt = mergeSort(nums, start, mid) + mergeSort(nums, mid + 1, end); + + // 下面就是要开始merge的操作,一遍merge一边得出当前层左半边与右半边的翻转对数量 + int l = start, i = start, r = end, c = 0; + // 临时数组装排好序的数字 + int[] temp = new int[end - start + 1]; + // 遍历看前半边和后半边到底有多少翻转对 + for (int j = mid + 1; j <= end; j++) { + // 记录左半边从左到右有多少个比2*nums[j]小的,直到遇到比他大的 + // 因为左半边和右半边都是排好序的,当左半边遇到了一个比当前nums[j]*2大的 + // 左半边后面的也一定比当前的nums[j] * 2大,就不用再去判断了。 + while (i <= mid && nums[i] <= 2 * (long)nums[j]) { + i++; + } + // 目的是将左半边和右半边合并起来排序记录到临时数组中 + // 这个就是归并排序的部分 + while (l <= mid && nums[l] <= nums[j]) { + temp[c++] = nums[l++]; + } + // 当上面循环走完的时候,左半边中比右半边当前数字小的数字都已经加入到临时数组了 + // 所以将当前右半边数字加入到数组中 + temp[c++] = nums[j]; + // 更新反转对数量,由于i是有少个小于等于2*nums[j]的,所以mid - i + 1就是有多少大于他的 + cnt += mid - i + 1; + } + + // for循环完了只能保证右半边肯定都加入数组了,左半边不一定 + while (l <= mid) { + temp[c++] = nums[l++]; + } + // 将temp赋值给nums + for (int p = 0; p < temp.length; p++) { + nums[start + p] = temp[p]; + } + + return cnt; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week08/valid-anagram.java b/week08/valid-anagram.java new file mode 100644 index 00000000..30cb7fad --- /dev/null +++ b/week08/valid-anagram.java @@ -0,0 +1,75 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 +// +// 示例 1: +// +// 输入: s = "anagram", t = "nagaram" +//输出: true +// +// +// 示例 2: +// +// 输入: s = "rat", t = "car" +//输出: false +// +// 说明: +//你可以假设字符串只包含小写字母。 +// +// 进阶: +//如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况? +// Related Topics 排序 哈希表 +// 👍 267 👎 0有效的字母异位词 + + +import java.lang.reflect.Array; +import java.util.Arrays; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接;https://leetcode-cn.com/problems/valid-anagram/ + public boolean isAnagram(String s, String t) { + // 排序 +// return func2(s, t); + + // HashMap存储每一个字符出现的次数,在第二个字符串中每遇到一个就把这个次数减一。 + // 如果最后所有的字符对应的次数都是0,就代表是 + + // 数组标记的方式,实际上你可以发现因为是字母,所以完全就不用hashmap了 +// return func1(s, t); + } + + private boolean func2(String s, String t) { + if (s.length() != t.length()) { + return false; + } + + char[] s1 = s.toCharArray(); + char[] s2 = t.toCharArray(); + + Arrays.sort(s1); + Arrays.sort(s2); + return Arrays.equals(s1, s2); + } + + private boolean func1(String s, String t) { + if (s.length() != t.length()) { + return false; + } + + int[] tag = new int[26]; + + for (char c : s.toCharArray()) { + tag[c - 'a']++; + } + + for (char c : t.toCharArray()) { + tag[c - 'a']--; + if (tag[c - 'a'] < 0) { + return false; + } + } + + return true; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/NOTE.md b/week09/NOTE.md index 50de3041..197ebdfe 100644 --- a/week09/NOTE.md +++ b/week09/NOTE.md @@ -1 +1,246 @@ -学习笔记 \ No newline at end of file +学习笔记 + +**算法训练营结束了,我在此再次感谢覃超老师和助教老师的辛勤付出。因为有你们,我才有机会跟着厉害的人学习。因为有你们,让我内心备受鼓舞,在无数个崩溃的夜晚坚持了下来。因为有你们,作为一个菜鸟的我才真正开始走上了自己逆袭的道路。我真心的说一句:谢谢。** + + + +**接下来是我算法训练营学习的一些心得。** + + + + + +### 一. 写代码学会写注释 + +**以下是合并区间这道题目的相关代码:** + +```java +for (int i = 0; i < intervals.length; i++) { + // 得到每个区间的第一个元素 + int first = intervals[i][0]; + // 得到每个区间的第二个元素 + int second = intervals[i][1]; + // 如果合并的列表长度为0或者合并列表的最后一个区间的右区间比当前区间的左区间小 + // 证明他们不可能有交集,直接在结果集中增加一个区间即可。如(3,5),(6,7) + if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < first) { + merged.add(new int[]{first, second}); + } else { + // 否则,就要合并区间,如(3,5),(4,6)或者(3,7)(4,5) + merged.get(merged.size() - 1)[1] = Math.max(second, merged.get(merged.size() - 1)[1]); + } +} +``` + + + +**以下是最长公共子序列这道题目的相关代码:** + +```java +for (int i = 1; i < length1; i++) { + for (int j = 1; j < length2; j++) { + // 1. + // 如果当前两个字符串目前为止最后一个字符相等,那么我只需要知道他们两个的子序列的公共长度,再加1即可 + // 比如 ****A 和 %%%%%A,A和A相等了,那么只需要看****和%%%%的最长公共子序列是多少然后加上1即可 + // 2. + // 否则,假如现在是****A,%%%%B,就要看****A和%%%%的公共子序列长度与****和%%%%B的最大值是多少 + if (t1[i - 1] == t2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } +} +``` + + + +### 二. N皇后II 位运算讲解 + +**1. 先贴上整体代码:** + +```java +// 边界条件 +if (n < 1) { + return 0; +} + +dfs(n, 0, 0, 0, 0); +return count; +``` + +```java +public void dfs(int n, int row, int lie, int pie, int na) { + // 递归终止条件, 每一次能够走到最后一行都对应N皇后的一种解法 + if (row >= n) { + count+=1; + return; + } + + int bits = (~(lie | pie | na)) & ((1 << n) - 1); // (1) + + // 循环开始判断 + while (bits != 0) { + // 取到最低的那一位 + int p = bits & -bits; // (2) + // 既然决定将p这个位置放上皇后,那bits中自然要去掉他 + bits = bits & (bits - 1); // (3) + + // 到这里位置都是在处理当前层逻辑 + + dfs(n, row + 1, lie | p, (pie | p) << 1, (na | p) >> 1); // (4) + + // 不需要回退lie, pie, na的状态,因为我是去与,并没有真正改掉他们的值。 + } +} +``` + + + +**2.代码注释中(1)处代码讲解:** + +``` +N皇后,我们就可以用二进制的N位来表示。如8皇后,0 0 0 1 0 0 1 0 表示当前层从左往右第4个格子和第7个格子是可以放皇后的,其他格子放进去都会有冲突。 +所以,每一层进来,我们都要求出当前层这个二进制数来知道当前层是有哪些位子已经被占有了,哪些位子可以放皇后。 + +代码中 lie, pie, na我们知道分别代表了当前层同一列,右斜上, 左斜上。有哪些位子是不能放皇后的, 那么 (lie | pie | na) 就可以得到当前层哪些位子是不能放皇后的。下面举的例子我们都以8皇后为例。 + +如:若 (lie | pie | na) = 000000000000000000000000 10101001 +前面一大串0是因为int类型是32位的,所以对于8皇后而言,前面的24位都是无用的。上面的情况易知当前层第1个,第3个,第5个,第8个格子都不可以放皇后。 + +所以我们对其取反: +则:(~(lie | pie | na)) = 111111111111111111111111 01010110, 这样最低的8位,为1的地方就是当前层可以放皇后的位置。但是这个操作做了之后,前面的24位也变成了1。而这些1是我们不需要的。所以要想办法将其去掉。 + +(1 << n) = 000000000000000000000001 00000000 (还是以8皇后为例,这里的n就是8) +``` + +``` +那不难得出: +((1 << n) - 1) = 000000000000000000000000 11111111。 +(~(lie | pie | na)) = 111111111111111111111111 01010110 + +(~(lie | pie | na)) & ((1 << n) - 1) = 000000000000000000000000 01010110 = bits + +这个时候算出来的bits, 二进制位为1的位就真正代表可以放皇后了。 +``` + + + +**3.代码注释中(2)处代码讲解:** + +``` +上面代码我们得到了哪些位置可以放皇后。再来回顾下那个二进制串: + +000000000000000000000000 01010110 = bits + +代码(2)这个操作就是位运算操作,可以得到二进制中最低位的那个1 +bits & -bits = 000000000000000000000000 00000010 = p = 2(十进制) +``` + + + +**4.代码注释中 (3)处代码讲解:** + +``` +000000000000000000000000 01010110 = bits + +// 既然决定将p这个位置放上皇后,那bits中自然要去掉他,这样能够保证回溯回来的时候这个位子也不能再用了 + bits = bits & (bits - 1) = 000000000000000000000000 01010100 +``` + + + +**5.代码注释中(4)处代码讲解:** + +``` +dfs(n, row + 1, col | p, (pie | p) << 1, (na | p) >> 1); + +col | p就是告诉下一层,有哪些列是被占着的。 +pie | p就是告诉下一层,有些pie的位置是被占着的。但是我们这个是二进制位注意,pie的位子下面一层是感知不到的。举个例子: +假如当前层取到的p的位子是00001000,第五个格子放了皇后了,那么对于pie而言,下一层不能访问的是第4个格子,也就是00010000这样的表示: +00001000 +00010000 +所以 (pie | p) 要左移一位 + +na正好相反: +00001000 +00000100 +所以 (na | p) 要右移一位 +``` + + + +### **三. N皇后I 位运算讲解** + +```java +// 定义结果集 +List> res = new ArrayList<>(); + +public List> solveNQueens(int n) { + // 边界条件 + if (n < 1) { + return res; + } + + // 申请一个char棋盘 + char[][] chess = new char[n][n]; + // 初始化棋盘 + for (char[] everyRow : chess) { + Arrays.fill(everyRow, '.'); + } + + // 开始回溯 + dfs2(n, 0, 0, 0, 0, chess); + return res; +} +``` + + + +```java +public void dfs2(int n, int row, int lie, int pie, int na, char[][] chess) { + if (row >= n) { + res.add(transfer(chess)); // 此处transfer只是把二维的char变成List + return; + } + + // 得到当前层的bits + int bits = (~(lie | pie | na)) & ((1 << n) - 1); + + int count = 0; + while (bits != 0) { + int p = bits & -bits; + + int index = (int) (Math.log(p) / Math.log(2)); // (1)根据当前p求出1在二进制中的位置 + + bits = bits & (bits - 1); + + chess[row][n - index - 1] = 'Q'; + + dfs2(n, row + 1, lie | p, (pie | p) << 1, (na | p) >> 1, chess); + + chess[row][n - index - 1] = '.'; + } +} +``` + +**1.代码注释中(1)位置的代码讲解**: + +```java +我们每一层都要在当前行的某一列放一个皇后,所以我们要根据p这个数字求出他对应的二进制中1所在的位置,1的位置就是代表当前行皇后的列。举个例子: +00000010, 这个二进制数的十进制代表2。 +实际上就是 1 * 2^1 = 2; 也就是1的位置是从右往左第1位(下标从0开始)。 + +而我们要求的实际上就是根据十进制的2求这个1 (指数),也就是对应二进制中1所在位置。这个就是用到了取对数 +指数 = log2 (P) = log2 (2) = 1 + +所以我们根据p就可以求出指数,也就是二进制中1的位子,也就是可以放皇后的下标(列) +index = Math.log(p) / Math.log(2) = log2 (P) +log2 (P) = loge (p) / loge (2); + +刚刚也说到,对00000010这个数取对数得到的1。这个是从右往左的位置,但我们的列数下标是要从左往右的 +col = n - index - 1; +col = 8 - 1 -1 = 6; +这样符合我们的预期 +00000010 本就代表第7列,也就是数组中下标为6的地方可以放皇后 +``` + diff --git a/week09/decode-ways.java b/week09/decode-ways.java new file mode 100644 index 00000000..3ee23443 --- /dev/null +++ b/week09/decode-ways.java @@ -0,0 +1,124 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//一条包含字母 A-Z 的消息通过以下方式进行了编码: +// +// +//'A' -> 1 +//'B' -> 2 +//... +//'Z' -> 26 +// +// +// 给定一个只包含数字的非空字符串,请计算解码方法的总数。 +// +// 题目数据保证答案肯定是一个 32 位的整数。 +// +// +// +// 示例 1: +// +// +//输入:s = "12" +//输出:2 +//解释:它可以解码为 "AB"(1 2)或者 "L"(12)。 +// +// +// 示例 2: +// +// +//输入:s = "226" +//输出:3 +//解释:它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。 +// +// +// 示例 3: +// +// +//输入:s = "0" +//输出:0 +// +// +// 示例 4: +// +// +//输入:s = "1" +//输出:1 +// +// +// 示例 5: +// +// +//输入:s = "2" +//输出:1 +// +// +// +// +// 提示: +// +// +// 1 <= s.length <= 100 +// s 只包含数字,并且可能包含前导零。 +// +// Related Topics 字符串 动态规划 +// 👍 564 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class 解码方法 { + // 题目链接:https://leetcode-cn.com/problems/decode-ways/ + public int numDecodings(String s) { + // 时间复杂度:O(N),一层遍历 + // 空间复杂度:O(N),额外开了一个一维数组 + return func1(s); + } + + private int func1(String s) { + // 1. 找重复性。当前字符串的解码方式分为几种情况。第一种当前s[i]为0, 如果s[i - 1]为1,或者2,证明最后两位可以拼成一个字母。那么以当前字符结尾的字符串的编码方式就 + // 是dp[i - 2].否则就是0。第二种如果当前s[i]不为0,那么至少dp[i] = dp[i - 1],也就是最后一位自己单独组。然后如果倒数第二位和倒数第一位组合的数字在[11, 26]之间, + // 证明最后两位也可以组成一组成为一个字母,这种情况下dp[i]要再加一个dp[i - 2] + // 2. 定义状态数组:dp[i] ,其中dp[i]表示以s[i]结尾的字符串有多少种编码方式 + // 3. DP方程:if s[i] = 0 && s[i-1] = 1 || 2, dp[i] = dp[i - 2]. + // if s[i] != 0 dp[i] = dp[i - 1], if 11 <= s[i - 1]s[i] <= 26, dp[i] += dp[i - 2] + // 实际上上面的思路还可以合并,具体在代码里体现 + + // 边界条件 + if (s.charAt(0) == '0') { + return 0; + } + + // 将字符串转化为字符数组 + char[] sArray = s.toCharArray(); + // 得到字符串的长度 + int length = s.length(); + + // 申请dp数组 + int[] dp = new int[length]; + // 初始化dp数组 + dp[0] = 1; + + // 循环dp + for (int i = 1; i < sArray.length; i++) { + // 如果最后一个字符不是0,铁定的可以单独解码出一个字母,这种情况dp[i]至少也是dp[i - 1] + if (sArray[i] != '0') { + dp[i] = dp[i - 1]; + } + + // 算出最后两个数的数值 + int num = 10 * (sArray[i - 1] - '0') + (sArray[i] - '0'); + + // 如果最后两个数在10 - 26之间,那么证明最后两个数是可以合并在一起解码一个字母的,那么dp[i]还要加上dp[i - 2]。 + // 这里其实就把最后一个字符是0的情况包含进去了。即便你是0,也只有10,20是符合要求的,而10,20都在10-26区间 + if (num >= 10 && num <= 26) { + // 这里如果i是1下标会越界,所以我们做一个特殊判断 + if (i == 1) { + dp[i] += 1; + } else { + dp[i] += dp[i - 2]; + } + } + } + + return dp[length - 1]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/distinct-subsequences.java b/week09/distinct-subsequences.java new file mode 100644 index 00000000..7a2b380f --- /dev/null +++ b/week09/distinct-subsequences.java @@ -0,0 +1,73 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 +// +// 字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列 +//,而 "AEC" 不是) +// +// 题目数据保证答案符合 32 位带符号整数范围。 +// +// +// +// 示例 1: +// +// +//输入:s = "rabbbit", t = "rabbit" +//输出:3 +//解释: +//如下图所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。 +//(上箭头符号 ^ 表示选取的字母) +//rabbbit +//^^^^ ^^ +//rabbbit +//^^ ^^^^ +//rabbbit +//^^^ ^^^ +// +// +// 示例 2: +// +// +//输入:s = "babgbag", t = "bag" +//输出:5 +//解释: +//如下图所示, 有 5 种可以从 s 中得到 "bag" 的方案。 +//(上箭头符号 ^ 表示选取的字母) +//babgbag +//^^ ^ +//babgbag +//^^ ^ +//babgbag +//^ ^^ +//babgbag +// ^ ^^ +//babgbag +// ^^^ +// +// +// +// 提示: +// +// +// 0 <= s.length, t.length <= 1000 +// s 和 t 由英文字母组成 +// +// Related Topics 字符串 动态规划 +// 👍 288 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution40 { + // 题目链接:https://leetcode-cn.com/problems/distinct-subsequences/ + public int numDistinct(String s, String t) { + int[][] dp = new int[t.length() + 1][s.length() + 1]; + for (int j = 0; j < s.length() + 1; j++) dp[0][j] = 1; + for (int i = 1; i < t.length() + 1; i++) { + for (int j = 1; j < s.length() + 1; j++) { + if (t.charAt(i - 1) == s.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1]; + else dp[i][j] = dp[i][j - 1]; + } + } + return dp[t.length()][s.length()]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/find-all-anagrams-in-a-string.java b/week09/find-all-anagrams-in-a-string.java new file mode 100644 index 00000000..0a31d6e1 --- /dev/null +++ b/week09/find-all-anagrams-in-a-string.java @@ -0,0 +1,151 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。 +// +// 字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。 +// +// 说明: +// +// +// 字母异位词指字母相同,但排列不同的字符串。 +// 不考虑答案输出的顺序。 +// +// +// 示例 1: +// +// +//输入: +//s: "cbaebabacd" p: "abc" +// +//输出: +//[0, 6] +// +//解释: +//起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。 +//起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。 +// +// +// 示例 2: +// +// +//输入: +//s: "abab" p: "ab" +// +//输出: +//[0, 1, 2] +// +//解释: +//起始索引等于 0 的子串是 "ab", 它是 "ab" 的字母异位词。 +//起始索引等于 1 的子串是 "ba", 它是 "ab" 的字母异位词。 +//起始索引等于 2 的子串是 "ab", 它是 "ab" 的字母异位词。 +// +// Related Topics 哈希表 +// 👍 431 👎 0 + + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution36 { + + public static void main(String[] args) { +// Solution36 s = new Solution36(); +// s.findAnagrams("cbaebabacd", "abc"); + + } + + // 题目链接:https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/ + public List findAnagrams(String s, String p) { + // 错误思路 +// return func1(s, p); + + // 滑动窗口 + // 时间复杂度:O(MN) + // 空间复杂度:O(N) + return func2(s, p); + } + + private List func2(String s, String p) { + List res = new ArrayList<>(); + + if (s.length() ==0 || p.length() ==0 || p.length() > s.length()) { + return res; + } + + int lenS = s.length(); + int lenP = p.length(); + int start = 0, end = 0; + + int[] dict = new int[26]; + char[] sArray = s.toCharArray(); + char[] pArray = p.toCharArray(); + + for (char c : pArray) { + dict[c - 'a']++; + } + int[] window = new int[26]; + + while (end < lenS) { + int current = sArray[end] - 'a'; + end++; + window[current] += 1; + while (window[current] > dict[current]) { + int curL = sArray[start] - 'a'; + window[curL] -= 1; + start++; + } + if (end - start == lenP) { + res.add(start); + } + } + + return res; + } + + private List func1(String s, String p) { + // 定义结果集 + List res = new ArrayList<>(); + + // 边界条件 + if (p.length() > s.length()) { + return res; + } + + int lenP = p.length(); + + char[] sAraay = s.toCharArray(); + char[] pArray = s.toCharArray(); + int[] temp = new int[26]; + + for (char c : pArray) { + temp[c - 'a']++; + } + + int[] everyTemp = temp; + + int start = 0, lenS = s.length(); + while (start < lenS) { + while (start < lenP) { + everyTemp[sAraay[start] - 'a']--; + start++; + } + + boolean tag = true; + for (int i : everyTemp) { + if (i != 0) { + tag = false; + break; + } + } + + if (tag) { + res.add(start - lenP); + } + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/first-unique-character-in-a-string.java b/week09/first-unique-character-in-a-string.java new file mode 100644 index 00000000..4a19955b --- /dev/null +++ b/week09/first-unique-character-in-a-string.java @@ -0,0 +1,100 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串,找到它的第一个不重复的字符,并返回它的索引。如果不存在,则返回 -1。 +// +// +// +// 示例: +// +// s = "leetcode" +//返回 0 +// +//s = "loveleetcode" +//返回 2 +// +// +// +// +// 提示:你可以假定该字符串只包含小写字母。 +// Related Topics 哈希表 字符串 +// 👍 297 👎 0 + + +import java.util.HashMap; +import java.util.Map; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution25 { + // 题目链接:https://leetcode-cn.com/problems/first-unique-character-in-a-string/ + public int firstUniqChar(String s) { + // 暴力法,超时 + // 时间复杂度:O(N^2),两层for循环 + // 空间复杂度:O(N),用了一个一维数组 +// return func1(s); + + // HashMap + // 时间复杂度:O(N), 两个for循环并列 + // 空间复杂度:O(N), 用了一个HashMap +// return func2(s); + + // 用一个数组 + // 时间复杂度:O(N), 两个for循环并列 + // 空间复杂度:O(1), 用了一个一维数组,长度26 + return func3(s); + } + + + private int func3(String s) { + int[] tag = new int[26]; + + char[] sArray = s.toCharArray(); + + for (int i = 0; i < sArray.length; i++) { + tag[sArray[i] - 'a'] ++; + } + + for (int i = 0; i < sArray.length; i++) { + if (tag[sArray[i] - 'a'] == 1) { + return i; + } + } + + return -1; + } + + private int func2(String s) { + Map hashMap = new HashMap<>(); + + char[] sArray = s.toCharArray(); + + for (int i = 0; i < sArray.length; i++) { + hashMap.put(sArray[i], hashMap.getOrDefault(sArray[i], 0) + 1); + } + + for (int i = 0; i < sArray.length; i++) { + if (hashMap.get(sArray[i]) == 1) { + return i; + } + } + + return -1; + } + + private int func1(String s) { + char[] sArray = s.toCharArray(); + + for (int i = 0; i < sArray.length; i++) { + int count = 0; + for (int j = 0; j < sArray.length; j++) { + if (i != j && sArray[i] == sArray[j]) { + count++; + } + } + if (count == 0) { + return i; + } + } + + return -1; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/isomorphic-strings.java b/week09/isomorphic-strings.java new file mode 100644 index 00000000..21273cc3 --- /dev/null +++ b/week09/isomorphic-strings.java @@ -0,0 +1,64 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定两个字符串 s 和 t,判断它们是否是同构的。 +// +// 如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。 +// +// 所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。 +// +// 示例 1: +// +// 输入: s = "egg", t = "add" +//输出: true +// +// +// 示例 2: +// +// 输入: s = "foo", t = "bar" +//输出: false +// +// 示例 3: +// +// 输入: s = "paper", t = "title" +//输出: true +// +// 说明: +//你可以假设 s 和 t 具有相同的长度。 +// Related Topics 哈希表 +// 👍 276 👎 0 + + +import java.util.HashMap; +import java.util.Map; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution33 { + // 题目链接:https://leetcode-cn.com/problems/isomorphic-strings/ + public boolean isIsomorphic(String s, String t) { + if (s.length() != t.length()) { + return false; + } + + char[] sArray = s.toCharArray(); + char[] tArray = t.toCharArray(); + Map hashMap1 = new HashMap<>(); + Map hashMap2 = new HashMap<>(); + + for (int i = 0; i < sArray.length; i++) { + if (hashMap1.containsKey(sArray[i])) { + if (hashMap1.get(sArray[i]) != tArray[i]) { + return false; + } + } else if (hashMap2.containsKey(tArray[i])) { + if (hashMap2.get(tArray[i]) != sArray[i]) { + return false; + } + } else { + hashMap1.put(sArray[i], tArray[i]); + hashMap2.put(tArray[i], sArray[i]); + } + } + + return true; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/longest-increasing-subsequence.java b/week09/longest-increasing-subsequence.java new file mode 100644 index 00000000..e5560d1a --- /dev/null +++ b/week09/longest-increasing-subsequence.java @@ -0,0 +1,118 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 +// +// 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序 +//列。 +// +// +// 示例 1: +// +// +//输入:nums = [10,9,2,5,3,7,101,18] +//输出:4 +//解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。 +// +// +// 示例 2: +// +// +//输入:nums = [0,1,0,3,2,3] +//输出:4 +// +// +// 示例 3: +// +// +//输入:nums = [7,7,7,7,7,7,7] +//输出:1 +// +// +// +// +// 提示: +// +// +// 1 <= nums.length <= 2500 +// -104 <= nums[i] <= 104 +// +// +// +// +// 进阶: +// +// +// 你可以设计时间复杂度为 O(n2) 的解决方案吗? +// 你能将算法的时间复杂度降低到 O(n log(n)) 吗? +// +// Related Topics 二分查找 动态规划 +// 👍 1217 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/longest-increasing-subsequence/ + public int lengthOfLIS(int[] nums) { + // 思路一:动态规划 + // 定义状态数组:dp[i], 代表第i个位子结尾的最长子序列的长度 + // 状态转移方程:如果nums[i] > nums[i - 1] , dp[i] = max(dp[0...(i - 1)] + 1。否则,dp[i] = 1; + // 时间复杂度: O(N^2),两层for循环 + // 空间复杂度:O(N), 使用了一个一维数组 +// return func1(nums); + + // 思路二:贪心 + 二分 + // 动态规划说到底就是纯暴力,所以是N^2的时间复杂度,那么我们运用贪心保证一个数组d是递增的 + return func2(nums); + } + + private int func2(int[] nums) { + int len = 1, n = nums.length; + if (n == 0) { + return 0; + } + + int[] d = new int[n + 1]; + d[len] = nums[0]; + + for (int i = 1; i < n; i++) { + if (nums[i] > d[len]) { + d[++len] = nums[i]; + } else { + int l = 1, r = len, pos = 0; + while (l <= r) { + int mid = l + ((r - l) >> 1); + if (d[mid] < nums[i]) { + pos = mid; + l = mid + 1; + } else { + r = mid - 1; + } + } + // pos这时就记录了最右边的比nums[i]小的位置 + d[pos + 1] = nums[i]; + } + } + + return len; + } + + private int func1(int[] nums) { + // 得到数组的长度 + int len = nums.length; + // 定义最终的结果,也就是最长的上升序列 + int res = 0; + // 申请一维的dp数组 + int[] dp = new int[len]; + for (int i = 0; i < len; i++) { + dp[i] = 1; + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + dp[i] = Math.max(dp[j] + 1, dp[i]); + } + } + res = Math.max(res, dp[i]); + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/longest-palindromic-substring.java b/week09/longest-palindromic-substring.java new file mode 100644 index 00000000..f24ce461 --- /dev/null +++ b/week09/longest-palindromic-substring.java @@ -0,0 +1,103 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。 +// +// 示例 1: +// +// 输入: "babad" +//输出: "bab" +//注意: "aba" 也是一个有效答案。 +// +// +// 示例 2: +// +// 输入: "cbbd" +//输出: "bb" +// +// Related Topics 字符串 动态规划 +// 👍 3020 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/longest-palindromic-substring/ + public String longestPalindrome(String s) { + // 枚举回文中心,超时,自己的版本 +// return func1(s); + + int len = s.length(); + if (len < 2) { + return s; + } + int maxLen = 1; + String res = s.substring(0, 1); + // 中心位置枚举到 len - 2 即可 + for (int i = 0; i < len - 1; i++) { + String oddStr = centerSpread(s, i, i); + String evenStr = centerSpread(s, i, i + 1); + String maxLenStr = oddStr.length() > evenStr.length() ? oddStr : evenStr; + if (maxLenStr.length() > maxLen) { + maxLen = maxLenStr.length(); + res = maxLenStr; + } + } + return res; + + } + + private String centerSpread(String s, int left, int right) { + // left = right 的时候,此时回文中心是一个字符,回文串的长度是奇数 + // right = left + 1 的时候,此时回文中心是一个空隙,回文串的长度是偶数 + int len = s.length(); + int i = left; + int j = right; + while (i >= 0 && j < len) { + if (s.charAt(i) == s.charAt(j)) { + i--; + j++; + } else { + break; + } + } + // 这里要小心,跳出 while 循环时,恰好满足 s.charAt(i) != s.charAt(j),因此不能取 i,不能取 j + return s.substring(i + 1, j); + } + + private String func1(String s) { + int mid = 0; + char[] sArray = s.toCharArray(); + int len = s.length(); + String res = ""; + + while (mid < len) { + String temp = ""; + int left = mid - 1, right = mid + 1; + while (left >= 0 && right < len) { + if (sArray[left] == sArray[right]) { + temp = s.substring(left, right); + left--; + right++; + } else { + break; + } + } + res = temp.length() > res.length() ? temp : res; + + left = mid; + right = mid + 1; + while (left >= 0 && right < len) { + if (sArray[left] == sArray[right]) { + temp = s.substring(left, right); + left--; + right++; + } else { + break; + } + } + + res = temp.length() > res.length() ? temp : res; + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/longest-valid-parentheses.java b/week09/longest-valid-parentheses.java new file mode 100644 index 00000000..a731338f --- /dev/null +++ b/week09/longest-valid-parentheses.java @@ -0,0 +1,118 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个只包含 '(' 和 ')' 的字符串,找出最长的包含有效括号的子串的长度。 +// +// 示例 1: +// +// 输入: "(()" +//输出: 2 +//解释: 最长有效括号子串为 "()" +// +// +// 示例 2: +// +// 输入: ")()())" +//输出: 4 +//解释: 最长有效括号子串为 "()()" +// +// Related Topics 字符串 动态规划 +// 👍 1085 👎 0 + + +import java.util.Deque; +import java.util.LinkedList; +import java.util.Stack; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution5 { + // 题目链接:https://leetcode-cn.com/problems/longest-valid-parentheses/ + public int longestValidParentheses(String s) { + // 时间复杂度:O(N),一层for循环 + // 空间复杂度:O(N),用了一个栈 +// return func1(s); + + // 时间复杂度:O(N),一层for循环 + // 空间复杂度:O(N),用了一个栈 + return func2(s); + + } + + /** + * 思路二:动态规划 + * + * @param s 原始字符串 + * @return 最大有效括号数 + */ + private int func2(String s) { + // 1. 找重复性。 + // 2. 定义状态数组:dp[i]表示以i结尾的字符串的最大有效括号数量 + // 3. 如果当前下标表示的字符是')',并且他的前一个是左括号。那么dp[i] = dp[i - 2] + 2 + // 反之,如果他的前一个不是左括号,而是右括号。在这个基础上 + // 如果s[i - dp[i - 1] - 1]代表的字符是左括号。如****(()())这种情况。 + // dp[i] = dp[i - 1] + dp[i - dp[i - 1] - 2] + 2。其中dp[i - 1]指的是()()这两个括号 + // 的长度,可以看到的(()())最外面的两个括号就是 2, 而***代表的就是dp[i-dp[i-1]-2] + + // 获取字符串长度 + int length = s.length(); + // 申请dp数组 + int[] dp = new int[length]; + // 定义最大值 + int max = 0; + + // 循环dp + for (int i = 1; i < length; i++) { + if (s.charAt(i) == ')') { + if (s.charAt(i - 1) == '(') { + dp[i] = dp[Math.max(i - 2, 0)] + 2; + } else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') { + dp[i] = dp[i - 1] + dp[Math.max(i - dp[i - 1] - 2, 0)] + 2; + } + max = Math.max(dp[i], max); + } + } + + return max; + } + + /** + * 思路一:栈 + * + * @param s 原字符串 + * @return 最大有效括号数 + */ + private int func1(String s) { + // 用栈去模拟 + // 将字符串转化为字符数组 + char[] sA = s.toCharArray(); + // 定义返回结果 + int res = 0; + // 获取字符串长度 + int length = s.length(); + + // 申请一个栈 + Deque stack = new LinkedList<>(); + // 防止第一个放进去的有括号的情况 + stack.push(-1); + + // 遍历字符串 + for (int i = 0; i < length; i++) { + // 如果遇到左括号,将它的下标入栈即可 + if (sA[i] == '(') { + stack.push(i); + } else { + // 如果遇到右括号 + // 1. 先出栈一个元素 + stack.pop(); + // 2. 如果此时栈为空,证明出掉的是我们最开始push进去的-1,那么我们将当前右括号的下标入栈 + // 3. 如果栈不为空,证明当前右括号被匹配了。那么我们计算比较一下以前的最大值以及当前右括号下标和栈顶的差值,得出最大值即可 + if (stack.isEmpty()) { + stack.push(i); + } else { + res = Math.max(res, i - stack.peek()); + } + } + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/race-car.java b/week09/race-car.java new file mode 100644 index 00000000..f73bf241 --- /dev/null +++ b/week09/race-car.java @@ -0,0 +1,87 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//你的赛车起始停留在位置 0,速度为 +1,正行驶在一个无限长的数轴上。(车也可以向负数方向行驶。) +// +// 你的车会根据一系列由 A(加速)和 R(倒车)组成的指令进行自动驾驶 。 +// +// 当车得到指令 "A" 时, 将会做出以下操作: position += speed, speed *= 2。 +// +// 当车得到指令 "R" 时, 将会做出以下操作:如果当前速度是正数,则将车速调整为 speed = -1 ;否则将车速调整为 speed = 1。 (当前所 +//处位置不变。) +// +// 例如,当得到一系列指令 "AAR" 后, 你的车将会走过位置 0->1->3->3,并且速度变化为 1->2->4->-1。 +// +// 现在给定一个目标位置,请给出能够到达目标位置的最短指令列表的长度。 +// +// 示例 1: +//输入: +//target = 3 +//输出: 2 +//解释: +//最短指令列表为 "AA" +//位置变化为 0->1->3 +// +// +// 示例 2: +//输入: +//target = 6 +//输出: 5 +//解释: +//最短指令列表为 "AAARA" +//位置变化为 0->1->3->7->7->6 +// +// +// 说明: +// +// +// 1 <= target(目标位置) <= 10000。 +// +// Related Topics 堆 动态规划 +// 👍 83 👎 0 + + +import java.util.Arrays; +import java.util.PriorityQueue; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution38 { + // 题目链接:https://leetcode-cn.com/problems/race-car/ + public int racecar(int target) { + int K = 33 - Integer.numberOfLeadingZeros(target - 1); + int barrier = 1 << K; + int[] dist = new int[2 * barrier + 1]; + Arrays.fill(dist, Integer.MAX_VALUE); + dist[target] = 0; + + PriorityQueue pq = new PriorityQueue( + (a, b) -> a.steps - b.steps); + pq.offer(new Node(0, target)); + + while (!pq.isEmpty()) { + Node node = pq.poll(); + int steps = node.steps, targ1 = node.target; + if (dist[Math.floorMod(targ1, dist.length)] > steps) continue; + + for (int k = 0; k <= K; ++k) { + int walk = (1 << k) - 1; + int targ2 = walk - targ1; + int steps2 = steps + k + (targ2 != 0 ? 1 : 0); + + if (Math.abs(targ2) <= barrier && steps2 < dist[Math.floorMod(targ2, dist.length)]) { + pq.offer(new Node(steps2, targ2)); + dist[Math.floorMod(targ2, dist.length)] = steps2; + } + } + } + + return dist[0]; + } + + class Node { + int steps, target; + Node(int s, int t) { + steps = s; + target = t; + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/reverse-only-letters.java b/week09/reverse-only-letters.java new file mode 100644 index 00000000..79fe8222 --- /dev/null +++ b/week09/reverse-only-letters.java @@ -0,0 +1,73 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串 S,返回 “反转后的” 字符串,其中不是字母的字符都保留在原地,而所有字母的位置发生反转。 +// +// +// +// +// +// +// 示例 1: +// +// 输入:"ab-cd" +//输出:"dc-ba" +// +// +// 示例 2: +// +// 输入:"a-bC-dEf-ghIj" +//输出:"j-Ih-gfE-dCba" +// +// +// 示例 3: +// +// 输入:"Test1ng-Leet=code-Q!" +//输出:"Qedo1ct-eeLg=ntse-T!" +// +// +// +// +// 提示: +// +// +// S.length <= 100 +// 33 <= S[i].ASCIIcode <= 122 +// S 中不包含 \ or " +// +// Related Topics 字符串 +// 👍 70 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/reverse-only-letters/ + public String reverseOnlyLetters(String S) { + StringBuilder sb = new StringBuilder(S); + + reverse(sb, 0, sb.length() - 1); + return sb.toString(); + } + + public static void main(String[] args) { +// Solution32 t = new Solution32(); +// t.reverseOnlyLetters("ab-cd"); + } + + public void reverse(StringBuilder sb, int start, int end) { + while (start < end) { + char startWord = sb.charAt(start); + char endWord = sb.charAt(end); + if (! (('a' <= startWord && startWord <= 'z') || ('A' <= startWord && startWord <= 'Z'))) { + start += 1; + continue; + } else if (! (('a' <= endWord && endWord <= 'z') || ('A' <= endWord && endWord <= 'Z'))) { + end -= 1; + continue; + } else { + char temp = startWord; + sb.setCharAt(start++, endWord); + sb.setCharAt(end--, temp); + } + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/reverse-string-ii.java b/week09/reverse-string-ii.java new file mode 100644 index 00000000..26f481a7 --- /dev/null +++ b/week09/reverse-string-ii.java @@ -0,0 +1,66 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。 +// +// +// 如果剩余字符少于 k 个,则将剩余字符全部反转。 +// 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。 +// +// +// +// +// 示例: +// +// 输入: s = "abcdefg", k = 2 +//输出: "bacdfeg" +// +// +// +// +// 提示: +// +// +// 该字符串只包含小写英文字母。 +// 给定字符串的长度和 k 在 [1, 10000] 范围内。 +// +// Related Topics 字符串 +// 👍 107 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution28 { + // 题目链接:https://leetcode-cn.com/problems/reverse-string-ii/ + + public static void main(String[] args) { + Solution28 solution = new Solution28(); + solution.reverseStr("abcdefg", 2); + } + + public String reverseStr(String s, int k) { + StringBuilder sb = new StringBuilder(s); + + int start = 0, end = s.length() - 1; + while (start <= end) { + if (end - start + 1 < k) { + reverse(sb, start, end); + break; + } else if (end - start + 1 >= k && end - start + 1 < 2 * k) { + reverse(sb, start, start + k - 1); + break; + } else { + reverse(sb, start, start + k - 1); + start += 2 * k; + } + } + + return sb.toString(); + } + + public void reverse(StringBuilder sb, int start, int end) { + while (start < end) { + char temp = sb.charAt(start); + sb.setCharAt(start++, sb.charAt(end)); + sb.setCharAt(end--, temp); + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/reverse-words-in-a-string-iii.java b/week09/reverse-words-in-a-string-iii.java new file mode 100644 index 00000000..4bc17430 --- /dev/null +++ b/week09/reverse-words-in-a-string-iii.java @@ -0,0 +1,51 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。 +// +// +// +// 示例: +// +// 输入:"Let's take LeetCode contest" +//输出:"s'teL ekat edoCteeL tsetnoc" +// +// +// +// +// 提示: +// +// +// 在字符串中,每个单词由单个空格分隔,并且字符串中不会有任何额外的空格。 +// +// Related Topics 字符串 +// 👍 260 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/reverse-words-in-a-string-iii/ + public String reverseWords(String s) { + StringBuilder sb = new StringBuilder(s); + + int start = 0, end = 0, len = s.length(); + while (end < len) { + if ((end + 1 < len && sb.charAt(end + 1) == ' ') || end + 1 == len) { + reverse(sb, start, end); + start = end + 2; + end = start; + } else { + end++; + } + } + + return sb.toString(); + } + + public void reverse(StringBuilder sb, int start, int end) { + while (start < end) { + char temp = sb.charAt(start); + sb.setCharAt(start++, sb.charAt(end)); + sb.setCharAt(end--, temp); + } + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/reverse-words-in-a-string.java b/week09/reverse-words-in-a-string.java new file mode 100644 index 00000000..f113d1f4 --- /dev/null +++ b/week09/reverse-words-in-a-string.java @@ -0,0 +1,179 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串,逐个翻转字符串中的每个单词。 +// +// 说明: +// +// +// 无空格字符构成一个 单词 。 +// 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 +// 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 +// +// +// +// +// 示例 1: +// +// 输入:"the sky is blue" +//输出:"blue is sky the" +// +// +// 示例 2: +// +// 输入:"  hello world!  " +//输出:"world! hello" +//解释:输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 +// +// +// 示例 3: +// +// 输入:"a good   example" +//输出:"example good a" +//解释:如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 +// +// +// 示例 4: +// +// 输入:s = " Bob Loves Alice " +//输出:"Alice Loves Bob" +// +// +// 示例 5: +// +// 输入:s = "Alice does not even like bob" +//输出:"bob like even not does Alice" +// +// +// +// +// 提示: +// +// +// 1 <= s.length <= 104 +// s 包含英文大小写字母、数字和空格 ' ' +// s 中 至少存在一个 单词 +// +// +// +// +// +// +// +// 进阶: +// +// +// 请尝试使用 O(1) 额外空间复杂度的原地解法。 +// +// Related Topics 字符串 +// 👍 248 👎 0 + + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/reverse-words-in-a-string/ + public String reverseWords(String s) { + // 思路1:语言特性 + // 时间复杂度:O(N) + // 空间复杂度:O(N),用了一个列表 +// return func1(s); + + // 思路2: 自己反转,有问题,中间的单词有可能有空格 +// return func2(s); + + // 思路3:所有的函数都自己实现下 + // 去掉左右两边的空格 + return func3(s); + } + + private String func3(String s) { + StringBuilder sb = trimSpace(s); + + // 反转整个字符串 + reverseAll(sb, 0, sb.length() - 1); + + // 反转字符串中的单词 + reverseWord(sb); + + return sb.toString(); + } + + private void reverseWord(StringBuilder sb) { + int start = 0, end = 0; + int n = sb.length(); + while (start < n) { + while (end < n && sb.charAt(end) != ' ') { + end++; + } + + reverseAll(sb, start, end - 1); + start = end + 1; + ++end; + } + } + + private void reverseAll(StringBuilder sb, int start, int end) { + while (start < end) { + char temp = sb.charAt(start); + sb.setCharAt(start++, sb.charAt(end)); + sb.setCharAt(end--, temp); + } + } + + private StringBuilder trimSpace(String s) { + int len = s.length() - 1; + int start = 0; + + // 去掉字符串开头的空格 + while (start <= len && ' ' == s.charAt(start)) { + start++; + } + + // 去掉字符串结尾的空格 + while (start <= len && ' ' == s.charAt(len)) { + len--; + } + + // 去掉字符串中间的空格 + StringBuilder sb = new StringBuilder(); + while (start <= len) { + char c = s.charAt(start); + + if (c != ' ') { + sb.append(c); + } else if (sb.charAt(sb.length() - 1) != ' ') { + sb.append(c); + } + + start++; + } + + return sb; + } + + private String func2(String s) { + s = s.trim(); + String[] sArray = s.split(" "); + int end = sArray.length - 1; + int start = 0; + while (start <= end) { + String temp = sArray[start]; + sArray[start++] = sArray[end]; + sArray[end--] = temp; + } + + return String.join(" ", sArray); + } + + private String func1(String s) { + // 去掉开头和结尾的空格字符 + s = s.trim(); + List sArray = Arrays.asList(s.split(" ")); + Collections.reverse(sArray); + return String.join(" ", sArray); + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/string-to-integer-atoi.java b/week09/string-to-integer-atoi.java new file mode 100644 index 00000000..91fc8a28 --- /dev/null +++ b/week09/string-to-integer-atoi.java @@ -0,0 +1,124 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//请你来实现一个 atoi 函数,使其能将字符串转换成整数。 +// +// 首先,该函数会根据需要丢弃无用的开头空格字符,直到寻找到第一个非空格的字符为止。接下来的转化规则如下: +// +// +// 如果第一个非空字符为正或者负号时,则将该符号与之后面尽可能多的连续数字字符组合起来,形成一个有符号整数。 +// 假如第一个非空字符是数字,则直接将其与之后连续的数字字符组合起来,形成一个整数。 +// 该字符串在有效的整数部分之后也可能会存在多余的字符,那么这些字符可以被忽略,它们对函数不应该造成影响。 +// +// +// 注意:假如该字符串中的第一个非空格字符不是一个有效整数字符、字符串为空或字符串仅包含空白字符时,则你的函数不需要进行转换,即无法进行有效转换。 +// +// 在任何情况下,若函数不能进行有效的转换时,请返回 0 。 +// +// 提示: +// +// +// 本题中的空白字符只包括空格字符 ' ' 。 +// 假设我们的环境只能存储 32 位大小的有符号整数,那么其数值范围为 [−231, 231 − 1]。如果数值超过这个范围,请返回 INT_MAX (231 +// − 1) 或 INT_MIN (−231) 。 +// +// +// +// +// 示例 1: +// +// 输入: "42" +//输出: 42 +// +// +// 示例 2: +// +// 输入: " -42" +//输出: -42 +//解释: 第一个非空白字符为 '-', 它是一个负号。 +//  我们尽可能将负号与后面所有连续出现的数字组合起来,最后得到 -42 。 +// +// +// 示例 3: +// +// 输入: "4193 with words" +//输出: 4193 +//解释: 转换截止于数字 '3' ,因为它的下一个字符不为数字。 +// +// +// 示例 4: +// +// 输入: "words and 987" +//输出: 0 +//解释: 第一个非空字符是 'w', 但它不是数字或正、负号。 +// 因此无法执行有效的转换。 +// +// 示例 5: +// +// 输入: "-91283472332" +//输出: -2147483648 +//解释: 数字 "-91283472332" 超过 32 位有符号整数范围。 +//  因此返回 INT_MIN (−231) 。 +// +// Related Topics 数学 字符串 +// 👍 923 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution35 { + // 题目链接:https://leetcode-cn.com/problems/string-to-integer-atoi/ + + public static void main(String[] args) { +// Solution35 t = new Solution35(); +// t.myAtoi("42"); + } + + public int myAtoi(String s) { + int len = s.length(); + char[] sArray = s.toCharArray(); + + // 去除开头的空格 + int index = 0; + while (index < len && sArray[index] == ' ') { + index++; + } + + // 如果极端情况,字符串全部是空格(" ") + if (index == len) { + return 0; + } + + // 仅第一个非空字符是+,-号有用 + char first = sArray[index]; + int tag = 1; + if (first == '+') { + index++; + } else if (first == '-') { + index++; + tag = -1; + } + + // 开始找整数 + int res = 0; + while (index < len) { + char currentNum = sArray[index]; + // 先判断是否合法 + if (currentNum > '9' || currentNum < '0') { + break; + } + + // 题目中说:环境只能存储 32 位大小的有符号整数,因此,需要提前判:断乘以 10 以后是否越界 + if (res > Integer.MAX_VALUE / 10 || (res == Integer.MAX_VALUE / 10 && (currentNum - '0') > Integer.MAX_VALUE % 10)) { + return Integer.MAX_VALUE; + } + if (res < Integer.MIN_VALUE / 10 || (res == Integer.MIN_VALUE / 10 && (currentNum - '0') > -(Integer.MIN_VALUE % 10))) { + return Integer.MIN_VALUE; + } + + + res = res * 10 + tag * (currentNum - '0'); + index++; + } + + return res; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/valid-palindrome-ii.java b/week09/valid-palindrome-ii.java new file mode 100644 index 00000000..35fcd35b --- /dev/null +++ b/week09/valid-palindrome-ii.java @@ -0,0 +1,63 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。 +// +// 示例 1: +// +// +//输入: "aba" +//输出: True +// +// +// 示例 2: +// +// +//输入: "abca" +//输出: True +//解释: 你可以删除c字符。 +// +// +// 注意: +// +// +// 字符串只包含从 a-z 的小写字母。字符串的最大长度是50000。 +// +// Related Topics 字符串 +// 👍 299 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution34 { + // 题目链接:https://leetcode-cn.com/problems/valid-palindrome-ii/ + public boolean validPalindrome(String s) { + // 双指针加记忆化寻找 + // 时间复杂度:O(N) + // 空间复杂度:O(N) + char[] sArray = s.toCharArray(); + int start = 0, end = s.length() - 1; + int delete = 0; + // 记录上一次不相等时删除的是哪一个 + int lastLeft = -1, lastRight = -1; + while (start <= end) { + if (sArray[start] != sArray[end]) { + if (delete == 0) { + lastLeft = start; + lastRight = end; + start++; + delete++; + } else if (delete == 1){ + start = lastLeft; + end = lastRight - 1; + delete++; + } else { + return false; + } + } else { + start++; + end--; + } + } + + return true; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git a/week09/wildcard-matching.java b/week09/wildcard-matching.java new file mode 100644 index 00000000..a4187bac --- /dev/null +++ b/week09/wildcard-matching.java @@ -0,0 +1,89 @@ +package Algorithm.leetcode.leetcode.editor.cn; +//给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。 +// +// '?' 可以匹配任何单个字符。 +//'*' 可以匹配任意字符串(包括空字符串)。 +// +// +// 两个字符串完全匹配才算匹配成功。 +// +// 说明: +// +// +// s 可能为空,且只包含从 a-z 的小写字母。 +// p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。 +// +// +// 示例 1: +// +// 输入: +//s = "aa" +//p = "a" +//输出: false +//解释: "a" 无法匹配 "aa" 整个字符串。 +// +// 示例 2: +// +// 输入: +//s = "aa" +//p = "*" +//输出: true +//解释: '*' 可以匹配任意字符串。 +// +// +// 示例 3: +// +// 输入: +//s = "cb" +//p = "?a" +//输出: false +//解释: '?' 可以匹配 'c', 但第二个 'a' 无法匹配 'b'。 +// +// +// 示例 4: +// +// 输入: +//s = "adceb" +//p = "*a*b" +//输出: true +//解释: 第一个 '*' 可以匹配空字符串, 第二个 '*' 可以匹配字符串 "dce". +// +// +// 示例 5: +// +// 输入: +//s = "acdcb" +//p = "a*c?b" +//输出: false +// Related Topics 贪心算法 字符串 动态规划 回溯算法 +// 👍 575 👎 0 + + +//leetcode submit region begin(Prohibit modification and deletion) +class Solution { + // 题目链接:https://leetcode-cn.com/problems/wildcard-matching/ + public boolean isMatch(String s, String p) { + int m = s.length(); + int n = p.length(); + boolean[][] dp = new boolean[m + 1][n + 1]; + dp[0][0] = true; + for (int i = 1; i <= n; ++i) { + if (p.charAt(i - 1) == '*') { + dp[0][i] = true; + } else { + break; + } + } + for (int i = 1; i <= m; ++i) { + for (int j = 1; j <= n; ++j) { + if (p.charAt(j - 1) == '*') { + dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; + } else if (p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } + } + } + return dp[m][n]; + } +} +//leetcode submit region end(Prohibit modification and deletion) diff --git "a/\346\257\225\344\270\232\346\200\273\347\273\223.md" "b/\346\257\225\344\270\232\346\200\273\347\273\223.md" new file mode 100644 index 00000000..e5d98a5f --- /dev/null +++ "b/\346\257\225\344\270\232\346\200\273\347\273\223.md" @@ -0,0 +1,26 @@ +### 算法训练营毕业总结 + +**一. 我为什么来到算法训练营**? + + 1. 因为平时工作都是一些接口对接的工作,重要的任务都是由厂商进行支撑,接触不到核心的技术,所以想要跳槽去到大公司,见识更多的牛人,更好的技术。而算法是进入大厂的必要条件。 + +2. 对于算法的理解也就是停留在大学阶段所学的那些零碎的知识,知识都是点,并没有很好的串成线,构成面,不成体系。太过于碎片化,好像什么算法都听过,但是又好像什么都不会写,所以就想着系统的学习一下数据结构以及算法。 +3. 感觉自己在平时工作和学习中,缺少一些方法的指导,经常会做出事倍功半的事情,所以也是想要借助算法训练营这个平台结交更多的朋友,学到更多的知识,从老师那里也能够获取一些经验和方法。 + + + +**二. 我收获了什么?** + +1. 最大的收获当然是老师平时常常介绍的方法论,也就是所谓的五毒神掌,刻意练习等观点。其实大家从小都知道温故而知新的道理,但是就是很少有人能够去践行他并且制定一套可行的理论指导。而覃超老师做到了。 +2. 其次就是助教老师对我代码的建议让我了解了自己写代码上面一些非常不好的习惯。如用中文作为类名,不写注释,不记录题目链接,不写总结等等等等。这些都是我逆袭路上的助推剂。同时助教老师的认真批改也让我感觉到了陪伴。助教老师也在陪伴着我们一周一周的成长。这种感觉很微妙,无法用言语形容。 +3. 我还收获了一群朋友,他们来自五湖四海,来自中国的各个角落,来自不同的岗位,有着不同的经历,从同学们身上我也学到了很多的东西。 +4. 我收获了我最初来到训练营想要得到的算法能力。不是说我就有去大厂面试的能力了。而是说我现在有了一套方法论的支持,有了刷题的方向以及套路,学会了总结,做的题目也越来越多,越来越得心应手。这一点很令自己欣慰。 +5. 由于我同时参加了两个训练营,一个算法,一个java,所以时间上特别的紧张,所以这一段时间我也学会了很多时间管理的窍门,比如番茄工作法,建立每日的toDO List等等。这让我对于自己的时间安排,时间管理也有了长足的进步。 + + + +**三. 总结** + +​ 就像覃超老师在最后一堂课说的,我们从算法训练营毕业了,但这不是终点,而是另一个起点。有一句话叫做靡不有初,鲜克有终。所以即便毕业了,我也一定会继续认真的刷题,直到达成自己的目标。 + +​ 最后,谢谢所有的人。谢谢覃超老师,谢谢助教老师,谢谢班主任老师,谢谢所有的同学们。我的成长路上,因为有你们,我并不孤单。 \ No newline at end of file