刷题常用算法模板(持续更新)

1、二分查找

需求:在一个有序数组中,快速查询某一个值。时间复杂度O(logN),空间复杂度O(1)。

举个例子:

int[] arr = {1, 2, 2, 2, 4, 5};

int target = 2;

以下二分查找的写法就是返回 >=2 的最左位置的下标。也就是返回 1下标。

切记:在数组中查询不到target时,返回的是无效的下标,上层调用时记得加判断。

新的需求:请返回 >=target 中的最右的下标。就只需调用 >= target + 1的函数,然后返回的下标再-1即可。当然上层调用时,还是需要判断返回的下标值是否合法。

LeetCode练习题

二分查找常见的三种写法,注意区分各自的不同之处。

// 1、闭区间写法。返回 >=target 的最左位置的下标
private static int lowerBound1(int[] arr, int target) {
    int l = 0;
    int r = arr.length - 1;
    while (l <= r) { // 闭区间
        int mid = (l + r) >> 1;
        if (arr[mid] < target) { // [mid + 1, right]
            l = mid + 1;
        } else { // [left, mid - 1]
            r = mid - 1;
        }
    }
    // 循环停止条件:l = r + 1。返回其一即可
    return l;
}
// 2、开区间写法。返回 >=target 的最左位置的下标
private static int lowerBound2(int[] arr, int target) {
    int l = -1;
    int r = arr.length;
    while (l + 1 < r) { // 开区间
        int mid = (l + r) >> 1;
        if (arr[mid] < target) { // (mid, right)
            l = mid;
        } else { // (left, mid)
            r = mid;
        }
    }
    return r; // 循环停止条件:left+1=right,返回其一即可
}
// 3、左闭右开区间写法。返回 >=target 的最左位置的下标
private static int lowerBound3(int[] arr, int target) {
    int l = 0;
    int r = arr.length;
    while (l < r) { // 左闭右开
        int mid = (l + r) >> 1;
        if (arr[mid] < target) { // [mid + 1, right)
            l = mid + 1;
        } else { // [left, mid)
            r = mid;
        }
    }
    // 循环停止条件:l == r。返回其一即可
    return r;
}

2、线段树

需求:为了快速的对数组某一段连续的区间进行增删改查操作。时间复杂度O(logN),空间复杂度O(N)。

关键字:范围更新。

写法并不统一,这里的写法是 5个数组搭配。(有的写法是4个数组,省去update数组。在change数组上使用Integer类型,若某个位置的元素 == null,说明是没有修改的情况,这里就不多赘述)。

切记:为了方便计算,线段树中的tree数组,是用于存储原数组的数据,但这里的tree数组0下标的空间省去不用,从1下标位置开始存储的。并且为了出现一些例外的情况,导致在后续递归调用时,会出现数组越界异常,所以change、lazy、sum、update这四个数组的存储空间要 开辟 (tree.length * 4)倍的长度。

下文代码的查询操作(query)写的是 某个区间的累加和。也可根据题目意思更改query的代码,将sum数组改成其他含义的数组表示,例如如下题目:

LeetCode练习题。这道题就是计算 某个区间的最大高度,将sum数组改写成hight数组即可。

private static class SegmentTree {
    private int[] tree; // 从下标1位置开始填入
    private int[] change; // 存储修改的值
    private int[] lazy; // 懒更新数组
    private int[] sum; // 存储某个范围内的数据总和,根据需求而定。
    private boolean[] update; // 记录相应下标位置是否需要进行更新
    private int length;

    public SegmentTree(int[] arr) {
        this.length = arr.length + 1;
        tree = new int[length];
        for (int i = 1; i < length; i++) { // 将数据填充到tree中
            this.tree[i] = arr[i - 1];
        }
        change = new int[length << 2];
        lazy = new int[length << 2];
        sum = new int[length << 2];
        update = new boolean[length << 2];
    }

    // 对sum数组进行初始化,也就是计算出相应区间的总和
    public void build(int l, int r, int rt) {
        if (l == r) {
            sum[rt] = tree[l];
            return;
        }
        int mid = (r + l) / 2;
        build(l, mid, rt << 1); // 递归左子树
        build(mid + 1, r, rt << 1 | 1); // 递归右子树
        pushUp(rt); // 两边汇总
    }

    /**
         * 在L和R范围内,添加某个数
         * @param L   需要修改数据的范围的左边界
         * @param R   需要修改数据的范围的右边界
         * @param l   当前递归的左边界
         * @param r   当前递归的右边界
         * @param num 添加的值
         * @param rt  lazy数组的下标(树的根节点)
         */
    public void add(int L, int R, int num, int l, int r, int rt) {
        if (L <= l && R >= r) { // 当前递归范围,超出了修改数据的范围,可以懒
            sum[rt] += (r - l + 1) * num; // 总和
            lazy[rt] += num;
            return;
        }
        // 不能懒的情况,取中位数进行递归
        int mid = (r + l) / 2;
        // 先将上次lazy数组留下的数据向下分发之后,再进行调用
        // mid - l + 1是左子树的节点数
        // r - mid 是右子树的节点数
        pushDown(rt, mid - l + 1, r - mid);
        if (L <= mid) { // 递归左子树
            add(L, R, num, l, mid, rt << 1);
        }
        if (R > mid) { // 递归右子树
            add(L, R, num, mid + 1, r, rt << 1 | 1);
        }
        pushUp(rt); // 等左右子树递归完,再做汇总
    }

    /**
         * L、R范围内更新值
         * @param L   待更新范围左边界(固定值)
         * @param R   待更新范围右边界(固定值)
         * @param num 更新值
         * @param l   当前递归的左边界
         * @param r   当前递归的右边界
         * @param rt  change数组的下标(树的根节点)
         */
    public void update(int L, int R, int num, int l, int r, int rt) {
        if (L <= l && R >= r) { // 当前递归范围超过了待更新的范围
            update[rt] = true;
            change[rt] = num;
            sum[rt] = (r - l + 1) * num; // 重新计算sum
            lazy[rt] = 0; // lazy数组对应的位置要归0
            return;
        }
        // 没有懒到,取中位数,往下递归
        int mid = l + ((r - l) >> 1);
        // 先往下分发数据,然后才是递归调用
        pushDown(rt, mid - l + 1, r - mid);
        if (L <= mid) {
            update(L, R, num, l, mid, rt << 1);
        }
        if (R > mid) {
            update(L, R, num, mid + 1, r, rt << 1 | 1);
        }
        pushUp(rt); // 汇总数据
    }

    // 查询L和R范围内的sum总和
    public long query(int L, int R, int l, int r, int rt) {
        if (L <= l && R >= r) {
            return sum[rt];
        }
        int mid = l + ((r - l) >> 1);
        pushDown(rt, mid - l + 1, r - mid); // 往下分发
        long ans = 0;
        if (L <= mid) {
            ans += query(L, R, l, mid, rt << 1);
        }
        if (R > mid) {
            ans += query(L, R, mid + 1, r, rt << 1 | 1);
        }
        return ans;
    }

    // lazy数组向下分发数据
    private void pushDown(int rt, int leftChildSum, int rightChildSum) {
        // 用于add方法
        if (lazy[rt] != 0) {  // 懒数组的数据不为0,说明要往下分发
            sum[rt << 1] += lazy[rt] * leftChildSum; //左子树的总和
            sum[rt << 1 | 1] += lazy[rt] * rightChildSum; // 右子树的总和
            // 更新左右子树的lazy数组
            lazy[rt << 1] += lazy[rt];
            lazy[rt << 1 | 1] += lazy[rt];
            lazy[rt] = 0; // 当然位置的lazy值归0
        }
        // 用于update方法
        if (update[rt]) { // 是否需要更新的情况
            // 标志update数组,表示需要更新
            update[rt << 1] = true;
            update[rt << 1 | 1] = true;
            // 更新左右子树的change值
            change[rt << 1] = change[rt];
            change[rt << 1 | 1] = change[rt];
            // 更新左右子树的sum总和
            sum[rt << 1] = change[rt] * leftChildSum;
            sum[rt << 1 | 1] = change[rt] * rightChildSum;
            // 左右子树的lazy数组都需要归0
            lazy[rt << 1] = 0;
            lazy[rt << 1 | 1] = 0;
            update[rt] = false; // 当前位置的数据分发完了,就改回false
        }
    }

    // 汇总数据
    private void pushUp(int rt) {
        sum[rt] = sum[rt << 1] + sum[rt << 1 | 1]; // 将左右子树的数据进行汇总
    }
}

3、树状数组

需求:会频繁的更新数组中某一个位置的数据,但又需要快速的计算某个区间的累加和问题。时间复杂度O(logN),空间复杂度O(N)。

树状数组,也称为IndexTree,算是线段树的另一种形式。也是实现数组区间内的快速增删改查。与线段树的区别是 能够实现单点更新,比线段树更轻量化。还有一个好处就是,可以很轻易的改写成二维的形式。

关键词:单点更新,快速计算某一个段区间的累加和。

IndexTree有三个函数,add、update、query。

query查询的是 0 ~ index位置的累加和。

比如要查询 3 ~ 5位置的累加和问题,就能转换为 求 0 ~ 5的累加和 减去 0~2的累计和

// 一维。上层调用时的下标,还是从0开始。只是进入IndexTree后,自己手动+1
public class IndexTree {
    public int[] nums; // 原数组
    public int[] tree; // 累加和数组
    public int length; // 0下标的空间省去不用

    public IndexTree(int N) {
        this.length = N + 1;
        tree = new int[this.length];
        nums = new int[this.length];
    }

