week05_task_搜索,二叉堆

五、搜索,二叉堆

在这里插入图片描述



来源

极客时间2021算法训练营

作者: 李煜东


1 DFS相关题目

状态,就是程序维护的所有动态数据构成的集合

所有可能状态构成的集合就是一个问题的状态空间

  • 搜索

搜索就是采用直接遍历整个状态空间的方式寻找答案的一类算法

根据遍历状态空间(图)方式的不同,可分为
•深度优先搜索(DFS, depth first search)
•广度优先搜索(BFS, breadth first search)

一般来说,每个状态只遍历一次
所以当状态空间是"图"而不是"树"时,要判重(记忆化)

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

  • 思路: 建立数字到字母的映射, 用dfs搜索考虑每一种状态 >>> 注意共享变量还原
class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        dic, ans = {}, []
        dic[2] = "abc"
        dic[3] = "def"
        dic[4] = "ghi"
        dic[5] = "jkl"
        dic[6] = "mno"
        dic[7] = "pqrs"
        dic[8] = "tuv"
        dic[9] = "wxyz"

        def dfs(index, strs):
            if index == len(digits):
                ans.append(strs)
                return 
            for i in dic[int(digits[index])]:
                dfs(index + 1, strs + i)
        if digits == '': return []
        dfs(0, "")
        return ans

1.2 51 . N 皇后

  • 思路: 选皇后位置>>> 即为全排列子集>>> 满足 i - j, i + j, i, j只出现一次
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        ans, p, result = [], [], []
        used, usedPlus, usedminus = {}, {}, {}
        for i in range(n):
            used[i] = False

        for i in range(n):
            for j in range(n):
                usedPlus[i + j] = False
        
        for i in range(n):
            for j in range(n):
                usedminus[i - j] = False

        def dfs(row):
            if row == n:
                ans.append(p[:])
                print(ans)
                return
              
            for col in range(n):
                if not used[col] and not usedPlus[col + row] and not usedminus[col - row]: 
                    p.append(col)
                    used[col] = True
                    usedPlus[col + row] = True
                    usedminus[col - row] = True
                    dfs(row + 1)
                    usedminus[col - row] = False
                    usedPlus[col + row] = False
                    used[col] = False
                    p.pop()
        dfs(0)
        for p in ans:
            pattern = ["."*n] * n
            for row in range(n):
                pattern[row] = pattern[row][:p[row]] + "Q" + pattern[row][p[row] + 1 :]
            result.append(pattern)
        return result

2 BFS相关题目

2.1 200 . 岛屿数量

  • 思路: 划分连通块 >>>BFS
class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        m, n, ans = len(grid),len(grid[0]), 0
        visited = [[False] * n for _ in range(m)] 

        def bfs(sx, sy):
            q = []
            dx = [-1, 0 , 0, 1]  # 行的变化: 上  右  左 下
            dy = [0, 1, -1, 0]   # 列的变化: 上  右  左 下
            q.append([sx, sy])
            visited[sx][sy] = True
            while q:
                # 第一步取对头
                x = q[0][0]
                y = q[0][1]
                q.pop(0)
                # 扩展队头
                for i in range(4): # x y四个方向尝试走一走
                    nx = x + dx[i]
                    ny = y + dy[i]
                    if nx < 0 or nx >= m or ny >= n or ny < 0: continue #越界
                    if grid[nx][ny] != "1":continue                   # 不为1
                    if visited[nx][ny]: continue         #走过
                    q.append([nx, ny])
                    visited[nx][ny] = True

        for i in range(m):
            for j in range(n):
                if grid[i][j] == "1" and not visited[i][j]:
                    ans += 1
                    bfs(i, j)
        return ans

2.2 433 . 最小基因变化

  • 思路: 求转换步数 >>>> 图里求层数
class Solution:
    def minMutation(self, start: str, end: str, bank: List[str]) -> int:
        dic = {}
        dic[start] = 0
        if end not in bank:return -1
        else:
            q = []
            gene = ["A", "C", "G", "T"]
            q.append(start)
            while q:
                s = q[0]
                q.pop(0)
                for i in range(8):
                    for j in range(4):
                        if s[i] != gene[j]:
                            ns = s
                            ns = ns[:i] + gene[j] + ns[i + 1:]
                            if ns not in bank:continue          #改变的值不在基因库
                            if ns in dic:continue               #改变的值已经有过
                            dic[ns] = dic[s] + 1 
                            q.append(ns) 
                            if ns == end:
                                return dic[ns]
        return -1

