浅浅记录一下自己的代码随想录刷题复盘记录

20240418:

复习 单调栈 + 动规基本题目:

1.每日温度(单调栈中存放单调递增的元素遍历发现当前元素大于栈顶元素,立即记录两者下标的差值弹出栈顶元素,若小于,就加入栈中,接着向后遍历)

2.下一个更大元素 (首先用unordered_map进行一个nums1数值与下标的映射,方便nums2快速找到nums1中各个元素的下标值(如果有)。用上一题相同的逻辑进行处理)

3. 下一个更大元素 (和每日温度类似,但数组是循环遍历的,用取模运算表示循环过程)

  1. 接雨水(通过单调栈确定左边和右边第一个比当前元素大或者小的元素值,若遍历发现当前元素值大于栈顶元素值,说明凹槽出现,可以承接雨水了,记录下当前的栈顶元素,然后弹出。当遍历发现元素值相同时,更新栈顶元素,)
  2. 柱状图中最大的矩形(接雨水类似,找到当前遍历数字左右两边第一个小于当前数字的值,即保证单调栈中数字为递减顺序。此时栈顶元素栈中下一元素即将进栈元素构成了我们要求最大面积矩形的高度与宽度,其他逻辑与接雨水大致相同
  3. 斐波那契数
  4. 爬楼梯
  5. 使用最小的花费爬楼梯 (没有考虑好cost 楼梯阶数n 的关系  n = cost.size()+1
  6. 不同路径(初始化dp[0][j]与dp[i][0]为1,因为到达这两行中各点的路径数量只有1
  7. 不同路径
  8. 整数拆分(递推公式部分:用另一个for去遍历 i 之前的正整数,dp[i] 为拆分数字i所能得到的最大乘积,dp[i]与拆成的dp[j]dp[i - j]有关)
  9. 不同的二叉搜索树(dp[i] = 头节点元素为1的二叉搜索树数量 + 头节点元素为2的二叉搜索树数量 + ··· + 头节点元素为i的二叉搜索树的数量 里面的for是用来遍历头节点的

2024.04.19:

复习 动规背包题目:

背包问题,即如何在有限数量的货物中选取使具有一定容量的背包中所装货物价值最大。使用动规五步曲进行分析,使用二维数组dp[i][j]表示下标从0i货物装在容量为j背包中的最大价值,dp[i][j]可由不放物品idp[i -1][j]放置物品idp[i - 1][j - weight[i]]两个方向确定,可得递推公式为dp[i][j] = max(dp[i -1][j], dp[i - 1][j - weight[i]])。再对dp数组进行初始化,j0时,背包里无法装入任何物品,最大价值dp[i][0] = 0,i为0时,在0号物品中进行选取,当j < weight[0]时,背包无法放入物品0,dp[0][j]=0,当j>=weight[0]时,背包里可以放入物品0,dp[0][j] = value[0]。dp数组其他位置初始化为0即可,因为这些位置的值是由dp[i - 1][j]dp[i - 1][j - weight[i]]决定的。遍历顺序为先遍历物品,再遍历背包

  1. 01背包理论基础-难在如何初始化和遍历顺序上, 01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次
  2.  01背包理论基础(滚动数组)把dp[i - 1]那一层拷贝到dp[i]上,

表达式是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);

dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]

dp[j]需要在自身与去掉物品i之间相比较,所以dp数组直接初始化为0即可。与二维dp数组不同的是,一维数组的背包遍历需要从后向前遍历,防止同一物品被多次取到

  1. 分割等和子集-(定义的vector 大小是 10001
  2. 最后一块石头的重量(跟上面那个类似,本题可以看作将一堆石头尽可能分成两份重量相似的石头,于是问题转化为如何合理取石头,使其装满容量为石头总重量一半的背包,且每个石头只能取一次,这样就变成了一个01背包问题)
  3. 目标和(题目转化为要装满容量为x的背包共有几种方法,且每个数的状态只能取一次,转化为01背包问题,同时需要注意以下几个问题:

1.target 可能小于0

2.target+sum要先判断奇偶,

3.dp[j] += dp[j-nums[i]]

只要找到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法,接着累加就行

这是一类题型:装满容量为x的背包共有多少种方法:dp[j] += dp[j-nums[i]]

  1. 一和零(完全没思路,本题的背包维度有两个:mn,即如何选取元素使元素满足0、1的个数要求。利用动规五步曲:dp[i][j]为拥有i个0,j个1的元素个数。dp[i][j]可由去掉上一个字符串时dp[i - 0nums][j - 1nums]得出,即dp[i][j] = max(dp[i][j], dp[i - 0nums][j - 1nums] + 1)。由题意可知dp数组初始化为0即可
  2. 完全背包理论:相比于01背包,物品数量不一,可复拿,所以内层要从小到大
  3. 零钱兑换(把背包装满有多少种方法,累加公式都是: dp[j] += dp[j – nums[i]]

如果求组合数就是外层for循环遍历物品内层for遍历背包

如果求排列数就是外层for遍历背包内层for循环遍历物品

该题求出的方法都属于组合问题,即选取硬币的顺序不会对组合数量产生影响

  1. 组合总数排列,老是忘了 if条件中有一个 dp[i] < INT_MAX -dp[i-nums[j]]
  2. 零钱兑换(没有想到初始化成最大值,要先判断dp[j-coins[i]] != INT_MAX,再推导

如何使用最少的零钱数凑满背包。动规五步曲:dp[j] 凑足总额为j的最小零钱数量;当遍历到面值为coins[i]的零钱时有两种选择,取与不取。coins[i]时,dp[j] = dp[j - coins[i]] + 1不取coins[i]时,dp[j] = dp[j],因此dp[j] = min(dp[j], dp[j - coins[i]] + 1)。题目中给出了dp[0] = 0,剩余dp数组值只有初始化为INT_MAX时,才能保证状态转移时能取到最小。由于本题中要求的是最少零钱数,遍历的顺序没有具体要求,先背包后物品,先物品后背包均可。)

  1. 完全平方数(跟上题一样)

还有另一种做法: 就是把1,4,9这些平方数视为零钱面额

  1. 单词拆分(没思路

单词看作一个个物品字符串看作需要装满的背包,由于能够重复使用字典中的单词,所以这是一道完全背包问题。dp[i]true时表示长度为i的字符可以被字典中的单词拆分。当dp[j]为true时,遍历发现字符串[j, i]在字典中,则dp[i]一定为true。初始化dp[0]为true,其余dp数组值初始化为false。由于本题中字典单词的排列会影响字符串的组成,因此可视作求排列数,先遍历背包,再遍历物品

  1. 多重背包:01背包里面在加一个for循环遍历一个每种商品的数量

背包问题总结:背包问题的维度一般有以下几种:

给定背包容量的情况下,装满背包的最大价值能否装满装满背包的方法数量装满时背包中物品最小数量

  1. 给定背包容量,装满背包的最大价值,递推公式一般是:

dp[j] = max(dp[j], dp[j-weight[i]] + value[i];

dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);

  1. 问能否能装满背包(或者最多装多少dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
  2. 求装满已知容量背包的方法数量,递推公式一般是:dp[j] += dp[j - nums[i]]
  3. 问装满背包的最小个数(初始化成INT_MAXdp[j] = min(dp[j - coins[i]] + 1, dp[j])
  4. 一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包

二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。

  1. 完全背包:

如果求组合数就是外层for循环遍历物品,内层for遍历背包。

如果求排列数就是外层for遍历背包,内层for循环遍历物品

如果求装满背包的最小数量,那么两层for循环的先后顺序就无所谓了.

2024.04.20

复习-动规-打家劫舍+股票系列

  1. 打家劫舍-动规经典题目-不太懂初始化(dp[0]=nums[0]

该房间偷与不偷依赖于前一个或前两个房间的状态,即当前状态是依赖于前一二个状态,对于第i个房间有偷与不偷两种状态,偷则dp[i] = dp[i - 2] + nums[i]。不偷则dp[i] = dp[i - 1]。

  1. 打家劫舍2(相比于上一题,成环了,首尾元素不能同时取到,分开讨论无首元素与无尾元素的情况,就是新建一个函数 传入nums 的范围
  2. 树型二叉树(没思路,求一个节点 偷(1)与不偷(0的两个状态所得到的金钱,那么返回值就是一个长度为2的数组,下标为0记录不偷该节点所得到的最大金钱,下标为1记录偷该节点所得到的最大金钱,二叉树三部曲+动规结合五部曲:1.确定函数的参数与返回值。返回该节点两种状态下能偷到的最大钱币数量,dp[0]是不偷该点时能得到的最大钱币数,dp[1]是偷该点时能得到的最大钱币数。2.确定终止条件,即遇到空节点返回。3.确定单层递归的逻辑。偷当前节点时,左右节点不偷不偷当前节点时,左右节点选偷。)
  3. 股票系列一:只能买卖一次(两种状态 持有 不持有 二维dp ,dp[i][0]表示持有股票的最大利润dp[i][1]表示不持有股票获得的最大利润。若第i天持有股票,则也有两种情况:i天前已持有股票,dp[i][0] = dp[i - 1][0];第i天时买入股票,dp[i][0] = - prices[i],dp[i][0]在两者中取最大值即可。若第i天未持有股票,对应情况相似:第i天前已经不在持有股票了,dp[i][1] = dp[i - 1][1];第i天时卖出股票,dp[i][1] = prices[i] + dp[i - 1][0],dp[i][1]取两者之间的最大值。根据题意与递推公式可知,dp[0][0] = -prices[0],dp[0][1] = 0。从前向后遍历dp数组)
  4. 股票系列二:能多次买卖(两种状态 持有0 不持有1 二维dp, 对于上一题中的dp[i][0]而言,当是i天买入股票时,dp[i][0] = dp[i - 1][1] - prices[i]
  5. 股票系列二:最多可以买卖 两次,直接跟下面那题一样 最多买卖 K,对应遍历的每一天,都有2k种状态,对应着第一次持有/不持有,直到第k次持有/不持有。建立一个大小为prices.size() * 2k+1大小的二维dp数组,递推公式dp[i][j+1] = max(dp[i-1][j+1] , dp[i-1][j] - prices[i])dp[i][j+2] = max(dp[i-1][j+2], dp[i - 1][j + 1] + prices[i])。将奇数下标的dp值初始化为-prices[0], dp[0][奇数下标] = -prices[0]。从前向后遍历dp数组。
  6. 股票系列三:可以多次买卖,但含冷冻期,分成4个状态
  7. 股票系列三:可以多次买卖,但含手续费(比系列2卖出的时候多了手续费)

股票系列总结:代码随想录 (programmercarl.com)

2024.04.21

复习 动规-子序列问题+回文

1.最长递增子序列(只有一个数组)(dp[i]: i之前的包括i的 以nums[i]为结尾的最长递增子序列长度,注意他不一定是从头开始的,当前下标i的递增子序列长度,其实i之前的下表j的子序列长度有关系,每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1,j其实就是遍历 0i-1能删除,对于遍历到的每一个数,都可以视为递增长度为1的独立子序列,初始化dp数组=1

2. 最长连续递增子序列(只有一个数组  不能删除 dp[i] 以i 结尾的最长连续递增子序列的长度)

3. 最长重复子数组(有两个数组 跟上两题有明显区别:关键在 i和j 必须能到 nums1.size() 和nums2.size(),才能保证覆盖了数组中的最后一个元素,因为dp[i][j] 表示的是 以nums1(i-1) nums2(j-1)结尾的最长重复子数组的长度)

4. 最长公共子序列(不要被字符串迷惑了,本质和上题类似,当nums[i - 1] != nums[j - 1]时,dp[i][j]状态取决于dp[i - 1][j] 与dp[i][j-1]当中的最大值,相当于各自将序列后退一个元素以避开不相同元素)

5. 不相交的线(跟上题一样,套壳的求最长公共子序列,因为本题中不仅要求了对应数字相同,同时要求相同数字在不同数组中的顺序保持一致,与最长公共子序列的题目描述如出一辙。)

总结: 只有一个数组或者字符串时,先考虑他是不是只有一个,是的话就返回长度1,

然后再去考虑比较的过程中能不能删除元素,能的话就再引入一个for循环去遍历 i 之前的数字, 当 nums[i] > nums[j] 时, dp[i] = max(dp[i], dp[j]+1);  如果不能删除, 那就只能挨个比较,看nums[i] > nums[i-1], 就不需要再引入j了,注意这里的dp[i]表示的是以 i 为结尾的连续递增子序列的长度。当有两个数组或者字符串,去找他们之间的最长公共子序列或者重复数组时,显然要设置二维的dp数组,尤其是dp[i][j] 表示的是 i-1 j-1结尾的公共子序列长度,因为是在两个数组之间进行比较,只有当nums1[i-1] == nums2[j-1]的时候,才有可能找出公共长度,尤其是要注意 vector<vector<int>> dp(nums1.size()+1, vector<int>(nums2.size()+1, 0)); 第二点需要注意:当可以删除数字或者字符时的判断条件只需要时if(nums1[j-1] == nums2[j-1]) dp[i][j] =max(dp[i-1][j-1] + 1, dp[i][j]); 当不可以删除数字或者字符时的判断条件还需要考虑不相等的情况(取两个数字移一位的较大者)if(nums1[j-1] == nums2[j-1]) dp[i][j] =dp[i-1][j-1] + 1; else dp[i][j] = max(dp[i-1][j], dp[i][j-1]),特别注意这一点,不相等时,就取让2个数组各退一步的最大值。

2024.04.22

复习  子序列问题 + 回文

  1. 最大子数组和 使用贪心的时候注意 totalsum 要初始化成 整数极小值!!!,在使用动规的时候注意初始化 dp[0] = nums[0]; result = dp[0]; dp[i] = max(nums[i], dp[i-1] + nums[i]);

编辑距离类型:

  1. 判断子序列(最长公共子序列的应用,当发现str1[i - 1] == str2[j - 1]时,相同子序列长度加一,dp[i][j]=dp[i - 1][j - 1] + 1,当发现str1[i - 1] != str2[j - 1]时,说明此次遍历位置字符并不相同,则dp[i][j]的状态不发生改变,即dp[i][j] = dp[i][j - 1]。dp[i][0],dp[0][j]表示字符串与空串的匹配情况,故dp[i][0]dp[0][j]均初始化为0。从前向后遍历dp数组
  2. 不同的子序列(有点难,主要是需要注意相等时的两种情况dp[i][j]表示以i - 1为结尾的字符串s中出现以j - 1为结尾的字符串t的个数。s[i - 1]==t[j - 1],说明可能会出现一次新的匹配,只需观察i-1j-1之前的匹配情况,判断能否出现一次新的匹配,同时不能丢掉以前的匹配数量,从i-1之前的匹配数量为dp[i-1][j]。综上dp[i][j] = dp[i-1][j-1] + dp[i - 1][j]。若s[i - 1] != t[j - 1],说明此次并未出现新的匹配,dp[i][j]的状态与遍历i-1前相同,即dp[i][j] = dp[i - 1][j]。当j=0时,t为空字符串,s只有当i=0时才会发生一次匹配,故dp[i][0] = 1;而i=0时,空字符串中不会含有字符串t,故dp[0][j] = 0。i,j同时为0时,空字符串中紧=仅含有一个空字符串,dp[0][0] = 1
  3. 两个字符串的删除操作(第一种解法可看作是:最长公共子序列的应用;第二种解法:dp[i][j]表示以i-1为结尾的字符串1,以j-1为结尾的字符串2想要达到相同,需要删除字符的最小步骤。当遍历字符发现word1[i - 1] == word2[j - 1]时,说明两字符相同,不需要删除操作,d[i][j]状态不变,dp[i][j] = dp[i - 1][j - 1]。当两者不同时,需要删除操作,可以删除word1[i - 1],也可以删除word2[j - 1],也可以同时删除word1[i - 1]word2[j - 1],dp[i][j]在dp[i - 1][j] + 1、dp[i][j - 1] + 1、dp[i - 1][j - 1]中取最小值,dp[i][j] = min(dp[i -1][j] + 1, dp[i][j -1] + 1, dp[i - 1][j - 1] + 2)。由递推公式可以发现,i j状态与i-1 j-1状态息息相关,因此我们需要对i = 0 || j = 0情况进行初始化,以保证递推公式的正确运行。i = 0时,word1为空字符串,长度为j的word2想要与word1保持相同,只需将j个字符全部删除即可,dp[0][j] = j。同理dp[i][0] = i)
  4. 编辑距离(dp[i][j]表示以i-1为结尾的字符串1,以j-1为结尾的字符串2想要达到相同,需要编辑字符的最小步骤。当word1[i - 1] == word2[j - 1]时,什么都不需要做,dp[i][j] = dp[i - 1][j - 1],当两者不同时,可选择:增加元素,删除元素,改变元素值。可以对两个word字符串同时操作,那么当一个单词需要添加一个字符使得和另一个单词保持一致,我们只需要把另一个单词的对应字符删除即可,换句话说,添加字符与删除字符是等价的,不用单独考虑添加操作。对于替换操作来说,仅需一步达到word1[i - 1]==word2[j - 1],此时替换前dp[i][j] = dp[i - 1][j - 1],替换后 dp[i][j] = dp[i - 1][j - 1] + 1。由递推公式可以发现,i j状态与i-1 j-1状态息息相关,因此需要对i = 0 || j = 0进行初始化,此时word1与word2中有空串的情况,dp[i][0] = idp[0][j] = j

回文子串系列:

  1. 回文子串(好难,dp[i][j] 表示[i, j]范围内的字串是否为回文子串,变量类型为bool。当s[i] == s[j]时,要确定i j的大小关系,有i==ji==j-1j-1>1三种情况。i == j,显然是回文子串,dp[i][j] = truej - i == 1时,字符串中仅含两个相同元素,也一定是回文子串,dp[i][j] = true;当串中含有2个以上字符时,dp数组要确定内侧字符串是否为回文子串dp[i][j] = dp[i + 1][j - 1]i往前走一步 i+1 j往后退一步j-1。dp数组为bool类型,初始化为false。dp[i][j]状态与dp[i + 1][j - 1]的状态一致,对于ij的遍历顺序是不同的,i从大到小遍历j从小到大遍历
  2. 最长回文子序列(好难, dp[i][j] 表示i开头以j结尾的字符串中的最大回文子序列长度。需要判断s[i] ,s[j]是否相同。当s[i] == s[j] 时,内侧的最长回文子序列长度加2,dp[i][j] = dp[i+1][j - 1] + 2(因为 i+1, j-1,走了 2 );当s[i] != s[j]时,说明两字符不能同时加入回文子串,应当分开讨论,若不取s[i]dp[i][j] = dp[i + 1][j] ,若不取s[j]dp[i][j] = dp[i][j - 1]。由于每个单字符都是长度为1回文子串dp[i][i]应初始化为1

注意 j 是从 i+1 开始的,不考虑 j == i)

2024.04.24

复习贪心(无套路,局部最优推出全局最优)

  1. 分发饼干(优先喂饱胃口小的孩子s[i] >= g[childindex],尽可能的喂饱更多的孩子,千万别忘了事先排序
  2. 摆动序列(删除单调过程中的点,保留峰值,确定摆动数量)
  3. 最大子序和(动规已经做过了)、
  4. 买卖股票的最佳时机(动规已经做过了)、
  5. 跳跃游戏(首先考虑nums.size()是否为1. 遍历的是下标,用覆盖范围确定最后一个元素是否能被覆盖掉)
  6. 跳跃游戏2(求最少跳跃次数,局部最优:求当前这步的最大覆盖(nextcover = max(nextcover, nums[i]+i)),那么尽可能多走(i==curcover,到达覆盖范围的终点(nextcover >= nums.size()),只需要一步。整体最优:达到终点,步数最少: 移动下标是到当前这步覆盖的最远距离,此时没有到终点,只能增加下一步来扩大覆盖范围。
  7. K次取反后最大化的数组和(根据K的奇偶,使用了两次贪心策略:第一次是优先将绝对值大的负数进行取反,若负数取完后,取反次数仍有剩余,则将小正数进行取反。)
  8. 加油站(求出每一站的剩余油量并相加,如果当前遍历发现剩余油量小于0,则重新从下一站开始遍历,并将剩余油量重置为0)
  9. 分发糖果(需要两次遍历确定当前元素与两边元素的大小关系。从前向后比较当前元素与左侧元素的大小关系,再从后向前比较当前元素与右边元素的大小关系。)
  10. 柠檬水找零
  11. 根据身高重建队列(两个维度,先确定身高,再确定人数,身高从大到小排列后对人数放心插入即可,因为前方都是大数,小数的插入并不影响第二维度

2024.04.25

复习贪心后半部分

  1. 射气球:用最小的弓箭数射最多的气球,使气球尽可能重叠就可以了。所以需要将气球左区间进行排列,判断相邻气球的左右区间情况,若当前气球右区间大于上一气球左区间,则需要弓箭数加一。若不大于,则将两气球视为重叠气球,同时更新重叠气球的右区间(min(points[i][1], points[i-1][1]),以便判断与下一气球的重叠情况。)
  2. 无重叠区间(跟上题异曲同工)

3. 划分字母区间(完全没思路,在遍历的过程中相当于是要找每一个字母的边界,如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了。此时前面出现过所有字母,最远也就到这个边界了。)

可以分为如下两步:

(1)统计每一个字符最后出现的位置(数组哈希表  一一映射)

(2)从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了(i==right),则找到了分割点

4.  合并区间(射气球的应用:按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,可以合并更多的区间,整体最优:合并所有重叠的区间。

具体操作:按照左边界从小到大排序之后,如果 intervals[i][0] < result.back()[1] 即intervals[i]左边界 < 容器中最后一个区间的右边界,则一定有重复,因为intervals[i]的左边界一定是大于等于容器中最后一个区间的左边界)

5.  单调递增的数字(给出小于题目值的最大数,且该数的各位都要满足单调递增从后向前遍历字符串,若不满足单调递增,则将前一位数字减一,同时将后续数字置为9.想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想strNum[i - 1]减一strNum[i]赋值9,这样这个整数就是89,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。)

6. 监控二叉树(二叉树的应用:从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!从低到上(后序遍历也就是左右中的顺序),先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点: 假定3种状态:0:该节点无覆盖;1:本节点有摄像头2:本节点有覆盖;并就假定空节点的状态是有覆盖2,)

贪心总结:

贪心简单题:分发饼干,K次取反后最大化的数组和,柠檬水找零

贪心中等题:靠常识可能就有点想不出来了。开始初现贪心算法的难度与巧妙之处。

摆动序列,单调递增的数字(opens new window)

两个维度权衡问题:在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。

分发糖果,根据身高重建队列

贪心解决区间问题:跳跃游戏,跳跃游戏II,用最少数量的箭引爆气球,无重叠区间,划分字母区间,合并区间

#其他难题

2024.04.26复习 回溯

回溯的本质是穷举,穷举所有可能,然后选出想要的答案,如果想让回溯法高效一些,可以加一些剪枝(去重)的操作,但也改不了回溯法就是穷举的本质。

回溯法,一般可以解决以下几种问题:

组合问题N个数里面按一定规则找出k个数的集合

切割问题:一个字符串按一定规则有几种切割方式

子集问题:N个数的集合里有多少符合条件的子集

排列问题:N个数按一定规则全排列,有几种排列方式

棋盘问题:N皇后解数独(困难)等等

回溯法一般是在集合中递归搜索,集合的大小(size)构成了树的宽度递归的深度构成的树的深度

终止条件——for循环遍历——递归——回溯

回溯模板框架:

  1. 组合(入门题,注意startindex 的使用是防止重复
  2. 上一题的优化:可以剪枝的地方就在递归中每一层的for循环所选择的起始位置

如果for循环选择的起始位置之后的元素个数(n-i 已经不足 我们需要的元素个数(k-path.size())了,那么就没有必要搜索了。

  1. 在第一题的基础上 多了个 和为sum 的判断
  2. 电话号码的字母组合(求的是不同集合下的组合,如果是一个集合来求组合的话,就需要startindex,如果是多个集合取组合,各个集合之间相互不影响,那么就不用)
  3. 组合总数(数组无重复+可重复取+结果不重复)
  4. 组合总数(数组有重复+可重复取+结果不重复)

另一种方法,不用mark, 用一个整型sum, 来记录和。

终止条件:当 sum == target,将结果放进result里,

在求和问题中,排序之后加剪枝是常见的套路

  1. 分割字符串

本题这涉及到两个关键问题:

1. 切割问题,有不同的切割方式+

2. 如何判断回文串

在处理组合问题的时候,递归参数需要传入startindex,表示下一轮递归遍历的起始位置,在本题中,这个startindex就是切割线。

终止条件:startindex >= s.size()

说明分割已经完成

2024.04.27

1. 复原IP地址(切割问题就可以使用回溯搜索法把所有可能性搜出来,startindex记录下一层递归分割的起始位置。pointNum,记录添加逗点的数量。)

2.  子集系列1-子集(子集问题与组合问题的不同之处在于:考虑子集问题时,树中每个结点都需要收集,即每层递归的结果都要加入result。)

  1. 子集2(数组中包含重复元素)

还有另一种标记已经遍历过的数组:用unordered_set来记录已经遍历过的组合数字

  1. 非递减子序列(注意终止条件的变化,数组不需要排序处理,本题要求是要递增子序列,排序后就没意义了)
  2. 排列问题-全排列(无需再考虑树层去重,只需在同一树枝上对已使用元素进行标记即可
  3. 全排列2(包含重复元素—要去重,必先考虑排序~!!!!要求在存在重复元素的数组中求出所有不重复的排列。需要灵活运用树枝去重与树层去重。)
  4. N皇后(通过每行遍历,将列依次放入皇后,向下一行递归,并调用自定义的函数判断合法性,若当前位置合法,则放入皇后.)
  5. 解数独(需要两个for循环确定一个点)

回溯是递归的副产品,只要有递归就会有回溯

去重之前想下要不要先对数组进行排序(组合总数)

非递减子序列:// 注意去重 条件: path 非空(因为后面) nums[i] < path.back()  或者 前面用过if((!path.empty() && nums[i] < path.back()) || mark.find(nums[i]) != mark.end())

{

                continue;

            }

排列是有序的,处理排列问题不用再使用startindex,一般是从0开始,另外需要数组记录path里都放了哪些元素了, 数组中无重复元素时的去重操作:if(mark[i] == true) continue; 如下所示:

N 皇后   解数独 要多做几遍!!!!!!!!

2024.04.30

复习二叉树

二叉树的种类:

满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。也可以说深度为k,有2k-1个节点的二叉树

完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2h-1 个节点。

二叉搜索树:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;它的左、右子树也分别为二叉排序树

平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。C++中mapsetmultimapmultiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意unordered_mapunordered_setunordered_mapunordered_set底层实现是哈希表

  • 深度优先遍历

前序遍历(递归法,迭代法)中序遍历(递归法,迭代法)后序遍历(递归法,迭代法)

  • 广度优先遍历: 层次遍历(迭代法)

中间节点的顺序就是所谓的遍历方式

  • 前序遍历:中左右
  • 中序遍历:左中右
  • 后序遍历:左右中

2024.05.01

二叉树递归法的三要素:

  1. 确定递归函数的参数和返回值:因为要打印出遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型一般是void
  2. 确定终止条件:在递归的过程中,如何算是递归结束,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return
  3. 确定单层递归的逻辑(上面的三种: 前中后)

二叉树的迭代法遍历其实就是用来实现递归的写法,主要 stack是先进后出

二叉树的层序遍历 (广度优先搜索,即逐层地,从左到右访问所有节点)

需要借用一个辅助数据结构即队列来实现,队列先进先出符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。

(1)  层序遍历(自上而下)                     (2)层序遍历(自下而上)

(3) 二叉树的右视图                                  (4)层平均值(数据类型是double

(5) N叉树的层序遍历(注意 children)(6)二叉树每一层的最大值(初始化成极小

(7) 填充每个节点的下一个右侧节点指针(8) 二叉树的 最大深度

最大深度即求从根节点最远叶子节点最长路径上的节点数,其实就是求最大层数

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。(只有当左右孩子都为空的时候,才说明遍历的最低点了

9. 翻转二叉树:

2024.05.03

1. 对称二叉树

对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(注意这不是层序遍历

(1) 递归                                         (2) 迭代(用队列模拟)

2. 完全二叉树的节点个数(直接按照普通为叉树的计算方法)

(1)递归(后序遍历)                      (2)迭代(层序遍历)

3. 平衡二叉树:给定一个二叉树,判断它是否是高度平衡的二叉树。(递归法)

一棵高度平衡二叉树定义为:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1

二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数

二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数

深度从上到下去查,前序遍历(中左右)

高度从下到上去查,后序遍历(左右中)

1 开始计数

递归(后序)迭代法太麻烦 求深度适合用前序遍历(层序),而求高度适合用后序遍历

4. 二叉树的所有路径:求从根节点到叶子的路径,只能前序遍历,方便让父节点指向子节点,找到对应的路径。还涉及到回溯,要把路径记录下来,需要回溯来回退一个路径再进入另一个路径

前序遍历:中写在终止条件前面是因为最后一个节点也要加入到path中, 整个遍历才到了叶子节点

两个pop_back() 是因为 添加的字符是两个

注意 这道题的返回的是 string 类型的, 而遍历的节点 是整型的

所以会用到 to_string 将 整型转换成 string

traversal传入的 string path 不需要加引用 &

5. 左叶子之和: 左叶子的明确定义:节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点

判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子

递归遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和。

单层递归的逻辑:当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。

(1)递归                               (2)迭代(用stack 模拟递归)

6. 找树左下角的值(层序遍历 用一个值 去记录和更新 每层第一个值即左下角的值)

7. 路径总和

递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

(1)如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。

(2)如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。

(3)如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。

(1)路径总和1(是否有符合条件的路径) (2)路径总和2(求所有符合条件的路径)

2024.05.04

1. 从中序后序遍历序列构造二叉树

思路:以后序数组(左右中)最后一个元素(必然是根节点)为切割点,先切中序数组,根据中序数组(左中右),反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。

构建新的 中序数组时,用的是左闭右开, 所以右半区间 起始要记得 + 1;

构建新的 后序数组时,首先去掉最初就确定了的根节点,再利用了前面的中序数组的 左半数组的大小

2. 最大二叉树

给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:

二叉树的是数组中的最大元素

左子树是通过数组中最大值左边部分构造出的最大二叉树。即区间left()index()内的区域,重新递归

右子树是通过数组中最大值右边部分构造出的最大二叉树。即区间index+1()right()内的区域,重新递归

通过给定的数组构建最大二叉树,并且输出这个树的根节点

构造树一般采用的是前序遍历先构造中间节点然后递归构造左子树和右子树

3. 合并二叉树

(1) 迭代法(用队列代替递归)        (2) 递归法(3种顺序都可以)

合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。(合并必须从两个树的根节点开始)

2024.05.07

二叉搜索树(BST)系列

1. 二叉搜索树中的搜索

给定二叉搜索树(BST)的根节点和一个值。 在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL

二叉搜索树是一个有序树:

若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

它的左、右子树也分别为二叉搜索树

二叉搜索树的节点是有序的,所以可以有方向的去搜索。如果root->val > val,搜索左子树,如果root->val < val,就搜索右子树,最后如果都没有搜索到,就返回NULL。

(1)递归法                             (2)迭代法

2. 验证二叉搜索树(BST)

中序遍历下,输出的二叉搜索树节点的数值是有序序列,有了这个特性,验证二叉搜索树,就相当于变成了判断中序遍历下一个序列是不是递增的. 思路就是:可以递归中序遍历将二叉搜索树转变成一个数组,然后比较一下,这个数组是否是有序的,注意二叉搜索树中不能有重复元素。

(1) 递归法                                 (2)迭代法(注意栈! 顺序相反)

3. 二叉搜索树的最小绝对差

计算一棵所有节点为非负值的二叉搜索中任意两节点差的绝对值的最小值

思路:中序遍历下在一个有序数组上求最值,求差值.

(1)递归法                              (2)迭代法

4. 二叉搜索树中的众数(这里二叉搜索树的定义有点不同,允许等于)

给定一个有相同值(BST),找出 BST 中的所有众数(出现频率最高的元素)

(1)递归                            (2) 迭代法

 2024.05.09

1. 二叉树的最近的公共祖先

最近公共祖先的定义为:对于有根树 T 的两个节点 pq,最近公共祖先表示为一个结点 x,满足 x pq 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)

条件:所有节点的值都是唯一的。 p、q 为不同节点且均存在于给定的二叉树中.

后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点

递归思路:如果递归遍历遇到q,就将q返回遇到p 就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q p 的最近祖先

如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树

要搜索一条边                                                       搜索整个树(或者构建 树!)

在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量leftright接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)

1. 求最小公共祖先,需要从底向上遍历,只能通过后序遍历实现从底向上的遍历方式。

2. 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。

3. 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。

 2. 二叉搜索树的最近的公共祖先

二叉搜索树是有序的:如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p。当从上向下去递归遍历,第一次遇到 cur节点是数值[q, p]区间中,那么cur就是 q和p的最近公共祖先

(1)递归法(可知使用上一题的,或者下面的) (2迭代法 while)

这里注意:迭代法中的  要用 if(){} else if

(){}  else

最后都不成立 就返回 NULL

3. 二叉搜索树中的插入操作

(1)递归法                          (2) 迭代法

终止条件:遍历的节点==null,

就是要插入节点的位置,

把插入的节点返回

                                  

2024.05.10

1. 删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用.

有以下五种情况:

(1)没找到删除的节点,遍历到空节点直接返回

找到删除的节点 node

(2)node的左右孩子都为空叶子节点),直接删除节点返回NULL为根节点

(3)node的左孩子为空右孩子不为空,删除节点,右孩子补位返回右孩子为根节点

(4)node的右孩子为空左孩子不为空,删除节点,左孩子补位返回左孩子为根节点

(5)node的左右孩子节点都不为空,则将node的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。

2. 修剪二叉搜索

给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。

3. 将有序数组转换为二叉搜索树(中序遍历下 由小到大)

将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

4. 二叉搜索树转换为累加树(反中序遍历并引入pre,当前节点值 += 上一个节点的值(pre))

给出二叉搜索树的根节点,该树的节点值各不相同,将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组[20, 18, 13]

从树中可以看出累加的顺序是右中左,所以需要反中序遍历这个二叉树,然后顺序累加

需要一个pre指针记录当前遍历节点cur的前一个节点,这样方便做累加

右中左来遍历二叉树, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值

二叉树总结:

涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点

求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。

求二叉搜索树的属性,一定是中序,利用有序性。

2024.05.11

复习栈与队列(重点 用栈实现队列 用队列实现栈(手撕代码))

1. 栈和队列的基础知识

栈(stack提供push pop 等等接口,所有元素必须符合先进后出规则,所以不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素.

栈是依靠底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器). 栈的底层实现可以是vectordequelist 都是可以的, 主要就是数组和链表的底层实现,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。deque是一个双向队列封住一段,只开通另一端就可以实现栈的逻辑了。

队列(queue)中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, SGI STL中队列一样是以deque为缺省情况下的底部结构。也可以指定list 为起底层实现, STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)

1. 用栈实现队列(必须熟练掌握!!!!!!!

使用栈实现队列的下列操作:

push(x) -- 将一个元素放入队列的尾部

pop() -- 从队列首部移除元素

peek() -- 返回队列首部的元素

empty() -- 返回队列是否为空

思路:使用栈来模式队列的行需要两个栈:

一个输入栈,一个输出栈

在push数据的时候,把数据放进输入栈,但pop,操作复杂一些,输出栈如果为空,就把输入栈数据全部导入进来(注意是全部导入),再从输出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据。如果输入栈输出栈都为空的话,说明模拟的队列为空了。

2. 用队列实现栈(必须熟练掌握)

使用队列实现栈的下列操作:

push(x) 元素 x 入栈 pop() 移除栈顶元素 top() 获取栈顶元素 empty() 返回栈是否为空

思路:用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。栈(先进后出)队列(先进先出)

两种实现方案(都要掌握!!

(1)两个队列 (que2更新que1) 用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用,que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。     

(2)1个队列

一个队列在模拟栈弹出元素的时候只要将队列头部的元素除了最后一个元素外, 这个通过提前size—来实现) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。

3. 有效的括号(成对存在的,括号匹配是使用栈解决的经典问题)

思路:对应字符s的每一个括号,先将其对应的结果放进stack中,再用来做匹配(s[i] 和 st.top())!

三种不匹配的情况:

第一种情况,字符串里左方向的括号多了。判断思路:遍历完了字符串之后,若栈不为空,说明有相应的左括号没有右括号来匹配,return st.empty()

第二种情况,括号没有多余,但是 括号的类型没有匹配上。判断思路:遍历字符串匹配的过程中,若发现栈里没有要匹配的字符(s[i] != st.top()),return false

第三种情况,字符串里右方向的括号多了。判断思路:遍历字符串匹配的过程中,栈为空了(st.empty()),没有匹配的字符了,说明右括号没有找到对应的左括号,return false

4. 删除字符串中的所有相邻重复项(用栈解决的经典题目)

给出由小写字母(26)组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。在 S 上反复执行重复项删除操作,直到无法继续删除。在完成所有重复项删除操作后返回最终的字符串。答案保证唯一

思路:在删除相邻重复项时,关键是要知道当前遍历的这个元素在前一位是不是已经遍历过一样数值的元素,那么用栈来存放前面遍历过的元素。在遍历当前的这个元素的时候,去栈里看一下是不是遍历过相同数值的相邻元素。再从栈中弹出剩余元素,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转就得到了最终的结果

(1)引入栈                               (2)优化:字符串本身作为栈

5. 逆波兰表达式求值(后缀表达式, 逆波兰表达式相当于是二叉树中的后序遍历

基础知识:逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面

平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 )

该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * )

逆波兰表达式主要有以下两个优点:

1. 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。

2. 适合用栈操作运算:遇到数字则压入栈遇到运算符取出栈顶两个数字进行计算,并将结果压入栈中

6. 滑动窗口最大值(使用单调队列的经典困难题目)

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。需要自己来实现一个单调队列(很难啊)

就是先自定义一个 单调队列(由大到小),再用vector 保存每次滑动的窗口内的最大值!

单调队列实现的主要功能就是 将队列内部的数字从大到小排列,确保最大值在队列的最前面。pop  push  front

第一步:将nums 的前 k 个元素 放进 单调队列中

第二步:滑动剩下的元素,并把最大值放进结果集。

单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列

定义的单调递增类 一定别忘了 放在 public 下,否则会无法调用!!!!!!!!!!

7. 前 K 个高频元素(给定一个非空的整数数组,返回其中出现频率前 k 的元素)

(1)统计元素出现频率:使用map来进行统计

(2)对频率排序:优先级队列,对外接口只能从队头取元素,从队尾添加元素,再无其他取元素的方式,内部元素是自动依照元素的权值排列。缺省情况下优先级队列(priority_queue)利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是vector为表现形式的complete binary tree(完全二叉树). 堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父结点是大于等于左右孩子就是大顶堆小于等于左右孩子就是小顶堆。(堆头是最大元素),小顶堆(堆头是最小元素),从小到大排就是小顶堆从大到小排就是大顶堆

本题要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。

(3)找出前K个高频元素

面试题:栈里面的元素在内存中是连续分布的么?

有两个陷阱:

1栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不一定是连续分布的。

2缺省情况下,默认底层容器是deque,那么deque在内存中的数据分布是不连续的

设计单调队列的时候,pop,和push操作要保持如下规则:

1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作

2.  push(value):如果push的元素value大于入口元素的数值,那么就将队列出口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止

2024.05.15

双指针

1. 移除元素()

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素

思路:数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖

双指针法: 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

快指针:寻找新数组的元素,新数组就是不含有目标元素的数组

慢指针:指向更新 新数组下标的位置

2. 反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。

思路:对于字符串,定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。

3. 给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。 例如,对于输入字符串 "a1b2c3",函数应该将其转换为 "anumberbnumbercnumber"。

第一步:统计 字符串中的 数字个数

第二步:扩充数组到每个数字字符替换成 "number" 之后的大小

第三步:从后向前替换数字字符,也就是双指针法,过程如下:i指向新长度的末尾,j指向旧长度的末尾。旧数组从后向前,开始赋值到新数组,新数组也是从后向前遍历将s[j]赋给s[i],

很多数组填充类的问题,其做法都是先预先给数组扩容到填充后的大小,然后再从后向前进行操作

两个好处:

1. 不用申请新数组。2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题

4. 翻转字符串里的单词

给定一个字符串,逐个翻转字符串中的每个单词。

思路:(1)去除所有空格并在相邻单词之间添加空格, 保证单词之间之只有一个空格,且字符串首尾没空格快慢指针

当快指针 非空格时,slow!=0,说明不是第一个,给他的下一个赋值成空格, 再通过while将单词 赋给slow下标的位置,并不断更新 slow 和 fast 的下标

(2)将整个字符串反转 reverse(s, 0, s.size() - 1);

(3)将每个单词反转,到达空格或者串尾,说明一个单词结束。进行翻转reverse(s, start, i - 1); 

这里注意一下, 我们引入了 start 帮忙 更新到下一个单词

For循环的  i<=s.size(); 这个跟平时不太一样,额外注意

主要是因为 我们自定义的翻转函数 是 左闭右闭的

确保能覆盖到 字符串的 最后一个 字符

5. 反转链表

反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL  输出: 5->4->3->2->1->NULL

只需要改变链表的next指针的指向,直接将链表反转,不用重新定义一个新的链表

(1) 定义一个cur指针指向头结点,再定义一个pre指针,初始化为null

(2) 先把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

(3) 接下来就要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

(4) 接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针

(5) 最后,当cur 指针 == null 时,循环结束,链表也反转完毕了。此时return pre指针就可以了,pre指针就指向了新的头结点

6. 删除链表的倒数第N个节点(虚拟头节点

结合虚拟头结点 双指针法

给一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

双指针(定义fast指针和slow指针,初始值为虚拟头结点):如果要删除倒数第n节点,先让fast移动n,然后再让fastslow同时移动直到fast指向链表末尾。此时slow所指向的节点就是需要删掉的节点

(1)定义fast和slow指针,初始为虚拟头结点

(2)fast首先走n + 1步,这样同时移动slow和fast的时候, slow才能指向删除节点的上一个节点(方便做删除操作, 让他指向删除节点的下一个节点)

(3)fast和slow同时移动,直到fast指向末尾

(4)删除slow指向的下一个节点

7. 链表相交(使用双指针来找到两个链表的交点(引用完全相同,即:内存地址完全相同的交点)

给两个单链表的头节点 headA 和 headB ,请找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

思路:就是求两个链表交点节点的指针。 这里要注意,交点不是数值相等,而是指针相等

(1)先求出两个链表的长度,并求出两个链表长度的差值,

(2)然后让curA移动到和curB 末尾对齐的位置

(3)当curA不为空时,比较curA和curB是否相同,如果不相同同时向后移动curAcurB,如果遇到curA == curB,则找到交点curA。否则循环退出返回空指针。

8. 环形链表II

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。不允许修改给定的链表。

主要考察两知识点:(1)判断链表是否环(2)如果有环,如何找到这个环的入口

判断链表是否有环:可以使用快慢指针法,分别定义 fast slow 指针,从头结点出发,fast指针每次移动两个节点slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环

如果有环,如何找到这个环的入口:假设从头结点到环形入口节点的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点的节点数为y。 从相遇节点再到环形入口节点节点数为 z。

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n才遇到slow指针,(y + z)为一圈内的节点个数A。

最终得到数学关系:x = (n - 1) (y + z) + z 其实与 n 没多大关系了,无非是多走了n 圈

做法:从头结点(index2)出发一个指针,从相遇节点(index1也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。让index1index2同时移动,每次移动一个节点,那么他们相遇的地方就是 环形入口的节点

9. 三数之和

给一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请找出所有满足条件且不重复的三元组。答案中不可以包含重复的三元组。

(1) 首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。a = nums[i],b = nums[left],c = nums[right]。

(2) 如果nums[i] + nums[left] + nums[right] > 0 说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。

(3) 如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

关键是去重:

a去重:要求的是:不能有重复的三元组,但三元组内的元素是可以重复的要做的是 不能有重复的三元组,但三元组内的元素是可以重复的。

if (i > 0 && nums[i] == nums[i - 1]) { continue;} 当前的 nums[i],判断前一位是不是一样的元素,再看 {-1, -1 ,2},当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里

10. 四数之和

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d == target?找出所有满足条件且不重复的四元组。

在三数之和的基础上再套一层for循环。

差别:三数之和 剪枝可以通过 nums[i] > 0 就返回了,因为 0 已经是确定的数了,四数之和中 target是任意值。剪枝逻辑: nums[i] > target && (nums[i] >=0 || target >= 0)

四数之和的双指针解法: 两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况

2024.05.23

1. 反转字符串II

给定一个字符串 s 和一个整数 k,从字符串开头算起, (1)每计数 2k 字符,就反转这 2k 个字符中的前 k 个字符

(2)如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

(3)如果剩余字符少于 k 个,则将剩余字符全部反转。

输入: s = "abcdefg", k = 2  输出: "bacdfeg"

思路:遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k,然后判断是否需要有反转的区间。

2. 右旋字符串

字符串的右旋转操作(先整体再局部)是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。

输入:输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。输出:输出共一行,为进行了右旋转操作后的字符串。

思路:使用整体反转+局部反转实现反转单词顺序,通过整体倒叙,把两段子串顺序颠倒,两个段子串里的字符再倒叙一次。

(1)右移n位,就是将第二段放在前面,第一段放在后面(2)两个段子串里的的字符再倒叙一次。

(1) 右旋子符串(先整体反转 再局部反转)(2) 左旋子符串(先局部翻转 再整体反转)

3. 实现 strStr() (KMP算法 再看下吧  这个 挺不好理解的

KMP的经典思想就是: 当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容利用这些信息避免从头再去做匹配。关键是前缀表的实现

实现 strStr() 函数:给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。当 needle 是空字符串时,应当返回0.

4. 重复的子字符串

给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。同样是 KMP算法 

next[len - 1] !=-1 && len % (len – next[len - 1] - 1) == 0

2024.05.25

复习 哈希表

哈希表(Hash table)是根据关键码的值直接进行访问的数据结构。直白来讲其实数组就是一张哈希表。关键码就是数组的索引下标,然后通过下标直接访问数组中的元素.

哈希表一般都是用来快速判断一个元素是否出现集合里

哈希函数是把传入的key映射到符号表的索引上

哈希碰撞处理有多个key映射到相同索引上时的情景,处理碰撞的普遍方式是拉链法线性探测法

常见的三种哈希结构: 数组,  set (集合),  map(映射)

std::unordered_set底层实现为哈希表,std::set std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加

std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。

1. 有效的字母异位词(使用数组来做哈希的题目,题目中限制了数值的大小。)

给定两个字符串 s 和 t ,判断 t 是否是 s 的字母异位词。

思路:

(1)定义一个数组record用来记录字符串s里字符出现的次数。需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25

(2)在遍历字符串s时候,只需要将 s[i] - a 所在的元素做+1 操作即可,不需要记住字符a的ASCII,只要求出一个相对数值就可以了,统计字符串s中字符出现的次数

(3)在遍历字符串t时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。

(4)最后检查一下,record数组如果有的元素不为零0,说明字符串st一定是谁多了字符或者谁少了字符,return false,如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。

2. 两个数组的交集(unordered_set)

3. 快乐数

定义:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和sum,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为  1,那么这个数就是快乐数。如果 n 是快乐数就返回 True ;不是,则返回 False 。

求和的过程中,sum会重复出现,使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum1为止。判断sum是否重复出现就可以使用unordered_set。

4. 两数之和

给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍. map中的存储结构为 {key:数据元素,value:数组元素对应的下标}

在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中, map存放的是访问过的元素。

5. 四数相加II

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。

题目中的是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况。

1)思路:首先定义 一个unordered_mapkeyab两数之和value ab两数之和出现的次数

2)遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。

3)定义int变量count,用来统计 a+b+c+d = 0 出现的次数。

4)在遍历大C和大D数组,找到如果 0-(c+d) map中出现过的话,就用countmapkey对应的value也就是出现次数统计出来。

5)最后返回统计值 count

6. 赎金信

给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。)

本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成

1)暴力解法                            2)哈希数组(只有小写字母组成

2024.05.27

复习链表

链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。链表的入口节点称为链表的头结点也就是head。链表在内存中可不是连续分布的,是通过指针域的指针链接在内存中各个节点,散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

1. 移除链表元素

删除链表中等于给定值 val 的所有节点。

两种方式:

1)直接在原链表上进行删除(很麻烦,分为移除头结点和移除其他节点)

2)设置一个虚拟头结点在进行删除(更简单)

2. 反转链表                               

3. 两两交换链表中的节点

给定一个链表,两两交换其中相邻的节点,并返回交  换后的链表。(使用虚拟头结点)

4. 设计链表(这类题一般都是经常考的)

在链表类中实现这些功能:

get(index):获取链表中第  index  个节点的值。如果索引无效,则返回-1

addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。

addAtTail(val):将值为 val 的节点追加到链表的最后一个元素

addAtIndex(index, val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。

deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

   

链表的种类主要为:单链表,双链表,循环链表

链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。

链表是如何进行增删改查的。

数组和链表在不同场景下的性能分析。

2024.0528

复习数组

数组在内存中的存储方: 数组是存放在连续内存空间上的相同类型数据的集合。数组可以方便的通过下标索引的方式获取到下标下对应的数据

(1)数组下标都是从0开始的。  (2)数组内存空间的地址是连续的

正是因为数组的在内存空间的地址是连续的,所以在删除或者增添元素的时候,就要移动其他元素的地址。数组的元素是不能直接删除的,只能覆盖。

1. 二分查找

给定一个  n  个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

区间的定义就是不变量。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则。

2. 有序数组的平方

非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。(有负数)

双指针思路:数组是有序的, 只不过负数平方之后可能成为最大数。那么数组平方的最大值一定在数组的两端,不可能是中间。双指针法,i指向起始位置j指向终止位置。定义一个新数组result,和A数组一样的大小,k指向result数组终止位置。如果A[i] * A[i] < A[j] * A[j] 那么result[k--] = A[j] * A[j]; 。如果A[i] * A[i] >= A[j] * A[j] 那么result[k--] = A[i] * A[i];

3. 长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其 s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0

滑动窗口(双指针法的一种),就是根据当前子序列和大小的情况,不断的调节子序列的起始位置和终止位置,从而得出结果。

4. 螺旋矩阵II

给定一个正整数 n,生成一个包含 1 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。

模拟顺时针画矩阵的过程:

填充上行从左到右 (变化的是Y

填充右列从上到下(变化的是X

填充下行从右到左 (倒序)

填充左列从下到上(倒序)

然后在更新 起始点和 偏移(由外向内 一圈一圈画下去 :圈数 = n / 2

最后别忘了判断 n 能否被 2 整除 不能的话,要单独处理最 中间的数值

注意 坐标轴 Y轴朝右    X轴朝下

还有二维数组的写法

  • 12
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值