从易到难解析-深度优先搜索DFS-以及常见面试题

DFS相关概念

在非二叉树上的深度优先搜索(Depth-first Search)中,90%的问题,不是求组合(Combination)就是求排列(Permutation)。特别是组合类的深度优先搜索的问题特别的多。而排列组合类的搜索问题,本质上是一个“隐式图”的搜索问题。

隐式图搜索问题:
一个问题如果没有明确的告诉你什么是点,什么是边,但是又需要你进行搜索的话,那就是一个隐式图搜索问题了。所以对于这类问题,我们首先要分析清楚什么是点什么是边。

BFS vs DFS
宽度优先搜索的空间复杂度取决于宽度
深度优先搜索的空间复杂度取决于深度

我们在使用DFS时,需要注意的三个要素

  • 递归的定义
  • 递归的拆解
  • 递归的出口

DFS主要可以分为:

DFS分类 问题模型 判断条件 时间复杂度
排列组合问题 求出所有满足条件的“组合” 组合中的元素是顺序无关的 O(2^n * n)
排列组合问题 求出所有满足条件的“排列” 组合中的元素是顺序“相关”的 O(n! * n)

DFS 时间复杂度通用计算公式:

O(方案个数 * 构造每个方案的时间)
所以排列问题 = O(n! * n) 组合问题 = O(2^n * n)

算法模板

public ReturnType dfs(参数列表){
	if(递归出口){
	记录答案
	return;
	}
	for(所有的拆解可能性){
		修改所有参数
		dfs(参数列表);
		还原所有被修改过的参数
	}
	return something;
	//很多时候不需要return,除了分治的写法

组合类组合DFS

首先来一道题让大家明了什么叫做组合类DFS:

LeetCode 71.子集

LeetCode 71. 子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],3],[2,3]]

示例 2:
输入:nums = [0]
输出:[[],[0]]

解决方法:

class Solution {
    public List<List<Integer>> subsets(int[] nums) {
        //首先进行异常检测
        if(nums == null){
            return results;
        }
        //创建答案集合
        List<List<Integer>> results = new ArrayList<>();
        
        Arrays.sort(nums);
        dfs(nums, 0 , new ArrayList<Integer>(), results);
        return results;
    }

    private void dfs(int[]nums, 
    				 int startIndex, 
    				 ArrayList<Integer>subset,
    				 List<List<Integer>> results){
        //直接加加的是reference,这时候后面pop也会让results里的对应pop
        results.add(new ArrayList<Integer>(subset));
		
        for(int i = startIndex; i < nums.length; i++ ){
            //[1] -> [1,2]
            subset.add(nums[i]);
                //去寻找以[1,2] 开头的子集
            dfs(nums, i + 1, subset, results);
            //[1,2] -> [1]
            subset.remove(subset.size() -1 );
        }
    }
}

Follow up:
在攻克了上面的问题之后,请同学思考下这个问题:如果所给的集合是[1,2,3],并且我输出的子集中还不允许出现两个[1,3],这时我们该怎么改动搜索过程呢?

LeetCode 90.子集ll

LeetCode 90. 子集ll

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。
示例:

