刷题总结

数据结构

一、Stack

1.1 Java API

Deque stack = new ArrayDeque<>();  // 或者是LinkedList<>()
// @deprecated
Stack stack = new Stack();
stack.push();
stack.pop();

1.2 经典例题分析

735. 行星碰撞

给定一个整数数组 asteroids,表示在同一行的行星。

对于数组中的每一个元素,其绝对值表示行星的大小,正负表示行星的移动方向(正表示向右移动,负表示向左移动)。每一颗行星以相同的速度移动。

找出碰撞后剩下的所有行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。

示例 1:
输入:asteroids = [5,10,-5]
输出:[5,10]
解释:10 和 -5 碰撞后只剩下 10 。 5 和 10 永远不会发生碰撞。
    
示例 2:
输入:asteroids = [8,-8]
输出:[]
解释:8 和 -8 碰撞后,两者都发生爆炸。
    
示例 3:
输入:asteroids = [10,2,-5]
输出:[10]
解释:2 和 -5 发生碰撞后剩下 -5 。10 和 -5 发生碰撞后剩下 10 。
    
示例 4:
输入:asteroids = [-2,-1,1,2]
输出:[-2,-1,1,2]
解释:-2 和 -1 向左移动,而 1 和 2 向右移动。 由于移动方向相同的行星不会发生碰撞,所以最终没有行星发生碰撞。 

🔽 通过题目分析,可以遍历asteroids数组,然后用stack数据结构记录结果

  • 入栈情况分析
    • stack.isEmpty()当前栈空
    • stack.peek()是负数,无论当前遍历到的asteroids[i]为正还是为负,都可以入栈
    • asteroids[i]是正数,无论stack.peek()为正还是为负,都可以入栈
  • 碰撞场景分析,此场景建立在!stack.isEmpty()stack.peek() >= 0asteroides[i] <= 0基础上
    • 如果栈顶元素stack.peek()大于abs(asteroides[i]),当前元素被销毁
    • 如果栈顶元素stack.peek()等于abs(asteroides[i]),栈顶弹出并和当前元素抵消
    • 如果栈顶元素stack.peek()小于abs(asteroides[i]),栈顶弹出,并循环执行碰撞分析场景
 

1.3 练习题目

20. 有效的括号

394. 字符串解码

算法

一、二分查找

1.1 算法模板

1.1.1 基础二分

public int basicBinarySearch(int[] nums, int target) {
    int start = 0;
    int end = nums.length - 1;
    while (start <= end) {
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            start = mid + 1;
        } else if (nums[mid] > target) {
            end = mid - 1;
        }
    }
    return -1;
}

1.1.2 下边界

第一个等于target

public int lowerbound(int[] nums, int target) {
    int start = 0;
    int end = nums.length - 1;
    while (start <= end) {
        if (nums[mid] == target) {
            end = mid -1;
        } else if (nums[mid] < target) {
            start = mid + 1;
        } else if (nums[mid] > target) {
            end = mid - 1;
        }
    }
    if (start < nums.length && nums[index] == target) {
        return start;
    }
    return -1;
}

第一个大于target

public int lowerbound(int[] nums, int target) {
    int start = 0;
    int end = nums.length - 1;
    while (start <= end) {
        int mid = start + (end - start) / 2;
        if (nums[mid] == target) {
            start = mid + 1;
        } else if (nums[mid] < target) {
            start = mid + 1;
        } else if (nums[mid] > target) {
            end = mid - 1;
        }
    }
    return start >= nums.length ? -1 : start;
}

第一个大于等于target

public int lowerbound(int[] nums, int target) {
    int start = 0;
    int end = nums.length - 1;
    while (start <= end) {
        int mid = start + (end - start) / 2;
        if (nums[mid] == target) {
            end = mid - 1;
        } else if (nums[mid] < target) {
            start = mid + 1;
        } else if (nums[mid] > target) {
            end = mid - 1;
        }
    }
    return start >= nums.length ? -1 : start;
}

1.1.3 上边界

最后一个等于target

public int upperbound(int[] nums, int target) {
    int start = 0;
    int end = nums.length - 1;
    while (start <= end) {
        int mid = start + (end - start) / 2;
        if (nums[mid] == target) {
            start = mid + 1;
        } else if (nums[mid] < target) {
            start = mid + 1;
        } elset if (nums[mid] > target) {
            end = mid - 1;
        }
    }
    if (end >= 0 && nums[end] == target) {
        return end;
    }
    return -1;
}

