剑指offer——java刷题总结【七】

Note

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

目录

题号题目名称
61序列化二叉树
62二叉搜索树的第k个节点
63数据流中的中位数
64滑动窗口的最大值
65矩阵中的路径
66机器人的运动范围
67剪绳子

正文

61、序列化二叉树
题目描述

请实现两个函数,分别用来序列化和反序列化二叉树。
二叉树的序列化是指:把一棵二叉树按照某种遍历方式的结果以某种格式保存为字符串,从而使得内存中建立起来的二叉树可以持久保存。序列化可以基于先序、中序、后序、层序的二叉树遍历方式来进行修改,序列化的结果是一个字符串,序列化时通过某种符号表示空节点(#),以 !表示一个结点值的结束(value!)。
二叉树的反序列化是指:根据某种遍历顺序得到的序列化字符串结果str,重构二叉树。

题目分析

解法一: 我们根据前序遍历序列来序列化二叉树,因为前序遍历序列是从根结点开始的,且遍历顺序是中左右,方便我们依次往后遍历字符串数组,并依次生成左子树和右子树。当在遍历二叉树过程中碰到Null指针时,这些Null指针被序列化为一个特殊的字符“#”,结点之间用感叹号隔开。首先将序列化后的字符串根据分隔符分隔成字符串数组,使用一个全局变量来标识当前遍历到哪个字符。

代码实现

解法一:

String Serialize(TreeNode root) {
    if (root == null) return "#!";
    return root.val + "!" + Serialize(root.left) + Serialize(root.right);
}

TreeNode Deserialize(String str) {
    String[] strings = str.split("!");
    return helper(strings);
}

int index = -1;
TreeNode helper(String[] strings) {
    index++;
    if (index >= strings.length) return null;
    TreeNode tNode = null;
    if (!strings[index].equals("#")) {
        tNode = new TreeNode(Integer.parseInt(strings[index]));
        tNode.left = helper(strings);
        tNode.right = helper(strings);
    }
    return tNode;
}
62、二叉搜索树的第k个节点
题目描述

给定一棵二叉搜索树,请找出其中的第k小的结点。例如,(5,3,7,2,4,6,8)中,按结点数值大小顺序第三小结点的值为4。

题目分析

解法一: 递归法。因为二叉搜索树的中序遍历是有序递增的,所以可以使用递归版的中序遍历对二叉搜索树进行遍历,当遍历到第k个节点时则返回。

代码实现

解法一:

int index = -1;
TreeNode res = null;
TreeNode KthNode(TreeNode pRoot, int k) {
    if (pRoot == null) return null;
    index = k;
    inOrderTraverse(pRoot);
    return res;
}

void inOrderTraverse(TreeNode root) {
    if (root != null) {
        inOrderTraverse(root.left);
        index--;
        if (index == 0) {
            res = root;
            return;
        }
        inOrderTraverse(root.right);
    }
}
63、数据流中的中位数
题目描述

如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。我们使用Insert()方法读取数据流,使用GetMedian()方法获取当前读取数据的中位数。

题目分析

解法一: 优先队列法。设置一个小根堆和一个大根堆,在Java中使用PriorityQueue即可实现,默认是小根堆,本解法通过lambda表达式重写了比较器,实现大根堆,大根堆和小根堆各放一半的数据即可,保证大根堆中的数据量永远比小根堆中的数据量相等或多一个。当两个堆大小一样时,两个堆顶元素的平均数即为中位数;当两个堆大小不等时,由于大根堆中的元素数量会多一个,所以大根堆的堆顶元素即为中位数。在数据插入堆时,为了保证该数据放在合理的堆中,可以通过先插入大根堆,然后再将大根堆的堆顶元素取出插入小根堆的方式,使得该数据放在应有的位置。每次调整的时间复杂度为O(logn)。

代码实现

解法一: O(logn)

PriorityQueue<Integer> lHeap = new PriorityQueue<>((a, b) -> b - a);  //大根堆
PriorityQueue<Integer> sHeap = new PriorityQueue<>();  //小根堆
void Insert(int num) {
    lHeap.add(num);
    sHeap.add(lHeap.remove());
    if (sHeap.size() > lHeap.size()) {
        lHeap.add(sHeap.remove());
    }
}

Double GetMedian() {
    if (sHeap.size() == lHeap.size()) {
        return (sHeap.peek() + lHeap.peek()) / 2.0;
    } else {
        return (double) lHeap.peek();
    }
}
64、滑动窗口的最大值
题目描述

给定一个数组和滑动窗口的大小,找出所有滑动窗口里数值的最大值。例如,如果输入数组{2,3,4,2,6,2,5,1}及滑动窗口的大小3,那么一共存在6个滑动窗口,他们的最大值分别为{4,4,6,6,6,5}; 针对数组{2,3,4,2,6,2,5,1}的滑动窗口有以下6个:{[2,3,4],2,6,2,5,1},{2,[3,4,2],6,2,5,1},{2,3,[4,2,6],2,5,1},{2,3,4,[2,6,2],5,1},{2,3,4,2,[6,2,5],1}, {2,3,4,2,6,[2,5,1]}。

题目分析

解法一: 优先队列法。维护一个大小为size的大根堆,对数组进行遍历,每次将窗口头部元素移除并将窗口尾部元素加入堆中,然后将堆顶元素返回即可。

代码实现

解法一: O(nlogk)

public static ArrayList<Integer> maxInWindows(int [] num, int size) {
        PriorityQueue<Integer> queue = new PriorityQueue<>((a, b) -> b - a);
        ArrayList<Integer> list = new ArrayList<>();
        if (num.length < size || size == 0) {
            return list;
        }
        for (int i = 0; i < size; i++) {
            queue.offer(num[i]);
        }
        list.add(queue.peek());
        for (int p1 = 0, p2 = size; p2 < num.length; p1++, p2++) {
            queue.remove(num[p1]);
            queue.offer(num[p2]);
            list.add(queue.peek());
        }
        return list;
}
65、矩阵中的路径
题目描述

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子。

题目分析

解法一: 回溯法。
1、根据给定数组,初始化一个标志位数组,false表示未走过,true表示已经走过。因为题目规定不能走进同一个格子两次;
2、根据行数和列数,遍历数组,先找到一个与str字符串的第一个元素相匹配的矩阵元素,进入judge,判断是否能够走通;
3、根据i和j确定当前点在一维数组matrix中的位置;
4、确定递归终止条件:越界、当前找到的矩阵值不等于数组对应位置的值、已经走过的点,这三类情况说明此路不通,返回false;
5、如果待判定字符串str的索引已经判断到了最后一位,此时说明匹配成功,返回true;
6、递归寻找当前格子周围的四个格子是否符合条件,只要有一条路满足条件则返回true;
7、如果周围的四个格子递归都没有找到路径,则说明当条路径寻找失败,将走过路径的数组flag重新标记为false。
此类问题都可以用回溯法解决,在递归过程中对数组越界进行判定是一种较为优雅的解法。

代码实现

解法一:

public boolean hasPath(char[] matrix, int rows, int cols, char[] str) {
    boolean[] flag = new boolean[matrix.length];
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (judge(matrix, i, j, rows, cols, str, 0, flag)) {
                return true;
            }
        }
    }
    return false;
}