2.3 329 . 矩阵中的最长递增路径

  • 法一 BFS 递增 >>> 有向无环图 >>> 拓扑排序 >>> 可把一个点之前所有的路径计算并求其最大值:
    1. 先建图 (m*n)>>> 二维行列号变为一维: i * n + j
    2. 开方向数组, 对合法两点间建边
    3. 从零入点出发, 拓扑排序 >>> 遍历出边 , 减度, 计算距离 >> 对入边为0点继续拓展
    4. 求最大值
class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        m, n, ans = len(matrix), len(matrix[0]), 0
        dx = [-1,0,0,1]
        dy = [0,1,-1,0]
        to, deg, q,dist = [[] for _ in range(m * n)], [0 for i in range(m * n)], [], [0 for i in range(m * n)]
        def num(i,j):
            return i * n + j

        def valid(x,y):
            return x >= 0 and x < m and y >= 0 and y < n

        def addEdge(u, v):       #拓扑排序 
            deg[v] += 1
            to[u].append(v)

        for i in range(m):
            for j in range(n):
                for k in range(4):
                    ni = i + dx[k]
                    nj = j + dy[k]
                    if valid(ni, nj) and matrix[ni][nj] > matrix[i][j] :
                        addEdge(num(i,j), num(ni, nj))
        for i in range(m * n):     #从零入度点出发
            if deg[i] == 0:
                q.append(i)
                dist[i] = 1
        while q:
            x = q[0]
            q.pop(0)
            for y in to[x]:   #遍历出边 减度
                deg[y] -= 1
                dist[y] = max(dist[y], dist[x] + 1)  #取x到y 加一步 与 dist[y]间的最大值
                if deg[y] == 0:  #当入边为0 继续扩展
                    q.append(y)

        for i in range(m * n):
            ans = max(ans, dist[i])
        return ans
  • 法二 : DFS(记忆化搜索)
class Solution:
    def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
        m, n, ans = len(matrix), len(matrix[0]), 0
        dx = [-1,0,0,1]
        dy = [0,1,-1,0]
        dist = [[0]*n for i in range(m)]
        def valid(x,y):
            return x >= 0 and x < m and y >= 0 and y < n

        def dfs(x, y):
            if dist[x][y] != 0:return dist[x][y]
            dist[x][y] = 1
            for k in range(4):
                nx = x + dx[k]
                ny = y + dy[k]
                if valid(nx, ny) and matrix[nx][ny] > matrix[x][y]:
                    dist[x][y] = max(dist[x][y], dfs(nx, ny) + 1)
            return dist[x][y]
        for i in range(m):
            for j in range(n):
                ans = max(ans, dfs(i , j))
        return ans
  • 对比:
    • DFS更适合搜索树形状态空间
      •递归本身就会产生树的结构
      •可以用一个全局变量维护状态中较为复杂的信息(例:子集方案、排列方案)
      •不需要队列,节省空间
    • 求"最小代价"、"最少步数"的题目,用BFS
      • BFS是按层次序搜索,第k步搜完才会搜k+1步,在任意时刻队列中至多只有两层
    • 状态空间为有向无环图
      • BFS拓扑排序/ DFS记忆化搜索均可

3 二叉堆

堆排序(Heap sort)基本思想:

借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆维持大顶堆性质。

  • 堆:符合以下两个条件之一的完全二叉树:

    • 大顶堆:根节点值 ≥ 子节点值。
    • 小顶堆:根节点值 ≤ 子节点值。

在这里插入图片描述
•本质上是一棵满足堆性质的完全二叉树

  • 常见操作
    • 建堆(build) : O(N)
    • 查询最值(get max/min) : O(1)
    • 插入(insert) : O(log N) : 新元素一律插入到数组heap的尾部
    • 取出最值(delete max/min) : O(logN) : 把堆顶(heap[1])与堆尾(heap[n])交换,删除堆尾(数组最后一个元素)

3.1 相关题目

  1. 23 . 合并K个升序链表
  • 思路: 利用小顶堆, 依次弹出堆顶构建合法的链表