最后一个小于target

public int upperbound(int[] nums, int target) {
    int start = 0;
    int end = nums.length - 1;
    while (start <= end) {
        int mid = start + (end - start) / 2;
        if (nums[mid] == target) {
            end = mid - 1;
        } else if (nums[mid] < target) {
            start = mid + 1;
        } else if (nums[mid] > target) {
            end = mid - 1;
        }
    }
    return end < 0 ? -1 : end;
}

最后一个小于等于target

public int upperbound(int[] nums, int target) {
    int start = 0;
    int end = nums.length - 1;
    while (start <= end) {
        int mid = start + (end - start) / 2;
        if (nums[mid] == target) {
            end = mid - 1;
        } else if (nums[mid] < target) {
            start = mid + 1;
        } else if (nums[mid] > target) {
            end = mid - 1;
        }
    }
    return end < 0 ? -1 : end;
}

1.2 练习题目

部分讲解

34. 在排序数组中查找元素的第一个和最后一个位置

240. 搜索二维矩阵Ⅱ

1712. 将数组分成三个子数组的方案数

33. 搜索选择排序数组

278. 第一个错误的版本

4. 寻找两个正序数组的中位数

410. 分割数组的最大值

1552. 两球之间的磁力

1482. 制作 m 束花所需的最少天数

1283. 使结果不超过阈值的最小除数

1292. 元素和小于等于阈值的正方形的最大边长

57. 插入区间

二、单调栈

2.1 算法模板 O(n)

基础题目:输入一个数组 nums =[2, 1, 2, 4, 3],你需要返回数组[4, 2, 4, -1, -1]

解释:find nextGreaterElement,找下一个比当前数字大的数。

// 倒序遍历
private int[] nextGreaterElement(int[] nums) {
    int[] res = new int[nums.length];
    Deque<Integer> stack = new ArrayDeque<>();
    for (int i = nums.length - 1; i >= 0; i--) {
        // step1 维护一个单调栈
        // 由于我们求的是nextGreaterElement,因此当前元素大于等于stack.peek()时,需要出栈
        while (!stack.isEmpty() && nums[i] >= stack.peek()) {
            stack.pop();
        }
        // step2 保存结果
        res[i] = stack.isEmpty() ? -1 : stack.peek();
        // step3 当前元素入栈
        stack.push(nums[i]);
    }
    return res;
}
// 正序遍历
private int[] nextGreaterElement(int[] nums) {
    int[] res = new int[nums.length];
    Arrays.fill(res, -1);
    Deque<Integer> stack = new ArrayDeque<>();
    for (int i = 0; i < nums.length; i++) {
        // step1 维护一个单调栈
        // 由于我们求的是nextGreaterElement,因此当前元素大于等于stack.peek()时,需要出栈
        while (!stack.isEmpty() && nums[i] >= stack.peek()) {
            res[i] = stack.peek();          
            stack.pop();   
        }
        // step3 当前元素入栈
        stack.push(i);
    }
    return res;
}

2.2 练习题目

部分讲解

496. 下一个更大元素Ⅰ

503. 下一个更大元素 II

1019. 链表中的下一个更大节点

739. 每日温度

316. 去除重复字母★★ 经典题解

1081. 不同字符的最小子序列

402. 移掉K位数字

42. 接雨水

84. 柱形图中最大矩形

三、扫描线

3.1 练习题目

部分讲解

252. 会议室

253. 会议室Ⅱ

56. 合并区间

57. 插入区间

1272. 删除区间

1288. 删除被覆盖区间

352. 将数据流变为多个不相交区间

1229. 安排会议议程

986. 区间列表的交集

435. 无重叠区间

759. 员工的空闲时间

218. 天际线问题

四、BFS

BFS问题的本质是:在一幅「图」中找到从起点 start 到终点 target 的最近距离,BFS空间复杂度高,所有的BFS都可以转化为DFS

问题变形:

  • 走迷宫,有的格子是围墙不能走,从起点到终点的最短距离是多少?
  • 两个单词,通过某些替换,把其中一个变成另一个,每次只能替换一个字符,最少要替换几次?
  • 连连看游戏,消除的条件不仅仅是图案相同,还得保证两个方块之间的最短连线不能多于两个拐点

