数据结构与算法(4)——搜索算法

数据结构与算法(4)——搜索算法

  • 1. 搜索算法的定义与特点
  • 2. 搜索算法的解题思路
    • 2.1 广度优先搜索
    • 2.2 深度优先搜索
    • 2.3 回溯法
  • 3. LeetCode中的搜索算法题

1. 搜索算法的定义与特点

定义:需要在“树”中或者“图”中搜索到我们需要的序列或者位置。

特点:通常是给定了一个“树”或“图”,然后要求里面满足要求的部分

常用的有三种搜索算法:深度优先搜索、广度优先搜索、回溯算法。

一般深度优先搜索能做的,广度优先搜索也能做。回溯算法是用来处理需要穷举出所有情况的问题,典型的为排列组合问题,通常这也称为“暴力搜索”,且回溯算法相比于深度优先搜索和广度优先搜索有一个显著的特点——“恢复现场”。

2. 搜索算法的解题思路

2.1 广度优先搜索

广度优先搜索的核心在于队列

在广度优先遍历中,需要首先给队列填充第一个节点,然后循环进行如下队列操作的三步,直到队列中不再有节点:

  1. 队列首部弹出一个节点,此节点是否访问过,如果访问过,跳过下面两步,重新弹出新的节点;否则访问此节点(访问这一步可以做很多操作)

  2. 查找此节点的所有可行节点(大多数情况下为不越界的邻接节点,但是有时候不仅要求邻接,对邻接节点的值的大小还有要求)

  3. 将第2步的可行节点们插入到队列尾部

队列中不再有节点时,就完成了一次广度优先搜索,因此需要在广度优先搜索外再套一层for循环,让每个节点都有机会被广度优先搜索遍历。

注意上面的模板并不是唯一的,我们可以在查找可行节点的时候,就要求节点是未访问的,这样我们就无需再在第一步判断。这种做法只是少了有限几步,两种模板在时间复杂度上并不会有差异。

695. 岛屿的最大面积 的广度优先搜索参考代码:

class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        rowLen, colLen = len(grid), len(grid[0])
        ans = 0

        for i in range(rowLen):
            for j in range(colLen):
                # 填充第一个节点
                queue = collections.deque([(i, j)])
                area = 0

                while queue:
                    # 弹出队首节点
                    cur_row, cur_col = queue.popleft()
                    # 查看节点是否访问过,访问过就跳过后面的步骤,重新开始加入弹出节点
                    if grid[cur_row][cur_col] == 0:
                        continue
                    # 如果节点未被访问,则开始访问 
                    grid[cur_row][cur_col] = 0      # 标记为访问过
                    area += 1                       # 面积+1
                    
                    for dr, dc in [[-1,0], [1, 0], [0, -1], [0, 1]]:
                        # 查询所有可行节点——即 不越界的 邻接节点
                        if cur_row+dr>=0 and cur_row+dr<rowLen and cur_col+dc>=0 and cur_col+dc<colLen:
                            queue.append((cur_row+dr, cur_col+dc))      # 可行节点加入到队列中
                ans = max(ans, area)
        
        return ans


2.2 深度优先搜索

深度优先搜索的核心在于递归。写递归的时候一定要想明白:递归终止条件是什么?递归函数的返回值是什么?

深度优先搜索子函数三步:

  1. 首先判断此节点是否访问过,如果访问过,直接退出子函数;否则访问此节点(访问这一步可以做很多操作)

  2. 查找此节点的所有可行节点(大多数情况下为不越界的邻接节点,但是有时候不仅要求邻接,对邻接节点的值的大小还有要求)

  3. 递归调用此子函数去搜索第2步的可行节点

在深度优先搜索中,主函数就是遍历所有的节点,让每个节点都有机会被深度搜索。

注意上面的模板并不是唯一的,我们可以在查找可行节点的时候,就要求节点是未访问的,这样我们就无需再在第一步判断。这种做法只是少了有限几步,两种模板在时间复杂度上并不会有差异。

695. 岛屿的最大面积 的深度优先搜索参考代码:

class Solution:
    def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
        rowLen, colLen = len(grid), len(grid[0])
        def DFS(grid, row, col):
            if grid[row][col]==0:
                return 0
            # 如果节点未被访问,则开始访问 
            grid[row][col] = 0
            area = 1
            for i,j in [[-1, 0], [1, 0], [0, -1], [0, 1]]:
                # 查询所有可行节点——即 不越界的 邻接节点
                if row+i>=0 and row+i<rowLen and col+j>=0 and col+j<colLen:
                    area += DFS(grid, row+i, col+j)     # 递归搜索所有可行节点
            return area
        
        ans = 0
        for i in range(rowLen):
            for j in range(colLen):
                ans = max(DFS(grid, i, j), ans)
        return ans


2.3 回溯法

回溯法有点像深度优先遍历,不过回溯法每次往回走的时候都需要恢复现场,而深度优先遍历不需要。

即深度优先遍历的过程为:修改当前节点状态(如:标记此节点已访问)-> 递归遍历节点的邻接节点

回溯法的过程为:修改当前节点状态(如:标记此节点已访问)-> 递归遍历节点的邻接节点 -> 恢复当前节点状态(如:标记此节点未访问)

