1 前言
题目来源于Leetcode。
重点:理清逻辑,忽略细节,模仿高手,五毒神掌
2 题目分析
题目很容易理解,先分成两个部分
- 正数
- 负数
先解决正数
最开始想到的是
唯一增加的就是,先判断整数是多少位。
之后再判断溢出
然后解决负数
先使用一个bool变量保存符号,如果是负数,则取绝对值,再按正数进行运算,之后再加上符号,再判断溢出。
整体思维非常容易想到,分治思想,下面是代码。
3 自己想
int reverse(int x) {// 不管正负数,全变正数bool isNegativeNumber = false;int xAbsoluteValue = 0;if (x < 0) {isNegativeNumber = true;if (x != INT_MIN)xAbsoluteValue = -x;elsereturn 0;}else {isNegativeNumber = false;xAbsoluteValue = x;}// 判断整数多少位【动态的】int xTemporary = xAbsoluteValue;int count = 0;for (int i = 0; i < sizeof(int)*8; i++) { // 【注意】字节数*8if (xTemporary == 0) {break;}else {count++;}xTemporary /= 10;}// 反转long long xNew = 0; // 不要用long,它在32位下也是4字节xTemporary = xAbsoluteValue;for (int i = 0; i < count; i++) {xNew = xNew * 10 + xTemporary % 10;xTemporary /= 10;}// 符号回归if (isNegativeNumber) {xNew = -xNew;}// 判断溢出if (xNew < INT_MIN || xNew > INT_MAX) {return 0;}else{return xNew;}}
运行结果还可以,就是系统本身不稳定,有时候是4ms,这不重要,重要的是,这种做法太啰嗦了,我先尝试按照这个思路优化一下。
去掉符号转换,这部分没有也一样
注意使用long long
,而不是long
,32位下前者8字节,后者和int一样4个字节
判断溢出,使用一行代码搞定,取缔if else
,使用三元运算符
// 判断整数多少位【动态的】int xTemporary = x;int count = 0;for (int i = 0; i < sizeof(int) * 8; i++) { // 【注意】字节数*8if (xTemporary == 0) {break;}else {count++;}xTemporary /= 10;}// 反转long long xNew = 0; xTemporary = x;for (int i = 0; i < count; i++) {xNew = xNew * 10 + xTemporary % 10;xTemporary /= 10;}return (xNew < INT_MIN || xNew > INT_MAX) ? 0 : xNew;
好了,这个程序已经优化了很多了,还有没有空间继续优化呢?
再优化,就只能在获取整数位数下手,将其直接变成反转的条件使用。
好吧……我做不下去了,看大神解法好了。
谈一谈收获
- 对待算法题,重点关注逻辑,对于防御性编程等细节可以不用深究
- 先把逻辑在纸面上搞清楚,再写代码!
- int、long等数据类型的大小,根据系统位数以及编译器决定,需要实际测试一下,尽量使用sizeof等通用的东西
- 计算位数部分,有一点动态规划的意思,很有趣。
毫无移位,按照我这么写算法题,可以凉凉了~~~~接下来,我来学习一下大神的解法吧。
4 看大神做法,直接模仿学会
4.1 大神一
long long res = 0;
while (x) {res = res * 10 + x % 10;x /= 10;
}
return (res < INT_MIN || res > INT_MAX) ? 0 : res;
这个大神与我的思路类似,只不过比我的又进一步优化,我们学习一下。
这里最重要的一点,不需要判断多少位,也不需要暂存,不用管循环次数,循环结束的条件,就是x = 0
。
这也不是本质,这题的本质是数学问题。
- 是
1234
变成4321
的问题 - 是
1234
提取出每一个数字的问题
来看看我的算法中愚蠢的点。
// 判断整数多少位【动态的】int xTemporary = x;int count = 0;for (int i = 0; i < sizeof(int) * 8; i++) { // 【注意】字节数*8if (xTemporary == 0) {break;}else {count++;}xTemporary /= 10;}// 反转long long xNew = 0; xTemporary = x;for (int i = 0; i < count; i++) {xNew = xNew * 10 + xTemporary % 10;xTemporary /= 10;}
关注一下两个循环的条件
- 循环32次,确定位数
- 根据位数再反转
事实上,我想的是,先确定好位数,这样就不用每次都循环32次了,但是,我在确定位数的时候,还是循环了32次……蠢到家……
虽然不是每次循环32次,但是,这种程序结构无疑是垃圾的,尽管是双重保险,但是没有必要阿,我们警惕一下!
值得警惕的结构
抛开题目本身,我们看一看这个结构
for (int i = 0; i < sizeof(int) * 8; i++) { // 【注意】字节数*8if (xTemporary == 0) {break;}else {count++;}xTemporary /= 10;
}
for循环中,嵌套一个通过if判断的跳出循环的装置,我们来改进一下
while(xTemporary){ xTemporary /= 10;count++;}
嗯,这俩功能完全一样,但是显然后者更加简洁
现在,我们是通过中介count来完成程序,那么,可以去掉中间商吗?
当然可以!
既然,xTemporary /= 10
就可以作为终止条件,我们直接用就好了,没必要再管中间商,忽略它!
看一下我们刚才优化的本质,将x /= 10;
和while(x)
二者配合,作为循环终止条件,因此,我们进一步优化。
// 判断整数多少位【动态的】int xTemporary = x;int count = 0;while(xTemporary){ xTemporary /= 10;count++;}// 反转long long xNew = 0; while(xTemporary) {xNew = xNew * 10 + xTemporary % 10;xTemporary /= 10;}
这样一来,你很容易发现,第一个循环完全没有用,直接删掉。
int reverse(int x) {long long xNew = 0; while(x) {xNew = xNew * 10 + x % 10;x /= 10;}return (xNew < INT_MIN || xNew > INT_MAX) ? 0 : xNew;
}
我们,成功将自己的烂程序一步步优化成了大神的程序。
为了程序的通用性,我们稍改一下
int reverse(int x) {long long xNew = 0; while(x != 0) {xNew = xNew * 10 + x % 10;x /= 10;}return (xNew < INT_MIN || xNew > INT_MAX) ? 0 : xNew;
}
因为只有C/C++使用0和false是一样的,但是Java就不允许,只能使用布尔值。
4.2 大神二
int reverse(int x) {int d = 0;while (x){if (d > INT_MAX / 10 || d < INT_MIN / 10)return 0;d = d * 10 + x % 10;x = x / 10;}return d;
}
我们分析大神的思路,我先缓缓下跪了!
在后面五毒神掌第二掌会分析。
5 收获
5.1 一个重要结构的优化
for循环内,通过if跳出的时候,可以优化。
for(int i = 0;i < sizeof(int)*8;i++){if(x){break; }x /= 10
}
while(x){x /= 10
}
5.2 去掉“中间商”的方法
对于一些共性的东西,不再单独列出中间结果,直接得到最终答案。
5.3 算法的本质是数学问题
这个数学表达式其实是这么来的
- 先分治,拆解为数字+权重的形式,本质是硬件思维
- 再调换数字的权重
至于最终的表达式,需要一点点优化过来。
我们需要知道,对于int x;
- 求最低位的数字:
x % 10
- 降维,降低数量级:
x / 10
(利用int直接抹掉小数点)
第一次的算法(使用伪代码)
while(遍历每一位的数字){number[i] = x % 10;x /= 10;
}
这是很容易想到的,那么,我们保存了每一位数字,怎么保存它的权重?真的有必要保存权重吗?
显然没有必要,我们试一下就知道,可以直接一边处理旧数字,一边计算新数字。
newX = 0;
while(遍历每一位的数字){number[i] = x % 10;x /= 10;newX = newX*10 + number[i];
}
这已经是最小单元,没法解释,自己试一下吧。
然后你会发现number[i]
是多余的,并且遍历的条件就是x != 0
。
long long newX = 0;
while(x != 0){newX = newX*10 + x % 10;x /= 10;
}
至于为什么用long long
,这叫先假想结果,因为结果会溢出,所以只能用long long
了。
5.4 一些衍生的题目
5.4.1 求整数位数
所有整数均可。
int reverse(int x) {int count = 0;while (x){x /= 10;count++;}return count;
}
5.4.2 求整数的每一位
void reverse(int x) {int count = 0;int xTemporary = x;while (xTemporary){xTemporary /= 10;count++;}int *everyNumber = new int[count];for (int i = 0; i < count; i++) {everyNumber[i] = x % 10;x /= 10;}for (int i = 0; i < count; i++) {cout << everyNumber[i] << endl;}
}
注意
char与int转换,记得差一个'0'
int i = 4;
char a = i + '0';
cout << a << endl;
6 五毒神掌
五毒神掌是什么?
关注代码逻辑和结构层面的细节
目标导向,一天一个,完全搞定300题
6.1 第一掌
- 先正确理解题目
- 自己想,5分钟想出来就写
- 想不出来,就直接看世界大神答案,并且理解
- 然后大致理解背下来(理解代替记忆,如果不理解,就先记忆,多用用就理解了)
- 边抄边背的方式写代码
自己的思路不能只有一种,每种都要尝试。
重点关注逻辑!画图+手算分析
6.1.1 自己思考的过程
题目很简单,就是整数反转,需要注意
- 正负数问题
- 反转后溢出问题:用
long long
存储
之后用几个数字试一试,研究一下数学公式,先写正确,再不断优化。
int reverse(int x) {long long xNew = 0;while (x != 0) {xNew = xNew * 10 + x % 10;x /= 10;}return (xNew < INT_MIN || xNew > INT_MAX) ? 0 : xNew;
}
6.1.2 大神的代码
public int reverse(int x)
{int result = 0;while (x != 0){int tail = x % 10;int newResult = result * 10 + tail;if ((newResult - tail) / 10 != result){ return 0; }result = newResult;x = x / 10;}return result;
}
基于我的思路,如果可能溢出,就直接使用更大的容器取存储数据,然后看看有没有超过小容器的值,那么,如果没有更大的容器,又该怎么办?
没有大容器,那就用2个小容器,比较新值和旧值。
对于重点公式x1新 = x1旧 * 10 + x % 10
,我们知道,在数学公式中,进行等价变形,等式应该相等,也就是等式(x1新 - x%10) / 10 = x1旧
成立。
但是对于计算机不同,如果第一个公式计算过程有溢出,就会丢失数据,那么第二个公式就不成立。
这也就是我们判断的重点:If overflow exists, the new result will not equal previous one.
如果溢出存在,那么,使用新值运算反过来得到的旧值,就不是原来的那个旧值。
代码如下:
int reverse(int x) {int xNew1 = 0; // 旧值int xNew2 = 0; // 新值while (x) {xNew2 = xNew1 * 10 + x % 10;if ((xNew2 - x % 10) / 10 != xNew1)return 0;xNew1 = xNew2;x /= 10;}return xNew2;
}
事实上,在Leetcode编译器,上面的写法是错误的!
新的收获:使用经典的测试用例
不得不说……任何的算法,在使用大量测试用例测试之前,都不一定完美,例如上面的算法,如果使用INT_MAX
作为测试用例,对于能够进行溢出检测的严格编译器来说,会出现报错(不过C++编译器一般不检测……),那么,报错的原因是什么?
我们看一下xNew2 = xNew1 * 10 + x % 10;
,试想一下,我们刚才假定这个过程中,编译器是允许溢出后直接截断,但不会报错,那么现在,我们假定,编译器不允许溢出的发生,我们又该怎么办?
【思维修炼】“治未病”思想:在问题发生之前处理掉
对于xNew2 = xNew1 * 10 + x % 10;
,我们需要在溢出发生前,就检测出来,因此有以下程序
int reverse(int x) {int xNew = 0;while(x != 0){if(xNew > INT_MAX/10 || xNew < INT_MIN/10) return 0;xNew = xNew * 10 + x % 10;x /= 10;}return xNew;}
更严格来说,是不是需要把x % 10
也“治未病”呢?显然不需要,因为不存在一个数字,乘10后没有溢出,但是再+1就溢出了。
思考:为什么不是
>=
?
因为,对于极限数字214748364
(也就是INT_MAX / 10
),乘10之后,再加上x % 10
是不可能溢出的(可以想象,如果溢出,那x % 10
的结果需要 >7,那么,在这个数反转之前,就已经溢出了,所以不可能)。
经典测试案例 + 严格编译器 = 优秀算法
对于经典测试案例,例如本题,可以有
123
-123
INT_MAX
INT_MIN
1230000
这些提交前的测试案例,足够描述各种情况了。
6.1.3 小结
- 1个大容器与2个小容器
- 算法与数学公式
6.2 第二掌
把大神的代码完全不看的情况下写出来。
- 搞定
自己的代码,多种写法,不断优化到极致。
6.3 第三掌
过了24 小时的时间以后,再次重复做题
不同解法的熟练程度 ——> 专项练习
- 新的收获
6.3.1 整数反转图解——安检排队模型
如果你从动态的角度去看一下,是不是像一个U型排队区人员流动的样子?
想一想你过安检排队的情形。
怎么样,是不是瞬间记住了这个整数反转模型?
int reverse(int x) {int xNew = 0;while(x){if(xNew > INT_MAX/10 || xNew < INT_MIN/10) return 0; // 预测xNew = xNew*10 + x%10;x /= 10;}return xNew;}
通过预测提高性能
这是伟大计算机思想之一,应用广泛,例如指令操作的分支预测,在本题中,溢出的检测就使用了预测思想。
6.4 第四掌
过了一周之后: 反复回来练习相同的题目
6.5 第五掌
面试前一周恢复性的训练,所有题目全都刷一遍