《剑指offer》各编程题Java版分析 -- 算法和数据操作

236 阅读4分钟

题目10 -- 斐波那契数列

题目一: 求斐波那契数列的第n项。 写一个函数,输入n,求斐波那契额(Fibonacci)数列的第n项。斐波那契额数列的定义如下:

斐波那契数列问题,和书中C++版本没什么不同,不再赘述

public long fibonacci(int n) {
    if (n <= 0) {
        return 0;
    }
    if (n == 1) {
        return 1;
    }
    return fibonacci(n - 1) + fibonacci(n - 2);
}

public long fibonacci2(int n) {
    if (n <= 0) {
        return 0;
    }
    if (n == 1) {
        return 1;
    }
    int fib1 = 0;
    int fib2 = 1;
    int fib = fib1 + fib2;
    for (i = 2; i <= n; i++) {
        fib = fib1 + fib2;
        fib1 = fib2;
        fib2 = fib;
    }
    return fib;
}

题目11 -- 旋转数组的最小数字

题目:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。

interview11.gif

二分法起手,通过array[mid]判断最小值在左半边区域or右半边区域。写出基本框架后,补充异常情况,并且在测试用例中考虑如下情况:

  1. 没旋转,完全是普通的递增数组,{1,2,3,4,5}
  2. 包含重复值的数组,{3,4,5,3,3,3,3}
  3. 所有值都是一个数字的数组,{3,3,3,3,3}

situation2.png

situation3.png

public class Solution {

    /**
     * 1. regular eg. 3,4,5,1,2
     * 1. accumulate eg. 1,2,3,4,5
     * 2. same number eg. 3,3,3,3,3
     */
    public int findMinOfRotateArray(int[] array) {
        if (array == null || array.length == 0) {
            throw new IllegalArgumentException("arrays is empty");
        }
        int left = 0;
        int right = array.length - 1;
        int mid = left;
        while (array[left] >= array[right]) {
            if (right - left == 1) {
                mid = right;
                break;
            }
            mid = (right - left) >> 1 + left;
            if (array[mid] == array[left] && array[mid] == array[right]) {
                return findInOrder(array, left, right);
            }
            if (array[left] <= array[mid]) { // min value in right region
                mid = left;
            } else if (array[mid] <= array[right]) { // min value in left region
                mid = right;
            }
        }
        return array[mid];
    }

    private int findInOrder(int[] array, int left, int right) {
        int result = array[left];
        for (int i = left+1; i <= right; i++) {
            result = Math.max(result, array[i]);
        }
        return result;
    }

}

面试题12 -- 矩阵中的路径

题目:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步去可以在矩阵中左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3*4的矩阵中包含一条字符串"bfce"的路径(路径中的字母用下划线标出)。但矩阵中不包含字符串"abfb"的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

interview12.gif

public class Solution {

