給定一個只包含數位的字串,復原它並返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四個整數(每個整數位於 0 到 255 之間組成,且不能含有前導 0),整數之間用 '.' 分隔。
例如:"0.1.2.201" 和 "192.168.1.1" 是 有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "[email protected]" 是 無效的 IP 地址。
範例 1:
範例 2:
範例 3:
範例 4:
範例 5:
提示:
實際上還是分割問題,但是又多了一些條件
本題的麻煩的點在於有很多邊界條件需要處理
待解決的問題有:
1、如何加入分割點
2、如何判斷分割的段落是否為合法IP
"分割"這個動作與 分割回文串 裡定義的一致,都是用beginIndex指到分割位置
不同的是,這裡我們beginIndex指到分割的位置時,我們需要在這個位置的後一位插入分割點
(後面細說)
這裡我們需要寫一個判斷函數來判斷IP 是否合法,函數的輸入是字串和待判斷的區間
bool isVaild(string& s, int start, int end){
}
這裡本質上我們還是在s上操作,只是指定了操作的位置
首先需要明確非法IP的情況:
0、所給的判斷區間不合法
1、以0開頭的數位,例如:192 . 031 . 2 . 1
2、負數
3、IP段數值大於255
bool isVaild(string& s, int start, int end){
if(start > end) return false;//所給的判斷區間不合法
//是否以0開頭
if(s[start] == '0' && start != end) return false;
int num4IP = 0;
for(int i = start; i < end; ++i){
//是否為正數
if(s[i] < 0 && s[i] > 9) return false;
//IP段數值是否大於255
num4IP = num4IP * 10 + (s[i] - '0');
if(num4IP > 255) return false;
}
return true;
}
這裡有幾個關鍵點:
請注意,我們這裡判斷的是false的情況,也就是有IP段使用了0開頭
判斷條件除了直接看開頭字元是否為0(s[start] == '0'
)以外,還需要確保當前IP段的數位不是第一個獲取到的,因此需要追加條件 start != end
什麼意思呢?這裡又涉及到單層處理中的邏輯
在單層處理時,我們會去遍歷由beginIndex控制的s中的區間,每次遍歷都會觸發isVaild函數來判斷當前遍歷所得的IP段是否合法
isVaild函數需要輸入一個判斷區間(這個區間其實就是[beginIndex, i])
當第一次遍歷的時候,輸入isVaild的區間是[0, 0],此時會出現一種特殊情況
例如,我們的數位字串是:s = "0000"
那麼第一次遍歷獲取IP段時,我們得到的是'0',從我們的角度來看,這種情況應該是合法的
如果我們不追加條件去判斷當前遍歷是不是第一次遍歷,那麼上述情況會被程式視為非法,就會導致錯誤
為什麼start != end
可以判斷遍歷是否為第一次遍歷?
前面也說過了,因為我們輸入的區間實際上是[beginIndex, i],那麼這個區間只有在第一次遍歷時才會出現左右邊界相等的情況,即[0, 0]
這裡容易犯的一個錯誤是直接拿s[i]
來和255進行大小判斷
拜託,s[i]
是什麼?它是組成當前IP段的數位之一,單個數位怎麼可能大於255,這樣判斷只會通過所有的情況
因此,這裡需要一點小小的計算,我來模擬一下這個過程
int num4IP = 0;
for(int i = start; i < end; ++i){
//是否為正數
if(s[i] < 0 && s[i] > 9) return false;
//IP段數值是否大於255
num4IP = num4IP * 10 + (s[i] - '0');
if(num4IP > 255) return false;
}
例如當前的IP段是164,
第一輪遍歷拿到s[i]是1,此時num = 0 * 10 + ('2' - '0') = 1
第二輪遍歷拿到s[i]是6,此時num = 1 * 10 + ('5' - '0') = 16
第三輪遍歷拿到s[i]是4,此時num = 16 * 10 + ('4' - '0') = 164
164顯然是滿足條件的
發現沒有, num = num * 10 + (s[i] - '0');
的作用就是把資料型別為字串的IP段轉化為整型,然後再判斷
開始寫,還是老一套,回溯三部曲
1、確定回溯函數的引數和返回值
本題中我們是直接操作的數位字串s,因此不需要返回值
輸入引數是數位字串s、beginIndex和countPoint
countPoint用於統計目前一共插入了幾個分割點,用於終止判定
class Solution {
private:
bool isVaild(string& s, int start, int end){
...
}
vector<string> res;
//確定回溯函數的引數和返回值
//引數:數位字串、beginIndex、分割點計數變數
void backtracking(string& s, int beginIndex, int countPoint){
}
public:
vector<string> restoreIpAddresses(string s) {
}
};
2、確定終止條件
這裡就用到countPoint了
參考IP的結構,當我們已經插入了3個分割點時,這時需要結束了(不論之後還剩什麼)
同時,我們還要判斷一下被分割出來的第四段是否合法,合法就把當前插好的數位字串s儲存到結果陣列(一定記得我們是對s進行操作,最後的結果也是處理好的s)
class Solution {
private:
bool isVaild(string& s, int start, int end){
...
}
vector<string> res;
//確定回溯函數的引數和返回值
//引數:數位字串、beginIndex、分割點計數變數
void backtracking(string& s, int beginIndex, int countPoint){
if(countPoint == 3){
if(isVaild(s, beginIndex, s.size() - 1)){//判斷第四段是否合法
res.push_back(s);
}
return;
}
}
public:
vector<string> restoreIpAddresses(string s) {
}
};
3、確定單層處理邏輯
在單層遞迴中,我們需要遍歷由beginIndex控制的當前區間
使用for迴圈不斷獲取IP段,然後判斷其是否合法
合法就在當前位置 i 的後一個位置插入分割點
class Solution {
private:
bool isVaild(string& s, int start, int end){
...
}
vector<string> res;
//確定回溯函數的引數和返回值
//引數:數位字串、beginIndex、分割點計數變數
void backtracking(string& s, int beginIndex, int countPoint){
//確定終止條件
//當分割點的數量達到3個時說明分割結束,此時字串已經被分成4段
//在這裡還需要驗證最後一段是否合法
if(countPoint == 3){
if(isVaild(s, beginIndex, s.size() - 1)){//判斷第四段是否合法
res.push_back(s);
}
return;
}
//確定單層處理邏輯
for(int i = beginIndex; i < s.size(); ++i){
//這裡beginIndex不是固定的,下一層遞迴時beginIndex會由上一層遞迴傳入
if(isVaild(s, beginIndex, i)){
s.insert(s.begin() + i + 1, '.');//插入分割點
countPoint++;//計數++
backtracking(s, i + 2, countPoint);//只加1的話就到分割點的位置,所以得多加一個
countPoint--;//回溯
s.erase(s.begin() + i + 1);
}
}
}
public:
vector<string> restoreIpAddresses(string s) {
}
};
這裡又涉及到一個邊緣處理問題:插入分割點的位置
本題中使用insert來在s中插入分割點
舉個例子說明insert的使用:
teststr = "1234";
teststr.insert(1, '.');//在原串下標為1的字元1前插入'.'->"1.234"
所以為什麼插分割點的時候的位置是s.begin() + i + 1
因為s.begin() + i
只定位到了當前指標 i 遍歷到的位置,而我們希望在該位置之後加點,所以要再加1
例如遍歷"1234",遍歷時 i 指向2時,即下標2,我們需要在2後面打點,此時要輸入insert的應該是下標3而不是下標2
本題思路很清晰,但是難點在於有很多的邊界條件
一旦處理不好或者忘了,就會出錯
class Solution {
private:
bool isVaild(string& s, int start, int end){
//判斷區間是否合法
if(start > end) return false;
//判斷數位開頭是否為0,為0非法
if(s[start] == '0' && start != end){
return false;
}
int num4IP = 0;
//遍歷待判斷區間
for(int i = start; i <= end; ++i){
//是否為正數
if(s[i] > '9' || s[i] < '0'){
return false;
}
//是否在255範圍內
num4IP = num4IP * 10 + (s[i] - '0');
if(num4IP > 255){
return false;
}
}
return true;
}
vector<string> res;
//確定回溯函數的引數和返回值
//引數:數位字串、beginIndex、分割點計數變數
void backtracking(string& s, int beginIndex, int countPoint){
//確定終止條件
//當分割點的數量達到3個時說明分割結束,此時字串已經被分成4段
//在這裡還需要驗證最後一段是否合法
if(countPoint == 3){
if(isVaild(s, beginIndex, s.size() - 1)){//判斷第四段是否合法
res.push_back(s);
}
return;
}
//確定單層處理邏輯
for(int i = beginIndex; i < s.size(); ++i){
//這裡beginIndex不是固定的,下一層遞迴時beginIndex會由上一層遞迴傳入
if(isVaild(s, beginIndex, i)){
s.insert(s.begin() + i + 1, '.');//插入分割點
countPoint++;//計數++
backtracking(s, i + 2, countPoint);//只加1的話就到分割點的位置,所以得多加一個
countPoint--;//回溯
s.erase(s.begin() + i + 1);
}
}
}
public:
vector<string> restoreIpAddresses(string s) {
//這裡可以先判斷一下字串s是否合法
if(s.size() < 4 || s.size() > 12) return res;
backtracking(s, 0, 0);
return res;
}
};