關于除法,你也許覺得沒什么值得談論的,畢竟小學的時候體育老師就教過我們了。然而對于編程中使用的除法,我覺得還是有很多值得注意的細節(jié)的。為什么我想深究一下?因為我日常主要使用Java和Python編程,而它們的除法在細節(jié)上有很多不同之處,全是坑啊…所以接下來我也將著重于Java和Python,但是相信我,就算你不用Java和Python,也會有所收獲的。
1.整數(shù)除法
對兩個不能整除的整數(shù)做除法,就要面對舍入的問題。和大多數(shù)編程語言一樣,Java的基本策略是向零取整(round to zero),也就是向絕對值變小的方向取整。舉幾個香甜的小栗子:3/2=1, -3/2=-1。而對于Python而言,情況就有所不同了。
>>>-1/10
-1```
顯然如果按照Java的取整策略,-1/10應該得0,而Python給出的結果是-1。事實上Python的取整方式是向下取整,也就是向著數(shù)軸上負無窮的方向取整。
好吧,Java和Python的取整方式不同,奪大點事兒啊…那么如果我們要在Python下采用向零取整的結果,咋整?一種比較直接的方式是:
```python
>>>int(float(-1)/10)
0```
##2.取余
誰說沒大事?(╰( ̄▽ ̄)╭) 大事來了!
Java和Python整數(shù)除法都遵循下面這個公式:
>(a/b)*b+c=a
也就是說:
>a mod b=c=a-(a/b)*b
這里的/表示的是整數(shù)除法。既然它們的取整方式不一樣,那么取余也會受到影響:
>For Java: -2 % 3==-2
For Python: -2 % 3==1
在某些實際應用中,我們可能會被要求得到一個整數(shù)的各位數(shù)字。如果輸入的整數(shù)的正的,Java和Python都可以用相同的方法來解決:
```python
def func(a):
pos, res=1, []
while a/pos:
res+=(a/pos)%10,
pos*=10
return res```
Java代碼也差不多就是這樣了。但如果輸入的整數(shù)是一個負數(shù),Java版本的代碼還是可以得到正確的結果,而Python不能(曾經(jīng)在這里被坑的,舉手)。那怎樣用Python正確地搞定這個問題嘞?可以先去絕對值和符號,當正數(shù)來處理,最后再在結果里搭上符號。
##3. Follow-ups
###3.1 Python中的另一個除法操作
我們知道,在Python中,基本的除號“/”是被重載了的。當兩個操作數(shù)都是整數(shù)時,進行整數(shù)除法,得到整數(shù)結果,否則進行浮點數(shù)除法(真除法),得到浮點數(shù)結果。從Python 2.2開始,另一個除號被引入://,它只執(zhí)行整數(shù)除法。注意,//的結果類型依操作數(shù)而定。
```python
>>>1.0/2
0.0
>>>1.0//2.0
0.0
>>>1//2
>0```
另外,如果想同時得到商和余數(shù),可以使用內建的函數(shù)divmod,結果是一個tuple。
```python
>>>divmod(7, 2)
(3, 1)
>>>divmod(7.0, 2)
(3.0, 1.0)```
###3.2 Python中的舍入
除了缺省的舍入方式,Python還有多種舍入可供選擇。
**Floor rounding:**
```python
>>>import math
>>>math.floor(1.2)
1.0
>>>math.floor(-1.2)
-2.0```
**Ceiling rounding:**
```python
>>>math.ceil(1.2)
2.0
>>>math.ceil(-1.2)
-1.0```
**Round-off:**
```python
>>>round(0.5)
1.0
>>>round(-0.4)
-0.0
>>>round(-0.5)
-1.0```
內嵌的round函數(shù)也可以一個指定保留小數(shù)位數(shù)的參數(shù):
```python
>>>round(0.21, 1)
0.2
>>>round(0.21, 2)
0.21```
**Caution !**
```python
>>>round(2.675, 2)
2.67
咦?bug啦?!當然不是。這里要明確一件事:計算機只認識0,1(量子計算機?懵)。就是說,我們輸入的十進制數(shù),在計算機內部都是用二進制來表示的。有的十進制數(shù)可以用二進制準確地表示出來,比如十進制的0.125可以表示為0b0.001;然而很多的小數(shù)是沒法用二進制數(shù)精確表示的,計算機里存儲的是它們的近似值,例如十進制的0.1,用二進制表示,可以近似為: 0b0.00011001100110011001100110011001100110011001100110011010
,所以當我們把它換回十進制數(shù)以輸出或者使用,得到的值就是0.1000000000000000055511151231257827021181583404541015625
。也就是說,0.1在計算機里并不是剛好等于1/10的。
>>>0.1+0.2
0.30000000000000004
同樣,當我們運行round()函數(shù),也是對計算機中實際存儲的值近似取舍。2.67實際上近似為2.67499999999999982236431605997495353221893310546875
,你看第三位小數(shù)是4,那么round(2.675, 2)就相當于round(2.674, 2),結果當然是2.67。值得注意的是,這種現(xiàn)象是廣泛存在于各種計算機和各種編程語言的,不是bug,只是有的語言選擇了不讓你看到。
3.3 Java中的舍入
Java提供了floor和ceil方法來實現(xiàn)向下和向上取整。
Math.floor(2.9)
Math.ceil(2.1)
這倆函數(shù)簡單方便,居家旅行必備。另外Java中也有個round函數(shù),可以實現(xiàn)各種復雜的取整。
System.out.println(Math.round(0.5));
//輸出 1
System.out.println(Math.round(-0.5));
//輸出 0
System.out.println(Math.round(-0.51));
//輸出 -1```
這什么鬼!Keep Calm and Carry On!
數(shù)學上有多種不同的策略來進行取整,比如我們體育老師教的四舍五入。各種取整策略的共同點就是要做真值作近似,那就會引入偏差。四舍五入顯然并不是一種公平的策略(想想0~4的舍和5~9的得)。
有一個叫做銀行家舍入(Banker’s Rounding)的東西,不造你聽過沒,反正我是最近才知道的。事實上.NET和VB6都是默認采用這種方式,而且IEEE 754默認采用這種Rounding。Banker’s Rounding 也就是** round to even **策略。
假設當前考慮那位的數(shù)字是d(其實d就是將不被保留的第一位),如果d<5,則舍(round to zero);如果d>5,則入(round away from zero);而當d==5時,就要根據(jù)d前后的數(shù)位來確定往哪邊取了。
>1) 如果d之后存在非零的數(shù)位,則入;
2)如果d之后不存在非零的數(shù)位,則看d之前的一個數(shù)位,用c表示:
a.如果c是奇數(shù),則入;
b.如果c是偶數(shù),則舍。
再來一把栗子,對下列數(shù)保留0位小數(shù),
第一位小數(shù)就是d,整數(shù)位就是c:
>BankRound(0.4)==0, BankRound(0.6)==1, BankRound(-0.4)==0, BankRound(-0.6)==-1
BankRound(1.5)==2.0, BankRound(-1.5)==-2.0, BankRound(2.5)==2.0, BankRound(-2.5)==-2.0
BankRound(1.51)==2.0, BankRound(-1.51)==-2.0, BankRound(2.51)==3.0, BankRound(-2.51)==-3.0
可以看出,Banker’s Rounding對正數(shù)和負數(shù)的處理是對稱的,因此不會引入符號帶來的偏差。另外它以均等的幾率來舍入數(shù)位(考慮c, c有各一半的幾率為奇數(shù)和偶數(shù)),所以多次舍入后與真值的差別會較小。
扯了這么多,跟Java的**Math.round( )**有什么關系呢?我也是寫到這才發(fā)現(xiàn),好像沒什么軟(luan)關系。因為它并沒有遵循Banker’s rounding。而是按照以下策略進行取整:
當考慮的數(shù)位d不是5,d<5就舍,d>5則入。
當d==5:
>a.如果d的右邊有非零數(shù)位,則入;
>b.如果d的右邊沒有非零數(shù)位,則** round to ceiling**,即對負數(shù)舍,對正數(shù)入。
[Java文檔里是這么表述的](http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html)
還有還有, 在Java里可以使用** BigDecimal **和** RoundingMode **實現(xiàn)更通用的取整方式。
```java
double d=-2.5;
BigDecimal bd=new BigDecimal(d);
double nd=bd.setScale(0,
RoundingMode.HALF_EVEN).doubleValue();
System.out.println(nd);
//輸出 -2.0```
** setScale **的第一個參數(shù)是保留的小數(shù)位數(shù),第二個參數(shù)是舍入模式。可選的舍入模式有:
**HALF_EVEN**, 也就是銀行家方式;
**HALF_UP**, 四舍五入;
**HALF_DOWN**, 五舍六入;
**CEILING、FLOOR**, 向正無窮、負無窮方向;
**UP、DOWN***, 向零和遠離零;
**UNNECESSARY**, 斷言舍入后的值和原值相等,也就是不需要舍入。如果斷言錯了,拋出**ArithmeticException**異常。
先寫到這,比較粗糙,但是希望你有所收獲吧。歡迎討論,有話好好說(╰( ̄▽ ̄)╭)
本文遵守[知識共享協(xié)議:署名-非商業(yè)性使用-相同方式共享 (BY-NC-SA)](http://creativecommons.org/licenses/by-nc-sa/3.0/cn/)及[簡書協(xié)議](http://www.lxweimin.com/p/c44d171298ce)
轉載請注明:**作者[曾會玩](http://www.lxweimin.com/users/5e81a35c8586/latest_articles)**