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

news2024/11/17 16:46:27

目录

    • 1、二分查找
    • 2、线段树
    • 3、树状数组
    • 4、差分数组
    • 5、前缀树
    • 6、并查集
    • 7、AC自动机
    • 8、Morris遍历
    • 9、二叉树非递归遍历
    • 10、KMP
    • 11、Manacher
    • 12、快速选择 bfprt
    • 13、滑动窗口
    • 14、加强堆
    • 15、有序表
    • 16、单调栈

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 + 1; 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, col2 - 1) + 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 pivot) {
    int less = -1; // 小于区
    int more = nums.length; // 大于区
    int index = 0;
    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);
    }
}
     } 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;
}

}




### 15、单调栈 

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

细分为单调递增栈、单调递减栈。[LeetCode练习题](https://leetcode.cn/problems/trapping-rain-water/description/)。

- 单调递增栈:从栈顶往下,元素越来越大。用于找元素的两侧第一个比它大的数。
- 单调递减栈:从栈顶往下,元素越来越小。用于找元素的两侧第一个比它小的数。

~~~java
// 单调递减栈。 反之单调递增栈,只需修改第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);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/592621.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

chatgpt赋能python:Python中画笔放下:掌握Python图形编程

Python 中画笔放下&#xff1a;掌握 Python 图形编程 Python 是一种高级编程语言&#xff0c;广泛应用于数据处理、人工智能、Web 应用程序等领域。除了这些应用外&#xff0c;Python 还可以用于图形编程&#xff0c;包括绘制 2D 和 3D 图形、创建游戏和交互式应用程序等。在 …

Vue的基本使用

文章目录 Vue简介Vue的使用1.Vue指令2.过滤器3.侦听器4.计算属性 Vue简介 ​ Vue是一套用于构建用户界面的前端框架。 vue的两个特性 (1)数据驱动视图 在使用vue的页面中,vue会监听数据的变化,从而自动重新渲染页面的结构。当页面数据发生变化的时候,会自动重新渲染(数据的…

GUI JFrame实战:六一节,爱她就给她画个哆啦A梦吧

文章目录 前言技术积累容器分类主要APIGraphics图像 绘画方法实战演示1、创建哆啦A梦渲染类2、创建测试方法3、查看渲染结果 写在最后 前言 相信很多使用JAVA高级语言的同学都知道GUI图形用户界面&#xff0c;开发人员可以使用java.awt、javax.swing两个API绘画想要的图形并通…

蓝牙规范系列--基础篇(第一篇)

一、前言 玩过物联网的小伙伴肯定知道ESP32&#xff08;一款WiFi/BT SoC&#xff09;&#xff0c;那肯定也知道蓝牙这个东西&#xff0c;蓝牙技术最近几年由于蓝牙耳机很火&#xff0c;那蓝牙技术到底是怎样的呢&#xff1f; 蓝牙无线技术是一种短距离的通信系统旨在替换便携式…

c++学习——友元、全局友元函数、友元类

友元 类的主要特点之一是数据隐藏&#xff0c;即类的私有成员无法在类的外部(作用域之外)访问。但是&#xff0c;有时候需要在类的外部访问类的私有成员&#xff0c;怎么办? 解决方法是使用友元函数&#xff0c;友元函数是一种特权函数&#xff0c;c允许这个特权函数访问私有…

python 自动编译VS项目

目录 一、python自动编译项目 1、环境变量 1) CMAKE 2&#xff09;VS 2、CMAKE 生成VS项目 1) cmake 命令介绍 2&#xff09;python 调用cmake 命令 3、devenv编译VS项目 1) devenv介绍 2&#xff09;devenv命令行开关 3) 自动编译VS项目 一、python自动编译项目 …

04 布隆过滤器BloomFilter

是什么 布隆过滤器&#xff08;英语&#xff1a;Bloom Filter&#xff09;是 1970 年由布隆提出的。它实际上是一个很长的二进制数组一系列随机hash算法映射函数&#xff0c;主要用于判断一个元素是否在集合中。通常我们会遇到很多要判断一个元素是否在某个集合中的业务场景&a…

C++ 和机器学习:使用 C++ 执行 ML 任务的简介

C 和机器学习&#xff1a;使用 C 执行 ML 任务的简介 介绍 C 是一种高性能编程语言&#xff0c;非常适合机器学习( ML ) 任务。尽管它在 ML 中可能不像 Python 或 R 那样流行&#xff0c;但它在速度和内存效率方面具有优势。 在本文中&#xff0c;我们将概述使用 C 执行 ML 任务…

“双碳同行者大会”成功举办,绿色家居企业参与其中

2023年5月27日 &#xff0c;由万科集团创始人、深石集团创始人王石先生创立的碳中和社区品牌“生物圈三号”在深圳大梅沙成功举办了“回归未来双碳同行——生物圈三号双碳同行者大会暨全球运河穿越新书发布会”。 生物圈三号作为社区碳中和综合解决方案平台&#xff0c;为建筑、…

Word控件Spire.Doc 【其他】教程(5):在 Word 中嵌入媒体文件

Spire.Doc for .NET是一款专门对 Word 文档进行操作的 .NET 类库。在于帮助开发人员无需安装 Microsoft Word情况下&#xff0c;轻松快捷高效地创建、编辑、转换和打印 Microsoft Word 文档。拥有近10年专业开发经验Spire系列办公文档开发工具&#xff0c;专注于创建、编辑、转…

Axios请求失败重刷接口

需求背景 页面接口请求时偶尔会出现 Network Error 异常报错&#xff0c;重新请求就会请求成功 接口没办法捕获异常原因&#xff0c;前端来做一次重刷解决问题 net::ERR_SSL_PROTOCOL_ERRORnet::ERR_CONNECTION_REFUSED 解决思路 记录请求map&#xff08;以url为唯一标识&…

Live800:智能客服机器人有哪些核心功能?

随着互联网的普及和用户服务要求的不断升级&#xff0c;许多企业已经开始关注在线客服系统以及智能客服机器人的应用。 那么&#xff0c;智能客服机器人在解决企业痛点的过程中&#xff0c;充分使用了哪些核心功能呢&#xff1f; 一、自然语言处理 客户提问的方式千奇百怪&am…

Hindawi官宣上线8本新刊!这6本被踢SCI期刊,EI正常在检

Hindawi官宣&#xff1a;8本新刊上线 2023年5月23日&#xff0c;Hindawi官宣与IET的8本合作期刊在完全开放获取模式下全面上线&#xff0c;所有期刊名称仍然保留IET抬头&#xff0c;但将由Hindawi全面负责出版、主持和运营等相关事宜。 No.1 IET BioMetrics 该期刊关注生物识…

linux shell编程规范和变量

一、shell简述 概念&#xff1a;shell解释器&#xff0c;翻译官功能&#xff0c;与内核进行沟通的应用程序。 把代码翻译为二进制&#xff0c;让内核处理&#xff0c;负责接收用户输入的操作指令&#xff08;命令&#xff09;并进行解释&#xff0c;将需要执行的操作传递给内核…

功能上新| GPU篇:SoC GPU信息、GPU Counter性能指标

在玩家要求游戏更流畅、画质更精美的今天&#xff0c;优化GPU过度使用导致的性能问题成了大多数游戏开发团队关注的核心。当项目的GPU压力达到一定阈值时&#xff0c;卡顿、掉帧、发热、降频等问题便随之而来&#xff0c;严重影响用户体验。 为了解决项目的GPU压力问题&#x…

Kafka的常用命令

提前说明&#xff1a;下述所有命令都是需要进入到Kafka服务的的bin目录下执行&#xff0c;即下图所示目录。 1.查看主题列表 kafka-topics.sh --list --bootstrap-server ip:端口 2.创建主题 如下为创建一个单分区单副本的主题的命令示例&#xff1a; kafka-topics.sh --c…

Hack The Box - sequel关卡

TASK 1 任务 1 During our scan, which port do we find serving MySQL? 在扫描过程中&#xff0c;我们发现哪个端口为 MySQL 提供服务&#xff1f; 3306TASK 2 任务 2 What community-developed MySQL version is the target running? 目标运行的是哪个社区开发的MySQL版…

基于nodejs+Vue的少儿编程课程网上报名系统express+vscode

近年来&#xff0c;随着网络技术的不断发展&#xff0c;越来越多人在网站查询各种信息&#xff0c;少儿编程网上报名系统对用户和管理员都有很大帮助&#xff0c;少儿编程网上报名系统通过和数据库管理系软件协作来实现用户与管理员之间的一个很好的操作平台&#xff0c;基于这…

【大数据学习篇11】广告点击流实时统计

学习目标/Target 掌握广告点击流实时统计实现思路 掌握利用Kafka生产用户广告点击流数据 了解数据库设计 掌握如何创建Spark Streaming连接 掌握利用Spark Streaming读取业务数据 掌握利用Spark读取黑名单用户 掌握利用Spark Streaming过滤黑名单用户 掌握利用Spark St…

bug:使用useClipboard不生效问题

背景 使用vueuse/core的useClipboard来实现复制功能&#xff0c;没有生效。 原因 useClipboard 的底层是是使用navigator.clipboard实现的。但是在 http协议 中&#xff0c;navigator.clipboard打印undefined&#xff08;如下图&#xff09;&#xff0c;导致在http协议中使用…