回溯法需要“恢复现场”是因为在“修改当前节点状态”时,有很多“当前节点”需要修改,且它们都需要被当做第一次修改来看待。因为回溯法本质也是递归,所以需要特别地注意递归终止条件。

使用回溯法的步骤:

  1. 首先判断回溯是否深入到底了,如果是,则进行相关操作后(如复制某一数组),直接退出子函数(注意返回值);

  2. 判断此节点是否访问过,如果访问过,直接退出子函数(注意返回值);否则开始访问此节点(访问这一步可以做很多操作)

  3. 查找此节点的所有可行节点(大多数情况下为不越界的邻接节点,但是有时候不仅要求邻接,对邻接节点的值的大小还有要求)

  4. 递归调用此子函数去搜索第2步的可行节点

  5. 恢复现场,也就是对第2步的访问操作进行逆操作。

还有另一种步骤如下所示,和上面的步骤相比就是在查找可行节点的时候,还要求可行节点是未访问的,这样在回溯法中就可以及时剪枝,减少时间复杂度,所以更加推荐使用这种方法

  1. 首先判断回溯是否深入到底了,如果是,则进行相关操作后(如复制某一数组),直接退出子函数(注意返回值);否则开始访问此节点(访问这一步可以做很多操作,访问这一步有时候还会移到第二步:“查找所有未访问的可行节点”之后,具体情况具体分析)

  2. 查找此节点的所有 未访问的 可行节点(大多数情况下为不越界的邻接节点,但是有时候不仅要求邻接,对邻接节点的值的大小还有要求)

  3. 递归调用此子函数去搜索第2步的 未访问的 可行节点

  4. 恢复现场,也就是对第1步的访问操作进行逆操作。

回溯法适用的题型有:

  1. 排列组合问题

  2. 字符串搜索问题

  3. 需要暴力搜索考虑的游戏问题,如:51. N 皇后,37. 解数独,488. 祖玛游戏

17. 电话号码的字母组合 的回溯法参考代码:

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        letterdict = {'2':'abc', '3':'def', '4':'ghi', '5':'jkl', '6':'mno', '7':'pqrs', '8':'tuv', '9':'wxyz'}
        digLen = len(digits)
        if digLen == 0:
            return []

        def backtrack(depth):
            # 1.判断是否回溯深入到底
            if depth == digLen:
                res.append("".join(strList))
                return
            
            # 1.开始访问此节点
            for tmpStr in letterdict[digits[depth]]:
                strList.append(tmpStr)
                # 2.查找此结点的所有未访问的可行节点  3. 并进行递归访问
                backtrack(depth+1)
                # 4.恢复现场
                strList.pop()
        
        res = []
        strList = []
        backtrack(0)
        return res


3. LeetCode中的搜索算法题

695. 岛屿的最大面积

简评:使用深度优先遍历或广度优先遍历。此题属于经典题,对于使用深度优先遍历而言,首先肯定需要给矩阵中每个元素进行初始的深度遍历机会,因此有两个for循环;其次在写深度优先遍历的时候,需要确定递归终止条件为元素访问过(此题的特殊性,可以直接用grid直接设置是否访问),因为要求面积,所以递归的返回值为目前访问过的面积。最后就是每做一次深度优先遍历,都需要比较目前这个面积和之前的“最大面积”,哪个大,从而存储下最终答案。

对于使用广度优先遍历而言,首先肯定需要给矩阵中每个元素进行初始的深度遍历机会,因此有两个for循环;其次在写广度优先遍历的时候,需要确定队列的入队条件,每有一个节点入队,面积就加1。最后就是每做一次广度优先遍历,都需要比较目前这个面积和之前的“最大面积”,哪个大,从而存储下最终答案。

547. 省份数量

简评:同样深度优先遍历和广度优先遍历都可以。深度优先遍历注意:递归终止条件是什么?递归函数的返回值是什么?

417. 太平洋大西洋水流问题

简评:此题需要小技巧,需要利用“水往高处流”的逆向思维,首先用深度优先遍历(或者广度优先遍历)来找到能流到太平洋水域的陆地,然后再找到能留到大西洋水域的陆地,最后两个陆地取交集即可。

46. 全排列

简评:此题为典型的回溯算法题,可以直接对原数组进行修改,节省空间。

77. 组合

简评:此题为典型的回溯算法题。

17. 电话号码的字母组合

简评:此题为回溯算法题,本质上也是组合问题。

79. 单词搜索

简评:此题为回溯算法题。注意最好进行逐字符比较,这样能够进行剪枝操作,提前确定有哪些字符不符合要求,以减少时间复杂度。

51. N 皇后

简评:此题为回溯算法题。注意最好逐行考虑来填充皇后,这样能够省去访问的操作,当然还需要额外地构建列访问数组,左斜线访问数组,右斜线访问数组,这三个数组来提前确定哪些结果是不符合要求的,以便进行剪枝操作,以减少时间复杂度。

784. 字母大小写全排列

简评:此题为回溯算法题。这里需要注意的是,自己需要编辑当前节点的所有可能情况。

你可能感兴趣的:(数据结构,算法,leetcode,数据结构)