问题描述
给定一个只包含数字的字符串,用以表示一个 IP 地址,返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 无效 IP 地址。
示例 1:
输入:s = “25525511135”
输出:[“255.255.11.135”,“255.255.111.35”]
问题分析
把字符串分解成符合规定的ip地址,首先要明白ip地址的性质
- ip地址分为四节,每节取值范围在[0,255]
- 对于取出的字符串,若它的第一位为’0’,那么它的长度只能为1,多位字符不能以0作为开头
明白这两点,我们可以每次拿出[1,3]个字符,判断是否符合ip地址的条件,若符合,则递归剩下的字符串,直到四节都符合,那么就得到了一个解
代码实现
#define MaxSize 100
#include<stdio.h>
typedef struct
{
char data[MaxSize];
int length;
}IP;
void addch(IP&ip,char ch)
{
ip.data[ip.length]=ch;
ip.length++;
}
IP addot(IP ip)
{
addch(ip,'.');
return ip;
}
void solveip(char s[],int n,int start,int step,IP ip)
{
if(start<=n)
{
if(start==n&&step==4)
{
for(int i=0;i<ip.length-1;i++)
printf("%c",ip.data[i]);
printf("\n");
}
}
int num=0;
for(int i=start;i<n&&i<start+3;i++)
{
num=10*num+(s[i]-'0');
if(num<=255)
{
addch(ip,s[i]);
solveip(s,n,i+1,step+1,addot(ip));
}
if(num==0) break;
}
}
int main()
{
char s[MaxSize]="25525511135 ";
int n=11;
IP ip;
ip.length=0;
solveip(s,n,0,0,ip);
return 1;
}
代码分析
算法
使用回溯算法,即每一次都按最优解向前搜索,不满足条件时则回溯到上一次的情况,进行次优解的搜索。
表现在本次代码中,最优解,就是每取一位字符,就按每一节的取值范围在[0,9]进行设置。若最优解不能满足,则回溯到上一个时间点,使用次优解进行搜索,而次优解即是取到两位字符,每一节按照取值范围[10,99]进行设置。若次优解仍不能满足,则回溯到上一个时间点,用最次解进行搜索,即取到三位字符,每一节按照取值范围[100,225]进行设置。
需要注意的是,其中的一节所选取的解的优劣,不会影响到下一节的选择,即下一节依然会使用最优解。
而且所谓回溯到上一个时间点,就是回溯到前一节的设置。
核心模块
void solveip(char s[],int n,int start,int step,IP ip)
{
if(start<=n)
{
if(start==n&&step==4)
{
for(int i=0;i<ip.length-1;i++)
printf("%c",ip.data[i]);
printf("\n");
}
}
int num=0;
for(int i=start;i<n&&i<start+3;i++)
{
num=10*num+(s[i]-'0');
if(num<=255)
{
addch(ip,s[i]);
solveip(s,n,i+1,step+1,addot(ip));
}
if(num==0) break;
}
}
终止条件
if(start<=n)
{
if(start==n&&step==4)
{
for(int i=0;i<ip.length-1;i++)
printf("%c",ip.data[i]);
printf("\n");
}
}
这里的终止条件有两个
- start==n,即字符串已经读取到最后一个
- step==4,即节数刚好是四个
注意这里的step,满足的终止条件是正好是四个,不论是过多还是过少都是不满足的。并且,过多的情况可以有效的限制从头开始数起的已经满足条件的含有三个字符的节不会被回溯掉,具体会在下面的执行过程中展示。
输出时,for循环的终止条件之所以设置为length-1,是为了避免输出ip串中最后储存的‘.’
而输出完成一个满足条件的ip串后,程序也不会停止,而是回溯到上一个时间点,去寻找其他形式的满足条件的串。
例如,在找到
255.255.11.135
之后,程序会回溯到
255.255.11
来继续查找,进而找到
255.255.111.35
直到再也找不出来,然后被调函数不断出栈,回到最开始调用的第一个solveip,结束。
递归过程
int num=0;
for(int i=start;i<n&&i<start+3;i++)
{
num=10*num+(s[i]-'0');
if(num<=255)
{
addch(ip,s[i]);
solveip(s,n,i+1,step+1,addot(ip));
}
if(num==0) break;
}
这里的for循环以取到三个字符或是字符串被取完为终止条件 ,限制了每一节的最大长度。
同时两个if语句,限制了:
- 每节取值范围在[0,255]
- 对于取出的字符串,若它的第一位为’0’,那么它的长度只能为1,多位字符不能以0作为开头
addch函数将取到的字符填入ip串中。
addot函数将传入下一个solveip的IP串尾部加上‘.’,意味着下一个soveip中进行的是下一个字节的操作,当然,在回溯的过程中,无论是下一个字节中的操作,还是调用时加上的‘.’,都会被回溯掉。
在还没有进行回溯时,会不断的调用函数进栈,然后一个节都是最优解法,如
2
2.2
2.2.5
…
2.2.5.2.2.5.1.1.1.3.5
在调用下一次的时候
2.2.5.2.2.5.1.1.1.3.5.
就会出现问题,字符串被取完了,for循环无法进行,调用终止,函数出栈,回到上一个时间点
2.2.5.2.2.5.1.1.1.3.5
但是这时再进行下一次循环的时候,同样达到了终止条件,于是调用终止,函数出栈,回到上一个时间点
2.2.5.2.2.5.1.1.1.3
进行下一次for循环
2.2.5.2.2.5.1.1.1.35
依然不满足条件,回溯
2.2.5.2.2.5.1.1.1
2.2.5.2.2.5.1.1.13
2.2.5.2.2.5.1.1.13.5
如此不断的搜索与回溯,可以达到第一个被调用的函数
2
这时继续for循环
22
22.5.2.2.5.1.1.1.3.5
重复上述过程,可以达到第一节被满足的情况
225
225.2.2.5.1.1.1.3.5
如果继续回溯,会自然的想到,如果回溯到
225
那么继续进行第一个被调函数中的for循环的话,循环终止,函数结束,就无法完成。
其实,根本不会回溯到这种情况,因为第二节会在达到这种情况之前被满足,即回溯到
225.22
就会有
225.225
225.225.1.1.1.3.5
最终,达到终止条件
225.225.11.135
总结
递归过程较为复杂,在搜索与回溯之间,函数不断地出栈入栈,因此时间复杂度会比较高。