输入: [1,2]
输出:
[
  [2],
  [1],
  [1,
  [2,
  []
]

这个问题是一个经典的全子集问题的follow up,稍加思考过后,来看看这种带有重复元素的集合是怎么处理的吧。

选代表:从若干个数字相同但顺序不同的小集合中拿出一个有序的集合作为代表,将剩下的无序集合舍弃。

class Solution {
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> results = new ArrayList<>();

        if(nums == null){
            return results;
        }
        Arrays.sort(nums);
        dfs(nums, int startIndex, ArrayList<Integer> subset,              
                    List<List<Integer>> results){
        
        results.add(new ArrayList<Integer>(subset));

        for(int i = startIndex; i < nums.length; i++ ){
            if( i != 0 && nums[i-1] == nums[i] && i > startIndex){
                continue;
            }
            subset.add(nums[i]);
            dfs(nums, results);
            subset.remove(subset.size() -1 );
        }
    }
}

那有没有一种数据结构,可以完成去重工作呢?答案自然是hash,所以我们可以对所有的集合进行哈希,每次将当前搜索到的集合subset放入结果集合subsets中的时候,只需将这个集合看做是key,如果对应的value不存在,就证明这个集合是个没出现过的新集合,这时再把新的集合放入subsets中即可。

public class Solution {
    /**
     * @param nums: A set of numbers.
     * @return: A list of lists. All valid subsets.
     */
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> subsets = new ArrayList<>();
        HashMap<String, Boolean> visited = new HashMap<String, Boolean>();
        Arrays.sort(nums);
        dfs(nums, 0, new ArrayList<>(), subsets, visited);
        
        return subsets;
    }
    
    String getHash(List<Integer> subset) {
        String hashString = "";
        for (int i = 0;i < subset.size(); i++) {
            hashString += subset.get(i).toString();
            hashString += "_";
        }
        
        return hashString;
    }
    
    void dfs(int[] nums, 
             int startIndex, 
             List<Integer> subset,
             List<List<Integer>> subsets,
             HashMap<String, Boolean> visited) {
        String hashString = getHash(subset);
        
        if (visited.containsKey(hashString)) {
            return ;
        }
        
        visited.put(hashString, true);
        subsets.add(new ArrayList<Integer>(subset));
        for (int i = startIndex;i < nums.length; i++) {
            subset.add(nums[i]);
            dfs(nums, visited);
            subset.remove(subset.size() - 1);
        }
        
    }
    
}

排列类组合DFS

全排列问题是“排列式”深度优先搜索问题的鼻祖。很多搜索的问题都可以用类似全排列的代码来完成。
那什么是排列式搜索?

问题的模型是求出一个集合中所有元素的满足某个条件的排列 排列和组合的区别是排列是有顺序的 [1,3] 和 [3,1] 是同一个组合但不是同一个排列

按照惯例,先上例题帮助大家理解:

LeetCode 46.全排列

LeetCode 46.全排列
题目简介: 给定一个 没有重复 数字的序列,返回其所有可能的全排列。

示例:

输入: [1,3]
输出: [ [1,3],[1,3,2],[2,1,1],[3,1] ]

class Solution {
    public List<List<Integer>> permute(int[] nums) {
        List<List<Integer>> permutations  = new ArrayList<>();
        if(nums == null){
            return null;
        }
		
        boolean[] visited = new boolean[nums.length];
        List<Integer> permutation = new ArrayList<>();
        dfs(nums, visited, permutation, permutations);
        return permutations;
    }

    private void dfs(int[] nums,
                    boolean[] visited,
                    List<Integer> permutation,
                    List<List<Integer>> permutations){
         //递归出口
        if(permutation.size() == nums.length){
            permutations.add(new ArrayList<Integer>(permutation));
            return;
        }

        for(int i = 0; i < nums.length; i++){
            //该数字已存在
            if(visited[i]){
                continue;
            }
            visited[i] = true;
            permutation.add(nums[i]);
            dfs(nums, permutations);
            //回溯
            permutation.remove(permutation.size()-1);
            visited[i] = false;
        }
    }
}

Follow up:

LeetCode 47.全排列ll

LeetCode 47.全排列ll

题目简介: 给定一个 有重复 数字的序列,返回其所有可能的全排列。

//与上一题只有两个不同的点:
class Solution {
    public List<List<Integer>> permuteUnique(int[] nums) {

        List<List<Integer>> permutations = new ArrayList<>();

        if(nums == null || nums.length == 0){
            return null;
        }

        List<Integer> permutation = new ArrayList<>();
        boolean[] visited = new boolean[nums.length];
        //
        Arrays.sort(nums);
        dfs(permutations, nums, visited);
        return permutations;
    }

    private void dfs(List<List<Integer>> permutations,
                    int[] nums,
                    boolean[] visited){
        
        if(permutation.size() == nums.length){
            permutations.add(new ArrayList<Integer>(permutation));
            return;
        }

        for(int i = 0; i < nums.length; i++){
            if(visited[i]){
                continue;
            }

            if(i > 0 && nums[i] == nums[i -1] && !visited[i - 1]){
                continue;
            }

            visited[i] = true;
            permutation.add(nums[i]);
            dfs(permutations, visited);
            visited[i] = false;
            permutation.remove(permutation.size() - 1 );
        }
    }
}

TSP问题

排列式搜索的典型代表
Traveling Salesman Problem 又称中国邮路问题

描述
给 n 个城市(从 1 到 n),城市和无向道路成本之间的关系为3元组 [A,B,C](在城市 A 和城市 B 之间有一条路,成本是 C)我们需要从1开始找到的旅行所有城市的付出最小的成本。

一个城市只能通过一次。
你可以假设你可以到达所有的城市。

class Result {
    int minCost;
    public Result(){
        this.minCost = 1000000;
    }
}

public class Solution {
    /**
     * @param n: an integer,denote the number of cities
     * @param roads: a list of three-tuples,denote the road between cities
     * @return: return the minimum cost to travel all cities
     */
    public int minCost(int n, int[][] roads) {
        int[][] graph = constructGraph(roads, n);
        Set<Integer> visited = new HashSet<Integer>();
        Result result = new Result();
        visited.add(1);
        
        dfs(1, n, graph, result);
        
        return result.minCost;
    }

    void dfs (int city, 
              int n, 
              Set<Integer> visited, 
              int cost,
              int[][] graph,
              Result result) {
        
        if (visited.size() == n) {
            result.minCost = Math.min(result.minCost, cost);
            return ;
        }
        
        for(int i = 1; i < graph[city].length; i++) {
            if (visited.contains(i)) {
                continue;
            }
            visited.add(i);
            dfs(i, cost + graph[city][i], result);
            visited.remove(i);
        }
    }

    int[][] constructGraph(int[][] roads, int n) {
        int[][] graph = new int[n + 1][n + 1];
        for (int i = 0; i < n + 1; i++) {
            for (int j = 0; j < n + 1; j++) {
                graph[i][j] = 100000;
            }
        }
        int roadsLength = roads.length;
        for (int i = 0; i < roadsLength; i++) {
            int a = roads[i][0], b = roads[i][1], c = roads[i][2];
            graph[a][b] = Math.min(graph[a][b], c);
            graph[b][a] = Math.min(graph[b][a], c);
        }

        return graph;
    }
    
}

DFS高频面试题

剑指offer 38. 字符串的不同排列

剑指offer 38. 字符串的不同排列

class Solution {
    public String[] permutation(String s) {
        if(s == null){
            return null;
        }

        char[] chars = s.toCharArray();
        Arrays.sort(chars);
        boolean[] visited = new boolean[chars.length];
        List<String> permutations = new ArrayList<>();
        dfs(chars, "", permutations, visited);
        return permutations.toArray(new String[permutations.size()]);
    }

    private void dfs(char[]chars,
                     String permutation,
                     List<String>  permutations,
                     boolean[] visited){
        if(permutation.length() == chars.length){
            permutations.add(permutation);
            return;
        }
        for(int i = 0; i < chars.length; i++){
            if(visited[i]){
                continue;
            }
            if(i != 0 && chars[i] == chars[i-1] && ! visited[i-1]){
                continue;
            }
            visited[i] = true;
            dfs(chars, permutation + chars[i], visited);
            visited[i] = false;
        }
    }
}

数字组合 Combination Sum

给定一个候选数字的集合 candidates 和一个目标值 target. 找到 candidates 中所有的和为 target 的组合.

在同一个组合中,candidates 中的某个数字不限次数地出现.

样例 1:
输入: candidates = [2, 3, 6, 7], target = 7
输出: [[7], [2, 2, 3]]

样例 2:
输入: candidates = [1], target = 3
输出: [[1, 1, 1]]

与 Subsets 比较

  • Combination Sum 限制了组合中的数之和 ,加入一个新的参数来限制
  • Subsets无重复元素,Combination Sum 有重复元素,需要先去重
  • Subsets 一个数只能选一次,Combination Sum 一个数可以选很多次
  • 搜索时从 index 开始而不是从 index + 1
public class Solution {
    /**
     * @param candidates: A list of integers
     * @param target: An integer
     * @return: A list of lists of integers
     */
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // write your code 
        List<List<Integer>> results = new ArrayList<>();
        if(candidates == null){
            return null;
        }
        candidates = removeDuplicates(candidates);
        dfs(candidates, target, results);
        return results;
    }

    private int[] removeDuplicates(int[] candidates){
        Arrays.sort(candidates);
        int index = 0;
        for(int i = 1; i < candidates.length; i++){
            if(candidates[i] != candidates[index]){
                candidates[++index] = candidates[i];
            }
        }
        int[] candidatesNew = new int[index + 1];
        for(int i = 0; i < index + 1; i++){
            candidatesNew[i] = candidates[i];
        }
        return candidatesNew;
    }
    private void dfs(int[]candidates,
                     int target,
                     int start,
                     ArrayList<Integer>result,
                     List<List<Integer>> results){
        if(target == 0){
            results.add(new ArrayList<>(result));
            return;
        }
        for(int i = start; i < candidates.length; i++){
            if(target < candidates[i]){
                break;
            }
            result.add(candidates[i]);
            dfs(candidates, target - candidates[i], i, result, results);
            result.remove(result.size()-1);
        }
    }
}

