剑指offer_面试题10:斐波那契数列,面试题11:旋转数组的最小数字(二分查找算法),面试题12:矩阵中的路径,面试题13:机器人的运动范围(岛屿的数量I和II)

面试题10:斐波那契数列
① 题目1:求斐波那契数列的第n项

剑指offer_面试题10:斐波那契数列,面试题11:旋转数组的最小数字(二分查找算法),面试题12:矩阵中的路径,面试题13:机器人的运动范围(岛屿的数量I和II)_第1张图片
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(n1)+f(n2),n>=2

  • 使用递归的方式,时间和空间复杂度很大,效率比较低。运行花了983ms
public int Fibonacci(int n){
    if (n <= 1) {
        return n;
    }
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}
  • n = 10进行分析发现,递归存在很多重复计算,导致时间复杂度以n的指数在增长。
    剑指offer_面试题10:斐波那契数列,面试题11:旋转数组的最小数字(二分查找算法),面试题12:矩阵中的路径,面试题13:机器人的运动范围(岛屿的数量I和II)_第2张图片
  • 将递归改为动态规划,花了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:斐波那契数列的应用——青蛙跳台阶

剑指offer_面试题10:斐波那契数列,面试题11:旋转数组的最小数字(二分查找算法),面试题12:矩阵中的路径,面试题13:机器人的运动范围(岛屿的数量I和II)_第3张图片

  • 情况分析:
  1. 如果只有1级台阶,则只有1种跳法;
  2. 如果有2级台阶,有2种跳法:一次性跳2级,一次只跳1级。
  3. 当台阶数 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(n1)+f(n2),其中 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:矩形覆盖

剑指offer_面试题10:斐波那契数列,面试题11:旋转数组的最小数字(二分查找算法),面试题12:矩阵中的路径,面试题13:机器人的运动范围(岛屿的数量I和II)_第4张图片

  • 问题分析:
  1. 我们将2 x 8区域的覆盖方法记为 f ( 8 ) f(8) f(8)
  2. 如果将2 x 1的矩形竖着放,则还剩下2 x 7的区域可以放置,我们 f ( 7 ) f(7) f(7)
  3. 如果将2 x 1的矩形横着放,则其下方必须横着再放一个2 x 1的矩形,还剩下2 x 6的区域需要放置,记为 f ( 6 ) f(6) f(6)
  4. 所以 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(n1)+f(n2)
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)=2n1
  • 如果使用动态规划,调到第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[i1]+dp[i2]++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[i1]+dp[i2]++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:旋转数组的最小数字

剑指offer_面试题10:斐波那契数列,面试题11:旋转数组的最小数字(二分查找算法),面试题12:矩阵中的路径,面试题13:机器人的运动范围(岛屿的数量I和II)_第5张图片

  • 情况分析:
  1. 将旋转数组一分为二,如果nums[mid] <= nums[end],说明最小值应该在start和mid之间;否则,最小值应该在mid+1和end之间。
  2. 特殊情1: 如过数组允许元素重复,则可能出现nums[start] == nums[mid] == nums[end]的情况,如 {1,1,1,0,1}。此时无法判断最小处于哪个部分,需要切换到顺序查找。只需要查找目前的start到end之间min即可。
  3. 特殊情况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:搜索旋转排序数组
  • 与上题的不同之处: 这里要查找的是某个指定的数,而非整个数组中的最小值;而且整个数组没有重复数字,不存在无法判断区间的情况。
  • 情况分析:
  1. 将数组一分为二,如果nums[mid]=target,说明找到了;
  2. 如果nums[start] <= nums[mid],则可能左半段有序,在左半段中确定start或者end的位置;
  3. 否则,可能右半段有序,在右半段中确定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值的公式,第二种公式更安全,因为第一种可能出现加法溢出。
  1. m i d = ( s t a r t + e n d ) / 2 mid = (start + end)/2 mid=(start+end)/2
  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+(endstart)/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:矩阵中的路径 —— 回溯法

剑指offer_面试题10:斐波那契数列,面试题11:旋转数组的最小数字(二分查找算法),面试题12:矩阵中的路径,面试题13:机器人的运动范围(岛屿的数量I和II)_第6张图片

  • 使用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

剑指offer_面试题10:斐波那契数列,面试题11:旋转数组的最小数字(二分查找算法),面试题12:矩阵中的路径,面试题13:机器人的运动范围(岛屿的数量I和II)_第7张图片

  • 矩阵中的单词搜索,需要在从字符a出发,四周没有合适的下一字符时,回退到字符a的上一个字符,并将字符a恢复到未访问状态。
  • 而机器人的运动范围,如果当前格子满足要求,则继续查找他的四周。深搜一次就可以遍历完所有的格子,不需要回溯,因此最后无需恢复格子的访问状态。
  • 代码如下:
  1. 只给出了矩阵的行列值,需要先初始化整个矩阵,每个格子的值为其行列的数位之和。
  2. 计算行列的数位之和,先计算0 ~ max(row, col)的数位之和,行列之和直接从里面取值相加即可。
  3. 对整个矩阵进行深度优先搜索,需要使用visited数组记录格子是否被访问过。
  4. 如果当前格子超出边界,或者已经访问过,或者行列数位之和超过阈值,直接返回;否则,计数变量加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);
     }
 }

你可能感兴趣的:(算法)