Given a string S
, check if the letters can be rearranged so that two characters that are adjacent to each other are not the same.
If possible, output any possible result. If not possible, return the empty string.
Example 1:
Input: S = "aab" Output: "aba"
Example 2:
Input: S = "aaab" Output: ""
Note:
S
will consist of lowercase letters and have length in range[1, 500]
.
這道題給了一個字符串,讓我們重構這個字符串,使得相同的字符不會相鄰,如果無法做到,就返回空串,題目中的例子很好的說明了這一點。如果先不考慮代碼實現,讓你來手動重構的話,該怎么做呢?其實就是把相同的字符分開。比如例子1中,兩個a相鄰了,所以把第二個a和后面的b交換位置,這樣分開了相同的字符,就是最終答案了。再來看一個例子,比如 "aaabbc",當發現第二個字符也是 ‘a’ 的時候,就需要往后遍歷找到第一個不是 ‘a’ 的字符,即 ‘b’,然后交換 ‘a’ 和 ‘b’ 即可,然后繼續往后面進行同樣的處理,當無法找到不同的字符后就返回空串。這種方法對有序的字符串S是可以的,雖然題目給的兩個例子中字符串S都是有序的,實際上不一定是有序的。所以博主最先的想法是給數組排序唄,但是博主的這個解法跪在了這個例子上 "vvvlo",我們發現排序后就變成 "lovvv",這樣上面提到的解法就跪了。其實這里次數出現多的字符串需要在前面,這樣才好交換嘛。那么還是要統計每個字符串出現的次數啊,這里使用 HashMap 來建立字母和其出現次數之間的映射。由於希望次數多的字符排前面,可以使用一個最大堆,C++ 中就是優先隊列 Priority Queue,將次數當做排序的 key,把次數和其對應的字母組成一個 pair,放進最大堆中自動排序。這里其實有個剪枝的 trick,如果某個字母出現的頻率大於總長度的一半了,那么必然會有兩個相鄰的字母出現。這里博主就不證明了,感覺有點像抽屜原理。所以在將映射對加入優先隊列時,先判斷下次數,超過總長度一半了的話直接返回空串就行了。
好,最大堆建立好以后,此時難道還是應該使用上面所說的交換的方法嗎?其實直接構建新的字符串要更加簡單一些。接下來,每次從優先隊列中取隊首的兩個映射對兒處理,因為要拆開相同的字母,這兩個映射對兒肯定是不同的字母,可以將其放在一起,之后需要將兩個映射對兒中的次數自減1,如果還有多余的字母,即減1后的次數仍大於0的話,將其再放回最大堆。由於是兩個兩個取的,所以最后 while 循環退出后,有可能優先隊列中還剩下了一個映射對兒,此時將其加入結果 res 即可。而且這個多余的映射對兒一定只有一個字母了,因為提前判斷過各個字母的出現次數是否小於等於總長度的一半,按這種機制來取字母,不可能會剩下多余一個的相同的字母,參見代碼如下:
解法一:
class Solution { public: string reorganizeString(string S) { string res = ""; unordered_map<char, int> m; priority_queue<pair<int, char>> q; for (char c : S) ++m[c]; for (auto a : m) { if (a.second > (S.size() + 1) / 2) return ""; q.push({a.second, a.first}); } while (q.size() >= 2) { auto t1 = q.top(); q.pop(); auto t2 = q.top(); q.pop(); res.push_back(t1.second); res.push_back(t2.second); if (--t1.first > 0) q.push(t1); if (--t2.first > 0) q.push(t2); } if (q.size() > 0) res.push_back(q.top().second); return res; } };
下面這種解法的原理和上面的很類似,就是寫法上很秀,堪比陳獨秀。這里使用了一個長度為 26 的一位數組 cnt 來代替上面的 HashMap 進行統計字母的出現次數,然后比較秀的一點是,把上面的映射對兒壓縮成了一個整數,做法是將次數乘以了 100,再加上當前字母在一位數字中的位置坐標i,這樣一個整數就同時 encode 了次數和對應字母的信息了,而且之后 decode 也很方便。數組 cnt 更新好了后,需要排個序,這一步就是模擬上面解法中最大堆的自動排序功能。不過這里是數字小的在前面,即先處理出現次數少的字母。這里除了和上面一樣檢測次數不能大於總長度的一半的操作外,還有一個小 trick,就是構建字符串的時候,是從第二個位置開始的。這里構建的字符串是直接對原字符串S進行修改的,因為 cnt 數組建立了之后,字符串S就沒啥用了。用一個變量 idx 來表示當前更新字母的位置,初始化為1,表示要從第二個位置開始更新。因為出現次數最多的字母一定要占據第一個位置才行,這就是留出第一個位置的原因。這里很叼的一點,就是隔位更新,這樣能保證相同的字母不相鄰,而且當 idx 越界后,拉回到起始位置0,這就有點遍歷循環數組的感覺。舉個栗子來說吧,比如 "aaabbc",更新順序為:
_ c _ _ _ _
_ c _ b _ _
_ c _ b _ b
a c _ b _ b
a c a b _ b
a c a b a b
解法二:
class Solution { public: string reorganizeString(string S) { int n = S.size(), idx = 1; vector<int> cnt(26, 0); for (char c : S) cnt[c - 'a'] += 100; for (int i = 0; i < 26; ++i) cnt[i] += i; sort(cnt.begin(), cnt.end()); for (int num : cnt) { int t = num / 100; char ch = 'a' + (num % 100); if (t > (n + 1) / 2) return ""; for (int i = 0; i < t; ++i) { if (idx >= n) idx = 0; S[idx] = ch; idx += 2; } } return S; } };
Github 同步地址:
https://github.com/grandyang/leetcode/issues/767
類似題目:
Rearrange String k Distance Apart
參考資料:
https://leetcode.com/problems/reorganize-string/
https://leetcode.com/problems/reorganize-string/discuss/113440/Java-solution-PriorityQueue
https://leetcode.com/problems/reorganize-string/discuss/113427/C%2B%2B-Greedy-sort-O(N)
https://leetcode.com/problems/reorganize-string/discuss/232469/Java-No-Sort-O(N)-0ms-beat-100