151. Reverse Words in a String
題目 / Problem
中文:
給定一個字串 s,將字串中的單字順序反轉。
- 一個單字定義為連續的非空白字元。
- 原字串中的單字之間至少有一個空白字元分隔。
- 回傳一個新字串,其中單字以反向順序排列,且彼此之間僅用一個空白分隔。
- 注意:
s可能包含前導空白、尾隨空白,或多個空白;回傳結果不能有多餘的空白。
English:
Given a string s, reverse the order of the words.
- A word is a maximal sequence of non-space characters.
- Words in
sare separated by at least one space. - Return a string of the words in reverse order, joined by a single space.
- The input may contain leading/trailing spaces and multiple spaces between words; the output must contain none of these.
Constraints:
- 1 <= s.length <= 10^4
- s contains English letters (upper- and lower-case), digits, and spaces.
- There is at least one word in s.
Example:
Input: " hello world "
Output: "world hello"
名詞解釋 / Glossary
- Two pointers / 雙指針:用兩個索引變數同時掃描字串,一個記錄單字的開頭、一個記錄結尾。常用來在一次遍歷中切出多個區段。A technique that uses two indices to walk through a string/array — here, one marks the start of a word, the other its end.
- In-place / 原地修改:直接在原本的記憶體上修改資料,不另外配置一個等大的陣列。Modifying data directly in the existing buffer instead of allocating a new array of equal size.
- Reverse trick / 三次反轉技巧:先反轉整個字串,再反轉每個單字。這樣字母順序在每個單字內被「修正回正向」,但單字之間的順序卻變成原本的相反。Reverse the whole string, then reverse each word individually — net effect: word order is flipped, but each word reads forward again.
malloc/free:C 語言中動態配置與釋放堆積(heap)記憶體的函式。LeetCode 的 C 題目通常要求回傳的字串用malloc配置。C functions for allocating and freeing heap memory; LeetCode's C runtime expects the returned string to bemalloc-ed.std::string/std::stringstream:C++ 標準函式庫的字串類別與「字串流」,可以像讀取cin一樣從字串中以空白為分隔讀出 token。C++ standard string and string-stream classes; the stream reads whitespace-delimited tokens from a string just likecindoes from input.std::vector<T>:C++ 的動態陣列,可自動擴充容量。A dynamic array in C++ that grows as you push elements onto it.
思路
最直觀的做法是:先把字串依照空白切成單字陣列,反轉這個陣列,再用單一空白接起來。這在 C++ 裡用 stringstream 幾乎一行就能寫完,時間是 O(n)、額外空間也是 O(n)(單字陣列本身)。對於初學者,這通常是最容易理解、最不容易出錯的版本,所以 C++ 的解法就採用它。但題目的 Follow-up 問「若字串可變動,能否在 O(1) 額外空間就地完成?」這就要用「三次反轉」的技巧:第一步,把整個字串先反轉一次("the sky is blue" → "eulb si yks eht");第二步,從左到右掃描,每遇到一個單字就把那個單字內部再反轉一次("eulb" → "blue"),這樣每個單字內部回到正向,但單字之間的順序已經和原本相反;第三步,處理多餘空白——用一個「寫入指標」覆寫原陣列,遇到單字才寫入,並且只在單字之間補一個空白。整個過程只需要常數額外變數,符合 in-place 的要求。為什麼三次反轉行得通?因為反轉是個可逆操作:兩次反轉抵消,所以只反轉「外層」(整串)但讓「內層」(每個單字)也跟著反轉一次,最後外層的順序變動才會「留下來」。
The brute-force idea is: split the string by spaces into a list of words, reverse the list, and join with a single space. In C++ this fits in a few lines using stringstream, runs in O(n) time, and uses O(n) extra space for the word list — perfectly fine and the easiest version to read, so that's what the C++ solution does. The follow-up, however, asks for O(1) extra space when the string is mutable, which is the natural setting for C (where s is a char* we can rewrite). The technique is three reversals: (1) reverse the entire string, so "the sky is blue" becomes "eulb si yks eht"; (2) walk left-to-right and reverse each individual word, restoring the letters inside each word to forward order while leaving the word order reversed; (3) compact the result by writing it back into the same buffer with exactly one space between words and no leading/trailing space. This works because reversal is its own inverse: reversing twice cancels out, so we reverse the outer sequence (the whole string, which flips word order) and then "undo" it only inside each word, leaving the word-order flip as the net effect. The compaction step handles arbitrary runs of spaces in one pass using a write index.
逐步走查 / Walkthrough
以輸入 s = "the sky is blue"(長度 15)走過 C 解法。
Tracing the C solution on s = "the sky is blue" (length 15).
Step 1 — Reverse the whole buffer / 反轉整個字串:
| Before | After |
|---|---|
the sky is blue |
eulb si yks eht |
Step 2 — Walk through and reverse each word / 逐一反轉每個單字:
| Pointer scan / 指針掃描 | Word found / 找到單字 | Buffer after reversing that word |
|---|---|---|
| i=0..3 | eulb |
blue si yks eht |
| i=5..6 | si |
blue is yks eht |
| i=8..10 | yks |
blue is sky eht |
| i=12..14 | eht |
blue is sky the |
Step 3 — Compact spaces / 壓縮多餘空白:
此例本來就只有單一空白,所以寫入指標 w 從頭走到尾後,內容不變:blue is sky the。最後在 s[w] 寫入 '\0' 結束字串。
In this example there were no extra spaces to begin with, so the write pointer w walks through and writes back the same characters, terminating with '\0'. The final string is "blue is sky the".
(若輸入是 " hello world ",第 3 步會跳過開頭兩個空白、保留 hello、寫入單一空白、保留 world、跳過尾部兩個空白,結果為 "world hello"。)
(If the input were " hello world ", Step 3 would skip the leading two spaces, keep hello, write one space, keep world, and skip the trailing two spaces, yielding "world hello".)
Solution — C
// 演算法:三次反轉 + 原地壓縮空白。
// Algorithm: three reversals + in-place space compaction.
// (1) 反轉整個字串 / reverse the whole buffer.
// (2) 反轉每個單字 / reverse each word individually.
// (3) 用寫入指標重新排版,去除多餘空白 / use a write pointer to remove extra spaces.
#include <string.h> // strlen — 取得字串長度 / get string length
// 反轉 s[l..r](含兩端)的字元 / Reverse characters of s[l..r] inclusive.
static void reverseRange(char *s, int l, int r) {
while (l < r) { // 當左指針還在右指針左邊 / while left is still left of right
char t = s[l]; // 暫存左邊字元 / stash the left character
s[l] = s[r]; // 把右邊搬到左邊 / move right char into left slot
s[r] = t; // 再把暫存的放到右邊 / place the stashed char into right slot
l++; // 左指針往右走一格 / advance left
r--; // 右指針往左走一格 / retreat right
}
}
// LeetCode 要求的函式簽名 / LeetCode-required signature.
// 回傳的字串可以是原 buffer(原地修改)/ returned string may be the same buffer (in-place).
char* reverseWords(char* s) {
int n = (int)strlen(s); // n = 字串長度 / length of s
// Step 1:反轉整個字串 / reverse the entire buffer.
reverseRange(s, 0, n - 1); // 把 s[0..n-1] 整段反轉 / flip the whole range
// Step 2:逐一反轉每個單字 / reverse every word in place.
int i = 0; // 掃描指標 / scanning index
while (i < n) {
while (i < n && s[i] == ' ') i++; // 跳過空白 / skip spaces between words
int j = i; // j 指向單字開頭 / j marks the word's start
while (j < n && s[j] != ' ') j++; // 把 j 推到單字結尾後一格 / push j just past the word
if (i < j) { // 若真的找到了一個單字 / if a word was found
reverseRange(s, i, j - 1); // 反轉 s[i..j-1](單字本身)/ reverse this word
}
i = j; // 從單字之後繼續掃描 / continue after the word
}
// Step 3:壓縮多餘空白,重寫回 s / compact spaces by rewriting s in place.
int w = 0; // w = 下一個寫入位置 / next write slot
int r = 0; // r = 讀取位置 / read position
int wroteWord = 0; // 是否已經寫過至少一個單字 / have we written any word yet
while (r < n) {
if (s[r] != ' ') { // 讀到非空白:屬於某個單字 / non-space: part of a word
if (wroteWord) { // 不是第一個單字 → 先補一個空白 / not first word → add separator
s[w++] = ' '; // 寫入分隔空白 / write the single space
}
while (r < n && s[r] != ' ') { // 把整個單字搬過去 / copy the whole word
s[w++] = s[r++]; // 一個字元一個字元複製 / character by character
}
wroteWord = 1; // 標記已寫過單字 / mark that we've emitted a word
} else {
r++; // 是空白:直接略過 / it's a space: skip it
}
}
s[w] = '