线段树/区间树(java实现版详解附leetcode例题)

news2024/11/20 13:18:05

目录

什么是线段树

线段树基础表示

创建线段树(Java版详解)

线段树的区间查询

leetcode上的线段树相关问题

leetcode303题.区域和检索-数组不可变

使用线段树解题

不使用线段树解题

leetcode307题.区域和检索-数组可修改

不使用线段树解题

线段树中的更新操作

使用线段树解题

更多线段树相关的话题

懒惰更新

二维线段树

动态线段树


什么是线段树

在介绍线段树前,我们先通过两个小问题引入一下为什么我们需要使用线段树。

最经典的线段树问题:区间染色

或者我们问一个更普遍的问题:在m次操作后,我们可以在[i,j]区间 中我们可以看见多少种颜色?

对于这种问题,我们可以用数组实现两种操作:

染色操作(更新区间) O(n)

查询操作(查询区间) O(n)

O(n)的时间复杂度在一些情况下是不适合的,所以我们要进一步寻找更优的算法。

另一经典问题:区间查询

以上两种经典问题的更新和查询都是O(n)的时间复杂度,这时候引入线段树就显得额外宝贵了。 

在用一个数组A创建线段树时,我们有一个前提,就是对于我们的线段树,我们是不考虑向线段树中添加元素或者删除元素的,比如我们墙的长度给出来那它就是固定的了,我们不再考虑再加长或者缩短这面墙。这样我们就保证了区间本身是固定的,所以我们用静态数组就好了。

根据数组A构造的线段树就是下图的样子:

我们可以看到,线段树每个结点都是一个区间,这个区间不是说把区间中的所有元素都存进这个结点,以线段树求和为例,每个结点存储的就是它所在区间的数值和。例如:A[4...7]存储的就是[4,7]这个区间中所有数字的

线段树基础表示

线段树不一定是满二叉树,我们上面举得数组A构成的线段树中有8个元素,8刚好是2的3次方,所以它恰好是一棵满二叉树。一般情况下,如果某个结点的区间元素个数是偶数可以平分,那么一个结点的左右孩子各自会存储一半的元素。否则,就左右孩子一个存的少一点一个存的多一点。

例如一个存储10个元素的数组A就和8个元素的数组A不一样:

我们可以看到,线段树的叶子节点不一定在最后一层,也可以在倒数第二层。

我们的线段树也不一定是满二叉树,也不一定是完全二叉树。

但我们的线段树一定是一棵平衡二叉树(最大深度和最小深度的差不超过1)。

平衡二叉树的优势是:它不会像二分搜索树一样退化成一个链表,一棵平衡二叉树的高度和结点的关系一定是一个log的关系,这使得在平衡二叉树上进行搜索查询是非常高效的。

线段树虽然不是一个完全二叉树,但是作为一棵平衡二叉树,我们仍然可以用数组来表示它。表示方法是什么呢?我们可以把线段树看作是一棵满二叉树,最后一层虽然有很多结点是不存在的,我们把它们看作是空就好了。满二叉树作为一棵完全二叉树,是可以用数组来表示的。

如果区间有n个元素,用数组表示需要有多少结点呢?

对于一棵满二叉树,层数和每一层的结点数的关系是 第h - 1层 : 2^(h - 1)。

h层是指从0层到h-1层共h层。

有了上图所给的结论,我们就能很好的分析所需要的结点数了。

当然,对于这4n的空间,我们并不是每一个都利用起来了,而且我们是一个估计值,线段树不一定是满二叉树,最后一层的很多地方就是空的,在最坏的情况下可能有一半的空间都是浪费的,如下图。

不过我们在这里不用过多的考虑这些浪费的情况,对现代计算机来说存储空间本身还是不叫问题的,我们做算法的原则一般还是需要用空间来换时间。当然这些浪费是可以避免的,我们在文章最后对线段树做更多拓展的时候会提到,有兴趣的朋友可以尝试不使用数组来存储而采用链式的结构来存储线段树。

我们现在是采用数组的方式来存储一棵线段树,我们先实现一个基础的代码。

创建线段树(Java版详解)