    /**
     * 在index位置插入val值。index从1开始
     * @param val 待插入的值
     * @param index 数组下标
     */
    public void add(int val, int index) {
        index += 1;
        nums[index] += val;
        for (int i = index; i < length; i += (i & -i)) { // index位置插入值,会影响后面位置的计算
            tree[i] += val;
        }
    }

    /**
     *  更新index位置的值
     * @param val 更新的值
     * @param index 下标
     */
    public void update(int val, int index) {
        index += 1;
        int num = val - nums[index]; // 差值
        nums[index] = val;
        for (int i = index; i < length; i += (i & -i)) {
            tree[i] += num; // 累加上 差值
        }
    }

    /**
     *  返回1下标~index下标的累加和
     * @param index 下标
     * @return 返回累加和
     */
    public int query(int index) {
        index += 1;
        int ans = 0;
        for (int i = index; i > 0; i -= (i & -i)) {
            ans += tree[i];
        }
        return ans;
    }
}
// 二维。上层调用时,还是从下标0开始,进入IndexTree后,下标自动+1
public class Code02_IndexTree2D {
    private int[][] nums;
    private int[][] tree;
    private int N; // 行数
    private int M; // 列数

    public Code02_IndexTree2D(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
            return;
        }
        N = matrix.length + 1;
        M = matrix[0].length + 1;
        nums = new int[N][M];
        tree = new int[N][M];
        for (int i = 0; i < N - 1; i++) {
            for (int j = 0; j < M - 1; j++) {
                update(matrix[i][j], i, j);
            }
        }
    }

    // row,col位置 更新值 val。 row,col的范围在 0~N-1,或者0~M-1
    public void update(int val, int row, int col) {
        if (N == 0 || M == 0 || row < 0 || col < 0 || row > N - 1 || col > M - 1) {
            return;
        }
        row += 1;
        col += 1;
        int num = val - nums[row][col]; // 差值
        nums[row][col] = val;
        for (int i = row; i < N; i++) {
            for (int j = col; j < M; j++) {
                tree[i][j] += num;
            }
        }
    }

    // 返回 row,col 到左上角的矩形的累加和
    private int sum(int row, int col) {
        if (row < 0 || col < 0 || row > N - 1 || col > M - 1) {
            return 0;
        }
        row += 1;
        col += 1;
        int ans = 0;
        for (int i = row; i > 0; i -= (i & -i)) {
            for (int j = col; j > 0; j -= (j & -j)) {
                ans += tree[i][j];
            }
        }
        return ans;
    }

    /**
     * @param row1 左上角
     * @param col1 左上角
     * @param row2 右下角
     * @param col2 右下角
     * @return 返回左上角 到 右下角 围成的矩形的累加和
     */
    public int sumRegion(int row1, int col1, int row2, int col2) {
        if (N == 0 || M == 0) {
            return 0;
        }
        return sum(row2, col2) - sum(row2, col1 - 1) - sum(row1 - 1, col2) + sum(row1 - 1, col1 - 1);
    }
}

4、差分数组

需求快速对数组的某一段连续区间进行加减法操作。时间复杂度O(N),空间复杂度O(N)。

/**
     * 差分数组
     * @param arr    原数组
     * @param option 操作数组,有3个参数。
     */
private static void fastUpdateOfArray(int[] arr, int[][] option) {
    /*
            option数组有三个参数:
            option[i][0] = 带更新范围的左边界
            option[i][1] = 带更新范围的右边界
            option[i][2] = 新值
     */
    int N = arr.length;
    // 1、由原数组 反推 差分数组
    int[] diff = new int[N + 1]; // 多开一个位置的空间
    diff[0] = arr[0];
    for (int i = 1; i < N; i++) {
        diff[i] = arr[i] - arr[i - 1];
    }
    // 2、将新值 更新到差分数组
    for (int[] pos : option) {
        int left = pos[0]; // 左边界
        int right = pos[1]; // 右边界
        int val = pos[2]; // 新值
        diff[left] += val; // 左边界 + val
        diff[right + 1] -= val; // 右边界的后一个位置 -val
    }
    // 3、再对diff数组求前缀和数组,就是更新过后的arr数组的值
    arr[0] = diff[0];
    for (int i = 1; i < N; i++) {
        arr[i] = diff[i] + arr[i - 1];
    }
}

5、前缀树

需求:给定一组字符串,将这些字符串插入前缀树中,后续可以查询某个子串,在前缀数中有多少个字符串是以这个子串开头的。

应用:后续的AC自动机,就是前缀树 + KMP写的。

public class TrieTree {
    private final TrieNode root;

    public TrieTree() {
        // 根节点不指向任何字符,root节点的pass值就是整颗前缀数有多少字符串
        root = new TrieNode(); 
    }

    private static class TrieNode {
        public int pass; //途径的数量
        public int end; // 某个单词的总数量
        // 这里的HashMap中的键值,也可以是其他的。这里只是以大小写字母的情况写的
        public HashMap<Character, TrieNode> map; //保存下一节点的地址

        public TrieNode() {
            map = new HashMap<>();
        }
    }

    public void add(String word) {
        if (word == null) {
            return;
        }
        char[] array = word.toCharArray();
        TrieNode node = root;
        node.pass++;
        for (char ch : array) {
            if (!node.map.containsKey(ch)) {
                node.map.put(ch, new TrieNode());
            }
            node = node.map.get(ch);
            node.pass++;
        }
        node.end++;
    }

    public int search(String word) {
        if (word == null) {
            return 0;
        }

        char[] array = word.toCharArray();
        TrieNode node = root;
        for (char ch : array) {
            if (!node.map.containsKey(ch)) {
                return 0;
            }
            node = node.map.get(ch); //拿到下一节点
        }
        return node.end; //返回最终的end值
    }

    /**
     * @param word 以word为前缀的字符串
     * @return 返回以word为前缀的字符串的数量
     */
    public int prefixNumber(String word) {
        if (word == null) {
            return 0;
        }

        char[] array = word.toCharArray();
        TrieNode node = root;
        for (char ch : array) {
            if (!node.map.containsKey(ch)) {
                return 0;
            }
            node = node.map.get(ch); //拿到下一节点
        }
        return node.pass;
    }

    public boolean delete(String word) {
        if (word != null && search(word) != 0) {
            char[] array = word.toCharArray();
            TrieNode node = root;
            node.pass--;
            for (char ch : array) {
                if (--node.map.get(ch).pass == 0) {
                    //pass值为0,所以从该节点一下的所有子树,都将不存在,所以直接全部回收即可
                    //C++ 的,需要遍历所有子树,调用析构函数
                    node.map.remove(ch);
                    return true;
                }
                node = node.map.get(ch);
            }
            node.end--;
            return true;
        }
        return false;
    }
}

6、并查集

需求快速的判断某两个节点是否属于同一集合。时间复杂度O(1),空间复杂度O(N)。

并查集的写法有很多种,理解其思想,方可改写。还有的是使用数组来写的并查集,比如使用Integer[] 数组,若 某个位置的元素 == null,说明这个位置的元素还没有进来过。

1、初始化时,每个节点的父节点都是指向自己本身的

2、find时,要进行路径压缩。这也是时间复杂度O(1)的来源

3、sizeMap,是在union时,让“小的集合 挂在 大的集合下面”,有一定的优化效果。但大多数OJ时,这个可以不用写,一般都是能过的。

// 包装Node节点的 + sizeMap优化版本。
public class UnionSet {
    private HashMap<Node, Node> fatherMap; //key表示当前这个数据,value表示这个数据的代表(父亲)是谁
    private HashMap<Node, Integer> sizeMap; //表示当前这个组(集合)的大小

    public UnionSet() { //构造方法
        fatherMap = new HashMap<>();
        sizeMap = new HashMap<>();
    }

    private static class Node {
        public int val;
        public Node next;
        public Node(int val) {
            this.val = val;
        }
    }

    //初始化并查集
    public void makeSet(List<Node> list) {
        if (list == null) {
            return;
        }
        fatherMap.clear();
        sizeMap.clear(); //先将表清空

        //遍历list,把每一个节点,都放入哈希表中
        for (Node node : list) {
            fatherMap.put(node, node); //第一个参数是节点本身,第二个参数就是这个组的代表
            sizeMap.put(node, 1); //第一个参数是这个组的代表,第二个参数是大小
        }
    }

    //判断是不是同一个组
    public boolean isSameSet(Node node1, Node node2) {
        if (node1 == null || node2 == null) {
            return false;
        }
        return findFather(node1) == findFather(node2); //查找各自的代表节点,看是不是同一个。
    }

    //查找代表节点,并做路径压缩
    private Node findFather(Node node) {
        if (node == null) {
            return null;
        }
        //查找代表节点
        Stack<Node> path = new Stack<>(); //存储沿途的节点
        while (node != fatherMap.get(node)) { //代表节点不是自己本身,就继续查找
            path.push(node);
            node = fatherMap.get(node);
        }
        //路径压缩
        while (!path.isEmpty()) {
            Node tmp = path.pop();
            fatherMap.put(tmp, node); //此时的node,就是这个组的代表节点
        }
        return node;
    }

