+ 不要纠结没有思路就直接看题解;
+ 不要死磕觉得自己很失败,怎么我们就想不出来;
+ 基本上这些算法题,让我们自己想出来是不可能的;
+ 拿跳表的来说,如果我们能从0-1把它想出来,那我们就可以拿到图灵奖了;
+ 所以记住!无思路就直接看题解,无思路就直接看题解,无思路就直接看题解!
+ 我们只需要知道并且能运用即可!
+ 自己开始写代码,没有,就马上看题解!
+ 做完题目后,我们需要记住这种题的思路和有N种解决办法;
+ 重复再重复的默写,直到自己有深刻的影响;
+ 到了这里如果我们还需要看别人代码,那就要回去背题;
+ 能到达这个阶段基本这种题你已经开始熟悉的,接下来就是反复练习;
那肯定是力扣了!没有账号的小伙伴,马上就去注册个账号开始日复一日的练习吧!~
283. 移动零|难度:简单
给定一个数组 nums
,编写一个函数将所有 0
移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
这里需要注意的重点:
0
移动到数组的末尾;思考题解时,使用MECE原则 — 每一个思路都相对独立的思维,然后想到完全穷尽。首先不要管附加条件,先把有可能解决这个问题的思路都想出来,再评估哪一个办法是最优解。面试的时候也是一样,说出你所有可以想到的思路,然后分别讲出各自的优点与缺点,最后提出最优答案。
+ 循环数组找到0的位置,遇到0就为0的个数加一;
+ 遇到不是0的时候,把非0的元素值与0的元素交换即可;
+ 给一个指针i
从数组的头部开始递增;
+ 给一个指针j
从数组的尾部开始递减(也就是原数组的总长度);
+ 遇到零就往j
指针的位置放,然后j--
;
+ 遇到非零就往i
指针的位置放,然后i++
;
+ 缺点:内存使用会高;
+ 不符合条件:必须在原数组上操作,所以可以实现但是不符合条件;
+ 给两个指针i
和j
,并且默认都从0开始;
+ i
指向的是当前位置;
+ j
指针会一直移动,直到找到一个非零元素,然后与i
位置的值交换;
+ 如果j
的位置与i
不是一致的话,就可以给j
的值换成0;
+ 这个与第三种方法一致,也是双指针;
+ 唯一的区别是不在i
指针扫描的时候替换零;
+ 而是在替换完毕所有非零元素后,把剩余的全部位数都改为0;
「方法一」 - 统计0的个数:
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
let zeroCount = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] == 0) {
zeroCount += 1;
} else if (zeroCount > 0) {
nums[i - zeroCount] = nums[i];
nums[i] = 0;
}
}
};
「方法二」 - 双指针交换:
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
let j = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== 0) {
nums[j] = nums[i];
if (j !== i) {
nums[i] = 0;
}
j++;
}
}
};
「方法三」 - 双指针替换后清零:
n减非零的个数
,那就是O(n+n-i)
,总的来说还是O(n)
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
var j = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] != 0) {
nums[j] = nums[i];
j++;
}
}
for (let k = j; k < nums.length; k++) {
nums[k] = 0;
}
};
[0,1,0,3,12] [1,2] [0,0]
注意:以下数据都是在力扣中提交后返回的结果,每次提交都有可能不一致。所以相近的方案输出的结果有所差异也是正常的,最终最优方案要通过分析代码来确定,*不能只以力扣输出的数据为准,只能供于我们作为参考*。
分析一下:
i - zeroCount
的运算,所以相对其他方法来说运行时间会更长一些;283. 盛最多水的容器|难度:<font color="orange">中等</font>
给你 n 个非负整数 a1,a2,...,an,每个数代表坐标中的一个点 (i, ai) 。在坐标内画 n 条垂直线,垂直线 i 的两个端点分别为 (i, ai) 和 (i, 0)。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且 n 的值至少为 2。
图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
示例:
输入:[1,8,6,2,5,4,8,3,7] 输出:49
题目重点:
高度 x 宽度 = 面积
+ 遍历左边和右边,找出所有面积;
+ 列出所有柱子的组合;
+ 算出所有组合各自的面积;
+ 最后输出最大的面积的一组;
+ 缺点:遍历次数过高,所以时间复杂度会相对偏高
+ 复杂度:时间复杂度 $O(n^2)$、空间复杂度 $O(1)$
+ 左右两边都往中间移动;
+ 需要移动左右两头的问题都可以考虑双指针;
+ 相同情况下两遍距离越远越好;
+ 区域受限于较短边的高度;
+ 所以让较矮的那边的指针往内移动;
+ 一直以上面的规则移动知道两个指针重合;
「方法一」 - 枚举(暴力破解):
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
let max = 0
for (let i = 0; i < height.length - 1; i++) {
for (let j = i + 1; j < height.length; j++) {
let area = (j - i) * Math.min(height[i], height[j])
max = Math.max(max, area)
}
}
return max
};
「方法二」 - 双指针:
/**
* @param {number[]} height
* @return {number}
*/
var maxArea = function(height) {
let max = 0
for (let i = 0, j = height.length - 1; i < j; ) {
let minHeight = height[i] < height[j] ? height[i ++] : height[j --]
let area = (j - i + 1) * minHeight
max = Math.max(max, area)
}
return max
};
分析一下
283. 移动零|难度:简单
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。 1. 1 阶 + 1 阶 2. 2 阶
示例 2:
输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。 1. 1 阶 + 1 阶 + 1 阶 2. 1 阶 + 2 阶 3. 2 阶 + 1 阶
其实题目本身并不难,在力扣(LeetCode)是属于“简单”级别的题目,但是如果没有思路,或者对这个题目完全不了解的话,一点头绪都没有也是正常的,这种题目也就是属于套路题。如果我们是不知道的话,我们自然会难到不知道怎么做。我们要是知道了的话,那就变得相当容易了。
这里讲一下解题的思想:
首先我们解题时最大的误区是什么? - 做题只做了一遍 - 至少要做五遍 然后我们优化的思想是什么? + 空间换时间 + 升维思想(升级到二维) 看题时懵了怎么办? + 首先我们能不能暴力破解? + 最基本的情况我们应该怎么解决?能否化繁为简? 破解所有问题的法则: 找最近重复的子问题 + 为什么?因为写程序我们只能写
if
,else
,for
,while
,recursion
(递归) + 计算机是人类发明的,计算机肯定是没有人脑那么强的,它其实就是一个简单的重复式机器 + 那么计算机运行的程序也是同理,它是用重复的东西来解决问题的 + 如果我们遇到算法题的时候,就是需要我们用程序去解决的问题,那问题的本身就是可重复的 + 无论是算法中的回述、分治、动态规划、递归等,全部都是在找重复性的原理 + 所以重点都是“找规律”
首先我们使用化繁为简的思维来分析:
要到达第一个台阶,我们只能爬1个台阶,所以只有一种方法的可能性,所以 n = 1 的时候,只有1种可能。
那如果我们要到达第二个台阶,我们要不就是连续爬2次1个跨度,要不就是一次性爬两个台阶到达第二个台阶。所以有2种可能性。
那如果是需要到达第三个台阶呢?
这里有个小技巧,要到达第三个台阶我们可以换一种思维去想,如果我们还是像第一个和第二个台阶的方式去列出可以到达第三个台阶的所有可能性,那如果
n
很大的时候,我们只靠人的大脑去想,那真的是太费劲了。但是这里有一个很巧妙的思维方式。 返过来想,我们想到达第三个台阶,只有两种可能可以到达: 1. 要不就是从第二个台阶爬1个台阶到达 2. 要不就是从第一个台阶爬2个台阶到达 那其实如果是第四个台阶是不是也是一样的? 1. 要不就是从第三个台阶爬1个台阶到达 2. 要不就是从第二个台阶爬2个台阶到达 这里就有一个规律
了。要到达第n
个台阶我们需要知道: 1. 到达第n-1
的台阶有多少种可能 2. 到达第n-2
的台阶有多少种可能 3. 然后这两个相加就是到达第n
的台阶有多少种可能
那其实这里就是老生常谈的斐波拉次
数列:
f(n) = f(n-1) + f(n-2)
+ 直接使用递归循环使用斐波拉次公式即可
+ 但是时间复杂度就很高 - $O(2^n)$
+ 用上面讲到的原理,到达第n
个台阶只需要:爬上 n-1 台阶的方式数 + 爬上 n - 2 台阶的方法数 = 爬上第 n 个台阶的方式数
+ 所以得出的公式是 dp[n] = dp[n-1] + dp[n-2]
+ 同时需要初始化: dp[0]=1 和 dp[1] = 1
+ 使用这种方式时间复杂度降到 $O(n)$
+ 与上面的动态规划的方法一样,但是这里我们只记录最后3个的台阶的爬楼方法数
+ 使用f1
,f2
,f3
作为储存变量
+ 默认 f1 = 1 和 f2 = 2 即可
+ 有观察数学规律的同学,或者数学比较好的同学,会发现本题是斐波那次数列,那么我们也可以用斐波那次的“通项公式”
+ 时间复杂度:O(logn)
「方法一」斐波那次
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
if (n <= 2) return n
return climbStairs(n-1) + climbStairs(n-2)
};
「方法二」动态规划
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
const dp = [];
dp[0] = 1;
dp[1] = 1;
for(let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
「方法三」动态规划2
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function (n) {
if (n <= 2) {
return n
}
let f1 = 1, f2 = 2, f3
for (let i = 3; i <= n; i++) {
f3 = f1 + f2
f1 = f2
f2 = f3
}
return f3
};
「方法四」通项公式
/**
* @param {number} n
* @return {number}
*/
var climbStairs = function(n) {
const sqrt_5 = Math.sqrt(5);
const fib_n = Math.pow((1 + sqrt_5) / 2, n + 1) - Math.pow((1 - sqrt_5) / 2,n + 1);
return Math.round(fib_n / sqrt_5);
};
分析一下
领取专属 10元无门槛券
私享最新 技术干货