写在前面
- 本系列包含《剑指Offer》66道算法题,预计一周刷完,这是第四篇。
系列汇总:剑指Offer 66题 Java 刷题笔记汇总 - 所有题目均可在牛客网在线编程平台进行调试。
网址:https://www.nowcoder.com/ta/coding-interviews - 本系列包含题目,解题思路及代码(Java)。
代码同步发布在GitHub:https://github.com/JohnnyJYWu/offer-Java
上一篇:算法 | 一周刷完《剑指Offer》 Day3:第27~37题
下一篇:算法 | 一周刷完《剑指Offer》 Day5:第50~60题
Day4:第38~49题
有几道题比较考逻辑和理解。到后面就简单了,好像更注重思维了。
- T38. 二叉树的深度
- T39. 平衡二叉树
- T40. 数组中只出现一次的数字
- T41. 和为S的连续正数序列
- T42. 和为S的两个数字
- T43. 左旋转字符串
- T44. 翻转单词顺序列
- T45. 扑克牌顺子
- T46. 孩子们的游戏(圆圈中最后剩下的数)
- T47. 求1+2+3+...+n
- T48. 不用加减乘除做加法
- T49. 把字符串转换成整数
T38. 二叉树的深度
题目描述
输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。
解题思路
分别对左右子树递归计算深度,取深度更大的一个。
public int TreeDepth(TreeNode root) {
if(root == null) return 0;
return 1//当前结点深度1
+ Math.max(TreeDepth(root.left), TreeDepth(root.right));//左右子树取深度更大的值
}
T39. 平衡二叉树
题目描述
输入一棵二叉树,判断该二叉树是否是平衡二叉树。
解题思路
平衡二叉树:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
此题即将上一题计算左右子树深度【取更大】的思想转化为了计算左右子树深度【判断差值是否不超过1】。
public boolean IsBalanced_Solution(TreeNode root) {
if(root == null) return true;
return getDepth(root) != -1;
}
private int getDepth(TreeNode root) {
if(root == null) return 0;
int left = getDepth(root.left);
if(left == -1) return -1;
int right = getDepth(root.right);
if(right == -1) return -1;
return Math.abs(left - right) > 1 ? -1 : 1 + Math.max(left, right);
}
T40. 数组中只出现一次的数字
题目描述
一个整型数组里除了两个数字之外,其他的数字都出现了偶数次。请写程序找出这两个只出现一次的数字。
解题思路
重点:注意题意,两个数只出现一次,其他数都出现偶数次。
方法一:HashSet。不包含则加入,包含则移除。最终出现偶数次的数一定都会被移除,仅留只出现了一次的数。
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public void FindNumsAppearOnce(int[] array, int[] num1, int[] num2) {//HashSet
Set set = new HashSet<>();
for(int num: array) {
if(set.contains(num)) {
set.remove(num);
} else {
set.add(num);
}
}
Iterator it = set.iterator();
num1[0] = it.next();
num2[0] = it.next();
}
方法二:异或运算。此方法比较巧妙,需仔细理解。
首先理解二进制位运算的几个概念:
与运算( & ):运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
或运算( | ):0|0=0; 0|1=1; 1|0=1; 1|1=1;
异或运算( ^ ):运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;
异或运算有此特性:n^n=0;即任意数与本身异或得0;
所以对array数组所有数进行异或运算,得到的结果为两个仅出现一次的数异或运算的结果different(其他数由于出现偶数次,异或运算后都为0了),这个different是区分这两个数不同的标志。
然后,与运算有此特性:n&-n取得n的二进制表示中最右边的1。(-n的表示百度补码)
对different进行与运算:different = different & (-different)。此时的different就成为了那个标志。
最后再对array数组所有数进行一次异或运算,不同的是这一次要根据different将两个数区分开,填入结果。
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public void FindNumsAppearOnce(int[] array, int[] num1, int[] num2) {//位运算
int different = 0;
for(int num: array) {//最终得到两个只出现一次的数相异的的结果
different ^= num;
}
//得到最右边的1
different &= - different;
for(int num: array) {
//由于different是那两个数像异的结果,那么这个取得的最右边的1,可将两个只出现一次的数区分开
//然后再进行一遍异运算即可
if((num & different) == 0) {
num1[0] ^= num;
} else {
num2[0] ^= num;
}
}
}
方法三:暴力解,不多赘述了。
//num1,num2分别为长度为1的数组。传出参数
//将num1[0],num2[0]设置为返回结果
public void FindNumsAppearOnce(int[] array, int[] num1, int[] num2) {//暴力解法
ArrayList list = new ArrayList();
Arrays.sort(array);
int length = array.length;
for(int i = 0; i < length; i ++){
if(i == length - 1 && array[i] != array[i - 1]) {
list.add(array[i]);
}else if(i == 0 && array[i] != array[i + 1]) {
list.add(array[i]);
}else{
if(i != 0 && i != length - 1 && array[i] != array[i - 1] && array[i] != array[i + 1]) {
list.add(array[i]);
}
}
}
num1[0] = list.get(0);
num2[0] = list.get(1);
}
T41. 和为S的连续正数序列
题目描述
小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!
输出描述:输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序
解题思路
夹逼思想,定义正数序列的左边界small和右边界big,求small到big的和。和比所求小则big后移,比所求大则small后移。
public ArrayList> FindContinuousSequence(int sum) {//算法,夹逼
ArrayList> result = new ArrayList<>();
if(sum <= 1) return result;
int small = 1;
int big = 2;
while(small < (sum + 1) / 2) {//要求最少是两个数字,所以small最大为(s+1)/2
int curSum = 0;//求small到big的和
for(int i = small; i <= big; i ++) {
curSum += i;
}
//夹逼思想,小则big后移,大则small后移
if(curSum < sum) {
big ++;
} else if(curSum > sum) {
small ++;
} else {
ArrayList list = new ArrayList<>();
for(int i = small; i <= big; i ++) {
list.add(i);
}
result.add(list);
small ++;
}
}
return result;
}
公式求解。
public ArrayList> FindContinuousSequence(int sum) {//公式
ArrayList> result = new ArrayList<>();
if(sum < 3) return result;
for(int i = 1; i <= sum / 2; i ++) {
int value = 1 + 4 * i * i - 4 * i + 8 * sum;
int valueSqrt = (int) Math.sqrt(value);
if(value >= 25 && valueSqrt * valueSqrt == value) {
ArrayList list = new ArrayList();
for(int j = i; j <= (valueSqrt - 1) >> 1; j ++) {
list.add(j);
}
result.add(list);
}
}
return result;
}
T42. 和为S的两个数字
题目描述
输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
输出描述:对应每个测试案例,输出两个数,小的先输出。
解题思路
夹逼思想,且和相等时,差越大乘积越小。因此从两端向中间夹逼时第一个和为S的即为所求。
public ArrayList FindNumbersWithSum(int[] array, int sum) {
//夹逼思想,且和相等时,差越大乘积越小
ArrayList result = new ArrayList<>();
int small = 0;//数组下标,从前往后移
int big = array.length - 1;//数组下标,从后往前移
while(small <= big) {
int curSum = array[small] + array[big];
if(curSum < sum) {
small ++;
} else if(curSum > sum) {
big --;
} else {
result.add(array[small]);
result.add(array[big]);
return result;
}
}
return result;
}
T43. 左旋转字符串
题目描述
汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!
解题思路
分三步走。(交换步骤顺序也可)
step1:先将左边3个字符串进行翻转:[abc]XYZdef --> [cba]XYZdef
step2:再将右边剩余字符串进行翻转:cba[XYZdef] --> cba[fedZYX]
step3:最后将整个字符串进行翻转: cbafedZYX --> XYZdefabc
public String LeftRotateString(String str, int n) {
if(str == null || str.length() == 0) return "";
char[] chars = str.toCharArray();
//step1:先将左边3个字符串进行翻转:[abc]XYZdef --> [cba]XYZdef
reverse(chars, 0, n - 1);
//step2:再将右边剩余字符串进行翻转:cba[XYZdef] --> cba[fedZYX]
reverse(chars, n, chars.length - 1);
//step3:最后将整个字符串进行翻转: cbafedZYX --> XYZdefabc
reverse(chars, 0, chars.length - 1);
return new String(chars);
}
private void reverse(char[] chars, int start, int end) {
while(start < end) {
char tmp = chars[start];
chars[start] = chars[end];
chars[end] = tmp;
start ++;
end --;
}
}
T44. 翻转单词顺序列
题目描述
牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?
解题思路
原理同上一题。先翻转每个单词的顺序,再翻转整个句子的顺序。(交换步骤顺序也可)
public String ReverseSentence(String str) {//原理同T43
if(str == null || str.length() == 0) return str;
char[] chars = str.toCharArray();
int length = chars.length;
int startIndex = 0;//单词开始标记
int endIndex = 0;//单词结束标记
//翻转每个单词的字母顺序
while(endIndex <= length) {
if(endIndex == length || chars[endIndex] == ' ') {//遇到空格或到句末,翻转单词
reversed(chars, startIndex, endIndex - 1);
startIndex = endIndex + 1;
}
endIndex ++;
}
//翻转整个句子的顺序
reversed(chars, 0, length - 1);
return new String(chars);
}
private void reversed(char[] chars, int start, int end) {
while(start < end) {
char tmp = chars[start];
chars[start] = chars[end];
chars[end] = tmp;
start ++;
end --;
}
}
T45. 扑克牌顺子
题目描述
LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张_)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。
解题思路
先对数组排序,计算大小王(癞子)数量。然后计算剩下的数两两的差值减1即为需要用癞子替代的张数。题那么长都是废话,别想太多正常找就行。
public boolean isContinuous(int[] numbers) {
if(numbers == null || numbers.length < 5) return false;
Arrays.sort(numbers);
int sum0 = 0;//大小王数量
for(int i = 0; i < numbers.length; i ++) {
if(numbers[i] == 0) {
sum0 ++;
}
}
for(int i = sum0; i < numbers.length - 1; i ++) {
if(numbers[i + 1] == numbers[i]) return false;//有相等的牌不可能为顺子
int interval = numbers[i + 1] - numbers[i] - 1;//这两个数之间差了几张牌,需要用大小王代替
if(interval > sum0) return false;//相差太大,大小王不够
sum0 -= interval;
}
return true;
}
T46. 孩子们的游戏(圆圈中最后剩下的数)
题目描述
每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!_)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)
解题思路
又是一堆废话,丢手绢游戏。。。
约瑟夫环,公式:
n = 1: f(n, m) = 0
n > 1: f(n, m) = [f(n - 1, m) + m] % n
public int LastRemaining_Solution(int n, int m) {//约瑟夫环
//公式:f(n, m) = 0 (n = 1)
//f(n, m) = [f(n - 1, m) + m] % n (n > 1)
if(n == 0) return -1;
if(n == 1) return 0;
return (LastRemaining_Solution(n - 1, m) + m) % n;
}
T47. 求1+2+3+...+n
题目描述
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
解题思路
递归相加。此题关键在于如何跳出递归,基本方向是采用逻辑与或的方式来计算,与的时候通过n>0来短路,这样在n=0的时候不需要计算递归的值,或的时候通过n==0来短路,在n=0的时候可以短路逻辑或运算。
public int Sum_Solution(int n) {
int sum = n;
//boolean b = (n > 0) && (sum += Sum_Solution(n - 1)) > 0;
boolean b = (n == 0) || (sum += Sum_Solution(n - 1)) > 0;
return sum;
}
T48. 不用加减乘除做加法
题目描述
写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。
解题思路
位运算。a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进位。
public int Add(int num1, int num2) {
while(num2 != 0) {
int tmp = num1 ^ num2;
num2 = (num1 & num2) << 1;
num1 = tmp;
}
return num1;
}
T49. 把字符串转换成整数
题目描述
将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。
输入描述:
输入一个字符串,包括数字字母符号,可以为空
输出描述:
如果是合法的数值表达则返回该数字,否则返回0
解题思路
正常转换即可。
注意:正负号的存在,字符型代表的整形在转换过程中的计算。
public int StrToInt(String str) {
if(str.length() == 0) return 0;
char[] chars = str.toCharArray();
boolean isNegative = chars[0] == '-';//判断是否有负号
int result = 0;
for(int i = 0; i < chars.length; i ++) {
if(i == 0 && (chars[i] == '+' || chars[i] == '-')) continue;//跳过正负号
if(chars[i] < '0' || chars[i] > '9') return 0;
result = result * 10 + (chars[i] - '0');
}
return isNegative ? -result : result;//三元
}
项目地址:https://github.com/JohnnyJYWu/offer-Java
上一篇:算法 | 一周刷完《剑指Offer》 Day3:第27~37题
下一篇:算法 | 一周刷完《剑指Offer》 Day5:第50~60题
希望这篇文章对你有帮助~