- 写于2019年5月29日
70. 爬楼梯
① 题目描述
- 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
- 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
- 注意:给定 n 是一个正整数。
- 示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
- 示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
② 递归求解(Time Limit Exceeded
)
- 用递归的思路想一下,要求 n 层的台阶的走法,由于一次走 1 或 2 个台阶,所以上到第 n 个台阶之前,一定是停留在第
n - 1
个台阶上,或者n - 2
个台阶上。所以如果用 f ( n ) 代表 n 个台阶的走法。那么,
f ( n ) = f ( n - 1) + f ( n - 2 )
f ( 1 ) = 1,f ( 2 ) = 2
- 发现个神奇的事情,这就是斐波那契数列(Fibonacci sequence)。
- 直接暴力一点,利用递归写出来。不过,竟然到37时,
Time Limit Exceeded
! - 时间复杂度:是一个树状图, O ( 2 n ) O(2^n) O(2n)。
- 代码如下:
public int climbStairs(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
return climbStairs(n - 2) + climbStairs(n - 1);
}
③ 递归的优化
- 以
f(4)
为例,我们需要分别求解f(3)
和f(2)
;求解f(3)
,需要分谢求解f(2)
和f(1)
,其实f(2)
早就已经求解过了。
- 优化方法就是把求出的解都存起来,后边求的时候直接使用,不用再进入递归了,叫做
memoization
技术。 - 这种技术,之前的递归方法的优化中,也使用了!
- 代码如下:
public int climbStairs(int n) {
HashMap<Integer, Integer> map = new HashMap<>();
return climbStairsN(n, map);
}
public int climbStairsN(int n, HashMap<Integer, Integer> map) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int f1 = 0;
if (map.containsKey(n - 1)) {
f1 = map.get(n - 1);
} else {
f1 = climbStairsN(n - 1, map);
map.put(n - 1, f1);
}
int f2 = 0;
if (map.containsKey(n - 2)) {
f2 = map.get(n - 2);
} else {
f2 = climbStairsN(n - 2, map);
map.put(n - 2, f2);
}
map.put(n, f1 + f2);
return f1 + f2;
}
④ 使用数组
- 新建数组
f[]
,用于存储斐波那契数列,初始化f[1] = 1, f[2] = 2;
,那么f[i] = f[i - 1] + f[i - 2]
。 - 虽然空间复杂度 O ( n ) O(n) O(n),但是时间复杂度也变为 O ( n ) O(n) O(n),所以运行时间为
0ms
. - 代码如下:
public int climbStairs(int n) {
if (n == 1) {
return 1;
}
if (n == 2) {
return 2;
}
int[] f = new int[n + 1];
f[1] = 1;
f[2] = 2;
for (int i = 3; i <= n; i++) {
f[i] = f[i - 1] + f[i - 2];
}
return f[n];
}
72. 编辑距离
② 题目描述
- 给定两个单词
word1
和word2
,计算出将word1
转换成word2
所使用的最少操作数 。 - 你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符 - 示例 1:
输入: word1 = “horse”, word2 = “ros”
输出: 3
解释:
horse -> rorse (将 'h’替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
- 示例 2:
输入: word1 = “intention”, word2 = “execution”
输出: 5
解释: intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)
② 动态规划
dp[i][j]
来表示字符串word1[ 0, i )
(word1 的第 0 到 第 i - 1个字符)和word2[ 0, j - 1)
的最小编辑距离。- 状态转移方程:
① 若word1[i -1] = word2[j - 1]
,dp[i][j] = dp[i - 1][j - 1]
,因为字符没有发生改变。
② 若word1[i -1] != word2[j - 1]
,dp[i][j] = Math.min(dp[i][j - 1], Math.min(dp[i - 1][j], dp[i - 1][j - 1])) + 1
,分别表示word1增加、删除、替换一个字符变成word2。 - 初始化:
①dp[0][0]=0
,表示二者长度都为0,最小编辑距离为0;
② 从i = 1
开始,dp[i][0] = i
,表示word1删除字符变成空的word2所需要的步骤;
③ 从j = 1
开始,dp[0][j] = j
,表示word1从空增加字符变成word2所需要的步骤。
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
// word1有非空变为空所需要的删除步骤
for (int i = 1; i <= m; i++) {
dp[i][0] = i;
}
// word1由空变成 word2所需要的添加的步骤
for (int j = 1; j <= n; j++) {
dp[0][j] = j;
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// word1的下标i-1.其实表示第i个字符,也就是我们当前要求解的字符
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
// min( add, delete, change)+1
dp[i][j] = Math.min(dp[i][j - 1], Math.min(dp[i - 1][j], dp[i - 1][j - 1])) + 1;
}
}
}
return dp[m][n];
}
75. 颜色分类
① 题目描述
- 给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
- 此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
- 注意: 不能使用代码库中的排序函数来解决这道题。
- 示例:
输入: [2,0,2,1,1,0]
输出: [0,0,1,1,2,2]
- 进阶:
一个直观的解决方案是使用计数排序的两趟扫描算法。
首先,迭代计算出0、1 和 2 元素的个数,然后按照0、1、2的排序,重写当前数组。
你能想出一个仅使用常数空间的一趟扫描算法吗?
② 双指针(自己的想法)
- 数组排序后一定是
0,...,0 1,...,1 2,...,2
的情况,我们可以将其看做三部分,每一部分分别都是数字0,1,2。 - 因此外层循环共3趟,每一趟别将所有的0放在第一段,所有的1放在第二段,所有的放在第三段。
- 内层循环使用双指针,
start指针
用于表示待插入数字的位置,cur指针
不断向后移动,寻找可以插入的数字。 - 每趟的内存循环,如果
nums[start]
刚好为i,需要更新start的指向,并且start不能越界;确定待插入数字的位置后,cur从start+1
开始向后遍历数组,查找可以插入的数字。 - 没有找到可以插入的数字,cur指向下一个数字;否则start和cur同时指向下一个数字。
- 代码如下:
public void sortColors(int[] nums) {
if (nums.length == 0 || nums.length == 1) {
return;
}
int start = 0;
for (int i = 0; i < 3; i++) {
while (start < nums.length && nums[start] == i) {
start++;
}
int cur = start + 1;
while (cur < nums.length) {
// cur向后遍历,直到遍历完数组
if (nums[cur] == i) {
// 找到了待插入的数字,交换cur和start指向数字
int temp = nums[start];
nums[start] = nums[cur];
nums[cur] = temp;
start++; // start+1指向下一个待插入数字的位置
}
cur++;
}
}
}
③ 统计0和1的个数,为数组添加对应数目的0,1,2
- 代码如下:
public void sortColors(int[] nums) {
if (nums.length == 0 || nums.length == 1) {
return;
}
int zero_count = 0;
int one_count = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] == 0) {
zero_count++;
}
if (nums[i] == 1) {
one_count++;
}
}
// 将对应位置的数字改为对应数目的0,1,2
for (int i = 0; i < nums.length; i++) {
if (zero_count > 0) {
nums[i] = 0;
zero_count--;
} else if (one_count > 0) {
// 只有当0的个数加满后,才能添加1
nums[i] = 1;
one_count--;
} else {
nums[i] = 2;
}
}
}