    //合并操作
    public void union(Node node1, Node node2) {
        if (node1 == null || node2 == null) {
            return;
        }
        int node1Size = sizeMap.get(node1);
        int node2Size = sizeMap.get(node2); //分别得到两个节点所在组的大小
        Node node1Father = fatherMap.get(node1);
        Node node2Father = fatherMap.get(node2); //分别拿到两个节点的代表节点
        if (node1Father != node2Father) { //两个节点,不在同一个组,就合并
            if (node1Size < node2Size) { //node1 挂在 node2
                fatherMap.put(node1Father, node2Father);
                sizeMap.put(node2Father, node1Size + node2Size); //新的组,大小是原来两个组的和
                sizeMap.remove(node1Father); //小组的数据,就不需要了,删除
            } else { //node2 挂在 node1
                //跟上面操作类似
                fatherMap.put(node2Father, node1Father);
                sizeMap.put(node1Father, node1Size + node2Size);
                sizeMap.remove(node1Father);
            }
        }
    }
}
// 稍微简单一点的并查集写法。初始化操作就写在find函数里
// 值得注意的是,这里并没有包装Node节点,只是单纯的使用Integer
// 有的题目,有可能出现两个相同的数字,导致并查集里的索引出现错乱的情况
private class UnionSet {
    private HashMap<Integer, Integer> father; // <index, fatherIndex>

    public UnionSet() {
        father = new HashMap<>();
    }

    // find要做三件事:
    // 查找父亲节点、初始化第一次进来的节点、路径压缩
    public int find(int index) {
        Integer fa = father.get(index);
        if(fa == null) { // 表示index是第一次进来,然后就初始化
            father.put(index, index); // 初始化,父亲节点就是自己
            return index;
        }
        if(fa == index) { // 如果查找出来的父亲节点就是自己,所以到头了,直接返回
            return fa;
        }
        // 还没走到最根部的父亲节点,递归继续
        fa = find(fa);
        // 路径压缩
        father.put(index, fa);
        return fa;
    }

    // 合并
    public void union(int index1, int index2) {
        int fa1 = find(index1);
        int fa2 = find(index2);
        if (fa1 != fa2) {
            // 在左神的讲解中,有个“小挂大”的优化,这里就没有优化
            // 直接这样写,也是能过的,只是可能常数项时间有点高
            father.put(fa1, fa2);
        }
    }
}

7、AC自动机

需求:给定一篇文章,和一组敏感词汇,问 这一篇文章中有哪些敏感词汇。

思路:先对这些敏感词汇建立一颗前缀树,然后在前缀树上写KMP。

核心要点:fail指针,其实就是对于到KMP中的那个next数组,将那边的思想搬运过来,如下的build()函数,可能就更好理解了。根节点的fail=null,根节点的下一级子节点的fail指向根节点,这一句话就对于了KMP中的next数组的初始化状态:next[0] = -1, next[1] = 0。都是为了在匹配失败的时候,往前跳转。

public class Code03_AC1 {
    public static void main(String[] args) {
        ACAutomation ac = new ACAutomation();
        ac.insert("dhe");
        ac.insert("he");
        ac.insert("abcdheks");
        // 设置fail指针
        ac.build();

        List<String> contains = ac.containWords("abcdhekskdjfafhasldkflskdjhwqaeruv");
        for (String word : contains) {
            System.out.println(word);
        }
    }

    // 前缀树节点
    private static class Node {
        public String end; // 以当前节点结尾,这条线路的字符串
        public boolean endUse; // 标记是否已经找到过这个敏感词
        public Node fail; // fail指针,匹配失败时,往上找最佳的前缀字符串的开始节点
        public Node[] nexts; // 下级节点,可以是数组,也可以是哈希表的形式,根据数据类型来定

        public Node() {
            this.end = null;
            this.endUse = false;
            this.fail = null;
            this.nexts = new Node[26]; // 假设是26个小写字母
        }
    }

    private static class ACAutomation {
        private Node root;

        public ACAutomation() {
            this.root = new Node();
        }

        public void insert(String str) {
            char[] chars = str.toCharArray();
            Node cur = root;
            for (int i = 0; i < chars.length; i++) {
                int num = chars[i] - 'a';
                if (cur.nexts[num] == null) {
                    cur.nexts[num] = new Node();
                }
                cur = cur.nexts[num];
            }
            cur.end = str; // 尾结点,记录这条线路的字符串
        }

        // 连接所有节点的fail指针 ----使用BFS
        // 根节点的fail是null,根节点的直接下级节点的fail都是 指向 根节点
        // fail=null和fail指向根节点,也就直接对应了KMP中next数组的前两个位置就是-1、0的情况
        public void build() {
            Queue<Node> queue = new LinkedList<>();
            queue.add(root);
            while (!queue.isEmpty()) { // BFS
                Node cur = queue.poll();
                // 遍历nexts数组
                for (int i = 0; i < 26; i++) { // 处理他的孩子节点
                    if (cur.nexts[i] != null) { // 有孩子节点的情况
                        cur.nexts[i].fail = root; // 先指向root。后续如果有其他情况,再修改
                        Node curFail = cur.fail;
                        while (curFail != null) {
                            if (curFail.nexts[i] != null) { // 父节点的fail指向的节点 也有走向i位置的路,就连接
                                cur.nexts[i].fail = curFail.nexts[i];
                                break; // 连上之后,直接跳出了
                            }
                            curFail = curFail.fail; // 再往下一个fail节点跳转
                        }
                        queue.add(cur.nexts[i]); // 当前节点入队列
                    }
                }
            }
        }

        // 查询content文章中的敏感词
        public List<String> containWords(String content) {
            if (content == null || content.length() == 0) {
                return new ArrayList<>();
            }
            Node cur = root;
            int length = content.length();
            List<String> ans = new ArrayList<>();
            for (int i = 0; i < length; i++) {
                int index = content.charAt(i) - 'a';
                // 没有走向index的路,但是cur又不是根节点的情况,继续沿着fail走
                while (cur.nexts[index] == null && cur != root) {
                    cur = cur.fail;
                }
                cur = cur.nexts[index] != null? cur.nexts[index] : root;
                // 现在cur要么是走到了下级节点,要么就还是在root位置
                // 以当前cur节点跑一遍fail指针,尝试搜集沿途的敏感词
                Node follow = cur;
                while (follow != root) {
                    if (follow.endUse) { // 说明当前节点已经搜集过敏感词了,无需再次搜集
                        break;
                    }
                    // 不同的需求,可修改一下代码
                    if(follow.end != null) {
                        ans.add(follow.end);
                        follow.endUse = true;
                    }
                    follow = follow.fail;
                }
            }
            return ans;
        }
    }
}

8、Morris遍历

需求以时间复杂度O(N), 空间复杂度O(1) 的要求,遍历二叉树。

核心要点:将某一颗左子树中,最右侧的节点的right指针,指向根节点。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LL3KOOzi-1685503429270)(image/image-20230530235620336.png)]

//可以在Morris的基础之上,改写前序、中序、后序遍历
// 前序是第一来到的节点就打印
// 中序是第二次来到的节点就打印
// 后序是在第二次来到mostRight时,往上逆序打印。需要反转right指针的走向,才能做到空间O(1)
private static class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(int val) {
        this.val = val;
    }
}

private static void morris(TreeNode node) {
    if (node == null) {
        return;
    }

    TreeNode cur = node;
    while(cur != null) {
        TreeNode mostRight = cur.left; // 左子树
        if(mostRight != null) { // 左子树不为空
            // 尽可能的往右子树走
            while(mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            // 循环停下来,就是走到了最右侧的节点
            if (mostRight.right == null) {
                // 第一次来到这个节点,right指针连上cur
                // 1、做你想做的操作,比如打印节点

                // 2、继续往左子树走
                cur = cur.left;
                continue;
            } else {
                // 第二次来到这个节点,断开right指针连 cur
                mostRight.right = null;
            }
        } else { // 左子树为空

        }
        cur = cur.right;
    }
}

9、二叉树非递归遍历

两种实现方式:1、Morris遍历;2、使用栈模拟

// TreeNode节点
private static class TreeNode {
    public int val;
    public TreeNode left;
    public TreeNode right;

    public TreeNode(int val) {
        this.val = val;
    }
}
// 1、Morris 前序遍历
// 前序遍历二叉树
public static void morrisPreOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    // 找到当前节点的左子树上 最右的节点,并将该节点的右指针指向当前cur节点
    TreeNode mostRight = null;
    TreeNode cur = root;
    while (cur != null) { // 只要cur没有遍历完,循环就继续
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) { // 往最右节点靠拢
                mostRight = mostRight.right;
            }
            // 停下来时,有两种情况
            // 1是右指针为null,说明是第一次遍历到当前节点
            // 2是右指针指向cur,说明是第二次遍历到当前节点
            if (mostRight.right == null) {
                mostRight.right = cur; // 指向cur节点
                System.out.print(cur.val + " ");
                cur = cur.left;
                continue; // 继续往左子树走
            } else {
                mostRight.right = null;
            }
        } else { // 往右子树走之前,先打印当前cur的值
            System.out.print(cur.val + " ");
        }
        cur = cur.right; // 转向右子树
    }
    System.out.println();
}
// 1、Morris 中序遍历
// 中序遍历二叉树
public static void morrisInOrder(TreeNode root) {
    if (root == null) {
        return;
    }

    TreeNode cur = root;
    TreeNode mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            // 往最右节点靠拢
            while(mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) { // 第1次来到cur节点
                mostRight.right = cur;
                cur = cur.left; // 继续往左子树走
                continue;
            } else { // 第2次来到cur节点
                mostRight.right = null;
                System.out.print(cur.val + " ");
            }
        } else {
            System.out.print(cur.val + " ");
        }
        cur = cur.right; // 往右子树转
    }
    System.out.println();
}
// 1、Morris 后序遍历
// 后序遍历二叉树
public static void morrisPostOrder(TreeNode root) {
    if (root == null) {
        return;
    }
    TreeNode cur = root;
    TreeNode mostRight = null;
    while (cur != null) {
        mostRight = cur.left;
        if (mostRight != null) {
            while (mostRight.right != null && mostRight.right != cur) {
                mostRight = mostRight.right;
            }
            if (mostRight.right == null) {
                mostRight.right = cur;
                cur = cur.left;
                continue;
            } else { // 第2次来到cur节点,此时就打印cur.left节点,最靠右这一列的节点
                mostRight.right = null;
                printList(cur.left);
            }
        }
        cur = cur.right; // 往右子树转
    }
    printList(root); // 最后打印根节点最靠右的一列
    System.out.println();
}

