数据机构与算法之深入解析“复原IP地址”的求解思路与算法示例

Posted Serendipity·y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据机构与算法之深入解析“复原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 地址。
  • 给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 ‘.’ 来形成。不能重新排序或删除 s 中的任何数字,可以按任何顺序返回答案。
  • 示例 1:
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]
  • 示例 2:
输入:s = "0000"
输出:["0.0.0.0"]
  • 示例 3:
输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
  • 提示:
    • 0 <= s.length <= 20;
    • s 仅由数字组成。

二、求解算法

① 回溯法

  • 回溯算法事实上就是在一个树形问题上做深度优先遍历,因此首先需要把问题转换为树形问题,模拟一下如何通过指定的字符串 s 生成 IP 地址的过程,把树形图画出来。
  • 在画树形图的过程中,一定会发现有些枝叶是没有必要的,把没有必要的枝叶剪去的操作就是剪枝,在代码中一般通过 break 或者 contine 和 return (表示递归终止)实现。
  • 一开始,字符串的长度小于 4 或者大于 12 ,一定不能拼凑出合法的 ip 地址(这一点可以一般化到中间结点的判断中,以产生剪枝行为);
  • 每一个结点可以选择截取的方法只有 3 种:截 1 位、截 2 位、截 3 位,因此每一个结点可以生长出的分支最多只有 3 条分支;
  • 根据截取出来的字符串判断是否是合理的 ip 段,这里写法比较多,可以先截取,再转换成 int ,再判断。我采用的做法是先转成 int,是合法的 ip 段数值以后,再截取;
  • 由于 ip 段最多就 4 个段,因此这棵三叉树最多 4 层,这个条件作为递归终止条件之一;
  • 每一个结点表示了求解这个问题的不同阶段,需要的状态变量有:
    • splitTimes:已经分割出多少个 ip 段;
    • begin:截取 ip 段的起始位置;
    • path:记录从根结点到叶子结点的一个路径(回溯算法常规变量,是一个栈);
    • res:记录结果集的变量,常规变量。

  • Java 示例:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Stack;

public class Solution 

    public List<String> restoreIpAddresses(String s) 
        int len = s.length();
        List<String> res = new ArrayList<>();
        // 如果长度不够,不搜索
        if (len < 4 || len > 12) 
            return res;
        

        Deque<String> path = new ArrayDeque<>(4);
        int splitTimes = 0;
        dfs(s, len, splitTimes, 0, path, res);
        return res;
    

    /**
     * 判断 s 的子区间 [left, right] 是否能够成为一个 ip 段
     * 判断的同时顺便把类型转了
     *
     * @param s
     * @param left
     * @param right
     * @return
     */
    private int judgeIfIpSegment(String s, int left, int right) 
        int len = right - left + 1;

        // 大于 1 位的时候,不能以 0 开头
        if (len > 1 && s.charAt(left) == '0') 
            return -1;
        

        // 转成 int 类型
        int res = 0;
        for (int i = left; i <= right; i++) 
            res = res * 10 + s.charAt(i) - '0';
        

        if (res > 255) 
            return -1;
        
        return res;
    

    private void dfs(String s, int len, int split, int begin, Deque<String> path, List<String> res) 
        if (begin == len) 
            if (split == 4) 
                res.add(String.join(".", path));
            
            return;
        

        // 看到剩下的不够了,就退出(剪枝),len - begin 表示剩余的还未分割的字符串的位数
        if (len - begin < (4 - split) || len - begin > 3 * (4 - split)) 
            return;
        

        for (int i = 0; i < 3; i++) 
            if (begin + i >= len) 
                break;
            

            int ipSegment = judgeIfIpSegment(s, begin, begin + i);
            if (ipSegment != -1) 
                // 在判断是 ip 段的情况下,才去做截取
                path.addLast(ipSegment + "");
                dfs(s, len, split + 1, begin + i + 1, path, res);
                path.removeLast();
            
        
    

  • Python 示例:
from typing import List

class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        size = len(s)
        if size < 4 or size > 12:
            return []

        path = []
        res = []
        self.__dfs(s, size, 0, 0, path, res)
        return res

    def __dfs(self, s, size, split_times, begin, path, res):
        if begin == size:
            if split_times == 4:
                res.append('.'.join(path))
            return

        if size - begin < (4 - split_times) or size - begin > 3 * (4 - split_times):
            return

        for i in range(3):
            if begin + i >= size:
                break

            ip_segment = self.__judge_if_ip_segment(s, begin, begin + i)

            if ip_segment != -1:
                path.append(str(ip_segment))
                self.__dfs(s, size, split_times + 1, begin + i + 1, path, res)
                path.pop()

    def __judge_if_ip_segment(self, s, left, right):
        size = right - left + 1

        if size > 1 and s[left] == '0':
            return -1

        res = int(s[left:right + 1])

        if res > 255:
            return - 1
        return res
  • 与上面的示例代码不同之处只在于剪枝少判断,而且也是先判断截取的 ip 段是否合法,然后用截取函数截取字符串,执行结果上会快一些:
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class Solution 

    public List<String> restoreIpAddresses(String s) 
        int len = s.length();
        List<String> res = new ArrayList<>();
        if (len > 12 || len < 4) 
            return res;
        

        Deque<String> path = new ArrayDeque<>(4);
        dfs(s, len, 0, 4, path, res);
        return res;
    

    // 需要一个变量记录剩余多少段还没被分割

    private void dfs(String s, int len, int begin, int residue, Deque<String> path, List<String> res) 
        if (begin == len) 
            if (residue == 0) 
                res.add(String.join(".", path));
            
            return;
        

        for (int i = begin; i < begin + 3; i++) 
            if (i >= len) 
                break;
            

            if (residue * 3 < len - i) 
                continue;
            

            if (judgeIpSegment(s, begin, i)) 
                String currentIpSegment = s.substring(begin, i + 1);
                path.addLast(currentIpSegment);

                dfs(s, len, i + 1, residue - 1, path, res);
                path.removeLast();
            
        
    

    private boolean judgeIpSegment(String s, int left, int right) 
        int len = right - left + 1;
        if (len > 1 && s.charAt(left) == '0') 
            return false;
        

        int res = 0;
        while (left <= right) 
            res = res * 10 + s.charAt(left) - '0';
            left++;
        

        return res >= 0 && res <= 255;
    

