算法解題:在一串無限的數字序列中,找到特定位置上的數字?
更新日期: 2025 年 3 月 8 日
在程式設計與數學問題中,數字序列相關的題目經常出現。
本篇文章將針對一個特定的數字序列謎題,進行詳細解說並提供完整的解題思路與程式實現,適合初學者理解與學習。
希望透過這篇文章,讓讀者能夠掌握這類問題的解法,並舉一反三,應用於類似的演算法問題中。
題目說明
給定一個無限延伸的數字序列,如下所示:
123456789101112131415161718192021……
這個序列是將自然數 (1, 2, 3, 4, …) 依序連接在一起形成的一個長串數字。
我們的任務是在這個序列中,找到特定位置 (索引) 上所對應的數字。例如:
- 序列中的第 9 位是數字
9
。 - 序列中的第 10 位是數字
1
(數字10
的第一位)。 - 序列中的第 11 位是數字
0
(數字10
的第二位)。
目標
我們需要找出以下位置上的數字:
- 第 1,000,000 位的數字。
- 第 1,000,000,000 位的數字。
- 第 1,000,000,000,000 位的數字。
性能要求
演算法的執行時間應保持在 2 秒以內,即使是面對超大數字的位置查詢。
概念轉換:難民與土地的比喻
理解題目的方式
為了更直觀地理解這個問題,我們可以將題目想像成有一群「難民」(數字)需要入住某個地區(序列中的土地位置)。
這些難民根據他們「體型」(數字位數)的不同,會被分配到不同大小的居住區域。
城市中的不同社區:
- 瘦子人種(1 位數,1 間臥室):
- 住在第一個社區,接收號碼牌 1 ~ 9 的人。
- 共有 9 個人,每個人只需要 1 間臥室。
- 因此,第一社區區總共需要
9 * 1 = 9
間臥室(土地 1 ~ 9 號)。
- 普通人種(2 位數,2 間臥室):
- 住在第二個社區,接收號碼牌 10 ~ 99 的人。
- 共有 90 個人,每個人需要 2 間臥室。
- 因此,第二社區總共需要
90 * 2 = 180
間臥室(土地 10 ~ 189 號)。
- 胖子人種(3 位數,3 間臥室):
- 住在第三個社區,接收號碼牌 100 ~ 999 的人。
- 共有 900 個人,每個人需要 3 間臥室。
- 因此,第三社區總共需要
900 * 3 = 2700
間臥室(土地 190 ~ 2889 號)。
- 更多空間的社區:
- 4 位數需要
4 * 9000 = 36000
間臥室。 - 5 位數需要
5 * 90000 = 450000
間臥室。 - 依此類推,每個社區根據人種的「體型」需要的空間成倍增長。
- 4 位數需要
數字的小房子與房間分配
我們將不同位數的數字比喻成不同大小的小房子,每個房間對應數字中的一位數字:
- 1 位數 (瘦子人種):小房子只有 1 間房間,例如:
[5]
代表數字 5。 - 2 位數 (普通人種):小房子有 2 間房間,例如:
[4][2]
代表數字 42。 - 3 位數 (胖子人種):小房子有 3 間房間,例如:
[7][0][3]
代表數字 703。
從 0 開始數的房間編號
在程式語言和數學中,編號經常從 0
開始,而不是從 1
開始,這與我們日常生活中的計數方式有所不同。例如:
- 第一個房間編號是
0
,第二個房間編號是1
,第三個房間編號是2
。 - 如數字
703
,房間編號與數字對應關係如下:
[7][0][3]
0 1 2
用餘數 (Modulo) 來計算房間編號
計算房間編號時,經常使用餘數 (%
) 運算。例如:
(位置 - 1) % 數字位數 = 房間編號
- 示例:計算
1811
號位置在數字703
中的房間位置:
(1811 - 1) % 3 = 1810 % 3 = 2
- 這表示
1811
號位置對應到數字703
的「第 2 號房間」,數字是3
。
範例解析:從理論到實際
三大人種 (不同位數的數字)
- 瘦子人種(1 位數):住在一個只有「1 間房間」的小房子裡,例如:[5] 代表數字 5。
- 普通人種(2 位數):住在「2 間房間」的小房子裡,例如:[4][2] 代表數字 42。
- 胖子人種(3 位數):住在「3 間房間」的小房子裡,例如:[7][0][3] 代表數字 703。
社區規劃 (不同位數區域)
- 瘦子人種社區:僅有 9 間房子(數字 1 到 9),每間房子只有 1 間房間,共 9 個房間。
- 普通人種社區:有 90 間房子(數字 10 到 99),每間房子有 2 間房間,共 180 個房間。
- 胖子人種社區:有 900 間房子(數字 100 到 999),每間房子有 3 間房間,共 2700 個房間。
第 13 號房間的推導過程
1️⃣ 確認「第 13 號房間」在哪個社區
- 瘦子人種社區:
- 這個社區有 9 間房子,共 9 個房間。
- 13 > 9,房間編號超出,需進入下一個社區。
- 普通人種社區:
- 這個社區有 90 間房子,每間房子有 2 個房間,共 180 個房間。
- 13 – 9 = 4,表示我們要找的「第 13 號房間」在「普通人種社區」的第 4 號房間。
2️⃣ 計算具體的「居民」(數字)
- 普通人種社區 的居民 (數字) 從
10
開始。 - 每位居民 (數字) 需要 2 個房間 (2 位數)。
- 哪位居民住在第 4 號房間?
居民編號 = (4 - 1) // 2 = 3 // 2 = 1
- 對應的居民 (數字):
起始數字 + 居民編號 = 10 + 1 = 11
- 數字
11
可以視為一間「有 2 間房間」的小房子:
[1][1]
0 1
3️⃣ 找到具體「房間」的數字
- 第 4 號房間 是在數字
11
這間房子中的「第幾間房間」呢?
(4 - 1) % 2 = 3 % 2 = 1
- 這個結果表示「第 4 號房間」對應到數字
11
中的「第 1 號房間」。 - 在小房子
[1][1]
裡,第 1 號房間裡住的是數字 1。
4️⃣ 結果:第 13 號房間裡住的是數字 1
第 2000 號房間的推導過程
1️⃣ 確認「第 2000 號房間」在哪個社區
- 瘦子人種社區:
- 9 間房子 (9 個房間),2000 > 9,超出範圍。
- 剩下:
2000 - 9 = 1991
- 普通人種社區:
- 90 間房子 (180 個房間),1991 > 180,超出範圍。
- 剩下:
1991 - 180 = 1811
- 胖子人種社區:
- 900 間房子 (2700 個房間),1811 <= 2700,確定在此社區。
2️⃣ 計算具體的「居民」(數字)
- 胖子人種社區 的居民 (數字) 從
100
開始。 - 每位居民 (數字) 需要 3 個房間 (3 位數)。
- 哪位居民住在第 1811 號房間?
居民編號 = (1811 - 1) // 3 = 1810 // 3 = 603
- 對應的居民 (數字):
起始數字 + 居民編號 = 100 + 603 = 703
- 數字
703
可視為一間「有 3 間房間」的小房子:
[7][0][3]
0 1 2
3️⃣ 找到具體「房間」的數字
- 第 2000 號房間 在數字
703
中的「第幾號房間」?
(1811 - 1) % 3 = 1810 % 3 = 1
- 這表示「第 2000 號房間」對應到數字
703
裡的「第 1 號房間」。 - 在小房子
[7][0][3]
裡,第 1 號房間裡住的是數字 0。
4️⃣ 結果:第 2000 號房間裡住的是數字 0
為什麼要 -1?深入解析位置與房間編號的差異
在解數字序列問題時,我們經常遇到「為什麼要 -1」的疑問。
這個 -1 不僅出現在計算「居民編號 (數字索引)」時,也出現在計算「房間編號 (數字中的具體位數)」時。
接下來,我將通過實際案例和詳細推導,解釋這個 -1 背後的邏輯,並說明「商數」的用途——如何通過商數來確定「實際居民」的數字。
現實世界 VS. 程式世界的編號方式
1️⃣ 真實世界中的編號方式
- 在日常生活中,房間、座位、樓層的編號通常從 1 開始:
房間 1, 房間 2, 房間 3, ... 房間 10
- 這種「自然數編號」的方式,直觀且符合我們的日常習慣。
2️⃣ 程式世界中的編號方式
- 在程式語言中,陣列 (Array)、字串 (String) 等數據結構中的編號 (索引) 是從 0 開始:
房間 0, 房間 1, 房間 2, ...
- 這被稱為「零基索引 (Zero-based Indexing)」,這種編號方式在程式計算中更方便,因為第一個位置 (索引) 就是
0
。
計算居民編號時為什麼要 -1?
在計算「居民編號」時,我們使用的公式是:
居民編號 = (目標位置 - 已用房間數 - 1) // 每位居民佔用的房間數
- 商數的用途:
- 計算的「商數」表示目標房間對應的「居民 (數字) 編號」。
- 再加上起始數字,確定「具體居民 (數字)」是誰。
1️⃣ 示例:計算第 15 號房間的居民編號
- 普通人種社區 (2 位數),每位居民佔 2 間房間。
- 先扣除「瘦子人種社區」的 9 間房間,剩下
15 - 9 = 6
間房間。 - 計算居民編號:
(15 - 9 - 1) // 2 = 5 // 2 = 2
這個商數 2 代表什麼?
- 居民數字計算:
實際居民數字 = 起始數字 + 居民編號
實際居民數字 = 10 + 2 = 10
- 解釋邏輯:
- 「普通人種社區」的數字從
10
開始。 - 這裡的商數
0
表示我們要找的是「第 2 位居民」,也就是數字12
。
2️⃣ 如果不 -1 會怎樣?
假如不減 1,公式會變成:
(15 - 9) // 2 = 6 // 2 = 3 (錯誤的結果)
- 計算出「第 3 位居民」,但實際上應該是「第 2 位居民」(從 0 位開始計算)。
- 如果用這個錯誤的結果去計算:
實際居民數字 = 10 + 3 = 13 (錯誤)
- 正確的數字應該是
12
而不是13
,這就是不減 1 導致的錯位問題。
📌 總結:計算居民編號時,扣 1 是為了將「從 1 開始數的房間位置」轉換為「從 0 開始數的居民編號」,這樣才能正確計算出實際的數字 (居民)。
計算房間編號時為什麼要 -1?
當找到具體的居民 (數字) 後,我們還需要計算「目標房間」在這間小房子 (數字) 中的具體「房間位置」。這時使用的公式是:
房間編號 = (目標位置 - 1) % 每位居民的房間數
1️⃣ 示例:計算數字 12 中的房間位置
- 數字
12
可以視為小房子[1][2]
,有 2 間房間,從 0 開始編號:
[1][2]
0 1
- 第 15 號房間在這間房子中的「第幾間房間」?
(6 - 1) % 2 = 5 % 2 = 1
- 為什麼要 -1?
假如不減 1,會變成:
6 % 2 = 0
- 這將錯誤地指向數字
12
中的「第 0 號房間」(數字 1),但實際上,我們應該得到「第 1 號房間」的數字 (2)。
📌 總結:扣 1 是因為在計算「第幾間房間」時,需要將自然數位置 (從 1 開始) 轉換成「從 0 開始數」的房間編號,避免錯位。
總結:-1 的兩個核心邏輯
- 計算居民編號時 -1:
- 用來正確計算「第幾位居民」,確保計算出的居民 (數字) 從 0 開始編號,避免計算跳位。
- 計算房間編號時 -1:
- 用來將「從 1 開始數」的目標位置轉換成「從 0 開始數」的房間編號,避免數字位數錯位。
這樣的 -1 邏輯不僅僅是數學中的「小技巧」,更是將「人類語言」轉換為「程式語言」的重要步驟,保證計算結果的準確性。
程式實現
def find_digit_at_land(land_position: int) -> int:
# 初始化變數
digit_length = 1 # 當前人種的位數 (1 位數, 2 位數, 3 位數 ...)
start_number = 1 # 當前人種數字的起始 (1, 10, 100, 1000 ...)
# 1. 確認土地所在的位數區間
while True:
# 計算當前區間的數字總數和佔據的土地數量
count_of_numbers = 9 * start_number
total_digits_in_range = count_of_numbers * digit_length
# 如果土地位置在當前區間內,跳出循環
if land_position <= total_digits_in_range:
break
# 扣除當前區間的土地數量,進入下一個區間
land_position -= total_digits_in_range
digit_length += 1
start_number *= 10
# 2. 計算土地對應的數字
# 計算是區間內第幾個數字 (從 0 開始)
number_index = (land_position - 1) // digit_length
# 計算是該數字中的第幾個「房間」(從 0 開始)
room_index = (land_position - 1) % digit_length
# 計算具體的數字
actual_number = start_number + number_index
# 3. 根據房間編號,返回對應的數字
return int(str(actual_number)[room_index])
# 🧪 測試
print(find_digit_at_land(13)) # 預期輸出:1
print(find_digit_at_land(200)) # 預期輸出:1
print(find_digit_at_land(1000)) # 測試更大的位置
# 挑戰題:更大的土地編號
print(find_digit_at_land(1000000)) # 預期可在 2 秒內運行
print(find_digit_at_land(1000000000)) # 更大的挑戰
print(find_digit_at_land(1000000000000)) # 超大土地編號
結語
在這篇文章中,我們運用了數學和邏輯推理,成功解決了這個看似複雜的數字序列謎題。
我們不僅通過比喻解釋了題目的本質,還提供了詳細的程式碼,適合初學者學習。
這個解法能夠快速計算出任何土地編號上的數字,並在大數場景下依然能保持高效,值得作為算法學習中的一個經典案例。