//线段树的各自基本实现
public class SegmentTree<E> {
    private E[] data;
    private E[] tree;
    private Merger<E> merger;
    //构造函数传进来的是我们整个要考察的范围
    public SegmentTree(E[] arr, Merger<E> merger){
        this.merger = merger;
        data = (E[])new Object[arr.length];
        for(int i = 0; i < arr.length; i++){
            data[i] = arr[i];
        }
        tree = (E[])new Object[4 * arr.length];
        //从根节点开始
        buildSegmentTree(0, 0, data.length - 1);
    }
    //在treeIndex的位置创建表示区间[l...r]的线段树
    private void buildSegmentTree(int treeIndex, int l, int r){
        if(l  == r){
            tree[treeIndex] = data[l];
            return;
        }
        //左右子树对应的索引
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        //左右子树对应的区间范围
        int mid = l + (r - l) / 2; //防止整型溢出
        //递归调用
        buildSegmentTree(leftTreeIndex, l, mid);
        buildSegmentTree(rightTreeIndex, mid + 1, r);
        /*因为我们整体代码采用的是泛型,所以tree[treeIndex]的具体实现是加减乘除还是其他什么是取决于用户的具体实现
         我们引入了一个接口融合器,否则直接写加减乘除还是怎样编译器会报错*/
        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }
    public int getSize(){
        return data.length;
    }
    public E get(int index){
        if(index < 0 || index >= data.length){
            throw new IllegalArgumentException("Index is illegal");
        }
        return data[index];
    }
    //返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子
    private int leftChild(int index){
        return 2 * index + 1;
    }
    //返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子
    private int rightChild(int index){
        return 2 * index + 2;
    }
    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append('[');
        for(int i = 0; i < tree.length; i++){
            if(tree[i] != null){
                res.append(tree[i]);
            }
            else{
                res.append("null");
            }
            if(i != tree.length - 1){
                res.append(",");
            }
        }
        res.append(']');
        return res.toString();
    }
}
//融合器接口实现
public interface Merger<E> {
    E merge(E a, E b);
}
//Main函数
//线段树结构的数组表示,以线段树求和为例
public class Main{
    public static void main(String[]args){
          Integer []nums = {-2, 0, 3, -5, 2, -1};
          SegmentTree<Integer> segTree = new SegmentTree<>(nums, (a, b) -> a + b);
          System.out.println(segTree);
      }

}

运行结果:

线段树的区间查询

线段树的查询还是蛮好理解的,只需要从根节点开始向下找相应的子区间,然后再把所有找到的子区间综合起来就好了 ,这个找的过程和树的高度相关,和我们需要查询的区间长度是无关的。因为线段树的高度是logn级别的,所以我们整个的查询也是logn级别的。

接下来我们来实现一下线段树的查询操作

//查询操作
    //返回待查询区间[queryL, queryR]的值
    public E query(int queryL, int queryR){
        //边界检查
        if(queryL < 0 || queryL >= data.length ||
         queryR < 0 || queryR >= data.length || queryL > queryR){
            throw new IllegalArgumentException("Index is illegal");
        }
        //递归函数,从根节点开始
        return query(0, 0, data.length - 1, queryL, queryR);
    }
    //设计递归函数
    //在以treeID为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值
    private E query(int treeIndex, int l, int r, int queryL, int queryR){
        if(l == queryL && r == queryR){
            return tree[treeIndex];
        }
        int mid = l + (r - l) / 2;
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        //待查询区间落在右孩子那边
        if(queryL >= mid + 1){
            return query(rightTreeIndex, mid + 1, r, queryL, queryR);
        }
        //落在左孩子那边
        else if(queryR <= mid){
            return query(leftTreeIndex, l, mid, queryL, queryR);
        }
        //一部分落在左孩子那边,一部分落在右孩子那边
        E leftResult = query(leftTreeIndex, l, mid, queryL, mid);
        E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
        //两边都找一下然后融合
        return merger.merge(leftResult, rightResult);
    }

把我们刚实现的查询操作加入咱们线段树的基础代码中,并在main函数中创建样例运行。

