面试题10:斐波那契数列

题目一:斐波那契数列

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

f(x) = 
\begin{cases}
0 &  n=0 \\
1 &  n=1 \\
f(n-1)+f(n-2) &{n>1}\\
\end{cases}
  • 算法:循环,并把结果取模防止溢出

测试:力扣

public int fib(int n) {
    if (n < 0) {
        return -1;
    }
    if (n < 2) {
        return n;
    }

    int fibMinusOne = 1;
    int fibMinusTwo = 0;
    int fibN = 0;
    for (int i = 2; i <= n; i++) {
        fibN = (fibMinusOne + fibMinusTwo) % 1000000007;    // 避免溢出;如果放到最后取模可能会在这一步发生溢出使最终结果错误
        fibMinusTwo = fibMinusOne;
        fibMinusOne = fibN;
    }

    return fibN;
}
  • 算法:使用long数据类型
public long fibonacci(int n) {
    if (n < 0) {
        return -1;
    }
    if (n < 2) {
        return n;
    }

    long fibOne = 0;
    long fibTwo = 1;
    long finN = 0;
    for (int i = 2; i <= n; i++) {
        finN = fibOne + fibTwo;
        fibOne = fibTwo;
        fibTwo = finN;
    }

    return finN;
}
  • 算法:自下向上和自上向下的动态规划
// DP-bottom up time-O(n) space-O(n)
public int fib_(int n) {
    if (n < 0) {
        return -1;
    }

    if(n < 2)
        return n;

    int[] fib = new int[n + 1];
    fib[1] = 1;
    for(int i = 2; i <= n; i++) {
        fib[i] = fib[i - 1] + fib[i - 2];
    }
    return fib[n];
}

// DP-top down time-O(n) space-O(n)
public int fib(int n) {
    if (n < 0) {
        return -1;
    }

    int[] fib = new int[n+1];
    return fibDP(n, fib);
}
public int fibDP(int n, int[] fib) {
    if(n < 2)
        return n;
    else if(fib[n] != 0)
        return fib[n];
    else
        return fib[n] = fib(n - 1) + fib(n - 2);
}
  • 算法:矩阵运算

算法:用归纳法可以证明以下数学公式:

\left[
\begin{matrix}
   f(n) & f(n-1) \\
   f(n-1) & f(n-2) \\
\end{matrix} 
\right]
=
{\left[
\begin{matrix}
   1 & 1 \\
   1 & 0 \\
\end{matrix} 
\right]}^{n-1}

然后初始矩阵的乘方可以通过以下递归实现O(logn)时间的算法

a^n = 
\begin{cases}
a^{\frac{n}{2}} * a^{\frac{n}{2}} & n为偶数\\
a^{\frac{n-1}{2}} * a^{\frac{n-1}{2}} * a & n为奇数\\
\end{cases}

测试:自测

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

    Matrix2By2 matrix2By2 = MatrixPower(n-1);
    return matrix2By2.m00;
}

class Matrix2By2 {
    long m00;long m01;long m10;long m11;
    Matrix2By2() {}
    Matrix2By2(long x, long y, long z, long w) { m00 = x;m01 = y;m10 = z;m11 = w;}
}

private Matrix2By2 MatrixMultiply(Matrix2By2 matrix1, Matrix2By2 matrix2) {
    return new Matrix2By2(matrix1.m00 * matrix2.m00 + matrix1.m01 * matrix2.m10,
                          matrix1.m00 * matrix2.m01 + matrix1.m01 * matrix2.m11,
                          matrix1.m10 * matrix2.m00 + matrix1.m11 * matrix2.m10,
                          matrix1.m10 * matrix2.m01 + matrix1.m11 * matrix2.m11);
}

private Matrix2By2 MatrixPower(int n) {
    Matrix2By2 matrix = new Matrix2By2();
    if (n == 1) {
        matrix = new Matrix2By2(1, 1, 1, 0);
    } else if(n % 2 == 0) {
        matrix = MatrixPower(n / 2);
        matrix = MatrixMultiply(matrix, matrix);
    } else if(n % 2 == 1) {
        matrix = MatrixPower((n - 1) / 2);
        matrix = MatrixMultiply(matrix, matrix);
        matrix = MatrixMultiply(matrix, new Matrix2By2(1, 1, 1, 0));
    }
    return matrix;
}

题目二:青蛙跳台阶1