4.1 算法模板

队列queue是BFS的核心数据结构;cur.adj() 泛指cur相邻的节点;visited反正走回头路

public int bfs(Node start, Node target) {
    Deque<Node> queue = new ArrayDeque<>();
    Set<Node> visited = new HashSet<>();
    queue.offer(start);
    visited.add(start);
    int step = 0;
    while (!queue.isEmpty()) {
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            Node cur = queue.poll();
            if (cur.val == target.val) {
                return step;
            }
            for (Node node : cur.adj()) {
                if (!visited.contains(node)) {
                    queue.offer(node);
                    visited.add(node);
                }
            }
        }
        step++;
    }
    return step;
}

4.2 练习题目

111. 二叉树的最小深度

102. 二叉树的层序遍历

752. 打开转盘锁 经典题解

127. 单词接龙

207. 课程表

210. 课程表 II

490. 迷宫

505. 迷宫Ⅱ

五、递归

递归三要素

  • 递归函数的参数和返回值
  • 终止条件
  • 单层递归的逻辑

5.1 返回值为空的递归

5.2.1 练习题目

5.2 返回值不为空的递归

5.2.2 练习题目

21. 合并两个有序链表

2. 两数相加

234. 回文链表

六、回溯

回溯算法是系统地搜索问题的解的纯暴力方法,某个问题的所有可能解的称为问题的解空间,若解空间是有限的,则可将解空间映射成树结构。

6.1 概念讲解

🔽 回溯法是求问题的解,使用的是DFS(深度优先搜索)。

在DFS的过程中发现不是问题的解,那么就开始回溯到上一层或者上一个节点。

DFS是遍历整个搜索空间,而不管是否是问题的解。所以更觉得回溯法是DFS的一种应用,DFS更像是一种工具

任何可以映射成树结构问题的解空间,都可以使用回溯法。很多问题,暴力法没有办法解决,例如

  • 排列问题:N个数⾥⾯按⼀定规则找出k个数的集合
  • 组合问题:N个数⾥⾯按⼀定规则找出k个数的集合
  • 切割问题:⼀个字符串按⼀定规则有⼏种切割⽅式
  • 子集问题:⼀个N个数的集合⾥有多少符合条件的⼦集
  • 棋盘问题:N皇后,解数独等等

⚠️ 回溯法解决的问题都可以抽象为树形结构!集合的大小为树的宽度、递归的深度为树的深度。

6.2 经典例题分析

6.2.1 组合问题

❓ 组合问题:给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。77. 组合

  1. 分析问题:抽象树形结构

    1. 通过分析可知,第一步取1后,下一步需要在2、3、4中再依次取1个数字,当取到2个数字时,收集
    2. 怎么控制上述中,取完1之后,再分别从2、3、4中依次再取数字呢?当取到2个元素时,怎么收集呢?

    image-20211116214204739

  2. 上述分析,感觉可以用递归,下面确定递归函数的返回值和参数

    // 全局变量,也可以作为递归函数的参数
    // 存放符合条件结果的集合
    List<List<Integer>> res = new ArrayList<>();
    // 存放符合条件的结果
    Deque<Integer> path = new ArrayDeque<>();
    

    递归函数的参数肯定需要n、k,因为时从集合n里面取k个数。此外,还需要一个参数startIndex,为什么?

    因为在集合[1,2,3,4]取1之后,下⼀层递归,就要在[2,3,4]中取数了,靠的就是startIndex,如图中红色的部分。

    函数的返回值为void,因此确定递归函数的形式,如下:

    private void dfs(int n, int k, int startIndex);
    
  3. 怎么判断什么时候收集元素呢?当path数组的大小达到k,说明我们找到了一个满足条件的组合,path就是根节点到叶子节点的路径

    image-20211116214603651

    // 终止条件代码
    if (path.size() == k) {
        result.add(path);
        return;
    }
    
  4. 单层搜索逻辑

    // 遍历可能的搜索起点
    for (int i = begin; i <= n; i++) {
        // 向路径变量里添加一个数
        path.offer(i);
        // 下一轮搜索,设置的搜索起点要加1,因为组合数理不允许出现重复的元素
        dfs(n, k, i + 1);
        // 深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
        path.pollLast();
    }
    
  5. 为什么要执行path.pollLast()操作?

    例如在path.offer()执行了取1、取2的操作后,结果为[1,2],搜集完成后,后续的操作需要执行取3,只能path.pollLast() pop出元素2,不然会追加。

