本文例子完整源码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword

《【好书推荐】《剑指Offer》之软技能》

《【好书推荐】《剑指Offer》之硬技能(编程题1~6)》

《【好书推荐】《剑指Offer》之硬技能(编程题7~11)》

持续更新,敬请关注公众号:coderbuff,回复关键字“sword”获取相关电子书。

12.矩阵中的路径

题目:请设计一个函数,用来判断一个矩阵中是否存在一条包含其字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。

*回溯法:适合由多个步骤组成的问题,并且每个步骤有多个选项。

 /**
* 矩阵中是否存在给定路径
* @author OKevin
* @date 2019/6/4
**/
public class Solution { /**
*
* @param matrix 一位数组表示矩阵
* @param rows 行数
* @param cols 列数
* @param path 路径
* @return true-存在;false-不存在
*/
public boolean findPath(char[] matrix, Integer rows, Integer cols, char[] path) {
if (matrix == null || rows <= 0 || cols <= 0 || path == null) {
return false;
}
boolean[] visited = new boolean[rows * cols];
int pathLength = 0;
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
if (findPathCore(matrix, rows, cols, row, col, path, pathLength, visited)) {
return true;
}
}
}
return false;
} private boolean findPathCore(char[] matrix, Integer rows, Integer cols, int row, int col, char[] path, int pathLength, boolean[] visited) {
if (pathLength == path.length) {
return true;
}
if (row >= 0 && row < rows && col >= 0 && col < cols && matrix[row * cols + col] == path[pathLength] && !visited[row * cols + col]) {
visited[row * cols + col] = true;
pathLength++;
if (findPathCore(matrix, rows, cols, row, col - 1, path, pathLength, visited)
|| findPathCore(matrix, rows, cols, row - 1, col, path, pathLength, visited)
|| findPathCore(matrix, rows, cols, row, col + 1, path, pathLength, visited)
|| findPathCore(matrix, rows, cols, row + 1, col, path, pathLength, visited)) {
return true;
}
visited[row * cols + col] = false; }
return false;
}
}

13.机器人的运动范围

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

此题有一个小的点需要靠平时的积累,数位和的计算。

 /**
* 计算数位和
* 例如:85的数位和为8+5=13
* 计算过程:
* 85 % 10 = 5(个位)
* 85 / 10 = 8(移除个位)
* 8 % 10 = 8(十位)
* 5 + 8 = 13
* @param number 数字
* @return 数位和
*/
private int getDigitSum(int number) {
int sum = 0;
while (number > 0) {
sum += number % 10;
number /= 10;
}
return sum;
}

另外还需要注意几个临界条件:

  1. 访问的行和列一定是大于等于0;

  2. 访问的行和列一定是小于总行数和总列数(并不是小于等于,因为是从第0行开始)

  3. 行和列的数位和小于阈值

  4. 没有被访问过

row >= 0 && row < rows && col >= 0 && col < cols && (getDigitSum(row) + getDigitSum(col) < threshold) && !visited[row * cols + col]

题目中看似提到了m行n列,立马想到了用二维数字来表示。实际上如果用二维数组是增加了复杂性,用一维数组同样能表示出二维数组。例如:m行n列就一共又m*n个元素,visited[m*n]。访问第1行第1列,在一维数组中则为visited[1*m+1],访问第1行第2列则为visited[1*m+2],也就是在一位数组中,数据是按照一列一列存放的。如果要访问第2行是2*cols+第几列。