题目二:一个青蛙一次可以跳上一级台阶,也可以跳上两级台阶。求该青蛙跳上n级台阶总共有多少种跳法。

算法:同斐波那契数列,但初始项为f(1) = 1; f(2) = 2;,且不能再用矩阵来计算

题目三:青蛙跳台阶2

题目三:一个青蛙一次可以跳上一级台阶,也可以跳上两级台阶······也可以跳上n级台阶。求该青蛙跳上n级台阶总共有多少种跳法。

算法:f(n) = f(1)+f(2)+···+f(n-1)+1

f(n) = 2^(n-1)

题目四:2X1矩形覆盖2Xn矩形

题目四:用2X1的小矩形横着或竖着去覆盖2Xn的大矩形总共有多少种方法。

算法:最左边第一块竖着放,则剩余2X(n-1);最左边第一块横着放,则必须再来一块横着放与它对齐,则剩余2X(n-2);所以f(n)=f(n-1)+f(n-2);f(1)=1;f(2)=2;

面试题11:旋转数组的最小数字

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

测试:力扣

public int minArray(int[] numbers) {
    if (numbers == null || numbers.length == 0) {
        return -1;
    }

    int index1 = 0;
    int index2 = numbers.length - 1;
    int indexMid = (index1 + index2) >> 1;
    if (numbers[index1] == numbers[index2] && numbers[indexMid] == numbers[index1]) {
        return minArrayInorder(numbers);
    }

    indexMid = index1;
    while (numbers[index1] >= numbers[index2]) {
        if (index2 - index1 <= 1) {
            return numbers[index2];
        }

        indexMid = (index1 + index2) >> 1;
        if (numbers[indexMid] >= numbers[index1]) {
            index1 = indexMid;
        }
        if (numbers[indexMid] <= numbers[index2]) {
            index2 = indexMid;
        }
    }
    return numbers[indexMid];
}

private int minArrayInorder(int[] numbers) {
    int res = numbers[0];
    for (int i = 0; i < numbers.length; i++) {
        if (numbers[i] < res) {
            res = numbers[i];
        }
    }
    return res;
}

面试题12:矩阵中的路径

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

A B T G

C F C S

J D E H

测试:力扣

public boolean exist(char[][] board, String word) {
    // 排除异常输入
    if (board == null || board.length == 0) {
        return false;
    }
    for (int i = 0; i < board.length; i++) {
        if (board[i] == null || board[i].length == 0) {
            return false;
        } else {
            if (i > 0 && board[i].length != board[i-1].length) {
                return false;
            }
        }
    }
    if (word == null || word.length() == 0) {
        return true;
    }

    int rows = board.length;
    int cols = board[0].length;
    boolean[][] visited = new boolean[rows][cols];
    int pathLength = 0;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (hasPathCore(board, i, j, word, pathLength, visited)) {
                return true;
            }
        }
    }
    return false;
}

private boolean hasPathCore(char[][] board, int row, int col, String word, int pathLength, boolean[][] visited) {
    if (pathLength == word.length()) {
        return true;
    }

    boolean hasPath = false;
    if (row >= 0 && row < board.length && col >= 0 && col < board[0].length &&
            board[row][col] == word.charAt(pathLength) && !visited[row][col]) {
        pathLength++;
        visited[row][col] = true;
        hasPath = hasPathCore(board, row, col+1, word, pathLength, visited) ||
                hasPathCore(board, row, col-1, word, pathLength, visited) ||
                hasPathCore(board, row+1, col, word, pathLength, visited) ||
                hasPathCore(board, row-1, col, word, pathLength, visited);

        if (!hasPath) {
            pathLength--;
            visited[row][col] = false;
        }
    }
    return hasPath;
}

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

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

  • 算法:
    • visited数组 + 递归
    • 任意位置可运动范围等于当前这个位置加上四个方向的运动范围
public int movingCount(int rows, int cols, int threshold) {
    if (rows <= 0 || cols <= 0 || threshold < 0) {
        return 0;
    }

    boolean[][] visited = new boolean[rows][cols];
    return movingCountCore(rows, cols, 0, 0, visited, threshold);
}