private List<List<Integer>> result = new ArrayList<>();
private Deque<Integer> path = new ArrayDeque<>();

public List<List<Integer>> combine(int n, int k) {
    if (k <= 0 || n < k) {
        return res;
    }
    Deque<Integer> path = new ArrayDeque<>();
    dfs(n, k, 1);
    return res;
}

private void dfs(int n, int k, int begin, ) {
    // 递归终止条件是:path 的长度等于 k
    if (path.size() == k) {
        res.add(new ArrayList<>(path));
        return;
    }
    // 遍历可能的搜索起点
    for (int i = begin; i <= n; i++) {
        // 向路径变量里添加一个数
        path.offer(i);
        // 下一轮搜索,设置的搜索起点要加 1,因为组合数理不允许出现重复的元素
        dfs(n, k, i + 1);
        // 深度优先遍历有回头的过程,因此递归之前做了什么,递归之后需要做相同操作的逆向操作
        path.pollLast();
    }
}
  1. 剪枝

    for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){}
    

❓ 排列问题:给定⼀个 没有重复数字的序列,返回其所有可能的全排列。 46. 全排列

6.3 练习题目

七、DFS

7.1 经典例题分析

124. 二叉树中的最大路径和

路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。

该路径 至少包含一个 节点,且不一定经过根节点。路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和 。

image-20211118194236115

🔽 解答此题之前,我们先看另外一道题,从根节点出发,到叶子节点的最大路径和?

private int process(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int left = process(root.left);
    int right = process(root.right);
    return Math.max(left, right) + root.val;
}

上述代码的功能是从根节点叶子节点的最大路径和,但是此题中,不要求从根节点到叶子节点。因此有两点不同

  • 返回给上层节点的信息要保证贡献度(和)最大,如果当前层的贡献度为负,则返回0,代码修改如下:

    • return Math.max(0, Math.max(maxLeft, maxRigt) + root.val);
  • 最大路径和不一定经过根节点,例如示例2中,最大的路径和为15->20->7,此时,每遍历一层都要更新全局的最大和结果,整体代码如下:

    private int globle = Integer.MIN_VALUE;
    private int process(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = process(root.left);
        int right = process(root.right);
        globle = Math.max(globle, left + right + root.val);
        return Math.max(0, Math.max(left, right) + root.val);
    }
    

934. 最短的桥

在给定的二维二进制数组 A 中,存在两座岛。(岛是由四面相连的 1 形成的一个最大组。)

现在,我们可以将 0 变为 1,以使两座岛连接起来,变成一座岛。

返回必须翻转的 0 的最小数目。(可以保证答案至少是 1 。)

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

🔽 解答此题之前,我们先看另外一道题 岛屿数量


给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成

示例 1:
输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1
    
示例 2:
输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

🔽 题目分析:显然要用DFS算法,标记访问过的点

public int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) {
        return 0;
    }
    int nr = grid.length;
    int nc = grid[0].length;
    int count = 0;
    for (int i = 0; i < nr; i++) {
        for (int j = 0; j < nc; j++) {
            if (grid[i][j] == '1') {
                ++count;
                process(grid, i, j, nr, nc);
            }
        }
    }
    return count;
}

private void process(char[][] grid, int i, int j, int nr, int nc) {
    if (i >= nr || i < 0 || j >= nc || j < 0 || grid[i][j] == '0') {
        return;
    }
    // 此处必须标记访问为'0'
    grid[i][j] = '0';
    process(grid, i - 1, j, nr, nc);
    process(grid, i + 1, j, nr, nc);
    process(grid, i, j - 1, nr, nc);
    process(grid, i ,j + 1, nr, nc);
}

🔽 现在我们回到 934. 最短的桥

