├── README.md ├── 二分 ├── 162.寻找峰值.md ├── 287.寻找重复数.md ├── 33.搜索旋转排序数组.md ├── 34 在排序数组中查找元素的第一个和最后一个位置.md ├── 540.有序数组中的单一元素.md ├── 69.X的平方根.md ├── 74.搜索二维矩阵.md └── 81.搜索旋转排序数组II.md ├── 其他 ├── 128. 最长连续序列.md ├── 137.只出现一次的数字II.md ├── 169.多数元素.md ├── 172.阶乘后的0.md ├── 231.2的幂.md ├── 258.各位相加.md ├── 260.只出现一次的数字 III.md ├── 29.两数相除.md ├── 381.常数时间插入、删除和获取随机元素 - 允许重复.md └── 58.最后一个单词的长度.md ├── 分治算法 ├── 215.数组中的第K个最大元素.md ├── 23. 合并K个升序链表.md ├── 241.为运算表达式设计优先级.md └── 95.不同的二叉搜索树 II.md ├── 动态规划 ├── 1143.最长公共子序列.md ├── 120.三角形最短路径和.md ├── 139.单词拆分.md ├── 152.乘积最大子数组.md ├── 188.买卖股票的最佳时机 IV.md ├── 198.打家劫舍.md ├── 279.完全平方数.md ├── 309.jpg ├── 309.最佳买卖股票时机含冷冻期.md ├── 32.最长有效括号.md ├── 322.公式.jpg ├── 322.零钱兑换.md ├── 343.整数拆分.md ├── 416.分割等和子集.md ├── 474.一和零.md ├── 494.目标和.md ├── 5.最长回文子串.md ├── 514.jpg ├── 514.自由之路.md ├── 518.零钱兑换 II.md ├── 53.最大子序和.md ├── 62.不同路径.md ├── 63.不同路径II.md ├── 70.爬楼梯.md ├── 714.买卖股票的最佳时机含手续费.md ├── 72. 编辑距离.md └── 91.解码方法.md ├── 双指针 ├── 11.盛水最多的容器.md ├── 167. 两数之和 II - 输入有序数组.md ├── 202.快乐数.md ├── 75.颜色分类.md └── 88.合并两个有序数组.md ├── 回溯算法 ├── 131.分割回文串.md ├── 22.jpg ├── 22.括号生成.md ├── 39-1.png ├── 39-2.png ├── 39.组合总和.md ├── 46.jpg ├── 46.全排列.md ├── 51. N皇后.md ├── 79.单词搜索.md └── 93.复原IP地址.md ├── 字符串 ├── 179.最大数.md ├── 205.同构字符串.md ├── 208.图1.jpg ├── 208.实现Trie树.md ├── 43. 字符串相乘.md ├── 43.图1.png ├── 459. 重复的字符串.md ├── 468.验证IP地址.md ├── 5. 最长回文子串.md └── 647 回文子串.md ├── 数组 ├── 1.两数之和.md ├── 121.买卖股票的最佳时机.md ├── 15.三数之和.md ├── 16. 最接近的三数之和.md ├── 167.两数之和 II - 输入有序数组.md ├── 18.四数之和.md ├── 268.缺失数字.md ├── 307. 区域和检索 - 数组可修改.md ├── 307.线段树.png ├── 31.下一个排列.md ├── 327.区间和的个数.md ├── 349.两个数组的交集.md ├── 384.打乱数组.md ├── 493. 翻转对.md ├── 55.跳跃游戏.md ├── 56.合并区间.md ├── 560.和为K的子数组.md ├── 57.插入区间.md ├── 59.螺旋矩阵II.md └── 73.矩阵置零.md ├── 栈与队列 ├── 20.有效的括号.md ├── 225.用队列实现栈.md ├── 232.用栈实现队列.md ├── 503. 下一个更大元素 II.md ├── 5614.找出最具竞争力的子序列.md ├── 71.简化路径.md ├── 739.每日温度.md └── 84.柱状图中最大的矩形.md ├── 树 ├── 102.二叉树的层序遍历.md ├── 105.从前序与中序遍历序列构造二叉树.md ├── 106.从中序与后序遍历序列构造二叉树.md ├── 108.将有序数组转换为二叉搜索树.md ├── 109.有序链表转换二叉搜索树.md ├── 112.路径总和.md ├── 114.二叉树展开为链表.md ├── 116.填充每个节点的下一个右侧节点指针.md ├── 117.填充每个节点的下一个右侧节点指针 II.md ├── 230. 二叉搜索树中第K小的元素.md ├── 257.二叉树的所有路径.md ├── 450.删除二叉搜索树中的节点.md ├── 450.图1.png ├── 508.出现次数最多的子树元素和.md └── 96.不同的二叉搜索树.md ├── 深度(广度)优先遍历 ├── 127.单词接龙.jpg ├── 127.单词接龙.md ├── 130.被围绕的区域.md ├── 133 克隆图.md ├── 200.岛屿数量.md ├── 207.课程表.md ├── 515.在每个树行中找最大值.md ├── 542.01矩阵.md └── 785 判断二分图.md ├── 滑动窗口 ├── 209.长度最小的子数组.md ├── 219.存在重复元素II.md ├── 3.无重复字符的最长子串.md ├── 424. 替换后的最长重复字符.md └── 438. 找到字符串中所有字母异位词.md ├── 系统设计 ├── 146. LRU缓存机制.md ├── 146.java ├── 146.图1.jpg └── 173.二叉搜索树迭代器.md ├── 贪心算法 ├── 134.加油站.md ├── 1579.保证图可完全遍历.md ├── 1663.具有给定数值的最小字符串.md ├── 252.会议室.md ├── 435.无重叠区间.md ├── 502.IPO.md ├── 621. 任务调度器.md ├── 621.png └── 861. 翻转矩阵后的得分.md └── 链表 ├── 141.环形链表.md ├── 142.环形链表 II.md ├── 143.重排链表.md ├── 147.对链表进行插入排序.md ├── 148.排序链表.md ├── 160.相交链表.md ├── 2.两数相加.md ├── 234.回文链表.md ├── 61.旋转链表.md ├── 82.删除排序链表中的重复元素 II.md ├── 86.分割链表.md └── 92.反转链表 II.md /二分/162.寻找峰值.md: -------------------------------------------------------------------------------- 1 | # 162 寻找峰值 2 | 3 | **题目:** 4 | 峰值元素是指其值大于左右相邻值的元素。 5 | 给定一个输入数组 nums,其中 nums[i] ≠ nums[i+1],找到峰值元素并返回其索引。 6 | 数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。 7 | 你可以假设 nums[-1] = nums[n] = -∞。 8 | 9 | 示例 1: 10 | 输入: nums = [1,2,3,1] 11 | 输出: 2 12 | 解释: 3 是峰值元素,你的函数应该返回其索引 2。 13 | 14 | 示例 2: 15 | 输入: nums = [1,2,1,3,5,6,4] 16 | 输出: 1 或 5 17 | 解释: 你的函数可以返回索引 1,其峰值元素为 2; 或者返回索引 5, 其峰值元素为 6。 18 | 19 | ### 方法一:线性查找 20 | **分析:** 21 | 可以从以下几个情况考虑: 22 | 抓住只需要返回一个峰值和nums[i]不等于nums[i+1]这两个重点 23 | * 第一个值即为峰值:因为nums[-1]可看作负无穷,所以只要nums[0]>nums[1]时,第一个值就成为了峰值。这种情况下无论数组后面是什么情况,直接返回0即可。 24 | * 第一个值不是峰值:这种情况下,nums[0] < nums[1],这种情况下数组的曲线一开始肯定是向上的走势,只要找到走势转向向下的那个点即可,即找到第一个nums[i]>nums[i+1]的地方。如果没有这样的点,直接返回最后一个索引nums.length - 1。 25 | 26 | **代码:** 27 | ```java 28 | public int findPeakElement(int[] nums) { 29 | for(int i = 0; i < nums.length - 1; i++){ 30 | if(nums[i] > nums[i+1]) 31 | return i; 32 | } 33 | return nums.length - 1; 34 | } 35 | ``` 36 | 37 | **时间复杂度O(n)** 38 | **空间复杂度O(1)** 39 | 40 | 41 | ### 方法二:二分查找 42 | **思路:** 43 | 由方法一的分析可知,只需要找到满足nums[i] > nums[i + 1]的最左元素即可,适用于二分查找过程 44 | 45 | ```java 46 | public int findPeakElement(int[] nums) { 47 | int left = 0; 48 | int right = nums.length - 1; 49 | int res = nums.length - 1; 50 | while(left <= right){ 51 | int mid = left + (right - left) / 2; 52 | if(mid + 1 < nums.length && nums[mid] > nums[mid + 1]){ 53 | res = mid; 54 | right = mid - 1; 55 | } 56 | else{ 57 | left = mid + 1; 58 | } 59 | } 60 | return res; 61 | } 62 | ``` 63 | **时间复杂度O(logn)** 64 | **空间复杂度O(1)** 65 | -------------------------------------------------------------------------------- /二分/287.寻找重复数.md: -------------------------------------------------------------------------------- 1 | # 287 寻找重复数 2 | 3 | 4 | **题目:** 5 | 给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。 6 | 7 | 示例 1: 8 | 输入: [1,3,4,2,2] 9 | 输出: 2 10 | 11 | 示例 2: 12 | 输入: [3,1,3,4,2] 13 | 输出: 3 14 | 15 | 16 | **思路:二分查找** 17 | 这种方法只和数组中存在哪些数有关,和数组中这些数的顺序无关。因此分析时可把数组看成有序的(但其实并没有对数组进行排序,只是这么分析而已) 18 | 数组中所有的数字都在1到n之间,我们先取1到n的中位数mid。统计数组中小于等于mid的元素个数count,如果count大于mid,则说明重复数存在于1到mid之间。否则,重复数存在于mid+1到n之间。 19 | 20 | **代码:** 21 | ```java 22 | public int findDuplicate(int[] nums) { 23 | int left = 1; 24 | int right = nums.length - 1; 25 | int res = -1; 26 | while(left <= right){ 27 | int mid = left + (right - left) / 2; 28 | int count = 0; 29 | for(int num : nums) 30 | count += num <= mid ? 1 : 0; 31 | if(count > mid){ 32 | right = mid - 1; 33 | res = mid; 34 | } 35 | else 36 | left = mid + 1; 37 | } 38 | return res; 39 | } 40 | ``` 41 | 42 | **时间复杂度:O(nlogn)** 43 | **空间复杂度:O(1)** 44 | 45 | 注:如果没有不能修改数组和空间复杂度的要求,可以采用排序数组和哈希表的方法 46 | -------------------------------------------------------------------------------- /二分/33.搜索旋转排序数组.md: -------------------------------------------------------------------------------- 1 | # 33 搜索旋转排序数组 2 | 3 | **题目:** 4 | 假设按照升序排序的数组在预先未知的某个点上进行了旋转。 5 | ( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。 6 | 搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。 7 | 你可以假设数组中不存在重复的元素。 8 | 你的算法时间复杂度必须是 O(log n) 级别。 9 | 10 | 示例 1: 11 | 输入: nums = [4,5,6,7,0,1,2], target = 0 12 | 输出: 4 13 | 示例 2: 14 | 输入: nums = [4,5,6,7,0,1,2], target = 3 15 | 输出: -1 16 | 17 | **思路:** 18 | 可以根据数组最右边的元素(记为pivot)来判断一个数组元素是在数组左半边还是右半边。它的特点是:数组左半边的元素都小于pivot,数组右半边的元素都大于等于pivot 19 | 20 | *代码* 21 | ```java 22 | class Solution { 23 | public int search(int[] nums, int target) { 24 | int pivot = nums[nums.length - 1]; 25 | int left = 0; 26 | int right = nums.length - 1; 27 | while(left <= right){ 28 | int mid = left + (right - left) / 2; 29 | //如果target在数组右半边,而当前mid在数组左半边,则目标区域在mid右边 30 | if(target <= pivot && nums[mid] > pivot) 31 | left = mid + 1; 32 | //如果target在数组左半边,而当前mid在数组右半边,则目标区域在mid左边 33 | else if(target > pivot && nums[mid] <= pivot) 34 | right = mid - 1; 35 | //如果target和mid都在数组左半边,或者target和mid都在数组右半边,那就相当于在一个有序数组中进行二分查找(因为数组左半边和右半边分别都是有序的) 36 | else{ 37 | if(target == nums[mid]) 38 | return mid; 39 | else if(target < nums[mid]) 40 | right = mid - 1; 41 | else 42 | left = mid + 1; 43 | } 44 | } 45 | return -1; 46 | } 47 | } 48 | ``` 49 | 50 | -------------------------------------------------------------------------------- /二分/34 在排序数组中查找元素的第一个和最后一个位置.md: -------------------------------------------------------------------------------- 1 | # 34 在排序数组中查找元素的第一个和最后一个位置 2 | **题目:** 3 | 给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 4 | 5 | 你的算法时间复杂度必须是 O(log n) 级别。 6 | 7 | 如果数组中不存在目标值,返回 [-1, -1]。 8 | 9 | 示例 1: 10 | 输入: nums = [5,7,7,8,8,10], target = 8 11 | 输出: [3,4] 12 | 示例 2: 13 | 输入: nums = [5,7,7,8,8,10], target = 6 14 | 输出: [-1,-1] 15 | 16 | **思路:** 17 | 用两次二分查找,一次查找左边,一次查找右边 18 | 19 | ```java 20 | public int[] searchRange(int[] nums, int target) { 21 | int left = searchLeft(nums, target); 22 | int right = searchRight(nums, target); 23 | return new int[]{left, right}; 24 | } 25 | //在nums中寻找target的右边界 26 | public int searchRight(int[] nums, int target){ 27 | int left = 0; 28 | int right = nums.length - 1; 29 | int res = -1; 30 | while(left <= right){ 31 | int mid = left + (right - left) / 2; 32 | if(nums[mid] > target) 33 | right = mid - 1; 34 | else if(nums[mid] < target) 35 | left = mid + 1; 36 | else{ 37 | left = mid + 1; 38 | res = mid; 39 | } 40 | } 41 | return res; 42 | } 43 | //在nums中寻找target的左边界 44 | public int searchLeft(int[] nums, int target){ 45 | int left = 0; 46 | int right = nums.length - 1; 47 | int res = -1; 48 | while(left <= right){ 49 | int mid = left + (right - left) / 2; 50 | if(nums[mid] > target) 51 | right = mid - 1; 52 | else if(nums[mid] < target) 53 | left = mid + 1; 54 | else{ 55 | right = mid - 1; 56 | res = mid; 57 | } 58 | } 59 | return res; 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /二分/540.有序数组中的单一元素.md: -------------------------------------------------------------------------------- 1 | # 540.有序数组中的单一元素 2 | 3 | ## 题目 4 | 给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。 5 | 6 | 示例 1: 7 | 输入: [1,1,2,3,3,4,4,8,8] 8 | 输出: 2 9 | 10 | 示例 2: 11 | 输入: [3,3,7,7,10,11,11] 12 | 输出: 10 13 | 14 | 注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。 15 | 16 | ## 方法(二分查找) 17 | 所给数组有序,考虑使用二分法。 18 | 19 | 单一的元素左右两边的特点为: 20 | * m为偶数时,在单一元素左边,nums[m] == nums[m + 1] 21 | * m为奇数时,在单一元素右边,nums[m] != nums[m + 1] 22 | 23 | 可以据此来进行二分的流程,但是注意:一定要保证mid为偶数 24 | 25 | ## 代码 26 | ```java 27 | public int singleNonDuplicate(int[] nums) { 28 | int left = 0; 29 | int right = nums.length - 1; 30 | while(left <= right){ 31 | int mid = left + (right - left) / 2; 32 | if(mid % 2 == 1) 33 | mid--; 34 | if(mid < nums.length - 1 && nums[mid] == nums[mid + 1]) 35 | left = mid + 2; 36 | else 37 | right = mid - 2; 38 | } 39 | return nums[left]; 40 | } 41 | ``` 42 | 43 | ## 注:二分查找 44 | 对于二分查找,要做到细节不出错,需要关注三件事: 45 | * 左右边界 46 | * 循环条件 47 | * 如何进入下一步循环 48 | 49 | ### 第一种情况 50 | 左右边界是指,初始时left和right的取值,如果 51 | ```java 52 | left = 0 , right = nums.length - 1 53 | ``` 54 | 则表明搜索范围是[left, right]这样一个闭区间 55 | 56 | 因此循环条件就应该写成: 57 | ```java 58 | while(left <= right) 59 | ``` 60 | 即:当left==right时,由于是闭区间,因此当前的查找范围[left, right]里尚且有一个元素,应该再次进入循环,而不是退出 61 | 62 | 进一步考虑,进入下一步循环就应该写为: 63 | ```java 64 | left = mid + 1; 65 | right = mid - 1; 66 | ``` 67 | 因为当前mid位置已经查找过,下一步查找范围为闭区间[left, mid - 1]或者闭区间[mid, right] 68 | 69 | ### 第二种情况 70 | 71 | 但是,如果在初始化left和right时 72 | ```java 73 | left = 0, right = nums.length 74 | ``` 75 | 则表明搜索范围是[left, right)这样一个左闭右开区间 76 | 77 | 因此循环条件就应该写成: 78 | ```java 79 | while(left < right) 80 | ``` 81 | 因为,left==right时,区间[left, right)里已经没有元素,不应该再进入循环。 82 | 83 | 进一步考虑,进入下一步循环就应该写为: 84 | ```java 85 | left = mid + 1; 86 | right = mid; 87 | ``` 88 | 89 | 因为在mid位置查找过后,因为查找区间均为左闭右开区间,原查找区间为[left, right),因此下一步查找范围为区间[left, mid),或者区间[mid + 1, right) -------------------------------------------------------------------------------- /二分/69.X的平方根.md: -------------------------------------------------------------------------------- 1 | # 69 X的平方根 2 | 3 | **题目:** 4 | 5 | 实现 int sqrt(int x) 函数。 6 | 计算并返回 x 的平方根,其中 x 是非负整数。 7 | 由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。 8 | 9 | 示例 1: 10 | 输入: 4 11 | 输出: 2 12 | 13 | 示例 2: 14 | 输入: 8 15 | 输出: 2 16 | 说明: 8 的平方根是 2.82842..., 17 |   由于返回类型是整数,小数部分将被舍去。 18 | 19 | **思路:二分查找** 20 | 21 | 若x的平方根为k,则所要求的即为满足$k^2<=x$的最大的k值。采用二分查找找到这个k值。k的左边界为0,右边界为x。 22 | 23 | **代码:** 24 | ```java 25 | public int mySqrt(int x) { 26 | if(x == 0) 27 | return 0; 28 | int left = 0; 29 | int right = x; 30 | int ans = -1; 31 | while(left <= right){ 32 | int mid = left + (right - left) /2; 33 | if((long)mid * mid <= x){ 34 | ans = mid; 35 | left = mid + 1; 36 | } 37 | else 38 | right = mid - 1; 39 | } 40 | return ans; 41 | 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /二分/74.搜索二维矩阵.md: -------------------------------------------------------------------------------- 1 | # 74 搜索二维矩阵 2 | 3 | **题目:** 4 | 编写一个高效的算法来判断 m x n 矩阵中,是否存在一个目标值。该矩阵具有如下特性: 5 | * 每行中的整数从左到右按升序排列。 6 | * 每行的第一个整数大于前一行的最后一个整数。 7 | 示例 1: 8 | 9 | 输入: 10 | matrix = [ 11 | [1, 3, 5, 7], 12 | [10, 11, 16, 20], 13 | [23, 30, 34, 50] 14 | ] 15 | target = 3 16 | 输出: true 17 | 示例 2: 18 | 19 | 输入: 20 | matrix = [ 21 | [1, 3, 5, 7], 22 | [10, 11, 16, 20], 23 | [23, 30, 34, 50] 24 | ] 25 | target = 13 26 | 输出: false 27 | 28 | **思路**: 29 | 先二分找到target所在的行,再在此行中二分查找是否存在target这个元素 30 | 31 | **代码:** 32 | ```java 33 | public boolean searchMatrix(int[][] matrix, int target) { 34 | if(matrix.length == 0 || matrix[0].length == 0) 35 | return false; 36 | int row = matrix.length - 1; 37 | int col = matrix[0].length - 1; 38 | int left = 0; 39 | int right = row; 40 | while(left <= right){ 41 | int mid = left + (right - left) / 2; 42 | //如果元素大于mid行的最大值,则其一定出现于大于mid的行 43 | if(target > matrix[mid][col]) 44 | left = mid + 1; 45 | //如果元素小于mid行的最小值,则其一定出现于小于mid的行 46 | else if(target < matrix[mid][0]) 47 | right = mid - 1; 48 | //否则,元素存在于mid行,在该行中进行二分查找 49 | else 50 | return binsearch(matrix[mid], target); 51 | } 52 | return false; 53 | } 54 | 55 | //此函数在数组num中查找元素target 56 | public boolean binsearch(int[] num, int target){ 57 | int n = num.length; 58 | int left = 0; 59 | int right = n - 1; 60 | while(left <= right){ 61 | int mid = left + (right - left) / 2; 62 | if(target == num[mid]) 63 | return true; 64 | else if(target > num[mid]) 65 | left = mid + 1; 66 | else 67 | right = mid - 1; 68 | } 69 | return false; 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /二分/81.搜索旋转排序数组II.md: -------------------------------------------------------------------------------- 1 | # 81 搜索旋转排序数组 II 2 | 3 | 这道题和[第33题](https://github.com/wyh317/Leetcode/blob/master/%E4%BA%8C%E5%88%86/33.%E6%90%9C%E7%B4%A2%E6%97%8B%E8%BD%AC%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84.md)的不同在于,这道题数组nums中可以存在重复元素。只有一种情况可以对结果造成影响,即:左边数组的起始元素等于右边数组的终止元素,这样如果nums[mid]<=nums[r],那么mid不止可能在右边,也可能在左边。无法再通过比较nums[r]判断左右。因此我们在进入二分前先进行一步预处理,当左数组起始和有数组终止相等时,right左移。直到右数组严格小于左数组为止。这样之后就可以应用33题中的代码来解决。 4 | 5 | ```java 6 | public boolean search(int[] nums, int target) { 7 | int left = 0; 8 | int right = nums.length - 1; 9 | int pivotIndex = nums.length - 1; 10 | if(nums.length == 1) 11 | return nums[0] == target; 12 | //预处理 13 | while(right > 0 && nums[left] == nums[right]){ 14 | right--; 15 | pivotIndex--; 16 | } 17 | while(left <= right){ 18 | int mid = left + (right - left) / 2; 19 | //target在左,mid在右,则目标区间在mid左边 20 | if(target > nums[pivotIndex] && nums[mid] <= nums[pivotIndex]) 21 | right = mid - 1; 22 | //target在右,mid在左。则目标区间在mid右边 23 | else if(target <= nums[pivotIndex] && nums[mid] > nums[pivotIndex]) 24 | left = mid + 1; 25 | //如果target和mid都在数组左半边,或者target和mid都在数组右半边,那就相当于在一个有序数组中进行二分查找(因为数组左半边和右半边分别都是有序的) 26 | else{ 27 | if(target == nums[mid]) 28 | return true; 29 | else if(nums[mid] < target) 30 | left = mid + 1; 31 | else 32 | right = mid - 1; 33 | } 34 | } 35 | return false; 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /其他/128. 最长连续序列.md: -------------------------------------------------------------------------------- 1 | # 128. 最长连续序列 2 | 3 | ## 题目 4 | 给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 5 | 6 | 进阶:你可以设计并实现时间复杂度为 O(n) 的解决方案吗? 7 | 8 |   9 | 10 | 示例 1: 11 | 12 | 输入:nums = [100,4,200,1,3,2] 13 | 输出:4 14 | 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 15 | 16 | 示例 2: 17 | 输入:nums = [0,3,7,2,5,8,4,6,0,1] 18 | 输出:9 19 | 20 | 注:对于示例1,nums的最长数字连续子序列其实为[4,1,3,2],在解释中为了方便表示其数字连续性,因此写为[1,2,3,4] 21 | 22 | 23 | ## 方法 24 | 对于数组nums,其可能有多个数字连续的序列,其中每个序列都会有一个左边界,例如示例1的结果中[1, 2, 3, 4]的左边界就为1。 25 | 26 | **核心思路:** 27 | 如果我们知道了每一个连续序列的左边界,并且知道以它为左边界的连续序列的长度。进而就可以知道所有连续序列的长度。在其中取最大值即为结果。 28 | 29 | 30 | 但都有哪些数可以成为连续序列的左边界呢? 31 | 32 | 设想,如果num为一个左边界,那么num - 1就不应该存在于数组中(因为如果num - 1存在于数组中,num - 1又与num连续,所以num不可能是连续序列的左边界)。 33 | 因此如果一个数字num满足:num-1不存在于数组中。这个数字num就可以成为连续序列的左边界。 34 | 35 | 具体的算法流程如下; 36 | 37 | 准备一个HashSet,将所有元素入set,之后遍历数组中的每一个数num 38 | * 如果num - 1存在于set中,那么num不可能是左边界,直接跳过 39 | * 如果num - 1不存在于set中,那么num会是一个左边界,我们再不断地查找num+1、num+2......是否存在于set中,来看以num为左边界的连续序列能有多长 40 | 41 | 在上述遍历中,我们知道了对于每一个可能的左边界,能扩出的最长连续序列的长度,再在这些长度中取最大即为结果。 42 | 43 | ```java 44 | public int longestConsecutive(int[] nums) { 45 | Set set = new HashSet<>(); 46 | for(int num : nums) 47 | set.add(num); 48 | int res = 0; 49 | for(int num : nums){ 50 | if(set.contains(num - 1)) 51 | continue; 52 | else{ 53 | //len记录以num为左边界的连续序列的长度 54 | int len = 0; 55 | while(set.contains(num++)) 56 | len++; 57 | res = Math.max(res, len); 58 | } 59 | } 60 | return res; 61 | } 62 | ``` 63 | 64 | * 时间复杂度:O(n) 65 | 看似有双重循环,但仔细分析可知,数组中的每一个元素最多也就会被访问两次,因此还是线性的时间复杂度 66 | * 空间复杂度:O(n) -------------------------------------------------------------------------------- /其他/137.只出现一次的数字II.md: -------------------------------------------------------------------------------- 1 | # 137.只出现一次的数字 II 2 | 3 | ## 题目 4 | 5 | 给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次。找出那个只出现了一次的元素。 6 | 7 | 说明: 8 | 9 | 你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗? 10 | 11 | 示例 1: 12 | 输入: [2,2,3,2] 13 | 输出: 3 14 | 15 | 示例 2: 16 | 输入: [0,1,0,1,0,1,99] 17 | 输出: 99 18 | 19 | ## 方法(位运算) 20 | 把每一个整数考虑成长度为32位的二进制数。我们累计这32位中每一位上1出现的次数。由于数组中除一个元素外,其他元素都出现了三次。所以每一位上1出现的次数只能是3的倍数或者是3的倍数+1。因此对这个次数模3后就是只出现一次的那个数在该位上的值(1或0)。 21 | 22 | ## 代码 23 | ```java 24 | public int singleNumber(int[] nums) { 25 | int res = 0; 26 | for(int i = 0; i < 32; i++){ 27 | //count用来统计第i位上出现了多少次1 28 | int count = 0; 29 | for(int num: nums){ 30 | count += (num >> i) & 1; //右边为num的第i位上的值 31 | } 32 | //count % 3只能是0或1 33 | //通过异或操作将num的第i位上的值设置好 34 | res ^= (count % 3) << i; 35 | } 36 | return res; 37 | } 38 | ``` 39 | 注:对于异或操作: 40 | * 任何一个数字异或0的结果都是这个数字本身 41 | * 两个相同的数字异或的结果为0 -------------------------------------------------------------------------------- /其他/169.多数元素.md: -------------------------------------------------------------------------------- 1 | # 169 多数元素 2 | 3 | **题目:** 4 | 给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。 5 | 你可以假设数组是非空的,并且给定的数组总是存在多数元素。 6 | 7 | 示例 1: 8 | 输入: [3,2,3] 9 | 输出: 3 10 | 11 | 示例 2: 12 | 输入: [2,2,1,1,1,2,2] 13 | 输出: 2 14 | 15 | 16 | **思路:** 17 | >哈希表方法: 18 | 我们使用哈希映射(HashMap)来存储每个元素以及出现的次数。对于哈希映射中的每个键值对,键表示一个元素,值表示该元素出现的次数。 19 | 我们用一个循环遍历数组 nums 并将数组中的每个元素加入哈希映射中。在这之后,我们遍历哈希映射中的所有键值对,返回值最大的键。 20 | 时间复杂度和空间复杂度都是O(n) 21 | 22 | **代码:** 23 | ```java 24 | class Solution { 25 | public int majorityElement(int[] nums) { 26 | //建立一个哈希表,并将数组中所有元素入表 27 | //key为数组中元素,value为其出现的次数 28 | HashMap map = new HashMap<>(); 29 | for(int num : nums){ 30 | if(!map.containsKey(num)) 31 | map.put(num, 1); 32 | else 33 | map.put(num, map.get(num) + 1); 34 | } 35 | //遍历每一个key,如果其value大于数组长度的一半,返回这个key 36 | for(Object o : map.keySet()){ 37 | if((Integer)map.get(o) > nums.length / 2) 38 | return (Integer)o; 39 | } 40 | return -1; 41 | } 42 | } 43 | ``` 44 | 45 | **方法二**(排序方法): 46 | 先排序,然后返回排序好的数组的中间位置的元素 47 | 时间复杂度和空间复杂度都是O(nlogn) 48 | -------------------------------------------------------------------------------- /其他/172.阶乘后的0.md: -------------------------------------------------------------------------------- 1 | # 172.阶乘后的0 2 | 3 | ## 题目 4 | 给定一个整数 n,返回 n! 结果尾数中零的数量。 5 | 6 | 示例 1: 7 | 输入: 3 8 | 输出: 0 9 | 解释: 3! = 6, 尾数中没有零。 10 | 11 | 示例 2: 12 | 输入: 5 13 | 输出: 1 14 | 解释: 5! = 120, 尾数中有 1 个零. 15 | 16 | 说明: 你算法的时间复杂度应为 O(log n) 。 17 | 18 | ## 方法 19 | 如果计算出阶乘后再统计尾数中0的数量的话,当n很大时计算阶乘的过程肯定会溢出。因此需要考虑其他方法 20 | 21 | 题目要求的是结果中***尾数为0***的数量,其他的0不用管。分析可知,只有乘10才会在一个数的尾数中添一个0。而10只能分解成2和5两个因子,因此在计算阶乘的过程中每出现的一对2 * 5都会在结尾贡献一个0。 22 | 23 | 进一步分析,每一个偶数都有一个因子2,每一个5的倍数都有一个因子5。在所有小于n的数中,偶数的个数肯定多于5的倍数的个数,因此因子2出现的次数肯定比因子5出现的次数多,而且必须2和5结合在一起才能凑成一个10。因此,要统计在计算阶乘的过程中乘过多少10,只需要统计乘过多少5即可(因为2出现的次数足够多)。 24 | 25 | 在小于n的数中,每一个5的倍数都会贡献5。其中: 26 | * 在5-25之间的5的倍数会贡献1个5 27 | * 在25-125之间的5的倍数会贡献2个5 28 | * 在125-625之间的5的倍数会贡献3个5 29 | * 依次类推 30 | 31 | ```java 32 | public int trailingZeroes(int n) { 33 | if(n <= 4) 34 | return 0; 35 | int res = 0; 36 | while(n > 0){ 37 | res += (n / 5); 38 | n = n / 5; 39 | } 40 | return res; 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /其他/231.2的幂.md: -------------------------------------------------------------------------------- 1 | # 231 2的幂 2 | 3 | **题目:** 4 | 给定一个整数,编写一个函数来判断它是否是 2 的幂次方。 5 | 6 | ## 方法一: 7 | 一直除2看最后得到的是否为1,这种方法的时间复杂度为O(logn)。 8 | ```java 9 | class Solution { 10 | public boolean isPowerOfTwo(int n) { 11 | if(n <= 0) 12 | return false; 13 | if(n == 1) 14 | return true; 15 | while(n % 2 == 0){ 16 | n = n / 2; 17 | if(n == 1) 18 | return true; 19 | } 20 | return false; 21 | } 22 | } 23 | ``` 24 | 25 | ## 方法二(位运算): 26 | 所有为2的幂的整数,其二进制均是除了其有效最高位以外全是0的数。也就是说,我们只需要判断这个数是否有除了有效最高位之外还有没有其他的1即可。 27 | 28 | 这里可以用到一个位运算小技巧:给定一个数n,将n和(n-1)做一次与运算,即可将n的最后一位1去掉。例如9和8,即1001 & 1000 = 1000,把最后一位的1去掉。再例如12和11,即1100 & 1011 = 1000,最后一位的1被去掉。 29 | 30 | 那么对于所有2的幂,我们将它与它-1后的数做一次与运算,就会将其唯一一位1消去,最后等于0。 31 | ```java 32 | public boolean isPowerOfTwo(int n) { 33 | return n > 0 && (n & (n - 1)) == 0; 34 | } 35 | ``` 36 | 时间复杂度和空间复杂度都为O(1) 37 | -------------------------------------------------------------------------------- /其他/258.各位相加.md: -------------------------------------------------------------------------------- 1 | # 258.各位相加 2 | 3 | ## 题目 4 | 5 | 给定一个非负整数 num,反复将各个位上的数字相加,直到结果为一位数。 6 | 7 | 示例: 8 | 输入: 38 9 | 输出: 2 10 | 解释: 各位相加的过程为:3 + 8 = 11, 1 + 1 = 2。 由于 2 是一位数,所以返回 2。 11 | 12 | ## 方法 13 | 分析如下: 14 | * 整数abc变为a+b+c的过程中减少的值一定是9的倍数,证明如下: 15 | $$abc = a * 100 + b * 10 + c = a * 99 + b * 9 + (a + b + c)$$ 16 | 17 | * 例如:从384变为15的过程中减去的369为9的倍数。从15变为6的过程中减去的9也为9的倍数.因此从384变到最后的一位数6的过程中减去的369+9也为9的倍数 18 | * 也就是说:我们对所给的数字nums榨干它包含的所有的9,即为所求的最后一位数。也即求num%9。但如果num本来即为9的倍数,如18、369,那么直接返回9. 19 | * 注意:nums为个位数时需要特殊处理 20 | 21 | ```java 22 | public int addDigits(int num) { 23 | if(num < 10) 24 | return num; 25 | return num % 9 == 0 ? 9 : num % 9; 26 | } 27 | ``` -------------------------------------------------------------------------------- /其他/260.只出现一次的数字 III.md: -------------------------------------------------------------------------------- 1 | # 260.只出现一次的数字 III 2 | 3 | ## 题目 4 | 给定一个整数数组 nums,其中恰好有两个元素只出现一次,其余所有元素均出现两次。 找出只出现一次的那两个元素。 5 | 6 | 示例 : 7 | 8 | 输入: [1,2,1,3,2,5] 9 | 输出: [3,5] 10 | 11 | ## 方法(位运算) 12 | 13 | 由于两个相同的数字异或的结果为0,因此将数组中所有元素异或得到的结果res即为两个Single Number异或的结果 14 | 15 | 我们进行如下考虑: 16 | 如果我们将数组中的元素分为两组,每组中只有一个Single Number,那么只要把这两个组的组内元素全部异或,就会分别得到两个Single Number。 17 | 18 | 但是如何进行分组才能将两个Single Number正确地分到两个组呢? 19 | 我们在上面得到了两个Single Number异或得到的结果res。在res中为1的bit位上,两个Single Number的取值肯定不同(如果两个数在这一位上都为1或者都为0,那么异或结果在这一位上必然为0)。我们可以用这一特性来进行分组: 20 | * 先找到res中值为1的最低bit位 21 | * 在这一位上为1的元素放到一组,在这一位上为0的元素放到另一组 22 | 23 | 这样就可以保证两个Single Number被分到两个不同的组,而且同样的元素肯定会被分到同一组。最后组内进行异或,便可分别得到这两个Single Number 24 | 25 | ## 代码 26 | ```java 27 | public int[] singleNumber(int[] nums) { 28 | int res = 0; 29 | for(int num : nums) 30 | res ^= num; 31 | int lowbit = res & (-res); 32 | int num1 = 0; 33 | int num2 = 0; 34 | for(int num : nums){ 35 | if((num & lowbit) == 0) 36 | num1 ^= num; 37 | else 38 | num2 ^= num; 39 | } 40 | return new int[]{num1, num2}; 41 | } 42 | ``` 43 | 44 | * 时间复杂度:O(n) 45 | * 空间复杂度:O(1) 46 | 47 | **注:** 48 | 将一个数与它的相反数进行与操作,可以获得这个数中值为1的最低bit位。 49 | ```java 50 | int lowbit = res & (-res) 51 | ``` 52 | 因为在计算机中,-res由res中的各位都取反,再在最低位上加1来得到 -------------------------------------------------------------------------------- /其他/29.两数相除.md: -------------------------------------------------------------------------------- 1 | # 29.两数相除 2 | 3 | ## 题目 4 | 给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法、除法和 mod 运算符。 5 | 6 | 返回被除数 dividend 除以除数 divisor 得到的商。 7 | 8 | 整数除法的结果应当截去(truncate)其小数部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2 9 | 10 |   11 | 12 | 示例 1: 13 | 输入: dividend = 10, divisor = 3 14 | 输出: 3 15 | 解释: 10/3 = truncate(3.33333..) = truncate(3) = 3 16 | 17 | 示例 2: 18 | 输入: dividend = 7, divisor = -3 19 | 输出: -2 20 | 解释: 7/-3 = truncate(-2.33333..) = -2 21 |   22 | 23 | 提示: 24 | * 被除数和除数均为 32 位有符号整数。 25 | * 除数不为 0。 26 | * 假设我们的环境只能存储 32 位有符号整数,其数值范围是 [−2^31,  2^31 − 1]。本题中,如果除法结果溢出,则返回 2^31 − 1。 27 | 28 | ## 方法 29 | * 首先考虑溢出的问题:在Java中,int型占4字节32位。int型的最大值为:Integer.MIN_VALUE= -2147483648 (-2的31次方)。最小值为:Integer.MAX_VALUE= 2147483647 (2的31次方-1)。因此,如果用-2147483648除以-1,得到的2147483648肯定会溢出,这种情况需要特殊处理。 30 | * 先根据除数和被除数的正负判断结果的正负。再把除数和被除数都转换为正数来计算。最后给结果加上正负号。 31 | * 真正的相除操作实现在div函数中,简单来说就是如下过程:60/8 = (60-32)/8 + 4 = (60-32-16)/8 + 2 + 4 = 1 + 2 + 4 = 7。具体看注释 32 | 33 | ```java 34 | public int divide(int dividend, int divisor) { 35 | if(dividend == 0) 36 | return 0; 37 | //为了处理溢出的问题,需要特殊考虑除数为1和-1的情况 38 | if(divisor == 1) 39 | return dividend; 40 | if(divisor == -1){ 41 | if(dividend == Integer.MIN_VALUE) 42 | return Integer.MAX_VALUE; 43 | return -dividend; 44 | } 45 | long a = dividend; 46 | long b = divisor; 47 | boolean isPoistive = true; 48 | //根据除数和被除数的正负判断结果的正负,并把除数和被除数都转换为正数 49 | if((a > 0 && b < 0) || (a < 0 && b > 0)) 50 | isPoistive = false; 51 | a = Math.abs(a); 52 | b = Math.abs(b); 53 | long res = div(a, b); 54 | if(isPoistive) 55 | return res > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) res; 56 | return (int) -res; 57 | } 58 | 59 | //例如25:4 60 | //1 * 4 < 25,于是让1翻倍 61 | //2 * 4 < 25,于是让2翻倍 62 | //4 * 4 < 25,于是让4翻倍 63 | //8 * 4 < 35,于是让8翻倍 64 | //16 * 4 > 35,可以判断结果肯定位于8和16之间 65 | //结果为: 8 + (35 - 4*8) / 4 66 | public int div(long a, long b){ 67 | if(a < b) 68 | return 0; 69 | long count = 1; 70 | long sum = b; 71 | while(sum + sum <= a){ 72 | count = count + count; 73 | sum = sum + sum; 74 | } 75 | return (int) (count + div(a - sum, b)); 76 | } 77 | ``` 78 | 79 | ## 参考 80 | [leetcode题解区](https://leetcode-cn.com/problems/divide-two-integers/solution/po-su-de-xiang-fa-mei-you-wei-yun-suan-mei-you-yi-/) -------------------------------------------------------------------------------- /其他/58.最后一个单词的长度.md: -------------------------------------------------------------------------------- 1 | # 58 最后一个单词的长度 2 | 3 | **题目:** 4 | 给定一个仅包含大小写字母和空格 ' ' 的字符串 s,返回其最后一个单词的长度。如果字符串从左向右滚动显示,那么最后一个单词就是最后出现的单词。 5 | 6 | 如果不存在最后一个单词,请返回 0 。 7 | 8 | 说明:一个单词是指仅由字母组成、不包含任何空格字符的 最大子字符串。 9 | 10 | 11 | 12 | 示例: 13 | 输入: "Hello World" 14 | 输出: 5 15 | 16 | 17 | 18 | **思路:** 19 | 从字符串末尾开始向前遍历,其中主要有两种情况: 20 | 第一种情况,以字符串"Hello World"为例,从后向前遍历直到遍历到头或者遇到空格为止,即为最后一个单词"World"的长度5 21 | 22 | 第二种情况,以字符串"Hello World "为例,需要先将末尾的空格过滤掉,再进行第一种情况的操作,即认为最后一个单词为"World",长度为5 23 | 24 | 所以完整过程为先从后过滤掉空格找到单词尾部,再从尾部向前遍历,找到单词头部,最后两者相减,即为单词的长度 25 | 时间复杂度:O(n),n为结尾空格和结尾单词总体长度 26 | 27 | **代码:** 28 | ```java 29 | class Solution { 30 | public int lengthOfLastWord(String s) { 31 | int end = s.length() - 1; 32 | //排除掉所有末尾的空格,用end标识最后一个单词的最后一个字母 33 | while(end >= 0 && s.charAt(end) == ' ') 34 | end--; 35 | if(end < 0) 36 | return 0; 37 | int start = end; 38 | //用start标识最后一个单词的第一个字母 39 | while(start >= 0 && s.charAt(start) != ' ') 40 | start--; 41 | return end - start; 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /分治算法/23. 合并K个升序链表.md: -------------------------------------------------------------------------------- 1 | # 23. 合并K个升序链表 2 | 3 | ## 题目 4 | 5 | 给你一个链表数组,每个链表都已经按升序排列。 6 | 7 | 请你将所有链表合并到一个升序链表中,返回合并后的链表。 8 | 9 |   10 | 11 | 示例 1: 12 | 输入:lists = [[1,4,5],[1,3,4],[2,6]] 13 | 输出:[1,1,2,3,4,4,5,6] 14 | 解释:链表数组如下: 15 | [ 16 | 1->4->5, 17 | 1->3->4, 18 | 2->6 19 | ] 20 | 将它们合并到一个有序链表中得到。 21 | 1->1->2->3->4->4->5->6 22 | 23 | 24 | ## 方法一(优先队列) 25 | 之前曾用过归并排序的思路解决过合并2个升序链表的问题。这个题我们也可以使用相同的思路,用k个指针分别指向这k个链表的节点,每次都取这k个节点中值最小的一个放到我们的结果链表中去。 26 | 27 | 既然我们每次都要在一系列值中找到最小的那个,很自然就会想到用一个最小堆来实现: 28 | * 准备一个小根堆,将lists中的所有链表加入到堆中 29 | * 每次从小根堆中取出一个链表,将这个链表的头挂到结果链表中去 30 | * 将上述链表头节点之后的剩余部分再放入堆中,相当于这个链表的cur指针向后移了一步。 31 | 32 | ```java 33 | public ListNode mergeKLists(ListNode[] lists) { 34 | if(lists == null || lists.length == 0) 35 | return null; 36 | PriorityQueue heap = new PriorityQueue<>((v1, v2) -> v1.val - v2.val); 37 | for(ListNode list : lists){ 38 | if(list != null) 39 | heap.add(list); 40 | } 41 | ListNode dummy = new ListNode(0); 42 | ListNode cur = dummy; 43 | while(!heap.isEmpty()){ 44 | cur.next = heap.poll(); 45 | cur = cur.next; 46 | if(cur.next != null) 47 | heap.add(cur.next); 48 | } 49 | return dummy.next; 50 | } 51 | ``` 52 | * 时间复杂度:O(Nlogk) 53 | * 空间复杂度:O(k) 54 | 55 | ## 方法二(分治法) 56 | 回顾分治法的基本思想为:将难以解决的大问题分解为多个容易解决的小问题,再逐个解决小问题,最终用小问题的解合并即得到了原大问题的解。 57 | 58 | 分治法只要考虑以下三点: 59 | * 分解:将合并k个链表的原问题,分解为合并lists中左半组链表和右半组链表这两个子问题 60 | * 合并:递归解决上述子问题后,我们得到了两个合并后的链表,这时将二者再归并就是答案。这时,问题已经由归并k个链表,转化为了归并两个链表 61 | * 递归返回条件:当把子问题分解为最小时,只有一个链表,无需归并,直接返回即可。 62 | ```java 63 | public ListNode mergeKLists(ListNode[] lists) { 64 | if(lists == null || lists.length == 0) 65 | return null; 66 | return merge(lists, 0, lists.length - 1); 67 | } 68 | //归并lists中索引从start到end间的这些链表 69 | public ListNode merge(ListNode[] lists, int start, int end){ 70 | if(start == end) 71 | return lists[start]; 72 | int mid = start + (end - start) / 2; 73 | ListNode list1 = merge(lists, start, mid); 74 | ListNode list2 = merge(lists, mid + 1, end); 75 | return merge2Lists(list1, list2); 76 | } 77 | //将问题转化为归并两个链表的问题 78 | public ListNode merge2Lists(ListNode list1, ListNode list2){ 79 | if(list1 == null && list2 == null) 80 | return null; 81 | if(list1 == null) 82 | return list2; 83 | if(list2 == null) 84 | return list1; 85 | if(list1.val < list2.val){ 86 | list1.next = merge2Lists(list1.next, list2); 87 | return list1; 88 | } 89 | else{ 90 | list2.next = merge2Lists(list2.next, list1); 91 | return list2; 92 | } 93 | } 94 | ``` 95 | 96 | * 时间复杂度:O(Nlogk) 97 | * 空间复杂度:O(1) -------------------------------------------------------------------------------- /分治算法/95.不同的二叉搜索树 II.md: -------------------------------------------------------------------------------- 1 | # 95.不同的二叉搜索树 II 2 | 3 | ## 题目 4 | 5 | 给定一个整数 n,生成所有由 1 ... n 为节点所组成的 二叉搜索树 。 6 | 7 |   8 | 9 | 示例: 10 | 输入:3 11 | 输出: 12 | [ 13 |   [1,null,3,2], 14 |   [3,2,null,1], 15 |   [3,1,null,null,2], 16 |   [2,1,3], 17 |   [1,null,2,null,3] 18 | ] 19 | 解释: 20 | 以上的输出对应以下 5 种不同结构的二叉搜索树: 21 | 22 | 1 3 3 2 1 23 | \ / / / \ \ 24 | 3 2 1 1 3 2 25 | / / \ \ 26 | 2 1 2 3 27 | 28 | ## 方法(分治法) 29 | 30 | 原问题让我们生成由1到n为节点组成的二叉搜索树。那么我们可以用分治法将原问题分解 31 | 32 | * 分解:将原问题分解为以下两个子问题,并递归求解(i为1到n的任意一个节点) 33 | * 生成由1到i-1为节点组成的二叉搜索树 34 | * 生成由i+1到n为节点组成的二叉搜索树 35 | * 合并:令节点i作为根节点,令它的左右子节点分别为由(1...i-1)组成的BST和由(i+1...n)组成的BST 36 | * 递归结束条件:start > end时,递归结束 37 | 38 | 39 | ## 代码 40 | ```java 41 | public List generateTrees(int n) { 42 | if(n == 0) 43 | return new LinkedList(); 44 | return generate(1, n); 45 | } 46 | 47 | public List generate(int start, int end){ 48 | List res = new LinkedList<>(); 49 | if(start > end){ 50 | res.add(null); 51 | return res; 52 | } 53 | for(int i = start; i <= end; i++){ 54 | List left = generate(start, i - 1); 55 | List right = generate(i + 1, end); 56 | for(TreeNode L : left){ 57 | for(TreeNode R : right){ 58 | TreeNode root = new TreeNode(i); 59 | root.left = L; 60 | root.right = R; 61 | res.add(root); 62 | } 63 | } 64 | } 65 | return res; 66 | } 67 | ``` -------------------------------------------------------------------------------- /动态规划/1143.最长公共子序列.md: -------------------------------------------------------------------------------- 1 | # 1143.最长公共子序列 2 | 3 | ## 题目 4 | 5 | 给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。 6 | 7 | 注:两个字符串的「公共子序列」(Longest Common Subsequence,简称 LCS)是这两个字符串所共同拥有的子序列。若这两个字符串没有公共子序列,则返回 0。 8 | 9 | 10 | 示例 1: 11 | 输入:text1 = "abcde", text2 = "ace" 12 | 输出:3 13 | 解释:最长公共子序列是 "ace",它的长度为 3。 14 | 15 | 示例 2: 16 | 输入:text1 = "abc", text2 = "abc" 17 | 输出:3 18 | 解释:最长公共子序列是 "abc",它的长度为 3。 19 | 20 | 示例 3: 21 | 输入:text1 = "abc", text2 = "def" 22 | 输出:0 23 | 解释:两个字符串没有公共子序列,返回 0。 24 | 25 | ## 方法(动态规划) 26 | 遇到子序列相关的问题,可以往动态规划上考虑。这道题让我们在**两个字符串**中寻找共同拥有的最长子序列,因此很容易想到用二维动态规划来解决。 27 | 28 | ### 1.定义dp数组 29 | 30 | 定义$dp[i][j]$为:$str1[0...i-1]和str2[0...j-1]$的最长公共子序列长度。 31 | 32 | ### 2.Base Case 33 | 根据上述dp数组的定义,可以写出如下Base Case: 34 | * i为0时,str1[0...i-1]不构成一个字符串,因此不存在与str2的公共子序列,因此dp[0][j]为0 35 | * j为0时,str2[0...j-1]不构成一个字符串,因此不存在于str1的公共子序列,因此dp[i][0]为0 36 | 37 | 38 | ### 3.状态转移方程 39 | 当str1遍历到i - 1,str2遍历到j - 1时,即要求$str1[0...i-1]和str2[0...j-1]$的最长公共子序列长度时,需要考虑以下的几种情况: 40 | * 如果str1[i - 1]与str2[j - 1]相等,那么肯定要将其放入str1[0...i−1]和str2[0...j−1]的LCS当中,有了这个字符,LCS的长度就会加1,因此 41 | $$dp[i][j] = dp[i - 1][j - 1] + 1$$ 42 | * 如果str1[i - 1]与str2[j - 1]不等,则又会分为以下的三种情况: 43 | * str1[i - 1]与str2[j - 1]都不放入LCS当中,那么LCS的长度不会产生变化,即dp[i][j] = dp[i - 1][j - 1] 44 | * str1[i - 1]放入LCS中,但str2[j - 1]不放。这时dp[i][j] = dp[i][j - 1] 45 | * str1[i - 1]不放入LCS中,但str2[j - 1]放。这时dp[i][j] = dp[i - 1][j] 46 | 47 | 由于在dp[i][j]这个位置可以做上述三种选择,因此取三种选择可能产生的最大值,即为dp[i][j]。即: 48 | $$dp[i][j] = max(dp[i - 1][j - 1],dp[i][j - 1],dp[i - 1][j])$$ 49 | 50 | ## 代码 51 | ```java 52 | public int longestCommonSubsequence(String str1, String str2) { 53 | int[][] dp = new int[str1.length() + 1][str2.length() + 1]; 54 | //Base Case,其实可以不用,因为数组初始化时全体元素就为0 55 | for(int j = 0; j <= str2.length(); j++) 56 | dp[0][j] = 0; 57 | for(int i = 0; i <= str1.length(); i++) 58 | dp[i][0] = 0; 59 | for(int i = 1; i <= str1.length(); i++){ 60 | for(int j = 1; j <= str2.length(); j++){ 61 | if(str1.charAt(i - 1) == str2.charAt(j - 1)) 62 | dp[i][j] = dp[i - 1][j - 1] + 1; 63 | else 64 | dp[i][j] = Math.max(dp[i - 1][j - 1], Math.max(dp[i][j - 1], dp[i - 1][j])); 65 | } 66 | } 67 | return dp[str1.length()][str2.length()]; 68 | } 69 | ``` 70 | 71 | * 时间复杂度:O(m * n) 72 | * 空间复杂度:O(m * n) 73 | 其中,m和n分别为str1和str2的长度 -------------------------------------------------------------------------------- /动态规划/120.三角形最短路径和.md: -------------------------------------------------------------------------------- 1 | # 120.三角形最短路径和 2 | 3 | ## 题目 4 | 给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 5 | 6 | 相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。 7 | 8 |   9 | 10 | 例如,给定三角形: 11 | 12 | [ 13 | [2], 14 | [3,4], 15 | [6,5,7], 16 | [4,1,8,3] 17 | ] 18 | 自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11) 19 | 20 | ## 方法1(暴力递归) 21 | 设f(i,j)为从位置(i,j)到最后一行的最短路径和。则可以发现如下规律: 22 | $$f(i,j) = min(f(i+1,j) , f(i+1,j+1)) + triangle[i][j]$$ 23 | 24 | 根据上述规律,很容易就可以写出以下暴力递归的方法。 25 | 26 | ```java 27 | public int minimumTotal(List> triangle) { 28 | return help(triangle, 0, 0); 29 | } 30 | public int help(List> triangle, int i, int j){ 31 | if(i = triangle.size()) 32 | return 0; 33 | return Math.min(help(triangle, i + 1, j), help(triangle, i + 1, j + 1)) + triangle.get(i).get(j)); 34 | } 35 | ``` 36 | 37 | 暴力递归的方法会产生很多重复计算,因此动态规划方法要用额外的dp数组来进行优化。 38 | 39 | ## 方法2(动态规划) 40 | 对暴力递归方法进行分析可以知道,用来确定返回值状态的可变参数为i和j,也即位置。 41 | 42 | 因此定义二维的dp数组。其中dp[i][j]的含义为:从(i, j)位置走到三角形最后一行的最短路径和。 43 | 44 | 状态转移方程为: 45 | $$dp(i,j) = min(dp(i+1,j) , dp(i+1,j+1)) + triangle[i][j]$$ 46 | 47 | 由三角形的最后一行不断向上进行状态转移。 48 | ```java 49 | public int minimumTotal(List> triangle) { 50 | int n = triangle.size(); 51 | int[][] dp = new int[n][n]; 52 | //先把不被依赖的位置(最后一行)对应dp数组的值填好 53 | for(int i = 0; i < n; i++){ 54 | dp[n - 1][i] = triangle.get(n - 1).get(i); 55 | } 56 | for(int i = n - 2; i >= 0; i--){ 57 | for(int j = 0; j <= i; j++) 58 | dp[i][j] = Math.min(dp[i+1][j] , dp[i+1][j+1]) + triangle.get(i).get(j); 59 | } 60 | return dp[0][0]; 61 | } 62 | ``` 63 | 64 | 时间复杂度:O(n^2) 65 | 空间复杂度:O(n^2) 66 | 67 | ## 方法3(对空间进行优化) 68 | 对方法2的实际运行过程进行分析,可以发现。对于dp[i][j]的计算,只需要知道dp数组第i+1行的情况即可,因此无需将整个dp数组的N行全部保存,只保存第i+1行这一行就可以了。 69 | 70 | ```java 71 | public int minimumTotal(List> triangle) { 72 | int n = triangle.size(); 73 | int[] dp = new int[n]; 74 | for(int j = 0; j < n; j++) 75 | dp[j] = triangle.get(n - 1).get(j); 76 | for (int i = n - 2; i >= 0; i--) { 77 | for (int j = 0; j <= i; j++) { 78 | dp[j] = Math.min(dp[j], dp[j + 1]) + triangle.get(i).get(j); 79 | } 80 | } 81 | return dp[0]; 82 | } 83 | ``` 84 | 85 | 时间复杂度:O(n^2) 86 | 空间复杂度:O(n) 87 | -------------------------------------------------------------------------------- /动态规划/139.单词拆分.md: -------------------------------------------------------------------------------- 1 | # 139.单词拆分 2 | 3 | ## 题目 4 | 5 | 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 6 | 7 | 说明: 8 | 拆分时可以重复使用字典中的单词。 9 | 你可以假设字典中没有重复的单词。 10 | 11 | 12 | 示例 1: 13 | 输入: s = "leetcode", wordDict = ["leet", "code"] 14 | 输出: true 15 | 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code" 16 | 17 | 示例 2: 18 | 输入: s = "applepenapple", wordDict = ["apple", "pen"] 19 | 输出: true 20 | 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。 21 |   注意你可以重复使用字典中的单词。 22 | 23 | 示例 3: 24 | 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] 25 | 输出: false 26 | 27 | ## 方法(动态规划) 28 | 分析题意: 29 | "applepenapple"能否被成功拆分取决于"applepen"能否被成功拆分以及"apple"是否为字典中的单词。 30 | 31 | 即:字符串的前i个字符能否被成功拆分,取决于字符串前j个字符能否被成功拆分以及s[j...i]是否是字典中的单词。 32 | 33 | ### 状态定义 34 | 定义dp[i]为字符串s的前i个字符(s[0..j-1])能否成功拆分。 35 | 36 | ### 设置Base Case 37 | dp[0] = true 38 | 39 | ## 状态转移方程 40 | dp[i] = dp[j] && (s[j...i]是否在字典中) 41 | 42 | ## 代码 43 | ```java 44 | public boolean wordBreak(String s, List wordDict) { 45 | boolean[] dp = new boolean[s.length() + 1]; 46 | dp[0] = true; 47 | Set set= new HashSet<>(); 48 | for(String word: wordDict){ 49 | set.add(word); 50 | } 51 | for(int i = 1; i <= s.length(); i++){ 52 | for(int j = i - 1; j >= 0; j--){ 53 | //flag记录s[j...i]是否在字典中 54 | boolean flag = false; 55 | if(set.contains(s.substring(j, i))) 56 | flag = true; 57 | dp[i] = dp[j] && flag; 58 | //只要有一种方法使得s[0..j-1]能成功拆分,那么s[0..j-1]就是可拆分的。 59 | //则无需判断之后的拆分方式,退出对于j的循环 60 | if(dp[i]) 61 | break; 62 | } 63 | } 64 | return dp[s.length()]; 65 | } 66 | ``` -------------------------------------------------------------------------------- /动态规划/152.乘积最大子数组.md: -------------------------------------------------------------------------------- 1 | # 152.乘积最大子数组 2 | 3 | ## 题目 4 | 给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。 5 | 6 |   7 | 8 | 示例 1: 9 | 输入: [2,3,-2,4] 10 | 输出: 6 11 | 解释: 子数组 [2,3] 有最大乘积 6。 12 | 13 | 示例 2: 14 | 输入: [-2,0,-1] 15 | 输出: 0 16 | 解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。 17 | 18 | ## 方法(动态规划) 19 | 因为数组中的数可负可正。由于存在负数,那么最大积可能乘以下一个数就变成最小积,最小积可能乘以下一个数就变成最大积。因此我们同时维护两个dp数组:maxDP和minDP。 20 | * max[i]记录以数组nums中第i个元素结尾的子数组的最大积 21 | * min[i]记录以数组nums中第i个元素结尾的子数组的最小积 22 | 23 | 状态转移方程: 24 | * $maxDP[i] = Max(maxDP[i-1]*nums[i], nums[i], minDP[i-1]*nums[i])$ 25 | * $minDP[i] = Min(minDP[i-1]*nums[i], nums[i], maxDP[i-1]*nums[i])$ 26 | 27 | 特殊情况:若nums[i]为0,则以nums[i]结尾的子数组的乘积只能为0(因为子数组中包括0),于是maxDP[i]和minDP[i]均为0 28 | 29 | ```java 30 | public int maxProduct(int[] nums) { 31 | int res = nums[0]; 32 | int[] maxDP = new int[nums.length]; 33 | int[] minDP = new int[nums.length]; 34 | //base case 35 | maxDP[0] = nums[0]; 36 | minDP[0] = nums[0]; 37 | for(int i = 1; i < nums.length; i++){ 38 | maxDP[i] = Math.max(maxDP[i-1] * nums[i], Math.max(nums[i], minDP[i-1] * nums[i])); 39 | minDP[i] = Math.min(minDP[i-1] * nums[i], Math.min(nums[i], maxDP[i-1] * nums[i])); 40 | res = Math.max(res, maxDP[i]); 41 | } 42 | return res; 43 | } 44 | ``` 45 | 46 | * 时间复杂度:O(n) 47 | * 空间复杂度:O(n) 48 | 49 | **空间优化** 50 | i位置上的状态(maxDP和minDP)只与i-1位置上的状态有关,所以更新 maxDP[i]的时候,我们只用到maxDP[i-1]的信息,再之前的信息就用不到了。所以我们不需要维护两个数组,只需要维护两个变量来记录i-1处的位置即可。 51 | ```java 52 | public int maxProduct(int[] nums) { 53 | int res = nums[0]; 54 | //base case 55 | int maxDP = nums[0]; 56 | int minDP = nums[0]; 57 | for(int i = 1; i < nums.length; i++){ 58 | //因为在更新minDP前需要更新maxDP,因此需要把前一步的maxDP先保存起来 59 | int preMax = maxDP; 60 | maxDP = Math.max(maxDP * nums[i], Math.max(nums[i], minDP * nums[i])); 61 | minDP = Math.min(minDP * nums[i], Math.min(nums[i], preMax * nums[i])); 62 | res = Math.max(res, maxDP); 63 | } 64 | return res; 65 | } 66 | ``` 67 | 68 | * 时间复杂度:O(n) 69 | * 空间复杂度:O(1) -------------------------------------------------------------------------------- /动态规划/188.买卖股票的最佳时机 IV.md: -------------------------------------------------------------------------------- 1 | # 188.买卖股票的最佳时机 IV 2 | ## 题目 3 | 给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。 4 | 5 | 设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。 6 | 7 | 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 8 | 9 |   10 | 11 | 示例 1: 12 | 13 | 输入:k = 2, prices = [2,4,1] 14 | 输出:2 15 | 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。 16 | 17 | 示例 2: 18 | 19 | 输入:k = 2, prices = [3,2,6,5,0,3] 20 | 输出:7 21 | 解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。 22 | 随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。 23 | 24 | ## 方法(动态规划) 25 | ### 定义dp数组 26 | hasStock[i][j]:表示第i天时,已经交易j次,并且当前持股时的最大收益 27 | noStock[i][j]:表示第i天时,已经交易j次,并且当前不持股时的最大收益 28 | 29 | 30 | ### Base Case 31 | 在第0天,还没进行过交易时,如果手中持股,则说明在第0天买入了股票,当前收益为-prices[0]。若手中不持股,当前收益为0.于是hasStock[0][0] = -prices[0], noStock[0][0] = 0。 32 | 33 | 由于买入和卖出需要在不同的时间,因此在第0天不可能有过任何交易,hasStock[0][1...k]和noStock[0][1...k]均无效,我们将其设为最小值Integer.MIN_VALUE 34 | 35 | ### 状态转移 36 | 对于hasStock[i][j]: 37 | * 如果手上的股票是在第i天买入的,那么第i-1天时,手中肯定不持有股票,对应着状态noStock[i-1][j] 38 | * 如果手中的股票不是在第i天买入的,那么第i-1天时,手中持有股票,对应着状态hasStock[i-1][j] 39 | 40 | 于是: 41 | $hasStock[i][j] = max(hasStock[i-1][j], noStock[i-1][j] - prices[i])$ 42 | 43 | 对于noStock[i][j]: 44 | * 如果手上的股票是第i天卖出的,那么在第i-1天时,手中持有股票,对应着状态hasStock[i-1][j-1] 45 | * 如果手上的股票不是第i天卖出的,那么在第i-1天时,手中不持有股票,对应着状态noStock[i-1][j] 46 | 47 | 于是: 48 | $noStock[i][j] = max(noStock[i-1][j], hasStock[i-1][j-1] + prices[i])$ 49 | 50 | 由于在最后一天时,手中持有股票肯定是不明智的,一定要在结束前将股票卖出才可能收获最大利润,因此最大利润一定为noStock[n - 1][0...k]中的一个,取最大值即可。 51 | 52 | 53 | ## 代码 54 | ```java 55 | public int maxProfit(int k, int[] prices) { 56 | if(prices.length == 0) 57 | return 0; 58 | int n = prices.length; 59 | //由于n天最多也就能进行n/2次交易,因此重新设置最多能进行的交易次数k 60 | k = Math.min(k, n / 2); 61 | int[][] hasStock = new int[n][k + 1]; 62 | int[][] noStock = new int[n][k + 1]; 63 | //设置base case 64 | hasStock[0][0] = -prices[0]; 65 | noStock[0][0] = 0; 66 | //设置为最小值的一半,防止溢出 67 | for (int j = 1; j <= k; j++) { 68 | hasStock[0][j] = Integer.MIN_VALUE / 2; 69 | noStock[0][j] = Integer.MIN_VALUE / 2; 70 | } 71 | //状态转移 72 | for(int i = 1; i < n; i++){ 73 | hasStock[i][0] = Math.max(hasStock[i - 1][0], noStock[i - 1][0] - prices[i]); 74 | for(int j = 1; j <= k; j++){ 75 | hasStock[i][j] = Math.max(hasStock[i-1][j], noStock[i-1][j] - prices[i]); 76 | noStock[i][j] = Math.max(noStock[i-1][j], hasStock[i-1][j-1] + prices[i]); 77 | } 78 | } 79 | //统计noStock[n - 1][1...k]的最大值 80 | int res = 0; 81 | for(int j = 1; j <= k; j++) 82 | res = Math.max(res, noStock[n - 1][j]); 83 | return res; 84 | } 85 | ``` 86 | 87 | ## 参考 88 | * [Leetcode题解区](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/solution/mai-mai-gu-piao-de-zui-jia-shi-ji-iv-by-8xtkp/) 89 | -------------------------------------------------------------------------------- /动态规划/198.打家劫舍.md: -------------------------------------------------------------------------------- 1 | # 198 打家劫舍 2 | 3 | **题目:** 4 | 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 5 | 6 | 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。 7 | 8 | >示例 1: 9 | 输入: [1,2,3,1] 10 | 输出: 4 11 | 解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 12 | 偷窃到的最高金额 = 1 + 3 = 4 。 13 | 14 | >示例 2: 15 | 输入: [2,7,9,3,1] 16 | 输出: 12 17 | 解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 18 | 偷窃到的最高金额 = 2 + 9 + 1 = 12 。 19 | 20 | **思路:** 21 | >用动态规划的思想来解决,用dp[i]表示前i家获取的最高金额。在第i家时,一共只有两个选择:偷或者不偷。如果偷第i家,则就不能偷第i-1家。获得的金额为dp[i]=dp[i-2]+nums[i]。如果不偷第i家,则无新的金额,即:dp[i] = dp[i-1]。 22 | 于是对于第i家,综合以上两种决策取最大值。前i家获得的最高金额为(也即状态转移方程): 23 | dp[i]=max(dp[i-1],dp[i-2]+nums[i]) 24 | DP 数组也可以叫”子问题数组”,因为 DP 数组中的每一个元素都对应一个子问题。 25 | 26 | 27 | **代码:** 28 | ```java 29 | class Solution { 30 | public int rob(int[] nums) { 31 | int len = nums.length; 32 | if(nums == null || len == 0) 33 | return 0; 34 | int[] dp = new int[len+1]; 35 | dp[0] = 0; 36 | dp[1] = nums[0]; 37 | for(int i = 2; i <= len; i++){ 38 | //状态转移方程。注意:dp中的第i号元素为nums中的第i-1号元素 39 | dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i-1]); 40 | } 41 | return dp[len]; 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /动态规划/279.完全平方数.md: -------------------------------------------------------------------------------- 1 | # 279.完全平方数 2 | 3 | ## 题目 4 | 给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 5 | 6 | 即:要用最少的完全平方数组成n,求这个最少的完全平方数的个数为多少。 7 | 8 | 示例 1: 9 | 输入: n = 12 10 | 输出: 3 11 | 解释: 12 = 4 + 4 + 4. 12 | 13 | 示例 2: 14 | 输入: n = 13 15 | 输出: 2 16 | 解释: 13 = 4 + 9. 17 | 18 | ## 方法1(动态规划) 19 | 20 | * 状态定义:dp[i]表示组成数字i要用的最少完全平方数个数 21 | * base case:dp[0] = 0, dp[1] = 1 22 | * 状态转移方程:$dp[i] = min(dp[i], dp[i - j*j] + 1)$ 23 | 最开始初始化dp[i]都为i,即用i个1(1也是完全平方数)可以组成i。 24 | j*j为完全平方数,状态转移方程的意思为:整数i-j * j最少要用dp[i - j * j]个完全平方数组成,那么算上这个完全平方数的话,整数i**可以**用dp[i - j * j] + 1个完全平方数组成,于是将dp[i - j * j] + 1与原来的dp[i]取较小的那个,便可得到整数i**最少**可用多少个完全平方数组成。 25 | 26 | ## 代码 27 | ```java 28 | public int numSquares(int n) { 29 | int[] dp = new int[n + 1]; 30 | dp[0] = 0; 31 | dp[1] = 1; 32 | for(int i = 2; i <= n; i++){ 33 | dp[i] = i; 34 | for(int j = 1; i - j * j >= 0; j++) 35 | dp[i] = Math.min(dp[i], dp[i - j * j] + 1); 36 | } 37 | return dp[n]; 38 | } 39 | ``` 40 | 41 | ## 方法2(BFS) 42 | 把0到n的每个整数看成图中的一个节点,如果两个整数之间相差一个平方数,那么就认为这两个整数所在的节点间有一条边。 43 | 44 | 所以题目要求的最小的平方数数量,也就是求解从节点 n 到节点 0 的最短路径。 45 | 46 | 在图中的最短路径问题,很容易想到BFS 47 | 48 | ```java 49 | public int numSquares(int n) { 50 | if(n <= 0) 51 | return 0; 52 | boolean[] visited = new boolean[n + 1]; 53 | Queue queue = new LinkedList<>(); 54 | List squares = generateNum(n); 55 | queue.add(n); 56 | visited[n] = true; 57 | int count = 0; 58 | while(!queue.isEmpty()){ 59 | int size = queue.size(); 60 | count++; 61 | for(int i = 0; i < size; i++){ 62 | int temp = queue.poll(); 63 | for(int square : squares){ 64 | if(temp - square == 0) 65 | return count; 66 | if(temp - square < 0) 67 | break; 68 | if(visited[temp - square]) 69 | continue; 70 | queue.add(temp - square); 71 | visited[temp - square] = true; 72 | } 73 | } 74 | } 75 | return count; 76 | } 77 | 78 | //生成小于n的所有完全平方数 79 | public List generateNum(int n){ 80 | List nums = new LinkedList<>(); 81 | for(int i = 1; i * i <= n; i++){ 82 | nums.add(i * i); 83 | } 84 | return nums; 85 | } 86 | ``` -------------------------------------------------------------------------------- /动态规划/309.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/动态规划/309.jpg -------------------------------------------------------------------------------- /动态规划/309.最佳买卖股票时机含冷冻期.md: -------------------------------------------------------------------------------- 1 | # 309.最佳买卖股票时机含冷冻期 2 | 3 | ## 题目 4 | 给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​ 5 | 6 | 设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): 7 | 8 | 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 9 | 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 10 | 11 | 示例: 12 | 输入: [1,2,3,0,2] 13 | 输出: 3 14 | 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出] 15 | 16 | ## 方法(二维DP) 17 | ### 1.状态定义 18 | 通过分析题意,我们可以将所有状态归结为以下三种(分别记为状态1、2、3): 19 | 20 | 1. 持股 21 | 2. 不持股且在冷冻期(不能买入) 22 | 3. 不持股且不在冷冻期(能买入) 23 | 24 | 返回值的状态由两个参数决定(天数和状态),因此这是一个二维DP的问题。 25 | 26 | **定义dp[i][j]为在第i天状态为j时的最大收益** 27 | 28 | ### 2.Base Case 29 | 在第0天只有买入和不买入的两种操作,不可能存在卖出(因为不可能在第0天前买入0。 30 | * 如果第0天买入,即持股,对应状态0,花费了prices[0]。于是$dp[0][0] = -prices[0]$; 31 | * 如果第0天不买入,即不持股,对应状态1、2。不花钱不赚钱。于是$dp[0][1]=dp[0][2]=0$ 32 | 33 | ### 3.状态转移方程 34 | * 持股状态可以由两种情况转换来: 35 | 1. 昨天持股,今天不卖 36 | 2. 昨天不持股且非冷冻期,今天买入 37 | * 冷冻期状态只能由以下一种情况转换来: 38 | * 昨天持股,今天卖出 39 | * 不持股且不在冷冻期状态可以由两种情况转换来: 40 | * 昨天处在冷冻期,今天不能买 41 | * 昨天不持股且不在冷冻期,今天可以买但是偏偏不买 42 | 43 | 具体转换过程如下图所示: 44 | 45 | ![](309.jpg) 46 | 47 | 注意:在最后一天(第i-1天)时,一定不能持股,无论什么价位都要卖出,不然会亏。即在最后一天时的持股状态不可能产生最大收益。因此最大收益应该为最后一天时不持股和冷冻期这两种状态的较大者。 48 | ## 代码 49 | ```java 50 | public int maxProfit(int[] prices) { 51 | if(prices == null || prices.length <= 1) 52 | return 0; 53 | int n = prices.length; 54 | int[][] dp = new int[n][3]; 55 | //设置base case 56 | dp[0][0] = -prices[0]; 57 | dp[0][1] = 0; 58 | dp[0][2] = 0; 59 | for(int i = 1; i < n; i++){ 60 | dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]); 61 | dp[i][1] = dp[i - 1][0] + prices[i]; 62 | dp[i][2] = Math.max(dp[i - 1][1], dp[i - 1][2]); 63 | } 64 | return Math.max(dp[n - 2][0] + prices[n - 1], Math.max(dp[n - 2][1], dp[n - 2][2])); 65 | } 66 | ``` 67 | 68 | * 时间复杂度:O(n) 69 | * 空间复杂度:O(n) 70 | 71 | ## 优化空间复杂度 72 | 由于第i天的状态只与第i-1天的状态有关,因此我们可以用如下方式降低空间复杂度。 73 | ```java 74 | public int maxProfit(int[] prices) { 75 | if(prices == null || prices.length <= 1) 76 | return 0; 77 | int n = prices.length; 78 | //设置base case 79 | int dp0 = -prices[0]; 80 | int dp1 = 0; 81 | int dp2 = 0; 82 | for(int i = 1; i < n; i++){ 83 | int new1 = Math.max(dp0, dp2 - prices[i]); 84 | int new2 = dp0 + prices[i]; 85 | int new3 = Math.max(dp1, dp2); 86 | dp0 = new1; 87 | dp1 = new2; 88 | dp2 = new3; 89 | } 90 | return Math.max(dp0 + prices[n - 1], Math.max(dp1, dp2)); 91 | } 92 | ``` 93 | 94 | * 时间复杂度:O(n) 95 | * 空间复杂度:O(1) 96 | 97 | ## 参考 98 | * [leetcode题解区](https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/solution/yi-tu-miao-dong-jie-fa-by-zi-gei-zi-zu/) -------------------------------------------------------------------------------- /动态规划/32.最长有效括号.md: -------------------------------------------------------------------------------- 1 | # 32.最长有效括号 2 | 3 | 给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。 4 | 5 |   示例 1: 6 | 7 | 输入:s = "(()" 8 | 输出:2 9 | 解释:最长有效括号子串是 "()" 10 | 示例 2: 11 | 12 | 输入:s = ")()())" 13 | 输出:4 14 | 解释:最长有效括号子串是 "()()" 15 | 示例 3: 16 | 17 | 输入:s = "" 18 | 输出:0 19 | ## 方法一(栈) 20 | * 用栈来找出所有匹配的左右括号,并用一个列表来记录匹配括号的索引。 21 | * 于是这个列表中最长连续子序列的长度就是最长有效括号的长度。 22 | 23 | ```java 24 | public int longestValidParentheses(String s) { 25 | Stack stack = new Stack<>(); 26 | List indexList = new LinkedList<>(); 27 | for(int i = 0; i < s.length(); i++){ 28 | if(s.charAt(i) == '(') 29 | stack.push(i); 30 | else{ 31 | if(!stack.isEmpty()){ 32 | indexList.add(stack.pop()); 33 | indexList.add(i); 34 | } 35 | } 36 | } 37 | if(indexList.size() == 0) 38 | return 0; 39 | //寻找列表中最长连续子序列的长度 40 | Collections.sort(indexList); 41 | int max = 0; 42 | int index = 0; 43 | while(index < indexList.size() - 1){ 44 | int count = 1; 45 | while(index < indexList.size() - 1 && indexList.get(index) == indexList.get(index + 1) - 1){ 46 | count++; 47 | index++; 48 | } 49 | max = Math.max(count, max); 50 | index++; 51 | } 52 | return max; 53 | } 54 | ``` 55 | * 时间复杂度:O(nlogn) 56 | * 空间复杂度:O(n) 栈和列表所需的额外空间 57 | ## 方法二(动态规划) 58 | ### 1.定义状态 59 | 定义`dp[i]`为以位置i结尾的最长有效括号的长度 60 | 61 | ### 2.Base Case 62 | * 一个单括号不可能形成有效括号,因此dp[0] = 0 63 | * 如果1位置为右括号,0位置为左括号,那么dp[1] = 2 64 | ### 3.状态转移 65 | * 如果i位置为左括号,那么没有以这个位置结尾的有效括号,dp[i] = 0 66 | * 如果i位置为右括号,分两种情况: 67 | * 如果i-1位置为左括号,那么i-1位置和i位置能组成一个长度为2的有效括号,再加上以i-2位置结尾的最长有效括号的长度,即为以i位置结尾的最长有小括号的长度。`dp[i] = dp[i-2] + 2` 68 | * 如果i-1位置为右括号,那么以它结尾可能会有一定长度的有效括号,我们先越过这些位置,来到这些匹配好的有效括号前面一个位置,如果这个位置是左括号,那么`dp[i] = dp[i - 1] + 2 + dp[i - dp[i - 1] - 2]` 69 | 70 | 例如:`"())(())"`,对于位置5,其前面的位置4已经有长度为2的匹配好的有效括号`"()"`,并且它们前面的位置2为左括号,可以与位置5的右括号匹配,因此dp[i]肯定能在dp[i-1]的位置上加2。但如例子所示,以位置1结尾还有长度为2的有小括号,与之后的有小括号连上了,这时我们就需要把这部分有效括号的长度dp[i - dp[i - 1] - 2]也加上。 71 | 72 | ```java 73 | public int longestValidParentheses(String s) { 74 | if(s == null || s.length() < 2) 75 | return 0; 76 | int[] dp = new int[s.length()]; 77 | //Base Case 78 | dp[0] = 0; 79 | dp[1] = s.charAt(1) == ')' && s.charAt(0) == '(' ? 2 : 0; 80 | int max = Math.max(dp[0], dp[1]); 81 | for(int i = 2; i < s.length(); i++){ 82 | if(s.charAt(i) == '(') 83 | dp[i] = 0; 84 | else{ 85 | if(s.charAt(i - 1) == '(') 86 | dp[i] = dp[i - 2] + 2; 87 | else{ 88 | if(i - dp[i - 1] - 1 >= 0 && s.charAt(i - dp[i - 1] - 1) == '('){ 89 | dp[i] = dp[i - 1] + 2; 90 | dp[i] += i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0; 91 | } 92 | } 93 | } 94 | max = Math.max(max, dp[i]); 95 | } 96 | return max; 97 | } 98 | ``` 99 | 100 | * 时间复杂度:O(n) 101 | * 空间复杂度:O(n) -------------------------------------------------------------------------------- /动态规划/322.公式.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/动态规划/322.公式.jpg -------------------------------------------------------------------------------- /动态规划/322.零钱兑换.md: -------------------------------------------------------------------------------- 1 | # 322 零钱兑换 2 | 3 | ## 题目 4 | 5 | 给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 6 | 7 | 示例 1: 8 | 输入: coins = [1, 2, 5], amount = 11 9 | 输出: 3 10 | 解释: 11 = 5 + 5 + 1 11 | 12 | 示例 2: 13 | 输入: coins = [2], amount = 3 14 | 输出: -1 15 | 16 | ## 方法(暴力递归) 17 | 如果想求 amount = 11 时的最少硬币数(原问题),若已经知道凑出 amount = 10 的最少硬币数(子问题),你只需要把子问题的答案加一(再选一枚面值为 1 的硬币)就是原问题的答案。因为硬币的数量是没有限制的,所以子问题之间没有相互制约,是互相独立的。 18 | 19 | 按如下步骤来考虑: 20 | 1. 分析**base case**:当目标金额 amount 为 0 时算法返回 0,因为不需要任何硬币就已经凑出目标金额了。 21 | 2. 分析**可变参数**,也就是原问题和子问题中会变化的变量。由于硬币数量无限,硬币的面额也是题目给定的,只有目标金额会不断地向 base case 靠近,所以唯一的**可变参数**就是目标金额amount。 22 | 3. 确定**决策**,也就是导致**可变参数**产生变化的行为。目标金额为什么变化呢,因为你在选择硬币,你每选择一枚硬币,就相当于减少了目标金额。所以说所有硬币的面值,就是你的**决策**。 23 | 4. 明确 dp 函数/数组的定义。如果是自顶向下的暴力递归情况,就会有一个递归的dp函数,一般来说函数的参数就是状态转移中会变化的量,也就是上面说到的**可变参数**;函数的返回值就是题目要求我们计算的量。 24 | 25 | 就本题来说,**可变参数**只有一个,即「目标金额」,题目要求我们计算凑出目标金额所需的最少硬币数量。所以我们可以这样定义 dp 函数:dp(n) 的定义:输入一个目标金额 n,返回凑出目标金额 n 的最少硬币数量。 26 | 27 | ### 代码 28 | ```java 29 | class Solution { 30 | public int coinChange(int[] coins, int amount) { 31 | return dp(coins, amount); 32 | } 33 | //编写dp函数,它对于一个要凑成的总金额,返回最少用的硬币个数 34 | public int dp(int[] coins, int amount){ 35 | if(amount == 0) 36 | return 0; 37 | if(amount < 0) 38 | return -1; 39 | int res = Integer.MAX_VALUE; 40 | for(int coin: coins){ 41 | int subProblem = dp(coins, amount - coin); 42 | if(subProblem == -1) 43 | continue; 44 | res = Math.min(res, 1 + subProblem); 45 | } 46 | return res != Integer.MAX_VALUE ? res : -1; 47 | } 48 | } 49 | ``` 50 | 51 | ## 方法(动态规划) 52 | 以上暴力解法的代码转化为数学形式就是状态转移方程: 53 | 54 | ![](322.公式.jpg) 55 | 56 | 上述的暴力递归方法是自顶向下的,因此会产生很多重复计算。我们接下来把它改成自底向上的动态规划: 57 | 58 | 动态规划需要一个dp数组,dp数组的定义与上述的dp函数类似,其索引是要凑成的金额amount的值,索引对应的值为要凑成amount需要的最小硬币数。 59 | 60 | ### 代码 61 | ```java 62 | public int coinChange(int[] coins, int amount) { 63 | int[] dp = new int[amount + 1]; 64 | //将dp数组的所有元素初始化为amount + 1,因为dp[amount] 最大不可能超过 amount(最小面值为 1 元),所以 amount + 1 是一个无意义的数。 65 | //如果索引index对应的数组元素为amount + 1,代表无法通过给定的硬币凑成金额index。 66 | Arrays.fill(dp, amount + 1); 67 | dp[0] = 0; 68 | for(int i = 0; i < dp.length; i++){ 69 | //计算dp[i]:要凑成金额i最少需要多少硬币 70 | for(int coin : coins){ 71 | if(i - coin < 0) 72 | continue; 73 | dp[i] = Math.min(dp[i], 1 + dp[i - coin]); 74 | } 75 | } 76 | return (dp[amount] == amount + 1) ? -1 :dp[amount]; 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /动态规划/343.整数拆分.md: -------------------------------------------------------------------------------- 1 | # 343.整数拆分 2 | 3 | ## 题目 4 | 5 | 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 6 | 7 | 示例 1: 8 | 输入: 2 9 | 输出: 1 10 | 解释: 2 = 1 + 1, 1 × 1 = 1。 11 | 12 | 示例 2: 13 | 输入: 10 14 | 输出: 36 15 | 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。 16 | 17 | 18 | ## 方法(动态规划) 19 | 20 | 每一个整数n拆分得到的最大乘积可以由比n小的整数拆分得到的最大乘积来得到,因此可以考虑用动态规划。 21 | 22 | 若要求i拆分得到的最大乘积,我们可以将i拆分为j和i - j之和,这时我们有两个选择: 23 | * j不再继续拆分,这时乘积为j * (i - j) 24 | * j继续向下拆分,因为j拆分得到的最大乘积为dp[j],因此这时的乘积为dp[j] * (i - j) 25 | 26 | 将这两种情况的乘积取较大的那个,即为拆分i所能得到的最大乘积。 27 | 28 | 动态规划步骤: 29 | * 定义dp[i]为将整数i拆分所能得到的最大乘积 30 | * Base Case:dp[0] = 0, dp[1] = 1 31 | * 状态转移方程:$\operatorname{dp}[i]=\max _{1 \leq j= 1; j--){ 42 | max = Math.max(max, Math.max(j * (i - j), (i - j) * dp[j])); 43 | } 44 | dp[i] = max; 45 | } 46 | return dp[n]; 47 | } 48 | ``` -------------------------------------------------------------------------------- /动态规划/416.分割等和子集.md: -------------------------------------------------------------------------------- 1 | # 416.分割等和子集 2 | 3 | ## 题目 4 | 给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 5 | 6 | 注意: 7 | 每个数组中的元素不会超过 100,数组的大小不会超过 200 8 | 9 | 10 | 示例 1: 11 | 输入: [1, 5, 11, 5] 12 | 输出: true 13 | 解释: 数组可以分割成 [1, 5, 5] 和 [11]. 14 |   15 | 示例 2: 16 | 输入: [1, 2, 3, 5] 17 | 输出: false 18 | 解释: 数组不能分割成两个元素和相等的子集. 19 | 20 | ## 方法(动态规划) 21 | 题目的意思我们可以换一种方式理解:假设数组nums的元素和为sum,是否能找到数组的一个子序列,使得它的和为sum/2。 22 | 23 | 在起初,我们可以先进行以下判断: 24 | * 如果nums的元素和sum为奇数,则不可能找到和为sum/2的子序列(因为nums中元素都为整数,和不可能为一个小数),直接返回false。 25 | * 如果nums中最大的元素大于sum/2,也就意味着nums中其余所有元素的和小于sum/2,直接返回false。 26 | ### 1.状态定义 27 | 定义布尔类型的二维数组dp,dp[i][j]表示数组nums[0...i]中是否有和为j的子序列。 28 | 29 | ### 2.Base Case 30 | base case为这个二维数组的第一行和第一列。 31 | * j=0时,无论i为多少,都为true。因为任何一个nums[0...i]都有一个和为0的子序列(子序列中元素为0)。所以,dp[i][0] = true 32 | * i = 0时,只能选择数组的第一个元素nums[0],因此当j与nums[0]相等时为true,其余都为false。即,dp[0][nums[0]] = true 33 | 34 | ### 3.状态转换方程 35 | 当遍历到一个数nums[i],我们有两种选择:选它或者不选它来构建和为j的子序列,这两种选择只要有一个为true,dp[i][j]就为true。 36 | 37 | 我们分为两种情况来讨论: 38 | * j >= nums[i]时: 39 | * 如果选nums[i],那么只须判断nums[0...i-1]是否有和为j-nums[i]的子序列即可。即dp[i][j] = dp[i - 1][j - nums[i]] 40 | * 如果不选nums[i],则dp[i][j] = dp[i - 1][j] 41 | * j < nums[i]时: 42 | 这种情况下不可以选nums[i],因为一旦选nums[i]进子序列,由于nums[i] > j,子序列的和一定大于j,不可能等于j。因此dp[i][j]一定为false。因此dp[i][j] = dp[i - 1][j] 43 | 44 | 最后返回dp[n-1][target]即可,即nums数组中是否有和为target的子序列(target为sum/2) 45 | ## 代码 46 | ```java 47 | public boolean canPartition(int[] nums) { 48 | if(nums.length <= 1) 49 | return false; 50 | int sum = 0; 51 | for(int num : nums) 52 | sum += num; 53 | if(sum % 2 != 0) 54 | return false; 55 | int target = sum / 2; 56 | boolean[][] dp = new boolean[nums.length][target + 1]; 57 | //设置base case 58 | for(int i = 0; i < n; i++) 59 | dp[i][0] = true; 60 | dp[0][nums] = true; 61 | for(int i = 1; i < nums.length; i++){ 62 | for(int j = 1; j <= target; j++){ 63 | if(j >= nums[i]) 64 | dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]]; 65 | else 66 | dp[i][j] = dp[i - 1][j] 67 | } 68 | } 69 | return dp[n - 1][target]; 70 | } 71 | ``` 72 | 73 | ## 优化空间复杂度 74 | 在填充dp数组时我们发现,每一行的dp值都只与它上一行的dp值有关,因此我们可以优化掉行这个维度,只留列的维度。于是可以将dp数组由二维将为一维。 75 | 76 | 空间复杂度由O(n*target)降到O(target) 77 | ```java 78 | public boolean canPartition(int[] nums) { 79 | if(nums.length <= 1) 80 | return false; 81 | int sum = 0; 82 | for(int num : nums) 83 | sum += num; 84 | if(sum % 2 != 0) 85 | return false; 86 | int target = sum / 2; 87 | boolean[] dp = new boolean[target + 1]; 88 | //设置base case 89 | dp[0] = true; 90 | for(int i = 0; i < nums.length; i++){ 91 | //注意:这里要逆向填充 92 | //如果我们对于j从小到大更新dp值,在计算dp[j]的时候,dp[j - nums[i]]已经是该行被更新过的状态,而我们希望它是上一行的状态。 93 | for(int j = target; j >= num[i]; j--){ 94 | dp[j] = dp[j] || dp[j - nums[i]]; 95 | } 96 | } 97 | return dp[target]; 98 | } 99 | ``` 100 | 101 | -------------------------------------------------------------------------------- /动态规划/514.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/动态规划/514.jpg -------------------------------------------------------------------------------- /动态规划/518.零钱兑换 II.md: -------------------------------------------------------------------------------- 1 | # 518.零钱兑换 II 2 | 3 | ## 题目 4 | 给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。  5 | 6 |   7 | 8 | 示例 1: 9 | 输入: amount = 5, coins = [1, 2, 5] 10 | 输出: 4 11 | 解释: 有四种方式可以凑成总金额: 12 | 5=5 13 | 5=2+2+1 14 | 5=2+1+1+1 15 | 5=1+1+1+1+1 16 | 17 | 示例 2: 18 | 输入: amount = 3, coins = [2] 19 | 输出: 0 20 | 解释: 只用面额2的硬币不能凑成总金额3。 21 | 22 | 示例 3: 23 | 输入: amount = 10, coins = [10] 24 | 输出: 1 25 | 26 | ## 方法一(暴力递归) 27 | 例如:arr = [200, 100, 50, 20, 5, 1],aim = 1000 28 | 对于第一个元素200: 29 | 选择0张200的,求出用后面的面值凑出1000的方法数 30 | 选择1张200的,求出用后面的面值凑出800的方法数 31 | 选择2张200的,求出用后面的面值凑出600的方法数 32 | ......... 33 | 选择5张200的,求出用后面的面值凑出0的方法数 34 | 35 | 将以上方法数加起来,就是答案 36 | ```java 37 | public static int coins1(int[] arr, int aim) { 38 | if (arr == null || arr.length == 0 || aim < 0) { 39 | return 0; 40 | } 41 | return process1(arr, 0, aim); 42 | } 43 | //自由使用index及其之后所有的面值,凑成目标钱数aim的方法数 44 | public static int process1(int[] arr, int index, int aim) { 45 | int res = 0; 46 | //递归结束条件:如果index来到了最后的位置,已经没有可选的面值了 47 | //如果这时目标钱数aim为0,则之前所选是一种方法。如果aim不为0,说明没有可选面值但还有钱没凑出来,说明之前所选不是一种可行方法。 48 | if (index == arr.length) { 49 | res = aim == 0 ? 1 : 0; 50 | } 51 | else { 52 | //i表示使用几张arr[index]面值 53 | for (int i = 0; arr[index] * i <= aim; i++) 54 | res += process1(arr, index + 1, aim - arr[index] * i); 55 | } 56 | return res; 57 | } 58 | ``` 59 | 60 | ## 方法二(动态规划) 61 | 由暴力递归改动态规划有如下几步: 62 | 1. 分析可变参数,本题中返回值仅由aim和index两个参数决定。因此可变参数为2个。需要一个二维dp数组。 63 | 2. 分析可变参数的变化范围:index的变化范围为数组arr的长度,aim的变化范围为0到aim这aim+1个值。分析完这前两步,我们就可以构造dp数组 64 | 65 | ```java 66 | //dp[i][j]表示用arr中0到i个面值凑出钱数j的方法数 67 | int[][] dp = new int[arr.length][aim + 1]; 68 | ``` 69 | 3. 确定Base Case: i为0和j为0时的情况 70 | 4. 根据暴力递归的递归主体,分析出一个普遍位置是怎么依赖的。然后依次填充dp数组 71 | 72 | ## 代码 73 | ```java 74 | public int change(int aim, int[] arr) { 75 | //处理边界 76 | if(aim == 0) 77 | return 1; 78 | if (arr == null || arr.length == 0 || aim < 0) { 79 | return 0; 80 | } 81 | //构造dp数组 82 | int[][] dp = new int[arr.length][aim + 1]; 83 | //设置base case 84 | for (int i = 0; i < arr.length; i++) { 85 | dp[i][0] = 1; 86 | } 87 | for (int j = 1; arr[0] * j <= aim; j++) { 88 | dp[0][arr[0] * j] = 1; 89 | } 90 | int num = 0; 91 | //填充dp数组 92 | for (int i = 1; i < arr.length; i++) { 93 | for (int j = 1; j <= aim; j++) { 94 | num = 0; 95 | for (int k = 0; j - arr[i] * k >= 0; k++) { 96 | num += dp[i - 1][j - arr[i] * k]; 97 | } 98 | dp[i][j] = num; 99 | } 100 | } 101 | return dp[arr.length - 1][aim]; 102 | } 103 | ``` -------------------------------------------------------------------------------- /动态规划/53.最大子序和.md: -------------------------------------------------------------------------------- 1 | # 53 最大子序和 2 | 3 | **题目:** 4 | 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 5 | 6 | > 示例: 7 | 输入: [-2,1,-3,4,-1,2,1,-5,4], 8 | 输出: 6 9 | 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 10 | 11 | **思路:** 12 | >方法:用动态规划求解。 13 | 用函数f(i)表示以第i个数字结尾的子数组的最大和。 14 | 题目要求的是max[f(i)] 15 | 若i=0或f(i-1)<=0,则f(i) = pData[i]. 16 | 若i!=0并且f(i-1) > 0, 则f(i) = pData[i] + f(i-1) 17 | 18 | 19 | **代码:** 20 | ```java 21 | class Solution { 22 | public int maxSubArray(int[] nums) { 23 | int maxSum = nums[0]; 24 | //pre表示以第i-1个数字结尾的子数组的最大和。将其初始化为nums[0] 25 | int pre = nums[0]; 26 | int cur; 27 | for(int i = 1;i < nums.length; i++){ 28 | //如果pre小于0,则以第i个数字结尾的子数组的最大值cur就是这个数字本身,不带上pre。更新maxSum 29 | if(pre <= 0){ 30 | cur = nums[i]; 31 | maxSum = Math.max(maxSum, cur); 32 | } 33 | //如果pre大于0,则以第i个数字结尾的子数组的最大值cur为以第i-1个数字结尾的子数组的最大和pre加上当前位置值。 34 | else{ 35 | cur = pre + nums[i]; 36 | maxSum = Math.max(maxSum, cur); 37 | } 38 | //更新pre 39 | pre = cur; 40 | } 41 | return maxSum; 42 | } 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /动态规划/62.不同路径.md: -------------------------------------------------------------------------------- 1 | # 62. 不同路径 2 | 3 | ## 题目 4 | 5 | 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 6 | 7 | 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 8 | 9 | 问总共有多少条不同的路径? 10 | 11 | 示例 1: 12 | 输入: m = 3, n = 2 13 | 输出: 3 14 | 解释: 15 | 从左上角开始,总共有 3 条路径可以到达右下角。 16 | 1. 向右 -> 向右 -> 向下 17 | 2. 向右 -> 向下 -> 向右 18 | 3. 向下 -> 向右 -> 向右 19 | 20 | 注意:如果把网格看成矩阵的话,m为列数,n为行数 21 | 22 | ## 方法(动态规划) 23 | 24 | * 设$dp[i,j]$为从左上角开始,到达点(i,j)的所有可能路径数。由题意可知,若想到达一个点,只能从该点的上方或者左方到达。因此有如下状态转移方程: 25 | $$ dp[i,j] = dp[i-1,j] + dp[i, j-1]$$ 26 | 27 | * 确定base case: 28 | * 对于网格第一列上的所有点(j = 0),若想到达它,只能从上方到达,即: 29 | $$ dp[i,j] = dp[i-1,j]$$ 30 | * 对于网格第一行上的所有点(i = 0),若想到达它,只能从左方到达,即: 31 | $$dp[i,j] = dp[i, j-1]$$ 32 | * 对于网格左上角的点,dp[0,0] = 1; 33 | 34 | ## 代码 35 | ```java 36 | public int uniquePaths(int m, int n) { 37 | if(m == 1 || n == 1) 38 | return 1; 39 | int[][] dp = new int[n][m]; 40 | //设置base case 41 | dp[0][0] = 1; 42 | for(int j = 1; j < m; j++) 43 | dp[0][j] = dp[0][j - 1]; 44 | for(int i = 1; i < n; i++) 45 | dp[i][0] = dp[i - 1][0]; 46 | //填写dp数组 47 | for(int i = 1; i < n; i++){ 48 | for(int j = 1; j < m; j++) 49 | dp[i][j] = dp[i-1][j] + dp[i][j-1]; 50 | } 51 | return dp[n - 1][m - 1]; 52 | } 53 | ``` 54 | 55 | ## 方法(数学) 56 | 机器人要从左上角走到右下角,需要向右走n-1步,向下走m-1步,总共需要走m+n-2步。 57 | 58 | 我们要找路径数,也就是要找在这m + n - 2步中,选择n - 1步向右走的方案数。即: 59 | $$ 60 | C_{m+n-2}^{n-1}=\frac{(m+n-2) !}{(n-1) !(n-1) !} 61 | $$ 62 | 63 | ```java 64 | public int uniquePaths(int m, int n) { 65 | int N = n + m - 2; 66 | double res = 1; 67 | for (int i = 1; i < m; i++) 68 | res = res * (N - (m - 1) + i) / i; 69 | return (int) res; 70 | } 71 | ``` -------------------------------------------------------------------------------- /动态规划/63.不同路径II.md: -------------------------------------------------------------------------------- 1 | # 63.不同路径 II 2 | 3 | ## 题目 4 | 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 5 | 6 | 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 7 | 8 | **现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径** 9 | 10 | 网格中的障碍物和空位置分别用 1 和 0 来表示。 11 | 12 | 示例 1: 13 | 输入: 14 | [ 15 |   [0,0,0], 16 |   [0,1,0], 17 |   [0,0,0] 18 | ] 19 | 输出: 2 20 | 解释: 21 | 3x3 网格的正中间有一个障碍物。 22 | 从左上角到右下角一共有 2 条不同的路径: 23 | 1. 向右 -> 向右 -> 向下 -> 向下 24 | 2. 向下 -> 向下 -> 向右 -> 向右 25 | 26 | ## 方法(动态规划) 27 | 本题源于[62.不同路径],不同之处在于本题的网格中存在障碍物。由题易知,障碍物是不能涉足之地,设$dp[i,j]$为从左上角开始,到达点(i,j)的所有可能路径数。因此: 28 | * 如果一个点的上面和左面都没有障碍物,那么到达它可能的路径数目为: 29 | $$dp[i,j] = dp[i-1,j] + dp[i, j-1]$$ 30 | * 如果一个点的上面有障碍物,那么若想到达它,只能从左方到达,即: 31 | $$dp[i,j] = dp[i, j-1]$$ 32 | * 如果一个点的左面有障碍物,若想到达它,只能从上方到达,即: 33 | $$dp[i,j] = dp[i-1,j]$$ 34 | * 如果一个点的上面和左面都有障碍物,那么该点对应的dp[i][j] = 0 35 | * base case: 36 | * 正常来说第一行的点对应的dp[i,j]都是1。但如果第一行存在障碍物,那么障碍物右面的点对应的dp[i,j]是0 37 | * 正常来说第一列的点对应的dp[i,j]都是1。但如果第一列存在障碍物,那么障碍物下面的点对应的dp[i,j]都是0 38 | 39 | ```java 40 | public int uniquePathsWithObstacles(int[][] obstacleGrid) { 41 | int n = obstacleGrid.length, m = obstacleGrid[0].length; 42 | if(obstacleGrid[0][0] == 1 || obstacleGrid[n - 1][m - 1] == 1) 43 | return 0; 44 | int[][] dp = new int[n][m]; 45 | //以下设置base case(第一行和第一列) 46 | dp[0][0] = 1; 47 | //以下两个变量分别记录第一行和第一列是否出现过障碍物 48 | boolean row_flag = false; 49 | boolean col_flag = false; 50 | for(int i = 1; i < n; i++){ 51 | col_flag = obstacleGrid[i][0] == 1 ? true : col_flag; 52 | dp[i][0] = col_flag ? 0 : 1; 53 | } 54 | for(int j = 1; j < m; j++){ 55 | row_flag = obstacleGrid[0][j] == 1 ? true : row_flag; 56 | dp[0][j] = row_flag ? 0 : 1; 57 | } 58 | //以下填充dp数组 59 | for(int i = 1; i < n; i++){ 60 | for(int j = 1; j < m; j++){ 61 | if(obstacleGrid[i - 1][j] == 1 && obstacleGrid[i][j - 1] == 1) 62 | dp[i][j] = 0; 63 | else if(obstacleGrid[i - 1][j] == 1) 64 | dp[i][j] = dp[i][j - 1]; 65 | else if(obstacleGrid[i][j - 1] == 1) 66 | dp[i][j] = dp[i - 1][j]; 67 | else 68 | dp[i][j] = dp[i-1][j] + dp[i][j-1]; 69 | } 70 | } 71 | return dp[n - 1][m - 1]; 72 | } 73 | ``` 74 | 75 | 76 | -------------------------------------------------------------------------------- /动态规划/70.爬楼梯.md: -------------------------------------------------------------------------------- 1 | # 70 爬楼梯 2 | 3 | **题目:** 4 | 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 5 | 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? 6 | 注意:给定 n 是一个正整数。 7 | 8 | 示例 1: 9 | 输入: 2 10 | 输出: 2 11 | 解释: 有两种方法可以爬到楼顶。 12 | 1. 1 阶 + 1 阶 13 | 2. 2 阶 14 | 15 | 示例 2: 16 | 输入: 3 17 | 输出: 3 18 | 解释: 有三种方法可以爬到楼顶。 19 | 1. 1 阶 + 1 阶 + 1 阶 20 | 2. 1 阶 + 2 阶 21 | 3. 2 阶 + 1 阶 22 | 23 | 24 | **思路:** 25 | >爬楼梯问题的本质是斐波那契数列,设对于n层台阶,爬到楼顶的方法数为climbStairs(n)。分为两种情况,第一步跳1阶,第2步跳2阶,方法数分别为climbStairs(n-1),climbStairs(n-2)。则climbStairs(n) = climbStairs(n-1) + climbStairs(n-2)。 26 | 但用递归的方法实现时,会产生很多的重复计算。因此可以采用迭代的方法。即采用自下而上的方法取代自上而下的方法。 27 | 28 | **代码:** 29 | ```java 30 | class Solution { 31 | //设对于n层台阶,爬到楼顶的方法数为climbStairs(n) 32 | //分为两种情况,第一步跳1阶,第2步跳2阶,方法数分别为climbStairs(n-1),climbStairs(n-2) 33 | //则climbStairs(n) = climbStairs(n-1) + climbStairs(n-2) 34 | public int climbStairs(int n) { 35 | if(n == 0 || n == 1) 36 | return 1; 37 | if(n == 2) 38 | return 2; 39 | int[] dp = new int[n + 1]; 40 | dp[1] = 1; 41 | dp[2] = 2; 42 | for(int i = 3; i <=n ; i++) 43 | dp[i] = dp[i-1] + dp[i - 2]; 44 | return dp[n]; 45 | } 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /动态规划/714.买卖股票的最佳时机含手续费.md: -------------------------------------------------------------------------------- 1 | # 714.买卖股票的最佳时机含手续费 2 | 3 | ## 题目 4 | 给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。 5 | 6 | 你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。 7 | 8 | 返回获得利润的最大值。 9 | 10 | 注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。 11 | 12 | 示例 1: 13 | 输入: prices = [1, 3, 2, 8, 4, 9], fee = 2 14 | 输出: 8 15 | 解释: 能够达到的最大利润: 16 | 在此处买入 prices[0] = 1 17 | 在此处卖出 prices[3] = 8 18 | 在此处买入 prices[4] = 4 19 | 在此处卖出 prices[5] = 9 20 | 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8. 21 | 22 | ## 方法(动态规划) 23 | 由于每一笔交易只需要支付一次手续费,依据生活经验,我们做如下的约定:只在买入股票时支付手续费,卖出时不支付手续费。 24 | ### 1.定义dp数组 25 | 由于在一个时刻,手中只有持有股票和不持有股票这两种情况,因此定义二维dp数组: 26 | * dp[i][0]:第i天时,如果手中没有股票,这时所能获得的最大利润 27 | * dp[i][1]: 第i天时,如果手中持有股票,这时所能获得的最大利润 28 | 29 | ### 2.Base Case 30 | 如果第0天不持有股票,说明还没有买过股票,利润为0,因此: 31 | $$dp[0][0] = 0$$ 32 | 如果第0天持有股票,说明就在第0天这天买了股票,不仅没有利润,还花费了prices[0]和手续费,因此: 33 | $$dp[0][1] = -prices[0] - fee$$ 34 | ### 3.状态转移方程 35 | * 第i天手中没有股票这个状态可以由两个状态转移而来: 36 | * 第i - 1天时,手中就没有股票 37 | * 第i - 1天时,手中持有股票,但在第i天时卖出了股票,赚了prices[i] 38 | 39 | 于是,dp[i][0]即为以上两种情况的较大值,即: 40 | $$dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i])$$ 41 | 42 | * 第i天手中持有股票这个状态可以由两个状态转移而来: 43 | * 第i - 1天时,手中就持有股票 44 | * 第i - 1天时,手中没有股票,但在第i天时买入了股票,花费了prices[i]以及手续费 45 | 46 | 于是,dp[i][1]即为以上两种情况的较大值,即: 47 | $$dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee) 48 | 49 | ## 代码 50 | ```java 51 | public int maxProfit(int[] prices, int fee) { 52 | int[][] dp = new int[prices.length][2]; 53 | dp[0][0] = 0; 54 | dp[0][1] = - prices[0] - fee; 55 | for(int i = 1; i < prices.length; i++){ 56 | dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); 57 | dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee); 58 | } 59 | //在最后一天也可能有手中有股票和无股票两种情况,但都最后一天了,手中有股票不卖肯定亏了,所以手中无股票才会是这时利润最大的情况 60 | return dp[prices.length - 1][0]; 61 | } 62 | ``` 63 | 64 | * 时间复杂度:O(n) 65 | * 空间复杂度:O(n) 66 | 67 | 因为一个时刻的状态只依赖于它前一个时刻的状态,所以可以我们可以进行空间复杂度优化如下: 68 | 69 | ```java 70 | public int maxProfit(int[] prices, int fee) { 71 | //noStock代表手中无股票,haveStock代表手中持有股票 72 | //二者分别对应着优化前的dp[i][0]和dp[i][1] 73 | int noStock = 0; 74 | int haveStock = - prices[0] - fee; 75 | for(int i = 1; i < prices.length; i++){ 76 | noStock = Math.max(noStock, haveStock + prices[i]); 77 | haveStock = Math.max(haveStock, noStock - prices[i] - fee); 78 | } 79 | return noStock; 80 | } 81 | ``` 82 | 83 | * 时间复杂度:O(n) 84 | * 空间复杂度:O(1) -------------------------------------------------------------------------------- /动态规划/72. 编辑距离.md: -------------------------------------------------------------------------------- 1 | # 72. 编辑距离 2 | 3 | ## 题目 4 | 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 5 | 6 | 你可以对一个单词进行如下三种操作: 7 | 8 | 插入一个字符 9 | 删除一个字符 10 | 替换一个字符 11 |   12 | 13 | 示例 1: 14 | 输入:word1 = "horse", word2 = "ros" 15 | 输出:3 16 | 解释: 17 | horse -> rorse (将 'h' 替换为 'r') 18 | rorse -> rose (删除 'r') 19 | rose -> ros (删除 'e') 20 | 21 | 示例 2: 22 | 输入:word1 = "intention", word2 = "execution" 23 | 输出:5 24 | 解释: 25 | intention -> inention (删除 't') 26 | inention -> enention (将 'i' 替换为 'e') 27 | enention -> exention (将 'n' 替换为 'x') 28 | exention -> exection (将 'n' 替换为 'c') 29 | exection -> execution (插入 'u') 30 | 31 | ## 方法(动态规划) 32 | ### 1.定义dp数组 33 | 定义dp[i][j] 为 word1的前i个字符word1[0...i-1]转换成word2的前j个字符word2[0...j-1]需要的最少操作数 34 | 35 | ### 2.Base Case 36 | * 首先,空串到到空串不需要任何操作。因此dp[0][0] = 0 37 | * i=0时,要找空串word1匹配到word2前j个字符的最小步数,也即对word1进行插入操作的数量,也就是j 38 | * j=0时,要找word1前i个字符匹配到空串word2的最小步数,也即对word1进行删除操作的数量,也就是i 39 | 40 | ### 3.状态转移方程 41 | * 当word1[i-1]等于word2[j-1]时,相当于在这一步不需要任何操作,因此dp[i][j] = dp[i-1][j-1] 42 | * 当word1[i-1]不等于word2[j-1],可以对word1[i-1]进行替换、插入、删除三种操作 43 | 1. 替换:将word1[i-1]替换成word2[j-1],需要一次操作。替换后二者相等,于是:dp[i][j] = dp[i - 1][j - 1] + 1 44 | 2. 插入:在word1[i-1]后面插入与word2[j-1]相等的字符,需要一次操作,插入的字符和word2[j-1]匹配,二者抵消,在word2中就不用再考虑word2[j-1]位置了。于是:dp[i][j] = dp[i][j - 1] + 1 45 | 3. 删除:既然word1[i-1]与word2[j-1]不相等,那我们就删除掉word1[i-1]这个字符,需要一次操作。于是:dp[i][j] = dp[i - 1][j] + 1 46 | 47 | 最后取上述三种操作的最小值即为dp[i][j],即: 48 | $$dp[i][j] = min(dp[i - 1][j - 1],dp[i - 1][j - 1],dp[i - 1][j]) + 1$$ 49 | 50 | ## 代码 51 | ```java 52 | public int minDistance(String word1, String word2) { 53 | int[][] dp = new int[word1.length() + 1][word2.length() + 1]; 54 | //Base Case 55 | dp[0][0] = 0; 56 | for(int j = 1; j <= word2.length(); j++) 57 | dp[0][j] = j; 58 | for(int i = 1; i <= word1.length(); i++) 59 | dp[i][0] = i; 60 | //状态转移 61 | for(int i = 1; i <= word1.length(); i++){ 62 | for(int j = 1; j <= word2.length(); j++){ 63 | if(word1.charAt(i - 1) == word2.charAt(j - 1)) 64 | dp[i][j] = dp[i - 1][j - 1]; 65 | else 66 | dp[i][j] = Math.min(dp[i - 1][j - 1], Math.min(dp[i][j - 1], dp[i - 1][j])) + 1; 67 | } 68 | } 69 | return dp[word1.length()][word2.length()]; 70 | } 71 | ``` -------------------------------------------------------------------------------- /动态规划/91.解码方法.md: -------------------------------------------------------------------------------- 1 | # 91.解码方法 2 | 3 | ## 题目 4 | 一条包含字母 A-Z 的消息通过以下方式进行了编码: 5 | 6 | 'A' -> 1 7 | 'B' -> 2 8 | ... 9 | 'Z' -> 26 10 | 给定一个只包含数字的非空字符串,请计算解码方法的总数。 11 | 12 | 示例 1: 13 | 输入: "12" 14 | 输出: 2 15 | 解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。 16 | 17 | 示例 2: 18 | 输入: "226" 19 | 输出: 3 20 | 解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。 21 | 22 | ## 方法(动态规划) 23 | 24 | 此题和[70.爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/)这道题相似。爬楼梯要求每次只能爬一个台阶或两个台阶,所以爬n层台阶的爬法是爬n-1层台阶和爬n-2层台阶的爬法之和。而对于此题,一个字符既可以单独解码(例如:'1' -> A),又可以和它的前一个字符共同解码(例如:'26' -> Z)。 25 | 26 | 设dp[i]表示字符串前i个字符组成的子串s[0...i-1]的解码方法个数。于是从整体逻辑上而言:dp[i] = dp[i-1] + dp[i-2](和爬楼梯一样) 27 | 但本题比爬楼梯复杂在如下两点: 28 | * 对"0"的判断:0不能单独进行解码 29 | * 对两个字符组合的判断:只有当1<=s[i-1...i]<=26,这两个字符才可以一起解码 30 | 31 | 32 | ## 代码 33 | ```java 34 | public int numDecodings(String s) { 35 | if(s == null || s.length() == 0) 36 | return 0; 37 | //合法的编码第一位不可能是0 38 | if(s.charAt(0) == '0') 39 | return 0; 40 | //dp[i]为s中前i个字符的解码方法个数 41 | int[] dp = new int[s.length() + 1]; 42 | //Base Case 43 | dp[0] = 1; 44 | dp[1] = s.charAt(0) == '0' ? 0 : 1; 45 | for(int i = 2; i <= s.length(); i++){ 46 | //one为单个位解码所表示的数字,two为两位一起解码所表示的数字 47 | int one = Integer.valueOf(s.substring(i - 1, i)); 48 | int two = Integer.valueOf(s.substring(i - 2, i)); 49 | //单个位能解码的前提是:这个位上不是0 50 | if(one != 0) 51 | dp[i] += dp[i - 1]; 52 | //两个位能一起解码的前提是:两个位所表示的数字小于26并且第一个位不能是0 53 | if(s.charAt(i - 2) != '0' && two <= 26) 54 | dp[i] += dp[i - 2]; 55 | } 56 | return dp[s.length()]; 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /双指针/11.盛水最多的容器.md: -------------------------------------------------------------------------------- 1 | # 11 盛水最多的容器 2 | **题目:** 3 | 给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 4 | 5 | 说明:你不能倾斜容器,且 n 的值至少为 2。 6 | 7 | 示例: 8 | 输入:[1,8,6,2,5,4,8,3,7] 9 | 输出:49 10 | 11 | 12 | **思路:双指针法** 13 | 水的面积为左柱子和右柱子中高度的最小值乘上两个柱子之间的距离。最开始将左右柱子分别设为最左边和最右边的柱子,此时两柱子间的距离是最大的,但两柱子的高度未必是。记下当前的面积,之后不断将两柱子向内移动,并同时更新最大面积。当两柱子相遇时,代表所有有可能产生最大面积的柱子组合都被尝试完毕,此时返回最大面积。 14 | 15 | 但两柱子具体是怎样移动的呢?考虑将两柱子向内移动意味着两根柱子之间的距离不断地缩小,所以要达到最大面积,必然寄希望于移动后两根柱子中的最小值大于当前两柱子中的最小值。如果较短的柱子不移动,两柱子高度的最小值则不变化,但两柱子间距变小了,这种情况肯定不可能得到更大的面积,所以这种情况就不必尝试。 16 | 17 | 每次将两柱子中较短的那个柱子向内移动,较长的柱子不移动。这种情况移动后可能产生更大面积,也可能不会产生更大面积。但每一次尝试都将当前的面积与之前保存的最大面积比较,并更新最大面积,这样就可以通过不断尝试获得最大面积。 18 | 19 | **代码:** 20 | ```java 21 | public int maxArea(int[] height) { 22 | int left = 0; 23 | int right = height.length - 1; 24 | int max_area = 0; 25 | while(left < right){ 26 | int cur_area = Math.min(height[left],height[right]) * (right - left); 27 | max_area = Math.max(max_area, cur_area); 28 | if(height[left] < height[right]) 29 | left++; 30 | else 31 | right--; 32 | } 33 | return max_area; 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /双指针/167. 两数之和 II - 输入有序数组.md: -------------------------------------------------------------------------------- 1 | # 167. 两数之和 II - 输入有序数组 2 | 3 | **题目:** 4 | 给定一个已按照升序排列的有序数组,找到两个数使得它们相加之和等于目标数。 5 | 函数应该返回这两个下标值 index1 和 index2,其中 index1必须小于index2。 6 | 7 | 示例: 8 | 输入: numbers = [2, 7, 11, 15], target = 9 9 | 输出: [1,2] 10 | 解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。 11 | 12 | 13 | **思路:双指针法** 14 | 定义两个指针left和right,初始化时分别指向数组的两端 15 | 如果left和right对应的值加起来等于target,那么可直接返回。因为数组是排序好的,如果和小于target,也即希望和大一些,所以将left右移一位。如果和大于target,也即希望和小一些,所以将right左移一位。如此进行循环直到找到相等的时候,若找不到则返回null 16 | 17 | **代码** 18 | ```java 19 | public int[] twoSum(int[] numbers, int target) { 20 | if(numbers == null) 21 | return null; 22 | int left = 0; 23 | int right = numbers.length - 1; 24 | while(left < right){ 25 | if(numbers[left] + numbers[right] == target) 26 | return new int[]{left+1, right+1}; 27 | else if(numbers[left] + numbers[right] < target) 28 | left++; 29 | else 30 | right--; 31 | } 32 | return null; 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /双指针/202.快乐数.md: -------------------------------------------------------------------------------- 1 | # 202 快乐数 2 | 3 | 编写一个算法来判断一个数 n 是不是快乐数。 4 | 5 | 「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为  1,那么这个数就是快乐数。 6 | 7 | 如果 n 是快乐数就返回 True ;不是,则返回 False 。 8 | 9 | 10 | 示例: 11 | 输入:19 12 | 输出:true 13 | 解释: 14 | 1*2 + 9*2 = 82 15 | 8*2 + 2*2 = 68 16 | 6*2 + 8*2 = 100 17 | 1*2 + 0*2 + 0*2 = 1 18 | 19 | ## 题目分析: 20 | 只会有以下可能的情况: 21 | * 最终得到1 22 | * 最终走进循环 23 | * 数越来越大趋近于无穷(但其实不可能,因为即使是最大的三位数999的下一个数也只能是243) 24 | 25 | 所以只有前两种情况可能出现,只需要判断最后是否进入循环即可。即是否出现过已经出现过的数字。分析到这里自然会想到用哈希(HashSet) 26 | 27 | ## 代码 28 | 分两部分实现 29 | 首先判断**一个数字对应的下一个数是什么**,我们只需要提取出这个数字的每个位即可. 30 | ``` Java 31 | //此函数用来获得n的每个位上的数字的平方和 32 | public int getSum(int n){ 33 | int sum = 0; 34 | while(n > 0){ 35 | sum += (n%10) * (n%10); 36 | n = n / 10; 37 | } 38 | return sum; 39 | } 40 | ``` 41 | 其次,判断这个数是否已经在HashSet中,若不在将它加入,若要放入的数字已经在HashSet中,说明走进了循环,返回false 42 | ``` java 43 | public boolean isHappy(int n) { 44 | //只有两种情况,一种循环到1,另一种循环到已经到过的一个数,进入了一个环。前者为true,后者为false 45 | Set seen = new HashSet<>(); 46 | while(n != 1 && !seen.contains(n)){ 47 | seen.add(n); 48 | n = getSum(n); 49 | } 50 | //退出循环有两种情况 51 | //1.已经循环到了1. 52 | //2. 循环到了曾经走过的点 53 | return n==1; 54 | } 55 | ``` 56 | 57 | **时间复杂度和空间复杂度:** 58 | 都是O(logn) 59 | 60 | ## 另一种方法:快慢指针法 61 | 既然上述分析时考虑到了是否存在循环的问题,则可以用链表的思想来考虑。一个节点计算其各位平方再求和的过程可以类似于链表用next指针指向下一个节点。 62 | 于是可以用快慢指针检查是否出现了环 63 | 64 | ```java 65 | public boolean isHappy(int n) { 66 | int slow = n; 67 | int fast = getSum(n); 68 | while(fast != 1 && fast != slow){ 69 | //以下两步可以看作: 70 | //slow = slow.next; 71 | //fast = fast.next.next; 72 | slow = getSum(slow); 73 | fast = getSum(getSum(fast)); 74 | } 75 | return fast == 1; 76 | } 77 | ``` 78 | -------------------------------------------------------------------------------- /双指针/75.颜色分类.md: -------------------------------------------------------------------------------- 1 | # 75 颜色分类 2 | **题目:** 3 | 给定一个包含红色、白色和蓝色,一共n个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。 4 | 此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。 5 | 6 | 示例: 7 | 输入: [2,0,2,1,1,0] 8 | 输出: [0,0,1,1,2,2] 9 | 10 | **直观思路:** 11 | 两次遍历数组,第一次遍历时记录下红白蓝三色(0、1、2)分别出现的次数,第二次遍历时重写当前数组 12 | 13 | **只用一次遍历的方法:** 14 | 同荷兰国旗问题的解决方法类似。方法阐述见代码: 15 | ```java 16 | public void sortColors(int[] nums) { 17 | //red表示红色区域的右边界,blue表示蓝色区域的左边界 18 | //初始化时,红色区域和蓝色区域都不存在 19 | int red = -1; 20 | int blue = nums.length; 21 | //cur为游标,代表遍历到的当前位置 22 | int cur = 0; 23 | //等到cur与blue区域相遇时,跳出循环,遍历结束 24 | while(cur < blue){ 25 | //如果当前节点是红色的,交换这个节点与红色区域的下一个节点,红色区域右移一位。 26 | if(nums[cur] == 0) 27 | swap(nums, ++red, cur++); 28 | //如果当前节点是蓝色的,交换这个节点与蓝色区域的前一个节点,蓝色区域左移一位 29 | //当前cur不变,交换过来的cur位置值需要继续进入while循环,判断此处是蓝色白色还是红色 30 | else if(nums[cur] == 2) 31 | swap(nums, --blue, cur); 32 | //如果当前节点是白色的,红色区域和蓝色区域均不变,cur继续右移 33 | else 34 | cur++; 35 | } 36 | } 37 | public void swap(int[] nums, int i, int j){ 38 | int temp = nums[i]; 39 | nums[i] = nums[j]; 40 | nums[j] = temp; 41 | } 42 | ``` 43 | -------------------------------------------------------------------------------- /双指针/88.合并两个有序数组.md: -------------------------------------------------------------------------------- 1 | # 88.合并两个有序数组 2 | 3 | ## 题目 4 | 给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。 5 | 6 | 说明: 7 | 8 | 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。 9 | 你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。 10 |   11 | 12 | 示例: 13 | 输入: 14 | nums1 = [1,2,3,0,0,0], m = 3 15 | nums2 = [2,5,6], n = 3 16 | 17 | 输出:[1,2,2,3,5,6] 18 | 19 | ## 方法(双指针) 20 | 使用两个指针,一个指向nums1中的元素,一个指向nums2中的元素 21 | 22 | 如果从前向后遍历的话,由于是从小到大重置数组nums1,向nums1中插入nums2元素的操作会造成后面元素的移动,增加时间复杂度。 23 | 24 | 因此我们需要从后向前遍历,也就是按元素从大到小重置数组nums1,每次比较指针p1和指针p2指向的nums1和nums2中的元素,每次将大的元素往nums1末尾放,直到放置好所有的元素。 25 | 26 | ```java 27 | public void merge(int[] nums1, int m, int[] nums2, int n) { 28 | int p1 = m - 1; 29 | int p2 = n - 1; 30 | int index = m + n - 1; 31 | while(p1 >= 0 && p2 >= 0){ 32 | nums1[index--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--]; 33 | } 34 | while(p1 >= 0) 35 | nums1[index--] = nums1[p1--]; 36 | while(p2 >= 0) 37 | nums1[index--] = nums2[p2--]; 38 | } 39 | ``` 40 | 41 | * 时间复杂度:O(n) 42 | * 空间复杂度:O(1) -------------------------------------------------------------------------------- /回溯算法/131.分割回文串.md: -------------------------------------------------------------------------------- 1 | # 131.分割回文串 2 | 3 | ## 题目 4 | 5 | 给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。 6 | 7 | 返回 s 所有可能的分割方案。 8 | 9 | 示例: 10 | 输入: "aab" 11 | 输出: 12 | [ 13 | ["aa","b"], 14 | ["a","a","b"] 15 | ] 16 | 17 | ## 方法(回溯) 18 | * 选择: 在决策树的每一个节点。我们可以做的选择为:只截取当前字符、截取当前字符和后一个字符、截取当前字符和后两个字符......截取当前字符和之后所有的字符。截取之后进行是否是回文的判断,如果是回文,则就是一个合理的选择。 19 | * 路径:记录结果的path列表。 20 | * 终止条件:遍历完了字符串s的每一个节点。 21 | 22 | 23 | ## 代码 24 | ```java 25 | List> res = new LinkedList<>(); 26 | String s; 27 | public List> partition(String s) { 28 | this.s = s; 29 | backtrack(0, new LinkedList<>()); 30 | return res; 31 | } 32 | 33 | public void backtrack(int start, List path){ 34 | if(start == s.length()){ 35 | res.add(new LinkedList<>(path)); 36 | return; 37 | } 38 | for(int i = start; i < s.length(); i++){ 39 | if(!isPalindrome(start, i)) 40 | continue; 41 | path.add(s.substring(start, i + 1)); 42 | backtrack(i + 1, path); 43 | path.remove(path.size() - 1); 44 | } 45 | } 46 | public boolean isPalindrome(int start, int end){ 47 | if(start < 0 && end > s.length() - 1 && start >= end) 48 | return false; 49 | int left = start; 50 | int right = end; 51 | while(left < right){ 52 | if(s.charAt(left) != s.charAt(right)) 53 | return false; 54 | left++; 55 | right--; 56 | } 57 | return true; 58 | } 59 | ``` -------------------------------------------------------------------------------- /回溯算法/22.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/回溯算法/22.jpg -------------------------------------------------------------------------------- /回溯算法/22.括号生成.md: -------------------------------------------------------------------------------- 1 | # 22.括号生成 2 | 3 | ## 题目 4 | 数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。 5 | 6 | 7 | 示例: 8 | 9 | 输入:n = 3 10 | 输出:[ 11 | "((()))", 12 | "(()())", 13 | "(())()", 14 | "()(())", 15 | "()()()" 16 | ] 17 | 18 | ## 方法(回溯法) 19 | 对于回溯法,需要重点关注三点: 20 | * 结束条件:起初我们手中有n个左括号和n个右括号,当左括号和右括号都用完时,递归结束。将路径字符串加入res 21 | * 选择:在每一步中我们都有两个选择:选一个左括号或选一个右括号。但是也有以下前提条件: 22 | * 能够选左括号的前提:左括号数量大于0 23 | * 能够选右括号的前提:已使用的左括号数量大于已使用的右括号数量。(产生右括号时,前面如果没有足够的左括号与之匹配,则会产生不合理的字符串结果),即左括号剩余数量left要大于右括号剩余数量right。 24 | 25 | 在做选择时,考虑以上两个条件,即可实现剪枝操作。 26 | * 路径:路径内容应该包括:已产生的字符串path,剩余的左括号left以及剩余的右括号right。 27 | 28 | 例如,n=2时,可以画出决策树如下: 29 | ![](22.jpg) 30 | 31 | ## 代码 32 | ```java 33 | class Solution { 34 | List res = new LinkedList<>(); 35 | int n; 36 | public List generateParenthesis(int n) { 37 | this.n = n; 38 | backtrack(new StringBuilder(), n, n); 39 | return res; 40 | } 41 | 42 | public void backtrack(StringBuilder path, int left, int right){ 43 | //结束条件 44 | if(left == 0 && right == 0){ 45 | res.add(new String(path)); 46 | return; 47 | } 48 | //选择1:使用左括号 49 | if(left > 0){ 50 | path.append('('); 51 | backtrack(path, left - 1, right); 52 | path.deleteCharAt(path.length() - 1); 53 | } 54 | //选择2:使用右括号 55 | if(left < right && right > 0){ 56 | path.append(')'); 57 | backtrack(path, left, right - 1); 58 | path.deleteCharAt(path.length() - 1); 59 | } 60 | } 61 | } 62 | ``` 63 | -------------------------------------------------------------------------------- /回溯算法/39-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/回溯算法/39-1.png -------------------------------------------------------------------------------- /回溯算法/39-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/回溯算法/39-2.png -------------------------------------------------------------------------------- /回溯算法/46.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/回溯算法/46.jpg -------------------------------------------------------------------------------- /回溯算法/79.单词搜索.md: -------------------------------------------------------------------------------- 1 | # 79.单词搜索 2 | 3 | ## 题目 4 | 给定一个二维网格和一个单词,找出该单词是否存在于网格中。 5 | 6 | 单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。 7 | 8 |   9 | 10 | 示例: 11 | 12 | board = 13 | [ 14 | ['A','B','C','E'], 15 | ['S','F','C','S'], 16 | ['A','D','E','E'] 17 | ] 18 | 19 | 给定 word = "ABCCED", 返回 true 20 | 给定 word = "SEE", 返回 true 21 | 给定 word = "ABCB", 返回 false 22 | 23 | 24 | ## 方法(回溯算法) 25 | 回溯法需要重点考察三个内容: 26 | * 路径:记录我们当前匹配到了word中的哪个字符。我们用index表示当前匹配到的word字符的索引。 27 | * 结束条件:当匹配到了word中的最后一个字符并且匹配成功时,回溯结束,返回true 28 | * 选择:由于同一个单元格中的字母不能被重复使用,因此我们用一个矩阵uesd记录位置[i,j]上的字符是否被使用过。如果board的当前位置[i,j]上的字符与word的index位置上的字符匹配,那么就做选择(使用[i,j]位置上的元素)。做完选择之后,我们递归匹配board中的下一个位置字符是否与word的index+1位置上的字符匹配(下一个位置有上下左右四种可能的情况,如果这个下一个位置已经使用过,那么剪枝即可),最后撤销选择。 29 | 30 | 具体见代码注释 31 | 32 | ## 代码 33 | ```java 34 | class Solution { 35 | private char[][] board; 36 | private boolean[][] used; 37 | private String word; 38 | private boolean res = false; 39 | //代表往上下左右四个方向走 40 | private int[][] direction = {{-1, 0}, {0, -1}, {0, 1}, {1, 0}}; 41 | public boolean exist(char[][] board, String word) { 42 | this.board = board; 43 | this.word = word; 44 | used = new boolean[board.length][board[0].length]; 45 | //以board矩阵中的每一个位置为起点开始,尝试能否和word匹配 46 | for(int i = 0; i < board.length; i++){ 47 | for(int j = 0; j < board[0].length; j++){ 48 | if(backtrack(i, j, 0)) 49 | return true; 50 | } 51 | } 52 | return false; 53 | } 54 | 55 | public boolean backtrack(int i, int j, int index){ 56 | //终止条件 57 | if(board[i][j] != word.charAt(index)) 58 | return false; 59 | else if(index == word.length() - 1){ 60 | return true; 61 | } 62 | //如果该位置元素和word中index位置上的元素相等,那么做选择(使用这个位置的元素) 63 | used[i][j] = true; 64 | //递归匹配下一个位置:下一个位置有上下左右四种情况 65 | for(int[] dir : direction){ 66 | //newi和newj为下一个位置的坐标 67 | int newi = i + dir[0]; 68 | int newj = j + dir[1]; 69 | //下一个位置的坐标不能越界 70 | if(newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length){ 71 | //如果下一个位置已经使用过,就直接跳过 72 | if(used[newi][newj]) 73 | continue; 74 | if(backtrack(newi, newj, index + 1)){ 75 | return true; 76 | } 77 | } 78 | 79 | } 80 | //撤销选择 81 | used[i][j] = false; 82 | return false; 83 | } 84 | } 85 | ``` -------------------------------------------------------------------------------- /字符串/179.最大数.md: -------------------------------------------------------------------------------- 1 | # 179.最大数 2 | 3 | ## 题目 4 | 给定一组非负整数,重新排列它们的顺序使之组成一个最大的整数。 5 | 6 | 示例 1: 7 | 输入: [10,2] 8 | 输出: 210 9 | 10 | 示例 2: 11 | 输入: [3,30,34,5,9] 12 | 输出: 9534330 13 | 14 | 15 | ## 方法 16 | * 先创建一个字符串数组strs,用来保存各数字的字符串格式 17 | * 应用以下排序判断规则,对strs进行排序: 18 | * 若拼接字符串 $x+y > y+x, 则x大于y$ 19 | * 若拼接字符串 $x+y < y+x, 则x小于y$ 20 | * 例如:"330" > "303",则3大于30 21 | 22 | 注意处理"0"的情况 23 | 24 | ## 代码 25 | ```java 26 | class Solution { 27 | public static String largestNumber(int[] nums) { 28 | String res = ""; 29 | String[] strs = new String[nums.length]; 30 | for(int i = 0; i < nums.length; i++){ 31 | strs[i] = String.valueOf(nums[i]); 32 | } 33 | Arrays.sort(strs, new MyComparator()); 34 | if(strs[strs.length - 1].equals("0")) 35 | return "0"; 36 | //Arrays.sort默认是从小到大排序,因此我们从排序后的字符串数组中从后向前依次读出大数 37 | for(int i = strs.length - 1; i >= 0; i--){ 38 | res += strs[i]; 39 | } 40 | return res; 41 | 42 | } 43 | public static class MyComparator implements Comparator{ 44 | @Override 45 | public int compare(String o1, String o2) { 46 | return (o1 + o2).compareTo(o2 + o1); 47 | } 48 | } 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /字符串/205.同构字符串.md: -------------------------------------------------------------------------------- 1 | # 205.同构字符串 2 | 3 | ## 题目 4 | 给定两个字符串 s 和 t,判断它们是否是同构的。 5 | 6 | 如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。 7 | 8 | 所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。 9 | 10 | 示例 1: 11 | 输入: s = "egg", t = "add" 12 | 输出: true 13 | 14 | 示例 2: 15 | 输入: s = "foo", t = "bar" 16 | 输出: false 17 | 18 | 示例 3: 19 | 输入: s = "paper", t = "title" 20 | 输出: true 21 | 22 | 简单来说,同构的意思是结构相同。即若s和t是同构的,那么: 23 | * s如果是ABB的结构,t也必须是ABB的结构 24 | * s如果是AABB的结构,t也必须是AABB的结构 25 | * 以此类推 26 | 27 | ## 方法(用哈希表) 28 | 准备一个哈希表。用于将s中的一个字符映射到t中的一个字符。 29 | 30 | 算法步骤: 31 | * 遍历s中的每一个字符 32 | * 如果s中的一个字符还未入哈希表(之前未出现过),但t中该位置的元素已作为value处在哈希表中(之前出现过),说明不同构,返回false。否则将该字符作为key,t中同位置的字符作为value,将这对(key, value)入哈希表。 33 | * 如果s中的一个字符已经存在于哈希表中,那么就检查该字符在哈希表中的value值于t中同位置的字符是否相同,如果不同,说明s和t结构不同,直接返回false 34 | 35 | ## 代码 36 | ```java 37 | public static boolean isIsomorphic(String s, String t) { 38 | HashMap map = new HashMap<>(); 39 | char[] sChar = s.toCharArray(); 40 | char[] tChar = t.toCharArray(); 41 | for(int i = 0; i < s.length(); i++){ 42 | if(!map.containsKey(sChar[i])){ 43 | //如果s中该位置元素之前未出现过,但是t中该位置元素已出现过 44 | //说明s和t不不同构,返回false。例如"ab"和"aa" 45 | if(map.containsValue(tChar[i])) 46 | return false; 47 | map.put(sChar[i], tChar[i]); 48 | } 49 | else { 50 | if (map.get(sChar[i]) != tChar[i]) 51 | return false; 52 | } 53 | } 54 | return true; 55 | } 56 | ``` -------------------------------------------------------------------------------- /字符串/208.图1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/字符串/208.图1.jpg -------------------------------------------------------------------------------- /字符串/43. 字符串相乘.md: -------------------------------------------------------------------------------- 1 | # 43. 字符串相乘 2 | 3 | ## 题目 4 | 给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。 5 | 6 | 示例 1: 7 | 输入: num1 = "2", num2 = "3" 8 | 输出: "6" 9 | 10 | 示例 2: 11 | 输入: num1 = "123", num2 = "456" 12 | 输出: "56088" 13 | 14 | ## 方法 15 | 对于这类题目,一定要想到大数溢出的情况。如果给定字符串num1和num2特别长,其自己也许都无法转换成int甚至long类型,更不用说让他们转换后再相乘了。所以这种办法行不通,必须要从对字符串的操作中寻找办法。 16 | 17 | 我们在小学时就学过字符串相乘,现在我们来回忆一下这个过程:例如对于123*45,我们会这样操作:让123先乘5,得到一个结果,再让123乘4得到另一个结果,两个结果相加即为乘法的结果。但如果123乘5这一步,如果数很大的话,还是可能溢出。因此我们的计算过程需要再简单一点,如下所示: 18 | 19 | ![](https://github.com/wyh317/Leetcode/blob/master/%E5%AD%97%E7%AC%A6%E4%B8%B2/43.%E5%9B%BE1.png) 20 | 21 | 即:我们将num1和num2的每一个字符进行相乘,再错位进行相加得到结果。 22 | 这一用人类思维方式表示的计算过程,对于计算机来说需要这样组织: 23 | 定义两个指针i和j,分别指向num1和num2上的某一位置上的字符,计算这两个字符串的乘积,同时将乘积叠加到结果数组res上的正确位置(数组res用于在地下接收相加的结果)。 24 | 25 | 但res上哪个位置是与字符num1[i]和num2[j]的乘积有关的呢?仔细观察上述计算过程就可发现,num1[i]和num2[j]的乘积对应这数组中的i+j位置和i+j+1位置。 26 | 27 | ## 代码 28 | ```java 29 | public String multiply(String num1, String num2) { 30 | if(num1.charAt(0) == '0' || num2.charAt(0) == '0') 31 | return "0"; 32 | int len1 = num1.length(), len2 = num2.length(); 33 | //定义结果数组并初始化(结果数组最多有len1+len2位数) 34 | int[] res = new int[len1 + len2]; 35 | Arrays.fill(res, 0); 36 | //从num1和num2的个位数开始逐个位相乘 37 | for(int i = len1 - 1; i >= 0; i--){ 38 | for(int j = len2 - 1; j >= 0; j--){ 39 | //mul为相乘的结果,由于是两个个位数相乘,mul最多为二位数。它的个位和十位分别影响res的i+j+1位和i+j位 40 | int mul = (num1.charAt(i) - '0') * (num2.charAt(j) - '0'); 41 | int p1 = i + j, p2 = i + j + 1; 42 | //用mul加上res的i + j + 1位上原有的值后,更新res数组。 43 | int sum = mul + res[p2]; 44 | res[p2] = sum % 10; 45 | res[p1] += sum / 10; 46 | } 47 | } 48 | //将结果数组转化为字符串 49 | String s = ""; 50 | for(int i = 0; i < res.length; i++) 51 | s += (char)(res[i] + '0'); 52 | //结果字符串前缀可能会有0,需要去掉 53 | int count = 0; 54 | while(s.charAt(count) == '0' && count < res.length) 55 | count++; 56 | return s.substring(count, s.length()); 57 | } 58 | ``` 59 | -------------------------------------------------------------------------------- /字符串/43.图1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/字符串/43.图1.png -------------------------------------------------------------------------------- /字符串/459. 重复的字符串.md: -------------------------------------------------------------------------------- 1 | # 459. 重复的字符串 2 | 3 | ## 题目 4 | 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。 5 | 6 | 示例 1: 7 | 输入: "abab" 8 | 输出: True 9 | 解释: 可由子字符串 "ab" 重复两次构成。 10 | 11 | 示例 2: 12 | 输入: "aba" 13 | 输出: False 14 | 15 | 示例 3: 16 | 输入: "abcabcabcabc" 17 | 输出: True 18 | 解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。) 19 | 20 | ## 方法1 21 | 一个字符串若可以由它的子串重复多次构成,那么这个子串的长度只可能为1、2、3、...n/2,且字符串s的长度必须要是子串长度的倍数。我们只要分别进行如下判断即可: 22 | * 长度为1的子串能否重复多次构成字符串s 23 | * 长度为2的子串能否重复多次构成字符串s 24 | * 长度为3的子串能否重复多次构成字符串s 25 | * ....... 26 | * 长度为$n/2$的子串能否重复多次构成字符串s 27 | 28 | 对于以上每种可能的情况,我们都需要遍历整个字符串,检查是否满足重复条件$s[j] == s[j - i]$ (其中,i为子串长度)。只要以上情况有一种满足,即可返回true。 29 | 30 | ```java 31 | public boolean repeatedSubstringPattern(String s) { 32 | int n = s.length(); 33 | //i为可能的重复子串长度 34 | for(int i = 1; i <= n/2; i++){ 35 | //字符串长度必须要是子串长度的倍数 36 | if(n % i == 0){ 37 | //flag用来记录长度为i的子串能否重复构成字符串 38 | boolean flag = true; 39 | for(int j = i; j < n; j++){ 40 | if(s.charAt(j) != s.charAt(j - i)) 41 | flag = false; 42 | } 43 | if(flag) 44 | return true; 45 | } 46 | return false; 47 | } 48 | } 49 | ``` 50 | 51 | 时间复杂度:O(n^2) 52 | 空间复杂度:O(1) 53 | 54 | 55 | ## 方法2 56 | 将两个s拼接到一起形成$s'$,然后在$s'$中寻找s(易知:如果某一重复子串能构成s,那么肯定也能构成$s'$) 57 | 58 | * 如果s不能由一子串重复构成,如"aba",那么$s'$中只有两个s,在$s':“abaaba"$中查找s,第一个出现s的位置必然是s的长度n 59 | * 如果s能由一子串重复构成,如"abab",那么$s'$中将不只有两个s(此例子中有3个),在$s':“abababab"$中查找s,第一个出现s的位置肯定不是s的长度n 60 | 61 | ```java 62 | public boolean repeatedSubstringPattern(String s) { 63 | return (s + s).indexOf(s, 1) != s.length(); 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /字符串/647 回文子串.md: -------------------------------------------------------------------------------- 1 | # 647 回文子串 2 | 3 | ## 题目 4 | 给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。 5 | 6 | 具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。 7 | 8 | 示例 1: 9 | 输入:"abc" 10 | 输出:3 11 | 解释:三个回文子串: "a", "b", "c" 12 | 13 | 示例 2: 14 | 输入:"aaa" 15 | 输出:6 16 | 解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa" 17 | ## 方法1(动态规划) 18 | * dp数组定义:dp[i][j]表示字符串s在[i,j]区间的子串是否是一个回文串 19 | * 状态转移方程:当s[i] == s[j] && (j - i < 2 || dp[i + 1][j - 1])时,dp[i][j] = true,否则为false。 20 | 21 | ```java 22 | public int countSubstrings(String s) { 23 | boolean[][] dp = new boolean[s.length()][s.length()]; 24 | int res = 0; 25 | for (int j = 0; j < s.length(); j++) { 26 | for (int i = 0; i <= j; i++) { 27 | if(s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) { 28 | dp[i][j] = true; 29 | res++; 30 | } 31 | } 32 | } 33 | return res; 34 | } 35 | ``` 36 | 37 | 时间复杂度:$O(n^2)$ 38 | 空间复杂度:$O(n^2)$ 39 | 40 | 41 | 42 | ## 方法2(中心扩展法) 43 | 假设字符串的长度为N,那么回文串可能的中心有2N-1种。其中,每个单字符串都可作为回文串的中心,这种情况有N种。其次,双字符串也可作为回文串的中心,这种情况有N-1种。单字符中心负责扩展成长度为奇数的字符串,双字符串中心可以扩展成长度为偶数的字符串。例如: 44 | * 字符串“aba”有5种可能的中心:a、b、c、ab、ba 45 | * 字符串“abba”有7种可能的中心:a、b、b、a、ab、bb、ba 46 | 47 | 中心扩展法的基本思想为:对于每一个中心都计算一次以其为中心的回文串个数。 48 | 49 | 具体算法:对于每一个可能的回文中心,都尽可能地扩展它对应的回文区间[left, right],直到left<0或者right>=N或者S[left] != S[right]为止 50 | 51 | ```java 52 | public int countSubstrings(String s) { 53 | int res = 0, N = s.length(); 54 | for(int center = 0; center < 2N - 1; center++){ 55 | //center为偶数时left和right指向同一个位置,center为奇数时right指向left的后一个位置 56 | int left = center / 2; 57 | int right = left + center % 2; 58 | while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){ 59 | res++; 60 | left--; 61 | right++; 62 | } 63 | } 64 | return res; 65 | } 66 | ``` 67 | 时间复杂度:$O(n^2)$(对于每一个可能的中心都要遍历一次字符串) 68 | 空间复杂度:O(1) 69 | 70 | 对于本题的中心扩展法稍加修改,可以解决[5.最长回文子串](https://github.com/wyh317/Leetcode/blob/master/%E5%AD%97%E7%AC%A6%E4%B8%B2/5.%20%E6%9C%80%E9%95%BF%E5%9B%9E%E6%96%87%E5%AD%90%E4%B8%B2.md) 71 | 72 | -------------------------------------------------------------------------------- /数组/1.两数之和.md: -------------------------------------------------------------------------------- 1 | # 1. 两数之和 2 | 3 | ## 题目 4 | 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 5 | 6 | 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 7 | 8 | 示例: 9 | 给定 nums = [2, 7, 11, 15], target = 9 10 | 因为 nums[0] + nums[1] = 2 + 7 = 9 11 | 所以返回 [0, 1] 12 | 13 | 14 | ## 方法1(暴力法) 15 | 从头到尾遍历数组。对于每遍历到的一个元素num,在剩下的数组中查找是否存在target-num。如果存在则返回,否则继续遍历。 16 | ```java 17 | public int[] twoSum(int[] nums, int target) { 18 | for(int i = 0; i < nums.length; i++){ 19 | for(int j = i + 1; j < nums.length; j++){ 20 | if(nums[i] + nums[j] == target) 21 | return new int[]{i, j}; 22 | } 23 | } 24 | return new int[0]; 25 | } 26 | ``` 27 | 28 | 时间复杂度:O(n^2) 29 | 空间复杂度:O(1) 30 | 31 | ## 方法2(用哈希表,两次遍历数组) 32 | 33 | 我们解这道题的核心思路即:对于数组中一个给定元素num,查找数组中是否存在另一个元素target-num。 34 | 35 | 上一个方法之所以复杂度高,是由于在一个数组中查找一个元素需要O(n)的时间复杂度。如果查找这一步能优化,那么整个算法就可以优化。于是我们很容易想到能用O(1)的时间复杂度实现最快查找的数据结构:哈希表 36 | 37 | 算法步骤: 38 | 1. 先遍历一次数组,将数组中所有元素加到哈希表中,key为元素的值,value为元素的索引 39 | 2. 再从头到尾遍历数组。对于每遍历到的一个元素num,在哈希表中查找是否存在target-num。 40 | 41 | ```java 42 | public int[] twoSum(int[] nums, int target) { 43 | Map map = new HashMap<>(); 44 | for(int i = 0; i < nums.length; i++) 45 | map.put(nums[i], i); 46 | for(int i = 0 ; i < nums.length; i++){ 47 | if(map.containsKey(target - nums[i]) && map.get(target - nums[i]) != i) 48 | return new int[]{i, map.get(target - nums[i])}; 49 | } 50 | return new int[2]; 51 | } 52 | ``` 53 | 时间复杂度:O(n) 54 | 空间复杂度:O(n) 55 | 56 | ## 方法3(用哈希表,一次遍历数组) 57 | 其实,我们只需遍历一遍数组就能把问题解决。从头到尾遍历数组。对于每遍历到的一个元素num,如果target-num在哈希表中,则返回。否则将当前元素加到哈希表中。 58 | 59 | ```java 60 | public int[] twoSum(int[] nums, int target) { 61 | Map map = new HashMap<>(); 62 | for(int i = 0; i < nums.length; i++){ 63 | if(map.containsKey(target - nums[i])) 64 | return new int[]{map.get(target - nums[i]), i}; 65 | else 66 | map.put(nums[i], i); 67 | } 68 | return new int[2]; 69 | } 70 | ``` 71 | 72 | 时间复杂度:O(n) 73 | 空间复杂度:O(n) 74 | -------------------------------------------------------------------------------- /数组/121.买卖股票的最佳时机.md: -------------------------------------------------------------------------------- 1 | # 121 买卖股票的最佳时机 2 | **题目:** 3 | (只能进行一次交易) 4 | 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 5 | 如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。 6 | 注意:你不能在买入股票前卖出股票。 7 | 8 |   9 | 10 | 示例 1: 11 | 输入: [7,1,5,3,6,4] 12 | 输出: 5 13 | 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 14 | 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 15 | 16 | 示例 2: 17 | 输入: [7,6,4,3,1] 18 | 输出: 0 19 | 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 20 | 21 | 22 | 23 | 24 | 25 | **思路:** 26 | 用一个变量记录最低的股票价格,在遍历的过程中更新它。 27 | 最大的利润,一定为,某天的股票价格与之前出现过的最低股票价格之差。遍历一次数组即可得到结果。时间复杂度O(n) 28 | 即:我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案。 29 | 30 | **代码:** 31 | ```Java 32 | public int maxProfit(int[] prices) { 33 | if(prices.length == 0) 34 | return 0; 35 | int min_price = prices[0]; 36 | int max_profit = 0; 37 | for(int i = 0; i < prices.length; i++){ 38 | min_price = Math.min(min_price, prices[i]); 39 | max_profit = Math.max(prices[i] - min_price,max_profit); 40 | } 41 | return max_profit; 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /数组/15.三数之和.md: -------------------------------------------------------------------------------- 1 | # 15. 三数之和 2 | 3 | ## 题目 4 | 5 | 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。 6 | 7 | 注意:答案中不可以包含重复的三元组。 8 | 9 |   10 | 11 | 示例: 12 | 给定数组 nums = [-1, 0, 1, 2, -1, -4], 13 | 满足要求的三元组集合为: 14 | [ 15 | [-1, 0, 1], 16 | [-1, -1, 2] 17 | ] 18 | 19 | 20 | ## 方法 21 | 注意题目中要求答案中不可以包含重复的三元组,所以时间复杂度为O(n^3)的暴力解法是行不通的。之前我们曾用时间复杂度O(n)和空间复杂度O(n)解决了两数之和问题。于是我们可以从头遍历数组,对于每遍历到的一个元素num,都在数组中查找是否有两数之和为-num的另外两个元素,这样这3个元素的和就为0了。 22 | 23 | 按照这种思路的时间复杂度为O(n^2),因为这个时间复杂度大于排序所需的O(nlogn)。所以我们可以先把数组进行排序,以方便后续的操作。排序有着以下两个好处: 24 | 25 | 1. 对于两数之和的问题,我们可以不用额外的辅助空间(哈希表)。由于数组的有序性,我们可以用双指针的思路来解决。 26 | 2. 如果nums[i] == nums[i-1],则可以直接跳过对于i处的遍历,以避免结果中存在重复。 27 | 28 | 算法步骤: 29 | * 先将数组排序,之后从头到尾遍历数组,当遍历到数组nums[i]时,用L和R左右两个指针指向在nums[i]后面的数组两端。计算三个数的和sum,判断是否满足为 0,满足则添加进结果集。 30 | * 如果 nums[i]大于0,则它后面的数组元素必然都大于0,三数之和必然无法等于0,结束循环. 31 | * 如果 nums[i] == nums[i-1],则说明该数字重复,会导致结果重复,所以应该跳过 32 | * 当 sum == 0时,nums[L] == nums[L+1],则会导致结果重复,应该跳过,L++。 33 | * 当 sum == 0时,nums[R] == nums[R-1],则会导致结果重复,应该跳过,R--。 34 | 35 | ## 代码 36 | 37 | ```java 38 | public static List> threeSum(int[] nums) { 39 | List> res = new ArrayList(); 40 | if(nums == null || nums.length < 3) 41 | return res; 42 | Arrays.sort(nums); 43 | for(int i = 0; i < nums.length; i++) { 44 | if(nums[i] > 0) 45 | break; 46 | if(i > 0 && nums[i] == nums[i-1]) 47 | continue; 48 | int L = i+1; 49 | int R = nums.length-1; 50 | while(L < R){ 51 | int sum = nums[i] + nums[L] + nums[R]; 52 | if(sum == 0){ 53 | res.add(Arrays.asList(nums[i],nums[L],nums[R])); 54 | while (L 0) 64 | R--; 65 | } 66 | } 67 | return res; 68 | } 69 | 70 | ``` 71 | -------------------------------------------------------------------------------- /数组/16. 最接近的三数之和.md: -------------------------------------------------------------------------------- 1 | # 16. 最接近的三数之和 2 | 3 | ## 题目 4 | 给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。 5 | 6 | 示例: 7 | 8 | 输入:nums = [-1,2,1,-4], target = 1 9 | 输出:2 10 | 解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。 11 | 12 | ## 方法 13 | 本题与[15.三数之和](https://github.com/wyh317/Leetcode/blob/master/%E6%95%B0%E7%BB%84/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.md) 非常相似。 14 | 15 | 可以使用暴力方法计算出数组中任意三个数的和,最后取离target最近的和即可。但这种方法的时间复杂度为$O(n^3)$。 16 | 17 | 我们采用和[15.三数之和](https://github.com/wyh317/Leetcode/blob/master/%E6%95%B0%E7%BB%84/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.md)相同的思路,先固定住第一个数,再通过双指针法找后两个数。 18 | 19 | 具体算法流程如下: 20 | * 先对数组排序,时间复杂度为O(nlogn) 21 | * 从头到尾遍历数组,当遍历到数组中元素nums[i]时,用双指针L和R指向在nums[i]后面的数组两端,即L = i+1, R = nums.length - 1 22 | * 如果 nums[i] == nums[i-1],则说明该数字重复,重复计算没有意义,所以应该跳过 23 | * 计算三个指针所指元素的和sum,判断sum与目标target的距离,如果二者距离更近则更新结果res。 24 | * 双指针移动:因为数组有序,因此如果sum < target,则L右移,如果sum > target,则R左移。否则sum=target,直接返回target。 25 | 26 | ## 代码 27 | ```java 28 | public int threeSumClosest(int[] nums, int target) { 29 | int minDif = Integer.MAX_VALUE; 30 | int res = nums[0] + nums[1] + nums[nums.length - 1]; 31 | Arrays.sort(nums); 32 | for(int i = 0; i < nums.length; i++){ 33 | if(i >= 1 && nums[i] == nums[i - 1]) 34 | continue; 35 | int L = i + 1; 36 | int R = nums.length - 1; 37 | while(L < R){ 38 | int sum = nums[i] + nums[L] + nums[R]; 39 | if(Math.abs(target - sum) < Math.abs(target - res)) 40 | res = sum; 41 | if(sum < target) 42 | L++; 43 | else if(sum > target) 44 | R--; 45 | else 46 | return target; 47 | } 48 | } 49 | return res; 50 | } 51 | ``` 52 | 53 | 时间复杂度:O(n^2) (数组中每一个元素都对应着一个双指针操作) 54 | 空间复杂度:O(1) 55 | -------------------------------------------------------------------------------- /数组/167.两数之和 II - 输入有序数组.md: -------------------------------------------------------------------------------- 1 | # 167. 两数之和 II - 输入有序数组 2 | 3 | **题目:** 4 | 给定一个已按照升序排列的有序数组,找到两个数使得它们相加之和等于目标数。 5 | 函数应该返回这两个下标值 index1 和 index2,其中 index1必须小于index2。 6 | 7 | 示例: 8 | 输入: numbers = [2, 7, 11, 15], target = 9 9 | 输出: [1,2] 10 | 解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。 11 | 12 | 13 | **思路:双指针法** 14 | 定义两个指针left和right,初始化时分别指向数组的两端 15 | 如果left和right对应的值加起来等于target,那么可直接返回。因为数组是排序好的,如果和小于target,也即希望和大一些,所以将left右移一位。如果和大于target,也即希望和小一些,所以将right左移一位。如此进行循环直到找到相等的时候,若找不到则返回null 16 | 17 | **代码** 18 | ```java 19 | public int[] twoSum(int[] numbers, int target) { 20 | if(numbers == null) 21 | return null; 22 | int left = 0; 23 | int right = numbers.length - 1; 24 | while(left < right){ 25 | if(numbers[left] + numbers[right] == target) 26 | return new int[]{left+1, right+1}; 27 | else if(numbers[left] + numbers[right] < target) 28 | left++; 29 | else 30 | right--; 31 | } 32 | return null; 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /数组/18.四数之和.md: -------------------------------------------------------------------------------- 1 | # 18. 四数之和 2 | 3 | ## 题目 4 | 5 | 给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。 6 | 7 | 注意: 8 | 9 | 答案中不可以包含重复的四元组。 10 | 11 | 示例: 12 | 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 13 | 满足要求的四元组集合为: 14 | [ 15 | [-1, 0, 0, 1], 16 | [-2, -1, 1, 2], 17 | [-2, 0, 0, 2] 18 | ] 19 | 20 | ## 方法 21 | 思路类似[15.三数之和](https://github.com/wyh317/Leetcode/blob/master/%E6%95%B0%E7%BB%84/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.md) 22 | * 第一层循环固定第一个数,然后去遍历其他三个数 23 | * 第二层循环固定第二个数,然后去遍历最后两个数 24 | * 通过双指针法找出最后两个数 25 | 26 | ```java 27 | public List> fourSum(int[] nums,int target){ 28 | List> res = new ArrayList<>(); 29 | if(nums==null||nums.length<4){ 30 | return res; 31 | } 32 | Arrays.sort(nums); 33 | //定义4个指针i,j,L,R 34 | //i从0开始遍历,用于固定第一个数。j从i+1开始遍历,用于固定第二个数。 35 | //L和R作为双指针,L指向j+1,R指向数组最右端,用于找出最后两个数 36 | for(int i = 0; i < nums.length-3; i++){ 37 | if(i > 0 && nums[i] == nums[i-1]){ 38 | continue; 39 | } 40 | //计算当前前四个元素的最小值,因为已经排好序,如果最小值比目标值大,说明不会找到符合条件的四个数 41 | int min1 = nums[i] + nums[i+1] + nums[i+2] + nums[i+3]; 42 | if(min1 > target){ 43 | break; 44 | } 45 | //计算当前最后四个元素的最大值,因为已经排好序,如果最大值比目标值小,说明以目前i的位置不会找到符合条件的四个数,忽略 46 | int max1 = nums[i] + nums[nums.length-1] + nums[nums.length-2] + nums[nums.length-3]; 47 | if(max1 < target){ 48 | continue; 49 | } 50 | //第二重循环用和第一重循环类似的逻辑固定第二个数的同时,用双指针去找最后两个数 51 | for(int j = i + 1; j < nums.length-2; j++){ 52 | if(j > i + 1 && nums[j] == nums[j-1]){ 53 | continue; 54 | } 55 | //定义双指针 56 | int L = j + 1; 57 | int R = nums.length - 1; 58 | int min = nums[i] + nums[j] + nums[L] + nums[L+1]; 59 | if(min > target){ 60 | continue; 61 | } 62 | int max = nums[i] + nums[j] + nums[R] + nums[R-1]; 63 | if(max < target){ 64 | continue; 65 | } 66 | //双指针法找剩下的两个数 67 | while (L < R){ 68 | int sum = nums[i] + nums[j] + nums[L] + nums[R]; 69 | if(sum == target){ 70 | res.add(Arrays.asList(nums[i], nums[j], nums[L], nums[R])); 71 | while(L < R && nums[L] == nums[L+1]){ 72 | L++; 73 | } 74 | while(L < R && j < R && nums[R]==nums[R-1]){ 75 | R--; 76 | } 77 | L++; 78 | R--; 79 | } 80 | else if(sum < target) 81 | L++; 82 | else 83 | R--; 84 | } 85 | } 86 | } 87 | return res; 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /数组/268.缺失数字.md: -------------------------------------------------------------------------------- 1 | # 268 缺失数字 2 | **题目:** 3 | 给定一个包含 0, 1, 2, ..., n 中 n 个数的序列,找出 0 .. n 中没有出现在序列中的那个数。 4 | 5 | 示例 1: 6 | 输入: [3,0,1] 7 | 输出: 2 8 | 9 | 示例 2: 10 | 输入: [9,6,4,2,3,5,7,0,1] 11 | 输出: 8 12 | 13 | 14 | ## 方法一:排序法 15 | **思路:** 先将数组排序,然后判断排序后的数组数字是否挨着,即若两个相邻数字之间相差2,则这两个数字间的那个数字即为缺失数字 16 | 17 | **代码:** 18 | ```java 19 | public int missingNumber(int[] nums) { 20 | Arrays.sort(nums); 21 | // 判断 n 是否出现在末位 22 | if (nums[nums.length-1] != nums.length) { 23 | return nums.length; 24 | } 25 | // 判断 0 是否出现在首位 26 | else if (nums[0] != 0) { 27 | return 0; 28 | } 29 | // 此时缺失的数字一定在 (0, n) 中 30 | for (int i = 1; i < nums.length; i++) { 31 | int expectedNum = nums[i-1] + 1; 32 | if (nums[i] != expectedNum) { 33 | return expectedNum; 34 | } 35 | } 36 | // 未缺失任何数字(保证函数有返回值) 37 | return -1; 38 | } 39 | ``` 40 | **复杂度:** 41 | 时间复杂度:O(nlogn),排序的复杂度 42 | 空间复杂度:O(1),实际上跟采用的排序算法有关 43 | ## 方法二:哈希法 44 | **思路:** 45 | 先将nums数组中所有数放进哈希表,然后寻找从0到n的哪一个数不在哈希表中,返回这个数字 46 | 47 | **代码:** 48 | ```java 49 | public int missingNumber(int[] nums) { 50 | //新建一个HashSet,将nums中所有元素加入HashSet 51 | Set set = new HashSet<>(); 52 | for(int num: nums) 53 | set.add(num); 54 | //判断从0到nums.length范围内数是否在set中,如果有数不在,返回这个数 55 | for(int i = 0; i <= nums.length;i++) 56 | if(!set.contains(i)) 57 | return i; 58 | return -1; 59 | } 60 | ``` 61 | **时间复杂度**:O(n)。HashSet的插入和查询操作都是O(1),对每一个数都进行这个操作,总时间复杂度为O(n) 62 | **空间复杂度**:O(n)。需要HashSet存储这些数 63 | 64 | ## 方法三:数学方法 65 | **思路:** 66 | 用高斯求和公式求出从1到n的和,即: 67 | 68 | ```math 69 | \sum^{n}_{i = 0}\frac{n(n-1)}{2} 70 | ``` 71 | 然后再将nums中的元素求和,二者相减即为缺失的元素 72 | 73 | **代码:** 74 | ```java 75 | public int missingNumber(int[] nums) { 76 | int expectedSum = nums.length*(nums.length + 1)/2; 77 | int sum = 0; 78 | for (int num : nums) 79 | sum += num; 80 | return expectedSum - sum; 81 | } 82 | ``` 83 | 84 | **时间复杂度:** 85 | 高斯求和的复杂度为O(1),数组元素求和的复杂度为O(n)。总时间复杂度为O(n) 86 | **空间复杂度:** O(1) 87 | 88 | ## 方法四:位运算 89 | **思路:** 90 | 如果数组中没有丢失数字的话,比如[0,1,2]。其中每个数字0,1,2作为数组索引中出现一次,作为值中又出现一次,总共出现两次 91 | 92 | 93 | 一旦丢失了数字,比如[0,1,3]。0,1这两个数字出现两次(作为索引一次,作为值一次),而3这个数字只作为值出现一次。 94 | 95 | 而我们知道,一个数字自己异或自己得到的结果为0。因此如果我们异或数组nums的所有索引和所有值。最终0,1会自己异或自己进而为0。最终异或结果其实是3和2的异或值。其中,3为数组nums的长度,2为缺失的数字 96 | 97 | 而数组nums的长度我们是知道的,上述得到的结果再异或一下它,得到的2即为丢失的数字 98 | 99 | **代码:** 100 | ```java 101 | public int missingNumber(int[] nums) { 102 | int res = 0; 103 | for(int i = 0; i < nums.length; i++) 104 | res = res ^ i ^ nums[i]; 105 | return res ^ nums.length; 106 | } 107 | ``` 108 | 109 | * 时间复杂度:O(N) 110 | * 时间复杂度:O(1) 111 | -------------------------------------------------------------------------------- /数组/307.线段树.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/数组/307.线段树.png -------------------------------------------------------------------------------- /数组/31.下一个排列.md: -------------------------------------------------------------------------------- 1 | # 31.下一个排列 2 | 3 | ## 题目 4 | 实现获取下一个排列的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。 5 | 6 | 如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。 7 | 8 | 必须原地修改,只允许使用额外常数空间。 9 | 10 | 以下是一些例子,输入位于左侧列,其相应输出位于右侧列。 11 | 1,2,3 → 1,3,2 12 | 3,2,1 → 1,2,3 13 | 1,1,5 → 1,5,1 14 | 15 | ## 方法 16 | 当给定排列已经是最大排列时,这种情况单独考虑,只需反转数组即可。 17 | 18 | 否则,下一个排列肯定比当前排列要大,而且要尽可能让变大的幅度很小。 19 | 20 | 步骤如下: 21 | 1. 从后向前遍历数组,**找到第一个升序位置对** (index,index+1),满足 a[index] < a[index+1]。在index之后的所有位置i,都符合降序。意味着nums[index...]为以index开头的最大排列。既然我们要找一个更大的排列,意味着不能再以index开头,我们要在index位置后面找到一个最小比它大的数代替它。 22 | 2. 于是我们**在index位置右边从后向前寻找第一个比nums[index]大的元素位置j**。因为nums[index+1....]为降序排列,因此第一个出现的比nums[index]大的元素就是最小比nums[index]大的元素。 23 | 3. 之前nums[index...]为以nums[index]开头的最大排列,现在我们要让nums[index...]变为以nums[j]开头的最小排列,于是**将index位置和j位置交换**,即让一个最小的比nums[index]大的元素nums[j]代替它。 24 | 4. 再**反转nums[index+1.....],让这段区间由降序变成升序**。此刻index位置右边为以nums[j]元素开头的最小排列。 25 | 26 | ```java 27 | public void nextPermutation(int[] nums) { 28 | int index = nums.length - 2; 29 | //1.寻找第一个升序位置对 30 | while(index >= 0 && nums[index] >= nums[index + 1]) 31 | index--; 32 | //如果没找到一个升序位置对,则整个数组为降序,即最大排列,这时将其反转得到最小排列 33 | if(index < 0) 34 | reverse(nums, 0, nums.length - 1); 35 | //否则,(index,index+1)为找到的第一个升序位置对 36 | else{ 37 | //2.在index右侧寻找最小的比nums[index]大的位置j 38 | int j = nums.length - 1; 39 | while (j >= 0 && nums[index] >= nums[j]) 40 | j--; 41 | //3.将位置i和位置index上的元素交换 42 | swap(nums, index, j); 43 | //4.反转index右边,使得其由降序变为升序 44 | reverse(nums, index + 1, nums.length - 1); 45 | } 46 | } 47 | 48 | public void swap(int[] nums, int a, int b){ 49 | int temp = nums[a]; 50 | nums[a] = nums[b]; 51 | nums[b] = temp; 52 | } 53 | //将数组arr逆序 54 | public void reverse(int[] nums, int start, int end){ 55 | if(end - start < 1) 56 | return; 57 | int left = start; 58 | int right = end; 59 | while(left < right){ 60 | swap(nums, left++, right--); 61 | } 62 | } 63 | ``` 64 | * 时间复杂度:O(N) 65 | * 空间复杂度:O(1) -------------------------------------------------------------------------------- /数组/349.两个数组的交集.md: -------------------------------------------------------------------------------- 1 | # 349 两个数组的交集 2 | **题目:** 3 | 给定两个数组,编写一个函数来计算它们的交集。 4 | 5 | 示例 1: 6 | 输入: nums1 = [1,2,2,1], nums2 = [2,2] 7 | 输出: [2] 8 | 9 | 示例 2: 10 | 输入: nums1 = [4,9,5], nums2 = [9,4,9,8,4] 11 | 输出: [9,4] 12 | 13 | 14 | 15 | 16 | **思路:** 17 | 由于检查一个元素是否在set中的时间复杂度为O(1),而且set中不包含重复元素。因此考虑用Hashset数据结构来解决。 18 | 准备两个set,将nums1中元素加进set1中,将nums2中元素加进set2中。遍历set1中的元素,若其也在set2中,则将此元素加入到结果数组中。 19 | 20 | 21 | **代码:** 22 | ```java 23 | public int[] intersection(int[] nums1, int[] nums2) { 24 | if(nums1 == null || nums2 == null) 25 | return null; 26 | Set set1 = new HashSet(); 27 | Set set2 = new HashSet(); 28 | for(int nums: nums1) 29 | set1.add(nums); 30 | for(int nums:nums2) 31 | set2.add(nums); 32 | List list = new ArrayList(); 33 | for(int num:set1){ 34 | if(set2.contains(num)) 35 | list.add(num); 36 | } 37 | int[] res = new int[list.size()]; 38 | for(int i = 0; i < list.size();i++) 39 | res[i] = list.get(i); 40 | return res; 41 | } 42 | } 43 | ``` 44 | 45 | list数据结构在定义时不需要声明大小。上述方法先将结果都加入到list中,再建立一个和list大小相同的数组,将list中的元素都加入到这个数组中,并返回。 46 | 注:list的添加操作为add,提取操作为get 47 | 48 | 也可以不用list,直接用数组来解决。 49 | ```java 50 | int [] res = new int[Math.max(set1.size(),set2.size())]; 51 | int idx = 0; 52 | for (int num : set1){ 53 | if (set2.contains(num)) 54 | res[idx++] = num; 55 | return Arrays.copyOf(res, idx); 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /数组/384.打乱数组.md: -------------------------------------------------------------------------------- 1 | # 384 打乱数组 2 | **题目:** 3 | 打乱一个没有重复元素的数组。 4 | 5 |   6 | 7 | 示例: 8 | 9 | // 以数字集合 1, 2 和 3 初始化数组。 10 | int[] nums = {1,2,3}; 11 | Solution solution = new Solution(nums); 12 | 13 | // 打乱数组 [1,2,3] 并返回结果。任何 [1,2,3]的排列返回的概率应该相同。 14 | solution.shuffle(); 15 | 16 | // 重设数组到它的初始状态[1,2,3]。 17 | solution.reset(); 18 | 19 | // 随机返回数组[1,2,3]打乱后的结果。 20 | solution.shuffle(); 21 | 22 | 23 | 24 | 25 | **思路:** 26 | 我们可以用一个简单的技巧来降低暴力算法的时间复杂度和空间复杂度,那就是让数组中的元素互相交换,这样就可以避免掉每次迭代中用于修改列表的时间了。 27 | 28 | 算法: 29 | Fisher-Yates 洗牌算法跟暴力算法很像。在每次迭代中,生成一个范围在当前下标到数组末尾元素下标之间的随机整数。接下来,将当前元素和随机选出的下标所指的元素互相交换 - 这一步模拟了每次从 “帽子” 里面摸一个元素的过程,其中选取下标范围的依据在于每个被摸出的元素都不可能再被摸出来了。此外还有一个需要注意的细节,当前元素是可以和它本身互相交换的 - 否则生成最后的排列组合的概率就不对了。 30 | 31 | **代码:** 32 | ```Java 33 | class Solution { 34 | private int[] array; 35 | private int[] original; 36 | Random rand = new Random(); 37 | //将原始数组赋给array,并将原始数组拷贝一份保存至original 38 | public Solution(int[] nums) { 39 | this.array = nums; 40 | this.original = nums.clone(); 41 | } 42 | 43 | /** Resets the array to its original configuration and return it. */ 44 | public int[] reset() { 45 | return original; 46 | } 47 | 48 | /** Returns a random shuffling of the array. */ 49 | public int[] shuffle() { 50 | for(int i = 0; i< array.length; i++) 51 | //生成一个范围在当前下标到数组末尾元素下标之间的随机整数 52 | //并将当前元素和随机选出的下标所指的元素互相交换 53 | swapAt(i, randRange(i ,array.length)); 54 | return array; 55 | } 56 | //此函数实现交换array数组的i处和j处的值 57 | private void swapAt(int i , int j){ 58 | int temp = array[i]; 59 | array[i] = array[j]; 60 | array[j] = temp; 61 | } 62 | //生成一个范围在min和max之间的随机整数 63 | private int randRange(int min, int max){ 64 | return rand.nextInt(max - min) + min; 65 | } 66 | } 67 | ``` 68 | 69 | 复杂度分析 70 | 时间复杂度 : O(n) 71 | Fisher-Yates 洗牌算法时间复杂度是线性的,因为算法中生成随机序列,交换两个元素这两种操作都是常数时间复杂度的。 72 | 空间复杂度: O(n) 73 | 因为要实现 重置 功能,原始数组必须得保存一份,因此空间复杂度并没有优化。 74 | -------------------------------------------------------------------------------- /数组/493. 翻转对.md: -------------------------------------------------------------------------------- 1 | # 493. 翻转对 2 | 3 | ## 题目 4 | 给定一个数组 nums ,如果 i < j 且 nums[i] > 2*nums[j] 我们就将 (i, j) 称作一个重要翻转对。 5 | 6 | 你需要返回给定数组中的重要翻转对的数量。 7 | 8 | 示例 1: 9 | 输入: [1,3,2,3,1] 10 | 输出: 2 11 | 12 | 示例 2: 13 | 输入: [2,4,3,5,1] 14 | 输出: 3 15 | 16 | ## 方法(归并排序) 17 | 在一个数组中寻找元素对这样的情况都可以考虑归并排序的思路。 18 | 19 | 如果我们已经将数组的左右两部分nums[L......mid]、nums[mid+1......R]排好序。则对于右半部分的位置j,如果在左边有一个位置i满足nums[i] > 2*nums[j],那么从i到mid中的所有位置都可以和j组成翻转对(因为左半部分有序,位置i右边的元素肯定大于位置i处的元素)。 20 | 21 | 当找完了j位置的所有翻转对之后,接下来找j+1位置的翻转对。由于nums[j+1]大于nums[j],因此i不用回退,一直向右移动判断即可。知道找出右半部分所有位置对应的翻转对数量。 22 | 23 | 总结一下,所有的翻转对由以下三部分构成: 24 | 1. i和j都在nums的左半部分 25 | 2. i和j都在nums的右半部分 26 | 3. i在nums的左半部分,j在nums的右半部分。 27 | 28 | 上面我们讨论的是第三部分,第一部分和第二部分的翻转对递归求得即可。 29 | 30 | 也就是说我们在归并排序的同时,在merge操作的过程中顺带统计出了翻转对的数目。 31 | 32 | ## 代码 33 | ```java 34 | public int reversePairs(int[] nums) { 35 | if(nums == null || nums.length == 0) 36 | return 0; 37 | return mergeSort(nums, 0, nums.length - 1); 38 | } 39 | 40 | public int mergeSort(int[] nums, int left, int right){ 41 | if(right == left) 42 | return 0; 43 | int mid = left + (right - left) / 2; 44 | //数组左半段内部产生的翻转对 45 | int count1 = mergeSort(nums, left, mid); 46 | //数组右半段内部产生的翻转对 47 | int count2 = mergeSort(nums, mid + 1, right); 48 | //翻转对的两端i和j分别在左半段和右半段时,产生的翻转对。 49 | int count3 = merge(nums, left, mid, right); 50 | return count1 + count2 + count3; 51 | } 52 | 53 | public int merge(int[] nums, int left, int mid, int right){ 54 | int res = 0; 55 | int curL = left; 56 | for(int i = mid + 1; i <= right; i++){ 57 | while(curL <= mid && (long)nums[curL] <= (long)2 * nums[i]) 58 | curL++; 59 | res += mid - curL + 1; 60 | } 61 | int[] help = new int[right - left + 1]; 62 | int index = 0; 63 | int L = left; 64 | int R = mid + 1; 65 | while(L <= mid && R <= right){ 66 | help[index++] = nums[L] < nums[R] ? nums[L++] : nums[R++]; 67 | } 68 | while(L <= mid){ 69 | help[index++] = nums[L++]; 70 | } 71 | while(R <= right){ 72 | help[index++] = nums[R++]; 73 | } 74 | for(int i = 0; i < help.length; i++){ 75 | nums[i + left] = help[i]; 76 | } 77 | return res; 78 | } 79 | ``` -------------------------------------------------------------------------------- /数组/55.跳跃游戏.md: -------------------------------------------------------------------------------- 1 | # 55 跳跃游戏 2 | **题目:** 3 | 给定一个非负整数数组,你最初位于数组的第一个位置。 4 | 数组中的每个元素代表你在该位置可以跳跃的最大长度。 5 | 判断你是否能够到达最后一个位置。 6 | 7 | 示例 1: 8 | 输入: [2,3,1,1,4] 9 | 输出: true 10 | 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 11 | 12 | 示例 2: 13 | 输入: [3,2,1,0,4] 14 | 输出: false 15 | 解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。 16 | 17 | 18 | 19 | 20 | **思路:** 21 | 维护一个最远距离(数组位置的索引),最开始初始化最远距离为0,然后遍历数组,如果以当前位置开始能跳到的位置大于最远位置,即数组当前位置值+当前位置索引>最远位置,则更新最远位置。最后判断这个最远位置和数组长度的大小即可。 22 | 23 | **简单来说**,把每一个能够起跳的地方都跳一次,不断更新能跳到的最远距离,看能不能跳到最后 24 | 25 | **代码:** 26 | ```Java 27 | public boolean canJump(int[] nums) { 28 | int longest_index = 0; 29 | for(int i = 0; i < nums.length;i++){ 30 | if(i > longest_index) 31 | break; 32 | //更新最长距离 33 | longest_index = Math.max(longest_index, i + nums[i]); 34 | } 35 | return longest_index >= nums.length - 1; 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /数组/56.合并区间.md: -------------------------------------------------------------------------------- 1 | # 56 合并区间 2 | 3 | **题目:** 4 | 给出一个区间的集合,请合并所有重叠的区间。 5 | 6 | 示例 1: 7 | 8 | 输入: [[1,3],[2,6],[8,10],[15,18]] 9 | 输出: [[1,6],[8,10],[15,18]] 10 | 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. 11 | 12 | 示例 2: 13 | 输入: [[1,4],[4,5]] 14 | 输出: [[1,5]] 15 | 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。 16 | 17 | **思路:** 18 | 先将数组intervals按照第一个元素的大小从小到大排序。之后遍历数组intervas中的每一个区间。 19 | * 如果该区间的左端点大于res中最后一个区间的右端点,则不重合、不需要合并,直接将这个区间加入到res的末尾。 20 | * 否则,需要合并,即用该区间的右端点更新res中最后一个区间的右端点,取二者的最大值。 21 | 22 | **代码:** 23 | 24 | ```java 25 | public int[][] merge(int[][] intervals) { 26 | if(intervals == null || intervals.length == 0) 27 | return new int[0][0]; 28 | if(intervals.length == 1) 29 | return intervals; 30 | //初始化结果数组,大小为intervals的长度 31 | int[][] res = new int[intervals.length][2]; 32 | //将数组intervals按照第一个元素的大小进行升序排序 33 | Arrays.sort(intervals, (v1, v2) -> v1[0] - v2[0]); 34 | //先将第一个区间加入结果集 35 | int index = 0; 36 | res[index++] = intervals[0]; 37 | //遍历数组中的每一个区间 38 | for(int i = 1; i < intervals.length; i++){ 39 | if(intervals[i][0] > res[index - 1][1]){ 40 | res[index++] = intervals[i]; 41 | } 42 | else{ 43 | res[index - 1][1] = Math.max(res[index - 1][1], intervals[i][1]); 44 | } 45 | } 46 | //由于res初始化时长度为intervals.length 47 | //因此最后只需要返回它的前index+1个元素的拷贝 48 | return Arrays.copyOf(res, index); 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /数组/560.和为K的子数组.md: -------------------------------------------------------------------------------- 1 | # 560.和为K的子数组 2 | 3 | ## 题目 4 | 给定一个整数数组和一个整数 k,你需要找到该数组中和为 k 的连续的子数组的个数。 5 | 6 | 示例 1 : 7 | 输入:nums = [1,1,1], k = 2 8 | 输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。 9 | 10 | 11 | ## 方法 12 | 一种暴力方法为每次固定一个左边界,在其右边不断尝试可能的右边界,统计有多少个由这样的左右边界构成的子数组使得其中的元素和为K。这种方法时间复杂度为O(N^2) 13 | 14 | 改进解法: 15 | * 先遍历一次数组,计算数组中每一个元素对应的前缀和(即在数组中该元素以及它前面的所有元素的和为多少),构建前缀和数组。 16 | * 为了避免二次遍历,我们构建一个哈希表,其key为前缀和,value为该前缀和出现的次数。 17 | * 这种方法的核心思想是:要找到有多少符合preSum[i] - preSum[j] = K的情况。i为子数组右边界,j为子数组左边界。也就是说我们在遍历到位置i时,想知道在之前有多少个位置j,符合preSum[j] = preSum[i] - K。即前缀和preSum[j]出现了多少次,而这恰好可以直接从哈希表中得到。 18 | * 于是我们相当于用了O(N)的空间复杂度,使算法的时间复杂度降为O(N) 19 | ```java 20 | public static int subarraySum(int[] nums, int k) { 21 | Map map = new HashMap<>(); 22 | int[] preSum = new int[nums.length + 1]; 23 | int res = 0; 24 | preSum[0] = 0; 25 | map.put(0,1); 26 | for(int i = 1; i <= nums.length; i++){ 27 | preSum[i] = preSum[i - 1] + nums[i - 1]; 28 | res += map.containsKey(preSum[i] - k) ? map.get(preSum[i] - k) : 0; 29 | if(!map.containsKey(preSum[i])) 30 | map.put(preSum[i], 1); 31 | else 32 | map.put(preSum[i], map.get(preSum[i]) + 1); 33 | } 34 | return res; 35 | } 36 | 37 | ``` -------------------------------------------------------------------------------- /数组/57.插入区间.md: -------------------------------------------------------------------------------- 1 | # 57.插入区间 2 | 3 | 4 | ## 题目 5 | 给出一个无重叠的 ,按照区间起始端点排序的区间列表。 6 | 7 | 在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。 8 | 9 |   10 | 11 | 示例 1: 12 | 输入:intervals = [[1,3],[6,9]], newInterval = [2,5] 13 | 输出:[[1,5],[6,9]] 14 | 15 | 示例 2: 16 | 输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] 17 | 输出:[[1,2],[3,10],[12,16]] 18 | 解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。 19 | 20 | 21 | ## 方法 22 | 本题和第56题类似。但注意本题所给的区间已经按起始端点升序排序。因此我们仍然遍历区间,按如下步骤构建结果集: 23 | * 先将完全在新区间左边的区间(右边界都小于新区间左边界的区间)加入结果集 24 | * 当遇到一个区间,这个区间的右边界大于等于新区间的左边界,并且这个区间的左边界小于等于新区间的右边界时,则说明需要将其合并,我们将合并后的区间作为新的要插入的区间,继续进入循环。 25 | * 当需要合并的区间都合并完全后,将要插入的区间插入 26 | * 最后将完全在新区间右边的区间(左边界都大于新区间右边界的区间)加入结果集 27 | 28 | ## 代码 29 | ```java 30 | public int[][] insert(int[][] intervals, int[] newInterval) { 31 | if(intervals == null || intervals.length == 0) 32 | return new int[][]{newInterval}; 33 | int[][] res = new int[intervals.length + 1][2]; 34 | int res_index = 0; 35 | int num_index = 0; 36 | while(num_index < intervals.length && intervals[num_index][1] < newInterval[0]) 37 | res[res_index++] = intervals[num_index++]; 38 | while(num_index < intervals.length && intervals[num_index][1] >= newInterval[0] && intervals[num_index][0] <= newInterval[1]){ 39 | newInterval[0] = Math.min(intervals[num_index][0], newInterval[0]); 40 | newInterval[1] = Math.max(intervals[num_index][1], newInterval[1]); 41 | num_index++; 42 | } 43 | res[res_index++] = newInterval; 44 | while(num_index < intervals.length && intervals[num_index][0] > newInterval[1]) 45 | res[res_index++] = intervals[num_index++]; 46 | return Arrays.copyOf(res, res_index); 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /数组/59.螺旋矩阵II.md: -------------------------------------------------------------------------------- 1 | # 59.螺旋矩阵 II 2 | 3 | ## 题目 4 | 给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。 5 | 6 | 示例: 7 | 输入: 3 8 | 输出: 9 | [ 10 | [ 1, 2, 3 ], 11 | [ 8, 9, 4 ], 12 | [ 7, 6, 5 ] 13 | ] 14 | 15 | 16 | ## 方法 17 | 18 | 定义四个索引top、bottom、left、right,分别代表长方形上下左右四条边的索引。 19 | 20 | 按照从左到右、从上到下、从右到左、从下到上的顺序将数值填入数组,每次填入后更新边界(比如说从左到右填入完之后,更新top为top+1) 21 | 22 | ## 代码 23 | ```java 24 | public int[][] generateMatrix(int n) { 25 | int[][] res = new int[n][n]; 26 | int count = 1; 27 | int top = 0, bottom = n - 1, left = 0, right = n - 1; 28 | while(count <= n * n){ 29 | for(int i = left; i <= right; i++){ 30 | res[top][i] = count++; 31 | } 32 | top++; 33 | for(int i = top; i <= bottom; i++){ 34 | res[i][right] = count++; 35 | } 36 | right--; 37 | for(int i = right; i >= left; i--){ 38 | res[bottom][i] = count++; 39 | } 40 | bottom--; 41 | for(int i = bottom; i >= top; i--){ 42 | res[i][left] = count++; 43 | } 44 | left++; 45 | } 46 | return res; 47 | } 48 | ``` -------------------------------------------------------------------------------- /数组/73.矩阵置零.md: -------------------------------------------------------------------------------- 1 | # 73.矩阵置零 2 | 3 | ## 题目 4 | 给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。 5 | 6 | 示例 1: 7 | 输入: 8 | [ 9 |   [1,1,1], 10 |   [1,0,1], 11 |   [1,1,1] 12 | ] 13 | 14 | 输出: 15 | [ 16 |   [1,0,1], 17 |   [0,0,0], 18 |   [1,0,1] 19 | ] 20 | 21 | ## 方法一 22 | 用两个set分别记录需要置0的行和需要置0的列。第一次遍历矩阵时,若发现一个元素为0,则将其行和列值分别加入到两个set中。第二次遍历矩阵时,将行set中的行全部置0,将列set中的列全部置0。 23 | 24 | ```java 25 | public void setZeroes(int[][] matrix) { 26 | if(matrix == null || matrix.length == 0) 27 | return; 28 | int m = matrix.length, n = matrix[0].length; 29 | Set row = new HashSet(); 30 | Set col = new HashSet(); 31 | for(int i = 0; i < m; i++){ 32 | for(int j = 0; j < n; j++){ 33 | if(matrix[i][j] == 0){ 34 | row.add(i); 35 | col.add(j); 36 | } 37 | } 38 | } 39 | for(int i : row){ 40 | for(int j = 0; j < n; j++) 41 | matrix[i][j] = 0; 42 | } 43 | for(int j : col){ 44 | for(int i = 0; i < m; i++) 45 | matrix[i][j] = 0; 46 | } 47 | } 48 | ``` 49 | 50 | * 时间复杂度:O(m * n) 51 | * 空间复杂度:O(m + n) 最坏情况是矩阵中全部元素为0的情况,这时两个set的大小分别为m和n。 52 | 53 | ## 方法二 54 | 思路:不用额外空间,让首行和首列记录某列和某行是否有0 55 | 56 | 算法步骤: 57 | 1. 首先用两个布尔类型变量firstRow和firstCol分别记录矩阵的首行和首列中是否有0 58 | 2. 遍历除首行和首列外的所有元素,若元素matrix[i][j]为0,则将它对应的首行和首列中的元素matrix[i][0]和matrix[0][j]置为0,意为此行和列后续需要被置0(由于修改后首行和首列是否有0的信息会被破坏掉,因此需要有之前的步骤一) 59 | 3. 遍历首行和首列,若发现首行中有元素为0,则将此元素所处的列全部置0,若发现首列中有元素为0,则将此元素所处的行全部置0。 60 | 4. 根据步骤一的布尔类型变量firstRow和firstCol来判断首行和首列是否需要被置0。 61 | 62 | ```java 63 | public void setZeroes(int[][] matrix) { 64 | if(matrix == null || matrix.length == 0) 65 | return; 66 | int m = matrix.length, n = matrix[0].length; 67 | boolean firstRow = false, firstCol = false; 68 | //步骤一 69 | for(int i = 0; i < m; i++){ 70 | if(matrix[i][0] == 0) 71 | firstCol = true; 72 | } 73 | for(int j = 0; j < n; j++){ 74 | if(matrix[0][j] == 0) 75 | firstRow = true; 76 | } 77 | //步骤二 78 | for(int i = 1; i < m; i++){ 79 | for(int j = 1; j < n; j++){ 80 | if(matrix[i][j] == 0){ 81 | matrix[i][0] = 0; 82 | matrix[0][j] = 0; 83 | } 84 | } 85 | } 86 | //步骤三 87 | for(int i = 1; i < m; i++){ 88 | if(matrix[i][0] == 0){ 89 | for(int j = 0; j < n; j++) 90 | matrix[i][j] = 0; 91 | } 92 | } 93 | for(int j = 1; j < n; j++){ 94 | if(matrix[0][j] == 0){ 95 | for(int i = 0; i < m; i++) 96 | matrix[i][j] = 0; 97 | } 98 | } 99 | //步骤四 100 | if(firstRow){ 101 | for(int j = 0; j < n; j++) 102 | matrix[0][j] = 0; 103 | } 104 | if(firstCol){ 105 | for(int i = 0; i < m; i++) 106 | matrix[i][0] = 0; 107 | } 108 | } 109 | ``` 110 | 111 | * 时间复杂度:O(m * n) 112 | * 空间复杂度:O(1) -------------------------------------------------------------------------------- /栈与队列/225.用队列实现栈.md: -------------------------------------------------------------------------------- 1 | # 225 用队列实现栈 2 | **注意**: 队列的基本操作为add和poll和isEmpty,栈的基本操作为push和pop和empty 3 | 4 | **思路:** 5 | * 准备两个队列,一个queue队列一个help队列 6 | * 执行push操作时,只向queue队列中add元素 7 | * 执行pop操作时,将queue中元素弹入help中,只剩下一个需要弹出的元素,将queue中剩下的这一个元素保存后弹出。 最后,将help和queue互换,因为此时queue已弹空,原来queue元素均导入到help中。为了实现任何入列都是入到queue中,因此需将两个队列互换 8 | * 执行peek操作时,与pop操作大体相同。只是peek并未真要弹出栈顶元素,只是取出而已。所以需要将这个元素也加进help中。最终的效果相当于把queue中元素均加入到help中,再将help与queue互换 9 | * queue队列为空栈即为空 10 | 11 | **代码:** 12 | ```java 13 | class MyStack { 14 | 15 | Queue queue1; 16 | Queue help; 17 | 18 | /** Initialize your data structure here. */ 19 | public MyStack() { 20 | queue1 = new LinkedList<>(); 21 | help = new LinkedList<>(); 22 | } 23 | 24 | public void push(int x) { 25 | //只向队列queue中push元素 26 | queue1.add(x); 27 | } 28 | 29 | /** Removes the element on top of the stack and returns that element. */ 30 | public int pop() { 31 | //将queue中元素弹入help中,只剩下一个需要弹出的元素 32 | while(queue1.size() != 1) 33 | help.add(queue1.poll()); 34 | //将queue中剩下的这一个元素保存后弹出 35 | int res = queue1.poll(); 36 | //将help和queue互换 37 | //因为此时queue已弹空,原来queue元素均导入到help中。 38 | //为了实现任何入列都是入到queue中,因此需将两个队列互换 39 | swap(); 40 | return res; 41 | } 42 | 43 | /** Get the top element. */ 44 | //取队顶元素的peek操作大体与pop相同,只是peek并未真要弹出栈顶元素,只是取出而已 45 | //所以需要将这个元素也加进help中。最终的效果相当于把queue中元素均加入到help中,再将help与queue互换 46 | public int top() { 47 | while(queue1.size() != 1) 48 | help.add(queue1.poll()); 49 | int res = queue1.poll(); 50 | help.add(res); 51 | swap(); 52 | return res; 53 | } 54 | 55 | /** Returns whether the stack is empty. */ 56 | public boolean empty() { 57 | return queue1.isEmpty(); 58 | } 59 | 60 | private void swap() { 61 | Queue tmp = help; 62 | help = queue1; 63 | queue1 = tmp; 64 | } 65 | } 66 | ``` 67 | -------------------------------------------------------------------------------- /栈与队列/232.用栈实现队列.md: -------------------------------------------------------------------------------- 1 | # 232 用栈实现队列 2 | **思路:** 3 | * 准备两个栈,一个push栈,一个pop栈 4 | * 执行push操作时,只向push栈中添加元素 5 | * 执行pop操作时,若pop栈为空,则先将push栈中的元素全倒入到pop栈中,然后弹出pop栈的栈顶 6 | * 执行peek操作时,和pop操作基本相同 7 | * 当push栈和pop栈均为空时,队列为空 8 | 9 | **代码:** 10 | ```java 11 | class MyQueue { 12 | private Stack stackPop; 13 | private Stack stackPush; 14 | /** Initialize your data structure here. */ 15 | public MyQueue() { 16 | this.stackPop = new Stack<>(); 17 | this.stackPush = new Stack<>(); 18 | } 19 | 20 | /** Push element x to the back of queue. */ 21 | public void push(int x) { 22 | stackPush.push(x); 23 | } 24 | 25 | /** Removes the element from in front of queue and returns that element. */ 26 | public int pop() { 27 | //当pop栈为空时,将push栈中的元素都加入到pop栈中来 28 | if(stackPop.empty()){ 29 | while(!stackPush.empty()) 30 | stackPop.push(stackPush.pop()); 31 | } 32 | //pop不空时,直接弹出pop栈的栈顶 33 | return stackPop.pop(); 34 | } 35 | 36 | /** Get the front element. */ 37 | //除了最后一行外,和pop操作基本一致 38 | public int peek() { 39 | //当pop栈为空时,将push栈中的元素都加入到pop栈中来 40 | if(stackPop.empty()){ 41 | while(!stackPush.empty()) 42 | stackPop.push(stackPush.pop()); 43 | } 44 | return stackPop.peek(); 45 | } 46 | 47 | /** Returns whether the queue is empty. */ 48 | public boolean empty() { 49 | return stackPush.empty() && stackPop.empty(); 50 | } 51 | } 52 | ``` 53 | -------------------------------------------------------------------------------- /栈与队列/503. 下一个更大元素 II.md: -------------------------------------------------------------------------------- 1 | # 503. 下一个更大元素 II 2 | 3 | ## 题目 4 | 给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。 5 | 6 | 示例 1: 7 | 输入: [1,2,1] 8 | 输出: [2,-1,2] 9 | 解释: 第一个 1 的下一个更大的数是 2; 10 | 数字 2 找不到下一个更大的数; 11 | 第二个 1 的下一个最大的数需要循环搜索,结果也是 2。 12 | 13 | 14 | ## 方法(栈) 15 | 题目的核心是要找一个数右边最近的比它大的数,因此我们想到用单调栈来解决。 16 | 17 | 但此题的特点在于,给定数组是一个循环数组。也就是说:对于数组中的一个数来说,下一个比它大的数可能在它的右边,也可能在它的左边。 18 | 19 | 那么最直观的方法,就是拿原来的两个数组拼成一个新数组,然后对这个新数组应用单调栈的流程,代码如下: 20 | 21 | ```java 22 | public int[] nextGreaterElements(int[] nums) { 23 | int[] arr = new int[nums.length * 2]; 24 | for(int i = 0; i < arr.length; i++){ 25 | arr[i] = nums[i % nums.length]; 26 | } 27 | Stack stack = new Stack(); 28 | for(int i = 0; i < arr.length; i++){ 29 | while(!stack.isEmpty() && arr[i] > arr[stack.peek()]){ 30 | int index = stack.pop(); 31 | arr[index] = arr[i]; 32 | } 33 | stack.push(i); 34 | } 35 | while(!stack.isEmpty()){ 36 | int index = stack.pop(); 37 | arr[index] = -1; 38 | } 39 | return Arrays.copyOf(arr, nums.length); 40 | } 41 | ``` 42 | 43 | 我们也可以不构造新数组,用"%"运算符来模拟循环数组即可 44 | 45 | ```java 46 | public int[] nextGreaterElements(int[] nums) { 47 | int[] arr = new int[nums.length]; 48 | int n = nums.length; 49 | Stack stack = new Stack(); 50 | for(int i = 0; i < 2 * n; i++){ 51 | while(!stack.isEmpty() && nums[i % n] > nums[stack.peek()]){ 52 | int index = stack.pop(); 53 | arr[index] = nums[i % n]; 54 | } 55 | //注意,当i大于n小于2n时,不再需要将元素重复入栈 56 | if(i < n) 57 | stack.push(i % n); 58 | } 59 | while(!stack.isEmpty()){ 60 | int index = stack.pop(); 61 | arr[index] = -1; 62 | } 63 | return arr; 64 | } 65 | ``` -------------------------------------------------------------------------------- /栈与队列/5614.找出最具竞争力的子序列.md: -------------------------------------------------------------------------------- 1 | # 5614.找出最具竞争力的子序列 2 | 3 | ## 题目 4 | 给你一个整数数组 nums 和一个正整数 k ,返回长度为 k 且最具 竞争力 的 nums 子序列。 5 | 6 | 在子序列 a 和子序列 b 第一个不相同的位置上,如果 a 中的数字小于 b 中对应的数字,那么我们称子序列 a 比子序列 b(相同长度下)更具 竞争力 。 例如,[1,3,4] 比 [1,3,5] 更具竞争力,在第一个不相同的位置,也就是最后一个位置上, 4 小于 5 。 7 | 8 | 示例 1: 9 | 输入:nums = [3,5,2,6], k = 2 10 | 输出:[2,6] 11 | 解释:在所有可能的子序列集合 {[3,5], [3,2], [3,6], [5,2], [5,6], [2,6]} 中,[2,6] 最具竞争力。 12 | 13 | 示例 2: 14 | 输入:nums = [2,4,3,3,5,4,9,6], k = 4 15 | 输出:[2,3,3,4] 16 | 17 | ## 方法(单调栈) 18 | 单调栈可以用于寻找在一个数组中,一个元素右边比它大的最小元素是谁,以及左边比它小的最大元素是谁。 19 | 20 | 而反观这道题,要寻找最具竞争力的子序列,不就是不断地在一个元素右边尽可能找到一个比它大的更小的元素吗 21 | 22 | 算法步骤如下: 23 | 准备一个栈,这个栈需要保持从栈底到栈顶元素大小从小到大,依次遍历数组中的每个元素 24 | * 如果该元素小于栈顶,并且这时弹出栈顶不会造成栈中元素和数组中剩余元素加在一起都凑不到k的情况,那么就放心弹出栈顶 25 | * 如果该元素大于栈顶,并且栈中元素尚未到K,那么就将该元素入栈。 26 | * 最后,将这个大小为K的栈弹出作为结果。 27 | 28 | ## 代码 29 | ```java 30 | public int[] mostCompetitive(int[] nums, int k) { 31 | Stack stack = new Stack<>(); 32 | stack.push(nums[0]); 33 | for(int i = 1; i < nums.length; i++){ 34 | while(!stack.isEmpty() && stack.peek() > nums[i] && stack.size() + nums.length - i > k){ 35 | stack.pop(); 36 | } 37 | if(stack.size() < k) 38 | stack.push(nums[i]); 39 | } 40 | int[] res = new int[k]; 41 | for(int i = k - 1; i >= 0; i--) 42 | res[i] = stack.pop(); 43 | return res; 44 | } 45 | ``` -------------------------------------------------------------------------------- /栈与队列/71.简化路径.md: -------------------------------------------------------------------------------- 1 | # 71.简化路径 2 | 3 | ## 题目 4 | 5 | 以 Unix 风格给出一个文件的绝对路径,你需要简化它。或者换句话说,将其转换为规范路径。 6 | 7 | 在 Unix 风格的文件系统中,一个点(.)表示当前目录本身;此外,两个点 (..) 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。 8 | 9 | 请注意,返回的规范路径必须始终以斜杠 / 开头,并且两个目录名之间必须只有一个斜杠 /。最后一个目录名(如果存在)不能以 / 结尾。此外,规范路径必须是表示绝对路径的最短字符串。 10 | 11 | 示例 1: 12 | 输入:"/home/" 13 | 输出:"/home" 14 | 解释:注意,最后一个目录名后面没有斜杠。 15 | 16 | 示例 2: 17 | 输入:"/../" 18 | 输出:"/" 19 | 解释:从根目录向上一级是不可行的,因为根是你可以到达的最高级。 20 | 21 | 示例 3: 22 | 输入:"/home//foo/" 23 | 输出:"/home/foo" 24 | 解释:在规范路径中,多个连续斜杠需要用一个斜杠替换。 25 | 26 | 示例 4: 27 | 输入:"/a/./b/../../c/" 28 | 输出:"/c" 29 | 30 | 示例 5: 31 | 输入:"/a/../../b/../c//.//" 32 | 输出:"/c" 33 | 34 | 示例 6: 35 | 输入:"/a//b////c/d//././/.." 36 | 输出:"/a/b/c" 37 | 38 | ## 方法 39 | 用栈解决,具体步骤如下: 40 | * 先将给定路径以"/"分割,分割得到的元素可能包含"." , ".." , ""这三种特殊情况以及正常的目录。 41 | * 如果遍历到正常的目录,则入栈。如果遍历到""和".",不入栈。如果遍历到"..",则弹出栈顶目录。 42 | * 遍历栈中的每一层目录,拼出规范路径的结果res 43 | 44 | ## 代码 45 | ```java 46 | public String simplifyPath(String path) { 47 | Stack stack = new Stack<>(); 48 | for(String str: path.split("/")){ 49 | if(str.equals("..")){ 50 | if(!stack.isEmpty()) 51 | stack.pop(); 52 | } 53 | else{ 54 | if(!str.isEmpty() && !str.equals(".")) 55 | stack.push(str); 56 | } 57 | } 58 | String res = ""; 59 | for(String str: stack) 60 | res += "/" + str; 61 | return res.isEmpty() ? "/" : res; 62 | } 63 | ``` 64 | 65 | ## 参考 66 | * [leetcode题解区](https://leetcode-cn.com/problems/simplify-path/solution/zhan-by-powcai/) -------------------------------------------------------------------------------- /栈与队列/739.每日温度.md: -------------------------------------------------------------------------------- 1 | # 739.每日温度 2 | 3 | ## 题目 4 | 请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。 5 | 6 | 例如, 7 | 给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73] 8 | 你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 9 | 10 | 提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。 11 | 12 | ## 方法(单调栈) 13 | 14 | 题目的意思为:要我们对于数组中的每一个数,找到右边最近的比它大的那个数离它有多远 15 | 16 | **对于在数组中寻找左右两边最近的比它大或比它小的数的情况,我们可以使用单调栈来解决。** 17 | 18 | 算法流程: 19 | 准备一个栈,然后遍历数组: 20 | * 如果当前元素比栈顶元素小,那么直接入栈。 21 | * 如果当前元素比栈顶元素大,那么不断地将栈顶元素弹出,直到当前元素比栈顶元素小为止,再将当前元素入栈。以让栈保持从栈底到栈顶从大到小的单调性。 22 | * 每当栈中一个元素弹出时,说明让它弹出的那个元素就是它右边最近的比它大的元素。我们这时进行结算,它们之间的距离就是它们在数组中的位置(索引)之差。也正因为如此,为了方便,我们在将元素入栈出栈时,都针对的是它在数组中的索引,而不是它具体的值。 23 | 24 | * 当遍历完所有元素后,如果栈中还有元素没有弹出,说明这些元素没有遇到右边比它大的元素,将结果数组中这些元素对应的位置处置0 25 | 26 | ## 代码 27 | ```java 28 | public int[] dailyTemperatures(int[] T) { 29 | int[] res = new int[T.length]; 30 | Stack stack = new Stack<>(); 31 | for(int i = 0; i < T.length; i++){ 32 | while(!stack.isEmpty() && T[i] > T[stack.peek()]){ 33 | int index = stack.pop(); 34 | res[index] = i - index; 35 | } 36 | stack.push(i); 37 | } 38 | while(!stack.isEmpty()){ 39 | int index = stack.pop(); 40 | res[index] = 0; 41 | } 42 | return res; 43 | } 44 | ``` -------------------------------------------------------------------------------- /栈与队列/84.柱状图中最大的矩形.md: -------------------------------------------------------------------------------- 1 | # 84.柱状图中最大的矩形 2 | 3 | ## 题目 4 | 5 | 给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 6 | 求在该柱状图中,能够勾勒出来的矩形的最大面积。 7 | 8 | 例如:height = [4,3,2,5,6]. 9 | 则柱状图中每一竖列的高分别为4,3,2,5,6。 10 | 能找到的最大矩形即为以5为底,以2为高的矩形。面积为10. 11 | 12 | ## 方法(单调栈) 13 | 14 | 依次计算以每一竖列为高的所有矩形中最大的面积。即统计从一个竖列向左右扩,左边和右边分别能扩多远(如果在左右遇到了低于它的竖列,则无法扩)。 15 | 16 | 我们维护一个单调栈结构来做到这一点(由栈底到栈顶,元素大小从小到大),准备一个栈。先将第一个元素入栈。之后尝试将数组中的每一个元素入栈。 17 | * 如果要加入的元素小于栈顶元素,那么为了维护栈的单调性,将栈顶依次弹出,直到栈顶元素小于要加入元素了,将元素入栈。对于弹出的每个元素,都代表着直方图中的每一个竖列。弹出时进行结算,此单调栈的原则是:谁让它弹出,谁就是它右边最近比它小的。而它在栈中下面的那个元素代表着它左边最近比它小的。因此,知道它左右两边最近比它小的,就可以知道以它为竖列的最大矩形面积。 18 | * 如果要加入的元素大于栈顶元素,符合栈的单调性,直接入栈。 19 | 20 | 当遍历完数组中的所有元素后,如果栈非空,将栈中元素弹空。每弹出一个元素,对它进行结算。计算以它为竖列的最大矩形面积。对于这些元素,因为没有元素使得它弹出,因此它没有右边比它小的元素,它可以扩到直方图的最右边。 21 | 22 | ## 代码 23 | ```java 24 | public static int maxRecFromBottom(int[] height){ 25 | if(height == null || height.length == 0) 26 | return 0; 27 | int maxArea = 0; 28 | Stack stack = new Stack<>(); 29 | for(int i = 0; i < height.length; i++){ 30 | //如果要加入的元素小于栈顶元素,将栈顶依次弹出。弹出时进行结算 31 | while(!stack.isEmpty() && height[i] <=height[stack.peek()]){ 32 | int j = stack.pop(); 33 | //k是以j为竖列能扩到的左边界。即它在栈中下面的那个元素。 34 | //如果j弹出时,它下面没元素,那么左边界为-1.否则,它的左边界为它下面那个元素在数组中的下标。 35 | int k = stack.isEmpty() ? -1 : stack.peek(); 36 | //遍历到i位置令j弹出,因此i为以j为竖列能扩到的右边界。 37 | //所以以j为竖列能扩出的最大矩形:底为i - k - 1。高为height[j] 38 | int curArea = (i - k - 1) * height[j]; 39 | maxArea = Math.max(maxArea, curArea); 40 | } 41 | //否则,直接将元素入栈。 42 | stack.push(i); 43 | } 44 | //在对数组遍历完后,对于栈中剩余的那些元素,不要忘记结算。 45 | //因为没有元素令它们弹出,所以它们在右边没有比它们小的元素。向右可以扩到头height.length 46 | while (!stack.isEmpty()){ 47 | int j = stack.pop(); 48 | int k = stack.isEmpty() ? -1 : stack.peek(); 49 | int curArea = (height.length - k - 1) * height[j]; 50 | maxArea = Math.max(maxArea, curArea); 51 | } 52 | return maxArea; 53 | } 54 | ``` -------------------------------------------------------------------------------- /树/102.二叉树的层序遍历.md: -------------------------------------------------------------------------------- 1 | # 102.二叉树的层序遍历 2 | 3 | ## 题目 4 | 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 5 | 6 |   7 | 8 | 示例: 9 | 二叉树:[3,9,20,null,null,15,7], 10 | 3 11 | / \ 12 | 9 20 13 | / \ 14 | 15 7 15 | 返回其层次遍历结果: 16 | [ 17 | [3], 18 | [9,20], 19 | [15,7] 20 | ] 21 | 22 | ## 方法(广度优先搜索) 23 | 用一个队列结构来实现广度优先搜索。从根节点开始搜索,每次遍历同一层的全部节点,使用一个列表存储该层的节点值。 24 | 25 | * 时间复杂度:O(n) 需要遍历到树中的每一个节点 26 | * 空间复杂度:O(n) 队列的长度不超过n 27 | 28 | ## 代码 29 | ```java 30 | public List> levelOrder(TreeNode root) { 31 | List> res = new LinkedList>(); 32 | if(root == null) 33 | return res; 34 | Queue queue = new LinkedList<>(); 35 | queue.offer(root); 36 | while(!queue.isEmpty()){ 37 | List level = new ArrayList<>(); 38 | int size = queue.size(); 39 | for(int i = 0; i < size; i++){ 40 | TreeNode node = queue.poll(); 41 | level.add(node.val); 42 | if(node.left != null) 43 | queue.offer(node.left); 44 | if(node.right != null) 45 | queue.offer(node.right); 46 | } 47 | res.add(level); 48 | } 49 | return res; 50 | } 51 | ``` 52 | 53 | ## 方法(深度优先搜索) 54 | 在每次第一次遍历到一层时,在结果数组res中添加一个新的空数组。在之后的遍历过程中,将节点值加入到该层在res中对应的数组中。 55 | ```java 56 | class Solution { 57 | List> res = new ArrayList<>(); 58 | public List> levelOrder(TreeNode root) { 59 | if(root == null) 60 | return res; 61 | dfs(root, 1); 62 | return res; 63 | } 64 | public void dfs(TreeNode root, int level){ 65 | if(root == null) 66 | return; 67 | if(level == res.size() + 1) 68 | res.add(new ArrayList<>()); 69 | res.get(level - 1).add(root.val); 70 | dfs(root.left, level + 1); 71 | dfs(root.right, level + 1); 72 | } 73 | } 74 | ``` -------------------------------------------------------------------------------- /树/105.从前序与中序遍历序列构造二叉树.md: -------------------------------------------------------------------------------- 1 | # 105. 从前序与中序遍历序列构造二叉树 2 | 3 | ## 题目 4 | 根据一棵树的前序遍历与中序遍历构造二叉树。 5 | 6 | 注意: 7 | 你可以假设树中没有重复的元素。 8 | 9 | 例如,给出 10 | 前序遍历 preorder = [3,9,20,15,7] 11 | 中序遍历 inorder = [9,3,15,20,7] 12 | 13 | 返回如下的二叉树: 14 | 3 15 | / \ 16 | 9 20 17 | / \ 18 | 15 7 19 | 20 | 21 | ## 方法(递归) 22 | 二叉树的三种遍历次序依次为: 23 | * 前序遍历:根、左、右 24 | * 中序遍历:左、根、右 25 | * 后序遍历:左、右、根 26 | 27 | 一个二叉树的前序、中序、后序遍历序列可以看作由如下部分构成: 28 | * 前序遍历序列: 29 | 30 | [根节点, [左子树的前序遍历], [右子树的前序遍历]] 31 | 32 | * 中序遍历序列: 33 | 34 | [[左子树的中序遍历], 根节点, [右子树的中序遍历]] 35 | 36 | * 后序遍历序列: 37 | 38 | [[左子树的后序遍历], [右子树的后序遍历], 根节点] 39 | 40 | 41 | 因此,我们由前序遍历和中序遍历构造二叉树的步骤为: 42 | 1. 在前序遍历序列中得到根节点 43 | 2. 在中序遍历序列中找到根节点的位置,其左边为左子树中序遍历序列,右边为右子树中序遍历序列。 44 | 3. 在前序遍历序列中也找到左右子树对应的前序遍历序列(根据一棵树的中序遍历序列和前序遍历序列的长度相同) 45 | 4. 递归生成左右子树 46 | 5. 将根节点与左右子树连接 47 | 48 | 为了降低空间复杂度,我们不对原树的preorder和inorder切分生成新的数组序列,而是采用双指针记下子树的序列在preorder和inorder的索引范围 49 | 50 | ## 代码 51 | ```java 52 | class Solution { 53 | int[] preorder; 54 | int[] inorder; 55 | Map map = new HashMap<>(); 56 | public TreeNode buildTree(int[] preorder, int[] inorder) { 57 | if(preorder == null || preorder.length == 0) 58 | return null; 59 | this.preorder = preorder; 60 | this.inorder = inorder; 61 | for(int i = 0; i < inorder.length; i++) 62 | map.put(inorder[i], i); 63 | return dfs(0, preorder.length - 1, 0, inorder.length - 1); 64 | } 65 | //根据preOrder[preStart, preEnd]和inOrder[inStart, inEnd]构造一个二叉树 66 | public TreeNode dfs(int preStart, int preEnd, int inStart, int inEnd){ 67 | if(preStart > preEnd || inStart > inEnd) 68 | return null; 69 | if(preStart == preEnd || inStart == inEnd) 70 | return new TreeNode(preorder[preStart]); 71 | TreeNode root = new TreeNode(preorder[preStart]); 72 | int rootIndex = map.get(root.val); 73 | int leftLen = rootIndex - inStart; 74 | TreeNode left = dfs(preStart + 1, preStart + leftLen, inStart, rootIndex - 1); 75 | TreeNode right = dfs(preStart + leftLen + 1, preEnd, rootIndex + 1, inEnd); 76 | root.left = left; 77 | root.right = right; 78 | return root; 79 | } 80 | } 81 | ``` 82 | 83 | * 时间复杂度:O(n), n为树中节点的个数 84 | * 空间复杂度:O(n), 需要用O(n)存储哈希表,O(h)存储递归时的栈空间(h为树高度 h < n),因此空间复杂度为O(n) 85 | -------------------------------------------------------------------------------- /树/106.从中序与后序遍历序列构造二叉树.md: -------------------------------------------------------------------------------- 1 | # 106. 从中序与后序遍历序列构造二叉树 2 | 3 | ## 题目 4 | 根据一棵树的中序遍历与后序遍历构造二叉树。 5 | 6 | 注意: 7 | 你可以假设树中没有重复的元素。 8 | 9 | 例如,给出 10 | 11 | 中序遍历 inorder = [9,3,15,20,7] 12 | 后序遍历 postorder = [9,15,7,20,3] 13 | 返回如下的二叉树: 14 | 15 | 3 16 | / \ 17 | 9 20 18 | / \ 19 | 15 7 20 | 21 | 22 | ## 方法(递归) 23 | 二叉树的三种遍历次序依次为: 24 | * 前序遍历:根、左、右 25 | * 中序遍历:左、根、右 26 | * 后序遍历:左、右、根 27 | 28 | 一个二叉树的前序、中序、后序遍历序列可以看作由如下部分构成: 29 | * 前序遍历序列: 30 | 31 | [根节点, [左子树的前序遍历], [右子树的前序遍历]] 32 | 33 | * 中序遍历序列: 34 | 35 | [[左子树的中序遍历], 根节点, [右子树的中序遍历]] 36 | 37 | * 后序遍历序列: 38 | 39 | [[左子树的后序遍历], [右子树的后序遍历], 根节点] 40 | 41 | 42 | 因此,我们由后序遍历和中序遍历构造二叉树的步骤为: 43 | 1. 在后序遍历序列中得到根节点 44 | 2. 在中序遍历序列中找到根节点的位置,其左边为左子树中序遍历序列,右边为右子树中序遍历序列。 45 | 3. 在后序遍历序列中也找到左右子树对应的后序遍历序列(根据一棵树的中序遍历序列和后序遍历序列的长度相同) 46 | 4. 递归生成左右子树 47 | 5. 将根节点与左右子树连接 48 | 49 | 为了降低空间复杂度,我们不对原树的preorder和inorder切分生成新的数组序列,而是采用双指针记下子树的序列在preorder和inorder的索引范围 50 | 51 | ## 代码 52 | ```java 53 | class Solution { 54 | private int[] inorder; 55 | private int[] postorder; 56 | Map map = new HashMap<>(); 57 | public TreeNode buildTree(int[] inorder, int[] postorder) { 58 | this.inorder = inorder; 59 | this.postorder = postorder; 60 | for(int i = 0; i < inorder.length; i++) 61 | map.put(inorder[i], i); 62 | return dfs(0, inorder.length - 1, 0, postorder.length - 1); 63 | } 64 | 65 | //根据inorder[inStart, inEnd]和postorder[postStart, postEnd]构造一颗二叉树 66 | public TreeNode dfs(int inStart, int inEnd, int postStart, int postEnd){ 67 | if(inStart > inEnd || postStart > postEnd) 68 | return null; 69 | if(inStart == inEnd || postStart == postEnd) 70 | return new TreeNode(inorder[inStart]); 71 | TreeNode root = new TreeNode(postorder[postEnd]); 72 | int rootIndex = map.get(root.val); 73 | int leftLen = rootIndex - inStart; 74 | TreeNode left = dfs(inStart, rootIndex - 1, postStart, postStart + leftLen - 1); 75 | TreeNode right = dfs(rootIndex + 1, inEnd, postStart + leftLen, postEnd - 1); 76 | root.left = left; 77 | root.right = right; 78 | return root; 79 | } 80 | } 81 | ``` 82 | 83 | * 时间复杂度:O(n), n为树中节点的个数 84 | * 空间复杂度:O(n), 需要用O(n)存储哈希表,O(h)存储递归时的栈空间(h为树高度 h < n),因此空间复杂度为O(n) 85 | -------------------------------------------------------------------------------- /树/108.将有序数组转换为二叉搜索树.md: -------------------------------------------------------------------------------- 1 | # 108. 将有序数组转换为二叉搜索树 2 | 3 | ## 题目 4 | 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。 5 | 6 | 本题中,一个高度平衡二叉树是指一个二叉树每个节点的左右两个子树的高度差的绝对值不超过 1。 7 | 8 | 示例: 9 | 给定有序数组: [-10,-3,0,5,9], 10 | 一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树: 11 | 12 | 0 13 | / \ 14 | -3 9 15 | / / 16 | -10 5 17 | 18 | ## 方法 19 | 由于二叉搜索树的中序遍历是升序的,因此本题相当于根据中序遍历的序列恢复二叉搜索树。因此我们可以拿序列中的任何一个元素作为根节点,以该元素左边的升序序列构建左子树,以该元素右边的升序序列构建右子树,这样就可以得到一颗二叉搜索树。又因为题目要求得到一颗平衡搜索二叉树BST,因此我们选择升序序列中的中间元素作为根节点。 20 | 21 | 22 | ## 代码 23 | ```java 24 | class Solution { 25 | private int[] nums; 26 | public TreeNode sortedArrayToBST(int[] nums) { 27 | this.nums = nums; 28 | return dfs(0, nums.length - 1); 29 | } 30 | //dfs函数用数组nums中从索引left到right间的元素构造二叉搜索树,返回构造后树的根节点。 31 | public TreeNode dfs(int left, int right){ 32 | if(left > right) 33 | return null; 34 | int mid = left + ((right - left) >> 1); 35 | TreeNode root = new TreeNode(nums[mid]); 36 | root.left = dfs(left, mid - 1); 37 | root.right = dfs(mid + 1, right); 38 | return root; 39 | } 40 | } 41 | ``` 42 | 本题的拓展为[109.有序链表转换二叉搜索树](https://github.com/wyh317/Leetcode/blob/master/%E6%A0%91/109.%E6%9C%89%E5%BA%8F%E9%93%BE%E8%A1%A8%E8%BD%AC%E6%8D%A2%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.md).两道题思路完全一样,只不过链表无法像数组一样通过索引直接找到中间元素,需要用快慢指针法找中间元素。 43 | -------------------------------------------------------------------------------- /树/109.有序链表转换二叉搜索树.md: -------------------------------------------------------------------------------- 1 | # 109.有序链表转换二叉搜索树 2 | 3 | ## 题目 4 | 5 | 给定一个单链表,其中的元素按升序排序,将其转换为高度平衡的二叉搜索树。 6 | 7 | 本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。 8 | 9 | 示例: 10 | 11 | 给定的有序链表: [-10, -3, 0, 5, 9], 12 | 一个可能的答案是:[0, -3, 9, -10, null, 5], 它可以表示下面这个高度平衡二叉搜索树: 13 | 14 | 0 15 | / \ 16 | -3 9 17 | / / 18 | -10 5 19 | 20 | ## 方法 21 | 本题和[108.将有序数组转换为二叉搜索树](https://github.com/wyh317/Leetcode/blob/master/%E6%A0%91/108.%E5%B0%86%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E8%BD%AC%E6%8D%A2%E4%B8%BA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.md)思路完全一样。 22 | 因为链表是升序排列的,所以只要找到它的中间节点,让它成为树的根节点,再让其前面的元素成为左子树,让其后面的元素成为右子树,再对左右子树分别递归进行相同的操作即可。我们用快慢指针法来寻找中间节点。 23 | 24 | ## 代码 25 | ```java 26 | public TreeNode sortedListToBST(ListNode head) { 27 | //递归结束条件 + 边界处理 28 | if(head == null) 29 | return null; 30 | if(head.next == null) 31 | return new TreeNode(head.val); 32 | //快慢指针法找到中间节点,pre指针用于记录slow的前一个节点 33 | ListNode slow = head, fast = head, pre = null; 34 | while(fast != null && fast.next != null){ 35 | pre = slow; 36 | slow = slow.next; 37 | fast = fast.next.next; 38 | } 39 | //让两个链表从中间断开 40 | pre.next = null; 41 | TreeNode root = new TreeNode(slow.val); 42 | root.left = sortedListToBST(head); 43 | root.right = sortedListToBST(slow.next); 44 | return root; 45 | } 46 | ``` 47 | -------------------------------------------------------------------------------- /树/112.路径总和.md: -------------------------------------------------------------------------------- 1 | # 112 路径总和 2 | 3 | **题目:** 4 | 给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。 5 | 6 | **思路:递归** 7 | 遍历整棵树,依次对子节点递归调用hasPathSum函数,调用时参数sum值更新为sum减去当前节点的值。如果当前节点是叶节点,并且sum减到了0。则说明找到了这一路径。 8 | 9 | **代码:** 10 | ```java 11 | class Solution { 12 | public boolean hasPathSum(TreeNode root, int targetSum) { 13 | if(root == null) 14 | return false; 15 | if(root.left == null && root.right == null) 16 | return root.val == targetSum; 17 | return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val); 18 | } 19 | } 20 | ``` 21 | -------------------------------------------------------------------------------- /树/114.二叉树展开为链表.md: -------------------------------------------------------------------------------- 1 | # 114 二叉树展开为链表 2 | 3 | **题目:** 4 | 给定一个二叉树,原地将它展开为一个单链表。 5 | 6 | 例如,给定二叉树 7 | 8 | 1 9 | / \ 10 | 2 5 11 | / \ \ 12 | 3 4 6 13 | 将其展开为: 14 | 15 | 1 16 | \ 17 | 2 18 | \ 19 | 3 20 | \ 21 | 4 22 | \ 23 | 5 24 | \ 25 | 6 26 | 27 | 28 | **思路:** 29 | 采用递归的思路来解决问题。递归的特点就在于,无需关注函内部数处理的细节,只需要关注函数的功能以及函数的输入输出即可。 30 | 对于这道题而言,可以分三步解决问题: 31 | 1. 将左子树展开为链表 32 | 2. 将右子树展开为链表 33 | 3. 将链表形式的右子树放在链表形式的左子数的右边 34 | 35 | **代码:** 36 | ```java 37 | class Solution { 38 | public void flatten(TreeNode root) { 39 | if(root == null) 40 | return; 41 | //先分别将左右子数转化为链表 42 | flatten(root.left); 43 | flatten(root.right); 44 | //先把root.right保存下来,再将左子树接到root.right上来,之后把root.left置空 45 | TreeNode temp = root.right; 46 | root.right = root.left; 47 | root.left = null; 48 | TreeNode cur = root; 49 | //将右子数接到当前链表的末尾 50 | while(cur.right != null) 51 | cur = cur.right; 52 | cur.right = temp; 53 | } 54 | } 55 | ``` 56 | -------------------------------------------------------------------------------- /树/116.填充每个节点的下一个右侧节点指针.md: -------------------------------------------------------------------------------- 1 | # 116 填充每个节点的下一个右侧节点指针 2 | 3 | **题目:** 4 | 给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: 5 | 6 | struct Node { 7 | int val; 8 | Node *left; 9 | Node *right; 10 | Node *next; 11 | } 12 | 13 | 填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 14 | 15 | 初始状态下,所有 next 指针都被设置为 NULL。 16 | 17 | 18 | ## 递归解法 19 | 每个 node 左子树的 next , 就是 node 的右子树 20 | 每个 node 右子树的 next, 就是 node next 的 左子树 21 | 22 | **代码:** 23 | ```java 24 | class Solution { 25 | public Node connect(Node root) { 26 | if(root == null) 27 | return null; 28 | if(root.left != null) 29 | root.left.next = root.right; 30 | if(root.right != null) 31 | root.right.next = root.next != null ? root.next.left : null; 32 | connect(root.left); 33 | connect(root.right); 34 | return root; 35 | } 36 | } 37 | ``` 38 | 39 | ## BFS解法 40 | **算法**: 41 | 1. 创建一个辅助队列 Q,通过以下方式实现层序遍历: 每一步都记录当前队列中全部 元素数量,即对应树中一个层级元素的数量。然后从队列中处理对应数量的元素。完成后,这一层级所有的节点都被访问,队列包含下一层级的 全部 节点 42 | ```java 43 | while (!Q.empty()) 44 | { 45 | size = Q.size() 46 | for i in range 0..size 47 | { 48 | node = Q.pop() 49 | Q.push(node.left) 50 | Q.push(node.right) 51 | } 52 | } 53 | ``` 54 | 2. 首先在队列中加入根节点。因为第 0 层级只有一个节点,不需要建立连接,直接进入 while 循环即可 55 | 3. 伪代码中 while 循环迭代的是树的层级,内部的 for 循环迭代的是一个层级上所有的节点。由于可以访问同一层级的所有节点,因此能够建立指针连接。 56 | 4. for 循环弹出一个节点时,同时把它的子节点加入队列。因此队列中每个层级的元素也是顺序存储的。可以通过已有的顺序建立 next 指针。 57 | 58 | ```java 59 | class Solution { 60 | public Node connect(Node root) { 61 | if(root == null) 62 | return null; 63 | Queue queue = new LinkedList<>(); 64 | queue.add(root); 65 | while(!queue.isEmpty()){ 66 | int size = queue.size(); 67 | for(int i = 0; i < size; i++){ 68 | Node node = queue.poll(); 69 | if(i < size - 1) 70 | node.next = queue.peek(); 71 | if(node.left != null) 72 | queue.add(node.left); 73 | if(node.right != null) 74 | queue.add(node.right); 75 | } 76 | } 77 | return root; 78 | } 79 | } 80 | 81 | ``` 82 | -------------------------------------------------------------------------------- /树/117.填充每个节点的下一个右侧节点指针 II.md: -------------------------------------------------------------------------------- 1 | # 117 填充每个节点的下一个右侧节点指针 II 2 | **题目:** 3 | 大体和题目116相同,只是给定的二叉树并不受限制 4 | 5 | **思路:** 6 | 递归法 7 | * 如果一个节点root既有左子节点又有右子节点,则左子节点的next指向右子节点,右子节点的next指向NextNoNullChild 8 | * 如果一个节点只有左子节点,则左子节点的next指向其NextNoNullChild 9 | * 如果一个节点只有右子节点,则右子节点的next指向其NextNoNullChild 10 | 11 | 这里注意一定要先构建右子树,再构建左子树,因为寻找父节点的兄弟节点是从左到右遍历的,如果右子树未构建好就遍历,则会出错 12 | 13 | BFS广度优先搜索: 14 | 116题的BFS解答同样适用于该题 15 | 16 | **代码:** 17 | ```java 18 | class Solution { 19 | public Node connect(Node root) { 20 | if(root == null) 21 | return null; 22 | if(root.right == null && root.left == null) 23 | return root; 24 | if(root.right != null && root.left != null){ 25 | root.left.next = root.right; 26 | root.right.next = NextNoNullChild(root); 27 | } 28 | if(root.right == null) 29 | root.left.next = NextNoNullChild(root); 30 | if(root.left == null) 31 | root.right.next = NextNoNullChild(root); 32 | 33 | root.right = connect(root.right); 34 | root.left = connect(root.left); 35 | return root; 36 | } 37 | //用while循环,找到和root及其兄弟节点的的下一个不为null的子节点 38 | public Node NextNoNullChild(Node root){ 39 | if(root == null) 40 | return null; 41 | while(root.next != null){ 42 | if(root.next.left != null) 43 | return root.next.left; 44 | if(root.next.right != null) 45 | return root.next.right; 46 | root = root.next; 47 | } 48 | return null; 49 | } 50 | } 51 | ``` 52 | -------------------------------------------------------------------------------- /树/230. 二叉搜索树中第K小的元素.md: -------------------------------------------------------------------------------- 1 | # 230. 二叉搜索树中第K小的元素 2 | 3 | ## 题目 4 | 5 | 给定一个二叉搜索树,编写一个函数 kthSmallest 来查找其中第 k 个最小的元素。 6 | 7 | 说明: 8 | 你可以假设 k 总是有效的,1 ≤ k ≤ 二叉搜索树元素个数。 9 | 10 | 示例 1: 11 | 输入: root = [3,1,4,null,2], k = 1 12 | 3 13 | / \ 14 | 1 4 15 | \ 16 |   2 17 | 输出: 1 18 | 19 | 示例 2: 20 | 输入: root = [5,3,6,2,4,null,null,1], k = 3 21 | 5 22 | / \ 23 | 3 6 24 | / \ 25 | 2 4 26 | / 27 | 1 28 | 输出: 3 29 | 30 | ## 方法一 31 | 根据二叉搜索树的中序遍历序列是升序的这一特性,我们可以对树进行中序遍历,得到树中元素的升序序列,最后再返回序列中的第k-1个元素即可。 32 | ```java 33 | class Solution { 34 | List res = new ArrayList<>(); 35 | public int kthSmallest(TreeNode root, int k) { 36 | dfs(root); 37 | return res.get(k - 1); 38 | } 39 | 40 | public void dfs(TreeNode root){ 41 | if(root == null) 42 | return; 43 | dfs(root.left); 44 | res.add(root.val); 45 | dfs(root.right); 46 | } 47 | } 48 | ``` 49 | 50 | * 时间复杂度: $O(n)$ 51 | * 空间复杂度: $O(n)$ 52 | 53 | ## 方法二 54 | 因为二叉搜索树左子树的元素都小于根节点,右子树的元素都大于根节点。因此: 55 | * 如果k不大于root的左子树的节点个数,那么以root为根的二叉搜索树的第k小元素也就是root左子树的第k小元素 56 | * 如果k正好比root的左子树的节点个数大一,那么第k小元素就是根节点本身。 57 | * 否则,二叉搜索树的第k小元素存在于右子树中,也即要寻找右子树的第$k - leftNum - 1$小的元素。(注:$leftNum+1$为左子树加根节点的节点个数) 58 | ```java 59 | public int kthSmallest(TreeNode root, int k) { 60 | //获取左子树的节点数leftNum 61 | int leftNum = number(root.left); 62 | if(leftNum >= k) 63 | return kthSmallest(root.left, k); 64 | else if(leftNum + 1 == k) 65 | return root.val; 66 | else 67 | return kthSmallest(root.right, k - leftNum - 1); 68 | } 69 | 70 | //此函数用于获取以root为根节点的树的节点个数 71 | public int number(TreeNode root){ 72 | if(root == null) 73 | return 0; 74 | return number(root.left) + number(root.right) + 1; 75 | } 76 | ``` 77 | 78 | * 时间复杂度: $O(nlogn)$ 79 | * 空间复杂度: $O(n)$ 80 | 81 | ## 方法三 82 | 方法三可看作是对方法一的一个改进,我们在对二叉搜索树进行中序遍历的时候,不再用额外空间来存储树中节点信息。而是用一个变量count来记录节点当前已遍历到的节点个数,当遍历到第k个节点时(count等于k),返回这个节点的值。 83 | ```java 84 | class Solution { 85 | int count = 0; 86 | int res; 87 | int k; 88 | public int kthSmallest(TreeNode root, int k) { 89 | this.k = k; 90 | dfs(root); 91 | return res; 92 | } 93 | 94 | public void dfs(TreeNode root){ 95 | if(root == null) 96 | return; 97 | dfs(root.left); 98 | if(++count == k){ 99 | res = root.val; 100 | return; 101 | } 102 | dfs(root.right); 103 | } 104 | } 105 | ``` 106 | 107 | * 时间复杂度: $O(n)$ 108 | * 空间复杂度: $O(1)$ -------------------------------------------------------------------------------- /树/257.二叉树的所有路径.md: -------------------------------------------------------------------------------- 1 | # 257.二叉树的所有路径 2 | 3 | ## 题目 4 | 给定一个二叉树,返回所有从根节点到叶子节点的路径。 5 | 6 | 说明: 叶子节点是指没有子节点的节点。 7 | 8 | 示例: 9 | 输入: 10 | 11 | 1 12 | / \ 13 | 2 3 14 | \ 15 | 5 16 | 17 | 输出: ["1->2->5", "1->3"] 18 | 19 | ## 方法(深度优先搜索) 20 | 对以root为根节点的树进行深度优先遍历,搜索从root到叶节点的路径: 21 | * 如果遍历到了叶子节点,则在当前路径的结尾处添加该节点,并将目前的整条路经加到结果数组中 22 | * 如果当前遍历不是叶子节点,则在当前路径的结尾处添加该节点,继续遍历其子节点 23 | 24 | ## 代码 25 | ```java 26 | class Solution{ 27 | private List res = new ArrayList<>(); 28 | public List binaryTreePaths(TreeNode root) { 29 | if(root == null) 30 | return res; 31 | dfs(root, ""); 32 | return res; 33 | } 34 | //搜索以node为起始的到叶节点的路径 35 | //cur为到达node前所经历过的路径 36 | public void dfs(TreeNode node, String cur){ 37 | if(node == null) 38 | return; 39 | cur += Integer.toString(node.val); 40 | if(node.left == null && node.right == null){ 41 | res.add(cur); 42 | } 43 | else{ 44 | cur += "->"; 45 | dfs(node.left, cur); 46 | dfs(node.right, cur); 47 | } 48 | } 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /树/450.删除二叉搜索树中的节点.md: -------------------------------------------------------------------------------- 1 | # 450.删除二叉搜索树中的节点 2 | 3 | ## 题目 4 | 给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。 5 | 6 | 7 | 示例: 8 | root = [5,3,6,2,4,null,7] 9 | key = 3 10 | 11 | 5 12 | / \ 13 | 3 6 14 | / \ \ 15 | 2 4 7 16 | 17 | 给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。 18 | 19 | 一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。 20 | 21 | 5 22 | / \ 23 | 4 6 24 | / \ 25 | 2 7 26 | 27 | 另一个正确答案是 [5,2,6,null,4,null,7]。 28 | 29 | 5 30 | / \ 31 | 2 6 32 | \ \ 33 | 4 7 34 | 35 | 36 | ## 方法 37 | 若要在一颗根为root的二叉搜索树中删除值为key的节点,一共有三种情况: 38 | 1. key < root.val 39 | 由于二叉搜索树左子树的节点值都小于根节点,因此值为key的节点一定位于左子树中,我们递归在左子树中删除值为key的节点,并将删除结束后的新的左子树与根节点连接即可。 40 | 2. key > root.val 41 | 由于二叉搜索树右子树的节点值都大于根节点,因此值为key的节点一定位于右子树中,我们递归在右子树中删除值为key的节点,并将删除结束后的新的右子树与根节点连接即可。 42 | 3. key = root.val 43 | 当key与根节点值相等时,意味着要删除的节点正是根节点root自己。于是可以分两种情况讨论: 44 | * 若root的右子树为空,则要想删除root节点,只需让root的左子树替代它的位置即可。 45 | * 若root的右子树不为空,由于右子树中最左的节点即为右子树中值最小的节点,root左子树的所有节点都比这个节点小,因此我们只要找到右子树中这个最左的节点,将root的左子树挂到它下面,成为它的左子树即可。如下图所示:将50的左子树挂到50的右子树的最左的节点60下,成为60节点的左子树 46 | 47 | ![](450.图1.png) 48 | 49 | 50 | ## 代码 51 | ```java 52 | public TreeNode deleteNode(TreeNode root, int key) { 53 | if(root == null) 54 | return null; 55 | //情况一:在root的左子树中删除值为key的节点 56 | if(key < root.val){ 57 | TreeNode L = deleteNode(root.left, key); 58 | root.left = L; 59 | } 60 | //情况二:在root的右子树中删除值为key的节点 61 | else if(key > root.val){ 62 | TreeNode R = deleteNode(root.right, key); 63 | root.right = R; 64 | } 65 | //情况三:删除root节点自身 66 | else{ 67 | if(root.right == null) 68 | return root.left; 69 | else{ 70 | //用cur找到root右子树中最左的节点 71 | TreeNode cur = root.right; 72 | while(cur.left != null) 73 | cur = cur.left; 74 | //将root的左子树挂到这个节点之下 75 | cur.left = root.left; 76 | return root.right; 77 | } 78 | } 79 | return root; 80 | } 81 | ``` -------------------------------------------------------------------------------- /树/450.图1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/树/450.图1.png -------------------------------------------------------------------------------- /树/508.出现次数最多的子树元素和.md: -------------------------------------------------------------------------------- 1 | # 508.出现次数最多的子树元素和 2 | 3 | ## 题目 4 | 5 | 给你一个二叉树的根结点,请你找出出现次数最多的子树元素和。一个结点的「子树元素和」定义为以该结点为根的二叉树上所有结点的元素之和(包括结点本身)。 6 | 7 | 你需要返回出现次数最多的子树元素和。如果有多个元素出现的次数相同,返回所有出现次数最多的子树元素和(不限顺序)。 8 | 9 | 示例 1: 10 | 输入: 11 | 12 | 5 13 | / \ 14 | 2 -3 15 | 返回 [2, -3, 4],所有的值均只出现一次,以任意顺序返回所有值。 16 | 17 | 示例 2: 18 | 输入: 19 | 20 | 5 21 | / \ 22 | 2 -5 23 | 返回 [2],只有 2 出现两次,-5 只出现 1 次。 24 | 25 | ## 方法 26 | 我们用一个哈希表记录每一个可能的和出现的次数。哈希表的key为所有可能的子树元素和的值,value为该值出现的次数。 27 | 28 | 在深度优先遍历树的过程中,不仅要计算各个子树的元素和。还要根据计算出来的和值更新哈希表和最多出现的次数。 29 | 30 | 如果当前和值出现的次数为max,那么将其加入列表res。如果当前和值出现的次数大于max,那么需要先将res清空,再将当前的和值加入。 31 | ## 代码 32 | ```java 33 | class Solution { 34 | //用一个哈希表记录每个和出现的次数 35 | private Map map = new HashMap<>(); 36 | //max为最多出现的次数 37 | private int max = 0; 38 | //用一个列表记录出现max次的和值 39 | private List res = new ArrayList<>(); 40 | public int[] findFrequentTreeSum(TreeNode root) { 41 | if(root == null) 42 | return new int[0]; 43 | valSum(root); 44 | int[] result = new int[res.size()]; 45 | for(int i = 0; i < res.size(); i++) 46 | result[i] = res.get(i); 47 | return result; 48 | } 49 | 50 | //求以root为根节点的树的所有元素的和 51 | public int valSum(TreeNode root){ 52 | if(root == null) 53 | return 0; 54 | int L = valSum(root.left); 55 | int R = valSum(root.right); 56 | int sum = root.val + L + R; 57 | if(!map.containsKey(sum)) 58 | map.put(sum, 1); 59 | else 60 | map.put(sum, map.get(sum) + 1); 61 | if(map.get(sum) > max){ 62 | res.clear(); 63 | res.add(sum); 64 | } 65 | if(map.get(sum) == max) 66 | res.add(sum); 67 | max = Math.max(max, map.get(sum)); 68 | return sum; 69 | } 70 | } 71 | ``` 72 | 73 | * 时间复杂度:O(N) 74 | * 空间复杂度:O(N) 75 |   76 | -------------------------------------------------------------------------------- /树/96.不同的二叉搜索树.md: -------------------------------------------------------------------------------- 1 | # 96 不同的二叉搜索树 2 | 3 | **题目:** 4 | 给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种? 5 | 6 | 示例: 7 | 8 | 输入: 3 9 | 输出: 5 10 | 解释: 11 | 给定 n = 3, 一共有 5 种不同结构的二叉搜索树: 12 | 13 | 1 3 3 2 1 14 | \ / / / \ \ 15 | 3 2 1 1 3 2 16 | / / \ \ 17 | 2 1 2 3 18 | 19 | **思路:** 20 | 采用动态规划的思路求解 21 | 设要求的以1到n为节点组成的二叉搜索树的数目为G(n)。分析可知,1到n的任何一个节点都可以作为根节点,令以节点i为根节点的二叉搜索树的数目为f(i)。于是有: 22 | ```math 23 | G(n)=f(1)+f(2)+f(3)+f(4)+...+f(n) 24 | ``` 25 | 两种特殊情况是n为0和n为1的情况,这两种情况对应的结果都为1。即: 26 | ```math 27 | G(0) = G(1) = 1 28 | ``` 29 | 然而,f(i)也与G函数有关系。f(i)代表的以i节点为根的可以生成的二叉搜索树的个数,等于其左子数个数和右子树个数的乘积。左子数的节点个数为i-1,右子树的节点个数为n-i。而且, G(n)和序列的内容无关,只和序列的长度有关,因此有: 30 | ```math 31 | f(i)=G(i−1)∗G(n−i) 32 | ``` 33 | 综合以上公式,可以得到[卡特兰数](https://baike.baidu.com/item/catalan/7605685?fr=aladdin)公式: 34 | ```math 35 | G(n)=G(0)∗G(n−1)+G(1)∗(n−2)+...+G(n−1)∗G(0) 36 | ``` 37 | **代码:** 38 | ```java 39 | class Solution { 40 | public int numTrees(int n) { 41 | int[] G = new int[n+1]; 42 | G[0] = 1; 43 | G[1] = 1; 44 | for(int i = 2; i <= n; i++) 45 | for(int j = 1; j <= i; j++) 46 | G[i] += G[j - 1] * G[i - j]; 47 | return G[n]; 48 | } 49 | } 50 | ``` 51 | 52 | 53 | G(n)语句的执行次数为: 54 | ```math 55 | \sum_{i=2}^{n} i=\frac{(2+n)(n-1)}{2} 56 | ``` 57 | 因此时间复杂度为O(n^2) 58 | 空间复杂度为O(n) 59 | 60 | 61 | -------------------------------------------------------------------------------- /深度(广度)优先遍历/127.单词接龙.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/深度(广度)优先遍历/127.单词接龙.jpg -------------------------------------------------------------------------------- /深度(广度)优先遍历/127.单词接龙.md: -------------------------------------------------------------------------------- 1 | # 127.单词接龙 2 | 3 | 4 | ## 题目 5 | 给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则: 6 | 7 | 每次转换只能改变一个字母。 8 | 转换过程中的中间单词必须是字典中的单词。 9 | 10 | 说明: 11 | * 如果不存在这样的转换序列,返回 0。 12 | * 所有单词具有相同的长度。 13 | * 所有单词只由小写字母组成。 14 | * 字典中不存在重复的单词。 15 | * 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。 16 | 17 | 18 | 示例: 19 | 20 | 示例 1: 21 | 输入: 22 | beginWord = "hit", 23 | endWord = "cog", 24 | wordList = ["hot","dot","dog","lot","log","cog"] 25 | 输出: 5 26 | 解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",返回它的长度 5。 27 | 28 | 示例 2: 29 | 输入: 30 | beginWord = "hit" 31 | endWord = "cog" 32 | wordList = ["hot","dot","dog","lot","log"] 33 | 输出: 0 34 | 解释: endWord "cog" 不在字典中,所以无法进行转换。 35 | 36 | 37 | ## 方法(广度优先遍历) 38 | 我们可以把beginWord、endWord还有wordList中的所有单词想象成一张无向图中的节点。如果两个单词只有一个字符不同,则这两个节点相连。 39 | 40 | ![](127.单词接龙.jpg) 41 | 42 | 因此题意就变为了:在beginWord到endWord两个节点间,找到一条最短路径。我们由“最短”想到了广度优先遍历。 43 | 44 | 要进行BFS,我们需要两个复制结构: 45 | * 一个队列 46 | * 一个visited数组(或集合),记录一个节点有没有被BFS遍历过,以避免重复遍历造成在图中无限循环。 47 | 48 | 注意:对于树的BFS只需要一个队列即可,因为树从上到下的结构决定了不会在其中重复遍历。 49 | 50 | 51 | ## 代码 52 | ```java 53 | public int ladderLength(String beginWord, String endWord, List wordList) { 54 | //将wordList中的单词加入HashSet,以便我们在之后快速查询一个单词是否在字典中 55 | Set set = new HashSet<>(wordList); 56 | //如果字典中有beginWord,先要把它删掉 57 | if(set.contains(beginWord)) 58 | set.remove(beginWord); 59 | Queue queue = new LinkedList<>(); 60 | //uesd集合记录哪些字符串已经在BFS中被遍历过 61 | Set used = new HashSet<>(); 62 | queue.add(beginWord); 63 | used.add(beginWord); 64 | int step = 0; 65 | //进入BFS流程 66 | while(!queue.isEmpty()){ 67 | int size = queue.size(); 68 | step++; 69 | for(int i = 0; i < size; i++){ 70 | String str = queue.poll(); 71 | //如果找到了endWord,直接返回 72 | if(str.equals(endWord)) 73 | return step; 74 | //依次尝试将str中的每一个字符修改为a到z中的一个,看看字典中有没有相应字符 75 | for(int j = 0; j < str.length(); j++){ 76 | for(int k = 'a'; k <= 'z'; k++){ 77 | char[] arr = str.toCharArray(); 78 | arr[j] = (char)k; 79 | String new_str = new String(arr); 80 | if(set.contains(new_str) && !used.contains(new_str)){ 81 | queue.add(new_str); 82 | used.add(new_str); 83 | } 84 | } 85 | } 86 | } 87 | } 88 | return 0; 89 | } 90 | ``` -------------------------------------------------------------------------------- /深度(广度)优先遍历/130.被围绕的区域.md: -------------------------------------------------------------------------------- 1 | # 130 被围绕的区域 2 | 3 | 4 | ## 题目 5 | 给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。 6 | 7 | 找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 8 | 9 | 示例: 10 | X X X X 11 | X O O X 12 | X X O X 13 | X O X X 14 | 15 | 运行你的函数后,矩阵变为: 16 | X X X X 17 | X X X X 18 | X X X X 19 | X O X X 20 | 21 | 解释: 22 | 被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 23 | 24 | ## 方法(DFS) 25 | 据题意,我们需要将矩阵中的中的'O'分为以下两种: 26 | * 边界上的'O'和与边界上相邻的'O' 27 | * 被'X'围绕的'O' 28 | 29 | 对于后者,我们需要将其改为'X'。而前者则无需改动。 30 | 31 | 先写一个深度优先遍历的函数,暂且叫它感染函数。它能实现将所有与当前位置相连的'O'修改成'#'。 32 | 所以第一步,我们先处理矩阵的四个边界,若在遍历边界的时候遇到'O',则进入感染过程,将与边界上的'O'相连的'O'全都改写成'#',以将上述两种'O'区别开来。 33 | 34 | 第二步,我们遍历整个矩阵,若遇到'O',则这个'O'是被围绕的'O',于是将其改为'X'。若遇到'#',则代表其是边界上的'O',于是将其改为'O'。 35 | ## 代码 36 | ```java 37 | public void solve(char[][] board) { 38 | if(board == null || board.length == 0) 39 | return; 40 | //第一步:处理边界 41 | int rows = board.length; 42 | int cols = board[0].length; 43 | for(int j = 0; j < cols ; j++){ 44 | if(board[0][j] == 'O') 45 | infect(board, 0, j); 46 | } 47 | for(int j = 0; j < cols ; j++){ 48 | if(board[rows - 1][j] == 'O') 49 | infect(board, rows - 1, j); 50 | } 51 | for(int i = 0; i < rows ; i++){ 52 | if(board[i][0] == 'O') 53 | infect(board, i, 0); 54 | } 55 | for(int i = 0; i < rows; i++){ 56 | if(board[i][cols - 1] == 'O') 57 | infect(board, i, cols - 1); 58 | } 59 | //第二步:遍历矩阵 60 | for(int i = 0; i < board.length; i++){ 61 | for(int j = 0; j < board[0].length; j++){ 62 | if(board[i][j] == 'O') 63 | board[i][j] = 'X'; 64 | if(board[i][j] == '#') 65 | board[i][j] = 'O'; 66 | } 67 | } 68 | } 69 | 70 | public void infect(char[][] board, int i, int j){ 71 | if(i < 0 || j < 0 || i >= board.length || j >= board[0].length || board[i][j] == 'X' || board[i][j] == '#') 72 | return; 73 | board[i][j] = '#'; 74 | infect(board, i - 1, j); 75 | infect(board, i + 1, j); 76 | infect(board, i, j - 1); 77 | infect(board, i, j + 1); 78 | } 79 | ``` 80 | -------------------------------------------------------------------------------- /深度(广度)优先遍历/133 克隆图.md: -------------------------------------------------------------------------------- 1 | # 133 克隆图 2 | 3 | ## 题目 4 | 给你无向连通图中一个节点的引用,请你返回该图的深拷贝(克隆)。 5 | 6 | 图中的每个节点都包含它的值 val(int) 和其邻居的列表(list[Node])。 7 | 8 | class Node { 9 | public int val; 10 | public List neighbors; 11 | } 12 | 13 | 邻接列表 是用于表示有限图的无序列表的集合。每个列表都描述了图中节点的邻居集。 14 | 15 | 给定节点将始终是图中的第一个节点(值为 1)。你必须将 给定节点的拷贝 作为对克隆图的引用返回。 16 | 17 | ## 方法一(DFS) 18 | 深拷贝一个图的意思为:要重新构建一张新的图,其中所有的节点都需要new出来,图的值和结构都和原图一样。 19 | 20 | 根据给定的这个节点,可以通过深度优先遍历走遍图中的所有节点。每到一个节点,先判断其是否访问过。 21 | * 若这个节点已经访问过,则直接返回它的拷贝节点。 22 | * 若这个节点还未访问过,就根据这个节点new出一个新的拷贝节点。先给这个新节点赋值,再填充它的邻接列表。 23 | 24 | ```java 25 | class Solution { 26 | //map的key为原图中的节点,value为根据它拷贝出的新节点 27 | Map map = new HashMap<>(); 28 | //此递归函数会返回node的深拷贝节点 29 | public Node cloneGraph(Node node) { 30 | //递归结束条件 31 | if(node == null) 32 | return null; 33 | //若这个节点已经访问过,则直接返回它的拷贝节点。 34 | if(map.containsKey(node)) 35 | return map.get(node); 36 | //若这个节点还未访问过,就根据这个节点构造一个新的拷贝节点 37 | Node newNode = new Node(node.val); 38 | //这时已经访问完了节点node,因此要更新map 39 | map.put(node, newNode); 40 | //根据node的邻接列表,填充新节点newNode的邻接列表 41 | for(Node neighbor: node.neighbors){ 42 | newNode.neighbors.add(cloneGraph(neighbor)); 43 | } 44 | return newNode; 45 | } 46 | } 47 | ``` 48 | 49 | * 时间复杂度:O(N) 每个节点只会被访问一次 50 | * 空间复杂度:O(N) 哈希表需要O(N)的空间,递归使用的栈深度需要O(H)的空间(H为图的深度)。总空间复杂度为O(N) 51 | 52 | ## 方法二(BFS) 53 | ```java 54 | class Solution { 55 | public Node cloneGraph(Node node) { 56 | if(node == null) 57 | return null; 58 | //map的key为原图中的节点,value为根据它拷贝出的新节点 59 | Map map = new HashMap<>(); 60 | Queue queue = new LinkedList<>(); 61 | //先将node节点入队列 62 | queue.add(node); 63 | map.put(node, new Node(node.val)); 64 | //通过BFS遍历到所有节点,在遍历的过程中更新map 65 | while(!queue.isEmpty()){ 66 | Node temp = queue.poll(); 67 | for(Node neighbor: temp.neighbors){ 68 | //如果此节点未被遍历过,一方面正常的走BFS的流程,将其入队列 69 | //另一方面构造它的拷贝节点,填充拷贝节点的值,并更新map 70 | if(!map.containsKey(neighbor)){ 71 | map.put(neighbor, new Node(neighbor.val)); 72 | queue.add(neighbor); 73 | } 74 | //填充该节点neighbor对应的拷贝节点neighbor'的邻接列表 75 | //在邻接链表中加入temp'和neighbor'的邻接关系 76 | map.get(neighbor).neighbors.add(map.get(temp)); 77 | } 78 | } 79 | //至此,所有新节点的值和邻接列表都已填充好,可以返回。 80 | return map.get(node); 81 | } 82 | } 83 | ``` 84 | 85 | * 时间复杂度:O(N) 每个节点只会被访问一次 86 | * 空间复杂度:O(N) 哈希表需要O(N)的空间,BFS所用的队列最多需要O(N)的空间,总空间复杂度为O(N) 87 | -------------------------------------------------------------------------------- /深度(广度)优先遍历/200.岛屿数量.md: -------------------------------------------------------------------------------- 1 | # 200.岛屿数量 2 | 3 | ## 题目 4 | 给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 5 | 6 | 岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。 7 | 8 | 此外,你可以假设该网格的四条边均被水包围。 9 | 10 | 示例 1: 11 | 输入: 12 | [ 13 | ['1','1','1','1','0'], 14 | ['1','1','0','1','0'], 15 | ['1','1','0','0','0'], 16 | ['0','0','0','0','0'] 17 | ] 18 | 输出: 1 19 | 20 | 示例 2: 21 | 输入: 22 | [ 23 | ['1','1','0','0','0'], 24 | ['1','1','0','0','0'], 25 | ['0','0','1','0','0'], 26 | ['0','0','0','1','1'] 27 | ] 28 | 输出: 3 29 | 解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。 30 | 31 | ## 方法 32 | 每遍历矩阵中的一个值,进入到感染函数中去,感染函数会把连在一块的1全部变成2。感染过程完成后,岛数量加1(初始为0)。之后遍历过程中如果是2或0直接跳过,直到再遇到1时进入感染过程。 33 | 34 | ## 代码 35 | ```java 36 | public int numIslands(char[][] m) { 37 | if (m == null || m.length == 0) { 38 | return 0; 39 | } 40 | int N = m.length; 41 | int M = m[0].length; 42 | int res = 0; 43 | for (int i = 0; i < N; i++) { 44 | for (int j = 0; j < M; j++) { 45 | if (m[i][j] == '1') { 46 | res++; 47 | infect(m, i, j, N, M); 48 | } 49 | } 50 | } 51 | return res; 52 | } 53 | 54 | public static void infect(char[][] m, int i, int j, int N, int M) { 55 | //只当一个位置为1时才进行感染过程 56 | if (i < 0 || i >= N || j < 0 || j >= M || m[i][j] != '1') { 57 | return; 58 | } 59 | m[i][j] = '2'; 60 | infect(m, i + 1, j, N, M); 61 | infect(m, i - 1, j, N, M); 62 | infect(m, i, j + 1, N, M); 63 | infect(m, i, j - 1, N, M); 64 | } 65 | ``` 66 | -------------------------------------------------------------------------------- /深度(广度)优先遍历/515.在每个树行中找最大值.md: -------------------------------------------------------------------------------- 1 | # 515.在每个树行中找最大值 2 | 3 | ## 题目 4 | 您需要在二叉树的每一行中找到最大的值。 5 | 6 | 7 | 示例: 8 | 输入: 9 | 10 | 1 11 | / \ 12 | 3 2 13 | / \ \ 14 | 5 3 9 15 | 16 | 输出: [1, 3, 9] 17 | 18 | ## 方法一(BFS) 19 | 在广度优先遍历的时候用变量size记录每一层的节点个数,在遍历这一层时找到此层中的最大值,添加到结果数组中即可。 20 | 21 | ```java 22 | public List largestValues(TreeNode root) { 23 | List res = new ArrayList<>(); 24 | if(root == null) 25 | return res; 26 | Queue queue = new LinkedList<>(); 27 | queue.add(root); 28 | //while循环代表整个BFS的逻辑 29 | while(!queue.isEmpty()){ 30 | //当前层节点数量 31 | int size = queue.size(); 32 | int max = Integer.MIN_VALUE; 33 | //for循环表示每一层的遍历 34 | for(int i = 0 ; i < size; i++){ 35 | TreeNode node = queue.poll(); 36 | max = Math.max(max, node.val); 37 | if(node.left != null) 38 | queue.add(node.left); 39 | if(node.right != null) 40 | queue.add(node.right); 41 | } 42 | res.add(max); 43 | } 44 | return res; 45 | } 46 | ``` 47 | 48 | ## 方法二(DFS) 49 | 在进行深度优先遍历时,每次遍历到新的一层时,我们都将现在遍历到的这个该层节点加入到结果数组res中(但其实上这个节点并不一定是该层最大的,不过没关系,我们之后再更新它)。之后再遍历到同在这一层的其他节点时再进行更新 50 | ```java 51 | class Solution { 52 | List res = new ArrayList<>(); 53 | public List largestValues(TreeNode root) { 54 | dfs(root, 1); 55 | return res; 56 | } 57 | public void dfs(TreeNode root, int level){ 58 | if(root == null) 59 | return; 60 | //如果是第一次来到第level层,那就先把当前遍历到达level层的这个节点加入到结果数组res中 61 | if(level == res.size() + 1) 62 | res.add(root.val); 63 | //之后再遍历到同在level层的其他节点时对res中的值进行更新 64 | else{ 65 | int max = Math.max(res.get(level - 1), root.val); 66 | res.set(level - 1, max); 67 | } 68 | dfs(root.left, level + 1); 69 | dfs(root.right, level + 1); 70 | } 71 | } 72 | ``` -------------------------------------------------------------------------------- /深度(广度)优先遍历/542.01矩阵.md: -------------------------------------------------------------------------------- 1 | # 542.01矩阵 2 | 3 | ## 题目 4 | 给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。 5 | 6 | 两个相邻元素间的距离为 1 。 7 | 8 | 示例 1: 9 | 输入: 10 | 0 0 0 11 | 0 1 0 12 | 0 0 0 13 | 14 | 输出: 15 | 0 0 0 16 | 0 1 0 17 | 0 0 0 18 | 19 | 示例 2: 20 | 输入: 21 | 0 0 0 22 | 0 1 0 23 | 1 1 1 24 | 25 | 输出: 26 | 0 0 0 27 | 0 1 0 28 | 1 2 1 29 | 30 | ## 方法(BFS) 31 | 对于二叉树的BFS,都是单源BFS。 32 | 而对于图的BFS,大多是多源BFS 33 | 34 | 因为二叉树只有一个根节点,先把根节点入队,再一层层遍历即可。而图可以有许多源点,因此需要先把这些源点都入队,再一层层遍历。 35 | 36 | 对于本题来说,先将所有的0(源点)入队列,再从0开始一层一层向周围未遍历到的1扩散。最后将矩阵中所有的点都遍历到。 37 | 38 | 注意:树是有向的,因此无需标记一个节点是否被访问过。而对于无向图来说,为了防止一个节点多次入队,需要在访问它之后将它标记为已访问。(对于本题来说,将其标记为非-1) 39 | 40 | 算法步骤: 41 | * 先遍历一遍矩阵,将0出现的位置索引入队列,并将所有的1置为-1,表明这是还没被访问过的1. 42 | * 用dx和dy两个矩阵来表示向上下左右四个位置的移动 43 | * 移动到没有访问过的1时,将其入队列,并更新matrix[newX][newY](本来为-1)为matrix[x][y] + 1。 44 | 45 | ## 代码 46 | ```java 47 | public int[][] updateMatrix(int[][] matrix) { 48 | Queue queue = new LinkedList<>(); 49 | int m = matrix.length, n = matrix[0].length; 50 | //先遍历一次矩阵,将0出现的位置索引入队列 51 | //并把1的位置设置为-1,表示这是还没被访问的1 52 | for(int i = 0; i < m; i++){ 53 | for(int j = 0; j < n; j++){ 54 | if(matrix[i][j] == 0) 55 | queue.offer(new int[] {i, j}); 56 | else 57 | matrix[i][j] = -1; 58 | } 59 | } 60 | int[] dx = new int[]{-1, 1, 0, 0}; 61 | int[] dy = new int[]{0, 0, -1, 1}; 62 | while(!queue.isEmpty()){ 63 | int[] node = queue.poll(); 64 | int x = node[0], y = node[1]; 65 | for(int i = 0; i < 4; i++){ 66 | //newX和newY为附近邻居的位置索引 67 | int newX = x + dx[i]; 68 | int newY = y + dy[i]; 69 | //如果邻居值为-1,说明它是还没被访问过的1. 70 | //则这个点到0的距离就可以更新为matrix[x][y] + 1,注意:这步更新完之后这个点就有实际值而不是-1了,相当于被标记了。 71 | //当遇到被标记过(访问过)的点时,不将其入队列 72 | if(newX >= 0 && newX < m && newY >= 0 && newY < n && matrix[newX][newY] == -1){ 73 | matrix[newX][newY] = matrix[x][y] + 1; 74 | queue.offer(new int[] {newX, newY}); 75 | } 76 | } 77 | } 78 | return matrix; 79 | } 80 | ``` 81 | 题目[1162. 地图分析](https://leetcode-cn.com/problems/as-far-from-land-as-possible/)和此题目类似 82 | 83 | ## 参考 84 | * [Sweetiee的leetcode题解](https://leetcode-cn.com/problems/01-matrix/solution/2chong-bfs-xiang-jie-dp-bi-xu-miao-dong-by-sweetie/) -------------------------------------------------------------------------------- /滑动窗口/209.长度最小的子数组.md: -------------------------------------------------------------------------------- 1 | # 209 长度最小的子数组 2 | 3 | **题目:** 4 | 给定一个含有n个正整数的数组和一个正整数s ,找出该数组中满足其和 ≥ s 的长度最小的连续子数组,并返回其长度。如果不存在符合条件的连续子数组,返回 0。 5 | 6 | 示例: 7 | 输入: s = 7, nums = [2,3,1,2,4,3] 8 | 输出: 2 9 | 解释: 子数组[4,3]是该条件下的长度最小的连续子数组。 10 | 11 | **思路:滑动窗口法** 12 | 当输出或比较的结果在原数据结构中是连续排列的时候,可以使用滑动窗口算法求解。 13 | 将两个指针比作一个窗口,通过移动指针的位置改变窗口的大小,观察窗口中的元素是否符合题意。 14 | 15 | 初始窗口中只有数组开头一个元素。 16 | 当窗口中的元素小于目标值,右指针向右移,扩大窗口。 17 | 当窗口中的元素大于目标值,比较当前窗口大小是否为最小值,左指针向右移,缩小窗口。 18 | 19 | **代码:** 20 | ```java 21 | class Solution { 22 | public int minSubArrayLen(int s, int[] nums) { 23 | if(nums == null) 24 | return 0; 25 | int minlength = Integer.MAX_VALUE; 26 | //left和right分别代表滑动窗口的左右端 27 | int left = 0; 28 | int right = 0; 29 | int sum = 0; 30 | while(right < nums.length){ 31 | sum += nums[right]; 32 | right++; 33 | //和sum大于目标值s时,left左移,滑动窗口缩小 34 | while(sum >= s){ 35 | minlength = Math.min(minlength, right - left ); 36 | sum -= nums[left]; 37 | left++; 38 | } 39 | } 40 | return minlength == Integer.MAX_VALUE? 0:minlength; 41 | } 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /滑动窗口/219.存在重复元素II.md: -------------------------------------------------------------------------------- 1 | # 219.存在重复元素II 2 | 3 | ## 题目: 4 | 给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。 5 | 6 | 示例 1: 7 | 输入: nums = [1,2,3,1], k = 3 8 | 输出: true 9 | 10 | 示例 2: 11 | 输入: nums = [1,0,1,1], k = 1 12 | 输出: true 13 | 14 | 示例 3: 15 | 输入: nums = [1,2,3,1,2,3], k = 2 16 | 输出: false 17 | 18 | ## 方法 19 | 维护一个最多包含k个元素的哈希set,让它起到一个滑动窗口的作用 20 | 21 | 从头到尾遍历数组: 22 | * 如果当前元素存在于set中,则说明在距当前元素k步的范围内存在重复元素,直接返回true 23 | * 如果当前元素不在set中,则将其加入set,如果加入后set大小大于k,则移除当前set中最早进来的数字,保证其大小始终不大于k。 24 | 25 | 时间复杂度:O(n) 26 | 空间复杂度:O(K) 27 | 28 | ## 代码 29 | ```java 30 | public boolean containsNearbyDuplicate(int[] nums, int k) { 31 | HashSet set = new HashSet<>(); 32 | for(int i = 0; i < nums.length; i++){ 33 | if(set.contains(nums[i])) 34 | return true; 35 | else{ 36 | set.add(nums[i]); 37 | if(set.size() > k) 38 | set.remove(nums[i - k]); 39 | } 40 | } 41 | return false; 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /滑动窗口/3.无重复字符的最长子串.md: -------------------------------------------------------------------------------- 1 | # 3 无重复字符的最长子串 2 | **题目:** 3 | 给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。 4 | 5 | 示例: 6 | 输入: "abcabcbb" 7 | 输出: 3 8 | 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 9 | 10 | ## 思路:滑动窗口法 11 | 想象一个窗口,当abc进入窗口后,无重复字符,目前最长子串长度为3.当后一个a进入后,变成了abca出现了重复字符a。所以要将窗口进行滑动,以满足要求。如何滑动?将队列左边的元素移除即可。一直维持这样的队列,直到找出最长长度。 12 | 13 | **代码:** 14 | ```java 15 | public int lengthOfLongestSubstring(String s) { 16 | if(s.length() == 0) 17 | return 0; 18 | //哈希表的value值为key值上一次出现过的位置(s中的索引) 19 | HashMap map = new HashMap(); 20 | //max用来储存不含重复字符的最长子串长度 21 | int max = 0; 22 | //left用来储存滑动窗口的左端 23 | int left = 0; 24 | for(int i = 0; i < s.length(); i++){ 25 | //如果map中出现过s[i],则再将s[i]加入时会出现重复字符,因此需要进行窗口滑动,即更新left 26 | if(map.containsKey(s.charAt(i))) 27 | //将left更新为之前出现过的与s[i]相同的字符的下一个位置,即将那个相同字符滑出窗口 28 | //例子abba可以说明为何需要和left取最大,当填入第二个a时,left在索引为2处,但第1个a的下一个位置为索引为1处。 29 | left = Math.max(left, map.get(s.charAt(i))+ 1); 30 | //不管s[i]是否出现过,都将它put进哈希表。 31 | //如果map中曾经有s[i],这一操作相当于把其value值更新 32 | map.put(s.charAt(i),i); 33 | //更新最长长度max值 34 | max = Math.max(max,i-left+1); 35 | } 36 | return max; 37 | } 38 | ``` 39 | 40 | ### [滑动窗口法题目汇总](https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/solution/hua-dong-chuang-kou-by-powcai/) 41 | -------------------------------------------------------------------------------- /滑动窗口/424. 替换后的最长重复字符.md: -------------------------------------------------------------------------------- 1 | 424. 替换后的最长重复字符 2 | 3 | ## 题目 4 | 给你一个仅由大写英文字母组成的字符串,你可以将任意位置上的字符替换成另外的字符,总共可最多替换 k 次。在执行上述操作后,找到包含重复字母的最长子串的长度。 5 | 6 | 示例 1: 7 | 输入:s = "ABAB", k = 2 8 | 输出:4 9 | 解释:用两个'A'替换为两个'B',反之亦然。 10 | 11 | 示例 2: 12 | 输入:s = "AABABBA", k = 1 13 | 输出:4 14 | 解释: 15 | 将中间的一个'A'替换为'B',字符串变为 "AABBBBA"。 16 | 子串 "BBBB" 有最长重复字母, 答案为 4。 17 | 18 | 19 | ## 方法一(滑动窗口) 20 | 要判断能否将一个子串替换k次以得到一个只包含重复字母的子串,只需要判断该子串中除了出现最多次的那个字符外的其他字符出现的总次数是否大于k即可。大于k则不可,小于k则可。 21 | 22 | 用left和right这两个指针分别指向滑动窗口的左端和右端。 23 | 24 | right不断向右移动,每次都在当前窗口后再添加一个新的字符,看看窗口是否还能继续满足要求。 25 | * 如果窗口内除出现次数最多的字符外的其他字符出现的总次数大于k了,则该窗口不能满足要求了,left要向右移动。 26 | * 否则,该窗口满足要求,用当前窗口的长度更新最大值res 27 | 28 | ```java 29 | public int characterReplacement(String s, int k) { 30 | //用桶来记录当前窗口中的字符以及其出现的次数 31 | int[] buckets = new int[26]; 32 | int res = 0; 33 | //left和right分别记录滑动窗口的两端 34 | int left = 0; 35 | int right = 0; 36 | int maxCount = 0; //maxCount记录当前窗口内出现最多的那个字符出现的次数 37 | while(right < s.length()){ 38 | //无论是左端还是右端,只要窗口移动了,就要更新maxCount 39 | buckets[s.charAt(right) - 'A']++; 40 | maxCount = Math.max(maxCount, buckets[s.charAt(right) - 'A']); 41 | while(right - left + 1 - maxCount > k){ 42 | buckets[s.charAt(left) - 'A']--; 43 | left++; 44 | for(int i = 0; i < 26; i++) 45 | maxCount = Math.max(maxCount, buckets[i]); 46 | } 47 | res = Math.max(res, right - left + 1); 48 | right++; 49 | } 50 | return res; 51 | } 52 | ``` 53 | * 时间复杂度:O(26n) 54 | * 空间复杂度:O(1) 55 | 56 | ## 方法二(改进) 57 | 58 | 在方法一中,窗口左端点left每移动一次,就要遍历一次桶以更新maxCount,效率不高。 59 | 60 | 因此我们采取如下改进方法: 61 | 62 | 对于滑动窗口,只允许两种操作: 63 | * 扩展:right右移,left不动 64 | * 平移:right和left同时右移 65 | 66 | 因为题目让我们求的是长度,而不是具体的子串。因此我们可以用滑动窗口的窗口大小来记录答案。 67 | * 右移时,窗口大小加1 68 | * 平移时,窗口大小不变 69 | 70 | 还是像方法一一样,right不断右移,每次尝试着将一个新的字符加入窗口。如果加入后窗口符合要求,那么扩展窗口。如果加入后窗口不符合要求,那么平移窗口。因为窗口的大小永远不可能减小,所以遍历结束后,当前窗口大小就是符合条件的最大窗口大小。 71 | 72 | ```java 73 | public int characterReplacement(String s, int k) { 74 | int[] buckets = new int[26]; 75 | int left = 0; 76 | int right = 0; 77 | int maxCount = 0; 78 | while(right < s.length()){ 79 | buckets[s.charAt(right) - 'A']++; 80 | maxCount = Math.max(maxCount, buckets[s.charAt(right) - 'A']); 81 | if(right - left + 1 - maxCount > k){ 82 | buckets[s.charAt(left) - 'A']--; 83 | left++; 84 | } 85 | right++; 86 | } 87 | return right - left; 88 | } 89 | ``` 90 | 91 | * 时间复杂度:O(n) 92 | * 空间复杂度:O(1) -------------------------------------------------------------------------------- /滑动窗口/438. 找到字符串中所有字母异位词.md: -------------------------------------------------------------------------------- 1 | # 438. 找到字符串中所有字母异位词 2 | 3 | ## 题目 4 | 给定一个字符串 s 和一个非空字符串 p,找到 s 中所有是 p 的字母异位词的子串,返回这些子串的起始索引。 5 | 6 | 字符串只包含小写英文字母,并且字符串 s 和 p 的长度都不超过 20100。 7 | 8 | 说明: 9 | * 字母异位词指字母相同,但排列不同的字符串。 10 | * 不考虑答案输出的顺序。 11 | 12 | **示例** 13 | 14 | 示例 1: 15 | 输入: 16 | s: "cbaebabacd" p: "abc" 17 | 输出: 18 | [0, 6] 19 | 解释: 20 | 起始索引等于 0 的子串是 "cba", 它是 "abc" 的字母异位词。 21 | 起始索引等于 6 的子串是 "bac", 它是 "abc" 的字母异位词。 22 | 23 | 示例 2: 24 | 输入: 25 | s: "abab" p: "ab" 26 | 输出: 27 | [0, 1, 2] 28 | 解释: 29 | 起始索引等于 0 的子串是 "ab", 它是 "ab" 的字母异位词。 30 | 起始索引等于 1 的子串是 "ba", 它是 "ab" 的字母异位词。 31 | 起始索引等于 2 的子串是 "ab", 它是 "ab" 的字母异位词。 32 | 33 | 34 | ## 方法(滑动窗口) 35 | 因为s和p中全部都是小写字符,因此我们可以分别用大小为26的桶来记录字符串中出现的字符及其出现的次数 36 | 37 | * 先将p中的字符全部入桶bucketP。 38 | * 用left和right分别记录s中滑动窗口的左右端点,准备一个桶bucketS记录此滑动窗口中的字符及其出现次数 39 | * right不断向右移动,每移动一次都将一个新的字符加入到桶bucketS中。 40 | * 当桶bucketS中元素种类或个数比桶bucketP多时,此时right再向右只会让二者差的更多,不可能找到字母异位词。因此这时应该将滑动窗口的左端向右移动,将一些字符踢出桶,直到桶bucketS和桶bucketP中元素相同为止。 41 | * 当桶bucketS中元素种类或个数与桶bucketP完全相同时,只需要判断窗口的长度和p是否相同,就可判断是否找到字母异位词 42 | 43 | ## 代码 44 | ```java 45 | public List findAnagrams(String s, String p) { 46 | //corner case 47 | List res = new LinkedList<>(); 48 | if(s == null || s.length() < p.length()) 49 | return res; 50 | int[] bucketP = new int[26]; 51 | for(int i = 0; i < p.length(); i++) 52 | bucketP[p.charAt(i) - 'a']++; 53 | int[] bucketS = new int[26]; 54 | //left和right分别为滑动窗口的左右端点 55 | int left = 0; 56 | int right = 0; 57 | while(right < s.length()){ 58 | int curR = s.charAt(right) - 'a'; 59 | bucketS[curR]++; 60 | right++; 61 | while(bucketS[curR] > bucketP[curR]){ 62 | int curL = s.charAt(left) - 'a'; 63 | bucketS[curL]--; 64 | left++; 65 | } 66 | if(right - left == p.length()) 67 | res.add(left); 68 | } 69 | return res; 70 | } 71 | ``` -------------------------------------------------------------------------------- /系统设计/146.java: -------------------------------------------------------------------------------- 1 | class LRUCache { 2 | //哈希表的键为key,值为key对应的Node节点(包含key和value) 3 | private HashMap map; 4 | private DoubleList cache; 5 | private int capacity; 6 | 7 | public LRUCache(int capacity){ 8 | this.capacity = capacity; 9 | map = new HashMap<>(); 10 | cache = new DoubleList(); 11 | } 12 | 13 | public int get(int key){ 14 | if(!map.containsKey(key)) 15 | return -1; 16 | int value = map.get(key).value; 17 | put(key, value); 18 | return value; 19 | } 20 | 21 | public void put(int key, int value){ 22 | Node x = new Node(key, value); 23 | //如果结构中本来有此key,则删除掉旧的节点,并把新的插到头部,然后更新map 24 | if(map.containsKey(key)){ 25 | cache.remove(map.get(key)); 26 | cache.addFirst(x); 27 | map.put(key, x); 28 | } 29 | //如果结构中原来没有此key 30 | else{ 31 | //如果容量已满,那么需要先删除尾部的节点,然后再将x添加到头部,同时更新map 32 | if(capacity == cache.size()){ 33 | Node last = cache.removeLast(); 34 | map.remove(last.key); 35 | } 36 | cache.addFirst(x); 37 | map.put(key, x); 38 | } 39 | } 40 | 41 | class Node{ 42 | private int key, value; 43 | private Node next, pre; 44 | public Node(int k, int v){ 45 | this.key = k; 46 | this.value = v; 47 | } 48 | } 49 | 50 | class DoubleList{ 51 | private Node head, tail; 52 | private int size; 53 | 54 | //在双向链表头部添加节点x 55 | public void addFirst(Node x){ 56 | //先处理向空链表中加入节点的情况 57 | if(head == null) 58 | head = tail = x; 59 | else { 60 | x.next = head; 61 | head.pre = x; 62 | head = x; 63 | } 64 | size++; 65 | } 66 | //删除链表中的x节点 67 | public void remove(Node x){ 68 | if(x == head && x == tail){ 69 | head = null; 70 | tail = null; 71 | } 72 | else if(x == tail){ 73 | x.pre.next = null; 74 | tail = x.pre; 75 | } 76 | else if(x == head){ 77 | x.next.pre = null; 78 | head = x.next; 79 | } 80 | else { 81 | x.pre.next = x.next; 82 | x.next.pre = x.pre; 83 | } 84 | size--; 85 | } 86 | //删除并返回双向链表的最后一个节点 87 | public Node removeLast(){ 88 | Node node = tail; 89 | remove(tail); 90 | return node; 91 | } 92 | //返回双向链表的长度 93 | public int size(){ 94 | return size; 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /系统设计/146.图1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/系统设计/146.图1.jpg -------------------------------------------------------------------------------- /系统设计/173.二叉搜索树迭代器.md: -------------------------------------------------------------------------------- 1 | # 173.二叉搜索树迭代器 2 | 3 | ## 题目 4 | 实现一个二叉搜索树迭代器。你将使用二叉搜索树的根节点初始化迭代器。 5 | 6 | 调用 next() 将返回二叉搜索树中的下一个最小的数。 7 | 8 | 示例: 9 | 10 | 7 11 | / \ 12 | 3 15 13 | / \ 14 | 9 20 15 | BSTIterator iterator = new BSTIterator(root); 16 | iterator.next(); // 返回 3 17 | iterator.next(); // 返回 7 18 | iterator.hasNext(); // 返回 true 19 | iterator.next(); // 返回 9 20 | iterator.hasNext(); // 返回 true 21 | iterator.next(); // 返回 15 22 | iterator.hasNext(); // 返回 true 23 | iterator.next(); // 返回 20 24 | iterator.hasNext(); // 返回 false 25 | 26 | 要求: 27 | * next() 和 hasNext() 操作的时间复杂度是 O(1),并使用 O(h) 内存,其中 h 是树的高度。 28 | * 你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 中至少存在一个下一个最小的数。 29 | 30 | 31 | ## 方法一(用队列) 32 | 我们知道对于二叉搜索树来说,其中序遍历的结果是一个升序序列。因此,我们先中序遍历二叉搜索树,将树中元素的升序排列存放在一个队列中。每当调用 next() 时,我们就从队列中弹出一个元素(升序保证它是最小的),每当调用hasNext() 时,我们只需检查队列中还有没有剩余元素即可。 33 | ```java 34 | class BSTIterator { 35 | private Queue queue; 36 | public BSTIterator(TreeNode root) { 37 | queue = new LinkedList<>(); 38 | dfs(root); 39 | } 40 | private void dfs(TreeNode root){ 41 | if(root == null) 42 | return; 43 | dfs(root.left); 44 | queue.add(root.val); 45 | dfs(root.right); 46 | } 47 | 48 | /** @return the next smallest number */ 49 | public int next() { 50 | return queue.poll(); 51 | } 52 | 53 | /** @return whether we have a next smallest number */ 54 | public boolean hasNext() { 55 | return !queue.isEmpty(); 56 | } 57 | } 58 | ``` 59 | 对于这种方法来说,next和hasNext函数的时间复杂度都为O(1),符合题目要求。但是需要用O(n)辅助空间的队列来存储元素,不符合题目中空间复杂度为O(h)的要求(h为树高度) 60 | ## 方法二(用栈) 61 | 对于二叉搜索树来说,其最左的那个元素就是树中最小的元素。因此,我们先将根节点root及其左子树的所有节点依次入栈,这样栈顶就是树中最左的元素,也即树中最小的元素。 62 | 63 | 每当调用next(),返回栈顶元素(树中最小元素),并将其右子节点及右子节点左子树的所有节点入栈,以便之后的next()操作。 64 | 65 | 每当调用hasNext()时,我们只需检查队列中还有没有剩余元素即可。 66 | 67 | ```java 68 | class BSTIterator { 69 | private Stack stack; 70 | public BSTIterator(TreeNode root) { 71 | stack = new Stack<>(); 72 | leftInorder(root); 73 | } 74 | //将root及其子树的所有节点入栈 75 | private void leftInorder(TreeNode root){ 76 | while(root != null){ 77 | stack.push(root); 78 | root = root.left; 79 | } 80 | } 81 | 82 | /** @return the next smallest number */ 83 | public int next() { 84 | TreeNode mostleftNode = stack.pop(); 85 | if(mostleftNode.right != null) 86 | leftInorder(mostleftNode.right); 87 | return mostleftNode.val; 88 | } 89 | 90 | /** @return whether we have a next smallest number */ 91 | public boolean hasNext() { 92 | return stack.size() > 0; 93 | } 94 | } 95 | ``` 96 | 97 | 时间复杂度: 98 | * next(): 平均下来为O(1) 99 | * hasNext(): O(1) 100 | 101 | 空间复杂度:O(h) h为树的高度 102 | -------------------------------------------------------------------------------- /贪心算法/134.加油站.md: -------------------------------------------------------------------------------- 1 | # 134.加油站 2 | 3 | ## 题目 4 | 在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 5 | 6 | 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 7 | 8 | 如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。 9 | 10 | 说明:  11 | 12 | 如果题目有解,该答案即为唯一答案。 13 | 输入数组均为非空数组,且长度相同。 14 | 输入数组中的元素均为非负数。 15 | 16 | 示例 1: 17 | 输入: 18 | gas = [1,2,3,4,5] 19 | cost = [3,4,5,1,2] 20 | 输出: 3 21 | 解释: 22 | 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 23 | 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 24 | 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 25 | 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 26 | 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 27 | 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 28 | 因此,3 可为起始索引。 29 | 30 | 示例 2: 31 | 输入: 32 | gas = [2,3,4] 33 | cost = [3,4,3] 34 | 输出: -1 35 | 解释: 36 | 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 37 | 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 38 | 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 39 | 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 40 | 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 41 | 因此,无论怎样,你都不可能绕环路行驶一周。 42 | 43 | ## 方法(贪心算法) 44 | 在考虑问题的时候,可以把问题理解成图的形式,每个节点表示添加的油量,每条边表示消耗的油量。题目即让我们找到从哪个节点出发走完每一条边还能回到该节点。 45 | 46 | 如果加油站提供的总油量大于走完每条边消耗的总油量(即gas[i]-cost[i]累加和大于0),则一定可以环绕一周,否则一定不可环绕一周。 47 | 48 | 算法步骤: 49 | * 从0位置开始遍历,一边记录gas[i]-cost[i]的差值总和total,一边寻找正确的出发点start 50 | * 如果一个位置的gas[i]-cost[i]小于0,则它不能作为起始位置(因为从它开始根本跑不到下一个节点),于是我们把可能的开始位置更新为它的下一个节点。当前油量cur重置为0. 51 | * 无论油够不够,一直遍历到结尾。如果遍历完成后(每个节点都走过了),total大于等于0,说明可以环绕一周,返回start。如果total小于0,说明不能环绕一周,返回-1. 52 | 53 | 54 | ## 代码 55 | ```java 56 | public int canCompleteCircuit(int[] gas, int[] cost) { 57 | int cur = 0; //cur记录目前的油量 58 | int total = 0; 59 | int start = 0; //记录开始位置(遍历过程不断更新) 60 | for(int i=0;i= 0 ? start: -1; 70 | } 71 | ``` 72 | -------------------------------------------------------------------------------- /贪心算法/1663.具有给定数值的最小字符串.md: -------------------------------------------------------------------------------- 1 | # 1663.具有给定数值的最小字符串 2 | 3 | ## 题目 4 | 小写字符 的 数值 是它在字母表中的位置(从 1 开始),因此 a 的数值为 1 ,b 的数值为 2 ,c 的数值为 3 ,以此类推。 5 | 6 | 字符串由若干小写字符组成,字符串的数值 为各字符的数值之和。例如,字符串 "abe" 的数值等于 1 + 2 + 5 = 8 。 7 | 8 | 给你两个整数 n 和 k 。返回 长度 等于 n 且 数值 等于 k 的 字典序最小 的字符串。 9 | 10 | 注意,如果字符串 x 在字典排序中位于 y 之前,就认为 x 字典序比 y 小,有以下两种情况: 11 | 12 | x 是 y 的一个前缀; 13 | 如果 i 是 x[i] != y[i] 的第一个位置,且 x[i] 在字母表中的位置比 y[i] 靠前。 14 |   15 | 16 | 示例 1: 17 | 输入:n = 3, k = 27 18 | 输出:"aay" 19 | 解释:字符串的数值为 1 + 1 + 25 = 27,它是数值满足要求且长度等于 3 字典序最小的字符串。 20 | 21 | 示例 2: 22 | 输入:n = 5, k = 73 23 | 输出:"aaszz" 24 | 25 | ## 方法(贪心算法) 26 | 假设当前来到了一个位置,要往这个位置放一个字符。包括这个位置在内还需要放n'个位置,这些位置的和为k' 27 | 28 | 放完这个位置后,剩余还有n'-1个位置,这些位置的和最大值为26(n'-1),最小值为n'-1。分别为全为z和全为a的情况。因此,只有满足:n' - 1 <= k' - c <= 26 * (n' - 1) 时,我们才能往这个位置放字符c。即:c需要满足:k' - 26(n' - 1) <= c <= k' - (n' - 1) 29 | 30 | 我们的贪心策略总是希望放一个尽可能小的字符,因此我们就放c的下限:k' - 26(n' - 1)。如果这个下限小于0,我们就放a。否则,我们放这个下限对应的字符 31 | 32 | ## 代码 33 | ```java 34 | public static String getSmallestString(int n, int k) { 35 | StringBuilder str = new StringBuilder(); 36 | while(n > 0){ 37 | int bound = k - 26 * (n - 1); 38 | if(bound > 0) { 39 | str.append((char) (bound + 'a' - 1)); 40 | k -= bound; 41 | } 42 | else{ 43 | str.append('a'); 44 | k -= 1; 45 | } 46 | n--; 47 | } 48 | return str.toString(); 49 | } 50 | ``` -------------------------------------------------------------------------------- /贪心算法/252.会议室.md: -------------------------------------------------------------------------------- 1 | # 252.会议室 2 | ## 题目 3 | 给定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,请你判断一个人是否能够参加这里面的全部会议。 4 | 5 | 示例 1: 6 | 输入:intervals = [[0,30],[5,10],[15,20]] 7 | 输出:false 8 | 9 | 示例 2: 10 | 输入:intervals = [[7,10],[2,4]] 11 | 输出:true 12 | 13 | ## 方法 14 | 将所有的会议按照开始时间排序,之后只需要判断在一个会议开始时,上一个会议是否结束即可。 15 | ## 代码 16 | ```java 17 | public boolean canAttendMeetings(int[][] intervals) { 18 | if(intervals == null || intervals.length == 0) 19 | return true; 20 | Arrays.sort(intervals, (v1, v2) -> (v1[0] - v2[0])); 21 | for(int i = 0; i < intervals.length; i++){ 22 | if(i - 1 >= 0 && intervals[i][0] < intervals[i - 1][1]) 23 | return false; 24 | } 25 | return true; 26 | } 27 | ``` 28 | 29 | # 253.会议室 II 30 | ## 题目 31 | 给定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi],为避免会议冲突,同时要考虑充分利用会议室资源,请你计算至少需要多少间会议室,才能满足这些会议安排。 32 | 33 | 示例 1: 34 | 输入: [[0, 30],[5, 10],[15, 20]] 35 | 输出: 2 36 | 37 | 示例 2: 38 | 输入: [[7,10],[2,4]] 39 | 输出: 1 40 | 41 | ## 方法 42 | **基本思路**: 43 | 如果我们能统计出每一个时刻要开会的会议数量,就能知道各时刻对于会议室数量的需求,则会议最繁忙的那个时刻需要的会议室数量就是我们想要的结果。只要我们的会议室数量满足了这个时刻的开会要求,则其他时刻都能够满足。 44 | 45 | **算法流程**: 46 | * 因为我们要按时间顺序遍历,所以先将数组按照会议的开始时间排序 47 | * 准备一个最小堆,堆中元素为每一个正在进行中会议的结束时间 48 | * 遍历数组,如果发现在当前时刻,有已经过了结束时间但还在堆中的会议,便将其从堆中弹出。之后将当前的会议加入到堆中 49 | * 在遍历过程中不断统计堆中元素的数量,这个数量代表一个时刻正在进行中的会议数量,也即对会议室数量的需求,这个需求的最大值即为结果。 50 | 51 | ## 代码 52 | ```java 53 | public int minMeetingRooms(int[][] intervals) { 54 | if(intervals == null || intervals.length == 0) 55 | return 0; 56 | Arrays.sort(intervals, (v1, v2) -> (v1[0] - v2[0])); 57 | PriorityQueue heap = new PriorityQueue<>(); 58 | int meetingCount = 0; 59 | for(int[] meeting : intervals){ 60 | while(!heap.isEmpty() && meeting[0] >= heap.peek()) 61 | heap.poll(); 62 | heap.add(meeting[1]); 63 | meetingCount = Math.max(meetingCount, heap.size()); 64 | } 65 | return meetingCount; 66 | } 67 | ``` -------------------------------------------------------------------------------- /贪心算法/435.无重叠区间.md: -------------------------------------------------------------------------------- 1 | # 435.无重叠区间 2 | 3 | ## 题目 4 | 给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 5 | 6 | 注意: 7 | 可以认为区间的终点总是大于它的起点。 8 | 区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 9 | 10 | 示例 1: 11 | 输入: [ [1,2], [2,3], [3,4], [1,3] ] 12 | 输出: 1 13 | 解释: 移除 [1,3] 后,剩下的区间没有重叠。 14 | 15 | 示例 2: 16 | 输入: [ [1,2], [1,2], [1,2] ] 17 | 输出: 2 18 | 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 19 | 20 | 示例 3: 21 | 输入: [ [1,2], [2,3] ] 22 | 输出: 0 23 | 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 24 | 25 | ## 方法(贪心算法) 26 | 题目要我们找最少需要移除多少个区间,实际上就是要找最多能选择多少个不重叠的区间。最后用总区间减去最多不重叠区间的个数,就是想要的结果。 27 | 28 | 贪心策略:选择不重叠的区间时,我们在不重叠的前提下,尽可能地选择长度最短的区间,这样可以给后面的区间留下更大的选择空间。 29 | 30 | ## 代码 31 | ```java 32 | public int eraseOverlapIntervals(int[][] intervals) { 33 | if (intervals.length == 0) 34 | return 0; 35 | Arrays.sort(intervals, new Mycomparator()); 36 | int count = 0; 37 | int bound = Integer.MIN_VALUE; 38 | for(int[] num : intervals){ 39 | if(num[0] >= bound){ 40 | count++; 41 | bound = num[1]; 42 | } 43 | } 44 | return intervals.length - count; 45 | } 46 | 47 | public class Mycomparator implements Comparator{ 48 | @Override 49 | public int compare(int[] o1, int[] o2) { 50 | return o1[1] - o2[1]; 51 | } 52 | } 53 | ``` 54 | -------------------------------------------------------------------------------- /贪心算法/502.IPO.md: -------------------------------------------------------------------------------- 1 | # 502 IPO 2 | ## 题目 3 | 假设 力扣(LeetCode)即将开始其 IPO。为了以更高的价格将股票卖给风险投资公司,力扣 希望在 IPO 之前开展一些项目以增加其资本。 由于资源有限,它只能在 IPO 之前完成最多 k 个不同的项目。帮助 力扣 设计完成最多 k 个不同项目后得到最大总资本的方式。 4 | 5 | 给定若干个项目。对于每个项目 i,它都有一个纯利润 Pi,并且需要最小的资本 Ci 来启动相应的项目。最初,你有 W 资本。当你完成一个项目时,你将获得纯利润,且利润将被添加到你的总资本中。 6 | 7 | 总而言之,从给定项目中选择最多 k 个不同项目的列表,以最大化最终资本,并输出最终可获得的最多资本。 8 | 9 | 示例 1: 10 | 输入: k=2, W=0, Profits=[1,2,3], Capital=[0,1,1]. 11 | 12 | 输出: 4 13 | 解释: 14 | 由于你的初始资本为 0,你尽可以从 0 号项目开始。 15 | 在完成后,你将获得 1 的利润,你的总资本将变为 1。 16 | 此时你可以选择开始 1 号或 2 号项目。 17 | 由于你最多可以选择两个项目,所以你需要完成 2 号项目以获得最大的资本。 18 | 因此,输出最后最大化的资本,为 0 + 1 + 3 = 4。 19 | 20 | ## 思路(贪心策略) 21 | 准备一个小根堆和一个大根堆。先将所有的项目放进小根堆,小根堆的堆顶为堆中cost最小的项目。然后让所有cost小于W的项目(即当前可以做的项目)进大根堆,大根堆的堆顶为profit最大的项目。每次从大根堆里取出项目做,每做完项目取得收益后W会更新,这时就会有新的项目从小根堆进入到大根堆。当做满了k个项目或者大根堆中没有项目可做时,结束。 22 | 23 | 每次在所有项目中找利润最高的项目做,是一种贪心策略。也符合生活中面临此类问题时的常规做法。 24 | ## 代码 25 | ```java 26 | class Solution { 27 | //Project项目类包含两个实例域,p代指利润,c代指花销 28 | public static class Project{ 29 | public int p; 30 | public int c; 31 | public Project(int p, int c){ 32 | this.p = p; 33 | this.c = c; 34 | } 35 | } 36 | //定义一个比较器,根据项目的花销值c比较Project(花销值小的Project小) 37 | public static class MinCostComparator implements Comparator{ 38 | @Override 39 | public int compare(Project o1, Project o2) { 40 | return o1.c - o2.c; 41 | } 42 | } 43 | //定义一个比较器,根据项目的利润值p比较Project(利润值高的Project小) 44 | public static class MaxProfitComparator implements Comparator{ 45 | @Override 46 | public int compare(Project o1, Project o2) { 47 | return o2.p - o1.p; 48 | } 49 | } 50 | 51 | public int findMaximizedCapital(int k, int W, int[] Profits, int[] Capital) { 52 | //定义一个项目数组并将其初始化 53 | Project[] projects = new Project[Profits.length]; 54 | for(int i = 0; i < Profits.length; i++){ 55 | projects[i] = new Project(Profits[i], Capital[i]); 56 | } 57 | //准备两个堆,一个堆顶表示堆中cost最小的项目,另一个堆顶表示堆中profit最大的项目 58 | PriorityQueue minCostHeap = new PriorityQueue<>(new MinCostComparator()); 59 | PriorityQueue maxProfitHeap = new PriorityQueue<>(new MaxProfitComparator()); 60 | for(Project item: projects){ 61 | minCostHeap.add(item); 62 | } 63 | for(int i = 0; i < k; i++){ 64 | //小根堆堆顶依次弹出进大根堆,直到堆顶大于w,遇到了没有足够资金做的项目为止 65 | while(!minCostHeap.isEmpty() && minCostHeap.peek().c <= W) 66 | maxProfitHeap.add(minCostHeap.poll()); 67 | //循环结束时有两种可能,一种为已经做完了k个项目,另一种为没做到k个项目但能做到项目都做完了 68 | if(maxProfitHeap.isEmpty()) 69 | return W; 70 | W += maxProfitHeap.poll().p; 71 | } 72 | return W; 73 | } 74 | } 75 | ``` 76 | -------------------------------------------------------------------------------- /贪心算法/621. 任务调度器.md: -------------------------------------------------------------------------------- 1 | # 621. 任务调度器 2 | 3 | ## 题目 4 | 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。在任何一个单位时间,CPU 可以完成一个任务,或者处于待命状态。 5 | 6 | 然而,两个 相同种类 的任务之间必须有长度为整数 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。 7 | 8 | 你需要计算完成所有任务所需要的 最短时间 。 9 |   10 | 11 | 示例 1: 12 | 输入:tasks = ["A","A","A","B","B","B"], n = 2 13 | 输出:8 14 | 解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B 15 | 在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。 16 | 17 | 示例 2: 18 | 输入:tasks = ["A","A","A","B","B","B"], n = 0 19 | 输出:6 20 | 解释:在这种情况下,任何大小为 6 的排列都可以满足要求,因为 n = 0 21 | ["A","A","A","B","B","B"] 22 | ["A","B","A","B","A","B"] 23 | ["B","B","B","A","A","A"] 24 | ... 25 | 诸如此类 26 | 27 | 示例 3: 28 | 输入:tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2 29 | 输出:16 30 | 解释:一种可能的解决方案是: 31 | A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> (待命) -> (待命) -> A -> (待命) -> (待命) -> A 32 | 33 | ## 方法 34 | 容易想到的一种贪心策略为:先安排出现次数最多的任务,让这个任务两次执行的时间间隔正好为n。再在这个时间间隔内安排其他的任务。 35 | 36 | 例如:tasks = ["A","A","A","B","B","B"], n = 2 37 | 38 | 我们先安排出现次数最多的任务"A",并且让执行两个"A"的时间间隔2。在这个时间间隔内,我们用其他任务类型去填充,又因为其他任务类型只有"B"一个,不够填充2的时间间隔,因此额外需要一个冷却时间间隔。具体安排如下图所示: 39 | 40 | ![](621.png) 41 | 42 | 其中,maxTimes为出现次数最多的那个任务出现的次数。maxCount为一共有多少个任务和出现最多的那个任务出现次数一样。 43 | 44 | 图中一共占用的方格即为完成所有任务需要的时间,即: 45 | $$(maxTimes - 1)*(n + 1) + maxCount$$ 46 | 47 | 此外,如果任务种类很多,在安排时无需冷却时间,只需要在一个任务的两次出现间填充其他任务,然后从左到右从上到下依次执行即可,由于每一个任务占用一个时间单位,我们又正正好好地使用了tasks中的所有任务,而且我们只使用tasks中的任务来占用方格(没用冷却时间)。因此这种情况下,所需要的时间即为tasks的长度。 48 | 49 | 由于这种情况时再用上述公式计算会得到一个不正确且偏小的结果,因此,我们只需把公式计算的结果和tasks的长度取最大即为最终结果。 50 | ## 代码 51 | ```java 52 | public int leastInterval(char[] tasks, int n) { 53 | int[] buckets = new int[26]; 54 | for(int i = 0; i < tasks.length; i++){ 55 | buckets[tasks[i] - 'A']++; 56 | } 57 | Arrays.sort(buckets); 58 | int maxTimes = buckets[25]; 59 | int maxCount = 1; 60 | for(int i = 25; i >= 1; i--){ 61 | if(buckets[i] == buckets[i - 1]) 62 | maxCount++; 63 | else 64 | break; 65 | } 66 | int res = (maxTimes - 1) * (n + 1) + maxCount; 67 | return Math.max(res, tasks.length); 68 | } 69 | ``` -------------------------------------------------------------------------------- /贪心算法/621.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wyh317/Algorithm_solving/b2d21a61e814d21abe0b3a917db217b744b87dda/贪心算法/621.png -------------------------------------------------------------------------------- /链表/141.环形链表.md: -------------------------------------------------------------------------------- 1 | # 141 环形链表 2 | **题目:** 3 | 给定一个链表。判断链表中是否有环 4 | 5 | 6 | **思路:** 7 | 双指针思路:一个快指针,一个慢指针。快指针一次走两步,慢指针一次走一步。两个指针若相遇,则有环。 8 | 9 | 哈希表思路:我们可以通过检查一个结点此前是否被访问过来判断链表是否为环形链表。常用的方法是使用哈希表。 10 | 11 | 算法:我们遍历所有结点并在哈希表中存储每个结点的引用(或内存地址)。如果当前结点为空结点 null(即已检测到链表尾部的下一个结点),那么我们已经遍历完整个链表,并且该链表不是环形链表。如果当前结点的引用已经存在于哈希表中,那么返回 true(即该链表为环形链表)。 12 | 13 | **代码:** 14 | ```java 15 | public boolean hasCycle(ListNode head){ 16 | Set nodesSeen = new HashSet<>(); 17 | while(head != null){ 18 | if(nodesSeen.contains(head)) 19 | return true; 20 | else 21 | nodesSeen.add(head); 22 | head = head.next; 23 | } 24 | return false; 25 | } 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /链表/142.环形链表 II.md: -------------------------------------------------------------------------------- 1 | # 142 环形链表 II 2 | **题目:** 3 | 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 4 | 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。 5 | 说明:不允许修改给定的链表。 6 | 7 | 示例 1: 8 | 输入:head = [3,2,0,-4], pos = 1 9 | 输出:tail connects to node index 1 10 | 解释:链表中有一个环,其尾部连接到第二个节点。 11 | 12 | 13 | 示例 2: 14 | 输入:head = [1,2], pos = 0 15 | 输出:tail connects to node index 0 16 | 解释:链表中有一个环,其尾部连接到第一个节点。 17 | 18 | 不仅要判断是否有环,还要找到入环的第一个节点 19 | 20 | 21 | 22 | 23 | **思路:** 24 | 在上一道题中已经知道如何判断一个链表有没有环。即采用快慢指针法,如果相遇则有环。进一步思考:两个指针相遇的节点一定在环中。可以从这个结点出发,一边继续移动一边计数,当再次回到这个节点时,就可以得到环中节点个数了。 25 | 26 | 知道了节点个数n。我们可以让快指针先向前移动n步。然后两个指针一起移动。当第慢指针指向环的入口节点时,快指针也已经围绕着环走了一圈,回到了入口节点,两指针相遇 27 | 28 | **代码:** 29 | ```java 30 | public class Solution { 31 | public ListNode detectCycle(ListNode head) { 32 | ListNode meetnode = meetingNode(head); 33 | if(meetnode == null) 34 | return null; 35 | //nodeinLoop变量用来保存环中节点的数目 36 | int nodeinLoop = 1; 37 | //从相遇的节点meetnode出发,继续移动,再回到meetnode时,计数结束,得到环中节点数目 38 | ListNode cur = meetnode; 39 | while(cur.next != meetnode){ 40 | cur = cur.next; 41 | nodeinLoop++; 42 | } 43 | //知道节点数目后找入口节点 44 | //先让快指针移动nodeinLoop步 45 | ListNode fast = head; 46 | for(int i = 0; i < nodeinLoop; i++) 47 | fast = fast.next; 48 | //再一起移动快慢指针,知道它们相遇于入口节点 49 | ListNode slow = head; 50 | while(fast != slow){ 51 | fast = fast.next; 52 | slow = slow.next; 53 | } 54 | return fast; 55 | } 56 | //meetingNode函数找到快慢指针在环中相遇的节点 57 | public ListNode meetingNode(ListNode head){ 58 | if(head == null) 59 | return null; 60 | //定义快慢指针,快指针一次走两步,满指针一次走一步。如果链表有环,则两指针一定会相遇 61 | ListNode fast = head; 62 | ListNode slow = head; 63 | while(fast != null && fast.next != null){ 64 | fast = fast.next.next; 65 | slow = slow.next; 66 | if(fast == slow) 67 | return fast; 68 | } 69 | return null; 70 | } 71 | } 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /链表/143.重排链表.md: -------------------------------------------------------------------------------- 1 | # 143 重排链表 2 | **题目:** 3 | 给定一个单链表 L:L0→L1→…→Ln-1→Ln , 4 | 将其重新排列后变为: L0→Ln→L1→Ln-1→L2→Ln-2→… 5 | 6 | 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 7 | 8 | 示例 1: 9 | 给定链表 1->2->3->4, 重新排列为 1->4->2->3. 10 | 示例 2: 11 | 给定链表 1->2->3->4->5, 重新排列为 1->5->2->4->3. 12 | 13 | 14 | **思路:** 15 | 分三个步骤: 16 | 1. 找到中间节点,根据中间节点把链表分为左右两部分 17 | 2. 把右半部分节点反转 18 | 3. 将右半部分链表的节点插入左半部分中点 19 | 20 | **代码:** 21 | ```java 22 | public ListNode reverse(ListNode head){ 23 | ListNode pre = null; 24 | ListNode cur = head; 25 | while(cur != null){ 26 | ListNode temp = cur.next; 27 | cur.next = pre; 28 | pre = cur; 29 | cur = temp; 30 | } 31 | return pre; 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /链表/147.对链表进行插入排序.md: -------------------------------------------------------------------------------- 1 | # 147.对链表进行插入排序 2 | 3 | ## 题目 4 | 对链表进行插入排序。 5 | 6 | 示例 1: 7 | 输入: 4->2->1->3 8 | 输出: 1->2->3->4 9 | 10 | 示例 2: 11 | 输入: -1->5->3->4->0 12 | 输出: -1->0->3->4->5 13 | 14 | ## 方法一 15 | 新建一个列表,将链表中的元素全部加入到这个链表中。然后对这个列表进行排序。最后将列表中排序好的元素再依次放回链表中。 16 | ```java 17 | public ListNode insertionSortList(ListNode head) { 18 | List list = new ArrayList<>(); 19 | ListNode cur = head; 20 | while(cur != null){ 21 | list.add(cur.val); 22 | cur = cur.next; 23 | } 24 | Collections.sort(list); 25 | cur = head; 26 | for(int num : list){ 27 | cur.val = num; 28 | cur = cur.next; 29 | } 30 | return head; 31 | } 32 | ``` 33 | ## 方法二 34 | 设置一个指针pre和一个指针cur,pre初始化为head,cur初始化为head.next。从前到后遍历链表中的每一个元素 35 | * 如果pre所指的值小于cur所指的值(已经有序),那么pre和cur正常向后移动 36 | * 如果pre所指的值大于cur所指的值,那么cur所指节点需要在前面已经有序的部分找到相应位置进行插入。 37 | 38 | ```java 39 | public ListNode insertionSortList(ListNode head) { 40 | //链表为空或者只有一个节点的情况 41 | if(head == null || head.next == null) 42 | return head; 43 | ListNode dummy = new ListNode(0); 44 | dummy.next = head; 45 | ListNode pre = head, cur = head.next; 46 | while(cur != null){ 47 | if(pre.val <= cur.val){ 48 | pre = cur; 49 | cur = cur.next; 50 | } 51 | else{ 52 | //节点node用来遍历找到插入位置 53 | ListNode node = dummy; 54 | //找到一个位置,使得node < cur < node.next 55 | while(node.next != null && node.next.val < cur.val) 56 | node = node.next; 57 | //将原有位置的cur删除 58 | pre.next = cur.next; 59 | //将cur插入到新位置 60 | cur.next = node.next; 61 | node.next = cur; 62 | //插入结束后,cur回到原有位置(pre后面),继续后面的遍历 63 | cur = pre.next; 64 | } 65 | return dummy.next; 66 | } 67 | ``` 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /链表/148.排序链表.md: -------------------------------------------------------------------------------- 1 | # 148.排序链表 2 | 3 | ## 题目 4 | 在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序。 5 | 6 | 示例 1: 7 | 输入: 4->2->1->3 8 | 输出: 1->2->3->4 9 | 10 | 示例 2: 11 | 输入: -1->5->3->4->0 12 | 输出: -1->0->3->4->5 13 | 14 | ## 方法(归并排序思路) 15 | 1. 首先用快慢指针法将链表从中间断开 16 | 2. 对左边链表排序 17 | 3. 对右边链表排序 18 | 4. 合并左右两个链表,返回链表头 19 | 20 | ## 代码 21 | ```java 22 | public ListNode sortList(ListNode head) { 23 | if(head == null || head.next == null){ 24 | return head; 25 | } 26 | ListNode slow = head; 27 | ListNode fast = head.next; 28 | while(fast != null && fast.next != null){ 29 | fast = fast.next.next; 30 | slow = slow.next; 31 | } 32 | ListNode l2 = slow.next; 33 | slow.next = null; 34 | return mergeTwoLists(sortList(head), sortList(l2)); 35 | } 36 | 37 | public ListNode mergeTwoLists(ListNode l1, ListNode l2){ 38 | if(l1 == null) 39 | return l2; 40 | if(l2 == null) 41 | return l1; 42 | ListNode head = new ListNode(0); 43 | if(l1.val < l2.val){ 44 | head = l1; 45 | head.next = mergeTwoLists(l1.next, l2); 46 | } 47 | else{ 48 | head = l2; 49 | head.next = mergeTwoLists(l1, l2.next); 50 | } 51 | return head; 52 | } 53 | ``` 54 | -------------------------------------------------------------------------------- /链表/160.相交链表.md: -------------------------------------------------------------------------------- 1 | # 160 相交链表 2 | **题目:** 3 | 编写一个程序,找到两个单链表相交的起始节点。 4 | 5 | **方法一:** 6 | 分三个步骤: 7 | 1. 获得两个链表的长度,相减得到长度差。 8 | 2. 让长的链表先走长度差步 9 | 3. 两个链表一起走,直到碰上相同的节点返回 10 | 11 | 注意: 12 | 边界处理 13 | 14 | **代码:** 15 | ```java 16 | public ListNode getIntersectionNode(ListNode headA, ListNode headB) { 17 | int lenA = getLength(headA); 18 | int lenB = getLength(headB); 19 | int diff = Math.abs(lenA - lenB); 20 | if(lenA > lenB){ 21 | for(int i = 0; i < diff; i++) 22 | headA = headA.next; 23 | } 24 | if(lenB > lenA){ 25 | for(int i = 0; i < diff; i++) 26 | headB = headB.next; 27 | } 28 | while(headA != headB){ 29 | headA = headA.next; 30 | headB = headB.next; 31 | } 32 | return headA; 33 | } 34 | 35 | public int getLength(ListNode head){ 36 | if(head == null) 37 | return 0; 38 | int count = 0; 39 | ListNode cur = head; 40 | while(cur != null){ 41 | count++; 42 | cur = cur.next; 43 | } 44 | return count; 45 | } 46 | ``` 47 | 48 | **方法二:** 49 | 设链表A前半部分长度为a,链表B前半部分长度为b,链表A和链表B相交的部分长度为c。 50 | 易知:a + c + b = a + b + c 51 | 52 | 因此我们先让指针curA在链表A上走a+c步,走到链表A末尾后,再让curA从链表B的头部开始走,在链表B上再走b步。 53 | 54 | 同理:让指针curB在链表B上走b+c步,走到链表B末尾后,再让curB从链表A的头部开始走,在链表A上再走a步。 55 | 56 | 这样当两个指针都走了a+b+c步时,它们会相遇于两个链表的交点。 57 | 58 | ```java 59 | public ListNode getIntersectionNode(ListNode headA, ListNode headB) { 60 | ListNode curA = headA; 61 | ListNode curB = headB; 62 | while(curA != curB){ 63 | curA = (curA == null) ? headB : curA.next; 64 | curB = (curB == null) ? headA : curB.next; 65 | } 66 | return curA; 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /链表/2.两数相加.md: -------------------------------------------------------------------------------- 1 | # 2.两数相加 2 | 3 | ## 题目 4 | 给出两个非空的链表用来表示两个非负的整数。其中,它们各自的位数是按照逆序的方式存储的,并且它们的每个节点只能存储 一位数字。 5 | 如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。 6 | 您可以假设除了数字 0 之外,这两个数都不会以 0 开头。 7 | 8 | 示例: 9 | 输入:(2 -> 4 -> 3) + (5 -> 6 -> 4) 10 | 输出:7 -> 0 -> 8 11 | 原因:342 + 465 = 807 12 | 13 | ## 方法 14 | 将两个链表看成是相同长度的进行遍历,如果一个链表较短则在前面补0,比如 987 + 23 = 987 + 023 = 1010 15 | 每一位计算的同时需要考虑上一位的进位问题,而当前位计算结束后同样需要更新进位值 16 | 如果两个链表全部遍历完毕后,进位值为1,则在新链表最前方添加节点1 17 | 18 | 19 | ## 代码 20 | ```java 21 | public ListNode addTwoNumbers(ListNode l1, ListNode l2) { 22 | ListNode dummy = new ListNode(0); 23 | ListNode cur = dummy; 24 | //carry表示进位值 25 | int carry = 0; 26 | while(l1 != null || l2 != null) { 27 | int x = l1 == null ? 0 : l1.val; 28 | int y = l2 == null ? 0 : l2.val; 29 | int sum = x + y + carry; 30 | //更新向下一位的进位值 31 | carry = sum / 10; 32 | sum = sum % 10; 33 | cur.next = new ListNode(sum); 34 | cur = cur.next; 35 | if (l1 != null) 36 | l1 = l1.next; 37 | if (l2 != null) 38 | l2 = l2.next; 39 | } 40 | if(carry == 1) 41 | cur.next = new ListNode(carry); 42 | return dummy.next; 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /链表/234.回文链表.md: -------------------------------------------------------------------------------- 1 | # 234.回文链表 2 | 3 | ## 题目: 4 | 请判断一个链表是否为回文链表。 5 | 6 | 示例 1: 7 | 输入: 1->2 8 | 输出: false 9 | 10 | 示例 2: 11 | 输入: 1->2->2->1 12 | 输出: true 13 | 14 | ## 方法1:用栈 15 | 将链表中的节点全部放入一个栈中,由于栈的先进后出的特性,它依次弹出的顺序为链表的逆序。因此,依次比较链表节点和栈弹出的节点,就可以判断链表是否为回文链表。 16 | ```java 17 | public boolean isPalindrome(ListNode head) { 18 | if(head == null) 19 | return true; 20 | ListNode cur = head; 21 | //将链表节点全部入栈 22 | Stack stack = new Stack(); 23 | while(cur != null){ 24 | stack.push(cur); 25 | cur = cur.next; 26 | } 27 | //依次比较链表节点和栈中弹出节点 28 | while(!stack.isEmpty()){ 29 | if(head.val != stack.pop().val) 30 | return false; 31 | head = head.next; 32 | } 33 | return true; 34 | } 35 | ``` 36 | 时间复杂度:O(n) 37 | 空间复杂度:O(n) 38 | 39 | ## 方法2:反转链表 40 | 41 | 彻底不用额外辅助空间的做法: 42 | 利用双指针,快指针一次走两步,慢指针一次走一步。快指针走完时,慢指针来到中点。然后将右半部分逆序。最后,一个指针指向链表尾,向前走,遍历右半部分;另一个指针指向链表头,向后走,遍历前半部分。依次比对这两个指 43 | 针所指元素是否相同。 44 | 注意:题目只让我们判断链表是否回文,我们不能改变题目给我们的结构,所以判断完之后别忘了把链表的指针恢复回来。 45 | ```java 46 | public boolean isPalindrome(ListNode head) { 47 | if(head == null) 48 | return true; 49 | ListNode fast = head, slow = head; 50 | //快指针走到末尾,慢指针走到中点 51 | while(fast.next != null && fast.next.next != null){ 52 | fast = fast.next.next; 53 | slow = slow.next; 54 | } 55 | //从中点开始,反转后部分链表 56 | ListNode tail = reverseList(slow); 57 | ListNode head_cur = head, tail_cur = tail; 58 | //分别从头和尾开始,比较链表节点 59 | while(head_cur != null && tail_cur != null){ 60 | if(head_cur.val != tail_cur.val) 61 | return false; 62 | head_cur = head_cur.next; 63 | tail_cur = tail_cur.next; 64 | } 65 | //恢复链表 66 | slow.next = reverseList(tail); 67 | return true; 68 | 69 | } 70 | //反转以head为头节点的链表 71 | public ListNode reverseList(ListNode head){ 72 | if(head == null) 73 | return null; 74 | ListNode pre = head, cur = pre.next; 75 | while(cur != null){ 76 | ListNode temp = cur; 77 | cur = cur.next; 78 | temp.next = pre; 79 | pre = temp; 80 | } 81 | head.next = null; 82 | return pre; 83 | } 84 | ``` 85 | 86 | 时间复杂度:O(n) 87 | 空间复杂度:O(1) 88 | -------------------------------------------------------------------------------- /链表/61.旋转链表.md: -------------------------------------------------------------------------------- 1 | # 61 旋转链表 2 | **题目:** 3 | 给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。 4 | 5 | 示例 1: 6 | 7 | 输入: 1->2->3->4->5->NULL, k = 2 8 | 输出: 4->5->1->2->3->NULL 9 | 解释: 10 | 向右旋转 1 步: 5->1->2->3->4->NULL 11 | 向右旋转 2 步: 4->5->1->2->3->NULL 12 | 13 | 示例 2: 14 | 输入: 0->1->2->NULL, k = 4 15 | 输出: 2->0->1->NULL 16 | 解释: 17 | 向右旋转 1 步: 2->0->1->NULL 18 | 向右旋转 2 步: 1->2->0->NULL 19 | 向右旋转 3 步: 0->1->2->NULL 20 | 向右旋转 4 步: 2->0->1->NULL 21 | 22 | **思路:** 23 | 只需要O(n)时间复杂度的算法: 24 | 25 | * 求出链表的长度n 26 | * k = k % n 27 | * 用双指针法找到链表倒数第k个位置 28 | * 记录慢指针的next节点,这就是要返回链表的头结点 29 | * 从慢指针的next节点开始断开链接,后一段的尾结点指向前一段的头结点 30 | 31 | **代码:** 32 | ```java 33 | class Solution { 34 | public ListNode rotateRight(ListNode head, int k) { 35 | if(head == null) 36 | return null; 37 | int length = getLength(head); 38 | k = k % length; 39 | if(length == 1 || k == 0) 40 | return head; 41 | for(int i = 0; i < k;i++){ 42 | //找到倒数第二个节点 43 | ListNode cur = head; 44 | while(cur.next.next != null) 45 | cur = cur.next; 46 | //在倒数第二个节点的地方进行操作 47 | cur.next.next= head; 48 | head = cur.next; 49 | cur.next = null; 50 | } 51 | return head; 52 | } 53 | public int getLength(ListNode head){ 54 | ListNode cur = head; 55 | int length = 1; 56 | while(cur.next != null){ 57 | cur = cur.next; 58 | length++; 59 | } 60 | return length; 61 | } 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /链表/82.删除排序链表中的重复元素 II.md: -------------------------------------------------------------------------------- 1 | # 82 删除排序链表中的重复元素 II 2 | **题目:** 3 | 给定一个排序链表,删除所有含有重复数字的节点,只保留原始链表中 没有重复出现 的数字。 4 | 5 | 示例 1: 6 | 输入: 1->2->3->3->4->4->5 7 | 输出: 1->2->5 8 | 示例 2: 9 | 输入: 1->1->1->2->3 10 | 输出: 2->3 11 | 12 | 13 | **思路:** 14 | 在迭代过程中,如果cur.next.val == cur.next.next.val说明此时有重复元素,此时创建一个临时指针temp,指向cur的下一个节点,即temp指向的第一个重复元素所在的位置。通过while循环去重,去重后,temp指向的是重复元素中的最后一个位置。最后cur.next = temp.next就实现了消除重复元素。 15 | 当然,如果未发现重复元素,则直接向后迭代即可。 16 | 17 | 18 | **代码:** 19 | ```java 20 | public ListNode deleteDuplicates(ListNode head) { 21 | //为了防止删除的是头节点,新建一个节点dummy,在头节点之前 22 | //cur从dummy开始遍历 23 | ListNode dummy = new ListNode(0); 24 | dummy.next = head; 25 | ListNode cur = dummy; 26 | while(cur.next != null && cur.next.next != null){ 27 | //当cur之后开始重复时,新建一个节点temp指向重复的第一个节点 28 | if(cur.next.val == cur.next.next.val){ 29 | ListNode temp = cur.next; 30 | //然后temp不断移动到该重复的最后一个节点 31 | while(temp.next != null && temp.val == temp.next.val) 32 | temp = temp.next; 33 | //temp标记着重复的最后一个节点。让cur的下一个节点指向它之后,即跳过了所有重复节点 34 | cur.next = temp.next; 35 | } 36 | //如果cur之后不遇到重复节点,则cur正常向后移动 37 | else 38 | cur = cur.next; 39 | } 40 | return dummy.next; 41 | } 42 | ``` 43 | 44 | **总结:** 45 | 对于此类链表题目,为了防止删除头节点的极端情况的产生。设置一个空结点dummy,使dummy指向传入的head头节点。cur遍历链表从dummy结点开始。最后函数返回dummy.next即head。 46 | -------------------------------------------------------------------------------- /链表/86.分割链表.md: -------------------------------------------------------------------------------- 1 | # 86 分割链表 2 | 3 | **题目:** 4 | 给定一个链表和一个特定值 x,对链表进行分隔,使得所有小于 x 的节点都在大于或等于 x 的节点之前。 5 | 6 | 7 | **示例:** 8 | 输入: head = 1->4->3->2->5->2, x = 3 9 | 输出: 1->2->2->4->3->5 10 | 11 | **思路:** 12 | * 建立两个链表min和max,分别收集小于x的元素和大于等于x的元素。头结点分别设为两个哑节点minhead和maxhead,也就是说实际的第一个节点为哑节点的next。 13 | * 遍历原有链表的每一个元素,若元素值小于x,则放入min链表中,若元素值大于等于x,则放入max链表中。 14 | * 遍历完所有元素之后,将min链表和max链表连接,max的最后指向null,min的最后指向max的开头,即maxhead.next 15 | 16 | **代码:** 17 | ```java 18 | public ListNode partition(ListNode head, int x) { 19 | ListNode minhead = new ListNode(0); 20 | ListNode mincur = minhead; 21 | ListNode maxhead = new ListNode(0); 22 | ListNode maxcur = maxhead; 23 | ListNode cur = head; 24 | while(cur != null){ 25 | if(cur.val < x){ 26 | mincur.next = cur; 27 | mincur = mincur.next; 28 | } 29 | else{ 30 | maxcur.next = cur; 31 | maxcur = maxcur.next; 32 | } 33 | cur = cur.next; 34 | } 35 | maxcur.next = null; 36 | mincur.next = maxhead.next; 37 | return minhead.next; 38 | } 39 | ``` 40 | 41 | **时间复杂度:O(n)** 42 | **空间复杂度:O(1)** 43 | -------------------------------------------------------------------------------- /链表/92.反转链表 II.md: -------------------------------------------------------------------------------- 1 | # 92 反转链表 II 2 | **题目:** 3 | 反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。 4 | 5 | 说明: 6 | $1 ≤ m ≤ n ≤$链表长度。 7 | 示例: 8 | 输入: 1->2->3->4->5->NULL, m = 2, n = 4 9 | 输出: 1->4->3->2->5->NULL 10 | 11 | 12 | 13 | 14 | **思路:** 15 | 1. 我们定义两个指针,分别称之为cur和pre。我们首先根据方法的参数m确定cur和pre的位置。将pre移动到第一个要反转的节点的前面,将cur移动到第一个要反转的节点的位置上 16 | 17 | 2. 将cur后面的元素删除,然后添加到pre的后面。也即头插法。 18 | 19 | 3. 根据m和n重复步骤2 20 | 21 | **注意:** 22 | 不能依赖头节点作pre,头节点的下一个节点做cur。这样的话当m=1时即第一个节点也要参与反转时就不适用了。于是建立一个在head前面的节点,并将它和链表联系起来,在通过它将pre和cur初始化 23 | 24 | **代码:** 25 | ```java 26 | public ListNode reverseBetween(ListNode head, int m, int n) { 27 | //新建一个节点,并将这个节点和原有链表联系起来 28 | ListNode preHeadNode = new ListNode(0); 29 | preHeadNode.next = head; 30 | //新建并初始化两个指针 31 | ListNode pre = preHeadNode; 32 | ListNode cur = preHeadNode.next; 33 | //移动这两个指针,直到cur指向要反转的第一个节点,pre指向它的前面 34 | for(int i = 0; i < m - 1; i++){ 35 | cur = cur.next; 36 | pre = pre.next; 37 | } 38 | // 39 | for(int i = 0; i < m - n;i++){ 40 | //先拿到要删除(即要插入)的节点 41 | ListNode remove = cur.next; 42 | //经过下面这步,跳过了cur的下一个节点。达到了把这个节点删除的效果 43 | cur.next = cur.next.next; 44 | //接下来是插入操作 45 | remove.next = pre.next; 46 | pre.next = remove; 47 | } 48 | return preHeadNode.next; 49 | } 50 | ``` 51 | 52 | --------------------------------------------------------------------------------