//实现了查询操作的线段树基本操作
public class SegmentTree<E> {
    private E[] data;
    private E[] tree;
    private Merger<E> merger;
    //构造函数传进来的是我们整个要考察的范围
    public SegmentTree(E[] arr, Merger<E> merger){
        this.merger = merger;
        data = (E[])new Object[arr.length];
        for(int i = 0; i < arr.length; i++){
            data[i] = arr[i];
        }
        tree = (E[])new Object[4 * arr.length];
        //从根节点开始
        buildSegmentTree(0, 0, data.length - 1);
    }
    //在treeIndex的位置创建表示区间[l...r]的线段树
    private void buildSegmentTree(int treeIndex, int l, int r){
        if(l  == r){
            tree[treeIndex] = data[l];
            return;
        }
        //左右子树对应的索引
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        //左右子树对应的区间范围
        int mid = l + (r - l) / 2; //防止整型溢出
        //递归调用
        buildSegmentTree(leftTreeIndex, l, mid);
        buildSegmentTree(rightTreeIndex, mid + 1, r);
        /*因为我们整体代码采用的是泛型,所以tree[treeIndex]的具体实现是加减乘除还是其他什么是取决于用户的具体实现
         我们引入了一个接口融合器,否则直接写加减乘除还是怎样编译器会报错*/
        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }
    public int getSize(){
        return data.length;
    }
    public E get(int index){
        if(index < 0 || index >= data.length){
            throw new IllegalArgumentException("Index is illegal");
        }
        return data[index];
    }
    //返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子
    private int leftChild(int index){
        return 2 * index + 1;
    }
    //返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子
    private int rightChild(int index){
        return 2 * index + 2;
    }
    //查询操作
    //返回待查询区间[queryL, queryR]的值
    public E query(int queryL, int queryR){
        //边界检查
        if(queryL < 0 || queryL >= data.length ||
         queryR < 0 || queryR >= data.length || queryL > queryR){
            throw new IllegalArgumentException("Index is illegal");
        }
        //递归函数,从根节点开始
        return query(0, 0, data.length - 1, queryL, queryR);
    }
    //设计递归函数
    //在以treeID为根的线段树中[l...r]的范围里,搜索区间[queryL...queryR]的值
    private E query(int treeIndex, int l, int r, int queryL, int queryR){
        if(l == queryL && r == queryR){
            return tree[treeIndex];
        }
        int mid = l + (r - l) / 2;
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        //待查询区间落在右孩子那边
        if(queryL >= mid + 1){
            return query(rightTreeIndex, mid + 1, r, queryL, queryR);
        }
        //落在左孩子那边
        else if(queryR <= mid){
            return query(leftTreeIndex, l, mid, queryL, queryR);
        }
        //一部分落在左孩子那边,一部分落在右孩子那边
        E leftResult = query(leftTreeIndex, l, mid, queryL, mid);
        E rightResult = query(rightTreeIndex, mid + 1, r, mid + 1, queryR);
        //两边都找一下然后融合
        return merger.merge(leftResult, rightResult);
    }

    @Override
    public String toString(){
        StringBuilder res = new StringBuilder();
        res.append('[');
        for(int i = 0; i < tree.length; i++){
            if(tree[i] != null){
                res.append(tree[i]);
            }
            else{
                res.append("null");
            }
            if(i != tree.length - 1){
                res.append(",");
            }
        }
        res.append(']');
        return res.toString();
    }
}
/融合器
public interface Merger<E> {
    E merge(E a, E b);
}
//线段树结构的数组表示,以线段树求和为例
public class Main{
    public static void main(String[]args){
          Integer []nums = {-2, 0, 3, -5, 2, -1};
          SegmentTree<Integer> segTree = new SegmentTree<>(nums, (a, b) -> a + b);
          //System.out.println(segTree);
          System.out.println(segTree.query(0, 2));//查询区间为-2 + 0 + 3
          System.out.println(segTree.query(0, 5));//查询区间为nums全加起来
      }

}

运行结果:

leetcode上的线段树相关问题

leetcode303题.区域和检索-数组不可变

303. 区域和检索 - 数组不可变 - 力扣(LeetCode)