public boolean judge(char[] matrix, int i, int j, int rows, int cols, char[] str, int k, boolean[] flag) {
    int index = i * cols + j;
    if (i < 0 || i >= rows || j < 0 || j >= cols || matrix[index] != str[k] || flag[index]) {
        return false;
    }
    if (k == str.length - 1) {
        return true;
    }
    flag[index] = true;
    if (judge(matrix, i - 1, j, rows, cols, str, k + 1, flag)
            || judge(matrix, i + 1, j, rows, cols, str, k + 1, flag)
            || judge(matrix, i, j - 1, rows, cols, str, k + 1, flag)
            || judge(matrix, i, j + 1, rows, cols, str, k + 1, flag)) {
        return true;
    }
    flag[index] = false;
    return false;
}
66、机器人的运动范围
题目描述

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

题目分析

解法一: 回溯法。与岛问题类似的解法。
1、根据给定的行列,创建并初始化二维数组,判断每一个位置是否满足小于threshold的要求,满足为1,不满足为0;
2、机器人从原点出发,开始递归。数组越界和数组当前值不为1说明此路走不通,返回能够到达的格子数0。注意:数组当前值不为1有两种情况——0或2。数组值为0说明该格子不满足题目中小于threshold的要求,数组值为2说明当前位置已经走过,不再重复走;
3、如果此路可以走通,则对该位置的值进行+1操作,用来标记该位置已经走过并计数;
4、对当前格子的上下左右方向能够到达的格子个数进行计数累加,并做+1操作,意味着当前格子可达。

代码实现

解法一:

public static int movingCount(int threshold, int rows, int cols) {
        int[][] matrix = new int[rows][cols];
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                matrix[i][j] = isAccess(threshold, i, j);
            }
        }
        return helper(matrix, 0, 0, rows, cols);
    }

    public static int helper(int[][] matrix, int i, int j, int rows, int cols) {
        if (i < 0 || i >= rows || j < 0 || j >= cols || matrix[i][j] != 1) {
            return 0;
        }
        matrix[i][j]++;
        return helper(matrix, i - 1, j, rows, cols)
                + helper(matrix, i + 1, j, rows, cols)
                + helper(matrix, i, j - 1, rows, cols)
                + helper(matrix, i, j + 1, rows, cols)
                + 1;
    }

    public static int isAccess(int threshold, int row, int col) {
        int count = 0;
        while (row != 0) {
            count += row % 10;
            row /= 10;
        }
        while (col != 0) {
            count += col % 10;
            col /= 10;
        }
        return count <= threshold ? 1 : 0;
    }
67、剪绳子
题目描述

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

题目分析

解法一: 动态规划。本解法采用非递归实现,节省空间。m[i]代表着当长度为i时能够剪成若干长度的最大乘积,其实就是对组成每个长度的可能性进行遍历,比如求解长度为10的绳子,可以划分成1+9、2+8、3+7、4+6、5+5这五个子问题,这五个子问题的最大值就是m[10],而子问题的解已经在之前已经被求解。所以可以直接使用for循环遍历的方式,而不需要采用递归求解,节省对某个值多次遍历的额外空间。而之所以要对子问题求max(i, m[i]),是因为题目要求至少对绳子剪一次,所以当绳子长度为10时,我们不能直接返回10,即结果不可能是它本身。但是在求后续问题的解中,m[i]只是一个子问题,可以不再进行划分,例如我们计算12时,需要2+10这两个子问题,而对于长度为10的这段绳子,我们可以不再对它进行剪切,而是把10作为一个乘数考虑进来。

代码实现

解法一: O(n²)

public int cutRope(int target) {
    if (target == 1) return 1;
    int[] m = new int[target + 1];
    m[1] = 1;
    for (int i = 2; i <= target; i++) {
        int max = m[i];
        for (int j = 1; j <= i / 2; j++) {
            max = Math.max(max, Math.max(j, m[j]) * Math.max(i - j, m[i - j]));
        }
        m[i] = max;
    }
    return m[target];
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值