private static void printList(TreeNode left) {
    // 首先反转最靠右的一列,从下面往上打印
    TreeNode node = reverseList(left);
    TreeNode cur = node;
    while (cur != null) {
        System.out.print(cur.val + " ");
        cur = cur.right;
    }
    reverseList(node); // 再反转回来
}

// 反转TreeNode最右这一列节点
public static TreeNode reverseList(TreeNode node) {
    TreeNode pre = null;
    TreeNode next = null;
    while (node != null) {
        next = node.right;
        node.right = pre;
        pre = node;
        node = next;
    }
    return pre;
}
// 2、用栈模拟
// 非递归前序遍历
public static void preOrderNoRecursion(TreeNode root) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while (!stack.isEmpty()) {
        root = stack.pop();
        System.out.print(root.val + " ");
        if (root.right != null) {
            stack.push(root.right);
        }
        if (root.left != null) {
            stack.push(root.left);
        }
    }
    System.out.println();
}

// 非递归中序遍历
public static void inOrderNoRecursion(TreeNode root) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    while (!stack.isEmpty() || root != null) {
        if (root != null) {
            stack.push(root);
            root = root.left;
        } else { // 此时root = null.就打印当前栈顶元素
            root = stack.pop();
            System.out.print(root.val + " ");
            root = root.right; // 转向右子树
        }
    }
    System.out.println();
}

// 非递归后序遍历1
public static void postOrderNoRecursion(TreeNode root) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    Stack<TreeNode> helpStack = new Stack<>(); // 将遍历的结果存储在栈中,最后打印
    stack.push(root);
    while (!stack.isEmpty()) {
        root = stack.pop();
        helpStack.push(root);
        if (root.left != null) {
            stack.push(root.left);
        }
        if (root.right != null) {
            stack.push(root.right);
        }
    }
    // 打印helpStack中的数据
    while (!helpStack.isEmpty()) {
        root = helpStack.pop();
        System.out.print(root.val + " ");
    }
    System.out.println();
}

// 非递归后序遍历2,省一个辅助栈的空间
public static void postOrderNoRecursion2(TreeNode root) {
    if (root == null) {
        return;
    }
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    TreeNode pre = root; // 上一次打印的节点
    while (!stack.isEmpty()) {
        root = stack.peek();
        if (root.left != null && root.left != pre && root.right != pre) {
            stack.push(root.left);
        } else if (root.right != null && root.right != pre) {
            stack.push(root.right);
        } else {
            pre = root;
            stack.pop(); // 弹出栈顶元素
            System.out.print(root.val + " ");
        }
    }
    System.out.println();
}

10、KMP

需求给定两个字符串,s1和s2,请问在s1中是否包含子串s2?

这算是大学课程里面,数据结构书上比较难的一个算法了。主要思想还是暴力解,在暴力解的基础之上,引入了next数组的。

// s2模式串,s1是主串,在s1里面找s2
public static int indexOf(String s1, String s2) {
    if (s1 == null || s2 == null || s2.length() == 0) {
        return -1;
    }
    char[] ch1 = s1.toCharArray();
    char[] ch2 = s2.toCharArray();
    int[] next = getNextArray(ch2);
    int index1 = 0; // 指向ch1
    int index2 = 0; // 指向 ch2
    while (index1 < ch1.length && index2 < ch2.length) {
        if (ch1[index1] == ch2[index2]) {
            index1++;
            index2++;
        } else if (index2 > 0) { // index2还能往前跳转的时候
            index2 = next[index2]; // index2往前跳
        } else { // 当前位置,既不相等,index2也不能往前跳了,说明index1位置出发行不通,index1后移
            index1++;
        }
    }
    return index2 == ch2.length ? index1 - index2 : -1;
}

private static int[] getNextArray(char[] s2) {
    if (s2.length == 1) {
        return new int[]{-1};
    }
    // 找的其实就是前后缀字符串
    int[] res = new int[s2.length];
    res[0] = -1;
    res[1] = 0;
    int i = 2; // 从第3个字符开始判断
    int cn = 0;
    while (i < s2.length) {
        if (s2[cn] == s2[i - 1]) { // 切记这里其实是i的前一个位置
            res[i++] = ++cn;
        } else if (cn > 0) {
            cn = res[cn];
        } else {
            res[i++] = 0;
        }
    }
    return res;
}

11、Manacher

需求在一个字符串中,问其中的最长回文子串的长度。也就是最长回文子串问题。时间复杂度O(N), 空间复杂度O(1)

暴力解,遍历每一个字符,在每一个字符的时候,往左右两边进行扩展,暴力解时间复杂度O(N^2)。

manacher也是在暴力解的基础之上,进行优化,引入了 回文半径数组

public static int manacher(String str) {
    if (str == null || str.length() == 0) {
        return 0;
    }
    // 加工字符串,避免偶数长度的字符串遗漏一些情况
    char[] chars = processStr(str); 
    int N = chars.length;
    int R = -1; // 右边界,左闭右开区间
    int C = -1; // 中心点
    int max = 0;
    int[] pArr = new int[N]; // 回文半径数组
    for (int i = 0; i < N; i++) {
        // 首先根据对称,拿到C点左侧相应的回文半径
        pArr[i] = i < R? Math.min(pArr[2 * C - i], R - i) : 1;
        // 根据已经计算出来的初始半径,此时再向两边扩展
        while (i - pArr[i] >= 0 && i + pArr[i] < N) {
            if (chars[i - pArr[i]] == chars[i + pArr[i]]) {
                pArr[i]++; // 回文半径增加
            } else { // 不相等,直接跳出循环
                break;
            }
        }
        // 更新R,C和max
        if (i + pArr[i] > R) {
            R = i + pArr[i]; // 右边界
            C = i; // 以当前点作为新的中心点
        }
        max = Math.max(max, pArr[i]);
    }
    return max - 1;
}

// 每个字符之间添加#,用于间隔
private static char[] processStr(String str) {
    int N = str.length();
    char[] res = new char[2 * N + 1];
    for (int i = 0; i < res.length; i += 2) {
        res[i] = '#';
    }
    int index = 0;
    for (int i = 1; i < res.length; i+= 2) {
        res[i] = str.charAt(index++);
    }
    return res;
}

12、快速选择 bfprt

需求:在一个无序数组中,返回 第 K 小的数字。时间复杂度O(N),空间复杂度O(1)。

// 0、排序之后,再找第k小的数,时间复杂度O(N*logN),这样写,面试直接挂。
// 1、常规解法,就是TopK问题,使用一个大根堆,将所有数过一遍大根堆就行,这里就不追溯了。时间O(N * logK),空间O(K)

// 2、归并排序中的merge函数,(荷兰国旗问题优化)。
// 因为这里的pivot是随机选取的,在数学证明上,时间复杂度是收敛于O(N)的
public static int fatsSelect(int[] nums, int k) {
    fastSelect(nums, 0, nums.length - 1, k - 1);
    return nums[k - 1];
}

private static void fastSelect(int[] nums, int l, int r, int k) {
    if (l >= r) {
        return;
    }
    int index = l + (int) (Math.random() * (r - l)) + 1; // 随机值
    int pivot = nums[index];
    int[] mid = partition(nums, pivot);
    if (mid[0] <= k && k <= mid[1]) {
        return;
    } else if (k < mid[0]) { // 往左侧走
        fastSelect(nums, l, mid[0] - 1, k);
    } else { // 往右侧走
        fastSelect(nums, mid[1] + 1, r, k);
    }
}
// 荷兰国旗问题优化
private static int[] partition(int[] nums, int l, int r, int pivot) {
    int less = l - 1; // 小于区
    int more = r + 1; // 大于区
    int index = l;
    while (less < more) {
        if (nums[index] == pivot) {
            index++;
        } else if (nums[index] < pivot) {
            swap(nums, index++, ++less); // 两数交换
        } else {
            swap(nums, index, --more);
        }
    }
    return new int[]{less + 1, more - 1};
}
// 2、bfprt算法,时间复杂度严格控制在O(N)
// 严格解析,请看https://blog.csdn.net/x0919/article/details/122246065
public static int bfprt(int[] arr, int l, int r, int k) {
    if (l == r) {
        return arr[l];
    }
    int pivot = medianOfMedians(arr, l, r);
    // 根据基准值进行荷兰国旗问题优化
    int[] mid = netherlands(arr, l, r, pivot);
    if (mid[0] <= k && k <= mid[1]) {
        return arr[k];
    } else if (mid[0] > k) {
        return bfprt(arr, l, mid[0] - 1, k);
    } else {
        return bfprt(arr, mid[1] + 1, r, k);
    }
}


private static int[] netherlands(int[] arr, int l, int r, int pivot) {
    int less = l - 1;
    int more = r + 1;
    int index = l;
    while (index < more) {
        if (arr[index] < pivot) {
            swap(arr, index++, ++less);
        } else if (arr[index] > pivot) {
            swap(arr, index, --more);
        } else {
            index++;
        }
    }
    return new int[]{less + 1, more - 1};
}

private static void swap(int[] arr, int l, int r) {
    int tmp = arr[l];
    arr[l] = arr[r];
    arr[r] = tmp;
}

