赞
踩
力扣算法题库引用自:https://cloud.tencent.com/developer/article/167101
关于动态规划的几道基本题目:
(子序列:去除某些字符,相对顺序保持不变;子串:截取的)
- public int lengthOfLIS(int[] nums) {
-
- if (nums.length == 0) {
- return 0;
- }
-
- // 定义dp 数组,dp[i] 表示以第i 个数字结尾,最长上升子序列的长度。
- int[] dp = new int[nums.length];
- dp[0] = 1;
- int maxAns = 1;
- // 我们从小到大计算dp 数组的值,在计算dp[i] 之前,我们已经计算出dp[0…i−1] 的值,则状态转移方程为:
- // dp[i]=max(dp[j])+1,其中0≤j<i且num[j]<num[i]
- for (int i = 1; i < nums.length; i++) {
- dp[i] = 1;
- for (int j = 0; j < i; j++) {
- if (nums[i] > nums[j]) {
- dp[i] = Math.max(dp[i], dp[j] + 1);
- }
- }
- maxAns = Math.max(maxAns, dp[i]);
- }
- return maxAns;
- }
-
- // 时间复杂度O(n^2)
- public int maxSubArray(int[] nums) {
- if (nums.length == 0) {
- return 0;
- }
- // 状态只能是遍历到nums 数组每个元素的时候,最大和为多少
- int[] dp = new int[nums.length];
-
- dp[0] = nums[0];
- int ans = nums[0];
- for (int i = 1; i < nums.length; i++) {
- // 做判断,选这个数字,还是不选,是加和,还是自己
- dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
- ans = Math.max(dp[i], ans);
-
- }
- return ans;
- }
- public int maxProduct(int[] nums) {
- int length = nums.length;
-
- int[] maxF = new int[length];
- int[] minF = new int[length];
-
- maxF[0] = nums[0];
- minF[0] = nums[0];
-
- int ans = maxF[0];
-
- for (int i = 1; i < length; i++) {
- maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
- minF[i] = Math.min(maxF[i - 1] * nums[i], Math.min(nums[i], minF[i - 1] * nums[i]));
-
- ans = Math.max(ans, maxF[i]);
- }
- return ans;
- }
理解:s1 插入或者删除,就是对应s2 的删除或者插入。修改a 或者修改b,其实上都一样。
本质不同的操作实际上只有三种:
在单词 A 中插入一个字符;
在单词 B 中插入一个字符;
修改单词 A 的一个字符。
对于这道题,dp 数组就是将s1 转为s2 的最小操作步骤。
- public static int minDistance(String s1, String s2) {
- int m = s1.length();
- int n = s2.length();
- // 初始化dp 二维数组,放入初始条件
- int[][] dp = new int[m + 1][n + 1];
- for (int i = 1; i <= m; i ++) {
- dp[i][0] = i;
- }
- for (int j = 1; j <= n; j++) {
- dp[0][j] = j;
- }
- for (int i = 1; i <= m; i++) {
- for (int j = 1; j <= n; j++) {
- if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
- // 如果字符相同,那么就是上一个位置的结果
- dp[i][j] = dp[i - 1][j - 1];
- } else {
- // 如果字符不同,那么就取最小的一个
- // A 字符串增加一个;B 字符串增加一个;A 修改一个
- dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i - 1][j - 1])) +1;
- }
- }
- }
- return dp[m][n];
- }
text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。- // 自底向上的解法
- public int longestCommonSubsequence(String s1, String s2) {
- int m = s1.length();
- int n = s2.length();
- int[][] dp = new int[m + 1][n + 1];
- // 定义:s1[0..i-1] 和 s2[0..j-1] 的 lcs 长度为 dp[i][j]
- // 目标:s1[0..m-1] 和 s2[0..n-1] 的 lcs 长度,即 dp[m][n]
- // base case: dp[0][..] = dp[..][0] = 0
- for (int i = 1; i <= m; i++) {
- for (int j = 1; j <= n; j++) {
- // 现在 i 和 j 从 1 开始,所以要减一
- if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
- // s1[i-1] 和 s2[j-1] 必然在 lcs 中
- dp[i][j] = 1 + dp[i - 1][j - 1];
- } else {
- // s1[i-1] 和 s2[j-1] 至少有一个不在 lcs 中
- dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
- }
- }
- }
- return dp[m][n];
- }
- // 此解法可以使用状态压缩,使得空间复杂度为O(N)
s
,找到 s
中最长的回文子串。
- class Solution {
- public String longestPalindrome(String s) {
- String result = "";
- for (int i = 0; i < s.length(); i++) {
- String s1 = findPalindrome(s, i, i);
- String s2 = findPalindrome(s, i, i + 1);
- result = s1.length() > result.length() ? s1 : result;
- result = s2.length() > result.length() ? s2 : result;
- }
- return result;
- }
-
- public String findPalindrome(String s, int l, int r) {
- while(l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
- l --;
- r ++;
- }
- return s.substring(l + 1, r);
- }
- }
动态规划看官方题解- class Solution {
- public String longestPalindrome(String s) {
-
- int len = s.length();
- if (len < 2) {
- return s;
- }
- int maxLen = 1;
- int begin = 0;
- // dp[i][j] 表示s[i...j] 是否为回文串
- boolean[][] dp = new boolean[len][len];
-
- for (int i = 0; i < len; i++) {
- dp[i][i] = true;
- }
- char[] charArray = s.toCharArray();
- // 递推开始
- // 枚举子串长度,长度从2 开始,长度为2、3、4、5...
- for (int L = 2; L <= len; L++) {
-
- // 枚举左边界,左边界上限设置可以宽松些
- // 上面for 循环设置的是串的长度;下面的是左边界在哪里
- for (int i = 0; i < len; i++) {
- // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
- int j = L + i - 1;
- // 如果右边界超限制,则退出循环
- if (j >= len) {
- break;
- }
- if (charArray[i] != charArray[j]) {
- dp[i][j] = false;
- } else if (charArray[i] == charArray[j]){
- // 字符相等,且长度小于等于2
- if (j - i <= 2) {
- dp[i][j] = true;
- } else {
- // 且字符相等,长度大于2
- dp[i][j] = dp[i + 1][j - 1];
- }
- }
-
-
- // 只要dp[i][j] == true 成立,就表示字符串s[i...j] 是回文子串,此时记录长度和起始位置
- // 每次循环要记录一下“最长的”
- if (dp[i][j] && j - i + 1 > maxLen) {
- maxLen = j - i + 1;
- begin = i;
- }
-
- }
-
- }
-
- return s.substring(begin, begin + maxLen);
- }
- }
s[i..j]
中,最长回文子序列的长度为dp[i][j]
i
从最后一个字符开始往前遍历,j
从 i + 1
开始往后遍历,这样可以保证每个子问题都已经算好了- class Solution {
- public int longestPalindromeSubseq(String s) {
- int n = s.length();
- int[][] dp = new int[n][n];
- // 因为是从长度较短的子序列向长度较长的转移
- // 所以从i = n - 1 开始
- for (int i = n - 1; i >= 0; i--) {
- dp[i][i] = 1;
- char c1 = s.charAt(i);
- for (int j = i + 1; j < n; j++) {
- char c2 = s.charAt(j);
- if (c1 == c2) {
- dp[i][j] = dp[i + 1][j - 1] + 2;
- } else {
- // 因为本身第i 个和第j 个不一致,
- // 而下面转移方程中是dp[i][j - 1] 和dp[i + 1][j] 这样,
- // 就不涉及第i 个和最后一个字符相等怎么处理这种事情了
- dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
- }
- }
- }
- // dp[0][n - 1] 的含义就是从第0 个字符串到第n - 1 个字符串的最长回文子序列的长度
- return dp[0][n - 1];
- }
- }
最长上升子序列的个数(673 题)
给定一个未排序的整数数组,找到最长递增子序列的个数。
- class Solution {
- public int findNumberOfLIS(int[] nums) {
-
- int n = nums.length;
- int maxLen = 0;
- int ans = 0;
- // dp[i] 为以nums[i] 结尾的最长上升子序列的长度
- int[] dp = new int[n];
- // cnt[i] 为考虑以nums[i] 结尾的最长上升子序列的个数
- int[] cnt = new int[n];
-
- for (int i = 0; i < n; i++) {
- dp[i] = 1;
- cnt[i] = 1;
- for (int j = 0; j < i; j++) {
- // nums[i] > nums[j] 的情况是
- // nums[i] 可以接在nums[j] 后面形成上升子序列
- if (nums[i] > nums[j]) {
- // 求最长上升子序列的长度,正常状态转移方程如下
- // dp[i] = Math.max(dp[i], dp[j] + 1);
- // 就是下面两种情况
- // (1)if (dp[i] < dp[j] + 1);
- // 说明以nums[i] 结尾的最长递增子序列长度
- // 是小于等于以nums[j] 结尾的dp[j]
- // dp[i] 会被dp[j] + 1 直接更新
- // 所以这个时候,cnt[i] 就直接等于cnt[j]
- // (2)if (dp[i] == dp[j] + 1);
- // 说明找到了一个新的符合条件的nums[i]
- // 可以附加到以j 结尾的最长递增子序列之后了
- // 所以,对于cnt[i] 要直接累加cnt[j]
- // 举例来说,之前cnt[j] 是2,组成元素分别为{1, 7} 和{2, 7}
- // 这个时候来了个数字num[i] = 9,可以附加到最后
- // (这块还是没有太理解,以后再说吧...)
- if (dp[i] < dp[j] + 1) {
- dp[i] = dp[j] + 1;
- cnt[i] = cnt[j];
- } else if (dp[i] == dp[j] + 1) {
- // 对于dp[i],因为相等,所以就不用重新赋值了
- cnt[i] += cnt[j];
- }
- }
- }
- // 最终结果判定 & 保存
- if (dp[i] > maxLen) {
- maxLen = dp[i];
- ans = cnt[i];
- } else if (dp[i] == maxLen) {
- ans += cnt[i];
- }
- }
- return ans;
- }
- }
以下思路及代码大部分来自于上述链接“王脸小”同学,同时有一部分摘抄自“labuladong 的算法小抄,以及网上的各个链接。
鸣谢@王脸小
同时题目参考:小浩算法
基础:
1. 读题后先想想有什么思路,不要让题解局限了想法,即使暴力解法也可以。
2. 懂得“递归”的思想。递归的基本思想是某个函数直接或者间接地调用自身,这样就把原问题的求解转换为许多性质相同但是规模更小的子问题。我们只需要关注如何把原问题划分为符合条件的子问题,而不需要研究这个子问题是如何解决的。递归的两个特征:自我调用(为了解决子问题)和结束条件(定义了最简子问题的答案)。
递归其实是有一个栈,每一层递归都是一个压栈的操作。
对于递归的内部逻辑理解:如果进入方法之后前几行代码就是判断某个条件,然后return,这样的操作其实是最底层的出栈,然后回到上一层的栈帧中,继续执行判断条件之后的代码。
关于其他的解释:
递归(recursion):递归常被用来描述以自相似方法重复事物的过程,在数学和计算机科学中,指的是在函数定义中使用函数自身的方法。(A调用A)
迭代(iteration):重复反馈过程的活动,每一次迭代的结果会作为下一次迭代的初始值。(A重复调用B)
递归:递推 + 回归。是一个树形结构。当“递推”到达底部时就会开始“回归”,其过程相当于树的深度优先遍历。
迭代:是一个环结构,从初始状态开始,每次迭代都遍历这个环,并更新状态,多次迭代直到到达结束状态。
发现一个规律:
如果使用递归的话,会发现递归的方法里面是有很多参数的。
比如说针对数组的操作,需要包含start、end 这些参数。这样可以方便下次调用的时候,变换参数。
同时,递归方法中的代码大致分为三个结构。
第一个部分是结束操作返回临界条件
第二部分是递归函数的引入
第三部分是逻辑处理代码
参考下面的归并排序,很明显
排序算法时间复杂度、空间复杂度、稳定性比较_排序算法的时间复杂度和空间复杂度
- // 对于start 和end,它们是每次循环,自身调用自己的时候的标志
- public static void quickSort(int[] nums, int start, int end) {
- // 所以在这里,需要使用start 和end 作为实际执行的时候的游标
- int low = start;
- int high = end;
- int key = nums[low];
- // 如果low 等于high,那么就说明不需要再向下执行了。所以这里只是小于而不是小于等于。
- // 这是针对这个数组的一次循环
- while (low < high) {
- // 从右边开始,与key 的大小左对比
- while (low < high && nums[high] >= key) {
- high --;
- }
- // 如果跳出了上面的while 循环,说明发现了比key 小的值,那么执行交换
- // 同时因为上面的while 循环有个low < high 的判断,所以这里也要加一下
- if (low < high) {
- nums[low] = nums[high];
- // 对low 赋值了,所以low 这个游标应该加一
- low ++;
- }
- // 然后再从左边开始
- while (low < high && nums[low] <= key) {
- low ++;
- }
- //
- if (low < high) {
- nums[high] = nums[low];
- high --;
- }
- }
- // 此时说明low 和high 已经相遇了
- nums[low] = key;
- // 如果low - 1 还是大于start 的,那么对相遇点左边的继续执行此函数
- if (low - 1 > start) {
- quickSort(nums, start, low - 1);
- }
- // 如果high + 1 小于end,那么对相遇点右边的数组元素再执行此函数
- if (high + 1 < end) {
- quickSort(nums, high + 1, end);
- }
- }
为什么先从右边开始:https://blog.csdn.net/he37176427/article/details/97795963
因为选择左边为key 的时候,从右边开始向左找,最终左右游标相遇的时候,那个值肯定是小于key 的。这个时候交换key 和相遇时的值,是符合条件的。但是如果选择了最左边的值为key,同时又从左边开始,那么最终两个游标相遇的时候,那个值肯定是大于key 的,这样就不符合快排的思想了。
快速排序的处理过程就是一个倒转的树结构,所以没有什么需要到最后进行“汇总”的代码。
- class Solution {
- int[] tmp;
-
- public int[] sortArray(int[] nums) {
- tmp = new int[nums.length];
- mergeSort(nums, 0, nums.length - 1);
- return nums;
- }
-
- public void mergeSort(int[] nums, int l, int r) {
- if (l >= r) {
- return;
- }
- int mid = (l + r) >> 1;
- mergeSort(nums, l, mid);
- mergeSort(nums, mid + 1, r);
- int i = l, j = mid + 1;
- int cnt = 0;
- while (i <= mid && j <= r) {
- if (nums[i] <= nums[j]) {
- tmp[cnt++] = nums[i++];
- } else {
- tmp[cnt++] = nums[j++];
- }
- }
- while (i <= mid) {
- tmp[cnt++] = nums[i++];
- }
- while (j <= r) {
- tmp[cnt++] = nums[j++];
- }
- // 一共r - l + 1 个数字
- // 把nums 中这些位置的元素替换掉,替换成tmp 中排好序的数字
- for (int k = 0; k < r - l + 1; ++k) {
- nums[k + l] = tmp[k];
- }
- }
- }
注意上面return 逻辑的理解,因为是基于递归而实现,在最后拆分为只有一个元素的时候,return 是返回到递归的上一层,然后继续执行后面的代码。(递归的思想)
堆排序
时间复杂度O(n log n) 最好、最坏、平均都是
空间复杂度O(1)
堆排序三个过程:1)建堆;2)调整;3)输出
建堆的过程就是把一个数组维护成每个点都比叶子节点大或者小的结构
排序,一个参数:nums[i]
建堆,两个参数:nums[i], int len
调整,三个参数:nums[i], int i, int len
其中,len 代表数组长度,i 代表
记住函数签名:
(1)调整:heapify(int[ ] nums, int i, int length)
(2)建堆:buildMaxHeap(int[ ] nums, int len)
举例:5 8 7 6 3 1 2
- public static int[] sort(int[] sourceArray) {
- // 对 arr 进行拷贝,不改变参数内容
- int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
-
- int len = arr.length;
-
- buildMaxHeap(arr, len);
-
- for (int i = len - 1; i > 0; i--) {
- swap(arr, 0, i);
- len--;
- heapify(arr, 0, len);
- }
- return arr;
- }
-
- // 建堆,过程就是从第一个非叶子节点开始调整
- private static void buildMaxHeap(int[] arr, int len) {
- for (int i = (int) Math.floor(len / 2) - 1; i >= 0; i--) {
- heapify(arr, i, len);
- }
- }
-
- // 对每个节点i 进行调整
- // 这是创建大顶堆的过程,创建大顶堆,然后可以进行正序排列
- private static void heapify(int[] arr, int i, int len) {
- int left = 2 * i + 1;
- int right = 2 * i + 2;
- int largest = i;
-
- if (left < len && arr[left] > arr[largest]) {
- largest = left;
- }
-
- if (right < len && arr[right] > arr[largest]) {
- largest = right;
- }
-
- if (largest != i) {
- swap(arr, i, largest);
- // 对于调整了的那个节点,还需要再继续向下进行调整
- heapify(arr, largest, len);
- }
- }
-
- private static void swap(int[] arr, int i, int j) {
- int temp = arr[i];
- arr[i] = arr[j];
- arr[j] = temp;
- }
正序排列,创建大顶堆。
因为在后续处理的时候,是把堆顶元素放到数组最后,然后再调整堆。
建堆的时候,是从第一个非叶子节点开始调整。从下往上调整。
逆序排列,创建小顶堆。
215 数组中第k 大的元素
- public static void bubble(int[] nums) {
- int length = nums.length;
- // 外层循环决定执行几次,只需要执行length - 1 次就好了
- for (int i = 0; i < length - 1; i++) {
- // 内层循环用来执行交换
- for (int j = 0; j < length -1 - i; j++) {
- if (nums[j] > nums[j + 1]) {
- nums[j] = nums[j] + nums[j + 1];
- nums[j + 1] = nums[j] - nums[j + 1];
- nums[j] = nums[j] - nums[j + 1];
- }
- }
- }
- for (int i = 0; i < nums.length; i++) {
- System.out.println(nums[i]);
- }
- }
做二分法需要自己理解并总结出,需要怎么进入循环,需要怎么退出循环。
即怎么判断边界条件。如果用数学方法表示mid,mid = (l + r)/2,这种是很明确的表示方法。但是到了编程的时候,就涉及mid 是应该(l + r)/2 还是(l + r + 1) / 2,判断l <= r 为结束条件,还是l < r 为结束条件。具体参考:算法浅谈——人人皆知却很多人写不对的二分法 - 知乎
二分法要注意不要造成死循环
对于区间的设置,可以使用“左闭右开”,对于每个人,设置一个自己的固定模式,这样更容易记一些。
- 查找给定数组的左边界的下标,相当于查找排序数组中元素小于target 的元素的个数。
- 所以在下面的代码中的判断,才返回的是nums.length。
- 重点在于num[mid] == target 时,right = mid。将范围逐步压缩。
-
- public static int left_bound(int[] nums, int target) {
-
- if (nums[nums.length - 1] < target) {
- return nums.length;
- }
-
- int left = 0;
- int right = nums.length - 1;
- while (left < right) {
- int mid = (left + right) / 2;
- if (nums[mid] == target) {
- right = mid;
- } else if (nums[mid] > target) {
- right = mid;
- } else if (nums[mid] < target) {
- left = mid + 1;
- }
- }
-
- if (nums[left] != target) {
- return -1;
- }
-
- return left;
-
- }
-
-
-
- 或者
- 看这个,这个能看懂
-
- public static int left_bount(int[] nums, int target) {
-
- int left = 0;
- int right = nums.length - 1;
- while (left < right) {
- int mid = (left + right) / 2;
- if (nums[mid] == target) {
- right = mid;
- } else if (nums[mid] < target) {
- left = mid + 1;
- } else if (nums[mid] > target) {
- right = mid - 1;
- }
- }
-
- if (nums[right] == target) {
- return right;
- } else {
- return -1;
- }
-
- }
-
-
- 注意在wile 块里面的循环逻辑里,如果有“等于”的判断,那么while 条件里,就不能是“小于等于”的判断
- 查找右边界
-
- public static int right_bound(int[] nums, int target) {
-
- if (nums.length == 0) return -1;
- int left = 0, right = nums.length;
- while (left < right) {
- int mid = (left + right) >> 1;
- if (nums[mid] == target) {
- left = mid + 1;
- } else if (nums[mid] > target) {
- right = mid;
- } else if (nums[mid] < target) {
- left = mid + 1;
- }
- }
- return left - 1;
-
- }
-
-
- 求mid 的时候要+1 就不会进入死循环。
- + 1 代表只剩两个数字的时候,选左边还是选右边
- 有时间再研究总结吧
-
- public static int right_bound(int[] nums, int target) {
-
- int left = 0;
- int right = nums.length - 1;
- while (left < right) {
- int mid = (left + right + 1) / 2;
- if (nums[mid] == target) {
- left = mid;
- } else if (nums[mid] > target) {
- right = mid - 1;
- } else if (nums[mid] < target) {
- left = mid + 1;
- }
- }
- if (nums[right] == target) {
- return right;
- } else {
- return -1;
- }
-
- }
[0,1,2,4,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]。
这个旋转的方式是在下标K 处旋转,是数组变成(num[k], num[k + 1] ... nums[0], nums[1] ...)
- class Solution {
- public int search(int[] nums, int target) {
-
- if (nums.length == 0) {
- return -1;
- }
- int low = 0;
- int high = nums.length - 1;
-
- while (low <= high) {
- int middle = (low + high) >> 1;
- if (nums[middle] == target) {
- return middle;
- }
-
- // 说明此区间有序
- if (nums[low] <= nums[middle]) {
- if (target >= nums[low] && target <= nums[middle]) {
- high = middle - 1;
- } else {
- low = middle + 1;
- }
-
- // 说明另外一个区间有序
- } else {
- if(target >= nums[middle] && target <= nums[high]) {
- low = middle + 1;
- } else {
- high = middle - 1;
- }
- }
- }
- return -1;
-
- }
- }
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,找出 这个重复的数 。
你设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间。
这道题二分的是1 ~ n 这n 个数字
- 首先,我们让中指针指向n/2, 由题意可得,小于n/2的元素只会有(n/2)-1个。
- 比如说,数组是4,1,3,2,5
- 对于数组中每个元素,小于1 的元素有0 个,小于2 的元素有1 个,小于3 的元素有2个...
-
- 如果超过了(n/2)-1,说明在[1,n/2)区间有重复的数字,此时我们将待查找的数据规模缩小到[1, n/2)
- 如果数组没有超过(n/2)-1。说明在[n/2, n]区间有重复的数字,此时我们讲查找的数据规模缩小到[n/2, n]
- 不断重复上述过程,直到找到某一个重复的数字。
- 时间复杂度O(nlogn), 二分法本身是O(logn), 每次缩小规模时需要统计一次数组(O(n))
- 空间复杂度O(1)
- 代码中start 是数字范围1 ~ n 的1;代码中的end 是数字范围,一共n + 1 个数字,范围1 ~ n,所以最大的是nums.length - 1。
-
-
- public static int findDuplicate(int[] nums) {
- int start = 1, end = nums.length - 1;
- int mid = (start + end) / 2;
- while (start <= end){
- int count = 0;
- for(int num: nums){
- // 计算小于中间数字mid 的数字个数
- if(num < mid) {
- count += 1;
- } else {
- break;
- }
- }
- if( count >= mid) {
- // 说明重复数字在此区间内
- end = mid - 1;
- } else {
- // 说明重复数字在另一个区间
- start = mid + 1;
- }
- mid = (start + end) / 2;
- }
-
- return mid;
- }
-
-
-
- 参考:https://zhuanlan.zhihu.com/p/102298178
- public int[] searchRange(int[] nums, int target) {
-
- int low = binarySearch(nums, target, true);
- int high = binarySearch(nums, target, false) - 1;
- if (low <= high && high < nums.length && nums[low] == target && nums[high] == target) {
- return new int[]{low, high};
- }
- return new int[]{-1, -1};
-
- }
-
-
- private static int binarySearch(int[] nums, int target, boolean lower) {
- int left = 0;
- int right = nums.length - 1;
- int ans = nums.length;
- while (left <= right) {
-
- int mid = (left + right) / 2;
- if (lower) {
- if (nums[mid] >= target) {
- right = mid - 1;
- ans = mid;
- } else {
- left = mid +1;
- }
- } else {
- if (nums[mid] > target) {
- right = mid -1;
- ans = mid;
- } else {
- left = mid + 1;
- }
- }
-
- }
- return ans;
- }
- public boolean searchMatrix(int[][] matrix, int target) {
- int m = matrix.length;
- int n = matrix[0].length;
- int i = m - 1;
- int j = 0;
- while(i >= 0 && j < n) {
- if (matrix[i][j] == target) {
- return true;
- } else if (matrix[i][j] < target) {
- j ++;
- } else {
- i --;
- }
- }
- return false;
- }
- class Solution {
- public int kthSmallest(int[][] matrix, int k) {
-
- int n = matrix.length;
- int left = matrix[0][0];
- int right = matrix[n-1][n-1];
- while (left < right) {
- int mid = (left + right) / 2;
- if (check(matrix, mid, k, n)) {
- right = mid;
- } else {
- left = mid +1;
- }
- }
- return left;
-
- }
-
- // 检查当前数字【mid】在矩阵中的情况
- // 返回结果:小于等于目标值mid 的数字个数是否大于等于k 个
- // 传参需要用到:矩阵,要查询的数字mid,范围k
- public boolean check(int[][] matrix, int mid, int k, int n) {
- // 从左下开始寻找,i 行,j 列。
- int i = n - 1;
- int j = 0;
- // 符合要求的个数
- int num = 0;
- while (i >= 0 && j < n) {
- if (matrix[i][j] <= mid) {
- // 元素上面的那些都是符合要求的
- num += i + 1;
- j ++;
- } else {
- i --;
- }
- }
- // 因为要寻找第k 小的元素,返回一个boolean,判断寻找到的数字的个数,是否多于k 个
- return num >= k;
-
- }
- }
- class Solution {
- public int mySqrt(int x) {
- int l = 0, r = x;
- int ans = -1;
- while (l <= r) {
- int mid = (l + r) / 2;
- if ((long)mid * mid <= x) {
- ans = mid;
- l = mid + 1;
- } else {
- r = mid - 1;
- }
- }
- return ans;
- }
- }
无法高效获取长度,无法根据偏移快速访问元素,是链表的两个劣势。然而面试的时候经常碰见诸如:
这些相关的问题都可以通过灵活运用双指针来解决。
- /**
- * Definition for singly-linked list.
- * public class ListNode {
- * int val;
- * ListNode next;
- * ListNode() {}
- * ListNode(int val) { this.val = val; }
- * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
- * }
- */
- class Solution {
- public ListNode reverseList(ListNode head) {
- ListNode prev = null;
- ListNode cur = head;
- while (cur != null) {
- ListNode next = cur.next;
- cur.next = prev;
- prev = cur;
- cur = next;
- }
- return prev;
- }
- }
- public ListNode reverseBetween(ListNode head, int left, int right) {
- ListNode dummyNode = new ListNode(-1);
- dummyNode.next = head;
- ListNode pre = dummyNode;
- for (int i = 0; i < left - 1; i++) {
- pre = pre.next;
- }
- ListNode cur = pre.next;
- ListNode next;
- for (int i = 0; i < right - left; i++) {
- next = cur.next;
- cur.next = next.next;
- next.next = pre.next;
- pre.next = next;
- }
- return dummyNode.next;
- }
- /*
- // Definition for a Node.
- class Node {
- int val;
- Node next;
- Node random;
- public Node(int val) {
- this.val = val;
- this.next = null;
- this.random = null;
- }
- }
- */
-
- class Solution {
- public Node copyRandomList(Node head) {
-
- if(head == null) {
- return null;
- }
- Node pointer = head;
- // 使用这个map 做原链表和老链表的映射,每个node 一一对应
- Map<Node, Node> map = new HashMap<>();
- while (pointer != null) {
- Node newNode = new Node(pointer.val);
- // 新链表节点只是存于map 中,还没有连接
- map.put(pointer, newNode);
- pointer = pointer.next;
- }
- pointer = head;
- while (pointer != null) {
- Node newNode = map.get(pointer);
- // random 节点赋值
- if (pointer.random != null) {
- newNode.random = map.get(pointer.random);
- }
- // next 节点赋值
- if (pointer.next != null) {
- newNode.next = map.get(pointer.next);
- }
- pointer = pointer.next;
- }
- return map.get(head);
- }
- }
- /**
- * Definition for singly-linked list.
- * public class ListNode {
- * int val;
- * ListNode next;
- * ListNode() {}
- * ListNode(int val) { this.val = val; }
- * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
- * }
- */
- class Solution {
- public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
-
- ListNode preHead = new ListNode(-1);
- ListNode prev = preHead;
- while (l1 != null && l2 != null) {
- if (l1.val <= l2.val) {
- prev.next = l1;
- l1 = l1.next;
- } else {
- prev.next = l2;
- l2 = l2.next;
- }
- prev = prev.next;
- }
-
- prev.next = l1 == null ? l2 : l1;
-
- return preHead.next;
-
- }
- }
- /**
- * Definition for singly-linked list.
- * public class ListNode {
- * int val;
- * ListNode next;
- * ListNode() {}
- * ListNode(int val) { this.val = val; }
- * ListNode(int val, ListNode next) { this.val = val; this.next = next; }
- * }
- */
- class Solution {
- public ListNode mergeKLists(ListNode[] lists) {
-
- Queue<ListNode> pq = new PriorityQueue<>((v1, v2) -> v1.val - v2.val);
- for (ListNode node : lists) {
- if (node != null) {
- pq.offer(node);
- }
- }
- ListNode dummyHead = new ListNode(0);
- ListNode tail = dummyHead;
- while (!pq.isEmpty()) {
- ListNode minNode = pq.poll();
- tail.next = minNode;
- tail = tail.next;
- if (minNode.next != null) {
- pq.add(minNode.next);
- }
- }
- return dummyHead.next;
- }
- }
- public ListNode reverseKGroup(ListNode head, int k) {
- ListNode hair = new ListNode(-1);
- hair.next = head;
- ListNode pre = hair;
- while (head != null) {
- ListNode tail = pre;
- // 查看剩余部分长度是否大于k
- for(int i = 0; i < k; i++) {
- tail = tail.next;
- if (tail == null) {
- return hair.next;
- }
- }
- ListNode pos = tail.next;
- ListNode[] reserved = reverseList(head, tail);
- // 翻转部分装回链表
- pre.next = reserved[0];
- reserved[1].next = pos;
- pre = reserved[1];
- head = pos;
- }
- return hair.next;
- }
-
- public ListNode[] reverseList(ListNode left, ListNode right) {
- ListNode[] result = new ListNode[]{right, left};
- // 最后返回的时候,因为不是一个孤单的链表,所以反转之后,left 得指向rightNext
- ListNode rightNext = right.next;
- ListNode pre = rightNext;
- ListNode leftNext;
- while (left != rightNext) {
- leftNext = left.next;
- left.next = pre;
- pre = left;
- left = leftNext;
- }
- return result;
- }
- public class Solution {
- public boolean hasCycle(ListNode head) {
-
- if (head == null || head.next == null) {
- return false;
- }
-
- ListNode slow = head;
- ListNode fast = head.next;
-
- while (fast != null && fast.next != null) {
- slow = slow.next;
- fast = fast.next.next;
- if (slow == fast) {
- return true;
- }
- }
-
- return false;
-
- // 第二种解法
- // if (head == null || head.next == null) {
- // return false;
- // }
- // ListNode slow = head;
- // ListNode fast = head.next;
- // while (slow != fast) {
- // if (fast == null || fast.next == null) {
- // return false;
- // }
- // slow = slow.next;
- // fast = fast.next.next;
- // }
- // return true;
- }
- }
- public ListNode detectCycle(ListNode head) {
- if (head == null) {
- return null;
- }
- ListNode slow = head;
- ListNode fast = head;
- while (fast != null) {
- if (fast.next != null) {
- slow = slow.next;
- fast = fast.next.next;
- } else {
- return null;
- }
- if (fast == slow) {
- ListNode ptr = head;
- while (ptr != slow) {
- ptr = ptr.next;
- slow = slow.next;
- }
- return ptr;
- }
- }
- return null;
- }
- public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
-
- if (headA == null || headB == null) {
- return null;
- }
- ListNode preA = headA;
- ListNode preB = headB;
- while (preA != preB) {
- if (preA != null) {
- preA = preA.next;
- } else {
- preA = headB;
- }
- if (preB != null) {
- preB = preB.next;
- } else {
- preB = headA;
- }
- }
- return preA;
- }
- public ListNode removeNthFromEnd(ListNode head, int n) {
-
- ListNode hair = new ListNode(0, head);
-
- ListNode first = head;
- ListNode second = hair;
- for (int i = 0; i < n; i++) {
- first = first.next;
- }
-
- while (first != null) {
- first =first.next;
- second = second.next;
- }
-
- second.next = second.next.next;
-
- return hair.next;
-
- }
- public boolean isPalindrome(ListNode head) {
-
- if (head == null) {
- return true;
- }
- // 找到前半部分尾节点,并翻转后半部分链表(重点)
- // 这样就可以避免了链表奇偶性的判断
- ListNode firstHalfEnd = endOfFirstHalf(head);
- ListNode reversedSecondHalfStart = reverseList(firstHalfEnd.next);
- ListNode p1 = head;
- ListNode p2 = reversedSecondHalfStart;
- boolean result = true;
- while (p1!= null && p2 != null) {
- if (p1.val != p2.val) {
- result = false;
- break;
- }
- p1 = p1.next;
- p2 = p2.next;
- }
- return result;
- }
-
- private ListNode reverseList(ListNode head) {
- ListNode prev = null;
- ListNode cur = head;
- while (cur != null) {
- ListNode next = cur.next;
- cur.next = prev;
- prev = cur;
- cur = next;
- }
- return prev;
- }
-
- // 有可能返回的是技术个数的中间打那个;有可能返回的事偶数个,前面那个链表的最后一个节点
- private ListNode endOfFirstHalf(ListNode head) {
- ListNode fast = head;
- ListNode slow = head;
- while (fast.next != null && fast.next.next != null) {
- fast = fast.next.next;
- slow = slow.next;
- }
- return slow;
- }
- class Solution {
- public ListNode oddEvenList(ListNode head) {
- if (head == null || head.next == null || head.next.next == null) {
- return head;
- }
- ListNode evenHead = head.next;
- ListNode odd = head;
- ListNode even = evenHead;
- while (even != null && even.next != null) {
- odd.next = even.next;
- odd = odd.next;
- even.next = odd.next;
- even = even.next;
- }
- odd.next = evenHead;
- return head;
- }
- }
- class Solution {
- public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
- int flag = 0;
- ListNode head = null;
- head = new ListNode();
- ListNode temp = head;
- while (l1 != null || l2 != null) {
-
- int l1Val;
- int l2Val;
- if (l1 == null) {
- l1Val = 0;
- } else {
- l1Val = l1.val;
- l1 = l1.next;
- }
-
- if (l2 == null) {
- l2Val = 0;
- } else {
- l2Val = l2.val;
- l2 = l2.next;
- }
-
- int result = l1Val + l2Val + flag;
- ListNode listNode = new ListNode();
- listNode.val = result % 10;
- flag = result / 10;
- temp.next = listNode;
- temp = temp.next;
- }
-
- if (flag >= 1) {
- ListNode listNode = new ListNode();
- listNode.val = flag;
- temp.next = listNode;
-
- }
- return head.next;
- }
- }
注:一般要分为两段的链表的双指针slow,fast = head, head.next; 不需要分为两段的slow,fast = head, head 对于slow 和fast,都设置head 为初始节点就行。
s
、一个字符串 t
。返回 s
中涵盖 t
所有字符的最小子串。如果 s
中不存在涵盖 t
所有字符的子串,则返回空字符串 ""
。- //模板
- /* 滑动窗口算法框架 */
- void slidingWindow(string s, string t) {
- Map<Character, Integer> need = new HashMap<>();
- Map<Character, Integer> window = new HashMap<>();
- for (char c : t.toCharArray())
- need.put(c,need.getOrDefault(c,0)+1);
- int left = 0, right = 0;
- int valid = 0;
- while (right < s.size()) {
- // c 是将移入窗口的字符
- char c = s.charAt(right);
- // 右移窗口
- right++;
- // 进行窗口内数据的一系列更新
- ...
-
- /*** debug 输出的位置 ***/
- System.out.println("window: ["+left+","+ right+")");
- /********************/
-
- // 判断左侧窗口是否要收缩
- while (window needs shrink) {
- // d 是将移出窗口的字符
- char d = s[left];
- // 左移窗口
- left++;
- // 进行窗口内数据的一系列更新
- ...
- }
- }
- }
- //题解
- class Solution {
- public String minWindow(String s, String t) {
- if (s == null || s == "" || t == null || t == "") {
- return "";
- }
-
- // 维护两个map记录窗口中的符合条件的字符以及need的字符
- // 其中key 是字母本身,value 是字符出现的次数
-
-
- // 存放t。含义是存储"需要的字符"(用它做标的去查询)以及需要的数量
- Map<Character, Integer> needsMap = new HashMap<>();
- // 滑动窗口中符合条件的字符
- Map<Character, Integer> windowMap = new HashMap<>();
-
- // 将t 存入needsMap 中
- for (int i = 0; i < t.length(); i++) {
- char temp = t.charAt(i);
- needsMap.put(temp, needsMap.getOrDefault(temp, 0) + 1);
- }
-
- // 滑动窗口的左边界
- int left = 0;
- // 滑动窗口的右边界
- int right = 0;
-
- // count 用来记录当前滑动窗口windowMap 中符合need 要求的字符的数量
- // 当count == need.size() 的时候,即可shrink 窗口
- int count = 0;
- // 符合要求最优解的开始位置
- int start = 0;
-
- int len = Integer.MAX_VALUE;
-
- // 一次遍历找"可行解"
- while(right < s.length()) {
- // 更新窗口
- char c = s.charAt(right);
- // 扩大窗口
- right ++;
-
- if (needsMap.containsKey(c)) {
- windowMap.put(c, windowMap.getOrDefault(c, 0) + 1);
- if (needsMap.get(c).equals(windowMap.get(c))) {
- count ++;
- }
- }
- // shrink 左边界,找到符合条件的最优解
- while (count == needsMap.size()) {
- // 不断"打擂",寻找满足条件的len 的最短值,并记录最短子串起始start 位置
- if (right - left < len) {
- len = right - left;
- start = left;
- }
- // 更新窗口
- char d = s.charAt(left);
- // 窗口缩小
- left ++;
- if (needsMap.containsKey(d)) {
- // window.put(d,window.get(d)-1);
- // ——bug:
- // 若一进去就将window对应的键值缩小,
- // 就永远不会满足下面的if,while也会一直执行,
- // 直到left越界,因此,尽管和上面对窗口的处理几乎一样,
- // 但是这个处理的顺序还是很关键的!要细心!
-
- // 如果滑动窗口windowMap 中包含了
- if (needsMap.get(d).equals(windowMap.get(d))) {
- // count 用来记录当前滑动窗口windowMap 中符合need 要求的字符的数量
- count --;
- }
- windowMap.put(d, windowMap.get(d) - 1);
- }
- }
- }
- return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);
- }
-
- }
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
(看不懂)- public static List<Integer> findAnagrams(String s, String t) {
-
- List<Integer> result = com.google.common.collect.Lists.newArrayList();
- int left, right;
- left = right = 0;
- Map<Character, Integer> needs = Maps.newHashMap();
- Map<Character, Integer> windows = Maps.newHashMap();
-
- for (int i = 0; i < t.length(); i++) {
- Character c = t.charAt(i);
- needs.put(c, needs.getOrDefault(c, 0) + 1);
- }
-
- int match = 0;
- while (right < s.length()) {
-
- char c1 = s.charAt(right);
- if (needs.containsKey(c1)) {
- windows.put(c1, windows.getOrDefault(c1, 0) + 1);
- if (Objects.equals(windows.get(c1), needs.get(c1))) {
- match ++;
- }
- }
- right ++;
-
- while (match == needs.size()) {
- if (right - left == t.length()) {
- result.add(left);
- }
- char c2 = s.charAt(left);
- if (needs.containsKey(c2)) {
- windows.put(c2, windows.get(c2) - 1);
- if (windows.get(c2) < needs.get(c2)) {
- match --;
- }
- }
- left ++;
- }
- }
- return result;
- }
- public static int lengthOfLongestSubString(String s) {
-
- int left, right;
- left = right = 0;
- int result = 0;
- Map<Character, Integer> windows = new HashMap<>();
- while (right < s.length()) {
- char c1 = s.charAt(right);
- windows.put(c1, windows.getOrDefault(c1, 0) + 1);
- right++;
-
- while(windows.get(c1) >1) {
- char c2 = s.charAt(left);
- windows.put(c2, windows.get(c2) - 1);
- left ++;
- }
- result = Math.max(result, right - left);
- }
- return result;
- }
给定一个字符串 s ,找出 至多 包含 k 个不同字符的最长子串 T。
示例 1: 输入: s = "eceba", k = 2 输出: 3 解释: 则 T 为 "ece",所以长度为 3。 示例 2: 输入: s = "aa", k = 1 输出: 2 解释: 则 T 为 "aa",所以长度为 2。
- public static int lengthOfLongestSubStringKDistinct(String s, int k) {
- int len = s.length();
- char[] charArray = s.toCharArray();
- // 这个数组用来统计遍历过程中字母出现的次数
- int[] freq = new int[123];
-
- // 使用count 来记录有多少个不同的字符
- int count = 0;
- int result = 0;
-
- int left = 0;
- int right = 0;
-
- while (right < len) {
- if (freq[charArray[right]] == 0) {
- count ++;
- }
- // 遍历到了某个字符之后,用来记录字母是否出现过
- freq[charArray[right]] ++;
- right ++;
-
- // 缩小窗口
- while (count > k) {
- freq[charArray[left]] --;
- if (freq[charArray[left]] == 0) {
- count --;
- }
- left ++;
- }
- result = Math.max(result, right - left);
- }
- return result;
- }
- class Solution {
- public String longestPalindrome(String s) {
-
- int len = s.length();
- if (len < 2) {
- return s;
- }
- int maxLen = 1;
- int begin = 0;
- // dp[i][j] 表示s[i...j] 是否为回文串
- boolean[][] dp = new boolean[len][len];
-
- for (int i = 0; i < len; i++) {
- dp[i][i] = true;
- }
- char[] charArray = s.toCharArray();
- // 递推开始
- // 枚举子串长度,长度从2 开始,长度为2、3、4、5...
- for (int L = 2; L <= len; L++) {
-
- // 枚举左边界,左边界上限设置可以宽松些
- // 上面for 循环设置的是串的长度;下面的是左边界在哪里
- for (int i = 0; i < len; i++) {
- // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
- int j = L + i - 1;
- // 如果右边界超限制,则退出循环
- if (j >= len) {
- break;
- }
- if (charArray[i] != charArray[j]) {
- dp[i][j] = false;
- } else if (charArray[i] == charArray[j]){
- // 字符相等,且长度小于等于2
- if (j - i <= 2) {
- dp[i][j] = true;
- } else {
- // 且字符相等,长度大于2
- dp[i][j] = dp[i + 1][j - 1];
- }
- }
-
-
- // 只要dp[i][j] == true 成立,就表示字符串s[i...j] 是回文子串,此时记录长度和起始位置
- // 每次循环要记录一下“最长的”
- if (dp[i][j] && j - i + 1 > maxLen) {
- maxLen = j - i + 1;
- begin = i;
- }
-
- }
-
- }
-
- return s.substring(begin, begin + maxLen);
- }
- }
text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。一条包含字母 A-Z
的消息通过以下映射进行了 编码 :
'A' -> "1" 'B' -> "2" ... 'Z' -> "26" 要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为: "AAJF" ,将消息分组为 (1 1 10 6) "KJF" ,将消息分组为 (11 10 6) 注意,消息不能分组为 (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6" 和 "06" 在映射中并不等价。 给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数
- public static int numDecodings(String s) {
-
- int n = s.length();
- int[] dp = new int[n + 1];
- dp[0] = 1;
- for (int i = 1; i <= n; i++) {
- if (s.charAt(i - 1) != '0') {
- dp[i] = dp[i - 1];
- }
- // 这里包含条件"当前数字为0" 的情况
- if (i > 1 && s.charAt(i - 2) != '0' && calculateSum(s.charAt(i - 2), s.charAt(i - 1)) <= 26) {
- dp[i] += dp[i - 2];
- }
- }
- return dp[n];
-
- }
-
- public static Integer calculateSum(char a, char b) {
- return (a - '0') * 10 + (b - '0');
- }
给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。
- public static int strStr(String haystack, String needle) {
- int n = haystack.length();
- int m = needle.length();
-
- for (int i = 0; i + m <= n; i ++) {
- boolean flag = true;
- for (int j = 0; j < m; j++) {
- if (haystack.charAt(i + j) != needle.charAt(j)) {
- flag = false;
- break;
- }
- }
- if (flag) {
- return i;
- }
- }
- return -1;
- }
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""
。
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串 ""。
- public String longestCommonPrefix(String[] strs) {
- if (strs == null || strs.length == 0) {
- return "";
- }
- // 第一个字符串的长度
- int length = strs[0].length();
- // 字符串数组的个数
- int count = strs.length;
- for (int i = 0; i < length; i++) {
- char c = strs[0].charAt(i);
- for (int j = 1; j < count; j++) {
- if (i == strs[j].length() || strs[j].charAt(i) != c) {
- return strs[0].subSequence(0, i).toString();
- }
- }
- }
- return strs[0];
- }
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
字母和数字都属于字母数字字符。
给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。
- public static boolean isPalindrome(String s) {
- StringBuilder stringBuilder = new StringBuilder();
- int length = s.length();
- for (int i = 0; i < length; i++) {
- char c = s.charAt(i);
- if (Character.isLetterOrDigit(c)) {
- stringBuilder.append(Character.toLowerCase(c));
- }
- }
- StringBuilder reversed = new StringBuilder(stringBuilder.toString()).reverse();
- return reversed.toString().equals(stringBuilder.toString());
- }
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
字母异位词 是由重新排列源单词的字母得到的一个新单词,所有源单词中的字母通常恰好只用一次。
- public List<List<String>> groupAnagrams(String[] strs) {
-
- Map<String, List<String>> map = new HashMap();
- for (String string : strs) {
- char[] array = string.toCharArray();
- Arrays.sort(array);
- String key = new String(array);
- List list = map.getOrDefault(map.get(key), new ArrayList());
- list.add(array);
- map.put(key, list);
- }
- return new ArrayList<List<String>>(map.values());
- }
- public int calculate(String s) {
-
- Deque<Integer> stack = new LinkedList<Integer>();
- char preSign = '+';
- int num = 0;
- int n = s.length();
- for (int i = 0; i < n; i++) {
- if (Character.isDigit(s.charAt(i))) {
- num = num * 10 + s.charAt(i) - '0';
- }
- if (!Character.isDigit(s.charAt(i))
- && s.charAt(i) != ' '
- || i == n - 1) {
- switch(preSign) {
- case '+':
- stack.push(num);
- break;
- case '-':
- stack.push(-num);
- break;
- case '*':
- stack.push(stack.pop() * num);
- break;
- case '/':
- stack.push(stack.pop() / num);
- break;
- }
- preSign = s.charAt(i);
- num = 0;
- }
- }
- int ans = 0;
- while(!stack.isEmpty()) {
- ans += stack.pop();
- }
- return ans;
- }
- public String serialize(TreeNode root) {
- return rserialize(root, "");
- }
-
- // 这是先根遍历,根、左、右。继续向下的时候,如果左边一直有,那么就是一直左左左
- public String rserialize(TreeNode root, String str) {
- if (root == null) {
- str += "None,";
- } else {
- str += String.valueOf(root.val) + ",";
- str = rserialize(root.left, str);
- str = rserialize(root.right, str);
- }
- return str;
- }
-
- public TreeNode deserialize(String data) {
- String[] dataArray = data.split(",");
- List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
- return rdeserialize(dataList);
- }
-
- // 所以在解析的时候,也是按照根、左、右的顺序处理
- public TreeNode rdeserialize(List<String> dataList) {
- if (dataList.get(0).equals("None")) {
- dataList.remove(0);
- return null;
- }
-
- TreeNode root = new TreeNode(Integer.valueOf(dataList.get(0)));
- dataList.remove(0);
- root.left = rdeserialize(dataList);
- root.right = rdeserialize(dataList);
-
- return root;
- }
- public List<Integer> postorderTraversal(TreeNode root) {
- List<Integer> result = new ArrayList<>();
- postorder(root, result);
- return result;
-
- }
-
- public void postorder(TreeNode root, List<Integer> res) {
- if (root == null) {
- return;
- }
- postorder(root.left, res);
- postorder(root.right, res);
- res.add(root.val);
- }
- public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
- List<List<Integer>> ans = new LinkedList<List<Integer>>();
- if (root == null) {
- return ans;
- }
-
- Queue<TreeNode> nodeQueue = new ArrayDeque<TreeNode>();
- nodeQueue.offer(root);
- boolean isOrderLeft = true;
-
- while (!nodeQueue.isEmpty()) {
- Deque<Integer> levelList = new LinkedList<Integer>();
- int size = nodeQueue.size();
- for (int i = 0; i < size; ++i) {
- TreeNode curNode = nodeQueue.poll();
- if (isOrderLeft) {
- levelList.offerLast(curNode.val);
- } else {
- levelList.offerFirst(curNode.val);
- }
- if (curNode.left != null) {
- nodeQueue.offer(curNode.left);
- }
- if (curNode.right != null) {
- nodeQueue.offer(curNode.right);
- }
- }
- ans.add(new LinkedList<Integer>(levelList));
- isOrderLeft = !isOrderLeft;
- }
-
- return ans;
- }
给定两个整数数组 preorder 和 inorder ,其中 preorder 是二叉树的先序遍历, inorder 是同一棵树的中序遍历,请构造二叉树并返回其根节点。
给你一个二叉树的根节点 root
, 检查它是否轴对称。
使用递归:
- public boolean isSymmetric(TreeNode root) {
- if (root == null) {
- return true;
- }
- return dfs(root.left, root.right);
- }
-
- public boolean dfs(TreeNode left, TreeNode right) {
- // 递归终止条件是两个节点都为空;或者其中只有一个为空;或者两个节点值不相等
- if (left == null && right == null) {
- return true;
- }
- if (left == null || right == null) {
- return false;
- }
- if (left.val != right.val) {
- return false;
- }
- // 再递归比较左节点左孩子、右节点有孩子等等
- return dfs(left.right, right.left) && dfs(left.left, right.right);
- }
使用迭代:
- public boolean isSymmetric(TreeNode root) {
- if (root == null || (root.left == null && root.right == null)) {
- return true;
- }
- // 用队列保存节点
- LinkedList<TreeNode> queue = new LinkedList<>();
- // 根节点的左右孩子分别放入队列中
- queue.add(root.left);
- queue.add(root.right);
- while(queue.size() > 0) {
- // 取出两个节点
- TreeNode left = queue.removeFirst();
- TreeNode right = queue.removeFirst();
- // 判断
- if (left == null && right == null) {
- continue;
- }
- if (left == null || right == null) {
- return false;
- }
- if (left.val != right.val) {
- return false;
- }
- queue.add(left.left);
- queue.add(right.right);
- queue.add(left.right);
- queue.add(right.left);
- }
- return true;
- }
- public Node connect(Node root) {
- if (root == null) {
- return null;
- }
- if (root.left != null) {
- root.left.next = root.right;
- if (root.next != null) {
- root.right.next = root.next.left;
- }
- }
- connect(root.left);
- connect(root.right);
- return root;
- }
迭代:- public Node connect(Node root) {
- if (root == null) {
- return root;
- }
- LinkedList<Node> queue = new LinkedList<>();
- queue.add(root.left);
- queue.add(root.right);
- while (queue.size() != 0) {
- int length = queue.size();
- Node tempNode = queue.poll();
- if (tempNode == null) {
- return root;
- }
- queue.add(tempNode.left);
- queue.add(tempNode.right);
- for (int i = 0; i < length - 1; i++) {
- Node popedNode = queue.poll();
- queue.add(popedNode.left);
- queue.add(popedNode.right);
- tempNode.next = popedNode;
- tempNode = popedNode;
- }
- }
- return root;
- }
- public Node connect(Node root) {
- if (root == null) {
- return root;
- }
- // cur 是当前层的没一层链表的指针
- Node cur = root;
- // 外层while 循环是针对二叉树的高度的遍历
- while (cur != null) {
- Node dummy = new Node(0);
- Node pre = dummy;
- while (cur != null) {
- // 遍历当前层,然后串联下一层
- if (cur.left != null) {
- pre.next = cur.left;
- pre = pre.next;
- }
- if (cur.right != null) {
- pre.next = cur.right;
- pre = pre.next;
- }
- cur = cur.next;
- }
- // 上面的逻辑的作用,就是把下面cur 层下面那层的串联起来
- cur = dummy.next;
- }
- return root;
- }
- public int maxDepth(TreeNode root) {
- if (root == null) {
- return 0;
- } else {
- return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
- }
- }
此题求二叉树所有层的最大宽度,比较直观的方法是求出每一层的宽度,然后求出最大值。
注意:求每一层的宽度时,因为两端点间的null 节点也需要计入宽度,因此可以对节点进行编号。
一个编号为index 的左子节点的编号记为2 * index;右子节点编号为2 * index + 1
那么每层的最大宽度就是用每层节点的最大编号减去最小编号再加1。
遍历节点时,可以用广度优先搜索来遍历每一层的节点,并求出最大值。
迭代:
- public int widthOfBinaryTree(TreeNode root) {
- int res = 1;
- // 存放当前层的节点。最开始的时候就只有root 层。
- List<Pair<TreeNode, Integer>> arr = new ArrayList<>();
- arr.add(new Pair<>(root, 1));
-
- while (!arr.isEmpty()) {
- List<Pair<TreeNode, Integer>> tmp = new ArrayList<>();
- for (Pair<TreeNode, Integer> pair : arr) {
- TreeNode node = pair.getKey();
- int index = pair.getValue();
-
- if (node.left != null) {
- tmp.add(new Pair<>(node.left, 2 * index));
- }
-
- if (node.right != null) {
- tmp.add(new Pair<>(node.right, 2 * index + 1));
- }
- }
- res = Math.max(res, arr.get(arr.size() - 1).getValue() - arr.get(0).getValue() + 1);
- arr = tmp;
- }
- return res;
- }
递归dfs:
遍历时如果是先访问左子节点,再访问右子节点,每一层最先访问到的节点会是最左边的节点,即每一层编号的最小值,需要记录下来进行后续的比较。一次深度优先搜索中,需要当前节点到当前行最左边节点的宽度,以及对子节点进行深度优先搜索,求出最大宽度,并返回最大宽度。
- Map<Integer, Integer> levelMin = new HashMap<Integer, Integer>();
-
- public int widthOfBinaryTree(TreeNode root) {
- return dfs(root, 1, 1);
- }
-
- public int dfs(TreeNode node, int depth, int index) {
- if (node == null) {
- return 0;
- }
- // 每一层最先访问到的节点会是最左边的节点,即每一层编号的最小值
- levelMin.putIfAbsent(depth, index);
- return Math.max(index - levelMin.get(depth) + 1,
- Math.max(dfs(node.left, depth + 1, index * 2),
- dfs(node.right, depth + 1, index * 2 + 1)));
- }
- // 树的直径可以分解成子问题,就是每个节点的左右子节点的深度的和+1
- // 而不是经过每个节点的子树的“直径”
- int ans = 0;
- public int diameterOfBinaryTree(TreeNode root) {
- if (root == null) {
- return ans;
- }
- dfs(root);
- return ans - 1;
- }
-
- public int dfs(TreeNode node) {
- if (node == null) {
- return 0;
- }
- int left = dfs(node.left);
- int right = dfs(node.right);
- ans = Math.max(left + right + 1, ans);
- return Math.max(left, right) + 1;
- }
- Map<Integer, TreeNode> parent = new HashMap<Integer, TreeNode>();
- Set<Integer> visited = new HashSet<Integer>();
-
- public void dfs(TreeNode root) {
- if (root.left != null) {
- parent.put(root.left.val, root);
- dfs(root.left);
- }
- if (root.right != null) {
- parent.put(root.right.val, root);
- dfs(root.right);
- }
- }
-
- public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
- dfs(root);
- while (p != null) {
- visited.add(p.val);
- p = parent.get(p.val);
- }
- while (q != null) {
- if (visited.contains(q.val)) {
- return q;
- }
- q = parent.get(q.val);
- }
- return null;
- }
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。
这道题类似层序遍历,可以使用广度优先搜索算法。但是不需要像层序遍历那样,计算queue 中的节点的个数。
- public boolean hasPathSum(TreeNode root, int targetSum) {
-
- if (root == null) {
- return false;
- }
- Queue<TreeNode> nodeQueue = new LinkedList<>();
- Queue<Integer> valueQueue = new LinkedList<>();
-
- nodeQueue.offer(root);
- valueQueue.offer(root.val);
-
- while (!nodeQueue.isEmpty()) {
- // 这个不像层序遍历,这个不需要感知queue 的长度
- TreeNode tempNode = nodeQueue.poll();
- Integer tempVal = valueQueue.poll();
-
- // 如果当前节点是叶子节点,那么就进行判断
- if (tempNode.left == null && tempNode.right == null) {
- if (tempVal == targetSum) {
- return true;
- }
- continue;
- }
-
- // 子节点进队列
- if (tempNode.right != null) {
- nodeQueue.offer(tempNode.right);
- valueQueue.offer(tempNode.right.val + tempVal);
- }
- if (tempNode.left != null) {
- nodeQueue.offer(tempNode.left);
- valueQueue.offer(tempNode.left.val + tempVal);
- }
- }
- return false;
- }
递归:- public boolean hasPathSum(TreeNode root, int targetSum) {
-
- if (root == null) {
- return false;
- }
- if (root.left == null && root.right == null) {
- return targetSum == root.val;
- }
- return hasPathSum(root.left, targetSum - root.val)
- || hasPathSum(root.right, targetSum - root.val);
-
- }
使用大顶堆做正序排序;使用小顶堆做逆序排序。
构建堆,就是从最后一个非叶子节点开始,如果构建大顶堆,就是将每个parent 与child 进行对比,如果child 大于parent,那么进行调整。同时调整到child 位置的原parent 可能还是小于其child,所以要继续调整。
堆在算法题目中的应用主要包括以下几点:
- // 以下代码都认为父节点序号为0
-
- /**
- * 调整为大顶堆(此方法只是一个调整的过程,并不是建堆的)
- * @param arr 待调整的数组
- * @param parent 当前父节点的下标
- * @param length 需要对多少个元素进行调整
- */
- private static void adjustHeap(int[] arr, int parent, int length){
- //临时保存父节点
- int temp = arr[parent];
- //左子节点的下标
- int child = 2 * parent + 1;
- //如果子节点的下标大于等于当前需要比较的元素个数,则结束循环
- while(child < length){
- //判断左子节点和右子节点的大小,若右边大,则把child定位到右边
- if(child + 1 < length && arr[child] < arr[child + 1]){
- child ++;
- }
- //若child大于父节点,则交换位置,否则退出循环
- if(arr[child] > temp){
- //父子节点交换位置
- arr[parent] = arr[child];
- //因为交换位置之后,不能保证当前的子节点是它子树的最大值,所以需要继续向下比较,
- //把当前子节点设置为下次循环的父节点,同时,找到它的左子节点,继续下次循环
- parent = child;
- child = 2 * parent + 1;
- }else{
- //如果当前子节点小于等于父节点,则说明此时的父节点已经是最大值了,
- //因此无需继续循环
- break;
- }
- }
- //把当前节点值替换为最开始暂存的父节点值
- arr[parent] = temp;
- }
-
- public static void main(String[] args) {
- int[] arr = {4,1,9,3,7,8,5,6,2};
- // 构建一个大顶堆,从最下面的非叶子节点开始向上遍历
- // 这才是建堆的过程
- for (int i = arr.length/2 - 1 ; i >= 0; i--) {
- adjustHeap(arr,i,arr.length);
- }
- System.out.println(Arrays.toString(arr));
- }
-
-
-
- //堆排序,大顶堆,升序
- private static void heapSort(int[] arr){
- //构建一个大顶堆,从最下面的非叶子节点开始向上遍历
- for (int i = arr.length/2 - 1 ; i >= 0; i--) {
- adjustHeap(arr,i,arr.length);
- }
- System.out.println(Arrays.toString(arr));
- //循环执行以下操作:1.交换堆顶元素和末尾元素 2.重新调整为大顶堆
- for (int i = arr.length - 1; i > 0; i--) {
- //将堆顶最大的元素与末尾元素互换,则数组中最后的元素变为最大值
- int temp = arr[i];
- arr[i] = arr[0];
- arr[0] = temp;
- //从堆顶开始重新调整结构,使之成为大顶堆
- // i代表当前数组需要调整的元素个数,是逐渐递减的
- adjustHeap(arr,0,i);
- }
-
- }
-
- public int[] topKFrequent(int[] nums, int k) {
- Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
- for (int num : nums) {
- occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
- }
-
- // int[] 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
- PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
- public int compare(int[] m, int[] n) {
- return m[1] - n[1];
- }
- });
- for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
- int num = entry.getKey(), count = entry.getValue();
- if (queue.size() == k) {
- if (queue.peek()[1] < count) {
- queue.poll();
- queue.offer(new int[]{num, count});
- }
- } else {
- queue.offer(new int[]{num, count});
- }
- }
- int[] ret = new int[k];
- for (int i = 0; i < k; ++i) {
- ret[i] = queue.poll()[0];
- }
- return ret;
- }
生成map 的时间复杂度是O(n);- public int kthSmallest(int[][] matrix, int k) {
- // 一共n 行
- int n = matrix.length;
- int left = matrix[0][0];
- int right = matrix[n - 1][n - 1];
- while (left < right) {
- int mid = (left + right) / 2;
- if (check(matrix, mid, k, n)) {
- right = mid;
- } else {
- left = mid + 1;
- }
- }
- return left;
- }
-
- public boolean check(int[][] matrix, int mid, int k ,int n) {
-
- int i = n - 1;
- int j = 0;
- int num = 0;
- while (i >=0 && j < n) {
- if (matrix[i][j] <= mid) {
- num += i + 1;
- j ++;
- } else {
- i --;
- }
- }
- return num >= k;
-
- }
上述寥寥几行,就把一个动态规划问题的基本点都描述出来了.
amount
。- public static int coinChange(int[] coins, int amount) {
- // dp 数组代表凑出金额i 的时候,最少需要走多少步
- int[] dp = new int[amount+1];
- // 初始化dp 数组,以方便后续的比较
- Arrays.fill(dp, amount + 1);
- // 初始化条件
- dp[0] = 0;
- // 循环amount 次,确认累积到金额i 需要多少次
- for (int i = 1; i <= amount; i ++) {
- // 按照每个硬币的面额做判断
- for (int coin : coins) {
- // 当i < coin 的时候,证明没有解,不用做什么变化
- if (coin <= i) {
- dp[i] = Math.min(dp[i], dp[i - coin] + 1);
- }
- }
- }
- // 判断当前是否有结果,就和初始化的数字做呼应
- return dp[amount] == amount + 1 ? -1 : dp[amount];
- }
对于“最优子结构”,很多问题可能都具有“最优子结构”,但是不存在“重叠子问题”,所以不能被归为“动态规划系列”。如果想满足“最优子结构”,子问题间必须相互独立。
对于动态规划,拿300 题和53 题举例子。两道题都是使用一维数组的,但是有点不一样,现在我总结不出来,理解一下,理解并总结不出来的话,那就记住好了。注意特点,300 是子序列,53 是子数组。
- public int lengthOfLIS(int[] nums) {
-
- if (nums.length == 0) {
- return 0;
- }
-
- // 定义dp 数组,dp[i] 表示以第i 个数字结尾,最长上升子序列的长度。
- int[] dp = new int[nums.length];
- dp[0] = 1;
- int maxAns = 1;
- // 我们从小到大计算 \textit{dp}dp 数组的值,在计算dp[i] 之前,我们已经计算出dp[0…i−1] 的值,则状态转移方程为:
- // dp[i]=max(dp[j])+1,其中0≤j<i且num[j]<num[i]
- for (int i = 0; i < nums.length; i++) {
- dp[i] = 1;
- for (int j = 0; j < i; j++) {
- if (nums[i] > nums[j]) {
- dp[i] = Math.max(dp[i], dp[j] + 1);
- }
- }
- maxAns = Math.max(maxAns, dp[i]);
- }
- return maxAns;
- }
nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。- // 两种解法,一看就懂
-
-
- // 第一种
- public int maxSubArray(int[] nums) {
- if (nums.length == 0) {
- return 0;
- }
- int[] dp = new int[nums.length];
-
- dp[0] = nums[0];
- int ans = nums[0];
- for (int i = 1; i < nums.length; i++) {
-
- dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
- ans = Math.max(dp[i], ans);
-
- }
- return ans;
- }
-
-
- // 第二种
-
-
- public int maxSubArray(int[] nums) {
- int pre = 0, maxAns = nums[0];
- for (int x : nums) {
- pre = Math.max(pre + x, x);
- maxAns = Math.max(maxAns, pre);
- }
- return maxAns;
- }
- public int maxProduct(int[] nums) {
- int length = nums.length;
- int[] maxF = new int[length];
- int[] minF = new int[length];
- maxF[0] = nums[0];
- minF[0] = nums[0];
- int ans = maxF[0];
- for (int i = 1; i < length; i++) {
- maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
- minF[i] = Math.min(maxF[i - 1] * nums[i], Math.min(nums[i], minF[i - 1] * nums[i]));
-
- ans = Math.max(ans, maxF[i]);
- }
- return ans;
- }
标签:动态规划
首先初始化长度为 n+1 的数组 dp,每个位置都为 0
如果 n 为 0,则结果为 0
对数组进行遍历,下标为 i,每次都将当前数字先更新为最大的结果,即 dp[i]=i,比如 i=4,最坏结果为 4=1+1+1+1 即为 4 个数字
动态转移方程为:dp[i] = MIN(dp[i], dp[i - j * j] + 1),i 表示当前数字,j*j 表示平方数
- class Solution {
- public int numSquares(int n) {
- int[] dp = new int[n + 1]; // 默认初始化值都为0
- for (int i = 1; i <= n; i++) {
- dp[i] = i; // 最坏的情况就是每次+1
- for (int j = 1; j * j <= i; j++) {
- dp[i] = Math.min(dp[i], dp[i - j * j] + 1); // 动态转移方程
- }
- }
- return dp[n];
- }
- }
- public static int coinChange(int[] coins, int amount) {
-
- int[] dp = new int[amount + 1];
- // 初始化数组,每个元素为amount + 1 的原因是为了避免混淆,初始化成别的也行,在最后做判断的时候确认一下就好
- for (int i = 1; i <= amount; i++) {
- dp[i] = amount + 1;
- }
-
- // 第一层for 循环是对每个子结构做遍历查找,即amount 为1,为2,为3... 的时候
- for (int i = 1; i <= amount; i++) {
- // 对每个选择做判断
- for (int c : coins) {
- if (i - c < 0) {
- continue;
- }
- // 找出最小的
- dp[i] = Math.min(dp[i], dp[i - c] + 1);
- }
-
- }
- // 判断当前是否有结果,就和初始化的数字做呼应
- return dp[amount] == amount + 1 ? -1 : dp[amount];
-
- }
- public static int climbStairs(int n) {
- if (n == 1) {
- return 1;
- }
- if (n == 2) {
- return 2;
- }
- int pre = 1;
- int cur = 2;
- int result = 3;
- for (int i = 3; i <= n; i++) {
- result = pre + cur;
- pre = cur;
- cur = result;
- }
- return result;
- }
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
一维数组进行动态规划,dp[i] 代表到了第i 个屋子能偷窃到的最高金额。- public int rob(int[] nums) {
- if (nums == null || nums.length == 0) {
- return 0;
- }
- int length = nums.length;
- if (nums.length == 1) {
- return nums[0];
- }
- int dp[] = new int[length];
- dp[0] = nums[0];
- dp[1] = Math.max(nums[0], nums[1]);
- for (int i = 2; i < length; i++) {
- dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
- }
- return dp[length -1 ];
- }
- public int uniquePaths(int m, int n) {
-
- int[][] f = new int[m][n];
- for (int i = 0; i < m; i++) {
- f[i][0] = 1;
- }
- for (int j = 0; j < n; j++) {
- f[0][j] = 1;
- }
- for (int i = 1; i < m; i++) {
- for (int j = 1; j < n; j++) {
- f[i][j] = f[i - 1][j] + f[i][j - 1];
- }
- }
- return f[m - 1][n - 1];
- }
- public static int minDistance(String s1, String s2) {
-
- int m = s1.length();
- int n = s2.length();
- int[][] dp = new int[m + 1][n + 1];
- // base case
- for (int i = 1; i <= m; i++) {
- dp[i][0] = i;
- }
- for (int j = 1; j < n; j++) {
- dp[0][j] = j;
- }
- for (int i = 1; i <= m; i++) {
- for (int j = 1; j <= n; j++) {
- if (s1.charAt(i) == s2.charAt(j)) {
- dp[i][j] = dp[i - 1][j - 1];
- } else {
- dp[i][j] = Math.min(dp[i - 1][j] + 1, Math.min(dp[i][j - 1] + 1, dp[i - 1][j - 1]));
- }
- }
- }
- return dp[m][n];
- }
f[i][j]
表示以 (i, j)
为右下角的正方形的最大边长,同时这个最大边长也可以表示以(i, j) 为右下角的正方形的个数。- public int countSquares(int[][] matrix) {
- int row = matrix.length;
- int column = matrix[0].length;
- int[][] dp = new int[row][column];
- int ans = 0;
-
- for (int i = 0; i < row; i++) {
- for (int j = 0; j < column; j++) {
- if (i == 0 || j == 0) {
- dp[i][j] = matrix[i][j];
- } else if (matrix[i][j] == 0) {
- dp[i][j] = 0;
- } else {
- dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
- }
- ans += dp[i][j];
- }
- }
- return ans;
- }
221. 最大正方形
在一个由 '0'
和 '1'
组成的二维矩阵内,找到只包含 '1'
的最大正方形,并返回其面积
使用动态规划,我们用dp[i][j] 表示以(i, j) 为右下角,且只包含1 的正方形的边长的最大值。
- public int maximalSquare(char[][] matrix) {
- int row = matrix.length;
- int column = matrix[0].length;
- int[][] dp = new int[row][column];
- int ans = 0;
-
- for (int i = 0; i < row; i++) {
- for (int j = 0; j < column; j++) {
- if (i == 0 || j == 0) {
- if (matrix[i][j] == '1') {
- dp[i][j] = 1;
- }
- } else if (matrix[i][j] == '0') {
- dp[i][j] = 0;
- } else {
- dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
- }
- ans = Math.max(ans, dp[i][j]);
- }
- }
- return ans;
- }
dp对容量的init都是dp[v+1], 从空开始init 如果要减少空间的话,把dp[i]省掉,dp[v+1]的循环逆序
- class Solution {
-
- /**
- * @param v v[i] 代表第i 个物品的体积
- * @param w w[i] 代表第i 个物品的价值
- * @param V 背包的总体积
- * @return
- */
- public int findMax(int[] v, int[] w, int V) {
-
- // 物品个数
- int n = v.length;
-
- // 构造二维dp 数组。这个二维数组整体多了一个0 行和0 列,所以不用初始化。
- // 否则就要初始化第一行,小于第i 个物品体积的遍历体积都是0,大于等于第i 个物品体积的遍历体积的位置数字都是第i 个物品的价值。i = 0
- // n 个物品,所以有n 行;因为背包总体积是V,所以遍历V,从0 开始
- // 数组初始化,各个元素都是0
- int[][] f = new int[n + 1][V + 1];
-
- for (int i = 1; i <= n; i++) {
- for (int j = 1; j <= V; j++) {
- // 如果第i 个物品的体积大于当前遍历到的最大的体积,那么当前物品肯定不能选择
- if (v[i - 1] > j) {
- f[i][j] = f[i -1][j];
- } else {
- // 转移方程计算f(i,j) 的大小
- // 分为选择还是不选择
- f[i][j] = Math.max(f[i][j - v[i - 1]] + w[i-1], f[i - 1][j]);
- }
- }
- }
- return f[n][V];
-
- }
-
-
- public int findMax2(int[] v, int[] w, int V) {
- // 可以优化控件,但是不能优化时间,时间还是O(n方)
- // 由状态转移方程可以看出,第i 行结果只和第i - 1 行结果有关。而为了防止数据被覆盖,所以j 要从后往前遍历
-
- int n = v.length;
- int[] dpTable = new int[V + 1];
- // 需要初始化
- for (int k = w[0]; k <= V; k++) {
- dpTable[k] = w[0];
- }
-
- for (int i = 1; i <= n; i++) {
- for (int j = V; j >=1; j--) {
- if (v[i - 1] > j) {
- // 如果第i 个物品的体积大于遍历体积数字,那么不变(还是上一行的结果)
- } else {
- // 否则做状态转移,判断选不选第i 个物品
- dpTable[j] = Math.max(dpTable[j - v[i - 1]] + w[i - 1], dpTable[j]);
- }
- }
- }
- return dpTable[V];
- }
-
- public static void main(String[] args) {
- Solution solution = new Solution();
- int[] v = {5,3,4,2};
- int[] w = {60,50,70,30};
-
- System.out.println(solution.findMax2(v, w, 5));
- }
-
- }
- public boolean canPartition(int[] nums) {
-
- int sum = 0;
-
- int length = nums.length;
- if (length < 2) {
- return false;
- }
-
- for (int num : nums) {
- sum += num;
- }
-
- if (sum % 2 != 0) {
- return false;
- }
-
- int target = sum / 2;
-
- boolean[][] dp = new boolean[length + 1][target + 1];
- dp[0][0] = true;
-
- for (int i = 1; i <= length; i++) {
- for (int j = 1; j <= target; j++) {
-
- if (nums[i - 1] > j) {
- // 不能选择
- dp[i][j] = dp[i - 1][j];
- } else {
- dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i - 1]];
- }
-
- }
- }
- return dp[length][target];
- }
- // 回溯解法,其中选择列表就是加法或者减法,nums 的遍历在backtrack 中使用参数“i” 实现
-
- int result = 0;
- public int findTargetSumWays(int[] nums, int S) {
-
- if (nums.length == 0) {
- return 0;
- }
- backtrack(nums, 0, 0, S);
- return result;
- }
-
- void backtrack(int[] nums, int i, int additionResult, int target) {
- // 判断条件等于length 而不是等于length - 1 的原因是,i == nums.length 的时候,还是需要继续进行循环的
- if (i == nums.length) {
- if (additionResult == target) {
- result ++;
- }
- return;
- }
-
- additionResult += nums[i];
- backtrack(nums, i + 1, additionResult, target);
- additionResult -= nums[i];
-
- additionResult -= nums[i];
- backtrack(nums, i + 1, additionResult, target);
- additionResult += nums[i];
- }
- class Solution {
-
- /**
- * @param v 每个物品的体积
- * @param w 每个物品的价值(重量)
- * @param V 背包的容积
- * @return 最大价值(重量)
- */
- public int findCompleteBackpackResult(int[] v, int[] w, int V) {
-
- // n 个物品
- int n = v.length;
-
- int dp[][] = new int[n + 1][V + 1];
-
- for (int i = 1; i <= n; i++) {
- for (int j = 1; j <= V; j++) {
- if (v[i - 1] > j) {
- dp[i][j] = dp[i - 1][j];
- } else {
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - v[i - 1]] + w[i - 1]);
- }
- }
- }
- return dp[n][V];
- }
-
- public int findCompleteBackpackResult2(int[] v, int[] w, int V) {
-
- // n 个物品
- int n = v.length;
-
- int[] dp = new int[V + 1];
-
- // 初始化dp 数组
- for (int k = v[0]; k <= V; k ++) {
- // k 是当前遍历到的容量,v[0] 是第一个物品的体积,w[0] 是第一个物品的价值(重量)
- dp[k] = k / v[0] * w[0];
- }
-
- for (int i = 1; i <= n; i++) {
- for (int j = 1; j <= V; j++) {
- if (v[i - 1] > j) {
- // 如果第i 个物品的体积大于当前遍历到的容积,则第i 个物品无法选择
- } else {
- dp[j] = Math.max(dp[j], dp[j - v[i - 1]] + w[i - 1]);
- }
- }
- }
-
- return dp[V];
- }
-
- public static void main(String[] args) {
- Solution solution = new Solution();
- int[] v = {3,2,5,1,6,4};
- int[] w = {6,5,10,2,16,8};
-
- System.out.println(solution.findCompleteBackpackResult(v, w, 10));
-
- System.out.println(solution.findCompleteBackpackResult2(v, w, 10));
- }
-
- }
回溯算法,就是一个决策树遍历的过程。
参考:回溯算法套路详解 - 知乎
决策树解释:比如说解决一个问题,一共有三个步骤,每个步骤可以选择“是”或者“否”。就是一个三层的二叉树(不考虑根节点)。决策选择哪个,形成的一棵树,就叫决策树。
解决这个问题,只需要考虑:
1)路径,即已经做出的选择。2)选择列表,就是当前可以做的选择。3)结束条件,就是已经到达决策树底层,无法再做选择的条件。
框架:
- result = Lists.newArrayList();
- void backtrack(路径, 选择列表) {
- if 满足结束条件:
- result.add(路径)
- return
-
- for 选择 in 选择列表:
- 做选择
- backtrack(路径, 选择列表)
- 撤销选择
- }
其核心就是 for 循环里面的递归,在递归调用之前“做选择”,在递归调用之后“撤销选择”
- class Solution {
-
- List<List<Integer>> result = new ArrayList<>();
-
- List<List<Integer>> premute(int[] nums) {
-
- LinkedList<Integer> track = new LinkedList<>();
- backTrack(nums, track);
- return result;
- }
-
- void backTrack(int[] nums, LinkedList<Integer> track) {
-
- // 出发结束条件
- if (track.size() == nums.length) {
- result.add(new ArrayList<>(track));
- return;
- }
- for (int i = 0; i < nums.length; i ++ ) {
- if (track.contains(nums[i])) {
- continue;
- }
- track.add(nums[i]);
- backTrack(nums, track);
- track.removeLast();
- }
-
- }
-
- }
- 参考:https://zhuanlan.zhihu.com/p/109523146
-
-
- 方法1:对于数组中每个元素,选择或者是不选择:
-
- List<List<Integer>> result = new ArrayList<>();
- public List<List<Integer>> subsets(int[] nums) {
- int len = nums.length;
-
- if (len == 0) {
- return result;
- }
- Deque<Integer> deque = new LinkedList<>();
- dfs(nums, len, 0, deque);
- return result;
-
- }
-
- public void dfs(int[] nums, int len, int index, Deque<Integer> path) {
-
- if (index == len) {
- result.add(new ArrayList<>(path));
- return;
- }
- path.addLast(nums[index]);
- dfs(nums, len, index + 1, path);
- path.removeLast();
- dfs(nums, len, index + 1, path);
-
- }
-
-
- 方法2:回溯法
-
- public List<String> generateParenthesis(int n) {
-
- List<String> resultList = new ArrayList<>();
- backtrack(resultList, new StringBuilder(), 0, 0, n);
- return resultList;
-
- }
-
- public void backtrack(List<String> ans, StringBuilder cur, int left, int right, int max) {
- if (cur.length() == max * 2) {
- ans.add(cur.toString());
- return;
- }
- if (left < max) {
- cur.append('(');
- backtrack(ans, cur, left + 1, right, max);
- cur.deleteCharAt(cur.length() - 1);
- }
- if (right < left) {
- cur.append(')');
- backtrack(ans, cur, left, right + 1, max);
- cur.deleteCharAt(cur.length() - 1);
- }
- }
拓扑排序详解 通俗易懂 - 知乎 拓扑排序
- public int numIslands(char[][] grid) {
- int count = 0;
- int height = grid.length;
- int width = grid[0].length;
- for (int i = 0; i < height; i++) {
- for (int j = 0; j < width; j++) {
- if (grid[i][j] == '1') {
- // 遍历所有连接的陆地,将其置位"已访问"
- dfs(grid, i, j, width, height);
- count ++;
- }
- }
- }
- return count;
- }
-
- // 函数的目的是做dfs,把周边的是1 的那些都给置位"已遍历过"状态
- public void dfs(char[][] grid, int i, int j, int width, int height) {
- // dfs 的时候首先要记得basecase
- if (i < 0 || j < 0 ||
- i >= height || j >= width ||
- grid[i][j] == '0' || grid[i][j] == '2') {
- return;
- }
- grid[i][j] = '2';
- dfs(grid, i + 1, j, width, height);
- dfs(grid, i -1, j, width, height);
- dfs(grid, i, j + 1, width, height);
- dfs(grid, i, j -1, width, height);
- }
广度优先搜索: - public int numIslands(char[][] grid) {
- int count = 0;
- int height = grid.length;
- int width = grid[0].length;
- for (int i = 0; i < height; i++) {
- for (int j = 0; j < width; j++) {
- if (grid[i][j] == '1') {
- count ++;
- // 然后进行广度优先搜索,对"已访问过的"陆地进行标记
- Queue<Pair<Integer, Integer>> queue = new LinkedList<>();
- queue.add(new Pair<>(i, j));
- // grid[i][j] = '2';
- while (queue.size() != 0) {
- Pair<Integer, Integer> land = queue.poll();
- int r = land.getKey();
- int c = land.getValue();
- grid[r][c] = '2';
- // 然后分别对旁边的那些"陆地"做查询并入队列
- if (r + 1 < height && grid[r + 1][c] == '1') {
- queue.offer(new Pair<>(r + 1, c));
- // grid[r + 1][c] = '2';
- }
- if (r - 1 >= 0 && grid[r -1][c] == '1') {
- queue.offer(new Pair<>(r - 1, c));
- // grid[r-1][c] = '2';
- }
- if (c + 1 < width && grid[r][c + 1] == '1') {
- queue.offer(new Pair<>(r, c + 1));
- // grid[r][c + 1] = '2';
- }
- if (c - 1 >= 0 && grid[r][c -1] == '1') {
- queue.offer(new Pair<>(r, c - 1));
- // grid[r][c - 1] = '2';
- }
- }
- }
- }
- }
- return count;
- }
给定编号从 0
到 n-1
的 n
个节点和一个无向边列表(每条边都是一对节点),请编写一个函数来计算无向图中连通分量的数目。
注意:
你可以假设在 edges 中不会出现重复的边。而且由于所以的边都是无向边,[0, 1] 与 [1, 0] 相同,所以它们不会同时在 edges 中出现。
用并查集、dfs、bfs 都可以做,但是我现在看不懂。
- class LRUCache {
-
- // 维护一个双向链表的节点结构
- class Node {
- int key, value;
- Node pre, next;
-
- Node(int key, int value) {
- this.key = key;
- this.value = value;
- pre = this;
- next = this;
- }
- }
-
- // LRU Cache的容量
- private final int capacity;
- // dummy节点是一个冗余节点,dummy的next是链表的第一个节点,dummy的pre是链表的最后一个节点
- private Node dummy;
- // 保存key-Node对,Node是双向链表节点,为了“快速查找”
- private Map<Integer, Node> map;
-
- public LRUCache(int capacity) {
- this.capacity = capacity;
- dummy = new Node(0, 0);
- map = new HashMap<>();
- }
-
- // get 的时候,除了正常取值,还要改变元素顺序,通过两个步骤完成
- // 1. 在链表中删除node
- // 2. 将node 添加到链表末尾
- public int get(int key) {
- Node node = map.get(key);
- if (node == null) {
- return -1;
- }
- remove(node);
- add(node);
- return node.value;
- }
-
- // put 的时候要判断当前节点是否存在,分别分析
- public void put(int key, int value) {
- Node node = map.get(key);
- // 如果不存在,需要判断当前容量大小,然后做删除最近最少未使用节点
- if (node == null) {
- if (map.size() >= capacity) {
- // 删除的时候,一个是要删除map 中的值
- map.remove(dummy.next.key);
- // 另一个是要删掉链表中的节点
- remove(dummy.next);
- }
- node = new Node(key, value);
- // 然后map 中存
- map.put(key, node);
- // 然后链表中存
- add(node);
- } else {
- // 如果结构中存在这个值,那么只需要改变这个node 的顺序即可
- map.remove(node.key);
- remove(node);
- node = new Node(key, value);
- map.put(key, node);
- add(node);
- }
- }
-
- /**
- * 在链表尾部添加新节点
- *
- * @param node 新节点
- */
- private void add(Node node) {
- dummy.pre.next = node;
- node.pre = dummy.pre;
- node.next = dummy;
- dummy.pre = node;
- }
-
- /**
- * 从双向链表中删除该节点
- *
- * @param node 要删除的节点
- */
- private void remove(Node node) {
- node.pre.next = node.next;
- node.next.pre = node.pre;
- }
- }
- class Solution {
- public int findCircleNum(int[][] isConnected) {
- // int[][] isConnected 是无向图的邻接矩阵,n 为无向图的顶点数量
- int n = isConnected.length;
- // 定义 boolean 数组标识顶点是否被访问
- boolean[] visited = new boolean[n];
- // 定义 cnt 来累计遍历过的连通域的数量
- int cnt = 0;
- for (int i = 0; i < n; i++) {
- // 若当前顶点 i 未被访问,说明又是一个新的连通域,则遍历新的连通域且cnt+=1.
- if (!visited[i]) {
- cnt++;
- dfs(i, isConnected, visited);
- }
- }
- return cnt;
- }
-
- private void dfs(int i, int[][] isConnected, boolean[] visited) {
- // 对当前顶点 i 进行访问标记
- visited[i] = true;
-
- // 继续遍历与顶点 i 相邻的顶点(使用 visited 数组防止重复访问)
- for (int j = 0; j < isConnected.length; j++) {
- if (isConnected[i][j] == 1 && !visited[j]) {
- dfs(j, isConnected, visited);
- }
- }
- }
- }
-
-
- 作者:sweetiee
- 链接:https://leetcode-cn.com/problems/number-of-provinces/solution/dfs-bfs-bing-cha-ji-3-chong-fang-fa-ji-s-edkl/
- 来源:力扣(LeetCode)
- 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- class Solution {
- public int findCircleNum(int[][] isConnected) {
- // int[][] isConnected 是无向图的邻接矩阵,n 为无向图的顶点数量
- int n = isConnected.length;
- // 定义 boolean 数组标识顶点是否被访问
- boolean[] visited = new boolean[n];
-
- // 定义 cnt 来累计遍历过的连通域的数量
- int cnt = 0;
- Queue<Integer> queue = new LinkedList<>();
- for (int i = 0; i < n; i++) {
- // 若当前顶点 i 未被访问,说明又是一个新的连通域,则bfs新的连通域且cnt+=1.
- if (!visited[i]) {
- cnt++;
- queue.offer(i);
- visited[i] = true;
- while (!queue.isEmpty()) {
- int v = queue.poll();
- for (int w = 0; w < n; w++) {
- if (isConnected[v][w] == 1 && !visited[w]) {
- visited[w] = true;
- queue.offer(w);
- }
- }
- }
- }
- }
- return cnt;
- }
- }
-
- 作者:sweetiee
- 链接:https://leetcode-cn.com/problems/number-of-provinces/solution/dfs-bfs-bing-cha-ji-3-chong-fang-fa-ji-s-edkl/
- 来源:力扣(LeetCode)
- 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
- /**
- * 并查集,要实现的功能:合并、查找
- */
-
- public int findCircleNum(int[][] isConnected) {
- // isConnected 是n x n 矩阵
- int provinces = isConnected.length;
- // 保存每个节点对应的祖宗节点下标
- int[] parent = new int[provinces];
- for (int i = 0; i < provinces; i++) {
- // 初始化,每个节点是是自己本身的祖宗节点
- parent[i] = i;
- }
- // 遍历isConnected 二维数组,只需要遍历右上角就可以
- for (int i = 0; i < provinces; i++) {
- for (int j = i + 1; j < provinces; j++) {
- if (isConnected[i][j] == 1) {
- union(parent, i, j);
- }
- }
- }
- // 最后遍历parent 数组,确定结果
- int circles = 0;
- for (int i = 0; i < provinces; i++) {
- if (parent[i] == i) {
- circles ++;
- }
- }
- return circles;
-
- }
-
- // 合并功能。合并index1 和index2
- public void union(int[] parent, int index1, int index2) {
-
- // 使index1 的祖宗节点等于index2 的祖宗节点。这样两个节点就联合了。
- parent[find(parent, index1)] = find(parent, parent[index2]);
-
- }
-
- // 查找功能
- public int find(int[] parent, int index) {
- // 如果符合此条件,说明index 节点还有祖宗点,则需要继续寻找祖宗节点
- if (parent[index] != index) {
- parent[index] = find(parent, parent[index]);
- }
- return parent[index];
- }
nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target
的那 两个 整数,并返回它们的数组下标。- public int[] twoSum(int[] nums, int target) {
-
- Map<Integer, Integer> map = new HashMap<>(nums.length);
- for (int i = 0; i < nums.length; i++) {
- if (map.containsKey(target - nums[i])) {
- return new int[]{i, map.get(target - nums[i])};
- } else {
- map.put(nums[i], i);
- }
- }
- return null;
- }
- public List<List<Integer>> threeSum(int[] nums) {
- int n = nums.length;
- Arrays.sort(nums);
- List<List<Integer>> ans = new ArrayList<List<Integer>>();
- // 枚举a
- for (int first = 0; first < n; first ++) {
- // 需要和上一次枚举的数不相同
- if (first > 0 && nums[first] == nums[first - 1]) {
- continue;
- }
- // c 对应的指针初始指向数组的最右端
- int third = n - 1;
- int target = - nums[first];
- // 枚举b
- for (int second = first + 1; second < third; second ++) {
- if (second > first + 1 && nums[second] == nums[second - 1]) {
- continue;
- }
- // 保证b 的指针在c 的左侧
- while (second < third && nums[second] + nums[third] > target) {
- -- third;
- }
- // 如果指针重合,随着b 的后续增加,就不会有满足a+b+c=0 并且b<c 的c 了,可以退出循环
- if (second == third) {
- break;
- }
- if (nums[second] + nums[third] == target) {
- List<Integer> list = new ArrayList<>();
- list.add(nums[first]);
- list.add(nums[second]);
- list.add(nums[third]);
- ans.add(list);
- }
- }
- }
- return ans;
- }
- public static List<List<Integer>> fourSum(int[] nums, int target) {
-
- List<List<Integer>> result = new ArrayList<>();
- if (null == nums || nums.length < 4) {
- return result;
- }
-
- Arrays.sort(nums);
-
- int length = nums.length;
- for (int i = 0; i < length - 3; i++) {
- // 防止重复解
- if (i > 0 && nums[i] == nums[i - 1]) {
- continue;
- }
- // 数组情况前提判断
- if (nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
- break;
- }
- // 数组情况前提判断
- if (nums[length - 1] + nums[length - 2] + nums[length - 3] + nums[length - 4] < target) {
- break;
- }
- // 数组情况前提判断
- if (nums[i] + nums[length - 3] + nums[length - 2] + nums[length -1] < target) {
- continue;
- }
- for (int j = i + 1; j < length - 2; j++) {
-
- // 防止重复解
- if (j > i + 1 && nums[j] == nums[j - 1]) {
- continue;
- }
- // 数组情况前提判断
- if (nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
- break;
- }
- // 数组情况前提判断
- if (nums[i] + nums[j] + nums[length - 1] + nums[length - 2] < target) {
- continue;
- }
- int left = j + 1;
- int right = length - 1;
- while (left < right) {
- int sum = nums[i] + nums[j] + nums[left] + nums[right];
- if (sum == target) {
- // 赋值
- result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
- // 因为有可能有多重情况,所以在这里面还需要继续执行左右指针的变动
- while (left < right && nums[left] == nums[left + 1]) {
- left ++;
- }
- left ++;
- while (left < right && nums[right] == nums[right - 1]) {
- right --;
- }
- right --;
- } else if (sum > target) {
- right --;
- } else if (sum < target) {
- left ++;
- }
- }
- }
- }
- return result;
- }
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
- public int maxProfit(int[] prices) {
- int ans = 0;
- int min = prices[0];
- for (int i = 1; i < prices.length; i++) {
- if (prices[i] > min) {
- ans = ans > prices[i] - min ? ans : prices[i] - min;
- } else {
- min = prices[i];
- }
- }
- return ans;
- }
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
动态规划:
- public int maxProfit(int[] prices) {
- int n = prices.length;
- // 定义状态dp[i][0] 表示第i 天交易完后,手里没有股票的最大利润
- // 定义状态dp[i][1] 表示第i 天交易完后,手里有股票的最大利润
- // 状态转移方程:
- // dp[i][0]=max{dp[i−1][0],dp[i−1][1]+prices[i]} 因为第i 天交易完成,股票卖出,所以要加price[i]。即当天股票卖出时的价格。
- // dp[i][1]=max{dp[i−1][1],dp[i−1][0]−prices[i]}
- int[][] dp = new int[n][2];
- dp[0][0] = 0;
- dp[0][1] = -prices[0];
- for (int i = 1; i < n; i++) {
- dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
- dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
- }
- return dp[n - 1][0];
- }
-
-
-
- // 升级版
-
- public int maxProfit(int[] prices) {
- int n = prices.length;
- int dp0 = 0, dp1 = -prices[0];
- for (int i = 1; i < n; ++i) {
- int newDp0 = Math.max(dp0, dp1 + prices[i]);
- int newDp1 = Math.max(dp1, dp0 - prices[i]);
- dp0 = newDp0;
- dp1 = newDp1;
- }
- return dp0;
- }
-
贪心:
- public int maxProfit(int[] prices) {
- int ans = 0;
- int n = prices.length;
- for (int i = 1; i < n; ++i) {
- ans += Math.max(0, prices[i] - prices[i - 1]);
- }
- return ans;
- }
什么时候用贪心?什么时候用动态规划?
状态转移树中,若后一状态仅仅取决于上一个状态,就用贪心算法;若后一状态取决于之前的多个状态,就用动态规划。(不是太准确)
更准确的说法应该是当局部最优解可以推导最终得出全局最优解时,用贪心,剩下的情况用动态规划
给定一个数组,它的第 i
个元素是一支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
- public int maxProfit(int[] prices) {
- int n = prices.length;
- int buy1 = -prices[0], sell1 = 0;
- int buy2 = -prices[0], sell2 = 0;
- for (int i = 1; i < n; ++i) {
- buy1 = Math.max(buy1, -prices[i]);
- sell1 = Math.max(sell1, buy1 + prices[i]);
- buy2 = Math.max(buy2, sell1 - prices[i]);
- sell2 = Math.max(sell2, buy2 + prices[i]);
- }
- return sell2;
- }
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格,和一个整型 k 。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
定一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,请你判断一个人是否能够参加这里面的全部会议
提示:
0 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti < endi <= 106
- class Solution {
- public boolean canAttendMeetings(int[][] intervals) {
- Arrays.sort(intervals, (o1, o2) -> (o1[0] == o2[0] ? o1[1] - o2[1] : o1[0] - o2[0]));
-
- for (int i = 1; i < intervals.length; i++) {
- if (intervals[i][0] < intervals[i - 1][1]) {
- return false;
- }
- }
- return true;
- }
- }
参考:leetcode 252.会议室 Java_云水冰的博客-CSDN博客
Arrays.sort() 的使用:Arrays.sort()的用法_Lin的博客-CSDN博客_arrays.sort()
首先进行一下排序,然后用一个小顶堆,维护当前每个会议室的结束时间,
然后当一个新的时间安排出现的时候,只需要判断一下是否需要新申请一个会议室,还是继续使用之前的会议室。
使用小顶堆的实现方式就是,小顶堆存了会议的结束时间,首先把第一个会议的结束时间存入,作为初始化条件。
遍历其他会议日程。如果会议开始时间(intervals[i][0])大于queue.peek(),就是大于当前最小的时间,那么就可以继续使用当前这个会议室。优先队列的操作方式就是把前一个会议日程踢出,然后add 进去当前会议的结束时间。否则不踢出。
- public int minMeetingRooms(int[][] intervals) {
- if (intervals == null || intervals.length == 0) {
- return 0;
- }
- Arrays.sort(intervals, new Comparator<int[]>() {
- @Override
- public int compare(int[] o1, int[] o2) {
- return o1[0] - o2[0];
- }
- });
- // 优先队列存放的是,之前的,最早结束的会议的,结束时间。
- // 这是一个小顶堆
- PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2) -> o1 - o2);
- // 队列初始化存入第一个会议的结束时间
- queue.offer(intervals[0][1]);
- for (int i = 1; i < intervals.length; i++) {
- // 如果当前的会议开始时间,大于队列里面的之前的会议的结束时间
- if (intervals[i][0] >= queue.peek()) {
- // 那么之前的那个会议和现在的会议就可以使用一个会议室
- // 所以之前的会议的记录就可以出队列了
- queue.poll();
- }
- // 如果不是的话,那么就肯定得多一个房间了。
- // 所以就将现在的会议结束时间入队。
- // 使用小顶堆的意思是:越早结束的会议,房间的空余肯定越多,所以使用小顶堆的堆顶来比较
- queue.offer(intervals[i][1]);
- }
- return queue.size();
- }
参考:leetcode253. 会议室II(java):最小堆_yinianxx的博客-CSDN博客
举例:994,腐烂的桔子。
在给定的网格中,每个单元格可以有以下三个值之一:
值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。
返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
主要就是看官方题解,主要就是细节
- public int orangesRotting(int[][] grid) {
- int[] x = {1, 0, -1, 0};
- int[] y = {0, 1, 0, -1};
-
- int row = grid.length;
- int column = grid[0].length;
-
- // 存放所有腐烂的句子
- Queue<Pair<Integer, Integer>> queue = new ArrayDeque<>();
- // 存放每个橘子是第几分钟腐烂的。对于开局就已经腐烂的橘子,他们的value 是0
- Map<Pair<Integer, Integer>, Integer> depth = new HashMap<>();
- // 处理开局就腐烂的橘子
- for (int i = 0; i < row; i++) {
- for (int j = 0; j < column; j++) {
- if(grid[i][j] == 2) {
- // 存储腐烂橘子
- queue.add(new Pair<>(i, j));
- // 存储橘子变腐烂的时间,key为橘子的一维数组下标,value为变腐烂的时间
- depth.put(new Pair<>(i, j), 0);
- }
- }
- }
- int ans = 0;
- // 腐烂的橘子队列,不为空
- while(!queue.isEmpty()) {
- Pair<Integer, Integer> poppedPair = queue.poll();
- int r = poppedPair.getKey();
- int c = poppedPair.getValue();
- for(int k = 0; k < 4; k++) {
- int tempI = r + x[k];
- int tempJ = c + y[k];
- if(tempI >= 0 && tempI < row && tempJ >= 0 && tempJ < column && grid[tempI][tempJ] == 1) {
- grid[tempI][tempJ] = 2;
- Pair<Integer, Integer> tempPair = new Pair<>(tempI, tempJ);
- queue.add(tempPair);
- // 记载橘子腐烂的时间
- // key 是中心节点散发的各个节点
- // value 是中心节点的腐烂时间 + 1
- depth.put(tempPair, depth.get(poppedPair) + 1);
- ans = depth.get(tempPair);
- }
- }
- }
- for(int i = 0; i < row; i++) {
- for(int j = 0; j < column; j++) {
- if (grid[i][j] == 1) {
- return -1;
- }
- }
- }
- return ans;
- }
radix sort 基数排序,也叫做bucket sort。
使用一个队列即可实现
主要是用top_elem 标识栈顶元素,和pop 操作,比较重要。
- class MyStack {
-
- private Queue<Integer> queue = new LinkedList<>();
- int top_elem = 0;
- public MyStack() {
-
- }
-
- // 添加元素到栈顶
- public void push(int x) {
- //x 是队列的队尾,是栈的栈顶
- queue.offer(x);
- top_elem = x;
- }
-
- // 队列的性质是先进先出,pop 元素的时候只能从队头取出元素
- // 但是栈是先进后出,解决办法就是
- // 把队列前面的元素全部取出,再加入到队尾,让之前队尾元素排到队头,这样就可以取出了
- public int pop() {
- int size = queue.size();
- // 留下两个元素是为了更新top_elem.
- while (size > 2) {
- queue.offer(queue.poll());
- size --;
- }
- top_elem = queue.peek();
- queue.offer(queue.poll());
- // 之前的队尾已经到了队头
- return queue.poll();
- }
-
- public int top() {
- return top_elem;
- }
-
- public boolean empty() {
- return queue.isEmpty();
- }
- }
使用两个栈实现一个队列
添加元素的时候,把元素放到s1 里面,pop 的时候,把s2 作为中转
- class MyQueue {
-
- private Stack<Integer> s1, s2;
- public MyQueue() {
- s1 = new Stack<>();
- s2 = new Stack<>();
- }
-
- public void push(int x) {
- s1.push(x);
- }
-
- // 对于pop 操作,也只操作s2 就可以了
- public int pop() {
- // 先调用peek,保证s2 非空。
- // 这样就保证,在s2 空了之后,再把s1 里面的值加进去
- // 否则就先不动s1 的数据
- peek();
- return s2.pop();
- }
-
- // 注意peek 是静态的,不会有元素的变更
- public int peek() {
- if (s2.isEmpty()) {
- // 把s1 压入s2 中
- while (!s1.isEmpty()) {
- s2.push(s1.pop());
- }
- }
- return s2.peek();
- }
-
- // 如果两个栈都空,就说明队列为空
- public boolean empty() {
- return s1.isEmpty() && s2.isEmpty();
- }
- }
以上参考:队列实现栈|栈实现队列 - 知乎
其他:
给你一个整数数组 nums
和一个整数 k
,请你统计并返回该数组中和为 k
的连续子数组的个数。
使用“前缀和”
前缀和指一个数组的某下标之前的所有数组元素的和(包含其自身)。前缀和分为一维前缀和,以及二维前缀和。前缀和是一种重要的预处理,能够降低算法的时间复杂度。
- (题目解法来自“labuladong”)
-
- 使用“前缀和”技巧
-
- public static int subArraySum(int[] nums, int k) {
-
- int n = nums.length;
- // 构造
- int[] preSum = new int[n + 1];
- preSum[0] = 0;
- for (int i = 0; i < n; i++) {
- preSum[i + 1] = preSum[i] + nums[i];
- }
-
- int ans = 0;
- for (int i = 1; i <= n; i++) {
- for (int j = 0; j < i; j++) {
- if (preSum[i] - preSum[j] == k) {
- ans ++;
- }
- }
- }
- return ans;
- }
-
-
-
- 优化:第⼆层 for 循环在⼲嘛呢?翻译⼀下就是, 在计算, 有⼏个 j 能够使得
- sum[i] 和 sum[j] 的差为 k。 毎找到⼀个这样的 j , 就把结果加⼀。
- 把第二个for 循环中的判断公式移项,可以得到if(sum[j] == sum[i] - k) ans ++;
- 直接记录下有⼏个 sum[j] 和 sum[i] - k 相等, 直接更
- 新结果, 就避免了内层的 for 循环。 我们可以⽤哈希表, 在记录前缀和的同
- 时记录该前缀和出现的次数。
-
- public static int subArraySum(int[] nums, int k) {
-
- int n = nums.length;
- HashMap<Integer, Integer> preSum = new HashMap<>();
- preSum.put(0, 1);
- int ans = 0;
- // 数组下标0 到下标i 的和
- int sum0_i = 0;
- for (int i = 0; i < n; i++) {
- sum0_i += nums[i];
- // 题目是要求有几个和为k 的子数组
- // sum0_j 是数组下标0 到下标j 的和,也就是要找的前缀和。用sum0-i 减去sum0-j 得到的就是k
- int sum0_j = sum0_i - k;
- // 如果map 中存在这个前缀和
- if (preSum.containsKey(sum0_j)) {
- ans += preSum.get(sum0_j);
- // 然后把前缀和sum0-i 加入记录,同时记录出现次数
- preSum.put(sum0_i, preSum.getOrDefault(sum0_i, 0) + 1);
- }
- }
- return ans;
- }
给⼀棵⼆叉树, 和⼀个⽬标值, 节点上的值有正有负, 返回树中和等于⽬标值的路径条数
- @Data
- class TreeNode {
- int val;
- TreeNode left;
- TreeNode right;
- }
-
- public int pathNum(TreeNode root, int sum) {
-
- if (root == null) {
- return 0;
- }
-
- int pathNumMe = pathNum(root, sum);
- int pathNumLeft = pathNum(root.left, sum);
- int pathNumRight = pathNum(root.right, sum);
- return pathNumLeft + pathNumRight + pathNumMe;
- }
-
- public static int count(TreeNode node, int sum) {
-
- if (node == null) {
- return 0;
- }
- int isMe = (node.val == sum) ? 1 : 0;
- int leftBro = count(node.left, sum - node.val);
- int rightBro = count(node.right, sum - node.val);
- return isMe + leftBro + rightBro;
-
- }
归并排序:典型的分治算法;分治,典型的递归结构。
coupang
九章算法 | Coupang面试题:捡苹果 - 知乎
2020 年面试记录 [Microsoft / Coupang / CoinMarketCap] - Popco - 博客园
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。