目錄 一、前言
二、把代碼寫好的四步
三、for循環沒算法快
1. for 循環實現
2. 算法邏輯實現
3. 耗時曲線對比
四、Java中的算法運用
五、程序員數學入門
六、總結
七、系列推薦
一、前言 數學離程序員有多近?
ifelse也好、for循環也罷,代碼可以說就是對數學邏輯的具體實現 。所以敲代碼的程序員幾乎就離不開數學,難易不同而已。
那數學不好就寫不了代碼嗎???不,一樣可以寫代碼,可以寫出更多的CRUD
出來。那你不要總覺得是產品需求簡單所以你的實現過程才變成了增刪改查,往往也是因為你還不具備可擴展、易維護、高性能的代碼實現方案落地能力,才使得你小小年紀寫出了更多的CRUD
!
與一錐子買賣的小作坊相比,大廠和超級大廠更會注重數學能力。
2004年 ,在硅谷的交通動脈 101 公路上突然出現一塊巨大的廣告牌,上面是一道數學題:{e 的連續數字中最先出現的 10 位質數}
.com。
廣告:這里的 e 是數學常數,自然對數的底數,無限不循環小數。這道題的意思就是,找出 e 中最先出現的 10 位質數,然后可以得出一個網址。進入這個網址會看到 Google 為你出的第二道數學題,成功解鎖這步 Google 會告訴你,我們或許是”志同道合“的人
,你可以將簡歷發到這個郵箱,我們一起做點改變世界的事情。
計算 e 值可以通過泰勒公式推導出來:e^x≈1 + x + x^2/2! + x^3/3! +……+ x^n/n! (1) 推導計算過程還包括埃拉托色尼篩選法(the Sieve of Eratosthenes)
、線性篩選法
的使用。感興趣的小伙伴可以用代碼實現下。
二、把代碼寫好的四步 業務提需求、產品定方案、研發做實現。
最終這個系統開發的怎么樣是由三方共同決定的!
這里的地基、磚頭、水電、格局,對應的就是,數據結構、算法邏輯、設計模式、系統架構。從下到上相互依賴、相互配合,只有這一層做好,下一層才好做!
數據結構 :高矮胖瘦、長寬扁細,數據的存放方式,是一套程序開發的核心基礎。不合理的設計往往是從數據結構開始,哪怕你僅僅是使用數據庫存放業務信息,也一樣會影響到將來各類數據的查詢、匯總等實現邏輯的難易。算法邏輯 :是對數據結構的使用,合適的數據結構會讓算法實現過程降低時間復雜度。可能你現在的多層for循環在合適的算法過程下,能被優化為更簡單的方式獲取數據。注意:算法邏輯實現,并不一定就是排序、歸并,還有你實際業務的處理流程。 設計模式 :可以這么說,不使用設計模式你一樣能寫代碼。但你愿意看到滿屏幕的ifelse判斷調用,還是喜歡像膏藥一樣的代碼,粘貼來復制去?那么設計模式這套通用場景的解決方案,就是為你剔除掉代碼實現過程中的惡心部分,讓整套程序更加易維護、易擴展。就是開發完一個月,你看它你還認識! 系統架構 :描述的是三層MVC,還是四層DDD。我對這個的理解就是家里的三居還是四局格局,MVC是我們經常用的大家都熟悉,DDD無非就是家里多了個書房,把各自屬于哪一個屋子的擺件規整到各自屋子里。那么亂放是什么效果呢,就是自動洗屁屁馬桶??給按到廚房了,再貴也格楞子! 好,那么我們在延展下,如果你的衛生間沒有流出下水道咋辦?是不這個地方的數據結構就是設計缺失的,而到后面再想擴展就難了吧!所以,研發在承接業務需求、實現產品方案的時候。壓根就不只是在一個房子的三居或者四居格局里,開始隨意碼磚。
沒有合理的數據結構、沒有優化的算法邏輯、沒有運用的設計模式,最終都會影響到整個系統架構變得臃腫不堪,調用混亂。在以后附加、迭代、新增的需求下,會讓整個系統問題不斷的放大,當你想用重構時,就有著千絲萬縷般調用關系。重構就不如重寫了!
三、for循環沒算法快 在《編程之美》一書中,有這樣一道題。求:1~n中,1出現的次數。比如:1~10,1出現了兩次。
1. for 循環實現long startTime = System.currentTimeMillis();int count = 0 ;for (int i = 1 ; i <= 10000000 ; i++) { String str = String.valueOf(i); for (int j = 0 ; j < str.length(); j++) { if (str.charAt(j) == 49 ) { count++; } } } System.out.println("1的個數:" + count); System.out.println("計算耗時:" + (System.currentTimeMillis() - startTime) + "毫秒" );
使用 for 循環的實現過程很好理解,就是往死了循環。之后把循環到的數字按照字符串拆解,判斷每一位是不是數字,是就+1。這個過程很簡單,但是時間復雜很高。
2. 算法邏輯實現
如圖 20-3 所示,其實我們能發現這個1的個數在100、1000、10000中是有規則的循環出現的。11、12、13、14或者21、31、41、51,以及單個的1出現。最終可以得出通用公式:abcd...=(abc+1)*1+(ab+1)*10+(a+1)*100+(1)*1000...
,abcd代表位數。另外在實現的過程還需要考慮比如不足100等情況,例如98、1232等。
實現過程
long startTime = System.currentTimeMillis();int num = 10000000 , saveNum = 1 , countNum = 0 , lastNum = 0 ;int copyNum = num;while (num != 0 ) { lastNum = num % 10 ; num /= 10 ; if (lastNum == 0 ) { // 如果是0那么正好是少了一次所以num不加1了 countNum += num * saveNum; } else if (lastNum == 1 ) { // 如果是1說明當前數內少了一次所以num不加1,而且當前1所在位置 // 有1的個數,就是去除當前1最高位,剩下位數,的個數。 countNum += num * saveNum + copyNum % saveNum + 1 ; } else { // 如果非1非0.直接用公式計算 // abcd...=(abc+1)*1+(ab+1)*10+(a+1)*100+(1)*1000... countNum += (num + 1 ) * saveNum; } saveNum *= 10 ; } System.out.println("1的個數:" + countNum); System.out.println("計算耗時:" + (System.currentTimeMillis() - startTime) + "毫秒" );
在《編程之美》一書中還不只這一種算法,感興趣的小伙伴可以查閱。但自己折騰實現后的興奮感更強哦!
3. 耗時曲線對比按照兩種不同方式的實現邏輯,我們來計算1000、10000、10000到一個億,求1出現的次數,看看兩種方式的耗時曲線。
for循環隨著數量的不斷增大后,已經趨近于無法使用了。 算法邏輯依靠的計算公式,所以無論增加多少基本都會在1~2毫秒內計算完成。 那么 ,你的代碼中是否也有類似的地方。如果使用算法邏輯配合適合的數據結構,是否可以替代一些for循環的計算方式,來使整個實現過程的時間復雜度降低。
四、Java中的算法運用 在 Java 的 JDK 實現中有很多數學知識的運用,包括數組、鏈表、紅黑樹的數據結構以及相應的實現類ArrayList、Linkedlist、HashMap等。當你深入的了解這些類的實現后,會發現它們其實就是使用代碼來實現數學邏輯而已。就像你使用數學公式來計算數學題一樣
接下來小傅哥就給你介紹幾個隱藏在我們代碼中的數學知識。
1. HashMap的擾動函數未使用擾動函數
已使用擾動函數
擾動函數的代碼
static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
描述 :以上這段代碼是HashMap中用于獲取hash值的擾動函數實現代碼。HashMap通過哈希值與桶定位坐標 那么直接獲取哈希值就好了,這里為什么要做一次擾動呢?作用 :為了證明擾動函數的作用,這里選取了10萬單詞計算哈希值分布在128個格子里。之后把這128個格子中的數據做圖表展示。從實現數據可以看到,在使用擾動函數后,曲線更加平穩了。那么,也就是擾動后哈希碰撞會更小。用途 :當你有需要把數據散列分散到不同格子或者空間時,又不希望有太嚴重的碰撞,那么使用擾動函數就非常有必要了。比如你做的一個數據庫路由,在分庫分表時也是盡可能的要做到散列的。 2. 斐波那契(Fibonacci)散列法描述 :在 ThreadLocal 類中的數據存放,使用的是斐波那契(Fibonacci)散列法 + 開放尋址。之所以使用斐波那契數列,是為了讓數據更加散列,減少哈希碰撞。具體來自數學公式的計算求值,公式 :f(k) = ((k * 2654435769) >> X) << Y對于常見的32位整數而言,也就是 f(k) = (k * 2654435769) >> 28
作用 :與 HashMap 相比,ThreadLocal的數據結構只有數組,并沒有鏈表和紅黑樹部分。而且經過我們測試驗證,斐波那契散列的效果更好,也更適合 ThreadLocal。用途 :如果你的代碼邏輯中需要存儲類似 ThreadLocal 的數據結構,又不想有嚴重哈希碰撞,那么就可以使用 斐波那契(Fibonacci)散列法。其實除此之外還有,除法散列法
、平方散列法
、隨機數法
等。 3. 梅森旋轉算法(Mersenne twister)
// Initializes mt[N] with a simple integer seed. This method is // required as part of the Mersenne Twister algorithm but need // not be made public. private final void setSeed (int seed) { // Annoying runtime check for initialisation of internal data // caused by java.util.Random invoking setSeed() during init. // This is unavoidable because no fields in our instance will // have been initialised at this point, not even if the code // were placed at the declaration of the member variable. if (mt == null ) mt = new int [N]; // ---- Begin Mersenne Twister Algorithm ---- mt[0 ] = seed; for (mti = 1 ; mti < N; mti++) { mt[mti] = (MAGIC_FACTOR1 * (mt[mti-1 ] ^ (mt[mti-1 ] >>> 30 )) + mti); } // ---- End Mersenne Twister Algorithm ---- }
梅森旋轉算法(Mersenne twister)是一個偽隨機數發生算法。由松本真和西村拓士在1997年開發,基于有限二進制字段上的矩陣線性遞歸。可以快速產生高質量的偽隨機數,修正了古典隨機數發生算法的很多缺陷。最為廣泛使用Mersenne Twister的一種變體是MT19937,可以產生32位整數序列。
描述 :梅森旋轉算法分為三個階段,獲得基礎的梅森旋轉鏈、對于旋轉鏈進行旋轉算法、對于旋轉算法所得的結果進行處理。用途 :梅森旋轉算法是R、Python、Ruby、IDL、Free Pascal、PHP、Maple、Matlab、GNU多重精度運算庫和GSL的默認偽隨機數產生器。從C++11開始,C++也可以使用這種算法。在Boost C++,Glib和NAG數值庫中,作為插件提供。五、程序員數學入門 與接觸到一個有難度的知識點學起來辛苦相比,是自己不知道自己不會什么!就像上學時候老師說,你不會的就問我。我不會啥?我從哪問?一樣一樣的!
代碼是對數學邏輯的實現,簡單的邏輯調用關系是很容易看明白的。但還有那部分你可能不知道的數學邏輯時,就很難看懂了。比如:擾動函數、負載因子、斐波那契(Fibonacci)等,這些知識點的學習都需要對數學知識進行驗證,否則也就學個概念,背個理論。
書到用時方恨少,在下還是個寶寶!
那如果你想深入的學習下程序員應該會的數學,推薦給你一位科技博主 Jeremy Kun 花了4年時間,寫成一本書《程序員數學入門》 。
這本書為程序員提供了大量精簡后數學知識,包括:多項式、集合、圖論、群論、微積分和線性代數等。同時在wiki部分還包括了抽象代數、離散數學、傅里葉分析和拓撲學等。
作者表示,如果你本科學過一些數學知識,那么本書還是挺適合你的,不會有什么難度。書中的前三章是基礎數學內容,往后的難度依次遞增。