// 获取基准值
private static int medianOfMedians(int[] arr, int l, int r) {
    // 5个数 5个数一组,取5个数的中间值
    int len = r - l + 1;
    int size = len / 5;
    int offset = len % 5 == 0 ? 0 : 1;
    int[] tmp = new int[size + offset];
    for (int i = 0; i < tmp.length; i++) {
        int left = l + 5 * i; // 5个数的左边界
        int right = Math.min(r, l + 4); // 5个数的右边界
        tmp[i] = sortAndGetMidNum(arr, left, right);
    }
    // 再返回tmp数组中的中间值
    return bfprt(tmp, 0, tmp.length - 1, tmp.length / 2);
}

// 排序这5个数,并返回中间值
private static int sortAndGetMidNum(int[] arr, int left, int right) {
    // 直接插入排序
    for (int i = left; i < right; i++) {
        int cur = arr[i];
        int j = i - 1;
        for (; j >= left; j--) {
            if (arr[j] > arr[j + 1]) {
                arr[j + 1] = arr[j];
            } else {
                break;
            }
        }
        arr[j + 1] = cur;
    }
    return arr[left + (right - left) / 2]; // 返回中间值
}

13、滑动窗口

需求常常用于子数组求解之类问题。

维持L、R边界,一般来说都是左闭右开区间的。

/**
 * Created by Terry
 * User: Administrator
 * Date: 2022-06-26
 * Time: 15:40
 * Description: 窗口最大值。
 * 假设一个固定大小为W的窗口,依次划过arr,
 * 返回每一次滑出状况的最大值
 * 例如,arr = [4,3,5,4,3,3,6,7], W = 3
 * 返回:[5,5,5,4,6,7]
 */
public class Code01_WindowMaxNumber {
    public static void main(String[] args) {
        int[] arr = {4, 3, 5, 4, 3, 3, 6, 7};
        int w = 3;
        System.out.println(Arrays.toString(windowMaxNumber(arr, w)));
    }

    public static int[] windowMaxNumber(int[] arr, int w) {
        if (arr == null || arr.length == 0 || arr.length < w) {
            return new int[]{};
        }
        int N = arr.length;
        int[] ans = new int[N - w + 1];
        int index = 0;
        LinkedList<Integer> queue = new LinkedList<>(); // 双端队列
        for (int i = 0; i < arr.length; i++) {
            while (!queue.isEmpty() && arr[queue.peekLast()] <= arr[i]) { // 维持头部大,尾部小的结构
                queue.pollLast();
            }
            queue.addLast(i);
            if (i - queue.peekFirst() == w) {
                queue.pollFirst();
            }
            if (i >= w - 1) {
                ans[index++] = arr[queue.peekFirst()];
            }
        }
        return ans;
    }
}

14、加强堆

需求:系统提供的堆,在压入元素进去之后,若此时需要修改堆中某个元素的数据,然后修改之后,堆的结构应该发生改变,系统提供的堆不能实现这个事。需要自己改堆结构。

举个例子:现在有一个大根堆 heap,类型为Node节点,比较方式是 Node里的val值。

假设现在修改堆中某一个节点node的val值,修改之后,需要手动调用 函数(向堆顶走、向堆下面走)两种情况,才能维持堆的结构。

// 手改堆。核心点就在 反向索引表indexMap,能够获取对象在数组中的下标值
public class HeapGenerate<T> {
    private ArrayList<T> arr; // 存储节点的数组
    private HashMap<T, Integer> indexOfMap; // 存储每个节点在堆上的下标
    private int size; // 堆的大小
    private Comparator<? super T> comp; // 比较器

    public HeapGenerate(Comparator<? super T> comp) {
        this.arr = new ArrayList<>();
        this.indexOfMap = new HashMap<>();
        this.size = 0;
        this.comp = comp;
    }

    public boolean isEmpty() {
        return this.size == 0;
    }

    public int size() {
        return size;
    }

    public List<T> getAllElements() {
        List<T> list = new ArrayList<>();
        for (T v : arr) {
            list.add(v);
        }
        return list;
    }

    public boolean contains(T obj) {
        return indexOfMap.containsKey(obj); // 查看当前堆中是否有该对象
    }

    public T peek() {
        return arr.get(0);
    }

    public void add(T value) {
        this.arr.add(value);
        this.indexOfMap.put(value, size); // 存储下标值
        heapInsert(size++); // 往上调整
    }

    // 弹出堆顶结果
    public T poll() {
        T res = arr.get(0);
        swap(0, size - 1); // 第一个数据和最后一个数据进行交换
        indexOfMap.remove(res); // 删除res对应的下标
        arr.remove(--size); // 删除在数组上的数据
        heapify(0); // 向下调整
        return res;
    }

    // 手改堆的核心,能删除非堆顶元素
    public void remove(T obj) {
        T replace = arr.get(size - 1); // 拿到最后一个元素
        int index = indexOfMap.get(obj);
        indexOfMap.remove(obj); // 删除在表中的下标
        arr.remove(--size); // 删除数组中的最后一个元素
        if (replace != obj) { // 被删除的元素并不是数组中的最后一个元素
            arr.set(index, replace);
            indexOfMap.put(replace, index); // 新的下标
            resign(replace);
        }
    }

    // 手改堆的核心方法
    public void resign(T value) { // 根据对象,获取对象在数组中的下下标,从而进行调整
        heapInsert(indexOfMap.get(value));
        heapify(indexOfMap.get(value)); // 二者,只可能有一个会调用,只可能向上或向下
    }

    private void heapify(int i) {
        int left = (i << 1) + 1;
        while (left < size) {
            int maxChild = left + 1 < size && comp.compare(arr.get(left + 1), arr.get(left)) < 0?
                    left + 1 : left;
            maxChild = comp.compare(arr.get(maxChild), arr.get(i)) < 0? maxChild : i; // 跟父节点做判断
            if (maxChild == i) {
                break;
            }
            swap(i, maxChild);
            i = maxChild;
            left = (i << 1) + 1; // 再次刷新左孩子
        }
    }

    // 往上走,调整堆结构
    private void heapInsert(int i) {
        // 根据自定义的比较器进行比较
        // 此处除以2,用位运算代替,要判断i是大于0才行
        while (i > 0 && comp.compare(arr.get(i), arr.get((i - 1) >> 1)) < 0) {
            swap(i, (i - 1) >> 1);
            i = (i - 1) >> 1;
        }
    }

    // 不仅要更新在数组上的值,还要更新indexOfMap中的值
    private void swap(int up, int down) {
        T o1 = arr.get(up);
        T o2 = arr.get(down);
        indexOfMap.put(o1, down);
        indexOfMap.put(o2, up);
        arr.set(up, o2); // 更新
        arr.set(down, o1); // 更新
    }
}

15、有序表

也就是能够排序的一些结构,比如AVL树、SB树、跳表、红黑树等。比较好写的可能就是SB树。

// Size Balance Tree,通过节点的数量来调整平衡的
private static class SBTNode<K extends Comparable<K>, V> {
    public K key;
    public V value;
    public int size;
    public SBTNode<K, V> left;
    public SBTNode<K, V> right;

    public SBTNode(K key, V value) {
        this.key = key;
        this.value = value;
        size = 1;
    }
}

private static class SizeBalanceTree<K extends Comparable<K>, V> {
    private SBTNode<K, V> root;

    private SBTNode<K, V> getIndex( SBTNode<K, V> cur, int kth) {
        if (kth == (cur.left != null ? cur.left.size : 0) + 1) {
            return cur;
        } else if (kth <= (cur.left != null ? cur.left.size : 0)) {
            return getIndex(cur.left, kth);
        } else {
            return getIndex(cur.right, kth - (cur.left != null ? cur.left.size : 0) - 1);
        }
    }

    public K getIndexKey(int index) {
        if (index < 0 || index >= this.size()) {
            throw new RuntimeException("invalid parameter.");
        }
        return getIndex(root, index + 1).key;
    }

    public V getIndexValue(int index) {
        if (index < 0 || index >= this.size()) {
            throw new RuntimeException("invalid parameter.");
        }
        return getIndex(root, index + 1).value;
    }

    public int size() {
        return root == null ? 0 : root.size;
    }

    public void put(K key, V val) {
        if (key == null) {
            throw new RuntimeException("key is invalid.");
        }
        SBTNode<K, V> lastNode = findLastIndex(key);
        if (lastNode != null && key.compareTo(lastNode.key) == 0) { // 更新值的情况
            lastNode.value = val;
        } else { // 新插入值
            root = add(root, key, val);
        }
    }