这里的不可变指的是不涉及线段树的更新操作,什么是线段树的更新操作我们一会儿会讲。

给定一个整数数组  nums,处理以下类型的多个查询:

计算索引 left 和 right (包含 left 和 right)之间的 nums 元素的  ,其中 left <= right

实现 NumArray 类:

  • NumArray(int[] nums) 使用数组 nums 初始化对象
  • int sumRange(int i, int j) 返回数组 nums 中索引 left 和 right 之间的元素的 总和 ,包含 left 和 right 两点(也就是 nums[left] + nums[left + 1] + ... + nums[right] )

示例 1:

输入:
["NumArray", "sumRange", "sumRange", "sumRange"]
[[[-2, 0, 3, -5, 2, -1]], [0, 2], [2, 5], [0, 5]]
输出:
[null, 1, -1, -3]

解释:
NumArray numArray = new NumArray([-2, 0, 3, -5, 2, -1]);
numArray.sumRange(0, 2); // return 1 ((-2) + 0 + 3)
numArray.sumRange(2, 5); // return -1 (3 + (-5) + 2 + (-1)) 
numArray.sumRange(0, 5); // return -3 ((-2) + 0 + 3 + (-5) + 2 + (-1))

提示:

  • 1 <= nums.length <= 10^4
  • -105 <= nums[i] <= 10^5
  • 0 <= i <= j < nums.length
  • 最多调用 10^4次 sumRange 方法

使用线段树解题


class NumArray {
    private int[] tree;
    private int[] data;
    private int left(int idx){
        return 2*idx+1;
    }
    private int right(int idx){
        return 2*idx+2;
    }
    private void buildSegmentTree(int treeIdx,int l,int r){
        if(l==r){
            tree[treeIdx]=data[l];
            return;
        }
        int mid=l+(r-l)/2;
        int leftTreeIndex=left(treeIdx);
        int rightTreeIndex=right(treeIdx);
        buildSegmentTree(leftTreeIndex,l,mid);
        buildSegmentTree(rightTreeIndex,mid+1,r);
        tree[treeIdx]=tree[leftTreeIndex]+tree[rightTreeIndex];
    }
    public NumArray(int[] nums) {
        data=new int[nums.length];
        for (int i = 0; i < nums.length; i++) {
            data[i]=nums[i];
        }
        tree=new int[nums.length*4];
        buildSegmentTree(0,0,data.length-1);
    }
    private int query(int idx,int l,int r,int qL,int qR){
        if(l==qL&&r==qR)return tree[idx];
        int mid=l+(r-l)/2;
        int leftTree=left(idx);
        int rightTree=right(idx);
        if(qL>=mid+1)return query(rightTree,mid+1,r,qL,qR);
        if(qR<=mid)return query(leftTree,l,mid,qL,qR);
        int leftRes=query(leftTree,l,mid,qL,mid);
        int rightRes=query(rightTree,mid+1,r,mid+1,qR);
        return leftRes+rightRes;
    }
    public int sumRange(int left, int right) {
        if(left<0||left>=data.length||right<0||right>=data.length||left>right)
            throw new IllegalArgumentException("Idx is illegal.");
        return query(0,0,data.length-1,left,right);
    }
}

不使用线段树解题

//进行预处理
class NumArray {
    //sum[i]存储前i个元素和,sum[0] = 0
    //sum[i]存储nums[0...i - 1]的和
    private int[]sum;
    public NumArray(int[] nums) {
        //因为sum[0]存储的不是第一个元素的值,只是一个数字0,sum[1]才是第一个元素的值,所以有一个偏移量
        sum = new int[nums.length + 1];
        sum[0] = 0;
        for(int left = 1; left < sum.length; left++){
            sum[left] = sum[left - 1] + nums[left - 1];
        }
    }
    
    public int sumRange(int left, int right) {
        //从0到right元素的和减去从0到left - 1对应的和
        return sum[right + 1] - sum[left];
    }
}