    public boolean findPathInMatrix(char[][] matrix, String str) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0 || str == null) {
            return false;
        }
        boolean[][] visited = new boolean[matrix.length][matrix[0].length];
        for (int i = 0; i < matrix.length; i++) {
            for (int j = 0; j < matrix[0].length; j++) {
                if (doFindPathInMatrix(matrix, i, j, str, 0, visited)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean doFindPathInMatrix(char[][] matrix, int row, int col, String str, int index, boolean[][] visited) {
        if (index == str.length()) {
            return true;
        }
        boolean found = false;
        if (row >= 0 && row < matrix.length && col >= 0 && col < matrix[0].length
                && !visited[row][col] && index < str.length()
                && matrix[row][col] == str.charAt(index)) {
            visited[row][col] = true;
            index++;
            found = doFindPathInMatrix(matrix, row-1, col, str, index, visited)
                    || doFindPathInMatrix(matrix, row, col+1, str, index, visited)
                    || doFindPathInMatrix(matrix, row+1, col, str, index, visited)
                    || doFindPathInMatrix(matrix, row, col-1, str, index, visited);
            if (!found) {
                visited[row][col] = false;
            }
        }
        return found;
    }
}

面试题13 -- 机器人的运动范围

题目:地上有一个m行n列的方格。一个机器人从左表(0,0)的格子开始移动,它每次可以向左、右、上、下移动一格,但不能进入行左表和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格(35,37),因为3+5+3+7=18。但它不能进入方格(35,38),因为3+5+3+8=19。请问该机器人能够到达多少个格子?

搜索的问题,书中提出的是用DFS回溯法解决,其实也可以通过BFS搜索解决。从(0,0)开始,机器人在每个格子(如0,0)上,都面临上下左右四个方向的选择,如(0,0)就是机器人在这个格子上向上移动后可能走的格子数 + 向下移动后可能走的格子数 + 向左移动后可能走的格子数 + 向右移动后可能走的格子数

interview13.gif

public class Solution {

    public int movingCount(int threshold, int rows, int cols) {
        if (threshold <= 0 || rows <= 0 || cols <0) {
            return 0;
        }
        boolean[][] visited = new boolean[rows][cols];
        return movingCountCore(threshold, visited, 0, 0);
    }

    private int movingCountCore(int threshold, boolean[][] visited, int i, int j) {
        if (check(threshold, visited, i, j)) {
            visited[i][j] = true;
            return 1 + movingCountCore(threshold, visited, i-1, j) +
                    movingCountCore(threshold, visited, i, j+1) +
                    movingCountCore(threshold, visited, i+1, j) +
                    movingCountCore(threshold, visited, i, j-1);
        }
        return 0;
    }

    private boolean check(int threshold, boolean[][] visited, int i, int j) {
        return i >= 0 && i < visited.length
                && j >= 0 && j < visited[0].length
                && !visited[i][j]
                && getDigitSum(i, j) <= threshold;
    }

    private int getDigitSum(int i, int j) {
        int sum = 0;
        while (i != 0) {
            sum += i % 10;
            i /= 10;
        }
        while (j != 0) {
            sum += j % 10;
            j /= 10;
        }
        return sum;
    }
}

面试题14 -- 剪绳子

题目:给你一根长度为n的绳子,请把绳子剪成n段(m,n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],k[2]...k[m]。请问k[0]*k[1]*...*k[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

这道题关于动态规划解法,书中描述的过于简单了,定义f(n)为长度为n的绳子的解法,首先通过列出f(1)、f(2)、f(3)...的规律,不难发现长度为n的绳子最大乘积为

f(n) = max(f(i) * f(n-i)), 1<=i<n

从而发现动态规划的重叠子问题、局部最优解的规律

cut_rope

书中对于dp[0]、dp[1]、dp[2]、dp[3]进行了自己的定义,因为在i<=3时,i本身是有可能大于dp[i]的,而从dp[4]开始,一定有dp[4]>4、dp[5]>5...

本文的解法只考虑直观的判断,即把i本身也考虑在内,代码如下

public class Solution {

    public int cutRope(int n) {
        if (n < 1) {
            throw new IllegalArgumentException("number n is negative.");
        }
        if (n <= 2) {
            return 1;
        }
        int[] maxProducts = new int[n+1];
        maxProducts[1] = 1;
        for (int len = 2; len <= n; len++) {
            for (int i = 1; i <= len/2; i++) {
                int maxLeft = Math.max(maxProducts[i], i);
                int maxRight = Math.max(maxProducts[len - i], len - i);
                maxProducts[len] = Math.max(maxProducts[len], maxLeft * maxRight);
            }
        }
        return maxProducts[n];
    }
}

贪心算法的解法是尽量将绳子切成长度为3的,其次是2,最次是1,实现较为简单,但是证明的过程需要公式求导等数学手段,可以参考 面试题14- I. 剪绳子(数学推导 / 贪心思想,清晰图解) - 剪绳子 - 力扣(LeetCode)

public int cutRopeGreedy(int n) {
    if (n < 2) {
        return 0;
    } else if (n == 2) {
        return 1;
    } else if (n == 3) {
        return 2;
    }

    int timesOf3 = n / 3;
    if (n - timesOf3 * 3 == 1) {
        timesOf3 -= 1;
    }

    int timesOf2 = (n - timesOf3 * 3) / 2;
    return (int) (Math.pow(3, timesOf3) * Math.pow(2, timesOf2));
}   

面试题15 -- 二进制中1的个数

题目:请实现一个函数,输入一个整数,输出该数二进制表示中1的个数。例如,把9表示成二进制是1001,有2位是1。因此,如果输入9,则该函数输出2。

题目比较简单,不再赘述。

public class Solution {

    public int numberOfOne(int n) {
        int count = 0;
        int flag = 1;
        while (flag != 0) { // flag依次和n的每个bit位对比
            if ((flag & n) != 0) {
                count++;
            }
            flag <<= 1;
        }
        return count;
    }

    public int numberOfOneBit(int n) {
        int count = 0;
        while (n != 0) {
            n &= (n - 1); // n & (n-1),每次都能干掉一个1
            count++;
        }
        return count;
    }

}