面试题10:斐波那契数列
① 题目1:求斐波那契数列的第n项
f
(
0
)
=
0
;
f
(
1
)
=
1
;
f
(
n
)
=
f
(
n
−
1
)
+
f
(
n
−
2
)
,
n
>
=
2
f(0)=0;f(1)=1;f(n)=f(n-1)+f(n-2),n>=2
f(0)=0;f(1)=1;f(n)=f(n−1)+f(n−2),n>=2
- 使用递归的方式,时间和空间复杂度很大,效率比较低。运行花了
983ms
。
public int Fibonacci(int n){
if (n <= 1) {
return n;
}
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
- 以
n = 10
进行分析发现,递归存在很多重复计算,导致时间复杂度以n的指数在增长。
- 将递归改为动态规划,花了
29ms
。注意: 如果n = 0
,则dp[1]
是不存在的,所以要对n = 0
单独处理。
public static int Fibonacci(int n) {
// n=0,要单独处理
if (n == 0) {
return 0;
}
int[] dp = new int[n + 1];
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
- 动态规划中,需要使用数组存储以前的计算结果,有
O
(
n
)
O(n)
O(n)的空间复杂度。其实,只需要保存前两个数就能求出当前的数。这种方法,值花费了
12ms
。
public static int Fibonacci(int n) {
// 对n<=1进行单独处理
if (n <= 1) {
return n;
}
int f0 = 0, f1 = 1;
int fn = 0;
for (int i = 2; i <= n; i++) {
fn = f0 + f1;
f0 = f1;
f1 = fn;
}
return fn;
}
② 题目2:斐波那契数列的应用——青蛙跳台阶
- 情况分析:
- 如果只有1级台阶,则只有1种跳法;
- 如果有2级台阶,有2种跳法:一次性跳2级,一次只跳1级。
- 当台阶数
n > 2
时,第一次可以跳1级,则总共的跳法是后面n - 1
级的跳法;第一次跳2级,则总共的跳法是后面n - 2
级的跳法。所以,总的跳法为 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n-1)+f(n-2) f(n)=f(n−1)+f(n−2),其中 f ( 0 ) = 0 , f ( 1 ) = 1 , f ( 2 ) = 2 f(0)=0,f(1)=1,f(2)=2 f(0)=0,f(1)=1,f(2)=2。
- 综上,青蛙跳台阶问题是典型的斐波那契数列的应用。
③ 题目3:矩形覆盖
- 问题分析:
- 我们将
2 x 8
区域的覆盖方法记为 f ( 8 ) f(8) f(8)。 - 如果将
2 x 1
的矩形竖着放,则还剩下2 x 7
的区域可以放置,我们 f ( 7 ) f(7) f(7)。 - 如果将
2 x 1
的矩形横着放,则其下方必须横着再放一个2 x 1
的矩形,还剩下2 x 6
的区域需要放置,记为 f ( 6 ) f(6) f(6)。 - 所以 f ( 8 ) = f ( 7 ) + f ( 6 ) f(8)=f(7)+f(6) f(8)=f(7)+f(6),其中 f ( 0 ) = 0 , f ( 1 ) = 1 , f ( 2 ) = 2 f(0)=0,f(1)=1,f(2)=2 f(0)=0,f(1)=1,f(2)=2。
- 青蛙跳台阶和矩形覆盖的代码是一样的,代码如下。注意: 从
n = 3
开始,才能使用 f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n-1)+f(n-2) f(n)=f(n−1)+f(n−2)。
public int RectCover(int target) {
if (target <= 2) {
return target;
}
int a = 1, b = 2, c = 0;
for (int i = 3; i <= target; i++) {
c = a + b;
a = b;
b = c;
}
return c;
}
④ 题目4:变态台阶跳
- 如果青蛙一次可以跳上1级,可以跳上2级,···,还可以跳上n级。则所有的跳法总数为 f ( n ) = 2 n − 1 f(n)=2^{n-1} f(n)=2n−1。
- 如果使用动态规划,调到第n级的总数,是跳到n-1级、n-2级、····一直到1级的跳法总和,再加上一次性跳到n级。即 d p [ i ] = d p [ i − 1 ] + d p [ i − 2 ] + ⋅ ⋅ ⋅ + d p [ 1 ] + 1 dp[i]=dp[i-1]+dp[i-2]+···+dp[1]+1 dp[i]=dp[i−1]+dp[i−2]+⋅⋅⋅+dp[1]+1。
- 因此dp[i],均初始化为1,需要求解 d p [ i − 1 ] + d p [ i − 2 ] + ⋅ ⋅ ⋅ + d p [ 1 ] + 1 dp[i-1]+dp[i-2]+···+dp[1]+1 dp[i−1]+dp[i−2]+⋅⋅⋅+dp[1]+1的结果。
- 代码如下:
public int JumpFloorII(int target) {
int[] dp = new int[target];
Arrays.fill(dp, 1);
for (int i = 1; i < target; i++) {
for (int j = 0; j < i; j++) {
dp[i] = dp[i] + dp[j];
}
}
return dp[target - 1];
}
面试题11:旋转数组的最小数字
- 情况分析:
- 将旋转数组一分为二,如果
nums[mid] <= nums[end]
,说明最小值应该在start和mid之间;否则,最小值应该在mid+1和end之间。 - 特殊情1: 如过数组允许元素重复,则可能出现
nums[start] == nums[mid] == nums[end]
的情况,如{1,1,1,0,1}
。此时无法判断最小处于哪个部分,需要切换到顺序查找。只需要查找目前的start到end之间min即可。 - 特殊情况2: 如果数组的长度为0,则不存在最小值,直接返回0。
- 代码如下:
public int minNumberInRotateArray(int[] array) {
if (array.length == 0) {
return 0;
}
int start = 0, end = array.length - 1;
while (start < end) {
int mid = start + (end - start) / 2;
if (array[start] == array[mid] && array[mid] == array[end]) {
return findOnebyOne(array, start, end);
} else if (array[mid] <= array[end]) {
end = mid;
} else {
start = mid + 1;
}
}
return array[start];
}
public int findOnebyOne(int[] array, int start, int end) {
int min = array[start];
for (int i = start + 1; i <= end; i++) {
if (min > array[i]) {
min = array[i];
}
}
return min;
}
相似题:leetcode33:搜索旋转排序数组
- 与上题的不同之处: 这里要查找的是某个指定的数,而非整个数组中的最小值;而且整个数组没有重复数字,不存在无法判断区间的情况。
- 情况分析:
- 将数组一分为二,如果
nums[mid]=target
,说明找到了; - 如果
nums[start] <= nums[mid]
,则可能左半段有序,在左半段中确定start或者end的位置; - 否则,可能右半段有序,在右半段中确定start或者end的位置。
- 注意: 这里查找的是目标值,循环的条件是
start <= end
;因为这个条件,所以无需对数组长度为0的情况进行处理。
public int search(int[] nums, int target) {
int start = 0, end = nums.length - 1;
while (start <= end) {
int mid = (start + end) / 2;
if (nums[mid] == target) {
return mid;
}
if (nums[start] <= nums[mid]) {// 左半段有序
if (target >= nums[start] && target < nums[mid]) {// 目标值在左半段
end = mid - 1;
} else {// 目标值在右半段
start = mid + 1;
}
} else {// 右半段有序
if (target > nums[mid] && target <= nums[end]) {// 目标值在右半段
start = mid + 1;
} else {// 目标值在左半段
end = mid - 1;
}
}
}
return -1;
}
leetocde704:二分查找的递归与非递归实现
- 递归实现:只要
start <= end
,就执行对nums[mid]
的判断。如果没有找到,则更新start或end,继续查找。
public int search(int[] nums, int target) {
int start = 0, end = nums.length - 1;
return binarySearch(nums, target, start, end);
}
public int binarySearch(int[] nums, int target, int start, int end) {
if (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
return binarySearch(nums, target, mid + 1, end);
} else {
return binarySearch(nums, target, start, mid - 1);
}
}
return -1;
}
- 关于两种计算mid值的公式,第二种公式更安全,因为第一种可能出现加法溢出。
- m i d = ( s t a r t + e n d ) / 2 mid = (start + end)/2 mid=(start+end)/2
- m i d = s t a r t + ( e n d − s t a r t ) / 2 mid = start + (end - start)/2 mid=start+(end−start)/2
- 非递归实现:
public int search(int[] nums, int target) {
int start = 0, end = nums.length - 1;
while (start <= end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return -1;
}
面试题12:矩阵中的路径 —— 回溯法
- 使用
visited
数组记录哪些位置被访问过,如果当前位置被选中,则值为true,继续回溯相邻的位置;如果当前位置不可行,需要将其标记为false,回到上一个位置。
private int row;
private int col;
private boolean[][] visited;
public boolean exist(char[][] board, String word) {
row = board.length;
col = board[0].length;
visited = new boolean[row][col];
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
// 当前字符与首字符匹配,栋当前位置开始,查找是否存在可行路径
if (board[i][j] == word.charAt(0) && backtrace(board, word, 0, i, j)) {
return true;
}
}
}
return false;
}
public boolean backtrace(char[][] board, String word, int curIndex, int i, int j) {
if (curIndex == word.length()) {
return true;
}
// 如果i,j超出矩阵,或者当前位置已经被访问过,
// 或者当前位置的字符与待查找的字符不匹配,停止查找
if (i < 0 || i >= row || j < 0 || j >= col ||
visited[i][j] || board[i][j] != word.charAt(curIndex)) {
return false;
}
// 先标记当前位置已经访问过
visited[i][j] = true;
// 从当前位置开始朝四周查找下一个字符,只要有一个路径可行,则当前位置可行
if (backtrace(board, word, curIndex + 1, i - 1, j) ||
backtrace(board, word, curIndex + 1, i + 1, j) ||
backtrace(board, word, curIndex + 1, i, j - 1) ||
backtrace(board, word, curIndex + 1, i, j + 1)) {
return true;
}
// 当前位置不可行,需要回溯,更改visited
visited[i][j] = false;
return false;
}
面试题13:机器人的运动范围 —— DFS
- 矩阵中的单词搜索,需要在从字符a出发,四周没有合适的下一字符时,回退到字符a的上一个字符,并将字符a恢复到未访问状态。
- 而机器人的运动范围,如果当前格子满足要求,则继续查找他的四周。深搜一次就可以遍历完所有的格子,不需要回溯,因此最后无需恢复格子的访问状态。
- 代码如下:
- 只给出了矩阵的行列值,需要先初始化整个矩阵,每个格子的值为其行列的数位之和。
- 计算行列的数位之和,先计算
0 ~ max(row, col)
的数位之和,行列之和直接从里面取值相加即可。 - 对整个矩阵进行深度优先搜索,需要使用visited数组记录格子是否被访问过。
- 如果当前格子超出边界,或者已经访问过,或者行列数位之和超过阈值,直接返回;否则,计数变量加1,并朝四周搜索其他矩阵。
private int[][] direct = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
private int[][] matrix;
private boolean[][] visited;
private int count = 0;
public int movingCount(int threshold, int rows, int cols) {
// 只需要计算行列中的最大值即可,避免重复计算
int[] sum = new int[Math.max(rows, cols)];
matrix = new int[rows][cols];
visited = new boolean[rows][cols];
// 先计算每个格子的数位之和
computeTotalSum(sum);
// 初始化矩阵,矩阵中的值为row和col的数位之和
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = sum[i] + sum[j];
}
}
// 深度优先搜索整个矩阵
dfs(matrix, 0, 0, threshold);
return count;
}
public void dfs(int[][] matrix, int i, int j, int threshold) {
// 超出矩阵边界,当前格子已经被访问过,超出阈值
if (i < 0 || i >= matrix.length || j < 0 || j >= matrix[0].length ||
visited[i][j] || matrix[i][j] > threshold) {
return;
}
// 当前格子满足条件,计数变量加1
visited[i][j] = true;
count++;
for (int d = 0; d < 4; d++) {
dfs(matrix, i + direct[d][0], j + direct[d][1], threshold);
}
}
public void computeTotalSum(int[] sum) {
for (int i = 0; i < sum.length; i++) {
sum[i] = helper(i);
}
}
public int helper(int num) {
int sum = 0;
while (num != 0) {
sum += num % 10;
num = num / 10;
}
return sum;
}
leetcode200:岛屿数量 —— DFS
- 如果当前位置为1且未被访问过,说明这是一个新的岛屿的开始,计数变量加1且从当前位置开始深度优先搜索。
- 深度优先搜索时,如果矩阵访问越界,或者当前位置已经被访问过,或者当前位置不是陆地,则直接停止搜索;否则,标记当前位置已经访问,并朝四周搜索可能的陆地。
- 代码如下,注意: 矩阵可能为空,导致
grid[0].length
无法获取。
public int numIslands(char[][] grid) {
// 特殊情况,如果矩阵为为空,直接返回0
if (grid.length==0||grid[0].length==0){
return 0;
}
int row = grid.length, col = grid[0].length;
boolean[][] visited = new boolean[row][col];
int count = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (grid[i][j] == '1' && !visited[i][j]) {
count++;
DFS(grid, i, j, visited);
}
}
}
return count;
}
public void DFS(char[][] grid, int i, int j, boolean[][] visited) {
// 矩阵访问越界,当前位置已经被访问过,
// 当前位置不是陆地,直接停止搜索
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length ||
visited[i][j] || grid[i][j] != '1') {
return;
}
visited[i][j] = true;
DFS(grid, i - 1, j, visited);
DFS(grid, i + 1, j, visited);
DFS(grid, i, j - 1, visited);
DFS(grid, i, j + 1, visited);
}
- 由于整个矩阵每个位置不是陆地就是water,如果访问过该陆地,直接将其置为water即可,无需使用额外的visited数组保存每个位置的状态。
public int numIslands(char[][] grid) {
// 特殊情况,如果矩阵为为空,直接返回0
if (grid.length==0||grid[0].length==0){
return 0;
}
int row = grid.length, col = grid[0].length;
int count = 0;
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (grid[i][j] == '1' ) {
count++;
DFS(grid, i, j);
}
}
}
return count;
}
public void DFS(char[][] grid, int i, int j) {
// 矩阵访问越界,当前位置已经被访问过,
// 当前位置不是陆地,直接停止搜索
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length ||
grid[i][j] != '1') {
return;
}
grid[i][j] = '0';
DFS(grid, i - 1, j);
DFS(grid, i + 1, j);
DFS(grid, i, j - 1);
DFS(grid, i, j + 1);
}
leetcode305:岛屿的数量II
- 与岛屿的数量相比,它要求计算不同的岛屿数量。如果岛屿数量为2,但两个岛屿一样,应该返回1;
- 不同的岛屿形状,使用Set进行保存。将每个岛屿的形状,存到Set中,最后返回Set的大小即可。
- 代码如下:
private int[][] direct = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
public int numDistinctIslands(int[][] grid) {
// 特殊情况
if (grid.length == 0 || grid[0].length == 0) {
return 0;
}
// 使用set存放每个岛屿的形状,形状用0,1,2,3表示上下左右
HashSet<List<String>> ilandShape = new HashSet<>();
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 1) {
List<String> item = new ArrayList<>();
dfsDistinct(grid, i, j, item);
ilandShape.add(item);
}
}
}
return ilandShape.size();
}
public void dfsDistinct(int[][] grid, int i, int j, List<String> list) {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] != 1) {
return;
}
grid[i][j] = 0;
for (int d = 0; d < 4; d++) {
list.add(String.valueOf(d));
dfsDistinct(grid, i + direct[d][0], j + direct[d][1], list);
}
}