private int movingCountCore(int rows, int cols, int row, int col, boolean[][] visited, int threshold) {
    int count = 0;
    if (check(rows, cols, row, col, visited, threshold)) {
        visited[row][col] = true;
        // all together how many grids the root can arrive
        count = 1 + movingCountCore(rows, cols, row, col+1, visited, threshold)
                + movingCountCore(rows, cols, row, col-1, visited, threshold)
                + movingCountCore(rows, cols, row+1, col, visited, threshold)
                + movingCountCore(rows, cols, row-1, col, visited, threshold);
    }
    return count;
}

private boolean check(int rows, int cols, int row, int col, boolean[][] visited, int threshold)  {
    if (row >= 0 && row < rows && col >= 0 && col < cols &&
            !visited[row][col] && getDigitSum(row) + getDigitSum(col) <= threshold) {
        return true;
    }
    return false;
}

private int getDigitSum(int x) {
    int sum = 0;
    while (x > 0) {
        sum += x % 10;
        x /= 10;
    }
    return sum;
}

面试题14:剪绳子

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

  • 算法:动态规划

测试:力扣

public int cuttingRope(int n) {
    // 异常输入
    if (n < 0) {
        return -1;
    }

    // 特殊输入
    if (n < 2) {
        return 0;
    }
    if (n < 4) {
        return n - 1;
    }

    int[] product = new int[n+1];
    product[0] = 0; // 注意这里的product[x]的初始化不是n=x的结果,而是动态规划的初始状态
    product[1] = 1;
    product[2] = 2;
    product[3] = 3;

    for (int i = 4; i <= n; i++) {
        int max = 0;
        for (int j = 1; j <= i >> 1; j++) {
            int products = product[j]*product[i-j];
            if (max < products) {
                max = products;
            }
        }
        product[i] = max;
    }
    return product[n];
}
  • 算法:贪婪算法

算法:当n≥5时,尽可能多地剪长度为3的绳子;当剩下的绳子为4时,把绳子剪成两段长度为2的绳子。

证明:当n≥5时,可以证明2(n-2)>n并且3(n-3)>n,也就是说,当绳子长度大于等于5时,就把它剪成长度为3或者2的绳子段;另外3(n-3)>2(n-2),因此应该尽可能地多剪长度为3的绳子段;当绳子的长度为4时,显然2×2>3×1;因为2×2=4,也可以选择不剪。

测试:力扣

public int cuttingRope(int n) {
    // 异常输入
    if (n < 0) {
        return -1;
    }

    // 特殊输入
    if (n < 2) {
        return 0;
    }
    if (n < 4) {
        return n - 1;
    }

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

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

说明:另一种形式的贪婪算法

测试:力扣

public int cuttingRope_1(int n) {
    // 异常输入
    if (n < 0) {
        return -1;
    }

    int[] product = new int[60];
    product[0] = 0;
    product[1] = 0;
    product[2] = 1;
    product[3] = 2;

    product[4] = 4;
    product[5] = 6;
    product[6] = 9;

    for (int i = 7; i <= n; i++) {
        product[i] = product[i-3] * 3;
    }
    return product[n];
}
  • 算法:取余避免溢出
public int cuttingRope(int n) {
    // 异常输入
    if (n < 0) {
        return -1;
    }

    // 特殊输入
    if (n < 2) {
        return 0;
    }
    if (n < 4) {
        return n - 1;
    }

    int timeOf3 = n / 3;
    if (n == 1 + timeOf3 * 3) {
        timeOf3 -= 1;
    }
    int timeOf2 = (n - timeOf3 * 3) / 2;

    long res = 1;   // long数据类型避免中间状态溢出
    for (int i = 0; i < timeOf3; i ++) {
        res *= 3;
        res %= 1000000007;
    }
    for (int i = 0; i < timeOf2; i ++) {
        res *= 2;
        res %= 1000000007;
    }

    return (int)res;
}

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

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

  • 算法:和n-1做按位与
public int numberOf1(int n) {
    int count = 0;
    while (n != 0) {
        count++;
        n &= n-1;
    }
    return count;
}
  • 算法:和2的幂逐个做按位与
public int numberOf1(int n) {
    int count = 0;
    int flag = 1;
    while (flag != 0) {
        if ((n & flag) != 0) {
            count++;
        }
        flag <<= 1;
    }
    return count;
}
  • 算法:逐位进行逻辑右移
public int numberOf1(int n) {
    int count = 0;
    while (n != 0) {
        if ((n & 1) == 1) {
            count++;
        }
        n >>>= 1;
    }
    return count;
}