② 回溯法(LeetCode 官方解法)

  • 由于需要找出所有可能复原出的 IP 地址,因此可以考虑使用回溯的方法,对所有可能的字符串分隔方式进行搜索,并筛选出满足要求的作为答案。
  • 设题目中给出的字符串为 s,用递归函数 dfs(segId,segStart) 表示正在从 s[segStart] 的位置开始,搜索 IP 地址中的第 segId 段,其中 segId∈0,1,2,3。由于 IP 地址的每一段必须是 [0,255] 中的整数,因此从 segStart 开始,从小到大依次枚举当前这一段 IP 地址的结束位置 segEnd。如果满足要求,就递归地进行下一段搜索,调用递归函数 dfs(segId+1,segEnd+1)。
  • 特别地,由于 IP 地址的每一段不能有前导零,因此如果 s[segStart] 等于字符 0,那么 IP 地址的第 segId 段只能为 0,需要作为特殊情况进行考虑。
  • 在搜索的过程中,如果已经得到了全部的 4 段 IP 地址(即 segId=4),并且遍历完了整个字符串(即 segStart=∣s∣,其中 ∣s∣ 表示字符串 s 的长度),那么就复原出了一种满足题目要求的 IP 地址,将其加入答案。在其它的时刻,如果提前遍历完了整个字符串,那么需要结束搜索,回溯到上一步。
  • Java 示例:
class Solution 
    static final int SEG_COUNT = 4;
    List<String> ans = new ArrayList<String>();
    int[] segments = new int[SEG_COUNT];

    public List<String> restoreIpAddresses(String s) 
        segments = new int[SEG_COUNT];
        dfs(s, 0, 0);
        return ans;
    

    public void dfs(String s, int segId, int segStart) 
        // 如果找到了 4 段 IP 地址并且遍历完了字符串,那么就是一种答案
        if (segId == SEG_COUNT) 
            if (segStart == s.length()) 
                StringBuffer ipAddr = new StringBuffer();
                for (int i = 0; i < SEG_COUNT; ++i) 
                    ipAddr.append(segments[i]);
                    if (i != SEG_COUNT - 1) 
                        ipAddr.append('.');
                    
                
                ans.add(ipAddr.toString());
            
            return;
        

        // 如果还没有找到 4 段 IP 地址就已经遍历完了字符串,那么提前回溯
        if (segStart == s.length()) 
            return;
        

        // 由于不能有前导零,如果当前数字为 0,那么这一段 IP 地址只能为 0
        if (s.charAt(segStart) == '0') 
            segments[segId] = 0;
            dfs(s, segId + 1, segStart + 1);
        

        // 一般情况,枚举每一种可能性并递归
        int addr = 0;
        for (int segEnd = segStart; segEnd < s.length(); ++segEnd) 
            addr = addr * 10 + (s.charAt(segEnd) - '0');
            if (addr > 0 && addr <= 0xFF) 
                segments[segId] = addr;
                dfs(s, segId + 1, segEnd + 1);
             else 
                break;
            
        
    

  • Python 示例:
class Solution:
    def restoreIpAddresses(self, s: str) -> List[str]:
        SEG_COUNT = 4
        ans = list()
        segments = [0] * SEG_COUNT
        
        def dfs(segId: int, segStart: int):
            # 如果找到了 4 段 IP 地址并且遍历完了字符串,那么就是一种答案
            if segId == SEG_COUNT:
                if segStart == len(s):
                    ipAddr = ".".join(str(seg) for seg in segments)
                    ans.append(ipAddr)
                return
            
            # 如果还没有找到 4 段 IP 地址就已经遍历完了字符串,那么提前回溯
            if segStart == len(s):
                return

            # 由于不能有前导零,如果当前数字为 0,那么这一段 IP 地址只能为 0
            if s[segStart] == "0":
                segments[segId] = 0
                dfs(segId + 1, segStart + 1)
            
            # 一般情况,枚举每一种可能性并递归
            addr = 0
            for segEnd in range(segStart, len(s)):
                addr = addr * 10 + (ord(s[segEnd]) - ord("0"))
                if 0 < addr <= 0xFF:
                    segments[segId] = addr
                    dfs(segId + 1, segEnd + 1)
                else:
                    break
        

        dfs(0, 0)
        return ans

以上是关于数据机构与算法之深入解析“复原IP地址”的求解思路与算法示例的主要内容,如果未能解决你的问题,请参考以下文章

数据机构与算法之深入解析“解码方法”的求解思路与算法示例

数据机构与算法之深入解析“柱状图中最大的矩形”的求解思路与算法示例

数据机构与算法之深入解析“完美矩形”的求解思路与算法示例

数据机构与算法之深入解析“交错字符串”的求解思路与算法示例

数据机构与算法之深入解析“最小覆盖子串”的求解思路与算法示例

数据结构与算法之深入解析“完美数”的求解思路与算法示例