class Solution:
    def mergeKLists(self, lists: List[ListNode]) -> ListNode:
        import heapq
        MinHeap = []
        for list_1 in lists:
            while list_1:
                heapq.heappush(MinHeap, list_1.val) #把listi中的数据逐个加到堆中
                list_1 = list_1.next
        dummy = ListNode(0) #构造虚节点
        p = dummy
        while MinHeap:
            p.next = ListNode(heapq.heappop(MinHeap)) #依次弹出最小堆的数据
            p = p.next
        return dummy.next 
  1. 239 . 滑动窗口最大值
  • 思路: 懒惰删除法: 将删除操作待到影响窗口取值范围时, 再检查其是否过界删除
class BinaryHeap:
    def __init__(self):
        self.heap = []
        
    def push(self, node):
        self.heap.append(node)
        for i in range((len(self.heap) - 2)//2, -1, -1): #从最后一个非叶子节点向上调整
            self.heapify(self.heap, i, len(self.heap) - 1)
    
    def pop(self):
        self.heap[0] = self.heap[-1]
        self.heap.pop()
        for i in range((len(self.heap) - 2)//2 + 1):   #从0开始向下建立大顶堆
            self.heapify(self.heap, i, len(self.heap) - 1)


    # 调整完全二叉树>>>>大顶堆   
    def heapify(self, arr: [int], index: int, end: int ):
        left = index * 2 + 1                        #index的左右子节点
        right = left + 1
        while left <= end:                          #当index为非子节点时
            max_index = index
            if arr[left][0] > arr[max_index][0]:
                max_index = left
            if right <= end and arr[right][0] > arr[max_index][0]:
                max_index = right
            if index == max_index:                  #若不用交换,则说明已经交换结束
                break
            arr[index],arr[max_index] = arr[max_index],arr[index]
            index = max_index                      #继续调整index下一节点
            left = index * 2 + 1
            right = left + 1

            
class Solution:
    def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
        if len(nums) <= 1: return nums
        ans, q = [],BinaryHeap()
        for i in range(len(nums)):
            q.push([nums[i], i])
            if i >= k - 1:
                while q.heap[0][1] <= i - k:
                    q.pop()
                ans.append(q.heap[0][0])
        return ans

4 二叉搜索树

二叉搜索树是一种节点值之间具有一定数量级次序的二叉树,对于树中每个节点:

若其左子树存在,则其左子树中每个节点的值都不大于该节点值
若其右子树存在,则其右子树中每个节点的值都不小于该节点值

请添加图片描述

二叉搜索树的中序遍历必然为一个有序序列

  • BST -建立
    为了避免越界,减少边界情况的特殊判断,一般在BST中额外插入两个保护结点
    • 一个关键码为正无穷(一个很大的正整数)
    • 一个关键码为负无穷
    仅由这两个结点构成的BST就是一棵初始的空BST

在这里插入图片描述

  • BST -检索
    检索关键码val是否存在
    从根开始递归查找
    •若当前结点的关键码等于vaL则已经找到
    •若关键码大于val,递归检索左子树(或不存在)
    •若关键码小于val,递归检索右子树(或不存在

  • BST -插入
    插入val与检索val的过程类似
    •若检索发现存在,则放弃插入(或者把val对应结点的计数+1,视要求而定)
    •若检索发现不存在(子树为空)、直接在对应位置新建关键码为val的结点

  • BST -求前驱/后继
    前驱:BST中小于val的最大结点
    后继:BST中大于val的最小结点
    求前驱和后继也是基于检索的,先检索val
    以后继为例:
    •如果检索到了 val,并且val存在右子树,则在右子树中一直往左走到底
    •否则说明没找到val或者val没有右子树,此时后继就在检索过程经过的结点中 (即当前结点的所有祖先节点,可以拿一个变量顺便求一下)

  • BST-删除
    从BST中删除关键码为val的结点,可以基于检索+求后继实现
    首先检索val
    如果val只有一棵子树,直接删除val,把子树和父结点相连就行了
    如果有两棵子树,需要找到后继,先删除后继,再用后继结点代替val的位置 (因为后继是右子树一直往左走到底,所以它最多只会有一棵子树)

4.1 相关题目

1.701 . 二叉搜索树中的插入操作

  • 思路: 插入基于检索 >>> 小往左, 大往右
class Solution:
    def insertIntoBST(self, root: TreeNode, val: int) -> TreeNode:
        if not root:return TreeNode(val)
        if val < root.val:
            root.left = self.insertIntoBST(root.left, val)
        if val > root.val:
            root.right = self.insertIntoBST(root.right, val)
        return root

2.面试题 04.06 . 后继者

  • 思路: 基于检索: 找到val>> 右子树最左; 没找到 >>> 检索过程中
class Solution:
    def inorderSuccessor(self, root: TreeNode, p: TreeNode) -> TreeNode:
        return self.getnext(root, p.val)
    def getnext(self, root, val):
        ans = None
        cur = root
        while cur:
            if cur.val == val:
                if cur.right:   #存在val且右子树非空
                    ans = cur.right
                    while ans.left:ans = ans.left
                break
            
            if val < cur.val:
                if not ans or ans.val > cur.val:  #检索过程中在比val大的搜索过程, 更新min
                    ans = cur
                cur = cur.left
            else:
                cur = cur.right
        return ans

3.450 . 删除二叉搜索树中的节点

  • 思路: 先检索: 找到val :无左返回右, 无右返回左, 都有用后继替换
class Solution:
    def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
        if not root:
            return None
        if root.val == key:
            if not root.left:
                return root.right
            if not root.right:
                return root.left
            Successor = root.right  #有两个子节点 : 寻找后继
            while Successor.left:
                Successor = Successor.left
            root.right = self.deleteNode(root.right, Successor.val)
            root.val = Successor.val
            return root
        if key > root.val:
            root.right = self.deleteNode(root.right, key)
        else:
            root.left = self.deleteNode(root.left, key)
        return root

习题

1 130 . 被围绕的区域

  • 思路一 dfs >>> 搜索出四周边的"O"及相连"O" >>> 即为未被包围的标记为"P" >>> 搜索其他"O"修改为"x"
class Solution:
    def solve(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        m, n = len(board), len(board[0])

        def dfs(x, y):
            if board[x][y] != "O":return
            else:
                board[x][y] = "Q"
            dx = [-1, 0, 1, 0]
            dy = [0, 1, 0, -1]
            for i in range(4):
                nx = x + dx[i]
                ny = y + dy[i]
                if 0 <= nx < m and 0 <= ny < n:
                    dfs(nx, ny)

        for row in range(m):  #第一列 及最后一列搜索"O"及相连"O"
            dfs(row, 0)
            dfs(row, n - 1)
        for col in range(1, n - 1): # 第一行及最后一行
            dfs(0, col)
            dfs(m - 1, col)
        for i in range(m):
            for j in range(n):
                board[i][j] = 'O' if board[i][j] == 'Q' else 'X'
  • 思路二: 类似的利用BFS
class Solution:
    def solve(self, board: List[List[str]]) -> None:
        """
        Do not return anything, modify board in-place instead.
        """
        m, n = len(board), len(board[0])

        def bfs(sx, sy):
            from collections import deque
            q = deque()
            board[sx][sy] = "Q"
            q.append((sx, sy))
            while q:
                x, y = q.popleft()
                dx = [-1, 0, 1, 0]
                dy = [0, 1, 0, -1]
                for i in range(4):
                    nx = x + dx[i]
                    ny = y + dy[i]
                    if 0 <= nx < m and 0 <= ny < n and board[nx][ny] == "O":
                        board[nx][ny] = "Q"
                        q.append((nx, ny))

        for row in range(m):  #第一列 及最后一列搜索"O"及相连"O"
            if board[row][0] == "O":
                bfs(row, 0)
            if board[row][n - 1] == "O":
                bfs(row, n - 1)
        for col in range(1, n - 1): # 第一行及最后一行
            if board[m - 1][col] == "O":
                bfs(m - 1, col)
            if board[0][col] == "O":
                bfs(0, col)

        for i in range(m):
            for j in range(n):
                board[i][j] = 'O' if board[i][j] == 'Q' else 'X'
  1. 538 . 把二叉搜索树转换为累加树
  • 思路: dfs
class Solution:
    def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        def dfs(root, sumBST):
            if not root:
                return 0
            if root.right:
                sumBST = dfs(root.right, sumBST)
            root.val += sumBST
            if root.left:
                sumBST = dfs(root.left, root.val)
                return sumBST
            else:
                return root.val
        dfs(root, 0)
        return root

参考资料

1.https://www.jianshu.com/p/ff4b93b088eb(数据结构(二):二叉搜索树(Binary Search Tree))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值