Array
*532. K-diff Pairs in an Array
方案一:暴力搜索, N平方的时间复杂度,空间复杂度N
数组长度为10000,使用O(N平方)的解法担心TLE,不建议使用,尽管最终验证还是可以过.
方案二:哈希 时间复杂度N 空间复杂度N
*1.two sum
思路:
使用hash, 顺序遍历数组,并记录每一个出现过的元素值和其下标,当遍历到当前元素时,查找target-当前元素值 是否出现在map中,存在则返回结果
*16. 3Sum Closest
思路一:暴力 for循环 时间复杂度n3次方,空间复杂度o(1) 需要优化
思路二:首先借鉴了排序的方法,然后枚举出包含第i个值的最优解,这里利用了求两个元素之和与target最接近的方法
利用数据的有序,假设i和j 如果i,j之和大于target,则j--, 如果i,j之和小于target,则i++, 利用有序对数据逼近,使得
求2个元素的closet变为o(n),总的时间复杂度变为o(n^2)
思路三:有没有更优的方法?
18. 4Sum
思路一:4重循环 时间复杂度n的4次方; 空间复杂度o(1)
思路二:将4sum转换为3sum,3sum转换为2sum,2sum可以O(n)求出, 空间复杂度O(N)
3sum 时间复杂度 O(n^2) 空间复杂度O(N^2)
4sum 时间复杂度O(N^3) 空间复杂度O(N^3) 最终TLE, 可以经过剪枝加速,可以得到结果
如何剪枝?
我们可以利用排序后,
当前元素和其后面的三个元素之和,如果大于target,则表示可以退出循环了
当前元素和数组最大的三个元素之和,如果小于target,则表示当前值不会出现在结果里,继续往后找
这样从4sum 退化到 3sum 再到 2sum, 这里还需要注意 判断重复的情况,因为利用了排序,所以判断重复比较容易
只需要考虑当前元素和前一个元素比较,是否值相等,若相等则直接跳过,
在很多题目中,利用有序可以帮助我们做重复,所以看到需要输出不重复的解时,可以考虑下是否要对数据进行预排序
k-sum的解法,同样与此类似, k-sum的剪枝,我们可以利用 最大元素和最小元素,
比如当前元素 + k-1 最小元素 > target, 则说明不需要再往下走了,后面的都比较大,不可能拿到解
比如当前元素 + k-1 * 最大元素 < target, 则说明取到最大也没有解,需要继续取下一位,当前位可以跳过了
所以说,当原始数据量超过1000的时候,如果想让高复杂度的算法通过,那么必须要考虑剪枝,否则肯定时间复杂度是过不了的
思路三:求出两两元素之和,并保存到map中
然后对map中的可能值进行合并,去重复后,输出
*798. Smallest Rotation with Highest Score
中等难度
题目数据量是20000,所以我们知道n平方的算法肯定挂,但是我们分析思路时,还是先考虑下如果用n平方的解法,怎么做?
思路一:题目说的是数组元素移动,我这里考虑是移动下标,即移动下标0,这样我们可以把数组分成两部分 k,k+1,... , n -1 ,0, 1 ,2, ..., k - 1
分法有n种,每种分法斗需要计算k到n-1 和 0到k-1 的points,最后相加,即为当前分发的结果
所以该算法时间复杂度n平方,空间复杂度1
思路二:另外一种思路,也是n平方的思路,只是维护的是每一步move的结果值,最后逐个遍历每一步的move值,取最大的即可
这里的思路是,假设a[i]<=i 那么当前得分是1,移动i-a[i]次时,得分都是1,所以我们更新k=0, 1, ..., i-a[i] 都加1,表示a[i]这个元素在这些移动里,都贡献了1
当k=i+1时,a[i]被移动到最后,我们知道从i+1 到n次移动,a[i]的得分都是1,所以对于这些移动,a[i]也是贡献了1,所以要更新这些值
当a[i]>i+1时,只有k=i+1的时候,points才为1,直到n + I - a[I], 所以也需要更新这些值, 最后得出数组
思路三:思路二记录了每次移动后 n次移动的值,如果能只记录当前移动相对于前一次的变化值,则经过n次遍历,我们应该就可以得到结果
意思是,我们算一个初始值,如果能记录到每次移动的变动,那么很容易可以得到每一步的结果值,而不是像思路二,每次都要更新n个值
所以我们要关注变化点,我们发现a[i]<=i时,第0次移动到第i-a[i]次移动之间,积分都是1,不会少,相当于变化为0,第i-a[i]+1到第i+1次之间,积分为0, 第i+1次开始,积分变为1,所以有两个地方发生了变化,因为我们考虑的是相邻两次的变动值,所以其实只需要关注i - a[i] + 1 和 i + 1 这两次移动
I -a[I] + 1次移动后, a[i]贡献了一次变化,值相对于上一次移动少了1, 第i+1次移动后,a[i]贡献了一次变化,值相对于上一次多了1
所以我们分别把这两次变更,记录到变化数组种对应的移动次数值上,这样通过遍历一遍数组元素,我们知道了每一步移动时的变化情况
依次累加这些变动值,即可
*55. Jump Game
思路一:dfs 在用java实现后,抱递归深度太大 stack overflow了
思路二:dfs + memory 同思路一
思路三:计算每个点能到达的最远距离,不断更新最远距离,看是否可以遇到一个点,可以到达最后的索引值
*45. Jump Game II
思路: 可以把输入数据分层,第一层是第一个数,第二层是第一个数可以到达的数,第三层是第二层数可以到达的数,
输出第几层有数可以到达数组末尾即可. 所以需要变量记录当前访问的元素是第几层.
同时需要记录当前层的最后一个下标,当i到达当前层最后一个下标时,更新层数
另外需要在遍历每一个当前层元素时,计算下是否需要更新下一层的最后一个下标.
*4. Median of Two Sorted Arrays
思路:这个题需要转换为求有序数组中的第kth个元素
当数组m + n之和为偶数时,即为求第(m+n)/2个和第(m+n)/2 + 1个元素的平均值
当数组m + n之和为奇数是,即为求第(m+n)/2+1个元素的值
如何求第kth个数,这里是每次先找出前k个元素来,首先确定数组元素少的为a1, 元素个数多的为a2
首先比较k/2 和 a1的长度,从a1取两者的小值个数,从另一个数组中取剩下的数(k减去小值)
然后比较 这两部分数的最大值,如果相等,则返回相等的数,因为它们是前k个小数,相等的正好是第k个
如果a1 的小于a2的,则a1的前半部分可以去掉,a2的后半部分可以去掉,因为两者都不可能有第k个元素
所以原数据规模减小了,原问题变为从剩余数组中取 第k - a1前半部分 或者 第k - a2前半部分
显然又是递归, 这里需要考虑递归的中止条件,当数组为空时,即右边界小于左边届 或者
k == 1时,分别返回第k个元素 即可
*11. Container With Most Water
思路:从最外层开始扫描,比较两端元素决定下一次扫描的方向
*26. Remove Duplicates from Sorted Array
思路一: 因为要原地,想使用交换最后一个元素,但是这样交换,顺序乱了,所以还是采用了暴力移动的方式
这里需要几个下标,用来记录重复元素的区间,找到整个区间后,整段覆盖掉,比一个一个找出来移动次数少很多
这是连续区间,进行去重复的优点.
思路二:思路一太死板了,用覆盖的方法更简单,一边扫描数组,一边挑选出符合条件的元素,依次追加到新数组后面
例如 prev=0 扫描第一个不等于prev的元素,把该元素赋值到prev + 1位置,prev=prev+1,同时新数组个数+1,
写法比思路一简单,执行速度也快, 记住覆盖写比移动元素好.
*31. Next Permutation
思路:如果求下一个升序序列,则只要找到当前的降序拐点,即当前值大于前一个值的点,(降序点)然后从尾部升序序列中找到第一个比拐点元素大的,交换
然后再升序排列序列,即得到下一个升序序列
如果求下一个降序序列,则需要先找到当前的升序拐点,即当前值小于前一个值的点(升序点),然后从尾部降序序列中找到第一个小于拐点的值,交换
然后再降序排序序列,即得到下一个降序序列
*33. Search in Rotated Sorted Array
思路一:可以使用map,空间复杂度o(n) 时间复杂度o(n) 简单容易理解,但是不够快
思路二:利用二分查找,这里有两种想法
第一种,利用mid值 根 最后一个数组元素比较,可以确认mid在上半部 还是下半部
再根据target值,判断target在上半部还是下半部, 这样总共分4种情况
每一种情况下,都是首先判断下target 和mid 值是否相等,如果不等,再判断两者的大小关系,
都在上半部时,如果mid 大于 target,则只需要在0-mid-1之间找,如果mid<target, 则在mid+1-n-1之间找
都在下半部时,如果mid 大于 target,则只需要在0-mid-1之间找,如果mid<target, 则在mid+1-n-1之间找
mid在上target在下,则从mid+1-n-1之间找
mid在下target在上,则从0-mid-1之间找
总结:当使用一个变量无法解决问题时,可以再引入一层变量,把问题分割的更细之后,可能问题就会迎刃而解
第二种思路:先找到分界点,利用二分查找,
如果mid 大于最后一个元素,则需要在mid - n-1之间找分界点
如果mid 小于等于最后一个元素,则需要在0-mid之间找分界点
这里使用二分查找的通用模版代码,写起来会非常容易
找到分界点后,跟思路一类似,判断target和 n-1的大小,如果target<n-1, 则二分查找分界点-n-1区间
如果target>=n-1, 则二分查找0-分界点区间
*34. Search for a Range
思路:题目要求复杂度logN, 且数组有序,显然需要使用二分查找,这个题目还是求上届和下届,只是判断结果时,需要考虑边界条件
首先使用二分查找通用模版
其次判断上届时,left 和right中必有一个是上届,这里我们判断left,如果left大于target,则看下left前一个数,如果left小于等于,则看下
right是否大于target,如果大于,则看下left是否等于target,不等于,则再看下right是否等于target
说白了,判断逻辑跟求上界相同.
*39. Combination Sum
思路:
dfs 把问题拆解为 求以第一个元素开始的解,以第二个元素开始的解,。。。以第n个元素开始的解
所以最外层直接实现为 for(int i = 0; i < n; i++) { dfs(); }
对于内部的dfs, 边界条件需要判断,除此之外,由于当前元素可以重复使用,所以元素下标不需要移动,
继续求解新的target,依然是把该问题分解为 求以第一个元素开始的,以第二个元素开始的。。。 以第n个元素开始的。。。
把符合条件的结果,输出到path中,将path的内容赋值给ans, 最后返回ans
*40. Combination Sum II
思路:dfs 与39题相同的解法,这里的问题是由于需要去重复,我们这里数据没有规律,所以需要借助排序,同时题目要求解的唯一性,
所以我们需要保证每一轮的元素,和上一轮的不相同, 这里只需要定义一个prev变量,每次for循环里,比较下当前值和prev是否相同,
注意每次进入新的for时,重新初始化prev,因为不同层级的值重复没问题,这里不希望重复的同层级的元素
*42. Trapping Rain Water
思路:采用两头扫描的方法,需要求出各个部分可以积的水量之和,求出每部分的左侧边界和右侧边界,取二者的最小值,减去当前部分高度
即为可以存储的水的容量,为负数时直接忽略
*48. Rotate Image
思路:假设矩形abcd, 旋转90度后变为 dabc, 如何做到? 可以分两步,第一步,上下翻转,变为 dcba, 第二步朝右上角翻转,变为dabc
上下翻转,只需要记录up和bottom row即可,右上翻转只需要遍历右上角的元素,将a[i][j] 与 a[j][i]互换下即可
*53. Maximum Subarray
思路:经典的一维dp, 时间复杂度o(n) 空间复杂度o(n)
*54. Spiral Matrix
思路:暴力 直接遍历 时间复杂度o(n) 空间复杂度o(n)
*56. Merge Intervals
思路: 不排序,需要n平方时间复杂度, 排序后只需要nlogn,按顺序合并即可
*57. Insert Interval
思路: 顺序遍历,如果区间在新区间左边,直接放入结果中,如果在右边,则需要先把新区间放入结果中,再把右边区间依次放入结果,注意考虑边界情况,如为空数组时.
62. Unique Paths
思路一: dfs 时间复杂度 空间复杂度
思路二: 动态规划, 空间复杂度可以从n平方优化到2n
*75. Sort Colors
思路一:因为题目希望分割成三份,左边0,中间1,右边2,我们可以定义中间部分的起点和终点
oneStart和oneEnd,然后i从onestart取到one end, 为什么不是取到n-1,因为oneend之后的都是2, 那我们没有必要继续取了
所以for(int I = oneStart; I <= oneEnd; I++)
如果遇到0,则当前元素和onestart互换,然后onestart自增1,如果遇到的是2,我们和oneend互换,然后因为我们不知道换过来的是0 还是 1,
所以下标需要维持原状,这里用了i--, 这个思路需要注意的就是 i搜索到oneEnd就可以停止了
思路二:这个思路比较巧妙,而且通用,用i j k分别表示 0 1 2 当前的结尾下标
初始化为-1,如果遇到了2,则需要扩展2的下标,如果遇到了1,则需要先扩展2,再扩展1, 如果遇到了0,则需要先扩展2,再扩展1,再扩展0
非常好的想法,很通用
78. Subsets
思路一:常规解法dfs 时间复杂度 2的n次幂,每个元素有取或不取两种,我们将问题拆解为 以第一个元素开头 以第二个元素开头。。。 以第n个元素开头
n个子问题,每个子问题下,又是重复同样的问题
思路二:更简单的解法是,每加入一个元素,相当于拓展了原来解的结果,将原先每一个解中添加新加入的元素,构成新解
时间复杂度也是2的n次幂,for(int i = 0; i < noms.size(); I++) int k = result.size() for(int j = 0; j < k; j++)
思路三:更巧妙的方法,但是时间复杂度较高,需要n 2的n次幂,利用bit,我们知道结果有2的n次幂,取某一位代表将该位设置为1,不取,则表示将
该位设置为0,这种方法的好处是,不需要额外的存储空间,没有复制操作,缺点是,每次都要做n次判断,所以比较慢
*79. Word Search
思路一:开始时尝试使用bfs,发现题目中有向回指的问题,导致visited没办法标记,所以改用dfs
这里如果不希望使用visited数组,则可以使用数组中未出现的元素,先将原有的元素覆盖掉,回溯的时候,再使用原有的值更新下,即可,这是一个优化点。
总结:bfs不适合使用的场景是,图中存在往回指向的情形,因为bfs是一层一层的,上一层被标记过后,下一层如果再指向上一层,会导致搜索终止,因为上一层已经
被标记过了,走不下去,所以需要理解,bfs是扩散的,经过的点,是不允许再被访问回来的。比如,求最短路径时,我们知道如果已经经过了一个点,从这个点出发到
终点的长度,一定比从其它点出发后,再指回这个点的距离短,所以指回的方案肯定不可取,所以这种才是bfs适合使用的场景。
*80. Remove Duplicates from Sorted Array II
思路一:使用map,空间复杂度n,时间复杂度n
思路二:two point,空间复杂度1,时间复杂度2n,使用i表示当前访问的元素下标,使用j表示输出结果中当前的下标,注意这里比较的是当前下标i
和输出结果的前两个数 j-1 和 j-2, 所以针对去重复,这是一种生长型的算法,允许k个重复时也是同样的思路
*81. Search in Rotated Sorted Array II
思路一:使用map
*84. Largest Rectangle in Histogram
思路一:找到左侧第一个比当前元素小的,找到右侧第一个比当前元素小的,这样就可以确定以当前元素为高时的矩阵范围,
找到所有高度的矩阵,其中面积最大的即为解
从当前位置找左侧第一个小的,可以直接遍历,每一次需要n,n个元素,共n平方,对于hard题目来说,有可能超时
我们可以利用一个数组,通过记录每个元素的左侧第一个小的,将查找过程优化为小于n的,最坏情况还是n,比如降序的数组
*85. Maximal Rectangle
思路一:这个题可以使用84题的思路,84题已经给出了一个高度数组,我们这道题相当于是有n个高度数组,因为有n行,前i行可以组成一个
高度数组,我们就是在所有的高度数组中,求出矩阵面积最大的一组解,其实最大的矩阵,其最后一行肯定只有n个选择
如果为第i行,我们需要考虑第i行的高度数组是什么样的,如果matrix[i][j] == 0, 则高度为0,如果是1,则高度是上一层高度+1
由于单次最坏情况是n平方的,所以总的时间复杂度最差是n立方,空间复杂度n
思路二:可以进一步优化,因为上一步的解法时间复杂度最差要n cub,我们可以优化到n squre
我们求遇到的每一个元素,在当前高度下的最左边界和最右边界,思路跟第一种方法相同,区别是求左右边界
还是逐层计算高度,并逐层计算左边界和右边界, 左边界默认为-1,右边界默认为col
把所有遇到的矩阵的面积都算出来,从里面找值最大的
所以这里可以看出,预处理对优化算法复杂度的帮助.
*88. Merge Sorted Array
常规思路,easy题目
*118. Pascal's Triangle
思路: 这道题目注意下层元素依赖于上一层元素, dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] 知道这个规律后,就很好求了.
*119. Pascal's Triangle II
思路一:因为只需要输出一个数组的元素,所以可以优化成二维数组,滚动,最终输出结果.
思路二:二维数组dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j] 发现 i 只和 i-1有关,而j也只和 j -1,j有关,所以可以进一步优化为 原地更新.
而原地更新,非常重要的一点就是更新顺序, 由于j 是依赖 j 和 j - 1的, 所以求j的值时,j -1 必须是旧的,所以 j要先更新, j -1 后更新,
即需要倒着从 j = i 更新回j = 0
*120. Triangle
思路一:使用dfs,因为没个数无非取下一行当前列的值或者下一行当前列+1的两种,dfs的优点是空间复杂度比较低,但是这道题直接使用dfs会TLE
思路二:使用dp,通常需要求最大最小最长最短最值问题时,题目如果不允许打乱顺序,那一般都是要使用dp来解决. 这道题目通过使用dp,由第一层求出第二层,
再有第二层求出第三层,依次类推,可以求出第n层,从第n层中求出最小值.
由于题目希望使用n的空间复杂度,所以这里需要注意使用一维数组时的更新顺序,dp[i][j] = dp[i-1][j-1] + dp[i-1][j], 故i更新前要依赖I-1的旧值
*121. Best Time to Buy and Sell Stock
思路一: 我们在每一天都可以卖出,当天卖出时,想获得最大的收益,就只能用当天值 减去 截至到当天,最小的买入价, 这样通过遍历n个数,即可求出最优.
*122. Best Time to Buy and Sell Stock II
思路:这道题如果想通了从i,j 之间的交易,相当于从i开始每天都进行了交易,直到第j天, 即 profit=d[i + 1] - dp[i] + d[i + 2] - d[i + 1] + ... + d[N] - d[N - 1]
那么从i到j的交易,如果想要得到最大的收益,我们应该避免亏本的那些天的交易,所以问题就变为了,只在有收益的天数进行交易, 自然可以拿到最大收益.
*123. Best Time to Buy and Sell Stock III
思路一: 可以利用两端扫的方式解决,从左到右,找到左边各段的最大收益,从右往左扫描,找到右边各段的最大收益
之所以可以使用两端扫的方式,是因为 i 左边的最大收益 和 i 右边的最大收益, 恰好可以算作0 到 n-1 的一组最大收益值, 最大收益值一共就n种可能.
这里需要理解 i 左边或右边的收益,并不是一定要包含i的,如果一定要包含i,那么这种方法不可行,因为你没法保证左边取了i后,右边是否会取i,
而当i的左侧最优解可能包含i可能不包含i时, 才会覆盖所有的可能.
故 对该题目从左到右计算时,当前值为卖出点,我们需要找到当前值之前所有买入点的最低值,用当前值-最低买入值 同 目前最大收益值比较,可以得出左侧最大收益
读该题目从右到左计算时,当前值为买入点,我们需要在右侧找到最高的卖出点,使用最高卖出点-当前买入值 同目前最大收益值比较,可以得出右侧最大收益
最后 从0-n-1 计算以改点分割时,左右两侧最大收益的和,取其中最大的即为解.
思路二:动态规划
*128. 最长连续序列
思路一:使用unordered_set,从某一个数开始,寻找左边连续的右边连续的,最后加和,与最大值比较
*152. 乘积最大子序列
思路一:定义dp[i][j] 直接计算从i到j的最大乘积,时间复杂度n平方, 只需要求出第一行, dp[i][j] = dp[i - 1][j] / nums[i - 1]
相当于枚举了所有的可能 brute force
思路二:数组有一个标准解法,两头扫,我们可以通过在单一方向先建立递推关系,将问题拆解为左右两部分,再结合左右两部分可以得到解
本题,最优解可以分为n种,即包含nums[i]的解,所以我们只需要分别求出以nums[i]结尾的左半部分最大乘积和最小乘积,以及以nums[i]
开头的最大乘积和最小乘积,然后再左右相乘,除以nums[i], 即可得到包含nums[i]的最优解,时间复杂度为o(n) 空间复杂度为o(4n)
*153. 寻找旋转排序数组中的最小值
思路一:已知数组是排序的,故使用二分查找算法即可
*162. 寻找峰值
思路一:分四种情况, mid - 1 < mid < mid + 1 peek在mid + 1这侧
mid - 1 < mid > mid + 1 peek是mid
mid - 1 > mid < mid + 1 peek在两侧,随便取一个
mid - 1 > mid > mid + 1 peek在mid-1
*167. 两数之和 II - 输入有序数组
思路一:有序数组,头尾去重复
*169. 求众数
思路一:顺序扫描,记录当前元素,比较下一个元素,相同则计数+1,不同则计数-1,减1后如果为0,则重新设置当前元素为结果值
思路二:排序,输出n/2位置的值.
*189. 旋转数组
思路一:翻手定律,需找到倒数第k个数,k对数组长度取余,做三次翻转得到结果
需要注意数组下标 boundary checking
*209. Minimum Size Subarray Sum
题目要求分别使用o(n)和o(nlogn)
思路一:o(n) 规律是:如果前k个数之和小于s,那么这k个数,任意部分的和都小于s,利用这个性质,可以采用滑动的方式,在o(n)时间内解决该问题
因为该问题可以分解为n个子问题,即以nums[i]开头的最短子数组
int start = 0;
for (int I = 0; I < n; I++) {
sum += nums[I];
while (sum >= s) {//当遇到和大于等于s时,表示以start开头的最短子数组,该子数组之和大于等于s
ans = min(ans, i + 1 - start);//计算该长度
sum -= nums[start++];//利用前面提到的性质,start向右递增,表示以start+1开头的子数组之和
}
}
这里需要注意的是:边界情况,当ans没有结果时,要把ans转为0.
思路二:这里的规律是,虽然前n个数无序,但这前n个数的前缀和是有序的,有序就意味着可以使用二分
所以发现规律很重要,从无序到有序的转换!!!然后本题变成了求上界, 故nlogn时间内可解
*216. 组合总和 III
思路一:因为要输出所有的结果,所以需要遍历全部的节点,时间复杂度o(2^n), 空间复杂度 输出结果集合的大小 + path集合大小
这是一个典型的dfs,需要注意的是,我们采用的模版为先push,后level + 1, 初识时level = 0, 这样的好处是,简化判断,
当level == k时,path中正好有k个元素,另外,边界的判断要仔细
思路二:暴力枚举出所有可能,然后遍历这些组合,只输出和为sum,个数为k的,因为职能取1-9,共2的9次种可能,是可以直接枚举出所有可能的
ans=[] 先ans中push1,变为[] [1]
再push2 变为[] [1] [2] [1 2]
再push3 变为[] [1] [2] [1 2] [3] [1 3] [2 3] [1 2 3]
等等, java因为用了list,我在循环的时候,需要修改扩展list,所以这里引入了CopyOnWriteArrayList,可以支持并发读,且读写分离,不会抛出并发
修改的异常
*219. Contains Duplicate II
思路一: 两层循环遍历,时间复杂度o(nk)
思路二: 滑动窗口方法,维护一个k大小的set, 先为set插入k个元素,需要去重复,再从k+1个元素开始,插入一个,删除一个. 时间复杂度o(n) 空间复杂度o(k)
268. Missing Number
1)use array as a map, so flag each one appears in array to -1 value
2)sum 0 -n, sum elements of array, do subtract, result is and
*283. Move Zeroes
思路一: 找到非0的,按顺序放在数组头部,遍历完之后,剩余的位置填0
*287. Find the Duplicate Number
思路一:n平方,两层for循环
思路二: nlogn , 排序后遍历
思路三: 使用set,发现重复,空间复杂度o(n)
思路四: 把数组看作链表,下标为节点号,数值为next编号, 因为n+1个格子只有n个元素,假设每个元素指向的下一个元素不重复,当进行到第n + 1个格子时,需要指向的元素要么是自己,要么必然在之前出现过,
无论如何都会形成一个环. 可以使用快慢指针(类似链表找环,以及找起点的方法求解)
*289. Game of Life
思路一:利用额外的数组,存储状态,再输出
思路二:原地,对元数据元素重新编码,原先只有0和1,把原先的状态放在地位,新的状态放在高位,这样每个元素同时包含了新状态和旧状态,所以对原数据重新编码,也是一种很好的解决问题的思路.
*380. Insert Delete GetRandom O(1)
思路一:使用set
思路二:使用ArrayList保存数据, 使用HashMap, 以value为key,以数组下标为值,
当插入时,根据HashMap判断key是否存在,不存在,则插入到数组末尾,并且在hashmap中维护新插入的元素值和数组下标, 若已存在key,则返回false
当删除时,根据HashMap判断key是否存在,若不存在,则返回false,若存在则根据key在hashmap中,找到元素位置,将最后一个元素值复制给要删除的元素,更新最后一个元素在hashmap中的value,
并删除掉hashmap中的key.
*414. Third Maximum Number
思路一:使用三个变量,a b c维护大小关系, 最后输出. 注意边界条件的处理.
思路二:c++ 可以使用set,用到了set 的erase 以及set的 begin, rbegin, java中set处理没有c++灵活. 这要得益于c++的迭代器
*442. Find All Duplicates in an Array
思路一:使用数组下标标记,所以现在把数组下标,当成数组中的元素,如果下标对应的value为正数,表示出现了偶数次,为负数表示出现了奇数次
所以,可以遍历一遍数组,然后根据数组中,值对应的数组元素是正数还是负数,把偶数的元素搞出来,在使用set做下去重,最后输出array
思路二:思路比较巧妙,利用首次出现时,数组下标对应的元素应该为正数,出现第二次时,数组下标对应的元素应该为负数,所以一旦以数组中值为下标的元素为负数时,表示该值已经出现过一次
,现在是第二次出现,所以直接放入结果中即可. 省去了思路一中的统计和去重. 效率提高50%.
*448. 找到所有数组中消失的数字
思路一:跟442类似,原地更新,使用数组下标表示元素,下标对应的值代表元素是否出现过
思路二:使用位数组或者flag数组,使用原数组的值作为下标,有则赋值1, 无赋值0.
*560. Subarray Sum Equals K
思路一:先一重循环,把和为K的个数统计出来,再两重循环,长度大的减去长度小的,判断两者差是否为K,再计数
思路二:之前我们总结过,数组有几种先天的优势,a.下标天然可以当map的key b.数组元素可以当next c.数组从左到右,从右到左,是天然的先后关系
如果是有序数组,关系更多. 这道题目,首先一定是求长度大的减去长度小的,判断和是否为K. 我们可以利用数组元素出现的先后顺序, 使用map维护已经出现过的,然后用当前的sum - K,在map中找是否有满足条件的和个数
如果有,就计入到最终结果去,没有则继续加入下一个元素.这里有个技巧,是需要先在map中put(0) 进去,使得处理从0开始的sum和更方便.
*605. Can Place Flowers
思路一:将问题分解为三类,处于两个1之间的,如果0个数减去2之后,为奇数,则个数+1 / 2, 如果0个数为偶数, 则个数 /2.
左侧或右侧都为0,则0个数减去1之后,为奇数,则个数+1 / 2, 如果0个数为偶数, 则个数 /2.
没有1时,则直接为奇数,则个数+1 / 2, 如果0个数为偶数, 则个数 /2.
所以,从头开始遍历这个数组.
注意判断边界条件, 下标必须判断上下界.
思路二:第一种方法,直观,性能也还可以,但是写法比较复杂,需要注意数据的越界检查, 我们有另一种思路,很巧妙
贪心的想法,如果当前位置可以种,我们就种, 这种策略是最优的.
所以判断条件比较简单, 当前位置是否可以种,需要判断前一个位置和后一个位置,如果都可以,该位置赋值为1,然后判断下一个,我们知道下一个肯定不可以,所以直接跳到下下一个.
if (左边位置可种或者左边是起点 && 当前位置可以种 && 下一个位置可以种植) 可以种并且计数, 跳到下下一个位置
*611. Valid Triangle Number
思路一:问题规模只有1000,所以使用n立方勉强还可以接受, 首先需要排序数组,然后三重循环,两边之和大于第三边即可.
828. Unique Letter String
思路一:暴力枚举,时间复杂度太高
思路二:问题是求解所有子串中,unique的字符个数, 所有有唯一出现的字符的字串问题,正向思维是先求出所有子串,然后依次求出子串中的唯一字符个数
也就是思路一,如果逆向考虑下,我们不考虑子串,考虑每一个字符,唯一出现时,它出现在那些子串里,没出现在一个子串里,ans +1
问题转换为针对字符串中的每一个字符,它作为唯一字符可能出现在的字符串个数,
等价于求解,唯一包含字符i的子串个数,或者求字符串个数的组合,这类题目有一个通用的解法,就是针对数组中每一个位置,求出其上一次出现和下一次出现的位置,
那么两个位置中间的串就是唯一包含了 当前位置的字符的串的组合,分为两部分, i - prev[i] 表示以i结尾的所有子串,next[i] - i 表示所有以i开头的子串
这两部分可以组合到一起,当i重叠的部分,为单纯的以i结尾的子串,故仅包含i位置元素的子串个数为(i - prev[i]) (next[i] - i)
这个问题出现过很多次了,需要注意的是问题转换的思路,怎么转换问题到已知的解题思路上来。
*565. Array Nesting
思路一:使用标记数组,以及set,从0到n依次遍历,并将结果保存在set中,单次循环结束后,判断数组大小
思路二:我们知道题目中,一次循环的终止条件是 i == nums[j] 时,所以可以利用这一点进行标记所已访问过的元素
对于所有已访问过的元素,让其归为到终止条件,则下次遇到时,就不会重复计算了,很巧妙的思路。
contest85
1.d[I] = d[I - 1] + d[I - 2] + ... + d[I - k] can be optimized to sum[I] = dp[i] + sum[i - 1] if problem meet accumulate property
because dp[I] = sum[I - 1] - sum[I - k - 1] then we get formula sum[I] = (sum[I - 1] - sum[I - k - 1]) + sum[I - 1]
by this we, we can reduce time complexity from nk to n, we must understand sum[I] means sum of I elements.
so when i <= k, sum[I] = sum[I - 1] + (sum[I - 1] - 0)
when I > k, sum[I] = sum[I - 1] + (sum[I - 1] - sum[I - k - 1])
2.for problems that want you give a answer true or false, most of them can be solved by using dp. we can use sequence dp method.
create a array, each item can[I] means whether Ith element can meet the requirement.
for dp, there are mainly three types, matrix dp, sequence dp, two sequence dp.
for matrix dp,usually use f[I][j], for sequence dp, usually use f[I], for two sequence dp, usually use f[I][j]
3.when you encounter similar string problems, please think whether can solve it by using bfs, bfs, recursive.
4.when encounter hard problems, first thing you should do is cfindlassify which kind of problems it is. Classfy is very important for you to think solution.
array/list/map/set/binary search/sort/bfs/dfs/union find/trie/suffix trie/dp/fenwick tree/heap/tree
*find the duplicate number
when you need to find duplicate number in unordered array, we can go through the array.
we can use binary search, to search range mid, not value mid, to specify which number is duplicate.
the process is like binary search. But you should know, not every case are same, they usually a changed binary search. so you should take care,
don't ignore it. if you think you can solve it by binary search tree, just think about it deeply, just do it.