剑指offer——Java刷题总结【一】

Note

  • 题解汇总:剑指offer题解汇总
  • 代码地址:Github 剑指offer Java实现汇总
  • 点击目录中的题名链接可直接食用题解~
  • 有些解法博文中未实现,不代表一定很难,可能只是因为博主太懒```(Orz)
  • 如果博文中有明显错误或者某些题目有更加优雅的解法请指出,谢谢~

目录

题号 题目名称
1 二维数组中的查找
2 替换空格
3 从尾到头打印链表
4 重建二叉树
5 用两个栈实现队列
6 旋转数组的最小数字
7 斐波那契数列
8 跳台阶
9 变态跳台阶
10 矩形覆盖

正文

1、二维数组中的查找

题目描述

在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

题目分析

解法一: 我们可以利用该二维数组的性质:每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。也就是说,对于数组左下角的值 m,m 是该行最小的数,同时也是该列最大的数。我们只需要每次将 m 和目标值 target 进行比较:
1、当 m < target 时,由于 m 是当前列的最大元素,想要得到更大的数只能对列进行右移;
2、当 m > target 时,由于 m 是当前行的最小元素,想要得到更小的数只能对行进行上移;
3、当 m = target 时,找到该值,返回 true。
用某行最小或某列最大与 target 比较,每次可剔除一整行或一整列。故时间复杂度是O(m+n),使用两层循环暴力破解需要时间复杂度为O(n²)。

代码实现

解法一:

public boolean Find(int target, int [][] array) {
    if (array == null || array.length == 0) return false;
    if (array[0].length == 0) return false;
    int row = array.length - 1;
    int col = 0;
    do {
        if (array[row][col] < target) {
            col++;
        } else if (array[row][col] > target) {
            row--;
        } else {
            return true;
        }
    } while (row >= 0 && col < array[0].length);
    return false;
}

2、替换空格

题目描述

请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

题目分析

解法一: 用Java自带的替换函数,str.toString().replace(" “,”%20")。
解法二: 在当前字符串上进行替换。
1、先计算替换后的字符串需要多大的空间,并对原字符串空间进行扩容;
2、从后往前替换字符串的话,每个字符串只需要移动一次;
3、如果从前往后,每个字符串需要多次移动,效率较低。
解法三: 开辟一个新的字符串。与解法二基本一致,只是不需要对字符进行移动,而是将需要替换的字符直接添加到新的字符串上,需要整个字符串的额外空间。

代码实现

解法一:

public String replaceSpace(StringBuffer str) {
    if (str == null || str.length() == 0) return "";
    return str.toString().replace(" ","%20");
}

解法二:

public static String replaceSpace1(StringBuffer str) {
    if (str == null || str.length() == 0) return "";
    int count = 0;
    for (int i = 0; i < str.length(); i++) {
        if (str.charAt(i) == ' ') {
            count++;
        }
    }
    int oldLen = str.length();
    str.setLength(oldLen + 2 * count);
    for (int i = oldLen - 1; i >= 0; i--) {
        if (str.charAt(i) != ' ') {
            str.setCharAt(i + 2 * count, str.charAt(i));
        } else {
            str.setCharAt(i + 2 * count, '0');
            str.setCharAt(i + 2 * count - 1, '2');
            str.setCharAt(i + 2 * count - 2, '%');
            count--;
        }
    }
    return str.toString();
}

解法三:

public String replaceSpace(StringBuffer str) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < str.length(); i++) {
        char c = str.charAt(i);
        if (c == ' ') {
            sb.append("%20");
        } else {
            sb.append(c);
        }
    }
    return sb.toString();
}

3、从尾到头打印链表

题目描述

输入一个链表,按链表从尾到头的顺序返回一个ArrayList。

题目分析

解法一: 依次遍历链表并将每个链表元素插入到ArrayList中,最后使用Collection.reverse对ArrayList进行翻转并返回。
解法二: ArrayList中有个方法是add(index,value),可以指定index位置插入value值。依次遍历链表并将每个链表元素插入到list的0位置,最后返回ArrayList即可得到逆序链表。
解法三: 利用递归,借助系统的栈将元素压栈,依次出栈并将元素添加至ArrayList中即可完成逆序。

代码实现

解法一:

public static ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ListNode head = listNode;
    ArrayList<Integer> list = new ArrayList<>();
    while (head != null) {
        list.add(head.val);
        head = head.next;
    }
    Collections.reverse(list);
    return list;
}

解法二:

public static ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    ArrayList<Integer> list = new ArrayList<>();
    ListNode tmp = listNode;
    while(tmp != null){
        list.add(0, tmp.val);
        tmp = tmp.next;
    }
    return list;
}

解法三:

ArrayList<Integer> list = new ArrayList();
public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
    if (listNode != null) {
        printListFromTailToHead(listNode.next);
        list.add(listNode.val);
    }
    return list;
}

4、重建二叉树

题目描述

输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树。假设输入的前序遍历和中序遍历的结果中都不含重复的数字。例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6},则重建二叉树并返回。

题目分析

解法一:
根据中序遍历和前序遍历可以确定二叉树,具体过程为:

  1. 根据前序序列第一个元素确定根结点; 遍历中序序列,查找前序序列第一个节点在中序序列所在位置;
  2. 根据根结点在中序序列中的位置分割出左右两个子序列,分别代表左子树和右子树的中序遍历结果;
  3. 根据左右子树的中序遍历长度,计算出左右子树的前序遍历结果;
  4. 现已获得左右子树的前序和中序遍历结果,对左右子树按照1-4步骤进行递归重构。

例如对于前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序列{4,7,2,1,5,3,8,6}而言,我们的步骤是:

  1. 根据前序遍历第一个元素,确定1为根节点;
  2. 遍历中序序列,查找到第四个元素为根节点1,因此对于根节点1而言,左子树的中序遍历序列为{4,7,2},右子树的中序遍历序列为{5,3,8,6};
  3. 根据左子树的中序遍历长度为3,可以知道在前序遍历序列中,{2,4,7}为左子树的前序遍历序列,{3,5,6,8}为右子树的前序遍历序列;
  4. 根据左子树的前序和中序遍历序列分别为{2,4,7}和{4,7,2},可以按照1-3步骤递归重构出一颗树,并将它作为1的左子树;
  5. 根据右子树的前序和中序遍历序列分别为{3,5,6,8}和{5,3,8,6},可以按照1-3步骤递归重构出一颗树,并将它作为1的右子树;
  6. 直至前序序列或中序序列长度为0,说明到达叶子节点。返回空,结束递归。
代码实现

解法一:

public TreeNode reConstructBinaryTree(int [] pre, int [] in) {
    if (pre.length == 0 || in.length == 0) {
        return null;
    }
    TreeNode h = new TreeNode(pre[0]);
    int index = -1;
    for (int i = 0; i < in.length; i++) {
        if (in[i] == pre[0]) {
            index = i;
        }
    }
    int[] in1 = Arrays.copyOfRange(in, 0, index);
    int[] in2 = Arrays.copyOfRange(in, index + 1, in.length);
    int[] p1 = Arrays.copyOfRange(pre, 1, in1.length + 1);
    int[] p2 = Arrays.copyOfRange(pre, in1.length + 1, pre.length);
    h.left = reConstructBinaryTree(p1, in1);
    h.right = reConstructBinaryTree(p2, in2);
    return h;
}

5、用两个栈实现队列

题目描述

用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

题目分析

解法一: 定义两个栈,栈A和栈B。栈A只负责正常的push;当栈B有元素时则直接pop,当栈B没有元素时,将栈A中的元素全部、一次性地pop到栈B,然后对栈B进行pop。

代码实现

解法一:

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    public void push(int node) {
        stack1.push(node);
    }
    public int pop() {
        if (stack2.empty()) {
            while (!stack1.empty()) {
                stack2.push(stack1.pop());
            }
        } 
        if (!stack2.empty()) {
            return stack2.pop();
        } else {
            return -1;
        }
    }
}

6、旋转数组的最小数字

题目描述

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

NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

题目分析

解法一: 一次遍历。数组在发生旋转后,以最小元素为中心的左右两端数组都是升序的,因此我们可以对数组进行遍历,当某个元素比它前一个元素小,则说明该元素是数组中的最小元素。
解法二: 二分查找。二分查找用于查找有序的数组中的值,题目所给数组在两段范围内有序,我们可以将给定数组分为两种情况:

  1. 其实并没有旋转,例如 {1,2,3,4,5},旋转后也是 {1,2,3,4,5},这样可以直接使用二分查找;
  2. 如题所示,旋转了一部分,例如 {1,2,3,4,5},旋转后为 {3,4,5,1,2},需要限定特殊条件后使用二分查找。

当数组如情况 1,有个鲜明的特征,即数组左边元素 < 数组右边元素,这时我们直接返回首元素即可;
当数组如情况 2,此时有三种可能找到最小值:

  1. 下标为 n+1 的值小于下标为 n 的值,则下标为 n+1 的值肯定是最小元素;
  2. 下标为 n 的值小于下标为 n-1 的值,则下标为 n 的值肯定是最小元素;
  3. 由于不断查找,数组查找范围内的值已经全为非降序(退化为情况1)。

再讨论每次二分查找时范围的变化,由于情况数组的情况 1 能直接找到最小值,需要变化范围的肯定是情况 2:

  1. 当下标为 n 的值大于下标为 0 的值,从 0 到 n 这一段肯定是升序,由于是情况 2,最小值肯定在后半段;
  2. 当下标为 n 的值小于下标为 0 的值,从 0 到 n 这一段不是升序,最小值肯定在这一段。
代码实现

解法一: O(n)

public int minNumberInRotateArray(int [] array) {
    if (array.length == 0) return 0;
    int index = 0;
    for (int i = 1; i < array.length; i++) {
        if (array[i] < array[i - 1]) {
            index = i;
            break;
        }
    }
    return array[index];
}

解法二: O(logn)

public int minNumberInRotateArray(int [] array) {
    if (array.length == 1) return array[0];
    int l = 0;
    int r = array.length - 1;
    while (l < r) {
        int m = l + ((r - l) >> 1);
        if (array[l] < array[r]) {
            return array[l];
        }
        if (array[m] > array[m + 1]) {
            return array[m + 1];
        }
        if (array[m] < array[m - 1]) {
            return array[m];
        }
        if (array[m] > array[0]) {
            l = m + 1;
        } else {
            r = m - 1;
        }
    }
    return 0;
}

7、斐波那契数列

题目描述

大家都知道斐波那契数列F(n)=F(n-1)+F(n-2),现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项为1)。n<=39.

题目分析

解法一: 递归。根据F(n)=F(n-1)+F(n-2)递归调用即可。
解法二: 动态规划。可以选择一个长度为n的数组用于保存每次计算的值,遍历数组,根据数组之前的元素得到当前元素,在该情况下空间复杂度为O(n)。由于在遍历过程中,实际上只用到n-1和n-2两个位置的值,因此可以只使用两个变量对历史的计算结果进行存储,将空间复杂度优化到O(1)。

代码实现

解法一: O(2^n)

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

解法一: O(n)

public int Fibonacci(int n) {
    if (n == 0) return 0;
    if (n <= 2) return 1;
    int pre1 = 1;
    int pre2 = 1;
    int res = 0;
    for (int i = 3; i <= n; i++) {
        res = pre1 + pre2;
        pre1 = pre2;
        pre2 = res;
    }
    return res;
}

8、跳台阶

题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

题目分析

解法一: 动态规划。斐波那契数列的变种,上一题的优化解法需要三个额外变量,该解法只需要两个额外变量,sum用来存储当前值,pre用来存储前一个值,但是空间复杂度仍为O(1)。

代码实现

解法一: O(n)

public int JumpFloor(int target) {
    if (target < 2) return 1;
    int pre = 1;
    int sum = 1;
    for (int i = 2; i <= target; i++) {
        sum = sum + pre;
        pre = sum - pre;
    }
    return sum;
}

9、变态跳台阶

题目描述

一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

题目分析

解法一: 动态规划。斐波那契数列的变种。由于青蛙每次可以跳n级台阶,因此青蛙跳上n级台阶的跳法为f(n)=f(n-1)+f(n-2)+……f(1)+f(0)。经过分析,发现结果呈规律性,f(n)以2的n次幂形式增长,f(0)=1,f(1)=1,f(2)=2…从而可以归纳得到计算表达式:f(n)=2^(n-1)。

代码实现

解法一: O(1)

public int JumpFloorII(int target) {
    return target == 0 ? 1 : (int) Math.pow(2, target - 1);
}

10、矩形覆盖

题目描述

我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?

题目分析

解法一: 动态规划。斐波那契数列的变种。其实和第9题是一样的,f(n)=f(n-1)+f(n-2)。因为每次多加1*2的矩形时,无非就是f(n-1)种然后加上一条竖着的矩阵,或者f(n-2)种然后加上两条横着的矩阵。在该解法中使用的是递归方法求解,时间复杂度极高。在这仅仅是给出递归解的实现,建议使用时间复杂度为O(n)、空间复杂度为O(1)的最优解法(参考第7-9题)。

代码实现

解法一: O(2^n)

public int RectCover(int target) {
    if (target == 0) return 0;
    if (target == 1) return 1;
    if (target == 2) return 2;
    return RectCover(target - 1) + RectCover(target - 2);
}

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