關(guān)于除法笆怠,你也許覺(jué)得沒(méi)什么值得談?wù)摰模吘剐W(xué)的時(shí)候體育老師就教過(guò)我們了誊爹。然而對(duì)于編程中使用的除法蹬刷,我覺(jué)得還是有很多值得注意的細(xì)節(jié)的。為什么我想深究一下频丘?因?yàn)槲胰粘V饕褂肑ava和Python編程办成,而它們的除法在細(xì)節(jié)上有很多不同之處,全是坑啊…所以接下來(lái)我也將著重于Java和Python椎镣,但是相信我诈火,就算你不用Java和Python兽赁,也會(huì)有所收獲的状答。
1.整數(shù)除法
對(duì)兩個(gè)不能整除的整數(shù)做除法,就要面對(duì)舍入的問(wèn)題刀崖。和大多數(shù)編程語(yǔ)言一樣惊科,Java的基本策略是向零取整(round to zero),也就是向絕對(duì)值變小的方向取整亮钦。舉幾個(gè)香甜的小栗子:3/2=1, -3/2=-1馆截。而對(duì)于Python而言,情況就有所不同了蜂莉。
>>>-1/10
-1```
顯然如果按照J(rèn)ava的取整策略蜡娶,-1/10應(yīng)該得0,而Python給出的結(jié)果是-1映穗。事實(shí)上Python的取整方式是向下取整窖张,也就是向著數(shù)軸上負(fù)無(wú)窮的方向取整。
好吧蚁滋,Java和Python的取整方式不同宿接,奪大點(diǎn)事兒啊…那么如果我們要在Python下采用向零取整的結(jié)果赘淮,咋整?一種比較直接的方式是:
```python
>>>int(float(-1)/10)
0```
##2.取余
誰(shuí)說(shuō)沒(méi)大事睦霎?(╰( ̄▽?zhuān)?╭) 大事來(lái)了梢卸!
Java和Python整數(shù)除法都遵循下面這個(gè)公式:
>(a/b)*b+c=a
也就是說(shuō):
>a mod b=c=a-(a/b)*b
這里的/表示的是整數(shù)除法。既然它們的取整方式不一樣副女,那么取余也會(huì)受到影響:
>For Java: -2 % 3==-2
For Python: -2 % 3==1
在某些實(shí)際應(yīng)用中蛤高,我們可能會(huì)被要求得到一個(gè)整數(shù)的各位數(shù)字。如果輸入的整數(shù)的正的碑幅,Java和Python都可以用相同的方法來(lái)解決:
```python
def func(a):
pos, res=1, []
while a/pos:
res+=(a/pos)%10,
pos*=10
return res```
Java代碼也差不多就是這樣了襟齿。但如果輸入的整數(shù)是一個(gè)負(fù)數(shù),Java版本的代碼還是可以得到正確的結(jié)果枕赵,而Python不能(曾經(jīng)在這里被坑的猜欺,舉手)。那怎樣用Python正確地搞定這個(gè)問(wèn)題嘞拷窜?可以先去絕對(duì)值和符號(hào)开皿,當(dāng)正數(shù)來(lái)處理,最后再在結(jié)果里搭上符號(hào)篮昧。
##3. Follow-ups
###3.1 Python中的另一個(gè)除法操作
我們知道赋荆,在Python中,基本的除號(hào)“/”是被重載了的懊昨。當(dāng)兩個(gè)操作數(shù)都是整數(shù)時(shí)窄潭,進(jìn)行整數(shù)除法,得到整數(shù)結(jié)果酵颁,否則進(jìn)行浮點(diǎn)數(shù)除法(真除法)嫉你,得到浮點(diǎn)數(shù)結(jié)果。從Python 2.2開(kāi)始躏惋,另一個(gè)除號(hào)被引入://幽污,它只執(zhí)行整數(shù)除法。注意簿姨,//的結(jié)果類(lèi)型依操作數(shù)而定距误。
```python
>>>1.0/2
0.0
>>>1.0//2.0
0.0
>>>1//2
>0```
另外,如果想同時(shí)得到商和余數(shù)扁位,可以使用內(nèi)建的函數(shù)divmod准潭,結(jié)果是一個(gè)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```
內(nèi)嵌的round函數(shù)也可以一個(gè)指定保留小數(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啦殉簸?闰集!當(dāng)然不是沽讹。這里要明確一件事:計(jì)算機(jī)只認(rèn)識(shí)0,1(量子計(jì)算機(jī)?懵)武鲁。就是說(shuō)爽雄,我們輸入的十進(jìn)制數(shù),在計(jì)算機(jī)內(nèi)部都是用二進(jìn)制來(lái)表示的沐鼠。有的十進(jìn)制數(shù)可以用二進(jìn)制準(zhǔn)確地表示出來(lái),比如十進(jìn)制的0.125可以表示為0b0.001挚瘟;然而很多的小數(shù)是沒(méi)法用二進(jìn)制數(shù)精確表示的,計(jì)算機(jī)里存儲(chǔ)的是它們的近似值饲梭,例如十進(jìn)制的0.1乘盖,用二進(jìn)制表示,可以近似為: 0b0.00011001100110011001100110011001100110011001100110011010
憔涉,所以當(dāng)我們把它換回十進(jìn)制數(shù)以輸出或者使用订框,得到的值就是0.1000000000000000055511151231257827021181583404541015625
。也就是說(shuō)兜叨,0.1在計(jì)算機(jī)里并不是剛好等于1/10的穿扳。
>>>0.1+0.2
0.30000000000000004
同樣,當(dāng)我們運(yùn)行round()函數(shù)国旷,也是對(duì)計(jì)算機(jī)中實(shí)際存儲(chǔ)的值近似取舍矛物。2.67實(shí)際上近似為2.67499999999999982236431605997495353221893310546875
,你看第三位小數(shù)是4跪但,那么round(2.675, 2)就相當(dāng)于round(2.674, 2)履羞,結(jié)果當(dāng)然是2.67。值得注意的是屡久,這種現(xiàn)象是廣泛存在于各種計(jì)算機(jī)和各種編程語(yǔ)言的忆首,不是bug,只是有的語(yǔ)言選擇了不讓你看到涂身。
3.3 Java中的舍入
Java提供了floor和ceil方法來(lái)實(shí)現(xiàn)向下和向上取整雄卷。
Math.floor(2.9)
Math.ceil(2.1)
這倆函數(shù)簡(jiǎn)單方便,居家旅行必備蛤售。另外Java中也有個(gè)round函數(shù),可以實(shí)現(xiàn)各種復(fù)雜的取整妒潭。
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ù)學(xué)上有多種不同的策略來(lái)進(jìn)行取整雳灾,比如我們體育老師教的四舍五入漠酿。各種取整策略的共同點(diǎn)就是要做真值作近似,那就會(huì)引入偏差谎亩。四舍五入顯然并不是一種公平的策略(想想0~4的舍和5~9的得)炒嘲。
有一個(gè)叫做銀行家舍入(Banker’s Rounding)的東西宇姚,不造你聽(tīng)過(guò)沒(méi),反正我是最近才知道的夫凸。事實(shí)上.NET和VB6都是默認(rèn)采用這種方式浑劳,而且IEEE 754默認(rèn)采用這種Rounding。Banker’s Rounding 也就是** round to even **策略夭拌。
假設(shè)當(dāng)前考慮那位的數(shù)字是d(其實(shí)d就是將不被保留的第一位)魔熏,如果d<5,則舍(round to zero)鸽扁;如果d>5蒜绽,則入(round away from zero);而當(dāng)d==5時(shí)桶现,就要根據(jù)d前后的數(shù)位來(lái)確定往哪邊取了躲雅。
>1) 如果d之后存在非零的數(shù)位,則入骡和;
2)如果d之后不存在非零的數(shù)位吏夯,則看d之前的一個(gè)數(shù)位,用c表示:
a.如果c是奇數(shù)即横,則入噪生;
b.如果c是偶數(shù),則舍东囚。
再來(lái)一把栗子跺嗽,對(duì)下列數(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對(duì)正數(shù)和負(fù)數(shù)的處理是對(duì)稱(chēng)的,因此不會(huì)引入符號(hào)帶來(lái)的偏差份帐。另外它以均等的幾率來(lái)舍入數(shù)位(考慮c, c有各一半的幾率為奇數(shù)和偶數(shù))璃吧,所以多次舍入后與真值的差別會(huì)較小。
扯了這么多废境,跟Java的**Math.round( )**有什么關(guān)系呢畜挨?我也是寫(xiě)到這才發(fā)現(xiàn),好像沒(méi)什么軟(luan)關(guān)系噩凹。因?yàn)樗](méi)有遵循Banker’s rounding巴元。而是按照以下策略進(jìn)行取整:
當(dāng)考慮的數(shù)位d不是5,d<5就舍驮宴,d>5則入逮刨。
當(dāng)d==5:
>a.如果d的右邊有非零數(shù)位,則入堵泽;
>b.如果d的右邊沒(méi)有非零數(shù)位修己,則** round to ceiling**恢总,即對(duì)負(fù)數(shù)舍,對(duì)正數(shù)入睬愤。
[Java文檔里是這么表述的](http://docs.oracle.com/javase/7/docs/api/java/lang/Math.html)
還有還有, 在Java里可以使用** BigDecimal **和** RoundingMode **實(shí)現(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 **的第一個(gè)參數(shù)是保留的小數(shù)位數(shù),第二個(gè)參數(shù)是舍入模式戴涝∽檀粒可選的舍入模式有:
**HALF_EVEN**, 也就是銀行家方式;
**HALF_UP**, 四舍五入啥刻;
**HALF_DOWN**, 五舍六入奸鸯;
**CEILING、FLOOR**, 向正無(wú)窮可帽、負(fù)無(wú)窮方向娄涩;
**UP、DOWN***, 向零和遠(yuǎn)離零映跟;
**UNNECESSARY**, 斷言舍入后的值和原值相等蓄拣,也就是不需要舍入。如果斷言錯(cuò)了努隙,拋出**ArithmeticException**異常球恤。
先寫(xiě)到這,比較粗糙荸镊,但是希望你有所收獲吧咽斧。歡迎討論,有話(huà)好好說(shuō)(╰( ̄▽?zhuān)?╭)
本文遵守[知識(shí)共享協(xié)議:署名-非商業(yè)性使用-相同方式共享 (BY-NC-SA)](http://creativecommons.org/licenses/by-nc-sa/3.0/cn/)及[簡(jiǎn)書(shū)協(xié)議](http://www.reibang.com/p/c44d171298ce)
轉(zhuǎn)載請(qǐng)注明:**作者[曾會(huì)玩](http://www.reibang.com/users/5e81a35c8586/latest_articles)**