public static int shortestBridge(Main main, int[][] A) {
    int[][] direction = new int[][]{{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
    Deque<int[]> queue = new ArrayDeque<>();
    int ans = -1;
    boolean[][] visited = new boolean[A.length][A[0].length];
    // flag防止2座岛屿都被标记
    boolean flag = true;
    for (int i = 0; i < A.length && flag; i++) {
        for (int j = 0; j < A[0].length; j++) {
            if (A[i][j] == 1) {
                main.dfs(A, i, j, queue, visited);
                flag = false;
                break;
            }
        }
    }
    while (!queue.isEmpty()) {
        int size = queue.size();
        ans++;
        for (int i = 0; i < size; i++) {
            int[] node = queue.poll();
            // 上下左右 四个方向BFS,按层BFS,每一个点的所有上下左右四个方向的点作为遍历的第二层
            for (int j = 0; j < 4; j++) {
                int nx = node[0] + direction[j][0];
                int ny = node[1] + direction[j][1];
                if (nx < 0 || nx >= A.length || ny < 0 || ny >= A[0].length || visited[nx][ny]) continue;
                // 只要有一层中点遍历访问到了另一座岛屿,返回
                if (A[nx][ny] == 1) return ans;
                // 标记防止重新访问
                visited[nx][ny] = true;
                // 添加下一层节点到队列
                queue.add(new int[]{nx, ny});
            }
        }
    }
    return ans;
}

// dfs标记一座岛屿
private void dfs(int[][] A, int i, int j, Deque queue, boolean[][] visited) {
    if (i < 0 || i >= A.length || j < 0 || j >= A[0].length || visited[i][j] || A[i][j] != 1) return;
    // 标记访问过的点
    visited[i][j] = true;
    // 比较的同时,讲岛屿坐标放到队列中
    queue.add(new int[]{i, j});
    dfs(A, i - 1, j, queue, visited);
    dfs(A, i + 1, j, queue, visited);
    dfs(A, i, j - 1, queue, visited);
    dfs(A, i, j + 1, queue, visited);
}

7.2 练习题目

7.2.1 二叉树

112. 路径总和

113. 路径总和 II

437. 路径总和 III

666. 路径总和 IV

124. 二叉树中的最大路径和

104. 二叉树的最大深度

101. 对称二叉树

200. 岛屿数量

八、滑动窗口

8.1 基本题型

  • Easy,size fixed
  • Midian,size可变,单条件限制
  • Median,size可变,双条件限制(模板)
  • Hard,size fixed 单条件限制

8.2 模板

// 本质仍然是two pointer, 左边是left, 右边是iterator(i)
public int lengthOfLongestSubstringKDistinct(String s, int k) {
    Map<Character, Integer> map = new HashMap<>();
    int left = 0;
    int res =0;
    for (int i = 0; i < s.length(); i++) {
        char cur = s.charAt(i);
        map.put(cur, map.getOrDefault(cur, 0) + 1); // step1: 进 当前遍历的i进入窗口 
        while (map.size() > k) {					// step2:出 当窗口不符合条件时,left持续退出窗口			
            char c = s.charAt(left);
            map.put(c, map.get(c) - 1);
            if (map.get(c) == 0) {
                map.remove(c);
            }
            left++; // 出的时候移动左pointer 
        }
        res = Math.max(res, i - left + 1);          // step2:窗口valid,计算结果
    }
    return res;
}

8.3 经典例题分析

76. 最小覆盖子串

public String minWindow(String s, String t) {
    // key: character value: fluent
    Map<Character, Integer> window = new HashMap<>();
    Map<Character, Integer> need = new HashMap<>();
    for (char c : t.toCharArray()) {
        need.put(c, need.getOrDefault(c, 0) + 1);
    }
    char[] charArrayS = s.toCharArray();
    int start = 0;
    int end = 0;
    // 标记窗口大小是否满足要求
    int distance = 0;
    // 结果截取范围
    int left = 0;
    int minLen = Integer.MAX_VALUE;
    while (end < s.length()) {
        char c = charArrayS[end];
        if (!need.containsKey(c)) {
            end++;
            continue;
        }
        window.put(c, window.getOrDefault(c, 0) + 1);
        if (window.get(c).equals(need.get(c))) {
            ++distance;
        }
        end++;

        // 左边窗口收缩
        while (distance == need.size()) {
            if (end - start < minLen) {
                minLen = end - start;
                left = start;
            }
            char d = charArrayS[start];
            if (!need.containsKey(d)) {
                start++;
                continue;
            }
            start++;
            if (window.get(d).equals(need.get(d))) {
                --distance;
            }
            window.put(d, window.getOrDefault(d, 0) - 1);
        }
    }
    return minLen == Integer.MAX_VALUE ? "" : s.substring(left, left + minLen);
}
posted @ 2021-11-13 23:36  __Helios  阅读(66)  评论(0编辑  收藏  举报