这么一看,好像不用线段树的方法更方便哎,那我们干嘛还用线段树?题目一开头我们说了,这道题不涉及线段树的更新操作,线段树更适合解决动态的情况,这道题所有的数值都是固定的、静态的,所以不用使用线段树这么复杂的数据结构就能解决。

让我们再来一道题作为静态的对比。

leetcode307题.区域和检索-数组可修改

307. 区域和检索 - 数组可修改 - 力扣(LeetCode)

给你一个数组 nums ,请你完成两类查询。

  1. 其中一类查询要求 更新 数组 nums 下标对应的值
  2. 另一类查询要求返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的  ,其中 left <= right

实现 NumArray 类:

  • NumArray(int[] nums) 用整数数组 nums 初始化对象
  • void update(int index, int val) 将 nums[index] 的值 更新 为 val
  • int sumRange(int left, int right) 返回数组 nums 中索引 left 和索引 right 之间( 包含 )的nums元素的  (即,nums[left] + nums[left + 1], ...,nums[right]

示例 1:

输入:
["NumArray", "sumRange", "update", "sumRange"]
[[[1, 3, 5]], [0, 2], [1, 2], [0, 2]]
输出:
[null, 9, null, 8]

解释:
NumArray numArray = new NumArray([1, 3, 5]);
numArray.sumRange(0, 2); // 返回 1 + 3 + 5 = 9
numArray.update(1, 2);   // nums = [1,2,5]
numArray.sumRange(0, 2); // 返回 1 + 2 + 5 = 8

提示:

  • 1 <= nums.length <= 3 * 10^4 
  • -100 <= nums[i] <= 100
  • 0 <= index < nums.length
  • -100 <= val <= 100
  • 0 <= left <= right < nums.length
  • 调用 update 和 sumRange 方法次数不大于 3 * 10^4 

我们可以看到这道题和303题唯一的区别就是多了一个update的更新操作,我们先用非线段树方法来试一试。

不使用线段树解题

//进行预处理
class NumArray {
    //sum[i]存储前i个元素和,sum[0] = 0
    //sum[i]存储nums[0...i - 1]的和
    private int[]sum;
    private int[]data;
    public NumArray(int[] nums) {
        data = new int[nums.length];
        for(int i = 0; i < nums.length; i++){
            data[i] = nums[i];
        }
        //因为sum[0]存储的不是第一个元素的值,只是一个数字0,sum[1]才是第一个元素的值,所以有一个偏移量
        sum = new int[nums.length + 1];
        sum[0] = 0;
        for(int left = 1; left < sum.length; left++){
            sum[left] = sum[left - 1] + nums[left - 1];
        }
    }
    public void update(int index, int val) {
    data[index] = val;
    for(int left = index + 1; left < sum.length; left++){
        sum[left] = sum[left - 1] + data[left - 1];
    }
}

    
    public int sumRange(int left, int right) {
        //从0到right元素的和减去从0到left - 1对应的和
        return sum[right + 1] - sum[left];
    }
}

我们可以看到,非线段树的方法只通过了12/16个样例,样例再大一点就超出运行时间了。 究其根本就是运行中存在大量的时间复杂度为O(n)的update操作,整体时间复杂度就是m * n 级别,是比较慢的。此时我们的数组就需要动态的改变了,线段树这种数据结构就要发挥作用了,接下来我们就要在我们的线段树中添加上update的操作,然后进一步解决307号这个问题。(线段树方法的题解放后文)

线段树中的更新操作

以下代码可以加入我们之前实现的线段树的基本操作。

//将index位置的值更新为e
    public void set(int index, E e){
        if(index < 0 || index >= data.length){
            throw new IllegalArgumentException("Index is illegal");
        }
        data[index] = e;
        //递归
        set(0, 0, data.length - 1, index, e);
    }
    private void set(int treeIndex, int l, int r, int index, E e){
        if(l == r){
            tree[treeIndex] = e;
            return;
        }
        int mid = l + (r - l) / 2;
        int leftTreeIndex = leftChild(treeIndex);
        int rightTreeIndex = rightChild(treeIndex);
        if(index >= mid + 1){
            set(rightTreeIndex, mid + 1, r, index, e);
        }
        else{
            set(leftTreeIndex, l, mid, index, e);
        }
        tree[treeIndex] = merger.merge(tree[leftTreeIndex], tree[rightTreeIndex]);
    }

学会了线段树的更新操作后,我们就可以回过头来去解决307号问题的线段树解法。

使用线段树解题

class NumArray {
    class TreeNode{
        public int sum;
        public int start, end;
        public TreeNode left, right;
        public TreeNode(int start, int end){
            this.start = start;
            this.end   = end;
        }
    }
    TreeNode root = null;
    public NumArray(int[] nums) {
        root = buildTree(nums, 0, nums.length - 1);
    }
    
    public void update(int index, int val) {
        update(root, index, val);
    }
    
    public int sumRange(int left, int right) {
        return query(root, left, right);
    }
    
    private int query(TreeNode root, int left, int right){
        if(root.start == left && root.end == right)
            return root.sum;
        else{
            int mid = root.start + (root.end - root.start) / 2;
            
            if(right <= mid)
                return query(root.left, left, right);
            else if(left > mid)
                return query(root.right, left, right);
            else
                return query(root.left, left, mid) + query(root.right, mid + 1, right);
        }
    }
    
    private void update(TreeNode root, int index, int val){
        if(root.start == root.end){
            root.sum = val;
            return;
        }else{
            int mid = root.start + (root.end - root.start) / 2;
            
            if(index <= mid)
                update(root.left, index, val);
            else 
                update(root.right, index, val);
            
            root.sum = root.left.sum + root.right.sum;
        }
        
    }
    
    private TreeNode buildTree(int[] nums, int start, int end){
        if(start > end)
            return null;
        else if(start == end){
            TreeNode node = new TreeNode(start, end);
            node.sum      = nums[start];
            return node;
        }else{
            TreeNode node = new TreeNode(start, end);
            int mid = start + (end - start) / 2;
            
            node.left = buildTree(nums, start, mid);
            node.right = buildTree(nums, mid + 1, end);
            
            node.sum  = node.left.sum + node.right.sum;
            return node;
        }
            
    }
}

更多线段树相关的话题

我们点了一下线段树的标签,发现leetcode上关于线段树的问题还挺难的。如果你不去参加算法竞赛的话,线段树不是一个重点,请合理安排自己的时间。

当我们赋予线段树合理的意义后,我们可以非常高效的处理和线段或者区间相关的问题。

我们实现的三个方法:创建线段树、查询线段树和更新线段树都采用了递归的写法。

懒惰更新

我们之前的更新操作都是对线段树某个结点存储的值进行的更新,但是如果我们想对区间进行更新呢?

我们可以使用一个lazy数组记录未更新的内容,大家有个印象就好,如果感兴趣可以自己去查阅资料学习。

二维线段树

我们之前接触的都是上图所示的一维线段树,在一个坐标轴中的。可以分为前半段作为左孩子,右半段作为右孩子。

如果我们扩展到二维呢?

我们可以记录的是一个矩阵的内容,然后我们可以把矩阵分成四块,就可以有四个孩子,每个孩子就是一个更小的矩阵,直到在叶子结点的时候就只剩下一个元素,这就是二维线段树。

以此类推,我们还可以有三维线段树,那我们就可以分成八块.......

线段树本身就是一个思想,我们要学会把一个大的数据单元拆分成一个个小的数据单元,递归的表示这些数据,这本身就是树这种数据结构的实质。

动态线段树

我们上文说过,从数组方式存储开辟4n的空间免不了浪费,所以我们可以用链式的方式存储。

比如如果线段树的结点数非常大,比如一亿,那我们刚开始并不着急直接创造一个4*一亿的空间,而是动态的创建,用到哪里创哪里,如下图所示:

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

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

相关文章

基于 ACK One 实现简单的跨云协同,让业务管理更高效

作者&#xff1a;庄宇 本文根据 2023 云栖大会现场分享实录整理 2 年前的云栖大会&#xff0c;我们发布分布式云容器平台 ACK One&#xff0c;随着 2 年的发展&#xff0c;很高兴看到 ACK One 在混合云&#xff0c;分布式云领域帮助到越来越多的客户&#xff0c;今天给大家汇报…

Burnside 引理 与 Pólya 定理 学习笔记

为了防止明天就把好不容易听完的东西都还给 rabbit_lb 了&#xff0c;还是记一点吧。 1. 群论基础 1.1 群(group) 的定义 给定集合 G G G 和 G G G上的二元运算 ⋅ \cdot ⋅&#xff0c;满足下列条件称之为群&#xff1a; 封闭性&#xff1a;若 a , b ∈ G a,b\in G a,…

机器学习之实验过程01

import pandas as pd import numpy as np import matplotlib.pyplot as plt data_path = /home/py/Work/labs/data/SD.csv # 请确保您的数据文件路径是正确的 df = pd.read_csv(data_path) df.head() # 创建散点图 # 创建散点图 plt.figure(figsize=(10, 6)) plt.scat…

DDD领域驱动设计系列-原理篇-战术设计

概述 上篇战略设计产出了领域及问题域领域模型&#xff1b;详见&#xff1a;DDD领域驱动设计系列-原理篇-战略设计-CSDN博客 战术设计篇聚焦如何落地&#xff0c;包含实际解决方案模型落地&#xff0c;架构分层&#xff08;Clean&#xff0c;CQRS&#xff09;&#xff0c;Rep…

基于Qt之QChart 图表(优美的曲线图案例)

## 项目演示 平台:ubuntu18.04 Qt版本:QT5.14.2 源码位置GitCode:https://gitcode.com/m0_45463480/QCharts/tree/main ## QChart 图表 自从 Qt 发布以来,给跨平台的用户带来很多便利。在 Qt5.7 之前,Qt 在开源社区版本里没有 Qt Charts(自带的绘图组件库)。这使得像…

蓝桥杯2020年10月青少组Python程序设计省赛真题

1、设计一个猜字母的程序,程序随机给出26个小写字母中的一个,答题者输入猜测的字母,若输入的不是26个小写字母之一,让用户重新输入,若字母在答案之前或之后,程序给出相应正确提示,如答错5次,则答题失败并退出游戏,若回答正确,程序输出回答次数并退出游戏。 2、试编一个“口…

收到口头offer千万别飘,离发书面offer还有很远!4个口头offer都没通过,血的教训!...

求职时一路过关斩将&#xff0c;终于收到了口头offer&#xff0c;算是稳了吗&#xff1f; 一位打工人给广大求职者一个建议&#xff1a; 当你收到口头offer时千万别飘&#xff01;因为离最后收到书面offer有很长距离&#xff0c;还有下面这些事要做&#xff1a; 1.提交各种材料…

算法模板之队列图文详解

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;算法模板、数据结构 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 &#x1f4cb;前言一. ⛳️模拟队列1.1 &#x1f514;用数组模拟实现队列1.1.1 &#x1f47b;队列的定…

第26关 K8s日志收集揭秘:利用Log-pilot收集POD内业务日志文件

------> 课程视频同步分享在今日头条和B站 大家好&#xff0c;我是博哥爱运维。 OK&#xff0c;到目前为止&#xff0c;我们的服务顺利容器化并上了K8s&#xff0c;同时也能通过外部网络进行请求访问&#xff0c;相关的服务数据也能进行持久化存储了&#xff0c;那么接下来…

集简云与语聚AI新增Google Gemini、Gemini Vision两大模型,让对话能力再升级

近日&#xff0c;人工智能又掀起一股热潮&#xff0c;Google最新推出的AI大模型Gemini备受瞩目。号称多模态任务处理能力首次超越人类的 AI 模型&#xff0c;不仅可以处理文本内容&#xff0c;还可以无缝丝滑地处理代码、音频、图像和视频等多种模态的信息。 随着Gemini pro版…

Python patchworklib任意合并子图,多图形混合排版

【背景】 数据展示时&#xff0c;在同一页面上混合排版多个图形是一种常见的用法。本次分享一个Python轮子patchworklib库&#xff1a;通过|、/轻松实现图形排列&#xff1b;比matplotlib、seaborn等自带子图功能更加灵活; 【patchworklib简介】 Patchworklib 是与 matplotl…

IDEA中使用数据库可视化操作工具

文章目录 1.入门介绍2. 没有数据库驱动3. 准备&测试连接3.1测试报错 4.连接5.编写SQL语句 1.入门介绍 在IDEA的专业版的右侧工具栏应该会有DataBase按钮如果没有的同学可以这样操作(必须是IDEA专业版) 新建数据库 2. 没有数据库驱动 如果提示: missing driver files ,…

qt项目-《图像标注软件》源码阅读笔记-类图

目录 1. 开源项目链接 2. 项目界面 3. 项目类图 3.1 形状的绘制及形状的存储 3.2 主窗口中心组件的界面管理 3.3 Command负责实现撤销和重做功能 3.4 其他类 3.5 枚举 3.5.1 Status 主窗口的状态变量 3.5.2 Mode 主窗口模式状态变量 3.5.3 shapeStatus 中心组件状态…

前菜---二叉树+堆的小练习

目录 前言&#x1f3dc;️ 1. 二叉树性质总结⛱️ 1.2 性质3⏰ 2. 二叉树性质小练习&#x1f3d5;️ 3. 答案解析&#x1f4a1; 4. 堆概念结构小练习&#x1fa94; 5. 答案解析&#x1f9ff; 6. 前/中/后/层序遍历小练习&#x1f52b; 7. 答案解析&#x1f9fa; 后语…

MySQL主从架构及读写分离实战

​​​​​​ 目录 一、实验目的与环境 二、基础环境介绍 三、搭建主从集群 1、理论基础 2、同步的原理 3、搭建主从集群 3.1 配置master主服务器 3.2 配置slave从服务 3.3 主从集群测试 3.4 集群搭建扩展&#xff1a; 3.5、GTID同步集群 4、集群扩容 5、半同步复…

Nessus详细安装-windows (保姆级教程)

Nessus描述 Nessus 是一款广泛使用的网络漏洞扫描工具。它由 Tenable Network Security 公司开发&#xff0c;旨在帮助组织评估其计算机系统和网络的安全性。 Nessus 可以执行自动化的漏洞扫描&#xff0c;通过扫描目标系统、识别和评估可能存在的安全漏洞和弱点。它可以检测…

【数据结构入门精讲 | 第十七篇】一文讲清图及各类图算法

在上一篇中我们进行了的并查集相关练习&#xff0c;在这一篇中我们将学习图的知识点。 目录 概念深度优先DFS伪代码 广度优先BFS伪代码 最短路径算法&#xff08;Dijkstra&#xff09;伪代码 Floyd算法拓扑排序逆拓扑排序 概念 下面介绍几种在对图操作时常用的算法。 深度优先D…

【数据结构入门精讲 | 第十四篇】散列表知识点及考研408、企业面试练习(1)

在上一篇中我们进行了树的专项练习&#xff0c;在这一篇中我们将进行散列表知识点的学习。 目录 概念伪代码线性探测法平方探测法查找成功的平均查找长度查找失败的平均查找长度判断题选择题 概念 散列表&#xff08;Hash Table&#xff09;&#xff0c;也被称为哈希表或散列映…

CogAgent:带 Agent 能力的视觉模型来了

之前我们分享过智谱AI新一代多模态大模型 CogVLM&#xff0c;该模型在不牺牲任何 NLP 任务性能的情况下&#xff0c;实现视觉语言特征的深度融合&#xff0c;其中 CogVLM-17B 在 14 个多模态数据集上取得最好或者第二名的成绩。 12月15日&#xff0c;基于 CogVLM&#xff0c;提…

【小沐学写作】Docsify制作在线电子书、技术文档(Docsify + Markdown + node)

文章目录 1、简介2、安装2.1 node2.2 docsify-cli 3、配置3.1 初始化3.2 预览效果3.3 加载对话框3.4 更多页面3.5 侧 栏3.6 自定义导航栏 结语 1、简介 https://docsify.js.org/#/?iddocsify 一个神奇的文档网站生成器。 简单轻巧没有静态构建的 html 文件多个主题 Docsify…