    // 返回等于key的,或者key的父节点。
    private SBTNode<K, V> findLastIndex(K key) {
        SBTNode<K, V> pre = root;
        SBTNode<K, V> cur = root;
        while (cur != null) {
            pre = cur;
            if (key.compareTo(cur.key) == 0) {
                break;
            } else if (key.compareTo(cur.key) < 0) {
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        }
        return pre;
    }

    private SBTNode<K, V> add(SBTNode<K, V> node, K key, V val) {
        if (node == null) {
            return new SBTNode<K, V>(key, val);
        } else {
            node.size++;
            if (key.compareTo(node.key) < 0) {
                node.left = add(node.left, key, val);
            } else {
                node.right = add(node.right, key, val);
            }
            // 维持平衡
            return maintain(node);
        }
    }

    public void remove(K key) {
        if (key == null) {
            throw new RuntimeException("key is invalid.");
        }
        if (containsKey(key)) {
            root = delete(root, key);
        }
    }

    private SBTNode<K, V> delete(SBTNode<K, V> node, K key) {
        node.size--;
        int compare = key.compareTo(node.key);
        if (compare < 0) {
            node.left = delete(node.left, key);
        } else if (compare > 0) {
            node.right = delete(node.right, key);
        } else { // ==0的情况
            if (node.right == null) { // 有左孩子的情况,或者左右孩子都没有
                node = node.left;
            } else if (node.left == null) { // 有右孩子的情况
                node = node.right;
            } else { // 左右孩子都有的情况
                SBTNode<K, V> pre = null;
                SBTNode<K, V> cur = node.right;
                cur.size--;
                while (cur.left != null) {
                    pre = cur;
                    cur = cur.left;
                    cur.size--;
                }
                if (pre != null) {
                    pre.left = cur.right;
                    cur.right = node.right;
                }
                cur.left = node.left;
                cur.size = cur.left.size + (cur.right != null ? cur.right.size : 0) + 1;
                node = cur;
            }
        }
        // 维持平衡---可以不用维持平衡,在add的时候再维护
        // node = maintain(node);
        return node;
    }

    public boolean containsKey(K key) {
        if (key == null) {
            throw new RuntimeException("key is invalid.");
        }
        SBTNode<K, V> lastNode = findLastIndex(key);
        return lastNode != null && key.compareTo(lastNode.key) == 0;
    }

    private SBTNode<K, V> maintain(SBTNode<K, V> node) {
        if (node == null) {
            return null;
        }
        // 计算node的下一级节点数和 下下一级节点数
        int leftSize = node.left != null ? node.left.size : 0;
        int rightSize = node.right != null ? node.right.size : 0;
        int leftLeftSize = node.left != null && node.left.left != null ? node.left.left.size : 0; // LL
        int leftRightSize = node.left != null && node.left.right != null ? node.left.right.size : 0; // LR
        int rightLeftSize = node.right != null && node.right.left != null ? node.right.left.size : 0; // RL
        int rightRightSize = node.right != null && node.right.right != null ? node.right.right.size : 0; // RR
        if (leftLeftSize > rightSize) { // RR型旋转
            node = rightRotate(node);
            node.right = maintain(node.right); // 先调整node下级节点的平衡
            node = maintain(node);
        } else if (leftRightSize > rightSize) { // LR型旋转
            node.left = leftRotate(node.left); // 先左旋转
            node = rightRotate(node); // 再右旋转
            node.left = maintain(node.left);
            node.right = maintain(node.right); // 先维持node的下级节点
            node = maintain(node);
        } else if (rightLeftSize > leftSize) { // RL型旋转
            node.right = rightRotate(node.right);
            node = leftRotate(node);
            node.left = maintain(node.left);
            node.right = maintain(node.right);
            node = maintain(node);
        } else if (rightRightSize > leftSize) { // LL型旋转
            node = leftRotate(node);
            node.left = maintain(node.left);
            node = maintain(node);
        }
        return node;
    }

    private SBTNode<K, V> leftRotate(SBTNode<K, V> node) {
        SBTNode<K, V> newHead = node.right;
        node.right = newHead.left;
        newHead.left = node;
        newHead.size = node.size;
        node.size = (node.left != null ? node.left.size : 0) + (node.right != null ? node.right.size : 0) + 1;
        return newHead;
    }

    private SBTNode<K, V> rightRotate(SBTNode<K, V> node) {
        SBTNode<K, V> newHead = node.left;
        node.left = newHead.right;
        newHead.right = node;
        newHead.size = node.size;
        node.size = (node.left != null ? node.left.size : 0) + (node.right != null ? node.right.size : 0) + 1;
        return newHead;
    }

    public V get(K key) {
        if (key == null) {
            throw new RuntimeException("key is invalid.");
        }
        SBTNode<K, V> lastNode = findLastIndex(key);
        if (lastNode != null && key.compareTo(lastNode.key) == 0) {
            return lastNode.value;
        }
        return null;
    }

    public K firstKey() {
        if (root == null) {
            return null;
        }
        SBTNode<K, V> node = root;
        while (node.left != null) {
            node = node.left;
        }
        return node.key;
    }

    public K lastKey() {
        if (root == null) {
            return null;
        }
        SBTNode<K, V> node = root;
        while (node.right != null) {
            node = node.right;
        }
        return node.key;
    }

    // <= key的,最接近key的
    public K floorKey(K key) {
        if (key == null) {
            throw new RuntimeException("key is invalid.");
        }
        SBTNode<K, V> ans = null;
        SBTNode<K, V> cur = root;
        while (cur != null) {
            if (key.compareTo(cur.key) == 0) {
                ans = cur;
                break;
            } else if (key.compareTo(cur.key) < 0) { // key < cur.key
                cur = cur.left;
            } else { // key >= cur.key
                ans = cur;
                cur = cur.right;
            }
        }
        return ans == null ? null : ans.key;
    }

    // >= key的,最接近key的
    public K ceilingKey(K key) {
        if (key == null) {
            throw new RuntimeException("key is invalid.");
        }
        SBTNode<K, V> ans = null;
        SBTNode<K, V> cur = root;
        while (cur != null) {
            if (key.compareTo(cur.key) == 0) {
                ans = cur;
                break;
            } else if (key.compareTo(cur.key) < 0) { // key < cur.key
                ans = cur;
                cur = cur.left;
            } else { // key >= cur.key
                cur = cur.right;
            }
        }
        return ans == null? null : ans.key;
    }
}

16、单调栈

需求:在一个无序数组中,找一个元素的左右两侧第一个比它大(小)的数。时间复杂度O(N),空间复杂度O(N)。

细分为单调递增栈、单调递减栈。LeetCode练习题

  • 单调递增栈:从栈顶往下,元素越来越大。用于找元素的两侧第一个比它大的数。
  • 单调递减栈:从栈顶往下,元素越来越小。用于找元素的两侧第一个比它小的数。
// 单调递减栈。 反之单调递增栈,只需修改第6行while循环中的 <=符号即可
public static void monotonousStack(int[] nums) {
    Stack<Integer> stack = new Stack<>(); // 存入的数组的下标值
    for (int i = 0; i < nums.length; i++) {
        // 下方的具体是 < 还是<=,也是看题意来定的
        while (!stack.isEmpty() && nums[i] <= nums[stack.peek()]) {
            // 此时弹出的pos下标,有以下性质:(假设弹出栈顶元素pos后,此时栈顶元素 = k)
            // 1、[i]的元素 <= [pos]
            // 2、[k] < [pos]的元素 
            // 所有就有 [k] < [pos] && pos >= [i],
            // 所有对于pos来说,左右两侧的比它小的数就出来了
            int pos = stack.pop();
            int k = stack.isEmpty() ? -1 : stack.peek();
            // 后续操作根据题意来定
        }
        stack.push(i);
    }
}

17、数位DP

最简单的数位DP,请看 leetcode233题 数字1的个数
思路:把n这个数字,转换成字符串类型,然后从左往右开始枚举每一个位置的数值,类似于全排列的那种递归展开。

from functools import cache
# leetcode 1012题 至少有1位重复数字
class Solution:
    def numDupDigitsAtMostN(self, n: int) -> int:
        # 现在的问题是,如何将cnt数组,进行状态转移
        # 换个思路:题目要计算有重复字符的数字,那么【全部数字组合】-【不重复数字组合】
        # 剩下的就是 【有重复字符的组合数】
        s = str(n)
        @cache
        # i指向字符串,
        # status的二进制位信息,表示每个位置的数字是否出现过 (左神讲的 状态压缩)
        # is_limit 表示是否到达n的上限了
        # pre_num 表示 0~i-1位置,是否生成了数值
        def f(i: int, status: int, is_limit: bool, pre_num: bool) -> int:
            if i == len(s):
                return int(pre_num)
            ans = 0
            if not pre_num:
                ans = f(i + 1, status, False, False)
            # 枚举
            # 0~i-1位置还没有生成数值的话,这个位置开始生成,比如从1开始,避开前导0
            low = 0 if pre_num else 1 
            up = int(s[i]) if is_limit else 9
            for d in range(low, up + 1):
                if (status >> d) & 1 == 0: # 说明d这个数字还没出现过
                    status += (1 << d) # 将第 d位的二进制位 标记
                    ans += f(i + 1, status, is_limit and d == up, True)
                    status -= (1 << d) # 恢复现场
            return ans
        
        return n - f(0, 0, True, False)

    # 正向思考,枚举每一个位置
    def numDupDigitsAtMostN1(self, n: int) -> int:
        s = str(n)
        cnt = [0] * 10
        def f(i: int, is_limit: bool, pre_num: bool) -> int:
            if i == len(s):
                for x in cnt: 
                    if x >= 2: return True
                return False
            ans = 0
            if not pre_num:
                ans = f(i + 1, False, False)
            # 枚举
            # 0~i-1位置还没有生成数值的话,这个位置开始生成,比如从1开始,避开前导0
            low = 0 if pre_num else 1 
            up = int(s[i]) if is_limit else 9
            for d in range(low, up + 1):
                cnt[d] += 1
                ans += f(i + 1, is_limit and d == up, True)
                cnt[d] -= 1
            return ans
        
        return f(0, True, False)

18、快速幂

核心思想:将指数折半拆分进行计算,例如指数8 等价于 4 + 4,那么只需要计算 a^4之后,再对其平方,就能得到a ^8。

// 快速幂, a^b次方
// 公式:(n1 * n2) % MOD = [(n1 % MOD) * (n2 % MOD)] % MOD
private int MOD = 1337;
private long quickMul(int a, int b) {
    if (b == 0) return 1;
    if (b == 1) return a;
    long sum = quickMul(a, b / 2) % MOD;
    // b是奇数,就拆分为偶数+偶数+1
    // b是偶数,就拆分为 偶数+偶数
    return b % 2 == 0? sum * sum % MOD : sum * sum * a % MOD;
}

19、图论

1).建图