Follow up:

k数和 II

给定n个不同的正整数,整数k(1<= k <= n)以及一个目标数字。

在这n个数里面找出K个数,使得这K个数的和等于目标数字,你需要找出所有满足要求的方案。

代码实现:

public class Solution {
    /**
     * @param A: an integer array
     * @param k: a postive integer <= length(A)
     * @param targer: an integer
     * @return: A list of lists of integer
     */
    public List<List<Integer>> kSumII(int[] A, int k, int targer) {
        // write your code here
        // if(A == null || A.length == 0){
        //     return null;
        // }
        Arrays.sort(A);
        List<List<Integer>> results = new ArrayList<>();
        dfs(A, results, k, targer, new ArrayList<Integer>());
        return results;
    }

    private void dfs(int[]A,
                     List<List<Integer>> results,
                     int k,
                     List<Integer> subset){
        if(k == 0 && target == 0){
            results.add(new ArrayList<Integer>(subset));
            return;
        }

        if(k == 0 || target <= 0){
            return;
        }

        for(int i = start; i < A.length; i++){
            subset.add(A[i]);
            dfs(A, k-1, target - A[i], subset);
            subset.remove(subset.size() - 1);
        }
    }
}

LeetCode 17. 电话号码的字母组合

LeetCode 17. 电话号码的字母组合

