牛客剑指Offer : 题解(1-10)

欢迎指正

题解(01-10):link

题解(11-20):link

题解(21-30):link

题解(31-40):link

题解(41-50):link

题解(51-60): link

题解(61-67): link


牛客中做题的时候,使用到工具类啥的需要自己导入

import java.util.* 类似这样的

1.二维数组中的查找

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

1.解法一:利用数组从左到右,从上到下递增的性质

  1. 我们从右上角或者左下角开始遍历整个数组;
  2. 本解法从右上角开始,如果 target > 右上角元素,则该行不可能存在目标值
  3. target < 右上角元素,则该列不可能存在目标值
  4. 依次去剔除不满足的行和列,使得时间复杂度变为 O(n)
public class Solution {
    public boolean Find(int target, int [][] array) {
        if (array == null || array.length == 0)  return false;
        int row = array.length;
        int col = array[0].length;
        for (int i = 0, j = col - 1;i < row && j >= 0; ) {
            if (array[i][j] == target) return true;
            else if (array[i][j] < target) i ++;
            else j --;
        }
        return false;
    }
}

2.解法二:暴力法,两重循环

2.替换空格

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

1.解法一:暴力法,使用额外的字符串进行处理

public class Solution {
    public String replaceSpace(StringBuffer str) {
    	if (str == null) return null;
        String res = "";
        for (int i = 0;i < str.length();i ++) {
            char c = str.charAt(i);
            if (c == ' ')	res += "%20";
            else	res += c;
        }
        return res;
    }
}

2.解法二:调用系统函数

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

3.从尾到头打印链表

题目描述: 输入一个链表,按链表从尾到头的顺序返回一个 ArrayList。

1.解法一:反转链表后再打印

public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        // 相当于先反转一个链表,再输出这个链表
        ArrayList<Integer> res = new ArrayList<>();
        if (listNode == null) return res;
        // 先将链表反转
        ListNode head = reverse(listNode);
        // 将链表内容输出到 res 中
        while (head != null) {
            res.add(head.val);
            head = head.next;
        }
        return res;
    }
    // 反转链表的递归形式
    private ListNode reverse(ListNode head) {
        if (head == null || head.next == null) return head;
        ListNode next = reverse(head.next);
        head.next.next = head;
        head.next = null;
        return next;
    }
}

2.解法二:灵活使用 ArrayListadd方法

  1. ArrayList 中的 add(int index, E element) 能够实现头插
public class Solution {
    public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList<Integer> res = new ArrayList<>();
        while (listNode != null) {
            // add(index, element) 能够灵活实现在链表各位置的插入
            res.add(0, listNode.val);
            listNode = listNode.next;
        }
        return res;
    }
}

3.解法三:根据题目,可以使用栈先存储,再出栈到结果中,需要用到额外的存储空间

4.重建二叉树(根据前序和中序遍历)

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

1.解法一

public class Solution {
    public Map<Integer,Integer> in_map;
    public TreeNode reConstructBinaryTree(int [] pre,int [] in) {
        if (pre == null || in == null || pre.length != in.length) return null;
        // 存放中序遍历中每个元素的位置
        // 键:元素,值:位置索引
        in_map = new HashMap<>();
        for (int i = 0;i < in.length;i ++) {
            in_map.put(in[i], i);
        }
        return helper(pre, in, 0, 0, in.length - 1);
    }
    public TreeNode helper(int[] pre,int[] in,int preStart,int inStart,int inEnd) {
        if (preStart >= pre.length || inStart > inEnd) return null;
        TreeNode root = new TreeNode(pre[preStart]);
        // 获取根节点值在中序遍历中的位置
        int inIndex = in_map.get(root.val);
        // 递归的重建左子树和右子树,需要注意索引的起点和终点
        root.left = helper(pre, in, preStart + 1, inStart, inIndex - 1);
        root.right = helper(pre, in, preStart + (inIndex - inStart) + 1, inIndex + 1, inEnd);
        return root;
    }
}

