機與雙指針?biāo)惴ㄉ疃冉馕? alt=)
1. 從“數(shù)單詞”到理解字符串處理的本質(zhì)最近在輔導(dǎo)一些剛接觸編程的同學(xué)時發(fā)現(xiàn)一個挺有意思的現(xiàn)象很多人拿到“計算一行單詞長度”這樣的題目第一反應(yīng)是去翻書找有沒有現(xiàn)成的“單詞分割函數(shù)”。這本身沒錯但如果你只停留在調(diào)用split()然后循環(huán)輸出len()的層面那可能就錯過了這道題背后更重要的東西——對輸入流和字符串底層邏輯的親手把控。這道來自《信息學(xué)奧賽一本通》的1142題表面看是簡單的輸入輸出和字符串處理實則是訓(xùn)練我們“親手拆解數(shù)據(jù)”能力的絕佳起點。它強迫你離開高級函數(shù)的舒適區(qū)去直面最原始的字符序列理解空格如何作為分隔符以及程序如何“一個字符一個字符”地構(gòu)建出對數(shù)據(jù)的認(rèn)知。今天我們就來徹底拆解這道題不僅給出能AC的代碼更要弄明白每一步“為什么這么做”以及在這個過程中那些容易栽跟頭的細(xì)節(jié)。2. 題目深度解析邊界條件與核心陷阱題目描述很短“輸入一行單詞序列相鄰單詞之間由1個或多個空格間隔請對應(yīng)地計算各個單詞的長度?!?但這里面藏著好幾個需要明確的關(guān)鍵點也是評判程序是否健壯的核心。2.1 輸入格式的明確與“一行”的含義首先“輸入一行”這個描述在編程中需要精確化。在控制臺或文件輸入中“一行”通常意味著以換行符\n作為結(jié)束標(biāo)志的一段字符序列。對于C可能是cin配合getline對于Python就是input()。這里的關(guān)鍵是我們必須一次性讀入整行而不是用cin stringC或input().split()Python的默認(rèn)行為因為后者會自動忽略開頭的空白符并以下一個空白符空格、制表符、換行作為截斷點這恰恰會讓我們丟失“連續(xù)多個空格”這一關(guān)鍵信息。所以第一步必須是讀取整行。2.2 “單詞”的定義與分隔符的處理題目說“相鄰單詞之間由1個或多個空格間隔”。這意味著分隔符唯一只有空格 是分隔符不包括制表符\t或其他空白字符。這是一個簡化但在嚴(yán)格的在線評測OJ環(huán)境下我們必須嚴(yán)格按照題意來??崭駭?shù)量不定可能是1個也可能是多個。這直接否決了用“遇到一個空格就切分”的簡單邏輯因為多個連續(xù)空格會產(chǎn)生空字符串片段這些片段不是單詞必須被跳過。開頭和結(jié)尾可能有空格輸入的行開頭和結(jié)尾也可能有空格。這些空格不構(gòu)成單詞在計算時必須被忽略。這是最常見的陷阱之一很多人的程序在遇到 hello world 這樣的輸入時就會出錯。2.3 輸出格式的精確對應(yīng)“對應(yīng)地計算各個單詞的長度”意味著輸出順序必須與單詞在輸入行中出現(xiàn)的順序嚴(yán)格一致。輸出通常是以空格分隔的每個單詞的長度。如果一行沒有單詞例如輸入全是空格根據(jù)常規(guī)邏輯應(yīng)該沒有輸出或者輸出一個空行。這些細(xì)節(jié)需要在編碼前就想清楚。3. 核心算法設(shè)計狀態(tài)機與雙指針法解決這類問題有兩種主流的底層思想狀態(tài)機State Machine和雙指針Two Pointers。它們都不依賴于高級的字符串分割函數(shù)能讓我們更透徹地理解過程。3.1 狀態(tài)機思路清晰地刻畫程序“思維”我們可以把程序讀取字符的過程看作是在幾個狀態(tài)間切換狀態(tài)0尋找單詞開始初始狀態(tài)。程序逐個讀取字符如果遇到非空格字符說明找到了一個單詞的開頭記錄當(dāng)前位置并切換到狀態(tài)1。狀態(tài)1記錄單詞中。繼續(xù)讀取字符只要不是空格就認(rèn)為字符屬于當(dāng)前單詞持續(xù)累加長度或記錄位置。一旦遇到空格說明當(dāng)前單詞結(jié)束輸出其長度然后切換回狀態(tài)0。這個思路邏輯非常清晰尤其適合用while循環(huán)和if-else來實現(xiàn)。它明確區(qū)分了“在單詞外”和“在單詞內(nèi)”兩種情形不容易出錯。C狀態(tài)機實現(xiàn)示例#include iostream #include string using namespace std; int main() { string line; getline(cin, line); // 讀取整行 int n line.length(); int i 0; bool inWord false; // 狀態(tài)標(biāo)志是否處于單詞內(nèi) int wordLen 0; while (i n) { if (line[i] ! ) { // 當(dāng)前字符不是空格 if (!inWord) { // 如果之前不在單詞內(nèi)說明是單詞開頭 inWord true; wordLen 1; // 開始計數(shù) } else { // 已經(jīng)在單詞內(nèi)繼續(xù)計數(shù) wordLen; } } else { // 當(dāng)前字符是空格 if (inWord) { // 如果之前是在單詞內(nèi)說明單詞結(jié)束了 cout wordLen ; inWord false; wordLen 0; } // 如果之前就不在單詞內(nèi)說明是連續(xù)空格直接跳過 } i; } // 循環(huán)結(jié)束后檢查是否最后一個字符是單詞結(jié)尾即行末沒有空格結(jié)尾 if (inWord) { cout wordLen; } return 0; }3.2 雙指針?biāo)悸犯咝Фㄎ粏卧~邊界雙指針法更直觀一些。我們用兩個“指針”通常是整數(shù)索引i和j來在字符串上滑動。指針i負(fù)責(zé)尋找單詞的起始位置。它不斷向前移動跳過所有的空格直到指向一個非空格字符。此時i的位置就是單詞的開始。指針j從i開始尋找單詞的結(jié)束位置。它繼續(xù)向前移動只要指向的不是空格就繼續(xù)移動。當(dāng)j指向空格或字符串末尾時j-1的位置就是單詞的結(jié)束。計算單詞長度j - i。輸出長度后將i移動到j(luò)的位置即空格處然后重復(fù)步驟1開始尋找下一個單詞。這種方法代碼緊湊效率高是競賽中的常用技巧。Python雙指針實現(xiàn)示例line input().rstrip(\n) # 讀取整行并去掉末尾可能的換行符input()通常已處理但更安全 n len(line) i 0 first_output True # 用于控制輸出空格使格式更美觀 while i n: # 階段1跳過前導(dǎo)空格和單詞間的多個空格 while i n and line[i] : i 1 if i n: # 如果跳完空格已經(jīng)到字符串末尾說明沒有單詞了 break # 此時 line[i] 是非空格字符即單詞開始 start i # 階段2找到這個單詞的結(jié)尾 while i n and line[i] ! : i 1 # 此時 i 指向了單詞后的第一個空格或字符串末尾 word_len i - start if not first_output: print( , end) print(word_len, end) first_output False # 循環(huán)結(jié)束所有單詞長度已輸出。如果一行沒有單詞則無輸出。 print() # 最后輸出一個換行符合多數(shù)OJ的格式要求注意上面Python示例中使用了first_output標(biāo)志來控制輸出格式避免了末尾多一個空格。有些OJ系統(tǒng)對末尾空格不敏感但養(yǎng)成輸出整潔的習(xí)慣總是好的。更簡單的做法是用列表先存儲結(jié)果最后用print(*length_list)一次性輸出。4. 不同語言下的實現(xiàn)策略與避坑指南雖然算法思想通用但在不同編程語言中實現(xiàn)細(xì)節(jié)和可利用的工具庫不同也會產(chǎn)生不同的“坑”。4.1 C/C 實現(xiàn)注重手動控制與效率對于C/C選手這道題是練習(xí)字符數(shù)組C風(fēng)格字符串和string類操作的經(jīng)典題??狱c1輸入整行C風(fēng)格用fgets(char_array, sizeof(char_array), stdin)。注意它會讀入換行符\n處理時需要判斷。C風(fēng)格用getline(cin, str)。這是最推薦的方式簡單安全??狱c2遍歷與邊界手動遍歷時務(wù)必注意數(shù)組下標(biāo)不要越界。在雙指針法中內(nèi)層while循環(huán)的條件i n至關(guān)重要??狱c3輸出格式C中連續(xù)用cout len ;輸出最后會多一個空格。雖然很多OJ接受但嚴(yán)格的題目可能判錯??梢圆捎妙愃芇ython中的“首次輸出”技巧。一個健壯的C雙指針實現(xiàn)#include iostream #include string using namespace std; int main() { string s; getline(cin, s); int n s.size(); int i 0; bool isFirst true; // 是否是第一個輸出的數(shù)字 while (i n) { // 跳過空格 while (i n s[i] ) i; if (i n) break; // 跳過空格后到末尾結(jié)束 // 找到單詞結(jié)束位置 int j i; while (j n s[j] ! ) j; // 計算并輸出長度 if (!isFirst) cout ; cout (j - i); isFirst false; i j; // i跳到當(dāng)前單詞結(jié)束的位置即空格處外層循環(huán)的i會使其進入下一個循環(huán) } cout endl; // 輸出換行 return 0; }4.2 Python 實現(xiàn)簡潔背后的陷阱Python讓這道題變得極其簡單但正因為簡單更容易忽略細(xì)節(jié)?!耙恍写a”的誘惑與問題print(*[len(w) for w in input().split()])這行代碼利用了input()讀取一行split()默認(rèn)以任意空白字符空格、換行、制表符等分割并自動過濾掉空字符串列表推導(dǎo)式計算長度最后用*解包打印。對于本題它完全正確且優(yōu)雅。因為題目明確分隔符是空格split()的行為完全符合要求。但是這里存在一個教學(xué)上的“陷阱”如果你只記住了這行代碼而沒有理解split()在沒有參數(shù)和有參數(shù)時的區(qū)別下次遇到“以逗號分隔”或者“嚴(yán)格以單個空格分隔需保留空單詞”的題目時就會出錯。s.split(): 按任意空白字符分割并自動移除結(jié)果中的空字符串。s.split( ): 嚴(yán)格按單個空格字符分割。如果存在連續(xù)空格會產(chǎn)生空字符串。所以更嚴(yán)謹(jǐn)?shù)慕虒W(xué)代碼應(yīng)該這樣寫以明確意圖line input() # 明確使用空格作為分隔符但這樣會得到空字符串片段 parts line.split( ) # 因此需要過濾掉空字符串 words [part for part in parts if part ! ] # 再計算長度 lengths [len(word) for word in words] print(*lengths)雖然最終結(jié)果和split()一樣但這個過程清晰地展示了“分割-過濾-計算”的步驟加深了對字符串處理的理解。4.3 其他語言如Java的注意點Java中常用的Scanner.nextLine()讀取整行然后用line.split( )可以按一個或多個空格進行正則分割但同樣要注意開頭結(jié)尾的空格會導(dǎo)致空字符串。更穩(wěn)健的做法是使用line.trim().split(\\s)先去掉首尾空格再按空白字符分割。但trim()可能會誤傷首尾的非空格字符本題不會所以最根本的還是手動遍歷或使用StringTokenizer類雖然已過時但思路清晰。5. 測試用例設(shè)計與調(diào)試技巧寫出代碼不算完能否通過所有邊界情況的測試才是關(guān)鍵。自己設(shè)計測試用例是一個優(yōu)秀程序員必備的習(xí)慣。針對本題必須設(shè)計以下幾類測試用例普通情況hello world-5 5多個連續(xù)空格hello world-5 5開頭有空格 hello world-5 5結(jié)尾有空格hello world -5 5首尾都有空格 hello world -5 5單個單詞programming-11單個單詞帶空格 programming -11空行或純空格或 - 無輸出或輸出一個空行長單詞與短單詞混合a bc def ghij-1 2 3 4調(diào)試技巧打印中間變量在狀態(tài)機或雙指針?biāo)惴ㄖ性陉P(guān)鍵步驟后打印i,j,inWord,wordLen等變量的值觀察其變化是否符合預(yù)期。可視化遍歷在紙上畫出字符串手動模擬你的算法用筆移動i和j指針這是理解算法最有效的方式。使用在線調(diào)試器如果環(huán)境允許使用IDE的調(diào)試功能單步執(zhí)行觀察變量和程序流程。6. 從本題延伸的常見變體與解題思路掌握了本題的核心可以輕松解決一系列變體問題這也是刷題舉一反三的關(guān)鍵。變體1統(tǒng)計單詞個數(shù)而非長度。這更簡單只需要在發(fā)現(xiàn)一個單詞開始時狀態(tài)機中!inWord變?yōu)閠rue或雙指針中找到start時計數(shù)器加1即可。變體2以特定單個字符如逗號分隔。這時分隔符不是空格了。算法完全一樣只需把判斷條件從line[i] ! 改為line[i] ! ,。但要注意如果用split(,)連續(xù)逗號會產(chǎn)生空字符串需要根據(jù)題目要求決定是否保留。變體3分隔符是多種字符如空格、逗號、句號。此時判斷“是否分隔符”的條件變成一個集合檢查。例如if (delimiters.find(line[i]) string::npos)C或者if line[i] not in ,.Python。核心算法框架不變。變體4不僅輸出長度還要輸出單詞本身。在記錄長度的同時用substr(start, length)C或切片line[start:end]Python把單詞子串也保存下來即可。變體5輸入包含多行直到文件結(jié)束EOF。這是OJ常見格式。需要將整個讀取和處理的邏輯包在一個while (getline(cin, line))或for line in sys.stdin:的循環(huán)里。每行獨立處理輸出各自的結(jié)果。通過這樣一道看似簡單的題目我們實際上深入探討了字符串處理的基石輸入緩沖、字符遍歷、狀態(tài)管理、邊界條件處理。這才是學(xué)習(xí)算法和編程的正確姿勢——不滿足于AC而要理解每一行代碼背后的“所以然”。下次再遇到字符串處理問題不妨先想想我的指針應(yīng)該怎么走程序現(xiàn)在處于什么狀態(tài)