算法 动态规划 股票问题 一次买卖
算法思想:
因为只有一次买卖,不需要存储之前的状态,与动态规划关系不大。
考虑到顺序,定义一个minprice初始化为最大值,进行一次遍历,maxprofit初始化为0
进行判断,更新最小值已经最大利润
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int maxProfit (vector<int >& prices) { int inf = 1e9 ; int minPrice = inf; int maxprofit = 0 ; int n = prices.size (); for (int i = 0 ; i < n; i++){ minPrice = min (prices[i] , minPrice); maxprofit = max (maxprofit, prices[i] - minPrice); } return maxprofit; } };
无限交易,最多持有一股
思路:
基本的动态规划问题。dp[i] [0]:第i天不持有股票的最大利润。dp[i] [1]:第i天持有股票的最大利润。
状态转移方程:持有股票:可能是之前持有,也可能是今天买入,所以两种方式取最大值
不持有股票:同理,可能是之前就不持有,也可能是今天卖出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int maxProfit (vector<int >& prices) { int n = prices.size (); int dp[n][2 ]; dp[0 ][0 ] = 0 ; dp[0 ][1 ] = -prices[0 ]; for (int i = 1 ; i < n; i++){ dp[i][0 ] = max (dp[i -1 ][0 ], dp[i - 1 ][1 ] + prices[i]); dp[i][1 ] = max (dp[i - 1 ][0 ] - prices[i], dp[i - 1 ][1 ]); } return dp[n - 1 ][0 ]; } };
两笔交易,最多持有一股
思路:
因为只能有两次,所以用dp[n] [4] 来记录四个状态,大致思路跟上题一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int n = prices.size (); if (n < 2 ){ return 0 ; } vector<vector<int >> dp (n, vector <int >(5 )); dp[0 ][0 ] = 0 ; dp[0 ][1 ] = -prices[0 ]; dp[0 ][3 ] = -prices[0 ]; for (int i = 1 ; i < n; i++){ dp[i][1 ] = max (dp[i-1 ][1 ], -prices[i]); dp[i][2 ] = max (dp[i-1 ][2 ], dp[i-1 ][1 ] + prices[i]); dp[i][3 ] = max (dp[i-1 ][3 ], dp[i-1 ][2 ] - prices[i]); dp[i][4 ] = max (dp[i-1 ][4 ], dp[i-1 ][3 ] + prices[i]); } return dp[n - 1 ][4 ];
k次交易,最多持有一股
思路:
与上一题基本相似,不同之处在于你需要将次数抽象成2 * k + 1;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : int maxProfit (int k, vector<int >& prices) { int n = prices.size (); if (n < 2 || k == 0 ){ return 0 ; } vector<vector<int >> dp (n,vector <int >(2 * k + 1 )); dp[0 ][0 ] = 0 ; int tmp = 1 ; while (tmp <= 2 * k){ dp[0 ][tmp] = -prices[0 ]; tmp += 2 ; } for (int i = 1 ; i < n; i++){ for (int j = 1 ; j <= 2 * k; j += 2 ){ dp[i][j] = max (dp[i-1 ][j], dp[i-1 ][j-1 ] - prices[i]); } for (int j = 2 ; j <= 2 * k; j += 2 ){ dp[i][j] = max (dp[i-1 ][j], dp[i-1 ][j-1 ] + prices[i]); } } int max_profit = 0 ; for (int i = 0 ; i <= 2 * k; i += 2 ){ max_profit = max (max_profit, dp[n - 1 ][i]); } return max_profit; } };
打家劫舍问题 非循环遍历
算法思想:
制约条件只有一个:不能连着计算。很容易就确定了状态转移方程
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i];
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Solution {public : int rob (vector<int >& nums) { int n = nums.size (); int dp[n + 1 ]; dp[0 ] = 0 ; dp[1 ] = nums[0 ]; for (int i = 2 ; i <= n; i++){ dp[i] = max (dp[i - 1 ], dp[i - 2 ] + nums[i - 1 ]); } return dp[n]; } };
循环问题
思路:因为有还,所以必须考虑首尾问题。即第一户和最后一户不能同时偷。
所以把这个问题拆解成两个子问题即可解决:即考虑第一户到第n - 1户 和 考虑第二户到第n户
这样变成两个非循环问题,将实现逻辑封装起来就可以解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Solution {private : vector<int > dp; public : int rob (vector<int >& nums) { int n = nums.size (); if (n == 0 ){ return 0 ; } if (n == 1 ){ return nums[0 ]; } int planA = robRange (nums, 0 , n - 2 ); int planB = robRange (nums, 1 , n - 1 ); return planA > planB ? planA : planB; } int robRange (vector<int >& nums,int l, int r) { if (l == r){ return nums[l]; } dp.resize (nums.size (), 0 ); dp[l] = nums[l]; dp[l + 1 ] = max (nums[l], nums[l + 1 ]); for (int i = l + 2 ; i <= r; i++){ dp[i] = max (dp[i - 2 ] + nums[i], dp[i - 1 ]); } return dp[r]; } };
二叉树形(树桩dp)
此题是树状dp的入门题
根据题意:偷了父节点,两个子节点遍不能再投。所以每一层可以用dp[2]来表示。dp[0]表示不偷该层的最大值,dp[1]表示偷了该层的最大值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : int rob (TreeNode* root) { vector<int > ret = treerob (root); return max (ret[0 ], ret[1 ]); } vector<int > treerob (TreeNode* root) { if (root == nullptr ){ return {0 , 0 }; } vector<int > dp (2 ) ; auto l = treerob (root->left); auto r = treerob (root->right); dp[0 ] = max (l[0 ], l[1 ]) + max (r[0 ], r[1 ]); dp[1 ] = l[0 ] + r[0 ] + root->val; return dp; } };
最长公共子序列
思路:
确定状态转移方程:if(text1[i - 1] == text2[j - 1]){
dp[i] [j] = dp[i - 1] [j -1] + 1;
}else{
dp[i] [j] = max(dp[i - 1] [j], max(dp[i] [j - 1], dp[i - 1] [ j - 1]));
}
dp[i] [j] :表示前一个字符串的0-i位和后一个字符串的0-j位中最长的公共前缀
当t1[i] == t2[j] 时,该长度为之前长度+ 1
不相等时,另外三种情况取最大值(写的比较简略,详见代码逻辑)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution {public : int longestCommonSubsequence (string text1, string text2) { int n1 = text1. size (); int n2 = text2. size (); int dp[n1 + 1 ][n2 + 1 ]; for (int i = 0 ; i <= n1; i++){ dp[i][0 ] = 0 ; } for (int j = 0 ; j <= n2; j++){ dp[0 ][j] = 0 ; } for (int i = 1 ; i <= n1; i++){ for (int j = 1 ; j <= n2; j++){ if (text1[i - 1 ] == text2[j - 1 ]){ dp[i][j] = dp[i - 1 ][j -1 ] + 1 ; }else { dp[i][j] = max (dp[i - 1 ][j], max (dp[i][j - 1 ], dp[i - 1 ][ j - 1 ])); } } } return dp[n1][n2]; } };
接雨水
对于下标i,水能达到的最大高度等于下标i的最大高度的最小值,下标i处能接的雨水量等于下标i处水能到达的最大高度减去height[i]
动态规划:
创建两个长度为n的数组leftMax和rightMax,分别表示i及其左边和及其右边的最大值。
我的思路:分别算出左右的最高高度,然后算出每一列能得到的最大雨水量相加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : int trap (vector<int >& height) { int n = height.size (); int leftMax[n]; int rightMax[n]; leftMax[0 ] = height[0 ]; rightMax[n - 1 ] = height[n - 1 ]; int ret = 0 ; for (int i = 1 ; i < n; i++){ leftMax[i] = max (leftMax[i - 1 ], height[i]); } for (int i = n - 2 ; i >= 0 ; i--){ rightMax[i] = max (rightMax[i + 1 ], height[i]); } for (int i = 0 ; i < n; i++){ int h = min (leftMax[i], rightMax[i]); ret += h - height[i]; } return ret; } };
目标和
这道题可以用dfs解决,现在主要巩固练习动态规划
算法思路:
加法总和为x,减法总和就是sum - x
希望得到 x - (sum - x) = target
x = (sum + target) / 2
因此可以将该题抽象为装满容量为x的背包有多少种方法
注意:当 x 为奇数时,代表没有方法,直接返回0
状态转移方程:
若不计算该点:dp[i] [j] = d[i - 1] [j] ,
若计算该点:注意此时需要相加,因为最后算的是有多少种情况 dp[i] [j] += dp[i - 1] [j - nums[j] ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : int findTargetSumWays (vector<int >& nums, int target) { int sum = 0 ; for (auto & n : nums){ sum += n; } if (sum < target || (sum - target) % 2 != 0 ){ return 0 ; } int x = (sum - target) /2 ; int n = nums.size (); vector<vector<int >> dp (n + 1 ,vector <int >(x + 1 )); dp[0 ][0 ] = 1 ; for (int i = 1 ; i <= n; i++){ int num = nums[ i - 1 ]; for (int j = 0 ; j <= x; j++){ dp[i][j] = dp[i - 1 ][j]; if (j >= num){ dp[i][j] += dp[i - 1 ][j - num]; } } } return dp[n][x]; } };
最长上升子序列
思路:
dp[i]表示以i结尾的元素的最长子序列的长度。
如何计算:即 dp0 , 1, 2 … i - 1的长度的最大值 + 1
所以两层for循环,时间复杂度为O(N²)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> #include <vector> #include <climits> using namespace std;const int N = 1010 ;int n;vector<int > dp; int nums[N];int max_length () { dp[0 ] = 1 ; for (int i = 1 ; i < n; i++){ dp[i] = 1 ; for (int j = 0 ; j < i; j++){ if (nums[i] > nums[j]){ dp[i] = max (dp[i], dp[j] + 1 ); } } } int ret = dp[0 ]; for (int i = 1 ; i < n; i++){ if (dp[i] > ret){ ret = dp[i]; } } return ret; } int main () { scanf ("%d\n" , &n); dp.resize (n); for (int i = 0 ; i < n; i++){ scanf ("%d" , &nums[i]); } int ret = max_length (); printf ("%d" , ret); return 0 ; }
时间复杂度O(N²)的算法容易超时
观察发现,当最大长度相同的数值 中,只需要存数值最小的那一个即可,因为满足大的数组一定满足小的数值(递增子序列)
随着长度的增加,结尾值的大小一定是严格递增的
求以a[i]结尾的最长上升子序列的长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include <iostream> #include <algorithm> using namespace std;const int N = 100010 ;int n;int a[N], q[N]; int main () { scanf ("%d" , &n); for (int i = 0 ; i < n; i++){ scanf ("%d" , &a[i]); } q[0 ] = -2e9 ; int len = 0 ; for (int i = 0 ; i < n; i++){ int l = 0 , r = len; while (l < r){ int mid = (l + r + 1 ) >> 1 ; if (q[mid] < a[i]){ l = mid; }else { r = mid - 1 ; } } len = max (len, r + 1 ); q[r + 1 ] = a[i]; } printf ("%d\n" , len); return 0 ; }
最长公共子序列
思路:
dp[i] [j]:表示text1 的前i个字符和text2的前j个字符的最长公共前缀
状态转移方程有两种情况
当text1[i] == text2[j]时:dp[i] [j] = dp[i - 1] [ j - 1] + 1
不相等时:dp[i] [j] = max(dp[i - 1] [j - 1], max(dp[i - 1] [j], dp[i] [j - 1]));
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> #include <vector> using namespace std;const int N = 1010 ;char a[N], b[N];int longestCommonSubsequence (string text1, string text2) { int n1 = text1. size (); int n2 = text2. size (); vector<vector<int >> dp (n1 + 1 ,vector <int >(n2 + 1 )); for (int i = 1 ; i <= n1; i++){ for (int j = 1 ; j <= n2; j++){ if (text1[i - 1 ] == text2[j - 1 ]){ dp[i][j] = dp[i - 1 ][j - 1 ] + 1 ; }else { dp[i][j] = max (dp[i - 1 ][j - 1 ], max (dp[i - 1 ][j], dp[i][j - 1 ])); } } } return dp[n1][n2]; } int main () { int n1, n2; scanf ("%d%d\n" , &n1, &n2); scanf ("%s%s" , a, b); string s1 = a; string s2 = b; int ret = longestCommonSubsequence (s1, s2); printf ("%d\n" , ret); }
最短编辑距离(字符串类问题)
解决两个字符串的动态规划问题,一般是用两个指针ij分别指向两个字符串的最后,然后一步步往前移动,缩小规模问题(也可以从前往后移动)
思路:
状态转移方程
当word1[i - 1] == word2[j - 1]:dp[i] [j] = dp[i- 1] [j - 1];
当word1[i - 1] != word2[j - 1]:相当于要在插入、删除、替换操作中选取操作次数最小的.
A变换成b
删除A:dp[i - 1] [j]
增添A: 相当于删除B:dp[i] [j - 1]
替换A:相当于删除A和:dp[i - 1] [j - 1]
取三者最小值 + 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <iostream> #include <vector> using namespace std;const int N = 1010 ;int n ,m;char a[N], b[N];int minDistance (string word1, string word2) { vector<vector<int >> dp (n + 1 , vector <int >(m + 1 )); for (int i = 0 ; i <= n; i++){ dp[i][0 ] = i; } for (int j = 0 ; j <= m; j++){ dp[0 ][j] = j; } for (int i = 1 ; i <= n; i++){ for (int j = 1 ; j <= m; j++){ if (word1[i - 1 ] == word2[j - 1 ]){ dp[i][j] = dp[i - 1 ][j - 1 ]; }else { dp[i][j] = min (dp[i - 1 ][j - 1 ], min (dp[i - 1 ][j], dp[i][j - 1 ])) + 1 ; } } } return dp[n][m]; } int main () { scanf ("%d%s" ,&n,a); scanf ("%d%s" ,&m,b); string s1 (a) ; string s2 (b) ; int ret = minDistance (s1,s2); printf ("%d\n" ,ret); return 0 ; }
记忆化搜索
记录运算结果,避免重复运算的方法就是记忆化搜索
记忆化搜索 = 深度优先搜索 的实现 + 动态规划 的思想
步骤:
1.合法性剪枝
因为在递归计算的时候,我们必须保证传入参数的合法性,所以这一步是必要的,比如坐标为负数之类的判断
2.偏序关系剪枝
3.记忆化剪枝
记忆化剪枝就是去对应的哈希数组判断这个状态是否曾经已经计算过,如果计算过则直接返回,时间复杂度 O(1)
4.递归计算结果并返回
这一步就是深度优先搜索的递归过程,也是递归子问题取最优解的过程,需要具体问题具体分析
思路:
挨个遍历,求出最大长度。
注意:用dp[x] [y]记录该坐标的最大长度。
每个坐标的最大长度:dp[x] [y] = max(dp[x] [y], findPath(new_x, new_y) + 1);
需要满足新坐标的高度低于当前坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include <iostream> #include <vector> #include <cstring> using namespace std;const int N = 310 ;int row, col;int h[N][N];int dx[4 ] = {0 , -1 , 0 , 1 };int dy[4 ] = {1 , 0 , -1 , 0 };int dp[N][N];int findPath (int x, int y) { if (dp[x][y] != -1 ){ return dp[x][y]; } dp[x][y] = 1 ; for (int i = 0 ; i < 4 ; i++){ int new_x = x + dx[i]; int new_y = y + dy[i]; if (new_x < 0 || new_x >= row || new_y < 0 || new_y >= col || h[new_x][new_y] >= h[x][y]){ continue ; } dp[x][y] = max (dp[x][y], findPath (new_x, new_y) + 1 ); } return dp[x][y]; } int main () { scanf ("%d%d" ,&row, &col); for (int i = 0 ; i < row; i++){ for (int j = 0 ; j < col; j++){ scanf ("%d" , &h[i][j]); } } memset (dp, -1 , sizeof (dp)); int ret = 0 ; for (int i = 0 ; i < row; i++){ for (int j = 0 ; j < col; j++){ ret = max (ret, findPath (i, j)); } } printf ("%d\n" , ret); return 0 ; }
最长相邻不相等子序列
思路:
题目要求比较繁杂,如果正常思路挨个考虑,基本不可能实现。所以用动态规划
从后往前遍历,记录以每个节点开始的最大子序列长度,另外需要开辟一个数组记录此节点的上一个元素坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Solution {public : vector<string> getWordsInLongestSubsequence (int n, vector<string>& words, vector<int >& groups) { vector<int > dp (n) ; vector<int > from (n) ; int mx = n - 1 ; for (int i = n - 1 ; i >= 0 ; i--) { for (int j = i + 1 ; j < n; j++) { if (dp[j] > dp[i] && groups[i] != groups[j] && is_ok (words[i], words[j])) { dp[i] = dp[j]; from[i] = j; } } dp[i]++; if (dp[i] > dp[mx]) { mx = i; } } int m = dp[mx]; vector<string> ans (m) ; for (int i = 0 ; i < m; i++) { ans[i] = words[mx]; mx = from[mx]; } return ans; } bool is_ok (string& s, string& t) { if (s.size () != t.size ()) { return false ; } bool diff = false ; for (int i = 0 ; i < s.size (); i++) { if (s[i] != t[i]) { if (diff) { return false ; } diff = true ; } } return diff; } };
使数组变美的最小增量
思路:动态规划,记忆化搜索
考虑最后一个元素选或不选
若选,将该数增大至k,对于左边那个数,右边就有一个k;
若不选,对于左边那个数,右边没有满足条件的数
同样,对于倒数第二个数
若选,将该数增大至k,对于倒数第三个数,右边就有一个k;
若不选,对于倒数第三个数,右边的2个数均布满足条件,所以该数必须选,即增大至k。(不考虑右边两个数是否大于k,在函数中会判断)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution {public : long long minIncrementOperations (vector<int >& nums, int k) { int n = nums.size (); vector<vector<long long >> v (n, vector <long long >(3 , -1 )); function<long long (int , int )> dfs = [&](int i, int j)-> long long { if (i < 0 ){ return 0 ; } auto & ret = v[i][j]; if (ret != -1 ){ return ret; } ret = dfs (i - 1 , 0 ) + max (k - nums[i], 0 ); if (j < 2 ){ ret = min (ret, dfs (i - 1 , j + 1 )); } return ret; }; return dfs (n - 1 , 0 ); } };
和为目标值的最长子序列
思路:经典的01背包问题
dp[i]:表示值为i时最大子序列的长度
用s记录和的最大值,不能大于target
循环遍历s到当前值,记录这区间内能得到的长度的最大值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { public : int lengthOfLongestSubsequence (vector<int >& nums, int target) { vector<int > dp (target + 1 , INT_MIN) ; dp[0 ] = 0 ; int s = 0 ; for (auto x : nums) { s = min (s + x, target); for (int j = s; j >= x; j--) { dp[j] = max (dp[j], dp[j - x] + 1 ); } } return dp[target] > 0 ? dp[target] : -1 ; } };
树形DP 基本案例
树形dp的基本思路:使用后序遍历 ,因为该层节点的值的处理逻辑需要下一层的结果作为中间值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution {public : int rob (TreeNode* root) { vector<int > ret = rob_tree (root); return max (ret[0 ], ret[1 ]); } vector<int > rob_tree (TreeNode* cur) { if (cur == nullptr ){ return vector<int >{0 , 0 }; } vector<int > left = rob_tree (cur->left); vector<int > right = rob_tree (cur->right); int val = cur->val + left[0 ] + right[0 ]; int non_val = max (left[0 ], left[1 ]) + max (right[0 ], right[1 ]); return vector<int >{non_val, val}; } };
贪心算法 理论基础
贪心没有固定套路
通过局部最优,推出整体最优
贪心的四个步骤:
将主问题拆分为子问题
找出合适的贪心策略
求解每一个子问题的最优解
将局部最优解堆叠成全局最优解
将钱分给最多的儿童
这段代码采用了一种贪心的逻辑来尽可能均匀地分配钱给孩子。其逻辑如下:
首先,检查是否有足够的钱来分给每个孩子。如果钱不足以分给每个孩子,就返回-1,表示无法分配。
然后,每个孩子应得的1块钱会从总金额中扣除,以确保每个孩子至少能得到1块钱。
接下来,尽可能多地分配7块钱给孩子,以确保余下的钱尽可能均匀地分配。计算最多可以分给多少个孩子7块钱,并将相应数量的钱从总金额中扣除。
最后,如果还有余下的钱且只剩一个孩子,但余下的钱不足以分给这个孩子,它会减少一个可以分到7块钱的孩子,以确保余下的钱均匀分配。
这种方法的目标是尽可能多地分配7块钱,以确保孩子获得尽可能多的钱,同时保持余下的钱均匀分配。这种贪心策略可以有效地满足问题的要求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution {public : int distMoney (int money, int children) { if (money < children) { return -1 ; } money -= children; int cnt = min (money / 7 , children); money -= cnt * 7 ; children -= cnt; if (children == 0 && money > 0 || children == 1 && money == 3 ) { cnt--; } return cnt; } };
最大子序和
思路:使用贪心
局部最优:每加一次与最大值进行判断,如果大于最大值则更新最大值。若相加之后小于零,说明这个数已经比之前的子序列和小了,而且之后也不需要加上他的子序列(因为现在是小于零的),所以重置为0,再计算
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Solution {private : int sum = INT_MIN; public : int maxSubArray (vector<int >& nums) { int tmp = 0 ; for (const auto & i : nums){ tmp += i; sum = max (sum, tmp); if (tmp < 0 ){ tmp = 0 ; } } return sum; } };
合并区间
算法:
判断前一个元素第二个值是否与该元素的第一个值重合,若重合则更新。但是注意,不能直接替换,需要比较两个元素第二个值的大小。比如[1, 4] [2, 3],此时就不需要替换
若没有重合,那么前一个元素确定,将该元素插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Solution {public : vector<vector<int >> merge (vector<vector<int >>& intervals) { vector<vector<int >> ret; sort (intervals.begin (), intervals.end ()); int n = intervals.size (); ret.push_back (intervals[0 ]); for (int i = 1 ; i < n; i++){ if (ret.back ()[1 ] < intervals[i][0 ]){ ret.push_back (intervals[i]); }else { ret.back ()[1 ] = max (intervals[i][1 ], ret.back ()[1 ]); } } return ret; } };
一手顺子
思路:
使用贪心的思维。利用一个map将每张牌出现的次数记录。注意需要对数组从小到大排序,这样才能保证不遗漏顺子。
排序后从头开始遍历,若该元素存在,则数组需要连续存在groupsize个元素,但凡有一个不存在,则表示不满足情况,返回false。每遍历一个需要再map中更新数量。
注意判断,若map中没有数组的元素,说名该元素已经被使用完,跳过该元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : bool isNStraightHand (vector<int >& hand, int groupSize) { int n = hand.size (); if (n % groupSize != 0 ){ return false ; } map<int , int > map; for (auto i : hand){ map[i]++; } sort (hand.begin (), hand.end ()); for (auto x : hand){ if (map.count (x) == 0 ){ continue ; } for (int j = 0 ; j < groupSize; j++){ int num = x + j; if (map.count (num) == 0 ){ return false ; } map[num]--; if (map[num] == 0 ){ map.erase (num); } } } return true ; } };
增减字符串匹配
思路:
可以使用贪心算法,当s[i] == I时,说明这个数比下一个数小,将从最小值0开始赋值,赋值之后右移
当s[i] == ‘D’时,说明这个数字比下一个数字大,从最大的数开始赋值,赋值之后左移。
对每一个数都如此判断,最后一个数时两个指针必定汇合
1 2 3 4 5 6 7 8 9 10 11 12 13 class Solution {public : vector<int > diStringMatch (string s) { int n = s.size (); int l =0 , r = n; vector<int > perm (n + 1 ) ; for (int i = 0 ; i < n; i++){ perm[i] = s[i] == 'I' ? l++ : r--; } perm[n] = l; return perm; } };
根据身高重建队列
贪心思路
遇到两个维度,若同时考虑,那么总会顾此失彼,先考虑一个维度,再考虑另一个维度。至于如何确定其中一个维度,先看确定它是否能确定一种规律。在本题中,若先考虑”有多少人在其之前”这个维度,那么什么都确定不了。若先考虑身高,按身高降序排列,那么至少能确定该元素之前的身高都比其高,可以优先插入前面的元素。这样思路就出来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : vector<vector<int >> reconstructQueue (vector<vector<int >>& people) { sort (people.begin (), people.end (), [](auto a, auto b){ if (a[0 ] == b[0 ]){ return a[1 ] <b[1 ]; } return a[0 ] > b[0 ]; }); list<vector<int >> ret; for (int i = 0 ; i < people.size (); i++){ int pos = people[i][1 ]; list<vector<int >>::iterator iter = ret.begin (); while (pos--){ iter++; } ret.insert (iter,people[i]); } return vector<vector<int >>(ret.begin (), ret.end ()); } };
分发糖果
还是老问题:题中有左右两个维度,若同时考虑,肯定会顾此失彼。所以先考虑一个维度在考虑另一个维度。这里考虑左边比右边大和右边比左边大都可以,两者是等价的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 class Solution {public : int candy (vector<int >& ratings) { int cnt = 1 ; int n = ratings.size (); vector<int > less (n,1 ) ; vector<int > greater (n,1 ) ; for (int i = 1 ; i < n; i++){ if (ratings[i] > ratings[i - 1 ]){ less[i] = less[i - 1 ] + 1 ; } } for (int i = n - 2 ; i >= 0 ; i--){ if (ratings[i] > ratings[i + 1 ]){ greater[i] = greater[i + 1 ] + 1 ; } } int ret = 0 ; for (int i = 0 ; i < n; i++){ if (less[i] < greater[i]){ ret += greater[i]; }else { ret += less[i]; } } return ret; } };
监控二叉树
该题也可以用dp做。现在介绍贪心。
要使摄像头数量最少,那么根节点和叶子节点不能按摄像头
从上往下考虑情况过于复杂,考虑从下往上遍历。如何从下往上遍历呢?后序遍历
用1 2 0分别表示此处加摄像头,此处被覆盖和此处没有被覆盖
为了满足第一个条件,空节点默认为状态2,即被覆盖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Solution {private : int ret; int traversal (TreeNode* cur) { if (cur == nullptr ){ return 2 ; } int left = traversal (cur->left); int right = traversal (cur->right); if (left == 2 && right == 2 ){ return 0 ; }else if (left == 0 || right == 0 ){ ret++; return 1 ; }else { return 2 ; } } public : int minCameraCover (TreeNode* root) { ret = 0 ; if (traversal (root) == 0 ){ ret++; } return ret; } };
dfs 所有可能的路径
思路:这是一道最常规的深度优先搜索算法。因为题目告诉你是有向无环图,不存在回路,所以无脑深搜就行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class Solution {private : vector<vector<int >> ret; vector<int > tmp; int n; public : vector<vector<int >> allPathsSourceTarget (vector<vector<int >>& graph) { n = graph.size (); tmp.push_back (0 ); dfs (graph, 0 ); return ret; } void dfs (vector<vector<int >>& graph, int index) { if (index == n - 1 ){ ret.push_back (tmp); return ; } for (int i = 0 ; i < graph[index].size (); i++){ tmp.push_back (graph[index][i]); dfs (graph, graph[index][i]); tmp.pop_back (); } } };
N皇后
思路:实质上是深度优先搜索(回溯)
难点是如何判断对角线是否访问过 用dg 和 udg数组来记录访问
dg[x + y] 和udg[x + n - y]分别代表正对角线和反对角线是否访问过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 class Solution {private : vector<bool > row; vector<bool > col; vector<bool > dg; vector<bool > udg; vector<vector<string>> ret; vector<string> d; int N; public : vector<vector<string>> solveNQueens (int n) { N = n; row.resize (n, false ); col.resize (n, false ); dg.resize (2 * n, false ); udg.resize (2 * n, false ); string s; for (int i = 0 ; i < n; i++){ s += '.' ; } for (int i = 0 ; i < n; i++){ d.push_back (s); } vector<string> ans; dfs (0 ,ans); return ret; } void dfs (int index, vector<string> ans) { if (index == N){ ret.push_back (ans); return ; } for (int j = 0 ; j < N; j++){ if (col[j] == false && row[index] == false && dg[j + index] == false && udg[index + N - j] == false ){ col[j] = true ; row[index] == true ; dg[j + index] = true ; udg[index + N - j] = true ; d[index][j] = 'Q' ; ans.push_back (d[index]); dfs (index + 1 , ans); ans.pop_back (); col[j] = false ; row[index] == false ; dg[j + index] = false ; udg[index + N - j] = false ; d[index][j] = '.' ; } } } };
bfs 岛屿数量
思路:这是一道最基本的宽度优先搜索。找到连续的’1’,具体实现时需要注意如何判断是同一片,我用了flags和visited来记录
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 class Solution {private : int cnt = 0 ; vector<vector<bool >> visited; int row; int col; int dx[4 ] = {1 , 0 , -1 , 0 }; int dy[4 ] = {0 , -1 , 0 , 1 }; bool flag = false ; public : int numIslands (vector<vector<char >>& grid) { row = grid.size (); if (row == 0 ){ return 0 ; } col = grid[0 ].size (); visited.resize (row, vector <bool >(col, false )); for (int i = 0 ; i < row; i++){ for (int j = 0 ; j < col; j++){ flag = false ; bfs (grid, i , j); if (flag){ cnt++; } } } return cnt; } void bfs (vector<vector<char >>& grid, int x, int y) { if (grid[x][y] == '0' || grid[x][y] == true ){ return ; } flag = true ; grid[x][y] = true ; for (int i = 0 ; i < 4 ; i++){ int new_x = x + dx[i]; int new_y = y + dy[i]; if (new_x < 0 || new_x >= row || new_y < 0 || new_y >= col){ continue ; } bfs (grid, new_x, new_y); } } };
双指针 链表相交
思路:
此题大致思路是挨个比较,返回第一个值相等的元素。
问题:如何解决长度不一样的问题
观察图形发现,只要一个元素地址相等,之后相当于是同一个链表(链表的结构所决定),相等之后的长度一定是一样的,我们只需要将长度更长的那一个链表向前移动两者长度之差即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class Solution {public : ListNode *getIntersectionNode (ListNode *headA, ListNode *headB) { int lenA = 0 ; int lenB = 0 ; ListNode* tmp = headA; while (tmp != nullptr ){ tmp = tmp->next; lenA++; } tmp = headB; while (tmp != nullptr ){ tmp = tmp->next; lenB++; } if (lenA > lenB){ int len = lenA - lenB; while (len > 0 ){ headA = headA->next; len--; } }else if (lenA < lenB){ int len = lenB - lenA; while (len > 0 ){ headB = headB->next; len--; } } while (headA != nullptr ){ if (headA == headB){ return headA; } headA = headA->next; headB = headB->next; } return headA; } };
环形链表
思路:这道题使用双指针解决
具体逻辑其实是通过数学计算得出的 ,假设从起点到入口有a个节点,环内有b个节点
有以下关系(slow走了f步)
快指针每次走的步数是慢指针的2倍, fast 走了 2s 步 slow 走了s 步
相遇时,fast比slow多走了n圈,fast = s + nb slow = s
解出 slow = nb ,fast = 2nb
之后怎么计算出入口呢?
已知起点到入口可以走a + nb步路 (n >= 0)
那么slow需要向前走a步即到达入口。但是怎么确定a呢
不要忘记初始节点到入口的距离就是a,所以只需要找一个指针从初始节点开始走,当两节点相遇时即为入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : ListNode *detectCycle (ListNode *head) { ListNode* slow = head; ListNode* fast = head; while (true ){ if (fast == nullptr || fast->next == nullptr ){ return nullptr ; } fast = fast->next->next; slow = slow->next; if (fast == slow){ break ; } } fast = head; while (slow != fast){ slow = slow ->next; fast = fast->next; } return slow; } };
翻转链表
思路:
定义三个指针,pre代表前一个,cur代表当前,next代表下一个
具体如何操作:画图观察,翻转链表,即当前元素的下一个元素需要指向现在元素的前一个节点。 cur->next = pre;
现在需要向后移,即pre = cur
cur = next; next = cur->next;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : ListNode* reverseList (ListNode* head) { if (head == nullptr || head->next == nullptr ){ return head; } ListNode* pre = nullptr ; ListNode* cur = head; ListNode* next = head->next; while (cur != nullptr ){ next = cur->next; cur->next = pre; pre = cur; cur = next; } return pre; } };
删除链表倒数第N个节点
思路:
先进行一次遍历求出链表长度,在计算要删除的倒数第N个节点具体位置
删除思路:先判断是否是头结点,若是,直接返回下一个节点即可。若不是,移动到要删除的节点的上一个节点,node->next = node->next->next
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Solution {public : ListNode* removeNthFromEnd (ListNode* head, int n) { int len = 0 ; ListNode* tmp = head; while (tmp != nullptr ){ tmp = tmp->next; len++; } len -= n; if (len == 0 ){ return head->next; } tmp = head; while (len > 1 ){ tmp = tmp->next; len--; } tmp->next = tmp->next->next; return head; } };
比较版本号
思路:
使用双指针,在每个区间内比较两数大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : int compareVersion (string version1, string version2) { int n = version1. size (), m = version2. size (); int i = 0 , j = 0 ; while (i < n || j < m){ long x = 0 ; for (; i < n && version1[i] != '.' ; i++){ x = x * 10 + version1[i] - '0' ; } i++; long y = 0 ; for (; j < m && version2[j] != '.' ; j++){ y = y * 10 + version2[j] - '0' ; } j++; if (x != y){ return x > y ? 1 : -1 ; } } return 0 ; } };
搜索旋转数组
思路:
基本思路还是使用双指针 。但是需要注意数组不是纯有序的
当中间值与目标值相同时,返回true
当中间值和左右两边相同时,不能判断目标值在哪边,左端点++ 右端点– 缩小范围
当左边值小于等于中间值时(左半段有序),分成两种情况
nums[l] <= target && target < nums[mid],说明左边到中间是有序的并且目标值在这一范围。更新
右端点r = mid - 1
否则目标值在右半部分,更新左端点 l = mid + 1
当中间值大于左端点值时(右半段有序),分成两种情况
nums[mid] < target && target <= nums[r]说明目标值在右半段,更新左指针
其余情况说明在左半段
此题精妙之处在于摈弃了复杂的半段,在左右半边分别都只判断了一种简单的情况,因为target不在左半边就在右半边,另一种情况直接用else即可。
为什么需要nums[l] == nums[mid] && nums[r] == nums[mid],因为数组中有重复的值,可能造成无法判断大小的情况
没有重复元素的情况更简单,具体参考力扣33题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution {public : bool search (vector<int >& nums, int target) { int l = 0 , r = nums.size () - 1 ; while (l <= r) { int mid = ((r - l) >> 1 ) + l; if (nums[mid] == target) { return true ; } if (nums[l] == nums[mid] && nums[r] == nums[mid]) { l++; r--; } else if (nums[l] <= nums[mid]) { if (nums[l] <= target && target < nums[mid]) { r = mid - 1 ; } else { l = mid + 1 ; } } else { if (nums[mid] < target && target <= nums[r]) { l = mid + 1 ; } else { r = mid - 1 ; } } } return false ; } };
找出满足差值条件的下标
思路
使用双指针+维护最大最小值
在枚举j的同时,维护nums[i]的最大值和最小值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class Solution {public : vector<int > findIndices (vector<int >& nums, int indexDifference, int valueDifference) { int max_idx = 0 , min_idx = 0 ; int n = nums.size (); for (int j = indexDifference; j < n; j++){ int i = j - indexDifference; if (nums[i] > nums[max_idx]){ max_idx = i; } if (nums[i] < nums[min_idx]){ min_idx = i; } if (nums[max_idx] - nums[j] >= valueDifference){ return {max_idx, j}; } if (nums[j] - nums[min_idx] >= valueDifference){ return {min_idx, j}; } } return {-1 ,-1 }; } };
多关键字自定义排序 华为机考
第一次接触这种题,思路如下:
因为是多关键字排序,每个关键字对排序都会有影响。那么需要将关键字之间存储起来以备后用,此时想到用类 ,将关键字声明为成员变量。
首先将其存储起来,用vector数组,然后我们需要对数组进行排序,这里使用sort,第三个参数定义为回调函数 ,我自定义了一个函数,一个是lambda表达式,一个是函数式。函数实现内容是题目要求排队方式
注意transform 函数,这个函数会前两个参数是迭代器,表示遍历范围,第三个参数也是迭代器,若返回容器.begin则代表覆盖该容器的元素,若是容器.end则代表重新开辟一块内存。建议使用后者。
tolower()只能传入一个字符,返回其小写形式,::代表作用域,左边空着表示全局作用域。
我给了两种实现,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include <algorithm> #include <iostream> #include <vector> using namespace std;int n;class Record {public : string m_country; int m_gold; int m_sliver; int m_bronze; Record (string country, int gold, int sliver, int bronze) : m_country (country), m_gold (gold), m_sliver (sliver), m_bronze (bronze) {} }; int main () { cin >> n; vector<Record> v; v.reserve (n); for (int i = 0 ; i < n; i++) { string country; int gold, sliver, bronze; cin >> country >> gold >> sliver >> bronze; Record record (country, gold, sliver, bronze) ; v.push_back (record); } sort (v.begin (), v.end (), [](auto r1, auto r2) -> bool { if (r1. m_gold != r2. m_gold) { return r1. m_gold > r2. m_gold; } else if (r1. m_sliver != r2. m_sliver) { return r1. m_sliver > r2. m_sliver; } else if (r1. m_bronze != r2. m_bronze) { return r1. m_bronze > r2. m_bronze; } else { string c1 = r1. m_country; string c2 = r2. m_country; transform (c1. begin (), c1. end (), c1. end (), ::tolower); transform (c2. begin (), c2. end (), c2. end (), ::tolower); return c1. compare (c2); } }); for (auto vec : v) { cout << vec.m_country << endl; } return 0 ; }
Trie前缀树
高效的插入和查找一个字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <iostream> #include <algorithm> #include <cstring> using namespace std;const int N = 100010 ;int son[N][26 ];int cnt[N];int idx = 0 ; char str[N];void insert (char str[]) { int p = 0 ; for (int i = 0 ; str[i]; i++) { int u = str[i] - 'a' ; if (!son[p][u]){ son[p][u] = ++idx; } p = son[p][u]; } cnt[p]++; } int query (char str[]) { int p = 0 ; for (int i = 0 ; str[i]; i++){ int u = str[i] - 'a' ; if (!son[p][u]){ return 0 ; } p = son[p][u]; } return cnt[p]; } int main () { int n; memset (son, 0 , sizeof (son)); scanf ("%d" , &n); while (n--){ char op[2 ]; scanf ("%s%s" , op, str); if (op[0 ] == 'I' ){ insert (str); }else { printf ("%d\n" ,query (str)); } } return 0 ; }
最大异或数
思路:
本题最先想到的肯定是暴力搜索,但是时间复杂度是o(N²),导致超时
使用tire前缀树
由高位到地位分别存取每一位数
从最高位开始遍历,找到与当前位的值相反的节点是否存在。若存在,对结果+ 1 << i,走相反的节点到下一位;若不存在,走对应的节点到下一位(1走0,0走1)
这样时间复杂度就能有效的缩短为O(mn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <iostream> #include <cstring> using namespace std;const int N = 100010 , M = 3000010 ;int idx;int a[N], son[M][2 ];void insert (int x) { int p = 0 ; for (int i = 30 ; i >= 0 ; i--){ if (!son[p][(x >> i) & 1 ]){ son[p][x >> i & 1 ] = ++idx; } p = son[p][x >> i & 1 ]; } } int query (int x) { int p = 0 ; int ret = 0 ; for (int i = 30 ; i >= 0 ; i--){ if (son[p][!(x >> i & 1 )]){ ret += 1 << i; p = son[p][!(x >> i & 1 )]; }else { p = son[p][x >> i & 1 ]; } } return ret; } int main () { idx = 0 ; memset (son, 0 , sizeof (son)); int n; scanf ("%d" , &n); for (int i = 0 ; i < n; i++){ scanf ("%d" , &a[i]); insert (a[i]); } int ret = 0 ; for (int i = 0 ; i < n; i++){ ret = max (ret, query (a[i])); } cout << ret <<endl; return 0 ; }
二叉树 平衡二叉树
思路:
1.自顶向下
每个节点的高度等于左右字节点高度最大值 + 1;
递归遍历每个节点,看每个节点是否都满足要求
2.自底向上
对于每个节点,height只调用一次(自顶下下会重复调用)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution {public : bool isBalanced (TreeNode* root) { if (root == nullptr ){ return true ; } return abs (height (root->left)- height (root->right)) <= 1 && isBalanced (root->left) && isBalanced (root->right); } int height (TreeNode* root) { if (root == nullptr ){ return 0 ; } return max (height (root->left), height (root->right)) + 1 ; } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class Solution {public : bool isBalanced (TreeNode* root) { if (root == nullptr ){ return true ; } return height (root) >= 0 ; } int height (TreeNode* root) { if (root == nullptr ){ return 0 ; } int leftHeight = height (root->left); int rightHeight = height (root->right); if (leftHeight == -1 || rightHeight == -1 || abs (leftHeight - rightHeight) > 1 ){ return -1 ; } return max (height (root->left), height (root->right)) + 1 ; } };
从中序和后续遍历构造二叉树
思路:使用递归构造,注意判断区间,利用中序遍历和后序遍历的性质:
中序遍历:左中右
后续遍历:左右中,后续遍历序列的最后一个元素即为当前字构造根节点的值,再在中序遍历中找到该值,值左边元素即为左子树元素,值右边元素即为右子树元素
使用index哈希表存储中序遍历的元素所在位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution { unordered_map<int ,int > index; public : TreeNode* buildTree (vector<int >& inorder, vector<int >& postorder) { int n = inorder.size (); for (int i = 0 ; i < n; i++){ index[inorder[i]] = i; } return mybuild (inorder, postorder, 0 , inorder.size () - 1 , 0 , postorder.size () - 1 ); } TreeNode* mybuild (vector<int >& inorder, vector<int >& postorder, int in_begin, int in_end, int post_begin, int post_end) { if (in_begin > in_end || post_begin > post_end){ return nullptr ; } TreeNode* root = new TreeNode (postorder[post_end]); int cnt = index[postorder[post_end]]; root->left = mybuild (inorder, postorder, in_begin, cnt - 1 , post_begin,post_begin + cnt - in_begin - 1 ); root->right = mybuild (inorder, postorder, cnt + 1 , in_end, post_begin + cnt - in_begin, post_end - 1 ); return root; } };
判读平衡二叉树
思路
从上之下逐个判断每个节点是否为平衡的,若不平衡则返回false
在每个节点又需要从该点开始判断其左右子树的高度,返回 该点左右子树的最高高度+1即为该点的高度
为了方便理解:可以用两个函数分别完成上述两个功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class Solution {public : bool isBalanced (TreeNode* root) { return isVaild (root); } bool isVaild (TreeNode* root) { if (root == nullptr ){ return true ; } bool l = isVaild (root->left); bool r = isVaild (root->right); if (l == false || r == false ){ return false ; } int left = height (root->left); int right = height (root->right); return abs (left - right) <= 1 ; } int height (TreeNode* root) { if (root == nullptr ){ return 0 ; } return 1 + max (height (root->left), height (root->right)); } };
哈希表 快乐数
思路:
如何判断无限循环:当出现重复的数时,即能确定该数是无限循环的。这时用一个set集合去存储已经存在的元素,每迭代一次进行一次判断,若该数存在,则返回false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class Solution {public : bool isHappy (int n) { unordered_set<int > set; set.insert (n); int sum = n; while (1 ){ sum = getNum (sum); if (sum == 1 ){ return true ; } if (set.find (sum) != set.end ()){ return false ; } set.insert (sum); } return false ; } int getNum (int n) { int sum = 0 ; while (n){ sum += (n % 10 ) * (n % 10 ); n /= 10 ; } return sum; } };
LRU缓存
思路:
因为要记录键值对,所以考虑使用哈希表 进行存储。
考虑有将元素移动到最前面的需求,所以使用双向链表 来设计。靠近头部的节点是最近使用过的节点
设计一个结构体模拟双向链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 struct DLinkedNode { int key, value; DLinkedNode* prev; DLinkedNode* next; DLinkedNode () : key (0 ), value (0 ), prev (nullptr ), next (nullptr ){} DLinkedNode (int _key, int _value) : key (_key), value (_value), prev (nullptr ), next (nullptr ){} }; class LRUCache {private : unordered_map<int , DLinkedNode*>cache; DLinkedNode* head; DLinkedNode* tail; int size; int capacity; public : LRUCache (int _capacity): capacity (_capacity), size (0 ) { head = new DLinkedNode (); tail = new DLinkedNode (); tail->prev = head; head->next = tail; } int get (int key) { if (!cache.count (key)){ return -1 ; } DLinkedNode* node = cache[key]; moveToHead (node); return node->value; } void put (int key, int value) { if (!cache.count (key)){ DLinkedNode* node = new DLinkedNode (key, value); cache[key] = node; addToHead (node); size++; if (size > capacity){ DLinkedNode* removed = removeTail (); cache.erase (removed->key); delete removed; size--; } }else { DLinkedNode* node = cache[key]; node->value = value; moveToHead (node); } } void addToHead (DLinkedNode* node) { node->next = head->next; head->next->prev = node; head->next = node; node->prev = head; } void moveToHead (DLinkedNode* node) { removeNode (node); addToHead (node); } void removeNode (DLinkedNode* node) { node->next->prev = node->prev; node->prev->next = node->next; } DLinkedNode* removeTail () { DLinkedNode* node = tail->prev; removeNode (node); return node; } };
字符串 strStr函数
思路:
strstr() 函数搜索一个字符串在另一个字符串中的第一次出现。
KMP算法,没搞懂过
遍历,效率低
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 class Solution {public : int strStr (string haystack, string needle) { int size = needle.size (); int n = haystack.size (); for (int i = 0 ; i <= n - size; i++){ int l = i , r = i + size; int index = 0 ; while (l + index < r){ if (haystack[l + index] != needle[index]){ break ; } index++; } if (index == size){ return i; } } return -1 ; } };
子串统计类问题
子串统计类问题的通用技巧:
将所有子串按照其末尾字符的下标分组
考虑两组相邻的子串:以s[i - 1]结尾的子串和以s[i] 结尾的子串
以 s[i] 结尾的子串,可以看成是以 s[i−1] 结尾的子串,在末尾添加上 s[i]组成。
字符串的总引力
思路:
从左往右遍历s,考虑将s[i]添加到以s[i - 1]结尾的子串末尾。添加后,这些以s[i - 1]结尾的子串引力值如何改变
若s[i]之前没有出现过,那么这些子串引力值都会增加1。这些子串的引力值之和会增加i,再加上1,即s[i]单独组成的子串的引力值
若s[i]之前出现过,设最后一次出现的下标为j,那么子串s[0..i - 1] s[1..i -1] …s[j..i- 1]末尾添加s[i]数量不会发生改变。而子串s[j +1..i - 1] .. s[i - 1..i - 1]的引力值会+ 1,即i - j - 1,再 +1,即s[i]单独组成的子串的引力值
上述过程只需要遍历s的过程中用一个变量sumG维护以s[i]结尾的子串的引力值之和,同时用一个数据结构记录上一次出现的下标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Solution {public : long long appealSum (string s) { long long ret = 0 ; vector<int > last_appear (26 , -1 ) ; int sum_index = 0 ; for (int i = 0 ; i < s.size (); i++){ int c = s[i] - 'a' ; sum_index += i - last_appear[c]; ret += sum_index; last_appear[c] = i; } return ret; } };
子数组不同元素的数目平方和
与上题不同是将值变成了平方和
区间和 求区间的交集
思路:要求至少射出的数量,其实就是求交集之后有多少互不相关的集合。
每遍历一次,与集合求一次交集:
mergerd.back()[0] = max(mergerd.back()[0], left);
mergerd.back()[1] = min(mergerd.back()[1], right);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Solution {public : int findMinArrowShots (vector<vector<int >>& points) { int n = points.size (); sort (points.begin (),points.end ()); vector<vector<int >> mereged; for (int i = 0 ; i < n; i++){ int left = points[i][0 ]; int right = points[i][1 ]; if (mereged.size () == 0 || mereged.back ()[1 ] < left){ mereged.push_back ({left, right}); } mereged.back ()[0 ] = max (left, mereged.back ()[0 ]); mereged.back ()[1 ] = min (right, mereged.back ()[1 ]); } return mereged.size (); } };
思路:
与上题思路一模一样,求交集,算差值,差值即为移除区间的最小数量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : int eraseOverlapIntervals (vector<vector<int >>& intervals) { if (intervals.empty ()) { return 0 ; } vector<vector<int >> mergerd; sort (intervals.begin (), intervals.end ()); int n = intervals.size (); for (int i = 0 ; i < n; i++){ int left = intervals[i][0 ]; int right = intervals[i][1 ]; if (mergerd.size () == 0 || mergerd.back ()[1 ] <= left){ mergerd.push_back ({left,right}); } mergerd.back ()[0 ] = max (mergerd.back ()[0 ], left); mergerd.back ()[1 ] = min (mergerd.back ()[1 ], right); } return n - mergerd.size (); } };
最大不相交区间数量
注意:最大不相交问题,排序时候需要用右端点。如果用左端点存在一个问题:区间长度可以很大,求不出最小值
方法二:同样可以使用求交集的办法。排序默认使用左端点递增
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> #include <algorithm> #include <vector> using namespace std;const int N = 100010 ;vector<vector<int >> nums; int n;int main () { scanf ("%d" , &n); while (n--){ int l, r; scanf ("%d %d" , &l, &r); nums.push_back ({l, r}); } vector<vector<int >> ret; sort (nums.begin (), nums.end (), [](auto a, auto b){ return a[1 ] < b[1 ]; }); ret.push_back (nums[0 ]); n = nums.size (); for (int i = 1 ; i < n; i++){ if (nums[i][0 ] > ret.back ()[1 ]){ ret.push_back (nums[i]); } } cout << ret.size () << endl; }
排序 快速排序
思路:
基于分治
1.先随便在数组中找一个值 作为分界点
2.根据x调整区间
3.递归处理左右两端
做法:
左右lr指针,当左边的数满足小于x时,l++;右边的数大于x时,r–。否则交换两个数。这样遍历一次之后满足l左边的数都小于等于x,r右边的数都大于等于x
期望时间复杂度O(nlogn)
每次层递归需要o(N)
总共有logN层
时间复杂度N*logN
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <iostream> using namespace std;const int N = 1e6 + 10 ;int q[N];void quick_sort (int q[], int l , int r) { if (l >= r){ return ; } int i = l - 1 ; int j = r + 1 ; int x = q[l + r >> 1 ]; while (i < j){ do { i++; }while (q[i] < x); do { j--; }while (q[j] > x); if (i < j){ swap (q[i], q[j]); } } quick_sort (q, l ,j); quick_sort (q, j + 1 , r); } int main () { int n; scanf ("%d" , &n); for (int i = 0 ; i < n; i++){ scanf ("%d" , &q[i]); } quick_sort (q, 0 , n - 1 ); for (int i = 0 ; i < n; i++){ printf ("%d " , q[i]); } return 0 ; }
归并排序
思路
主要思想还是分治,但是具体分法与快速排序不同。
快速排序是先处理在递归 ,归并排序是先递归在处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <iostream> using namespace std;const int N = 100010 ;int q[N], tmp[N]; int n;void merge_sort (int q[], int l, int r) { if (l >= r){ return ; } int mid = ((r - l) >> 1 ) + l; merge_sort (q, l, mid); merge_sort (q, mid + 1 , r); int i = l, j = mid + 1 ; int k = 0 ; while (i <= mid && j <= r){ if (q[i] < q[j]){ tmp[k++] = q[i++]; }else { tmp[k++] = q[j++]; } } while (i <= mid){ tmp[k++] = q[i++]; } while (j <= r){ tmp[k++] = q[j++]; } k = 0 ; for (i = l; i <= r;){ q[i++] = tmp[k++]; } } int main () { scanf ("%d" , &n); for (int i = 0 ; i < n; i++){ scanf ("%d" , &q[i]); } merge_sort (q,0 , n - 1 ); for (int i = 0 ; i < n; i++){ printf ("%d " , q[i]); } }
堆排序
堆是一颗二叉树(完全二叉树 除了最后一层外,所有节点都是非空的)
小根堆:每个节点≤左右儿子
存储方式
堆用一个一维数组存储 ,注意下标从1开始
1号节点为根节点。
x的左儿子:2x
x的右儿子:2x+ 1
STL优先队列中的底层是堆
两个基本操作
其他操作可以由这两个基本操作得出
插入:在整个堆的最后一个元素后加入x,up
最小值:整个数组的第一个元素
删除最小值:用整个堆(数组)的最后一个元素覆盖第一个元素,再down
删除任意一个元素k:用最后一个元素覆盖掉heap[k]。再做一次down和up操作
修改任意一个元素k heap[k] = x,再做一次down和up
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <iostream> #include <algorithm> using namespace std;const int N = 100010 ;int n, m;int heap[N];void down (int u) { int t = u; if (u * 2 <= n && heap[u * 2 ] < heap[t]){ t = u * 2 ; } if (u * 2 + 1 <= n && heap[u * 2 + 1 ] < heap[t]){ t = u * 2 + 1 ; } if (t != u){ swap (heap[t], heap[u]); down (t); } } int main () { scanf ("%d %d" , &n, &m); for (int i = 1 ; i <= n; i++){ scanf ("%d" , &heap[i]); } for (int i = n /2 ; i >= 0 ; i--){ down (i); } while (m--){ cout << heap[1 ] << " " ; heap[1 ] = heap[n]; n--; down (1 ); } }
拓扑排序
入度为0的边都可以作为起点,所以用一个队列维护入度为0的节点
宽度搜索,每次取出队头t,枚举t的所有出边 ,枚举其边t->j,减少j的入度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <iostream> #include <cstring> using namespace std;const int N = 100010 ;int h[N], ne[N], e[N], idx;int d[N],q[N];int n, m;bool topsort () { int hh = 0 , tt = -1 ; for (int i = 1 ; i <= n; i++){ if (d[i] == 0 ){ q[++tt] = i; } } while (hh <= tt){ int t = q[hh++]; for (int i = h[t]; i != -1 ; i = ne[i]){ int j = e[i]; d[j]--; if (d[j] == 0 ){ q[++tt] = j; } } } return tt == n - 1 ; } void add (int a, int b) { e[idx] = b; ne[idx] = h[a]; h[a] = idx++; } int main () { memset (h, -1 , sizeof (h)); scanf ("%d%d" , &n,&m); while (m--){ int a, b; scanf ("%d%d" , &a,&b); add (a,b); d[b]++; } if (topsort ()){ for (int i = 0 ; i < n; i++){ printf ("%d " , q[i]); } puts ("" ); }else { puts ("-1" ); } }
链表 k个一组翻转链表
思路:
需要用三个指针在k个数内反转:pre,start和next,这里逻辑简单来说就是将start链表的下一个元素指向pre,具体逻辑不在赘述
需要注意的是到第k个数如何解决:cur指针记录的是上k个数的最后一个数,所以tail尾指针应该是cur->next,也就是反转前当前k个数的第一个数,反转后的最后一个数。它的下一个元素指向下一组的第一个元素,即为现在的start。前k个数的最后一个数(pre)现在变为首元素。让cur->next 指向当前分组的头结点,再将cur更新为当前分组的尾结点。做到上一个分组的尾结点与当前分组的头结点相连接。最后再更新链表长度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 class Solution {public : ListNode* reverseKGroup (ListNode* head, int k) { ListNode* dummy = new ListNode (0 ); dummy->next = head; int len = 0 ; ListNode* cur = dummy; while (cur->next) { len++; cur = cur->next; } cur = dummy; while (len >= k) { ListNode* pre = nullptr ; ListNode* next = nullptr ; ListNode* start = cur->next; for (int i = 0 ; i < k; i++) { next = start->next; start->next = pre; pre = start; start = next; } ListNode* tail = cur->next; cur->next = pre; tail->next = start; cur = tail; len -= k; } return dummy->next; } };
合并k个有序链表
思路
将k个链表看成k次两个链表的合并
这样问题就变成了双链表合并
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 class Solution {public : ListNode* mergeTwoLists (ListNode* a, ListNode* b) { if (!a || !b){ return a == nullptr ? b : a; } ListNode* head = new ListNode (); ListNode* tail = head; ListNode* aPtr = a, *bPtr = b; while (aPtr && bPtr){ if (aPtr->val < bPtr->val){ tail->next = aPtr; aPtr = aPtr->next; }else { tail->next = bPtr; bPtr = bPtr->next; } tail = tail->next; } tail->next = (aPtr == nullptr ) ? bPtr : aPtr; return head->next; } ListNode* merge (vector<ListNode*> lists, int l, int r) { if (l == r){ return lists[l]; } if (l > r){ return nullptr ; } int mid = l + ((r - l) >> 1 ); return mergeTwoLists (merge (lists, l, mid), merge (lists, mid + 1 , r)); } ListNode* mergeKLists (vector<ListNode*>& lists) { return merge (lists, 0 , lists.size () - 1 ); } };
环形链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Solution {public : ListNode *detectCycle (ListNode *head) { ListNode* slow = head; ListNode* fast = head; while (fast && fast->next){ slow = slow->next; fast = fast->next->next; if (slow == fast){ slow = head; while (slow != fast){ slow = slow->next; fast = fast->next; } return slow; } } return nullptr ; } };
滑动窗口 最短且字典序最小的美丽子字符串
思路:
最开始枚举长度为k的子字符串,一次递增到字符串长度。
寻找字典序最小的子字符串,更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Solution {public : string shortestBeautifulSubstring (string s, int k) { if (count (s.begin (), s.end (), '1' ) < k){ return "" ; } for (int size = k;;size++){ string ans = "" ; for (int i = size; i <= s.size (); i++){ string t = s.substr (i - size, size); if ((ans =="" || t < ans) && count (t.begin (), t.end (), '1' ) == k){ ans = t; } } if (!ans.empty ()){ return ans; } } return "" ; } };