常见的邻接矩阵、邻接表这里就不细说了,各大数据结构中的教材都有,这里主要说一下一个比赛常用的建图:链式前向星。他的优点在于 能省空间, 假设 一共N个点,M条边。空间复杂度能做到 O(N + M).

思路:对每一条边进行编号,存储边的信息,边与边进行串联。

// 5000个点,2 * 10^5条边。这里乘2,是为了处理有向边
static int[] head = new int[5001]; // head[i] =j,表示i这个点从j号边开始遍历
static int[] next = new int[200001 * 2]; // next[j] = k,表示j号边的后面是k号边
static int[] to = new int[200001 * 2]; // to[j] = d,表示 j号边去往的是d
static int[] weight = new int[200001 * 2]; // weight[j] = w, 表示j号边的权重是w
static int cnt = 1; // 加边的边号。 从1开始,边号=0,说明就是没有边

private static void addEdge(int x, int y, int w) {
    next[cnt] = head[x];
    to[cnt] = y;
    weight[cnt] = w;
    head[x] = cnt++;
}

2).最小生成树

a). Kruskal

思路:以边为主体,每次挑选权重最小的边,判断起点、终点是否连通,最后凑齐 N-1条边即可。(N指的是 点数)

public class Kruskal {
    static int[][] edges = new int[200001][3];
    static int[] father = new int[5001];

    private static int find(int x) {
        if (father[x] == x) return x;
        int next = find(father[x]);
        father[x] = next;
        return next;
    }

    // 如果x、y不在一个集合,就合并,并返回true
    // 如果在一个集合,就返回false
    private static boolean union(int x, int y) {
        int fa1 = find(x), fa2 = find(y);
        if (fa1 != fa2) {
            father[fa1] = fa2;
            return true;
        }
        return false;
    }

    private static String kruskal(int n, int m ) {
        for (int i = 0; i <= n; i++) father[i] = i;
        Arrays.sort(edges, 0, m,(o1, o2) -> o1[2] - o2[2]);
        int ans = 0, edgeCnt = 0;
        for (int i = 0; i < m ; i++) {
            if (union(edges[i][0], edges[i][1])) {
                ans += edges[i][2];
                edgeCnt++;
            }
        }
        return edgeCnt == n - 1? ans + "" : "orz";
    }
}
b). Prim

思路:随机从一个点出发,以点去解锁边,将边放入小根堆中,弹出最小权重边,凑齐 N个点就行。时间复杂度O(N*logM)。Prim算法还有优化版本,就是堆中只需要存储N个点以及到该点的权重,时间复杂度能做到 O(N*logN)。

// 普通小根堆版本
public class PrimOfDynamic {
    static boolean[] vis = new boolean[5001];
	// 邻接表的形式
    private static String prim(ArrayList<ArrayList<int[]>> g, int n, int m) {
        PriorityQueue<int[]> heap = new PriorityQueue<>((o1, o2) -> {
            return o1[1] - o2[1];
        });
        Arrays.fill(vis, 0, n + 1, false);
        heap.add(new int[]{1, 0});
        int ans = 0, nodeCnt = 0;
        while (!heap.isEmpty()) {
            int[] pos = heap.poll();
            if (!vis[pos[0]]) {
                ans += pos[1];
                nodeCnt++;
                vis[pos[0]] = true;
                for (int[] next : g.get(pos[0])) {
                    heap.add(next);
                }
            }
        }
        return nodeCnt == n ? ans + "" : "orz";
    }
}

3).最短路径

a. Dijkstra

思路:以起点解锁边,弹出最小权重边,再去解锁点,直到跑完整个图。

public class Dijkstra {
    // s是	起点
    private static int[] dijkstra(int n, int m, int s, int[][] edges) {
        ArrayList<int[]>[] g = new ArrayList[n + 1]; // 建邻接表
        for (int i = 1; i <= n; i++) g[i] = new ArrayList<>();
        for (int[] arr : edges) {
            int x = arr[0], y = arr[1], w = arr[2];
            g[x].add(new int[]{y, w});
        }
        int[] dis = new int[n + 1];
        boolean[] vis = new boolean[n + 1];
        Arrays.fill(dis, Integer.MAX_VALUE);
        dis[s] = 0;
        PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> {
            return o1[1] - o2[1];
        });
        pq.add(new int[]{s, 0});
        while (!pq.isEmpty()) {
            int x = pq.poll()[0];
            if (vis[x]) continue;
            vis[x] = true;
            for (int[] next : g[x]) {
                int y = next[0], w = next[1];
                if(!vis[y] && dis[x] + w < dis[y]) {
                    dis[y] = dis[x] + w;
                    pq.add(new int[]{y, dis[x] + w});
                }
            }
        }
        return dis;
    }
}
b. 分层图最短路

所谓分层图最短路,就是在 题目所给的点上,再添加状态,用来表示 “逻辑上的点”,然后在这种方式下,再去跑 Dijkstra 或者BFS这些。

示例:leetcode LCP 35. 电动车游城市

class Solution {
    // 分层图最短路
    // 以 景点+剩余电量,这个整体看做 一个虚拟的点
    // 问 这个虚拟点 -> end点的最短路径问题
    public int electricCarPlan(int[][] paths, int cnt, int start, int end, int[] charge) {
        int n = charge.length;
        ArrayList<ArrayList<int[]>> g = new ArrayList<>();
        for (int i = 0; i < n; i++) g.add(new ArrayList<>());
        for (int[] arr: paths) { // 建邻接表
            int x = arr[0], y = arr[1], w = arr[2];
            g.get(x).add(new int[]{y, w});
            g.get(y).add(new int[]{x, w});
        }

        // 0:表示景点;1:表示电动车的剩余电量
        int[][] dis = new int[n][cnt + 1];
        boolean[][] vis = new boolean[n][cnt + 1];
        for(int i = 0; i < n; i++) Arrays.fill(dis[i], Integer.MAX_VALUE);
        dis[start][0] = 0;
        // 0: 景点
        // 1: 剩余电量
        // 2: 从start -> 该点的 最少时间
        PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> o1[2] - o2[2]);
        pq.add(new int[]{start, 0, 0});
        while (!pq.isEmpty()) { // 跑 Dijkstra算法
            int[] pos = pq.poll();
            int x = pos[0], rest = pos[1], cost = pos[2];
            if (vis[x][rest]) continue;
            if (x == end) return cost;
            vis[x][rest] = true;
            // 跑后续节点
            for (int[] next : g.get(x)) {
                int y = next[0], w = next[1];
                // 1、继续充电
                if (rest < cnt) {
                    dis[x][rest + 1] = cost + charge[x];
                    pq.add(new int[]{x, rest + 1, cost + charge[x]});
                }
                // 2、骑车 去下一站点
                if (rest >= w && cost + w < dis[y][rest - w]) {
                    dis[y][rest - w] = cost + w;
                    pq.add(new int[]{y, rest - w, cost + w});
                }
            }
        }
        return -1;
    }
}
c. Floyd

需求:得到 每一个点 到其他点的最短路径。

思路:枚举每一个点,作为跳板,看看能不能拉近 A、B两点的距离。时间复杂度O(N^3)。

public class Floyd {
    static int[][] grid = new int[101][101]; // 邻接矩阵,初始值是 int最大值
    static int n, m;

    private static void floyd() {
        // 枚举 中间跳板 k
        // a -> k ->b,看看能不能缩短 a->b的距离
        for (int k = 1; k <= n; k++) { // 跳板
            for (int i = 1; i <= n; i++) { // 起点
                for (int j = 1; j <= n; j++) { // 终点
                    if (grid[i][k] != Integer.MAX_VALUE && grid[k][j] != Integer.MAX_VALUE &&
                            grid[i][k] + grid[k][j] < grid[i][j]) {
                        grid[i][j] = grid[i][k] + grid[k][j];
                    }
                }
            }
        }
    }
}
d. A星

需求:在二维矩阵中,计算某一点 ->另一点的最小代价(距离)。本身可以跑Dijkstra,但是不够快。

A星算法就是在Dijkstra的基础之上,对 堆上的“权重”进行调整。换句话说,就是对 压入堆中的数据,额外的添加一些“吸引力”。而具体如何添加,常见的有三种方式:

  • 曼哈顿距离。|x1 - x2| + |y1 - y2|
  • 对角线距离。|x1 - x2| , |y1 - y2|,二者取最大值。
  • 欧式距离。就是勾股定理,求斜边的长度。

本质上就是 在Dijkstra的基础上,在将边压入堆的时候,调整一下权重,加了预估值。其他的都和Dijkstra一样

// 左神代码仓库所写的 A*与Dijkstra的测试
import java.util.PriorityQueue;

// A*算法模版(对数器验证)
public class Code01_AStarAlgorithm {
	// 0:上,1:右,2:下,3:左
	public static int[] move = new int[] { -1, 0, 1, 0, -1 };