在这里插入图片描述

class Solution {
    public static String[] KEYBOARD = {
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz"
    };

    public List<String> letterCombinations(String digits) {
        List<String> combinations = new ArrayList<>();
        if(digits == null || digits.length() == 0){
            return combinations;
        }
        StringBuffer sb = new StringBuffer();
        dfs(digits, sb, combinations);
        return combinations;
    }

    private void dfs(String digits, 
                     int index, 
                     StringBuffer combination, 
                     List<String> combinations){

        if(index == digits.length()){
            combinations.add(combination.toString());
            return;
        }

        int digit = digits.charAt(index) - '0';
        for(int i = 0; i < KEYBOARD[digit].length(); i++){
            combination.append(KEYBOARD[digit].charAt(i));

            dfs(
                digits,
                index + 1,
                combination,
                combinations
            );

            combination.deleteCharAt(combination.length()-1);
        }

    }
}

LeetCode 212. 单词搜索 II

LeetCode 212. 单词搜索 II

给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,找出所有同时在二维网格和字典中出现的单词。

单词必须按照字母顺序,通过 相邻的单元格 内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用

public class Solution {
    public static int[] dx = {0, -1, 0};
    public static int[] dy = {1, -1};

    public List<String> findWords(char[][] board, String[] words) {
        if (board == null || board.length == 0) {
            return null;
        }
        
        if (board[0] == null || board[0].length == 0) {
            return null;
        }
        
        boolean[][] visited = new boolean[board.length][board[0].length];
        Map<String, Boolean> prefixIsWord = getPrefixSet(words);
        Set<String> wordSet = new HashSet<>();
        
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[i].length; j++) {
                visited[i][j] = true;
                dfs(board, j, String.valueOf(board[i][j]), prefixIsWord, wordSet);
                visited[i][j] = false;
            }
        }
        
        return new ArrayList<String>(wordSet);
    }
    
    private Map<String, Boolean> getPrefixSet(String[] words) {
        Map<String, Boolean> prefixIsWord = new HashMap<>();
        for (String word : words) {
            for (int i = 0; i < word.length() - 1; i++) {
                String prefix = word.substring(0, i + 1);
                if (!prefixIsWord.containsKey(prefix)) {
                    prefixIsWord.put(prefix, false);
                }
            }
            prefixIsWord.put(word, true);
        }
        
        return prefixIsWord;
    }
    
    private void dfs(char[][] board,
                     boolean[][] visited,
                     int x,
                     int y,
                     String word,
                     Map<String, Boolean> prefixIsWord,
                     Set<String> wordSet) {
        if (!prefixIsWord.containsKey(word)) {
            return;
        }
        
        if (prefixIsWord.get(word)) {
            wordSet.add(word);
        }
        
        for (int i = 0; i < 4; i++) {
            int adjX = x + dx[i];
            int adjY = y + dy[i];
            
            if (!inside(board, adjX, adjY) || visited[adjX][adjY]) {
                continue;
            }
            
            visited[adjX][adjY] = true;
            dfs(board, adjY, word + board[adjX][adjY], wordSet);
            visited[adjX][adjY] = false;
        }
    }
    
    private boolean inside(char[][] board, int x, int y) {
        return x >= 0 && x < board.length && y >= 0 && y < board[0].length;
    }
}