5.用两个栈实现队列

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

1.解法一

  1. 使用 stack1 来取队尾元素,使用 stack2 来取队首元素

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

6.旋转数组的最小数字

题目描述: 把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个非递减排序的数组的一个旋转,输出旋转数组的最小元素。例如数组 {3,4,5,1,2}{1,2,3,4,5} 的一个旋转,该数组的最小值为 1。
NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

1.解法一:暴力解法,遍历整个数组,找到最小值

public class Solution {
    public int minNumberInRotateArray(int [] array) {
        // 暴力解法:出题人显然不想要这种答案,哈哈
        if (array == null || array.length == 0) return 0;
        int min = array[0];
        for (int i = 1;i < array.length;i ++) {
            if (array[i] < min) 
                min = array[i];
        }
        return min;
    }
}

2.解法二:双指针

  1. 根据数组的性质,先增,遇到一个间断点开始又增,所以从前遍历,遇到一个山顶,山顶下的元素就是最小值,从后往前遍历,遇到一个山峰,山峰前一个元素就是最小值
  2. 这种做法显然和暴力法差不多,最坏情况也需要遍历完整个数组(即最小数字在中间的情况)
public class Solution {
    public int minNumberInRotateArray(int [] array) {
        // 使用两个指针,一个从前往后遍历,一个从后往前
        if (array == null || array.length == 0) return 0;
        int front = 0, tail = array.length - 1;
        int min = array[0];
        while (front < tail) {     
            if (array[front] <= array[front + 1]) front ++;
            // 遇到山顶,山顶下为最小值
            else if (array[front] > array[front + 1]) {
                min = array[front + 1];
                break;
            }
            // 遇到山峰,山峰下为最小值
            if (array[tail] >= array[tail - 1]) tail --;
            else if (array[tail] < array[tail - 1]) {
                min = array[tail];
                break;
            }
        }
        return min;
    }
}

3. 二分查找:O(lg n)

  1. 如果右边比中间大,那么右边一定有序,最小肯定不会再右边(更新右边界)
  2. 如果右边小于中间,那么左边肯定有序,最小肯定不会再左边(更新左边界)
class Solution {
    public int minArray(int[] numbers) {
        // 二分查找
        int left = 0, right = numbers.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (numbers[right] > numbers[mid]) {
                // 不排除中间就是最小值的可能
                right = mid;
            } else if (numbers[right] < numbers[mid]) {
                // 排除掉中间就是最小值的可能
                left = mid + 1;
            } else {
                right --;
            }
        }
        // 左右相等了,返回这个最小值
        return numbers[left];
    }
}

7.斐波那契数列

使用传统递归有大量重复子问题,可能会超时

提目描述: 大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。n<=39

1. 解法一:动态规划

public class Solution {
    private int[] memo;
    public int Fibonacci(int n) {
        // 使用暴力法会有大量重复子问题,所以应该使用记忆化递归或者动态规划
        if (n <= 0) return 0;
        memo = new int[n + 1];
        memo[0] = 0;
        memo[1] = 1;
        for (int i = 2;i <= n;i ++) {
            memo[i] = memo[i - 1] + memo[i - 2];
        }
        return memo[n];         
    }
}

2.解法二:记忆化递归

  1. 与动态规划稍微不同的一点地方在于对于记忆数组的初始化,和初始边界条件的判断。
  2. 因为数组的初始化值全部是0,如果不加memo[2] = 1if (n == 1) return 1; 的判断,会导致递归函数计算recursive(0) 导致数组越界异常。
public class Solution {
    private int[] memo;
    public int Fibonacci(int n) {
        // 使用暴力法会有大量重复子问题,所以使用记忆化递归或者动态规划
        if (n <= 0) return 0;
        if (n == 1) return 1;
        memo = new int[n + 1];
        memo[0] = 0;
        memo[1] = 1;
        memo[2] = 1;
        return recursive(n);        
    }
    private int recursive(int n) {
        if (memo[n] != 0) return memo[n];
        memo[n] = recursive(n - 1) +recursive(n - 2);
        return memo[n];
    }
}