	// A*算法
	// grid[i][j] == 0 代表障碍
	// grid[i][j] == 1 代表道路
	// 只能走上、下、左、右,不包括斜线方向
	// 返回从(startX, startY)到(targetX, targetY)的最短距离
	public static int minDistance2(int[][] grid, int startX, int startY, int targetX, int targetY) {
		if (grid[startX][startY] == 0 || grid[targetX][targetY] == 0) {
			return -1;
		}
		int n = grid.length;
		int m = grid[0].length;
		int[][] distance = new int[n][m];
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				distance[i][j] = Integer.MAX_VALUE;
			}
		}
		distance[startX][startY] = 1;
		boolean[][] visited = new boolean[n][m];
		// 0 : 行
		// 1 : 列
		// 2 : 从源点出发到达当前点的距离 + 当前点到终点的预估距离
		PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[2] - b[2]);
		heap.add(new int[] { startX, startY, 1 + f1(startX, startY, targetX, targetY) });
		while (!heap.isEmpty()) {
			int[] cur = heap.poll();
			int x = cur[0];
			int y = cur[1];
			if (visited[x][y]) {
				continue;
			}
			visited[x][y] = true;
			if (x == targetX && y == targetY) {
				return distance[x][y];
			}
			for (int i = 0, nx, ny; i < 4; i++) {
				nx = x + move[i];
				ny = y + move[i + 1];
				if (nx >= 0 && nx < n && ny >= 0 && ny < m && grid[nx][ny] == 1 && !visited[nx][ny]
						&& distance[x][y] + 1 < distance[nx][ny]) {
					distance[nx][ny] = distance[x][y] + 1;
					heap.add(new int[] { nx, ny, distance[x][y] + 1 + f1(nx, ny, targetX, targetY) });
				}
			}
		}
		return -1;
	}

	// 曼哈顿距离
	public static int f1(int x, int y, int targetX, int targetY) {
		return (Math.abs(targetX - x) + Math.abs(targetY - y));
	}

	// 对角线距离
	public static int f2(int x, int y, int targetX, int targetY) {
		return Math.max(Math.abs(targetX - x), Math.abs(targetY - y));
	}

	// 欧式距离
	public static double f3(int x, int y, int targetX, int targetY) {
		return Math.sqrt(Math.pow(targetX - x, 2) + Math.pow(targetY - y, 2));
	}
}
e. bellman-ford + SPFA

Bellman-Ford算法,解决可以有负权边但是不能有负环(保证最短路存在)的图,单源最短路算法。

松弛操作:假设源点为A,从A到任意点F的最短距离为distance[F],假设从点P出发某条边,去往点S,边权为W如果发现,distance[P] + W < distance[S],也就是通过该边可以让distance[S]变小,那么就说,P出发的这条边对点S进行了松弛操作。

Bellman-Ford过程:1,每一轮考察每条边,每条边都尝试进行松弛操作,那么若干点的distance会变小。2,当某一轮发现不再有松弛操作出现时,算法停止。

Bellman-Ford算法时间复杂度假设点的数量为N,边的数量为M,每一轮时间复杂度O(M)最短路存在的情况下,因为1次松弛操作会使1个点的最短路的边数+1,而从源点出发到任何点的最短路最多走过全部的n个点,所以松弛的轮数必然 <= n - 1,所以Bellman-Ford算法时间复杂度O(M*N)

重要推广:判断从某个点出发能不能到达负环上面已经说了,如果从A出发存在最短路(没有负环),那么松弛的轮数必然 <= n - 1而如果从A点出发到达一个负环,那么松弛操作显然会无休止地进行下去所以,如果发现从A点出发,在第n轮时松弛操作依然存在,说明从A点出发能够到达一个负环。

示例:leetcode 787. K站中转内最便宜的航班

这道题也是可以用上述所讲的 分层图最短路进行求解,也可以用 bellman-ford解。

// Bellman-Ford算法
public static int findCheapestPrice(int n, int[][] flights, int start, int target, int k) {
    int[] cur = new int[n];
    Arrays.fill(cur, Integer.MAX_VALUE);
    cur[start] = 0;
    for (int i = 0; i <= k; i++) {
        int[] next = Arrays.copyOf(cur, n);
        for (int[] edge : flights) {
            // a -> b , w
            if (cur[edge[0]] != Integer.MAX_VALUE) {
                next[edge[1]] = Math.min(next[edge[1]], cur[edge[0]] + edge[2]);
            }
        }
        cur = next;
    }
    return cur[target] == Integer.MAX_VALUE ? -1 : cur[target];
}

Bellman-Ford + SPFA的优化。

思路:因为bellman-Ford是对每条边都进行遍历,其实并不然,如果上一轮 只有A、B两个点的距离被缩短了,那么来到当前这一轮就只需要考察 A、B两点向外的边,看看能不能缩短。

只是优化了常数项时间,整体的时间复杂度还是在 O(N *M)级别。这个算法作用有两个:1、可以跑负权边的图;2、可以判断图中是否存在 负环。但是只能跑小规模的图,不然容易超时。

// Bellman-Ford + SPFA优化模版(洛谷)
// 给定一个 n个点的有向图,请求出图中是否存在从顶点 1 出发能到达的负环
// 负环的定义是:一条边权之和为负数的回路。
// 测试链接 : https://www.luogu.com.cn/problem/P3385
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下所有代码,把主类名改成Main,可以直接通过
import java.io.*;
import java.util.*;
public class SPFA {
    public static int MAXN = 2001;
    public static int MAXM = 6001;
    // 链式前向星建图需要
    public static int[] head = new int[MAXN];
    public static int[] next = new int[MAXM];
    public static int[] to = new int[MAXM];
    public static int[] weight = new int[MAXM];
    public static int cnt;
    // SPFA需要
    public static int MAXQ = 4000001;
    // 源点出发到每个节点的距离表
    public static int[] distance = new int[MAXN];
    // 节点被松弛的次数
    public static int[] updateCnt = new int[MAXN];
    // 节点是否已经在队列中
    public static boolean[] enter = new boolean[MAXN];

    public static void build(int n) {
        cnt = 1;
        Arrays.fill(head, 1, n + 1, 0);
        Arrays.fill(enter, 1, n + 1, false);
        Arrays.fill(distance, 1, n + 1, Integer.MAX_VALUE);
        Arrays.fill(updateCnt, 1, n + 1, 0);
    }

    public static void addEdge(int u, int v, int w) {
        next[cnt] = head[u];
        to[cnt] = v;
        weight[cnt] = w;
        head[u] = cnt++;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StreamTokenizer in = new StreamTokenizer(br);
        PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
        in.nextToken();
        int cases = (int) in.nval;
        for (int i = 0, n, m; i < cases; i++) {
            in.nextToken(); n = (int) in.nval;
            in.nextToken(); m = (int) in.nval;
            build(n);
            for (int j = 0, u, v, w; j < m; j++) {
                in.nextToken(); u = (int) in.nval;
                in.nextToken(); v = (int) in.nval;
                in.nextToken(); w = (int) in.nval;
                if (w >= 0) {
                    addEdge(u, v, w);
                    addEdge(v, u, w);
                } else {
                    addEdge(u, v, w);
                }
            }
            out.println(spfa(n) ? "YES" : "NO");
        }
        out.flush();
        out.close();
        br.close();
    }

    // Bellman-Ford + SPFA优化的模版
    public static boolean spfa(int n) {
        distance[1] = 0;
        updateCnt[1]++;
        Queue<Integer> queue = new LinkedList<>();
        queue.add(1);
        enter[1] = true;
        while (!queue.isEmpty()) {
            int u = queue.poll();
            enter[u] = false;
            for (int ei = head[u], v, w; ei > 0; ei = next[ei]) {
                v = to[ei];
                w = weight[ei];
                if (distance[u] + w < distance[v]) {
                    distance[v] = distance[u] + w;
                    if (!enter[v]) {
                        if (updateCnt[v]++ == n) {
                            return true;
                        }
                        queue.add(v);
                        enter[v] = true;
                    }
                }
            }
        }
        return false;
    }
}

4).拓扑排序

思路:入度为0的点,进入队列,然后BFS扫描即可。


public void f(int[][] edges, int n) {
    int[] indegree = new int[n + 1]; // 入度表
    ArrayList<ArrayList<Integer>> g = new ArrayList<>();
    for (int i =0; i <= n; i++) g.add(new ArrayList<>());
    for (int[] arr: edges) { // 建图
        int x = arr[0], y = arr[1], w = arr[2];
        g.get(x).add(new int[]{y, w});
        indegree[y]++;
    }
    Queue<Integer> q = new LinkedList<>();
    for (int i = 1; i <= n; i++) {
        if (indegree[i] == 0) {
            q.add(i);
        }
    }
    while (!q.isEmpty()) {
        int x = q.poll();
        for (int[] next: g.get(x)) {
            int y = next[0], w = next[1];
            if (--indegree[y] == 0) {
                q.add(y);
            }
        }
    }
    // 队列跑完之后,如果indegree表中,还有非0元素,说明就是存在 环的
}

  • 12
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
在C++中,algorithm和numeric库中有几个常用模板函数可以用于刷题,包括swap,reverse,sort,unique和accumulate。 1. swap(): 这个函数可以交换两个变量的值。它的模板定义如下: ```cpp template <class T> void swap(T& a, T& b) { T c(a); a = b; b = c; } ``` 通过调用swap()函数,可以交换两个变量的值。 2. reverse(): 这个函数可以反转容器中元素的顺序。例如,可以使用reverse()函数反转一个vector容器中的元素。 3. sort(): 这个函数可以对容器中的元素进行排序。可以通过提供一个比较函数来指定排序的规则。比如,可以使用sort()函数对一个vector容器中的元素进行升序排序或者降序排序。 4. unique(): 这个函数可以去除容器中的重复元素,并返回指向新的逻辑尾部的迭代器。需要注意的是,在调用unique()函数之前,需要先对容器进行排序。 5. accumulate(): 这个函数可以对容器中的元素进行累加操作,并返回累加结果。accumulate()函数接受三个参数:容器的起始迭代器、容器的结束迭代器和一个初始值。可以使用accumulate()函数计算数组或者vector容器中元素的和。 以上是C++中一些常用算法函数,可以在刷题过程中使用它们来简化代码并提高效率。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C++ LeetCode刷题常用函数](https://blog.csdn.net/qq_40876059/article/details/126245632)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

听雨7x

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值