在算法面试或系统编程中,我们经常面临各种看似棘手的限制条件。试想一下,如果由于某些底层硬件限制(比如在嵌入式内核开发中)或者面试官的刻意要求,我们不能直接使用编程语言提供的除法(INLINECODE9ad15d92)或取模(INLINECODE4cd3bc7b)运算符,该如何计算出两个整数相除的商和余数呢?
在我们最近的一个涉及底层驱动优化的项目中,就遇到了这样一个经典的场景。为了在特定架构上获得极致性能,我们需要避开昂贵的硬件除法指令,转而利用更基础的位运算来完成工作。在这篇文章中,我们将深入探讨这一有趣的问题。我们将从最简单的逻辑出发,逐步过渡到更加高效的解决方案,并辅以丰富的代码示例和实际应用场景。无论你是为了准备面试,还是出于对底层算法的好奇,我相信这篇文章都会为你提供有价值的见解。
问题背景与核心逻辑
首先,我们需要明确数学上除法的定义。给定两个正整数 dividend(被除数)和 divisor(除数),我们的目标是找到满足以下等式的 quotient(商)和 remainder(余数):
dividend = divisor * quotient + remainder
其中,0 <= remainder < divisor。
#### 示例分析
让我们通过具体的例子来理解。如果我们想要计算 INLINECODEf34845f9 除以 INLINECODEe7a73c4e:
- 输入:dividend = 10, divisor = 3
- 输出:3, 1
- 解释:
3 * 3 + 1 = 10,商是 3,余数是 1。
再看一个例子 INLINECODE256ed836 除以 INLINECODE67604200:
- 输入:dividend = 11, divisor = 5
- 输出:2, 1
- 解释:
5 * 2 + 1 = 11,商是 2,余数是 1。
方法一:长除法的模拟(循环减法)
在介绍二分查找这种“高级”方法之前,我想先聊聊最直观的解法:重复减法。
这就好比我们在纸上做除法时的运算过程。我们只需要不断地从被除数中减去除数,直到被除数小于除数为止。减法的次数就是商,剩下的那个被除数就是余数。
实现思路:
- 初始化商
quotient = 0。 - 当
dividend >= divisor时,循环执行:
* dividend = dividend - divisor
* quotient++
- 最终的 INLINECODEc4337fbd 就是商,剩下的 INLINECODE908b498a 就是余数。
这种方法非常简单,但有一个致命的弱点:效率低。如果被除数是 1,000,000,除数是 1,我们需要循环一百万次!时间复杂度是 O(N),其中 N 是商的大小。这在处理大数时是不可接受的,但在处理非常小的数据或者作为快速原型验证时,它依然有其价值。
方法二:优化的二分查找法
为了提高效率,我们可以转变思路。我们不需要一个一个地“尝试”商,而是可以利用二分查找在可能的范围内快速锁定正确的商。这种方法在2026年的AI辅助编程面试中非常常见,因为它展示了对对数级时间复杂度的深刻理解。
#### 核心思想
商的可能范围是什么呢?对于一个正整数被除数 INLINECODE6aeca94f 和除数 INLINECODE7c1f821c,商的最大值不可能超过 INLINECODE043720f9 本身(当除数为1时)。因此,我们可以在区间 INLINECODE1aba2417 内进行二分查找。
我们要找的目标 mid(潜在的商),必须满足以下不等式关系:
divisor * mid <= dividend < divisor * (mid + 1)
为了避免使用除法,我们通过乘法来验证:
计算 n = dividend - divisor * mid。
- 如果 INLINECODE329298e7 是负数,说明 INLINECODE2d086a4d 太大了(乘积超过了被除数),我们需要向左搜索(
end = mid - 1)。 - 如果 INLINECODEb5891760 大于等于 INLINECODE49c17139,说明 INLINECODE3879a55f 太小了(减去乘积后剩下的部分还可以再除一次),我们需要向右搜索(INLINECODE03a9bd8e)。
- 如果 INLINECODEbed93b7c,恭喜!我们找到了商 INLINECODE1fb8f050 和余数
n。
#### 代码实现
为了让你更好地理解,我们将上述算法转化为多种编程语言的实现。请注意,为了保持代码的健壮性,我们在实际工程中通常还会考虑负数的情况(通过符号位处理),但在本文的核心示例中,我们专注于正整数运算,以便你清晰地掌握二分查找的核心逻辑。
#### C++ 实现
// C++ 实现:不使用 / 和 % 运算符,利用二分查找求商和余数
#include
#include
using namespace std;
// 辅助函数:通过二分查找确定商和余数
// pair的第一个元素是商,第二个元素是余数
pair find(int dividend, int divisor, int start, int end) {
// 递归基准情况:如果起始位置超过了结束位置
// 这种情况通常只在边界异常时发生,正常二分查找会在中途返回
if (start > end) {
return { 0, dividend };
}
// 计算中间值,防止溢出的写法是 start + (end - start) / 2
int mid = start + (end - start) / 2;
// 核心公式:余数 = 被除数 - 除数 * 商
// 这里我们用 mid 模拟商
int remainder = dividend - divisor * mid;
// 情况 1:余数为负数
// 说明 divisor * mid > dividend,即 mid(商)太大了,需要往左找
if (remainder = divisor
// 即 dividend >= divisor * (mid + 1),意味着 mid(商)太小了,还可以再加 1
else if (remainder > divisor) {
return find(dividend, divisor, mid + 1, end);
}
else {
// 情况 3:0 <= remainder <= divisor
// 这里有个特殊情况,如果 remainder 正好等于 divisor
// 说明我们其实还可以再多除一次,商应该加 1,余数归零
if (remainder == divisor) {
mid++;
remainder = 0;
}
// 找到了最终答案,返回商和余数
return { mid, remainder };
}
}
// 封装函数
pair divide(int dividend, int divisor) {
// 被除数小于除数时,商为0,余数为被除数本身
if (dividend < divisor) return { 0, dividend };
// 搜索范围是 1 到 dividend
return find(dividend, divisor, 1, dividend);
}
// 主函数测试
int main() {
int dividend = 10;
int divisor = 3;
pair ans = divide(dividend, divisor);
cout << "商: " << ans.first << ", 余数: " << ans.second << endl;
return 0;
}
进阶与实战:位运算法——工业级的首选
虽然二分查找已经将复杂度降低到了对数级别,但在计算机底层,二进制位运算往往能带来更高的性能提升。这是很多高性能库函数(如内核中的除法实现)所采用的方法。在2026年的现代开发中,当我们编写边缘计算节点的高性能代码时,位运算法是首选。
基本原理:
我们可以将除数不断左移(相当于乘以2),直到它刚好小于或等于被除数。这类似于我们在十进制做除法时,先看最大的倍数。
步骤:
- 初始化
quotient = 0。 - 从高位(比如31位)向低位遍历。
- 尝试将 INLINECODEf6c82be6 作为新的临时商,看看 INLINECODE90703528 是否小于等于
dividend。 - 如果是,则更新当前的商。
这种方法完全基于加法和位移,效率极高,且没有递归调用的开销。
# Python 示例:位运算法求商
def divide_bitwise(dividend, divisor):
# 处理符号(此处仅展示正数逻辑)
quotient = 0
# 我们从 2^31, 2^30 ... 2^0 尝试构建商
for i in range(31, -1, -1):
# 检查 (divisor << i) 是否小于等于 dividend
# (divisor << i) 等价于 divisor * 2^i
if (divisor << i) <= dividend:
# 如果是,说明这一位可以是 1
dividend -= (divisor << i)
quotient |= (1 << i) # 将第 i 位置为 1
# 最后 dividend 剩下的部分就是余数
return quotient, dividend
print(divide_bitwise(10, 3)) # 输出 (3, 1)
企业级开发:生产环境下的健壮实现
在面试或算法竞赛中,我们通常只处理正整数。但在我们实际的工程项目中,处理边界情况(如溢出、负数)往往比算法本身更重要。让我们思考一下这个场景:如果输入是 INLINECODE466cb3c0 且除数是 INLINECODE155fcea2,结果是 INT_MAX + 1,这会导致32位有符号整数溢出,这是未定义行为,也是许多安全漏洞的根源。
在2026年的开发理念中,安全左移 意味着我们必须在代码层面预防这些潜在风险。下面是一个考虑了符号处理和溢出检查的 C++ 完整实现。
#include
#include
using namespace std;
// 2026风格的企业级实现:处理符号与溢出
class Solution {
public:
pair divide(int dividend, int divisor) {
// 1. 处理除数为0的异常情况
if (divisor == 0) {
// 在实际生产环境中,这里可能需要抛出异常或返回错误码
// 这里为了演示简单,返回最大值表示错误
return {INT_MAX, 0};
}
// 2. 处理唯一的溢出情况:INT_MIN / -1
if (dividend == INT_MIN && divisor == -1) {
// 根据题目要求或业务逻辑,这里通常返回 INT_MAX
return {INT_MAX, 0};
}
// 3. 确定结果的符号
// 使用异或操作:负负得正,正正得正,一负一负为负
bool negative = (dividend < 0) ^ (divisor = 0; i--) {
if ((absDivisor << i) <= absDividend) {
absDividend -= (absDivisor << i);
quotient += (1LL << i);
}
}
// 此时 absDividend 剩下的值就是余数(绝对值形式)
long long remainder = absDividend;
// 6. 应用符号
if (negative) {
// 注意:余数的符号通常与被除数相同(这是数学定义),但在C++中 % 运算结果符号取决于实现
// 这里我们遵循标准的数学定义:remainder 符号与 dividend 相同
// 商取负
quotient = -quotient;
// 如果原被除数是负数,余数也应该是负数(在这个特定的实现逻辑下)
if (dividend < 0) remainder = -remainder;
}
return {(int)quotient, (int)remainder};
}
};
// 测试我们的企业级代码
int main() {
Solution sol;
// 测试用例 1: 普通情况
auto res1 = sol.divide(10, 3);
cout < Quotient: " << res1.first << ", Remainder: " << res1.second << endl;
// 测试用例 2: 负数情况
auto res2 = sol.divide(-10, 3);
cout < Quotient: " << res2.first << ", Remainder: " << res2.second << endl;
// 测试用例 3: 边界情况 (INT_MIN / -1)
auto res3 = sol.divide(INT_MIN, -1);
cout < Quotient: " << res3.first << ", Remainder: " << res3.second << endl;
return 0;
}
AI辅助开发时代的最佳实践
在2026年的技术趋势下,像 Vibe Coding 这样的概念正在重塑我们的开发方式。当我们面对这类算法问题时,我们不再仅仅是“编写代码”,而是在与 AI 结对编程。
如何利用现代 IDE (如 Cursor, Windsurf, Copilot) 解决这类问题?
- 生成测试用例先行: 在我们动手写逻辑之前,我们通常会先让 AI 生成一套包含边界条件(如 INLINECODEed0e34f7, INLINECODE04b107fd, 大质数)的测试用例。这符合 TDD(测试驱动开发)的思想。
- 交互式调试: 当你发现位运算逻辑在处理负数时出错,不要干瞪眼。把代码抛给 AI Agent,询问:“我在处理
INT_MIN转正数时遇到了溢出问题,如何修改这段位运算逻辑?” - 多模态理解: 优秀的算法工程师会画图。你可以让 AI 生成一张解释“左移除法”原理的二进制位示意图,这比枯燥的代码解释直观得多。
常见错误与避坑指南
在实现上述算法时,有几个陷阱你可能会遇到,这也是我们在过去的项目中曾经“踩过的坑”:
- 整数溢出的隐蔽性: 在计算 INLINECODE09bb4af0 或 INLINECODE7df04c1b 时,如果数字非常大,结果可能会超出整型的表示范围。在 C++ 或 Java 等静态语言中,这会导致未定义行为或错误的数值。
* 解决方案: 始终使用比输入更大的数据类型(如 long long)进行中间计算,或者在移位前显式检查是否会溢出。
- 递归深度风险: 在二分查找的递归写法中,如果处理极大数值(如 64 位整数),递归层级虽不深(约 64 层),但函数调用栈仍有开销。在嵌入式开发中,我们更倾向于将其改写为
while循环的迭代形式。 - 负数取模的不一致性: 在 C++11 之前,负数取模的行为是未定义的(或是向零截断,或是向负无穷截断)。即使现在有了标准,不同语言(如 Python vs C++)对负数取模的结果也不同。
* 解决方案: 明确业务需求。通常我们希望余数的绝对值尽可能小,或者遵循“floored division”规则(类似 Python)。在上文的企业级代码中,我们展示了如何控制这一点。
总结
在这篇文章中,我们探讨了在不使用除法运算符的情况下如何计算商和余数。我们从最简单的重复减法开始,分析了其效率瓶颈,进而学习了基于二分查找的高效算法,并最终触及了底层性能极佳的位运算法。最后,我们还分享了在 2026 年的工程环境下,如何编写健壮的、考虑了溢出的生产级代码。
希望这些内容不仅能帮助你应对面试题,更能让你理解计算机底层处理运算的基本原理,以及如何在现代开发范式中利用 AI 工具来优化这一过程。编码愉快!
引用来源: 本文参考了经典的算法题解思路,并结合了 2026 年最新的 AI 辅助开发与企业级工程实践进行了深度扩展。