運算:手動實現(xiàn)字符串相加與相乘的算法精解)
1. 項目概述從“相加”到“相乘”的字符串運算之旅在編程世界里處理數(shù)字字符串的運算是一個既基礎(chǔ)又充滿陷阱的領(lǐng)域。乍一看“字符串相加”和“字符串相乘”似乎只是小學(xué)算術(shù)的翻版但當(dāng)你真正動手去實現(xiàn)尤其是在處理大數(shù)比如長度超過1000位的數(shù)字字符串時你會發(fā)現(xiàn)這遠非調(diào)用int()或BigInteger那么簡單。很多新手甚至是有一定經(jīng)驗的開發(fā)者在面對諸如“12345678901234567890” “98765432109876543210”或者“123” * “456”這樣的問題時第一反應(yīng)可能是將其轉(zhuǎn)換為整數(shù)。然而當(dāng)字符串長度超過語言內(nèi)置整數(shù)類型的表示范圍時這條路就走不通了。這正是我們今天要深入探討的核心如何在不依賴大數(shù)庫的情況下純手工實現(xiàn)這兩個看似簡單、實則考驗算法基本功的運算。我最初接觸這個問題是在準(zhǔn)備技術(shù)面試時它幾乎是各大公司算法題庫中的???。在實際工作中我也遇到過需要處理金融金額計算、高精度ID生成或密碼學(xué)相關(guān)的大數(shù)運算場景直接轉(zhuǎn)換類型會導(dǎo)致精度丟失或溢出錯誤。因此掌握其底層的手動模擬算法不僅是為了通過面試更是為了在關(guān)鍵時刻寫出健壯、可靠的代碼。本文將帶你從最樸素的思路出發(fā)一步步拆解“字符串相加”和“字符串相乘”的實現(xiàn)細節(jié)、邊界條件以及那些容易踩坑的“暗礁”最終你將獲得兩個可以直接用于生產(chǎn)環(huán)境的、魯棒性極強的函數(shù)實現(xiàn)。2. 核心思路拆解模擬豎式計算的本質(zhì)無論是加法還是乘法我們解決問題的核心思想都是模擬人類手工進行豎式計算的過程。計算機不會像我們一樣“一眼”看出結(jié)果但它擅長按照明確的規(guī)則一步步執(zhí)行。我們的任務(wù)就是把我們心算或筆算的規(guī)則翻譯成計算機能理解的精確步驟。2.1 字符串相加逐位計算與進位處理字符串相加的目標(biāo)是給定兩個非負(fù)整數(shù)字符串num1和num2返回它們的和同樣以字符串形式。我們不能直接將其轉(zhuǎn)為整數(shù)相加因為可能存在大數(shù)?;舅悸啡缦聫淖畹臀蛔址哪┪查_始計算就像我們列豎式時從個位開始對齊一樣。逐位相加取出兩個字符串當(dāng)前位的數(shù)字如果某個字符串已經(jīng)遍歷完則用0補位加上來自低位的進位值。處理當(dāng)前位結(jié)果與進位當(dāng)前位的和 (數(shù)字1 數(shù)字2 進位) % 10。新的進位 (數(shù)字1 數(shù)字2 進位) // 10。向前推進將計算出的當(dāng)前位數(shù)字拼接到結(jié)果字符串中然后指針向前移動一位向字符串開頭方向。循環(huán)與終止重復(fù)步驟2-4直到兩個字符串的所有位都處理完畢。處理最后的進位循環(huán)結(jié)束后如果進位值不為0需要將其作為最高位拼接到結(jié)果中。反轉(zhuǎn)結(jié)果因為我們是從低位開始拼接結(jié)果的所以最終需要將結(jié)果字符串反轉(zhuǎn)才能得到正確的順序。這個思路清晰直接關(guān)鍵在于對進位carry的維護和邊界條件的處理。例如當(dāng)兩個字符串長度相差很大時如“123” “456789”短字符串提前遍歷完后需要用0來參與后續(xù)位的運算。2.2 字符串相乘分解為多次加法與錯位字符串相乘要復(fù)雜一些給定兩個非負(fù)整數(shù)字符串num1和num2返回它們的乘積。最直觀的思路是模擬乘法豎式我們以“123” * “456”為例1 2 3 (num1) * 4 5 6 (num2) ------------------ 7 3 8 (3*6的結(jié)果記作中間結(jié)果1) 6 1 5 0 (2*6的結(jié)果需要左移一位即末尾補一個0記作中間結(jié)果2) 4 9 2 0 0 (1*6的結(jié)果需要左移兩位即末尾補兩個0記作中間結(jié)果3) (然后同理計算3*5, 2*5, 1*5... 和 3*4, 2*4, 1*4...) 最后將所有中間結(jié)果相加。觀察可知num2的每一位從低位到高位都需要與整個num1相乘得到一個中間結(jié)果。并且num2中越靠左的位越高位其對應(yīng)的中間結(jié)果在最后相加時需要向左“錯位”得越多本質(zhì)就是在末尾補零。因此我們可以將字符串相乘分解為兩個步驟實現(xiàn)一個輔助函數(shù)計算一個字符串num與一個單個數(shù)字字符ch的乘積返回字符串結(jié)果。這本質(zhì)上是一個簡單的“一位數(shù)乘法”同樣需要注意進位。主乘法邏輯遍歷num2的每一位從低位開始用這位數(shù)字字符與整個num1相乘調(diào)用步驟1的輔助函數(shù)得到中間結(jié)果字符串。然后根據(jù)當(dāng)前位在num2中的位置第幾位在中間結(jié)果的末尾補上相應(yīng)數(shù)量的零i位就補i個零。最后將所有補零后的中間結(jié)果通過我們之前實現(xiàn)的字符串相加函數(shù)累加起來得到最終乘積。這個“分解-相加”的策略完美復(fù)用了字符串相加的功能使得乘法實現(xiàn)變得模塊化且清晰。它避免了直接處理多層嵌套進位的復(fù)雜性。注意這里有一個常見的性能優(yōu)化點。上述方法的時間復(fù)雜度是 O(m * n n^2)假設(shè) m 和 n 是字符串長度因為我們需要進行 n 次字符串相加而每次相加的字符串長度可能接近 mn。更優(yōu)的算法是直接用一個長度為mn的數(shù)組來存儲最終結(jié)果的每一位在一次嵌套循環(huán)中同時計算乘積累加和進位可以將復(fù)雜度優(yōu)化到 O(m * n)。但為了思路清晰和教學(xué)目的我們先從易于理解的“錯位相加法”開始。3. 字符串相加的完整實現(xiàn)與細節(jié)剖析理論清晰了我們開始動手寫代碼。我將使用 Python 語言進行演示因其語法清晰易于理解。其他語言的思路完全一致。3.1 基礎(chǔ)版本實現(xiàn)我們先實現(xiàn)一個基礎(chǔ)、未優(yōu)化的版本以徹底理解流程。def addStrings(num1: str, num2: str) - str: 返回兩個非負(fù)整數(shù)字符串 num1 和 num2 的和。 i, j len(num1) - 1, len(num2) - 1 # 指針從字符串末尾個位開始 carry 0 # 進位初始為0 result [] # 使用列表存儲結(jié)果數(shù)字字符效率高于字符串拼接 # 當(dāng)任意一個字符串還有位未處理或者還有進位時繼續(xù)循環(huán) while i 0 or j 0 or carry: # 獲取當(dāng)前位的數(shù)字如果指針已越界則用0補位 digit1 int(num1[i]) if i 0 else 0 digit2 int(num2[j]) if j 0 else 0 # 計算當(dāng)前位的和及新的進位 total digit1 digit2 carry current_digit total % 10 # 當(dāng)前位結(jié)果 carry total // 10 # 新的進位 # 將當(dāng)前位數(shù)字字符加入結(jié)果列表注意是追加最后需要反轉(zhuǎn) result.append(str(current_digit)) # 移動指針 i - 1 j - 1 # 由于是從低位開始追加的需要反轉(zhuǎn)列表得到正確順序 result.reverse() return .join(result)逐行解析與實操要點指針初始化i和j分別指向num1和num2的最后一個字符即個位。這是模擬豎式從右向左計算的關(guān)鍵。使用列表存儲結(jié)果在循環(huán)中我們不斷在result列表的末尾追加數(shù)字字符。如果使用字符串的操作每次都會創(chuàng)建新的字符串對象在循環(huán)中效率很低。列表的append操作是 O(1) 的最后再用‘’.join(result)一次性轉(zhuǎn)換為字符串效率高得多。這是一個重要的性能優(yōu)化習(xí)慣。循環(huán)條件while i 0 or j 0 or carry:這是最容易出錯的地方之一。條件不能只是i 0 or j 0??紤]“5” “5”當(dāng)i和j都變?yōu)?-1 時循環(huán)如果結(jié)束我們就漏掉了最后產(chǎn)生的進位1結(jié)果是“10”。因此必須加上or carry確保所有進位都被處理。補零操作digit1 int(num1[i]) if i 0 else 0。當(dāng)某個字符串的指針已經(jīng)遍歷完i 0我們就認(rèn)為該位是0。這優(yōu)雅地處理了長度不同的字符串相加。進位計算total % 10取個位得到當(dāng)前位結(jié)果total // 10取十位得到新的進位。這是十進制運算的核心。結(jié)果反轉(zhuǎn)因為我們是先計算個位然后十位、百位……并依次追加到result中所以result里存儲的順序是 [個位 十位 百位…]。最后需要reverse()反轉(zhuǎn)才能得到從高位到低位的正確字符串。3.2 測試與邊界條件驗證寫完代碼必須用多種情況測試。我們可以設(shè)計一個簡單的測試集# 測試用例 test_cases [ (“0”, “0”, “0”), (“123”, “456”, “579”), (“999”, “1”, “1000”), # 測試連續(xù)進位 (“1”, “999”, “1000”), # 交換順序 (“123456789”, “987654321”, “1111111110”), # 大數(shù)和長度增加 (“”, “123”, “123”), # 空字符串處理假設(shè)空串視為”0” (“123”, “”, “123”), ] for num1, num2, expected in test_cases: # 處理空字符串在實際函數(shù)中我們假設(shè)輸入合法非空。這里為測試做保護。 num1 num1 if num1 else “0” num2 num2 if num2 else “0” result addStrings(num1, num2) print(f”‘{num1}’ ‘{num2}’ ‘{result}’ 預(yù)期 ‘{expected}’ {‘正確’ if result expected else ‘錯誤’}“)注意事項與心得輸入驗證生產(chǎn)環(huán)境中函數(shù)開頭應(yīng)添加輸入驗證。確保num1和num2都是只包含數(shù)字字符‘0’-‘9’的非空字符串。對于空字符串或非法字符應(yīng)拋出明確的異常或返回錯誤標(biāo)識。前導(dǎo)零問題我們的算法可能會產(chǎn)生前導(dǎo)零嗎考慮“0” “0”結(jié)果是“0”正確??紤]“000” “123”由于我們直接按字符轉(zhuǎn)換數(shù)字“000”會被當(dāng)作0處理結(jié)果是“123”這通常是符合數(shù)學(xué)語義的整數(shù)000就是0。但如果要求嚴(yán)格保留輸入格式則需要額外處理。通常在最終返回前可以去掉結(jié)果中除了單個‘0’之外的所有前導(dǎo)零。性能該算法的時間復(fù)雜度是 O(max(m, n))空間復(fù)雜度也是 O(max(m, n))用于存儲結(jié)果列表對于大數(shù)運算是非常高效的。4. 字符串相乘的完整實現(xiàn)與優(yōu)化探討有了可靠的addStrings函數(shù)作為基石實現(xiàn)乘法就變得有章可循。我們先實現(xiàn)直觀的“錯位相加法”。4.1 實現(xiàn)“一位數(shù)”乘法輔助函數(shù)這個函數(shù)計算一個數(shù)字字符串num與一個單個數(shù)字字符digit_char的乘積。def multiplyOneDigit(num: str, digit_char: str) - str: “”“返回字符串 num 與單個數(shù)字字符 digit_char 的乘積字符串?!薄啊?if digit_char ‘0’: return ‘0’ # 任何數(shù)乘以0都得0快速返回 if digit_char ‘1’: return num # 任何數(shù)乘以1都得自身快速返回 digit int(digit_char) carry 0 result [] # 從 num 的個位開始乘 for i in range(len(num) - 1, -1, -1): product int(num[i]) * digit carry current_digit product % 10 carry product // 10 result.append(str(current_digit)) # 處理最后的進位 if carry: result.append(str(carry)) result.reverse() return ‘’.join(result)要點解析快速路徑對于乘數(shù)digit_char是‘0’或‘1’的情況直接返回可以避免不必要的計算。這是一個簡單但有效的優(yōu)化。邏輯類似加法同樣是逆序遍歷、計算乘積、處理進位、反轉(zhuǎn)結(jié)果。區(qū)別在于這里是乘法口訣表里的“一位乘多位”。4.2 實現(xiàn)主乘法函數(shù)錯位相加法現(xiàn)在我們利用addStrings和multiplyOneDigit來實現(xiàn)完整的乘法。def multiplyStrings(num1: str, num2: str) - str: if num1 “0” or num2 “0”: return “0” # 任何數(shù)與0相乘都得0 result “0” # 初始結(jié)果為0 len_num2 len(num2) # 遍歷 num2 的每一位從低位即末尾開始 for i in range(len_num2 - 1, -1, -1): digit_char num2[i] # num2 的當(dāng)前位數(shù)字字符 # 1. 計算 num1 * 當(dāng)前位數(shù)字 partial_product multiplyOneDigit(num1, digit_char) # 2. 根據(jù)當(dāng)前位的位置補零錯位 # num2 的倒數(shù)第1位個位補0個零倒數(shù)第2位十位補1個零依此類推。 zeros_to_append (len_num2 - 1 - i) if partial_product ! “0”: # 如果部分積是0補零也沒意義 partial_product ‘0’ * zeros_to_append # 3. 將補零后的部分積加到總結(jié)果中 result addStrings(result, partial_product) return result關(guān)鍵步驟與操作意圖零值處理如果任意一個乘數(shù)為“0”乘積必然是“0”。這是一個重要的邊界條件也避免了后續(xù)無意義的計算。遍歷順序for i in range(len_num2 - 1, -1, -1)確保了我們從num2的個位開始計算。變量i是索引。錯位計算zeros_to_append (len_num2 - 1 - i)是核心。當(dāng)i指向個位i len_num2 - 1時zeros_to_append 0不補零。當(dāng)i指向十位時zeros_to_append 1補一個零相當(dāng)于結(jié)果左移一位數(shù)值乘以10。這完美模擬了豎式中“錯一位寫”的動作。累加初始化result “0”然后不斷將補零后的部分積partial_product累加進去。這里充分復(fù)用了我們之前寫的addStrings函數(shù)。4.3 測試乘法函數(shù)同樣我們需要用多種用例測試。# 乘法測試用例 multiply_test_cases [ (“0”, “123”, “0”), (“123”, “0”, “0”), (“123”, “1”, “123”), (“2”, “3”, “6”), (“12”, “12”, “144”), (“99”, “99”, “9801”), # 測試進位 (“123”, “456”, “56088”), # 標(biāo)準(zhǔn)用例 (“999”, “999”, “998001”), (“123456789”, “987654321”, “121932631112635269”), # 大數(shù)乘法 ] for num1, num2, expected in multiply_test_cases: res multiplyStrings(num1, num2) print(f”‘{num1}’ * ‘{num2}’ ‘{res}’ 預(yù)期 ‘{expected}’ {‘正確’ if res expected else ‘錯誤’}“)4.4 性能分析與優(yōu)化直接定位法“錯位相加法”易于理解但存在性能問題。假設(shè)num1長度為mnum2長度為n。multiplyOneDigit復(fù)雜度為 O(m)。我們需要調(diào)用multiplyOneDigit共n次。每次乘法后我們調(diào)用addStrings來累加。addStrings的復(fù)雜度取決于當(dāng)前result和partial_product的長度最壞情況下result的長度會增長到mn而我們需要進行n次這樣的加法。因此總時間復(fù)雜度粗略為 O(m * n n * (mn))可以近似為 O(n^2 m*n)。當(dāng)m和n很大時比如都是1000位效率較低。更優(yōu)的算法直接定位法豎式優(yōu)化法我們可以觀察乘法的豎式發(fā)現(xiàn)結(jié)果的每一位res[x]可以由num1和num2的某些位乘積求和得到。具體來說設(shè)num1[i]和num2[j]相乘其乘積會影響結(jié)果的第[ij]和[ij1]位分別是個位和十位考慮進位。我們可以用一個長度為mn的數(shù)組res_arr來存儲最終結(jié)果的每一位初始為0。然后使用兩層循環(huán)遍歷num1和num2的每一位for i in range(m-1, -1, -1): for j in range(n-1, -1, -1): mul int(num1[i]) * int(num2[j]) p1 i j # 乘積影響的低位在結(jié)果數(shù)組中的索引 p2 i j 1 # 乘積影響的高位在結(jié)果數(shù)組中的索引 sum_val mul res_arr[p2] # 將乘積加到當(dāng)前位上 res_arr[p2] sum_val % 10 # 更新當(dāng)前位 res_arr[p1] sum_val // 10 # 進位加到前一位兩層循環(huán)結(jié)束后res_arr中存儲了結(jié)果的每一位可能包含進位。我們需要處理數(shù)組中大于9的位因為進位可能累加并將其轉(zhuǎn)換為字符串。這種方法的優(yōu)勢在于只需要一次 O(m * n) 的雙層循環(huán)以及一次 O(mn) 的進位整理和字符串構(gòu)建總體復(fù)雜度為 O(m * n)比錯位相加法更優(yōu)??臻g復(fù)雜度為 O(mn)。實操心得對于面試或?qū)W習(xí)掌握“錯位相加法”足以證明你理解了問題的本質(zhì)和模塊化思想。在實際項目或性能要求高的場景特別是需要自己實現(xiàn)高精度運算庫時“直接定位法”是必須掌握的優(yōu)化。我建議先徹底理解并實現(xiàn)基礎(chǔ)版本再挑戰(zhàn)優(yōu)化版本這樣知識結(jié)構(gòu)更牢固。5. 常見問題與排查技巧實錄在實際編碼和面試中以下幾個問題是高頻出錯點5.1 問題一結(jié)果字符串順序錯誤癥狀輸入“123”和“456”期望得到“579”實際得到“975”或其他顛倒的結(jié)果。根因忘記在最后反轉(zhuǎn)結(jié)果列表。我們在計算時是從低位開始填充result列表的append操作使得低位在前。必須通過result.reverse()或從后往前構(gòu)建字符串來糾正順序。排查在循環(huán)中打印每一步的current_digit和result列表觀察其生長順序?;蛘哂米詈唵蔚挠美?” “2”進行單步調(diào)試。5.2 問題二遺漏最高位的進位癥狀輸入“5”和“5”期望得到“10”實際得到“0”。根因循環(huán)條件錯誤。只寫了while i 0 or j 0:當(dāng)兩個指針都變?yōu)?-1 時循環(huán)結(jié)束但此時進位carry還為 1沒有被處理。解決務(wù)必確保循環(huán)條件包含or carry。這是此類“模擬進位計算”題目的一個通用模板務(wù)必牢記。5.3 問題三乘法結(jié)果出現(xiàn)前導(dǎo)零癥狀輸入“123”和“0”期望得到“0”但可能得到“000”如果實現(xiàn)不當(dāng)或者“0”正確。根因在multiplyOneDigit函數(shù)中如果num是“123”digit_char是‘0’我們通過快速路徑返回“0”這是正確的。但在主函數(shù)multiplyStrings的累加過程中如果部分積是“0”我們依然將其補零后“000…”進行加法addStrings(“0”, “000”)可能會返回“000”這取決于addStrings是否做了去除前導(dǎo)零的處理。最佳實踐在最終返回結(jié)果前統(tǒng)一處理前導(dǎo)零??梢栽赼ddStrings和multiplyStrings的函數(shù)末尾添加一個清理步驟# 去除結(jié)果中除了單個‘0’之外的所有前導(dǎo)零 def trimLeadingZeros(s: str) - str: i 0 while i len(s) - 1 and s[i] ‘0’: # 保留最后一個字符防止全零字符串被清空 i 1 return s[i:]然后在返回‘’.join(result)或最終結(jié)果前調(diào)用trimLeadingZeros。注意“0”本身應(yīng)該被保留。5.4 問題四處理包含非數(shù)字字符或空字符串的輸入癥狀函數(shù)傳入“12a”或空字符串“”時崩潰或返回錯誤結(jié)果。根因缺乏輸入驗證。健壯性建議在生產(chǎn)代碼中應(yīng)在函數(shù)開始處進行嚴(yán)格的輸入校驗。def validateNumberString(s: str): if not s: # 檢查空字符串 raise ValueError(“Input string cannot be empty”) if not s.isdigit(): # 檢查是否全為數(shù)字字符 raise ValueError(f“Invalid character in number string: ‘{s}’”)在addStrings和multiplyStrings開頭調(diào)用此驗證函數(shù)或內(nèi)聯(lián)校驗。5.5 性能問題排查癥狀當(dāng)字符串長度非常大上萬位時程序運行緩慢或內(nèi)存占用高??赡茉蚣皟?yōu)化使用了字符串拼接在循環(huán)中使用result_str digit_char。務(wù)必改用列表append最后join。使用了“錯位相加法”進行乘法如前所述該方法有 O(n^2) 級別的加法操作。對于高性能場景應(yīng)改用“直接定位法”。不必要的類型轉(zhuǎn)換在熱循環(huán)中反復(fù)調(diào)用int(digit_char)??梢钥紤]預(yù)先把整個字符串轉(zhuǎn)換成整數(shù)列表[int(ch) for ch in num]但要注意這需要額外 O(n) 空間。對于大多數(shù)情況每次轉(zhuǎn)換的開銷可以接受。內(nèi)存結(jié)果列表result的長度最多為max(m,n)1加法或mn乘法在合理范圍內(nèi)。6. 擴展與變種思路掌握了基礎(chǔ)版本后我們可以思考一些變種和擴展這有助于深化理解。6.1 支持負(fù)數(shù)運算當(dāng)前的實現(xiàn)只支持非負(fù)整數(shù)。如果要支持負(fù)數(shù)的加減乘除我們需要在函數(shù)入口判斷字符串是否以‘-’開頭。剝離符號位記錄最終結(jié)果的符號。乘法是“同號得正異號得負(fù)”加法和減法需要比較絕對值大小。調(diào)用核心的無符號運算函數(shù)即我們上面實現(xiàn)的函數(shù)計算絕對值的運算結(jié)果。根據(jù)符號規(guī)則在結(jié)果前添加‘-’如果需要。這本質(zhì)上將問題轉(zhuǎn)化為了無符號運算和符號處理。6.2 實現(xiàn)字符串減法思路與加法類似但更復(fù)雜因為涉及借位。核心步驟確保被減數(shù)大于或等于減數(shù)如果要做絕對值減法。否則交換兩者并標(biāo)記結(jié)果為負(fù)。從低位開始相減如果不夠減則向高位借位。同樣需要注意最后結(jié)果的前導(dǎo)零處理。 減法比加法更容易出錯因為借位可能會連續(xù)發(fā)生例如“1000” - “1”。6.3 應(yīng)用于超大數(shù)計算場景我們實現(xiàn)的算法是“十進制”的。在計算機科學(xué)中為了最大化利用計算機的位運算能力高精度大數(shù)庫如 Python 的int類型底層、GMP 庫通常采用更高的進制作為基底比如 2^30 或 2^64。這樣一個“位”就能存儲一個很大的數(shù)從而減少運算的位數(shù)和循環(huán)次數(shù)極大提升性能。理解了我們這里的十進制模擬再去學(xué)習(xí)高進制如萬進制、億進制的實現(xiàn)就會容易得多。6.4 與語言內(nèi)置大數(shù)類型的對比像 Python、JavaBigInteger等語言本身就支持任意精度整數(shù)。為什么還要手動實現(xiàn)學(xué)習(xí)價值深刻理解運算原理和進位/借位機制是算法和計算機基礎(chǔ)素養(yǎng)的體現(xiàn)。面試需求這是經(jīng)典的面試題考察候選人的基本編碼能力、邊界條件處理和對細節(jié)的把握。特定環(huán)境限制在極少數(shù)嵌入式或特定限制的環(huán)境下可能無法使用語言的大數(shù)庫。自定義需求可能需要實現(xiàn)一些標(biāo)準(zhǔn)庫不支持的特殊運算或格式。我個人在項目中使用時99% 的情況會直接使用語言提供的高精度類型因為它們經(jīng)過極度優(yōu)化且絕對可靠。手動實現(xiàn)這些函數(shù)更像是一次深刻的“練兵”讓你在遇到更復(fù)雜的、沒有現(xiàn)成庫的模擬類問題時能夠游刃有余。