设计一个函数把两个数字相加。不得使用 + 或者其他算术运算符。
示例:
输入: a = 1, b = 1 输出: 2
提示:
a
,b
均可能是负数或 0- 结果不会溢出 32 位整数
我的答案:
一、信息
1.设计一个函数把两个数相加
2.不得使用+或者其他运算符
3.a,b均为负数或0
4.结果不会溢出32位整数
二、分析
问题出现:
1.如何实现不用加法就实现两数相加呢?
我的思路:
思路1:通过将a,b转化成二进制,这样我们就能用二进制加法求解了。
新的问题出现:
该如何实现二进制加法呢?
首先我们可以观察二进制加法的规律
1101
+1010
10111
规律就是:
1 0 得1
1 1 得0
0 0 得0
我的答案其实很简单我们只需要通过与异或运算即可实现
问题2:该如何避免结果不会溢出32位整数呢?
我的实现:
为了不使用`+`或其他算术运算符来实现数字的加法,我们可以使用位操作。以下是基于此思路的解决方案:
### 1. C语言:
#include <stdio.h>int add(int a, int b) {while (b != 0) {unsigned int carry = (unsigned int)(a & b) << 1; // 计算进位a = a ^ b; // 不计算进位的加法b = carry; // 把进位放在b上,继续进行加法}return a;
}int main() {int a = 1, b = 1;printf("Sum: %d\n", add(a, b));return 0;
}
2. C++:
#include <iostream>int add(int a, int b) {while (b != 0) {unsigned int carry = (unsigned int)(a & b) << 1;a = a ^ b;b = carry;}return a;
}int main() {int a = 1, b = 1;std::cout << "Sum: " << add(a, b) << std::endl;return 0;
}
JAVA:
public class AddWithoutPlus {public static int add(int a, int b) {while (b != 0) {int carry = a & b; // 计算进位a = a ^ b; // 不计算进位的加法b = carry << 1; // 把进位放在b上,继续进行加法}return a;}public static void main(String[] args) {int a = 1, b = 1;System.out.println("Sum: " + add(a, b));}
}
这些解决方案都是基于二进制表示的加法原理,利用位操作来实现的。当我们加两个二进制数时,可以分为两步:1) 不计算进位的加法,和 2) 计算进位。
英雄师傅的分析:
将a和b都转化成二进制以后,执行相加,举个简单的例子,例如a是21(二进制是10101),b是12(二进制是1100),它们两个相加的值应该是33
对于两个数的对应相加,如果不产生进位就是异或的结果。(在我看来就是用异或来模拟这个过程)
比如说:
唯一没有提到的,就是1和1相加的情况,这种情况会产生进位,所以异或结果并不等于相加的结果,但是异或的结果等于相加后低位的值。换言之1+1=10,异或结果等于0,0和0相等,很合理。
基于上述观点,如果两个数二进制在相加的过程中,都没有出现1和1的情况,那么加法就等于两个数的异或。如果有进位,那么就要把进位的那部分单独拎出来。
什么时候有进位呢?当两个都为1的时候,也就是两个位与为1的时候,所以我们可以把a+b拆成两个部分
a^b和a&b
英雄师傅的实现过程:
int add(int a, int b){if(b==0){return a;}return add(a^b,((unsigned int)(a&b))<<1);
}
Leetcode 题解
Leetcode地址:Leetcode题解
方法一:位运算
预备知识
有符号整数通常用补码来表示和存储,补码具有如下特征:
正整数的补码与原码相同;负整数的补码为其原码除符号位外的所有位取反后加 111。
可以将减法运算转化为补码的加法运算来实现。
符号位与数值位可以一起参与运算。
思路和算法
虽然题目只要求了不能使用算术运算符,但是原则上来说也不宜使用类似的运算符 +=\texttt{+=}+= 和 -=\texttt{-=}-= 以及 sum\texttt{sum}sum 等方法。于是,我们使用位运算来处理这个问题。
首先,考虑两个二进制位相加的四种情况如下:
0 + 0 = 0
0 + 1 = 1
1 + 0 = 1
1 + 1 = 0 (进位)
可以发现,对于整数 aaa 和 bbb:
在不考虑进位的情况下,其无进位加法结果为 a⊕b\texttt{a} \oplus \texttt{b}a⊕b。
而所有需要进位的位为 a & b\texttt{a \& b}a & b,进位后的进位结果为 (a & b) << 1\texttt{(a \& b) << 1}(a & b) << 1。
于是,我们可以将整数 aaa 和 bbb 的和,拆分为 aaa 和 bbb 的无进位加法结果与进位结果的和。因为每一次拆分都可以让需要进位的最低位至少左移一位,又因为 aaa 和 bbb 可以取到负数,所以我们最多需要 log(max_int)\log (max\_int)log(max_int) 次拆分即可完成运算。
因为有符号整数用补码来表示,所以以上算法也可以推广到 000 和负数。
实现
在 C++\texttt{C++}C++ 的实现中,当我们赋给带符号类型一个超出它表示范围的值时,结果是 undefined\text{undefined}undefined;而当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模的余数。因此,我们可以使用无符号类型来防止溢出。
在 Python\texttt{Python}Python 的实现中,因为 Python\texttt{Python}Python 的整数类型为是无限长的,所以无论怎样左移位都不会溢出。因此,我们需要对 Python\texttt{Python}Python 中的整数进行额外处理,以模拟用补码表示的 323232 位有符号整数类型。具体地,我们将整数对 2322^{32}2
32
取模,从而使第 333333 位及更高位均为 000;因为此时最终结果为用补码表示的包含符号位的 323232 位整数,所以我们还需要再次将其换算为 Python\texttt{Python}Python 的整数。
C++:
class Solution {
public:int add(int a, int b) {while (b != 0) {unsigned int carry = (unsigned int)(a & b) << 1;a = a ^ b;b = carry;}return a;}
};
JAVA:
class Solution {public int add(int a, int b) {while (b != 0) {int carry = (a & b) << 1;a = a ^ b;b = carry;}return a;}
}
总结:
个人
从这个问题中,我们可以学到多方面的知识和技能:
1. **基础计算机科学知识**:这道题目介绍了如何使用位操作来模拟基本的算术运算,这反映了计算机在底层如何处理加法。
2. **递归和迭代思维**:即使在这样的问题中,递归和迭代的应用也是一个重要的思维模式。我们反复应用相同的逻辑,直到达到预期的结果。
3. **处理边界情况**:考虑到整数溢出和32位限制,这提醒我们在解决问题时总是要注意潜在的边界情况和限制。
4. **位操作技能**:位操作是许多算法和数据结构问题中的一个关键技能。这道题目为我们提供了一次实际应用的机会,加深了我们对`AND`、`XOR`、左移等操作的理解。
5. **创新思维**:当面对某些明显的方法(例如使用加法运算符)不可用时,寻找其他解决方案需要创新思维。这种思维方式对于不断发展的计算机科学领域是至关重要的。
6. **优化和效率**:尽管我们可以使用递归来解决此问题,但递归可能会导致效率问题,特别是对于大的整数。这提醒我们,即使某个方法是可行的,我们仍然需要考虑其效率。
7. **实际应用与理论知识**:在真实的计算机系统中,加法和其他算术操作确实是通过硬件逻辑(例如加法器)和位操作来实现的。通过这种方法,我们不仅学习了一种算法技巧,而且还深入了解了计算机的工作原理。
总的来说,这道题目为我们提供了一次深入学习计算机科学基础、算法设计和优化的机会,并激发了我们的创新思维。
感受:
其实这道题目和计算机组成原理中定点加法和减法这一章很像,象在哪里呢?其实就是像在对二进制加法的推导和探索,其中二者都处理了很多进位问题。其实我觉得出题人是想让我们体验一把当初计算机科学家们是如何一步一步设计出加法器的,就是绕开编译器原本就带着的+法函数而是自己写一个这个函数的底层。
(传送门:计算机组成原理 2.2 定点加法)