LeetCode 127. 单词接龙

LeetCode 127. 单词接龙

字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:

序列中第一个单词是 beginWord 。
序列中最后一个单词是 endWord 。
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典 wordList 中的单词。
给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。

public class Solution {
    public int ladderLength(String start, String end, List<String> wordList) {
        Set<String> dict = new HashSet<>();
        for (String word : wordList) {    //将wordList中的单词加入dict,有一个test case里面wordlist很长,会超时。所以要转成set,因为list的contains操作是O(n),set的contains操作是O(1)
            dict.add(word);
        }
        
        if (start.equals(end)) {
            return 1;
        }
        
        HashSet<String> hash = new HashSet<String>();
        Queue<String> queue = new LinkedList<String>();
        queue.offer(start);
        hash.add(start);
        
        int length = 1;
        while (!queue.isEmpty()) {			//开始bfs
            length++;
            int size = queue.size();
            for (int i = 0; i < size; i++) {		//枚举当前步数队列的情况
                String word = queue.poll();
                for (String nextWord: getNextWords(word, dict)) {
                    if (hash.contains(nextWord)) {
                        continue;
                    }
                    if (nextWord.equals(end)) {
                        return length;
                    }
                    
                    hash.add(nextWord);				//存入新单词
                    queue.offer(nextWord);
                }
            }
        }
        
        return 0;
    }


    // get connections with given word.
    // for example,given word = 'hot',dict = {'hot','hit','hog'}
    // it will return ['hit','hog']
    private ArrayList<String> getNextWords(String word, Set<String> dict) {
        ArrayList<String> nextWords = new ArrayList<String>();
        for (int i = 0; i < word.length(); i++) {
        String left = word.substring(0, i);
        String right = word.substring(i + 1);
            for (char ch = 'a'; ch <= 'z'; ch++) {
                if (word.charAt(i) == ch) {
                    continue;
                }
                String nextWord = left + ch + right;
                if (dict.contains(nextWord)) {				//如果dict中包含新单词,存入nextWords
                    nextWords.add(nextWord);
                }
            }
        }
        return nextWords;									//构造当前单词的全部下一步方案
    }
}

相关文章

背景:计算机内部用补码表示二进制数。符号位1表示负数,0表...
大家好,我们现在来讲解关于加密方面的知识,说到加密我认为不...
相信大家在大学的《算法与数据结构》里面都学过快速排序(Qui...
加密在编程中的应用的是非常广泛的,尤其是在各种网络协议之...
前言我的目标是写一个非常详细的关于diff的干货,所以本文有...
对称加密算法 所有的对称加密都有一个共同的特点:加密和...