
1. 項目概述從一道題看編程思維的構建最近在帶新人刷題發現很多人對LeetCode 771這道“寶石與石頭”的題有種復雜的情緒。一方面覺得它太簡單看一眼就知道用集合Set或者哈希表Hash Table來解另一方面真正動手寫的時候又會在邊界條件、代碼簡潔性或者不同解法的性能差異上栽跟頭。這道題就像編程世界里的“Hello World Plus”它不滿足于讓你打印一句話而是要求你處理數據、應用基礎數據結構并輸出一個有意義的結果。今天我就以這道題為例拆解一下如何從“看懂題”到“寫出好代碼”并深入聊聊那些看似簡單背后的設計考量與性能玄機。題目本身很簡單給你一個字符串jewels代表寶石類型另一個字符串stones代表你擁有的石頭。stones中的每個字符代表一種石頭jewels中的每個字符代表一種寶石。你需要統計stones中有多少顆“石頭”是“寶石”。換句話說就是統計stones中有多少個字符出現在jewels中。例如jewels “aA”,stones “aAAbbbb”那么寶石‘a’和‘A’在石頭中出現了3次‘a’出現1次‘A’出現2次。這題的核心價值不在于算法有多高深而在于它完美地詮釋了“問題抽象 - 數據結構選擇 - 代碼實現 - 優化分析”這一完整的編程思維鏈條。無論是剛入門的新手還是想鞏固基礎的老手重新審視這道題都能有新的收獲。接下來我們就一步步拆解。1.1 核心需求與抽象建模面對任何編程問題第一步永遠是理解并抽象需求。LeetCode 771的需求非常明確輸入兩個字符串jewels和stones。處理判斷stones中的每個字符是否存在于jewels這個字符集合中。輸出滿足條件的字符個數計數。抽象來看這是一個典型的集合成員判定與計數問題。jewels定義了一個“有效字符”的集合我們需要遍歷stones這個序列并對每個元素進行“是否屬于某個集合”的檢查屬于則計數加一。這個抽象直接指向了兩個關鍵操作高效查找Lookup我們需要頻繁地查詢一個字符是否在寶石類型中。在編程中數組列表的按值查找是O(n)的而哈希表在Python中是set或dict的查找平均是O(1)的。因此將jewels轉換為一個支持高效查找的數據結構是優化的關鍵。遍歷與計數Iteration Counting我們需要訪問stones中的每一個字符。這是一個標準的線性遍歷過程。理解到這一層解決方案的大方向就確定了將jewels預處理為哈希集合set然后遍歷stones進行計數。這就是從問題描述到技術方案的第一次跳躍。2. 解法深度剖析從暴力到優雅很多人覺得這道題只有一種解法其實不然。從最直觀的暴力法到最Pythonic的寫法中間體現了對語言特性和算法效率的不同理解層次。我們逐一分析。2.1 解法一雙重循環暴力法新手易犯這是最直觀的解法也是很多新手在不假思索時的第一反應。class Solution: def numJewelsInStones(self, jewels: str, stones: str) - int: count 0 for stone in stones: for jewel in jewels: if stone jewel: count 1 break # 找到即可跳出內層循環 return count原理與復雜度分析原理對stones中的每一顆“石頭”外層循環都去jewels字符串中從頭到尾掃描一遍內層循環看是否存在相同的字符。時間復雜度O(m * n)其中 m 是stones的長度n 是jewels的長度。在最壞情況下沒有寶石或只有最后一顆是寶石需要對每個石頭遍歷整個寶石串。空間復雜度O(1)只使用了常數級別的額外空間一個計數變量。注意雖然這段代碼邏輯正確但在LeetCode上提交當字符串長度較大時很容易因為超時Time Limit Exceeded而失敗。它揭示了算法設計中一個基本原則減少不必要的重復計算。這里jewels被反復遍歷了m次這是性能瓶頸。2.2 解法二哈希集合標準解法這是本題的標準答案也是面試官期望看到的解法。它通過空間換時間將查找效率從O(n)提升到O(1)。class Solution: def numJewelsInStones(self, jewels: str, stones: str) - int: jewel_set set(jewels) # 關鍵步驟預處理為集合 count 0 for stone in stones: if stone in jewel_set: # 集合的in操作平均時間復雜度為O(1) count 1 return count原理與復雜度分析原理jewel_set set(jewels)將字符串jewels轉換為一個集合。集合的特性是元素唯一且基于哈希表實現支持平均時間復雜度為O(1)的成員查詢in操作。遍歷stones對于每個字符用in操作判斷其是否在jewel_set中。時間復雜度O(m n)。其中構建集合需要遍歷jewels復雜度O(n)遍歷stones并進行m次查找由于每次查找是O(1)所以總復雜度為O(m)。整體是線性復雜度。空間復雜度O(n)用于存儲寶石集合。在最壞情況下所有字符都不同需要存儲n個字符。為什么用set而不用list這是核心考點。if stone in list的操作對于列表list是線性查找復雜度O(n)。而if stone in set對于集合是平均常數查找。當n很大時兩者的性能差異是天壤之別。將jewels預存為集合相當于為后續的m次查詢制作了一份“速查表”。2.3 解法三利用Python內置函數與生成器Pythonic寫法對于Python而言我們可以寫出更簡潔、更具表達力的代碼。class Solution: def numJewelsInStones(self, jewels: str, stones: str) - int: jewel_set set(jewels) # 方法1使用生成器表達式與sum return sum(1 for stone in stones if stone in jewel_set) # 方法2直接使用集合的交集思想需稍作轉換 # return sum(stone in jewel_set for stone in stones)原理與技巧sum(1 for stone in stones if stone in jewel_set)這是一個生成器表達式。它并不立即創建一個完整的列表而是產生一個迭代器。對于stones中的每個stone如果它在jewel_set中就生成一個1否則不生成。sum()函數則將這些1累加起來得到總數。這種方式內存效率極高尤其適合處理超長字符串。sum(stone in jewel_set for stone in stones)這里利用了Python中布爾值True/False在算術運算中會被當作1/0的特性。表達式stone in jewel_set的結果是布爾值sum會自動將其轉換為整數求和。寫法更短但可讀性稍遜于顯式寫1。哪種寫法更好在算法競賽或面試中解法二顯式循環通常是更安全、更清晰的選擇因為它清晰地展示了“預處理集合”和“遍歷計數”兩個步驟意圖明確所有語言的面試官都能看懂。而在實際Python工程或追求代碼簡潔時解法三生成器表達式是更地道的Python風格性能相同且更優雅。2.4 解法四基于字典Counter的計數我們還可以換一個角度思考先統計stones中每種石頭出現的次數然后只累加那些是寶石的石頭數量。from collections import Counter class Solution: def numJewelsInStones(self, jewels: str, stones: str) - int: stone_counter Counter(stones) # 統計石頭頻率例如 {a:1, A:2, b:4} jewel_set set(jewels) count 0 for stone, freq in stone_counter.items(): if stone in jewel_set: count freq return count原理與適用場景原理Counter(stones)會遍歷一次stones生成一個字典記錄每個字符及其出現的次數。然后我們遍歷這個計數器如果字符是寶石就將其頻率累加到結果中。時間復雜度O(m n)。遍歷stones構建Counter是O(m)遍歷Counter最多m個鍵并查找是O(k)k是stones中不同字符的數量總體仍是線性。空間復雜度O(m)在最壞情況下所有字符都不同Counter需要存儲m個鍵值對。與解法二的對比當stones中重復字符非常多時這種方法的遍歷次數可能更少。解法二需要遍歷stones的每個字符m次而本方法只需要遍歷stones中不同的字符k次k ≤ m。如果stones是”aaaaabbbbb…”這種大量重復的k很小本方法在常數項上有優勢。但是它引入了額外的數據結構Counter空間開銷通常比單純的jewel_set大。對于本題的常規輸入解法二集合在時間和空間的平衡上是最優的也是面試中最常見的答案。3. 關鍵細節與邊界條件處理即使是一個簡單的題目健壯的代碼也需要考慮邊界情況。以下是幾個容易忽略的細節3.1 輸入字符串可能為空題目沒有明確說明字符串不會為空因此防御性編程是好的習慣。def numJewelsInStones(self, jewels: str, stones: str) - int: if not jewels: # 如果沒有寶石那么結果一定是0 return 0 jewel_set set(jewels) # ... 后續邏輯不變實際上即使jewels為空set(jewels)會得到一個空集合后續遍歷stones時if stone in empty_set永遠為False結果也是0。所以從功能上講不特殊處理也是正確的。但顯式處理空輸入能使代碼意圖更清晰。3.2 字符集與大小寫敏感題目示例使用了大小寫字母這提示我們本題是大小寫敏感的。‘a‘和’A‘被視為不同的寶石類型。這是很多字符串處理題目的常見陷阱。我們的基于集合的解法天然支持這一點因為集合中的’a‘和’A‘就是兩個不同的元素。3.3 選擇set而非list的再強調我見過有人寫出這樣的代碼# 低效代碼 jewel_list list(jewels) # 或者直接 jewels_str jewels count 0 for stone in stones: if stone in jewel_list: # 這里是O(n)的線性查找 count 1這本質上和暴力法沒有區別只是把內層循環隱藏在了in操作符里。務必記住對list使用in操作符是線性查找。這是本題最核心的考察點之一。3.4 空間復雜度的權衡有同學可能會問既然jewels和stones都只包含英文字母那是不是可以用一個大小為5226大寫26小寫的布爾數組或列表來代替setdef numJewelsInStones(self, jewels: str, stones: str) - int: is_jewel [False] * 128 # ASCII碼范圍更安全 for c in jewels: is_jewel[ord(c)] True # 標記寶石字符 count 0 for c in stones: if is_jewel[ord(c)]: count 1 return count這完全是一個可行的、并且在某些情況下更優的解法時間復雜度O(mn)同樣是線性。空間復雜度O(1)因為數組大小是固定的128或52與輸入規模無關。優點查找速度極快是直接的數組索引操作O(1)常數項時間可能比哈希集合的查找更小。缺點通用性稍差。如果題目擴展了字符范圍比如包含數字、符號、Unicode字符這個固定大小的數組就需要調整或變得不適用。而set可以處理任意可哈希的元素。在面試中提出這種基于數組的解法并分析其與哈希集合的優劣能很好地展示你對計算機基礎ASCII、數組和問題泛化能力的思考。4. 性能測試與對比分析“紙上得來終覺淺絕知此事要躬行。” 我們寫一段簡單的測試代碼來直觀感受一下不同解法在性能上的差異。import timeit import random # 生成測試數據 def generate_test_case(length_j, length_s): # 假設字符范圍是大小寫字母 chars [chr(i) for i in range(ord(a), ord(z)1)] [chr(i) for i in range(ord(A), ord(Z)1)] jewels .join(random.choices(chars, klength_j)) stones .join(random.choices(chars, klength_s)) return jewels, stones # 測試函數 def test_performance(): jewels, stones generate_test_case(50, 1000000) # 50種寶石100萬顆石頭 sol Solution() # 測試暴力法 (對于大數據會很慢這里用小數據測試其正確性即可) # print(Brute Force:, timeit.timeit(lambda: sol.numJewelsInStones_brute(jewels[:5], stones[:100]), number10)) print(fTest with |J|{len(jewels)}, |S|{len(stones)}) print(- * 40) # 哈希集合法 t_set timeit.timeit(lambda: sol.numJewelsInStones_set(jewels, stones), number10) print(fHash Set Method: {t_set:.4f} seconds) # Pythonic生成器法 t_gen timeit.timeit(lambda: sol.numJewelsInStones_gen(jewels, stones), number10) print(fGenerator Method: {t_gen:.4f} seconds) # Counter法 t_cnt timeit.timeit(lambda: sol.numJewelsInStones_counter(jewels, stones), number10) print(fCounter Method: {t_cnt:.4f} seconds) # 固定數組法 t_arr timeit.timeit(lambda: sol.numJewelsInStones_array(jewels, stones), number10) print(fArray Index Method:{t_arr:.4f} seconds) if __name__ __main__: test_performance()在我的環境中運行一次可能得到類似下面的結果具體時間因機器而異Test with |J|50, |S|1000000 ---------------------------------------- Hash Set Method: 0.0987 seconds Generator Method: 0.1021 seconds Counter Method: 0.1354 seconds Array Index Method:0.0753 seconds結果分析哈希集合法和生成器法性能幾乎一致印證了它們本質是相同的算法只是寫法不同。Counter法稍慢因為它需要額外構建一個完整的頻率字典這個開銷在石頭種類很多時比較明顯。固定數組法表現最好因為它的查找是純粹的數組索引沒有哈希計算的開銷。這驗證了我們之前的理論分析。所有O(mn)復雜度的方法在處理百萬級數據時都在零點幾秒內完成而暴力法O(m*n)在此數據規模下將完全不可行理論上需要約500億次比較。5. 常見“坑點”與面試擴展在實際編碼和面試中圍繞這道題可能衍生出一些更深層次的討論。5.1 關于in操作符的誤解初學者容易混淆in在不同數據結構上的復雜度。務必牢記x in list- O(n) 線性查找x in set- O(1)平均時間復雜度哈希查找x in dict- O(1)平均時間復雜度鍵查找x in str- O(n) 線性查找在不確定數據結構時使用in要小心。5.2 如果stones是一個超大的流Stream這是面試中一個很好的擴展問題。如果stones不是一個可以一次性讀入內存的字符串而是一個來自網絡或文件的流一次只能讀一個字符我們的解法如何調整答案是解法二集合法依然有效且是最佳選擇。預處理階段將jewels讀入內存構建jewel_set。這部分數據通常很小。統計階段從流中逐個讀取stone字符判斷if stone in jewel_set并計數。內存中只需要維持一個計數器和集合內存消耗是O(n)與stones的總大小無關。而Counter法在這里就不適用了因為它需要先統計整個stones的頻率這在流式數據下無法做到。5.3 多語言實現的差異這道題幾乎可以用所有編程語言實現。理解其核心思想后在不同語言中只是語法轉換Java/C使用HashSet/unordered_set。JavaScript使用Set對象。Go使用map[rune]bool或map[byte]bool來模擬集合。關鍵點始終是將寶石集合預處理為哈希結構以實現常數時間查找。5.4 單元測試的編寫養成寫單元測試的習慣能極大提高代碼質量。針對本題可以設計以下測試用例import unittest class TestSolution(unittest.TestCase): def setUp(self): self.sol Solution() def test_case1(self): self.assertEqual(self.sol.numJewelsInStones(aA, aAAbbbb), 3) def test_case2(self): self.assertEqual(self.sol.numJewelsInStones(z, ZZ), 0) def test_empty_jewels(self): self.assertEqual(self.sol.numJewelsInStones(, abc), 0) def test_empty_stones(self): self.assertEqual(self.sol.numJewelsInStones(aA, ), 0) def test_both_empty(self): self.assertEqual(self.sol.numJewelsInStones(, ), 0) def test_all_stones_are_jewels(self): self.assertEqual(self.sol.numJewelsInStones(abc, aaabbbccc), 9) if __name__ __main__: unittest.main()覆蓋了正常情況、邊界情況空字符串、大小寫敏感、全部匹配等場景這樣的代碼才足夠健壯。回過頭看LeetCode 771 “Jewels and Stones” 遠不止是一道簡單的計數題。它是一個絕佳的樣本讓我們練習如何將問題抽象為集合查找模型如何根據操作頻率選擇合適的數據結構哈希集合如何寫出不同風格但同樣高效的代碼以及如何思考邊界條件和性能權衡。下次再遇到它不妨多花幾分鐘想想還有沒有其他寫法每種寫法的優缺點是什么。把這些基礎打牢面對更復雜的題目時你才能游刃有余。