8.跳台阶

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

1.解法一:与斐波那契差不多的动态规划

  1. 与斐波那契稍有不同是对于 memo[0] 的初始化,对于跳台阶来说,跳上0级台阶也有 1 种方法
public class Solution {
    // 使用动态规划
    private int[] memo;
    public int JumpFloor(int target) {
        if (target <= 0) return 0;
        memo = new int[target + 1];
        memo[0] = 1;
        memo[1] = 1;
        for (int i = 2;i <= target;i ++) {
            memo[i] = memo[i - 1] + memo[i - 2];
        }
        return memo[target];
    }
}

9.变态跳台阶

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

1.解法一:动态规划

  1. 与跳台阶类似,稍有不同就是在循环里面对于 memo[i] 的计算,该题说第 n 阶台阶可以从第 0,1,2,,n-1 阶台阶跳上来,所以转化方程应该为 memo[n] = memo[0] + memo[1] + ...+ memo[n - 1]
public class Solution {
    // 动态规划
    private int[] memo;
    public int JumpFloorII(int target) {
        if (target <= 1) return 1;
        memo = new int[target + 1];
        memo[0] = 1;
        memo[1] = 1;
        for (int i = 2;i <= target;i ++) {
            // 前面所有台阶的方法求和
            for (int j = 0;j < i;j ++) {
                memo[i] += memo[j];
            }
        }
        return memo[target];
    }
}

2.解法二:记忆化递归

  1. 实现思路同上,和跳台阶的记忆化递归思路一致,多一步求和
public class Solution {
    // 动态规划
    private int[] memo;
    public int JumpFloorII(int target) {
        if (target <= 0) return 0;
        if (target == 1) return 1;
        memo = new int[target + 1];
        memo[0] = 1;
        memo[1] = 1;
        return recursive(target);
    }
    private int recursive(int n) {
        if (memo[n] != 0) return memo[n];
        else {
            for (int j = 0;j < n;j ++) {
                memo[n] += recursive(j);
            }
        }
        return memo[n];
    }
}

10.矩形覆盖(思路非常重要)

1.解法一:理解题意 -》转化为斐波那契数列

1. 解题思路

我们以 2 x 8 的矩形为例

  1. 先把 2 x 8 的覆盖方法记为 f(8),用 1 x 2 的小矩形去覆盖时,有两种选择:横着放或者竖着放。
  2. 当竖着放时:

  1. 当横着放时:

  1. 综上所述:f(8) = f(7) + f(6) ,可以看出为一个斐波那契数列
  2. 初始化值:f(1) = 1, f(2) = 2
public class Solution {
    // 抽象一下,这个题依然是斐波拉契
    private int[] memo;
    public int RectCover(int target) {
        if (target <= 1) return target;
        memo = new int[target + 1];
        memo[1] = 1;
        memo[2] = 2;
        for (int i = 3;i <= target;i ++) {
            memo[i] = memo[i - 1] + memo[i - 2];
        }
        return memo[target];
    }  
}

2.优化:对动态规划空间复杂度的优化,上面跳台阶等使用动态规划解法的在一维空间上的可以使用下述解法优化空间复杂度
  1. 因为上面的解法使用额外的数组 memo[n + 1] ,造成空间的浪费
  2. 我们使用两个整型代表计算中的两个结果,然后往后遍历的时候时刻更新这两个值
  3. 使用动态规划时,可以避免,如下解法二

2.解法二:优化空间复杂度为O(1)

public class Solution {
    public int RectCover(int target) {
        if (target <= 2) return target;
        // 仅使用两个整型来记录结果
        int first = 1, second = 2, res = 0;
        for (int i = 3;i <= target;i ++) {
            res = first + second;
            first = second;
            second = res;
        }
        return res;
    }  
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值