另外既然需要求出达到多少个格子,则是需要访问格子周围即:(i - 1, j)、(i, j - 1)、(i + 1, j)、(i, j + 1)。

 /**
* Description:
* 机器人的运动范围
* 2019-06-18
* Created with OKevin.
*/
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];
int count = movingCountCore(threshold, rows, cols, 0, 0, visited);
return count;
} private int movingCountCore(int threshold, int rows, int cols, int row, int col, boolean[] visited) {
int count = 0;
if (check(threshold, rows, cols, row, col, visited)) {
visited[row * cols + col] = true;
/**
* 当前访问到了(i, j)坐标,此时则继续访问(i - 1, j)、(i, j - 1)、(i + 1, j)、(i, j + 1)
*/
count = 1 + movingCountCore(threshold, rows, cols, row - 1, col, visited) + movingCountCore(threshold, rows, cols, row, col-1, visited) + movingCountCore(threshold, rows, cols, row + 1, col, visited) + movingCountCore(threshold, rows, cols, row + 1, col, visited);
}
return count;
} private boolean check(int threshold, int rows, int cols, int row, int col, boolean[] visited) {
//横坐标与纵坐标的数位和相加小于阈值,且没有访问过
if (row >= 0 && row < rows && col >= 0 && col < cols && (getDigitSum(row) + getDigitSum(col) <= threshold) && !visited[row * cols + col]) {
return true;
}
return false;
} /**
* 计算数位和
* 例如:85的数位和为8+5=13
* 计算过程:
* 85 % 10 = 5(个位)
* 85 / 10 = 8(移除个位)
* 8 % 10 = 8(十位)
* 5 + 8 = 13
* @param number 数字
* @return 数位和
*/
private int getDigitSum(int number) {
int sum = 0;
while (number > 0) {
sum += number % 10;
number /= 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。

这道题是求解最优化问题。理论上讲,在题目中出现最大、最小、一共有多少种解法都可以用动态规划求解。

解法一:动态规划

拿到这道题,习惯性的可能会先从由上往下的解题思路去想,比如:长度为9,可以分为几段:1,1,7;1,2,6等等。会去思考这个长度会分成几个段,再将每个段的乘积求出来,取最大的那个段。

但实际上,对于求解最优化问题,可以转换为一系列子问题。对于本题一段绳子来讲,它无论如何都至少被切为2段。例如长度为8时,可能被切为:1,7;2,6;3,5;4,4。当然还有5,3,这实际上又和前面重复了,所以一段绳子如果被切为2段,就只有n/2种可能性。

切为2段并不是最终的最大乘积长度,例如8切为了以上4种可能性的两段,并不意味着8的切成m段的最大乘积长度为15(3*5)。它当然还能切为2*3*3=18。那为什么说只需要切为2段呢?

这是因为我们需要把这个问题不断地划分为小的问题。

例如8被切为了1和7,这两段不能再继续切分,它就是最小的问题;同理,8被切为了2和6,但是6仍然可以继续被切为1和5,2和4,3和3,所以2和6并不是最小的问题,以此类推,最终推出长度为6的绳子切成m段的最大乘积是9(3*3),那么8被切为2和6时,2*9就等于18。同理继续推3和5,4和4。

上面的分析得出了什么样的结论呢?结论就是,只需要想象成2段,再各自继续切2段。也就是说假设长度为n的绳子,f(n)是它的各段最大乘积长度,它在被切第一刀时,第一段长度为(1,2,...n-1),第二段的长度为(n-1,n-2,...,1)。推出f(n)=max(f(i)*f(n-1))的关联关系。这里一定需要好好理解,切成2段后,并不是直接将两段相乘,而是再继续将各段切分直至不能再切且取最大乘积长度

在《算法笔记》(刁瑞 谢妍著)一书中对动态规划做了求解步骤的总结:

  1. 定义子问题

  2. 定义状态转换规则,即递推关系

  3. 定义初始状态

套用到这套题上,我认为就是需要明确以下3点:

  1. 该问题的核心在于求出每段的最大乘积长度,这是子问题,也就是上文所述,再被切为两段时,需要明确是否能继续切直至不能再切且取最大乘积长度。

  2. 递推关系,也已明确(n)=max(f(i)*f(n-1))

  3. 初始状态,长度为1不能切,长度为2最长为1,长度为3最长为2。

 /**
* Description:
* 剪绳子——动态规划
* 2019-06-19
* Created with OKevin.
*/
public class Solution1 { public int maxProductAfterCutting(int length) {
if (length < 2) {
return 0;
}
if (length == 2) {
return 1;
}
if (length == 3) {
return 2;
}
int[] products = new int[length + 1]; //数组中存储的是每段的最优解
//大于长度3的绳子,当然可以划分出1,2,3长度的绳子
products[0] = 0;
products[1] = 1;
products[2] = 2;
products[3] = 3;
int max = 0;
for (int i = 4; i <= length; i++) {
max = 0;
for (int j = 1; j <= i / 2; j++) { //除以2的原因在上文中也以提到,将一段绳子划分为2段时,实际上中间后的切分和前面是重复的
int product = products[j] * products[i - j]; //递推关系f(i)*f(n-1)
if (max < product) {
max = product;
}
products[i] = max;
}
}
max = products[length];
return max;
}
}

优点:动态规划类似于分治算法,将大的问题逐步划分为小的问题求解。

缺点:此题采用动态规划的时间复杂度为O(n^2),且空间复杂度为O(n)

解法二:贪婪算法

贪婪算法的核心是,先挑最大的,再挑比较大的,再挑小的(贪婪嘛)。

本题对于长度为n(n>=5)的绳子应尽量多划分为长度3的段。对于长度为4的段,应划分为长度为2的段。

也即是,如果长度为10,那么10/3=3个长度为3的段,划分结果为3*3*3*1,最后一个段为1,划分为3*3*4。

 /**
* Description:
* 剪绳子——贪婪算法
* 2019-06-20
* Created with OKevin.
*/
public class Solution2 {
public int maxProductAfterCutting(int length) {
if (length < 2) {
return 0;
}
if (length == 2) {
return 1;
}
if (length == 3) {
return 2;
}
int timesOf3 = length / 3;
if (length - timesOf3 * 3 == 1) {
timesOf3 -= 1;
}
int timesOf2 = (length - timesOf3*3) / 2;
return (int) (Math.pow(3, timesOf3) * Math.pow(2, timesOf2));
}
}

15.二进制中1的个数

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

此题可采用移位运算+与运算求解

 /**
* Description:
* 移位运算+与运算
* 2019-06-20
* Created with OKevin.
*/
public class Solution {
public int NumberOf1(int num) {
int count = 0;
while (num != 0) {
if ((num & 1) == 1) {
count++;
}
num = num >>> 1; //因为运算>>>表示无符号右移,意味着如果是负数,仍然会向右移,同时用0补齐。如果使用>>有符号右移,那么符号位1永远会存在,也就是会产生死循环
}
return count;
}
}

16.数值的整数次方

题目:实现函数Math.pow,求m的n次方。

循环暴力法

 /**
* Description:
* 循环暴力法
* 2019-06-20
* Created with OKevin.
*/
public class Solution1 {
public int pow(int m, int n) {
int result = 1;
for (int i = 0; i < n; i++) {
result *= m;
}
return result;
}
}

很遗憾,这种解法连校招级都算不上,顶多算是刚学习编程时的水平。

其实这道题,并没有考查过多的算法,更多的是考查对细节的把握。一个数的整数次方,不光是整数,还有可能是负数,也有可能是0。如果数值为0,则0的幂是没有意义的。

 /**
* Description:
* 考虑指数为0,负数,整数;数值为0的情况;0^0在数学上没有意义
* 2019-06-21
* Created with OKevin.
*/
public class Solution2 { public double pow(int m, int n) {
double result = 0;
if (m == 0 && n < 0) {
return -1;
}
int absN = Math.abs(n); //取绝对值
result = calc(m, absN);
if (n < 0) {
result = 1 / result;
}
return result;
} private int calc(int m, int n) {
int result = 1;
for (int i = 0; i < n; i++) {
result *= m;
}
return result;
}
}

改进后的代码考虑到了指数是负数的情况。但实际上这仍然有优化的空间。如果指数是32,意味着calc方法需要循环31次。然而实际上循环到一半的时候就可以求它本身。也就是说a^n/2 * a^n/2,n为偶数;a^(n-1)/2 * a^(n-1)/2 * a,n为奇数。

改进后的calc方法:

 private int calc(int m, int n) {
if (n == 0) {
return 1;
}
if (n == 1) {
return m;
}
int result = calc(m, n >> 1); //右移1位表示除以2
result *= result;
if ((m & 1) == 1) { //位运算判断是会否为奇数,奇数的二进制第一位一定是1与1做与运算即可判断是否为奇数,代替m%2是否等于0
result *= m;
}
return result;
}

本文例子完整源码地址:https://github.com/yu-linfeng/BlogRepositories/tree/master/repositories/sword

《【好书推荐】《剑指Offer》之软技能》

《【好书推荐】《剑指Offer》之硬技能(编程题1~6)》

《【好书推荐】《剑指Offer》之硬技能(编程题7~11)》

持续更新,敬请关注公众号:coderbuff,回复关键字“sword”获取相关电子书。

这是一个能给程序员加buff的公众号 (CoderBuff)

【好书推荐】《剑指Offer》之硬技能(编程题12~16)的更多相关文章

  1. 剑指offer(41-45)编程题

    41.入一个递增排序的数组和一个数字S,在数组中查找两个数,是的他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的. class Solution { public: vector&l ...

  2. 剑指offer(36-40)编程题

    两个链表的第一个公共结点 数字在排序数组中出现的次数 二叉树的深度 平衡二叉树 数组中只出现一次的数字 36.输入两个链表,找出它们的第一个公共结点. class Solution1 { public ...

  3. (4)剑指Offer之链表相关编程题

    一 链表中倒数第k个节点 题目描述: 输入一个链表,输出该链表中倒数第k个结点 问题分析: 一句话概括: 两个指针一个指针p1先开始跑,指针p1跑到k-1个节点后,另一个节点p2开始跑,当p1跑到最后 ...

  4. 剑指offer——python【第54题】字符流中第一个不重复的字符

    题目描述 请实现一个函数用来找出字符流中第一个只出现一次的字符.例如,当从字符流中只读出前两个字符"go"时,第一个只出现一次的字符是"g".当从该字符流中读出 ...

  5. 剑指offer——python【第21题】栈的压入、弹出序列

    题目描述 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序.假设压入栈的所有数字均不相等.例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压 ...

  6. 《剑指offer》第十九题(正则表达式匹配)

    // 面试题19:正则表达式匹配 // 题目:请实现一个函数用来匹配包含'.'和'*'的正则表达式.模式中的字符'.' // 表示任意一个字符,而'*'表示它前面的字符可以出现任意次(含0次).在本题 ...

  7. 剑指offer——python【第59题】按之子形顺序打印二叉树

    题目描述 请实现一个函数按照之字形打印二叉树,即第一行按照从左到右的顺序打印,第二层按照从右至左的顺序打印,第三行按照从左到右的顺序打印,其他行以此类推. 解题思路 这道题其实是分层打印二叉树的进阶版 ...

  8. 剑指offer——python【第60题】把二叉树打印成多行

    题目描述 从上到下按层打印二叉树,同一层结点从左至右输出.每一层输出一行.#类似于二维列表[[1,2],[4,5]] 解题思路 其实这倒题和其他类似的题有所区别,这里是分层打印,把每层的节点值放在同一 ...

  9. 剑指offer——python【第39题】平衡二叉树

    题目描述 输入一棵二叉树,判断该二叉树是否是平衡二叉树.   解题思路 平衡二叉树首先是二叉搜索树,且它每个节点的左子树和右子树高度差至多等于1:只要从根节点,依次递归判断每个节点是否满足如上条件即可 ...

随机推荐

  1. 模板引擎Thymeleaf

    1.Thymeleaf简介 Thymeleaf 是一个跟 Velocity.FreeMarker 类似的模板引擎,它可以完全替代 JSP .相较与其他的模板引擎,它有如下三个极吸引人的特点 Thyme ...

  2. luogu P3805 【模板】manacher算法

    题目描述 给出一个只由小写英文字符a,b,c...y,z组成的字符串S,求S中最长回文串的长度. 字符串长度为n 输入格式 一行小写英文字符a,b,c...y,z组成的字符串S 输出格式 一个整数表示 ...

  3. The file “XXX.app” couldn’t be opened because you don’t have permission to view it.问题修复

    出现下列问题 怎么解决呢 如图 将info.plist的文件中的Executable.file中的文件修改为:$(PRODUCT_NAME) 重新编译 com+R不出意外的话 运行成功 ok 解决~

  4. HYSBZ-2002弹飞绵羊

    某天,Lostmonkey发明了一种超级弹力装置,为了在他的绵羊朋友面前显摆,他邀请小绵羊一起玩个游戏.游戏一开始,Lostmonkey在地上沿着一条直线摆上n个装置,每个装置设定初始弹力系数ki,当 ...

  5. python 金融应用(三)数据可视化

    matplotlib 库( http://www.matp1otlìb.org )的基本可视化功能. 主要是2-D绘图.金融绘图和3-D绘图 一.2-D绘图 1.1一维数据集 #导入所需要的包impo ...

  6. GlusterFS 存储

    GlusterFS简介: 互联网四大开源分布式文件系统分别是:MooseFS.CEPH.Lustre.GusterFS. GluterFS最早由Gluster公司开发,其目的是开发一个能为客户提供全局 ...

  7. python之pymysql模块简单应用

    众所周知,想要在python程序中执行SQL语句需要使用第三方模块:pymysql. 下面,我将为大家简述一下pymysql第三方库的安装到使用的大体流程. pymysql的安装 1.windows系 ...

  8. IDEA2019.3安装和激活

    1.JetBrain网站下载它旗舰版,有zip也有exe, 我下载的zip包,752M 2.解压到自己系统目录下,比如:  D:\ideaIU-2019.3.win 3.去导bin目录下,将idea6 ...

  9. 201871010119-帖佼佼《面向对象程序设计(java)》第7周学习总结

    博文正文开头格式:(2分) 项目 内容 这个作业属于哪个课程 https://www.cnblogs.com/nwnu-daizh/ 这个作业的要求在哪里 https://www.cnblogs.co ...

  10. 1篇文章搞清楚8种JVM内存溢出(OOM)的原因和解决方法

    前言 撸Java的同学,多多少少会碰到内存溢出(OOM)的场景,但造成OOM的原因却是多种多样. 堆溢出 这种场景最为常见,报错信息: java.lang.OutOfMemoryError: Java ...