In-depth understanding of line segment trees | JD Logistics Technical Team

Segment Tree is a commonly used data structure for maintaining interval information . It can implement single point modification, interval modification, and interval query (interval summation, interval maximum value or interval minimum value) at a time complexity of O(logn) ) and other operations are often used to solve RMQ problems.

The RMQ (Range Minimum/Maximum Query) question refers to: for a sequence A of length n, answer several queries RMQ(A, i, j) where i, j <= n, return the subscript in the sequence A in i, j The minimum (largest) value of . In other words: the RMQ problem refers to the problem of finding the maximum value of the interval. Usually the solutions to this type of problem include recursive divide and conquer , dynamic programming , line segment tree and monotonic stack/monotone queue .

I wrote this content intermittently for two weeks. As my understanding of the line segment tree continued to deepen through practice, I gradually learned it and did not find it difficult. More experience is that practice makes perfect, although it does seem to have a larger amount of code at first. , but I think it’s not difficult as long as everyone keeps a calm mind and masters the three parts below step by step.

1. Line segment tree

The line segment tree will divide each interval whose length is not 1 into two left and right intervals for recursive solution, and obtain the information of the current interval by merging the information of the left and right intervals.

For example, we convert an array nums = {10, 11, 12, 13, 14} of size 5 into a line segment tree, and specify that the root node number of the line segment tree is 1. Use the array tree[] to save the nodes of the line segment tree. tree[i] represents the node numbered i on the line segment tree, as shown below:

image.png

Each node in the diagram shows the interval sum and interval range. The left subtree node of tree[i] is tree[2i], and the right subtree node is tree[2i + 1]. If the interval recorded by tree[i] is [a, b], then the interval recorded by the left subtree node is [a, mid], and the interval recorded by the right subtree node is [mid + 1, b], where mid = (a + b) / 2.

Now that we have a basic understanding of line segment trees, let's look at the code implementation of interval query and single point modification .

Interval query and single point modification of line segment tree

First, we define the nodes of the line segment tree:

    /**
     * 定义线段树节点
     */
    class Node {
        /**
         * 区间和 或 区间最大/最小值
         */
        int val;

        int left;

        int right;

        public Node(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }

Note that the val field stores the sum of the intervals. After defining the nodes of the tree, let's take a look at the logic of building the tree. Pay attention to the comments in the code. The size of the node array we allocate for the segment tree is 4 times the size of the original array. This is the worst case scenario when the array is converted into a full binary tree. .

    public SegmentTree(int[] nums) {
        this.nums = nums;
        tree = new Node[nums.length * 4];
        // 建树,注意表示区间时使用的是从 1 开始的索引值
        build(1, 1, nums.length);
    }

    /**
     * 建树
     *
     * @param pos   当前节点编号
     * @param left  当前节点区间下界
     * @param right 当前节点区间上界
     */
    private void build(int pos, int left, int right) {
        // 创建节点
        tree[pos] = new Node(left, right);
        // 递归结束条件
        if (left == right) {
            // 赋值
            tree[pos].val = nums[left - 1];
            return;
        }

        // 如果没有到根节点,则继续递归
        int mid = left + right >> 1;
        build(pos << 1, left, mid);
        build(pos << 1 | 1, mid + 1, right);

        // 当前节点的值是左子树和右子树节点的和
        pushUp(pos);
    }

    /**
     * 用于向上回溯时修改父节点的值
     */
    private void pushUp(int pos) {
        tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
    }

When we build the tree, the representation interval does not start from index 0, but from index 1, so as to ensure that when calculating the left subtree node index, it is 2i, and the right subtree node index is 2i + 1.

build()When the method is executed, we will first create a node at the corresponding position without assigning a value. We will only assign a value when recursing to the leaf node. At this time, the interval size is 1, and the node value is the value of the current interval. Afterwards, the non-leaf node values ​​are calculated by pushUp()backtracking and adding the values ​​of the two child nodes of the current node.

Next, let’s look at modifying the values ​​in the interval and how the line segment tree updates the values. Pay attention to the comments:

    /**
     * 修改单节点的值
     *
     * @param pos    当前节点编号
     * @param numPos 需要修改的区间中值的位置
     * @param val    修改后的值
     */
    private void update(int pos, int numPos, int val) {
        // 找到该数值所在线段树中的叶子节点
        if (tree[pos].left == numPos && tree[pos].right == numPos) {
            tree[pos].val = val;
            return;
        }
        // 如果不是当前节点那么需要判断是去左或右去找
        int mid = tree[pos].left + tree[pos].right >> 1;
        if (numPos <= mid) {
            update(pos << 1, numPos, val);
        } else {
            update(pos << 1 | 1, numPos, val);
        }

        // 叶子节点的值修改完了,需要回溯更新所有相关父节点的值
        pushUp(pos);
    }

The modification method is relatively simple. When the leaf node values ​​are updated, we still need to call pushUp()the method to update the values ​​of all relevant parent nodes.

Next we look at the method of finding the corresponding interval sum:

    /**
     * 查找对应区间的值
     *
     * @param pos   当前节点
     * @param left  要查询的区间的下界
     * @param right 要查询的区间的上界
     */
    private int query(int pos, int left, int right) {
        // 如果我们要查找的区间把当前节点区间全部包含起来
        if (left <= tree[pos].left && tree[pos].right <= right) {
            return tree[pos].val;
        }

        int res = 0;
        int mid = tree[pos].left + tree[pos].right >> 1;
        // 根据区间范围去左右节点分别查找求和
        if (left <= mid) {
            res += query(pos << 1, left, right);
        }
        if (right > mid) {
            res += query(pos << 1 | 1, left, right);
        }
        
        return res;
    }

This method is also relatively simple. It needs to determine whether the interval range requires separate searches and calculations for the left child node and the right child node.

Now that the line segment tree representing the interval sum has been explained, in order to facilitate everyone to learn and read the code, I posted the full code here:

public class SegmentTree {

    /**
     * 定义线段树节点
     */
    static class Node {
        /**
         * 区间和 或 区间最大/最小值
         */
        int val;

        int left;

        int right;

        public Node(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }

    Node[] tree;

    int[] nums;

    public SegmentTree(int[] nums) {
        this.nums = nums;
        tree = new Node[nums.length * 4];
        // 建树,注意表示区间时使用的是从 1 开始的索引值
        build(1, 1, nums.length);
    }

    /**
     * 建树
     *
     * @param pos   当前节点编号
     * @param left  当前节点区间下界
     * @param right 当前节点区间上界
     */
    private void build(int pos, int left, int right) {
        // 创建节点
        tree[pos] = new Node(left, right);
        // 递归结束条件
        if (left == right) {
            // 赋值
            tree[pos].val = nums[left - 1];
            return;
        }

        // 如果没有到根节点,则继续递归
        int mid = left + right >> 1;
        build(pos << 1, left, mid);
        build(pos << 1 | 1, mid + 1, right);

        // 当前节点的值是左子树和右子树节点的和
        pushUp(pos);
    }

    /**
     * 修改单节点的值
     *
     * @param pos    当前节点编号
     * @param numPos 需要修改的区间中值的位置
     * @param val    修改后的值
     */
    private void update(int pos, int numPos, int val) {
        // 找到该数值所在线段树种的叶子节点
        if (tree[pos].left == numPos && tree[pos].right == numPos) {
            tree[pos].val = val;
            return;
        }
        // 如果不是当前节点那么需要判断是去左或右去找
        int mid = tree[pos].left + tree[pos].right >> 1;
        if (numPos <= mid) {
            update(pos << 1, numPos, val);
        } else {
            update(pos << 1 | 1, numPos, val);
        }

        // 叶子节点的值修改完了,需要回溯更新所有相关父节点的值
        pushUp(pos);
    }

    /**
     * 用于向上回溯时修改父节点的值
     */
    private void pushUp(int pos) {
        tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
    }

    /**
     * 查找对应区间的值
     *
     * @param pos   当前节点
     * @param left  要查询的区间的下界
     * @param right 要查询的区间的上界
     */
    private int query(int pos, int left, int right) {
        // 如果我们要查找的区间把当前节点区间全部包含起来
        if (left <= tree[pos].left && tree[pos].right <= right) {
            return tree[pos].val;
        }

        int res = 0;
        int mid = tree[pos].left + tree[pos].right >> 1;
        // 根据区间范围去左右节点分别查找求和
        if (left <= mid) {
            res += query(pos << 1, left, right);
        }
        if (right > mid) {
            res += query(pos << 1 | 1, left, right);
        }
        
        return res;
    }
}

If you want to create a line segment tree that represents the maximum or minimum value of an interval, the logic of building the tree remains unchanged. You only need to modify the **pushUp() method and query()** method to the logic of calculating the maximum or minimum value.

2. Interval modification and lazy marking of line segment trees

If we not only modify a single point, but also modify the interval, then when modifying the interval, we need to modify the current interval value and the sub-interval value containing the current interval. The overhead generated by this is unacceptable, so Here we will use a lazy markup approach to help us avoid this immediate overhead .

Simply put, lazy marking delays changes to node information, thereby reducing the number of potentially unnecessary operations. Each time a modification is performed, we use a marking method to indicate that the interval corresponding to the node has been changed in a certain operation, but the information of the node's child nodes is not updated. Substantial modifications will only be made the next time "upcoming access (update or query)" occurs to a child node of a node with a lazy mark.

We record the lazy mark by adding the add field in the node class, which indicates the "change size" of the sub-interval value of the interval (must be understood well), and is "accumulated" to the two sub-intervals of the current interval through the pushDown method. in the node interval value .

As long as the sub-interval of the current interval is not accessed, the sub-interval value will never change. The amount of change equivalent to the sub-interval value is "held" by the current node through the add field.

The pushDown method is different from the pushUp method we mentioned above. The former synchronizes the accumulated lazy mark value of the current node value to the child node, while the latter retroactively modifies the parent node value of the current child node after completing the modification of the child node. We You can better understand the direction and modification range of these two methods based on Down and Up.

Let's take a look at the process and specific code. The node class is as follows, and the add field is added:

    static class Node {
        int left;

        int right;

        int val;

        int add;

        public Node(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }

Interval modification

The process of building a tree is consistent with what we mentioned above, so we won’t go into details here. We mainly focus on the process of interval modification, taking the following initial line segment tree as an example. At this time, the add of each node is 0:

image.png

Next, we modify the interval [3, 5] and the change of each value in the interval is 1. The process is as follows:

First traverse node 1 and find that the interval [3, 5] cannot completely include the interval [1, 5] . No modification is made and continue to traverse node 2. Node 2 is still not included in the interval [3, 5]. You need to continue traversing node 5 and find that the node is completely included in the interval. Modify it and add a lazy mark value, as shown in the following figure:

image.png

After completing this step, you need to backtrack and modify the value of the tree[2] node:

image.png

Now 3 in the interval [3, 5] has been modified, and 4, 5 have not been modified. We need to continue the recursive search in the right subtree and find that the interval [3, 5] in tree[3] is the interval we want to modify. ] is completely included, then this node needs to be modified and marked lazily, as follows. Note that although the tree[3] node has two child nodes, because we have not accessed its child nodes, there is no need to synchronize the add value to each child node. :

image.png

Similarly, completing this step also requires backtracking to modify the value of the parent node:

image.png

So far our interval modification has been completed. According to this process, the code example is as follows:

    /**
     * 修改区间的值
     *
     * @param pos   当前节点编号
     * @param left  要修改区间的下界
     * @param right 要修改区间的上界
     * @param val   区间内每个值的变化量
     */
    public void update(int pos, int left, int right, int val) {
        // 如果该区间被要修改的区间包围的话,那么需要将该区间所有的值都修改
        if (left <= tree[pos].left && tree[pos].right <= right) {
            tree[pos].val += (tree[pos].right - tree[pos].left + 1) * val;
            // 懒惰标记
            tree[pos].add += val;
            return;
        }

        // 该区间没有被包围的话,需要修改节点的信息
        pushDown(pos);

        int mid = tree[pos].left + tree[pos].right >> 1;
        // 如果下界在 mid 左边,那么左子树需要修改
        if (left <= mid) {
            update(pos << 1, left, right, val);
        }
        // 如果上界在 mid 右边,那么右子树也需要修改
        if (right > mid) {
            update(pos << 1 | 1, left, right, val);
        }
        // 修改完成后向上回溯修改父节点的值
        pushUp(pos);
    }

    private void pushDown(int pos) {
        // 根节点 和 懒惰标记为 0 的情况不需要再向下遍历
        if (tree[pos].left != tree[pos].right && tree[pos].add != 0) {
            int add = tree[pos].add;
            // 计算累加变化量
            tree[pos << 1].val += add * (tree[pos << 1].right - tree[pos << 1].left + 1);
            tree[pos << 1 | 1].val += add * (tree[pos << 1 | 1].right - tree[pos << 1 | 1].left + 1);

            // 子节点懒惰标记累加
            tree[pos << 1].add += add;
            tree[pos << 1 | 1].add += add;

            // 懒惰标记清 0
            tree[pos].add = 0;
        }
    }

    private void pushUp(int pos) {
        tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
    }

Interval query

The tree[3] node has a lazy mark 1. If we query the value of the interval [5, 5] at this time, we need to perform pushDown lazy mark calculation when recursively passing through the tree[3] node, and add tree[6] and The node value of tree[7] is modified, and the result is as follows:

image.png

Finally, we will get the result value as 15. The sample code of the interval query process is as follows:

    public int query(int pos, int left, int right) {
        if (left <= tree[pos].left && tree[pos].right <= right) {
            // 当前区间被包围
            return tree[pos].val;
        }

        // 懒惰标记需要下传修改子节点的值
        pushDown(pos);

        int res = 0;
        int mid = tree[pos].left + tree[pos].right >> 1;
        if (left <= mid) {
            res += query(pos << 1, left, right);
        }
        if (right > mid) {
            res += query(pos << 1 | 1, left, right);
        }

        return res;
    }

Similarly, in order to facilitate everyone's learning, I have also listed the full code. I think the most important point in learning the interval modification of the line segment tree is to understand the meaning of the add field and the timing of the pushDown method. It is also necessary to note that only certain areas in the line segment tree are When each interval is fully included by the interval we want to modify (conditional judgment of the update and query methods), the value is modified and marked lazily. Otherwise, the interval value is only modified when the pushUp method backtracks.

public class SegmentTree2 {

    static class Node {
        int left;

        int right;

        int val;

        int add;

        public Node(int left, int right) {
            this.left = left;
            this.right = right;
        }
    }

    Node[] tree;

    int[] nums;

    public SegmentTree2(int[] nums) {
        this.tree = new Node[nums.length * 4];
        this.nums = nums;

        build(1, 1, nums.length);
    }

    private void build(int pos, int left, int right) {
        tree[pos] = new Node(left, right);
        // 递归结束条件
        if (left == right) {
            tree[pos].val = nums[left - 1];
            return;
        }

        int mid = left + right >> 1;
        build(pos << 1, left, mid);
        build(pos << 1 | 1, mid + 1, right);

        // 回溯修改父节点的值
        pushUp(pos);
    }

    /**
     * 修改区间的值
     *
     * @param pos   当前节点编号
     * @param left  要修改区间的下界
     * @param right 要修改区间的上界
     * @param val   区间内每个值的变化量
     */
    public void update(int pos, int left, int right, int val) {
        // 如果该区间被要修改的区间包围的话,那么需要将该区间所有的值都修改
        if (left <= tree[pos].left && tree[pos].right <= right) {
            tree[pos].val += (tree[pos].right - tree[pos].left + 1) * val;
            // 懒惰标记
            tree[pos].add += val;
            return;
        }

        // 该区间没有被包围的话,需要修改节点的信息
        pushDown(pos);

        int mid = tree[pos].left + tree[pos].right >> 1;
        // 如果下界在 mid 左边,那么左子树需要修改
        if (left <= mid) {
            update(pos << 1, left, right, val);
        }
        // 如果上界在 mid 右边,那么右子树也需要修改
        if (right > mid) {
            update(pos << 1 | 1, left, right, val);
        }
        // 修改完成后向上回溯修改父节点的值
        pushUp(pos);
    }

    public int query(int pos, int left, int right) {
        if (left <= tree[pos].left && tree[pos].right <= right) {
            // 当前区间被包围
            return tree[pos].val;
        }

        // 懒惰标记需要下传修改子节点的值
        pushDown(pos);

        int res = 0;
        int mid = tree[pos].left + tree[pos].right >> 1;
        if (left <= mid) {
            res += query(pos << 1, left, right);
        }
        if (right > mid) {
            res += query(pos << 1 | 1, left, right);
        }

        return res;
    }

    private void pushDown(int pos) {
        // 根节点 和 懒惰标记为 0 的情况不需要再向下遍历
        if (tree[pos].left != tree[pos].right && tree[pos].add != 0) {
            int add = tree[pos].add;
            // 计算累加变化量
            tree[pos << 1].val += add * (tree[pos << 1].right - tree[pos << 1].left + 1);
            tree[pos << 1 | 1].val += add * (tree[pos << 1 | 1].right - tree[pos << 1 | 1].left + 1);

            // 子节点懒惰标记
            tree[pos << 1].add += add;
            tree[pos << 1 | 1].add += add;

            // 懒惰标记清 0
            tree[pos].add = 0;
        }
    }

    private void pushUp(int pos) {
        tree[pos].val = tree[pos << 1].val + tree[pos << 1 | 1].val;
    }
}

3. Dynamic opening point of line segment tree

The dynamic opening point of the line segment tree is actually not difficult to understand. It is different from the direct creation of all the nodes of the line segment tree as mentioned above. The dynamic opening point of the line segment tree initially creates only one root node to represent the entire interval, and other nodes are only created when needed. , saving space. Of course, we can no longer use pos << 1and pos << 1 | 1to find the left and right child nodes of the current node. Instead, we use left and right in the node to record the positions of the left and right child nodes in tree[]. This needs to be noted:

    static class Node {

        // left 和 right 不再表示区间范围而是表示左右子节点在 tree 中的索引位置
        int left, right;

        int val;

        int add;
    }

Let's take the interval [1, 5] as an example. The process of creating the interval [5, 5] as 14 is as follows:

image.png

We can find that the default root node tree[1] will be created first, and then the tree[2] and tree[3] nodes in the above figure will be created. However, the interval [5, 5] is not found at this time, so we need to continue creating it. The tree[4] and tree[5] nodes in the above figure (different from directly creating all nodes, if all nodes are directly created, their positions should be in tree[6] and tree[7]), now tree[ 5] The interval represented by the node meets the conditions we are looking for, and we can perform assignment and pushUp operations. Compared with directly creating all nodes, dynamically opening points creates 4 fewer nodes, which are the four nodes marked red in the figure. is not created.

Since each operation may create and access a new series of nodes, the space complexity of the nodes after m single-point operations is O(mlogn). If we use line segment trees to dynamically open points to solve the problem, the space must be opened as much as possible. It may be large, Java can be opened to more than 5e6 nodes at 128M .

Based on the diagram, you should be able to understand the process of dynamically opening points~~ (If you don’t understand, just draw it yourself)~~, let’s take a look at the specific code:

    /**
     * 修改区间的值
     *
     * @param pos   当前节点的索引值
     * @param left  当前线段树节点表示的范围下界
     * @param right 当前线段树节点表示的范围上界
     * @param l     要修改的区间下界
     * @param r     要修改的区间上界
     * @param val   区间值变化的大小
     */
    public void update(int pos, int left, int right, int l, int r, int val) {
        // 当前区间被要修改的区间全部包含
        if (l <= left && right <= r) {
            tree[pos].val += (right - left + 1) * val;
            tree[pos].add += val;
            return;
        }

        lazyCreate(pos);

        pushDown(pos, right - left + 1);

        int mid = left + right >> 1;
        if (l <= mid) {
            update(tree[pos].left, left, mid, l, r, val);
        }
        if (r > mid) {
            update(tree[pos].right, mid + 1, right, l, r, val);
        }

        pushUp(pos);
    }

    // 为该位置创建节点
    private void lazyCreate(int pos) {
        if (tree[pos] == null) {
            tree[pos] = new Node();
        }
        // 创建左子树节点
        if (tree[pos].left == 0) {
            tree[pos].left = ++count;
            tree[tree[pos].left] = new Node();
        }
        // 创建右子树节点
        if (tree[pos].right == 0) {
            tree[pos].right = ++count;
            tree[tree[pos].right] = new Node();
        }
    }

    private void pushDown(int pos, int len) {
        if (tree[pos].left != 0 && tree[pos].right != 0 && tree[pos].add != 0) {
            // 计算左右子树的值
            tree[tree[pos].left].val += (len - len / 2) * tree[pos].add;
            tree[tree[pos].right].val += len / 2 * tree[pos].add;

            // 子节点懒惰标记
            tree[tree[pos].left].add += tree[pos].add;
            tree[tree[pos].right].add += tree[pos].add;

            tree[pos].add = 0;
        }
    }

    private void pushUp(int pos) {
        tree[pos].val = tree[tree[pos].left].val + tree[tree[pos].right].val;
    }

The overall logic is not difficult. The new lazyCreate method is the logic of dynamically opening points. It should be noted that when performing interval update, the parameters of our method have left and right parameters that represent the range of the current node, because our current node Only the positions of the left and right child nodes are saved in , but there is no interval information , so we need to carry it in the parameters, otherwise we have no way to judge whether the current interval matches the interval we are looking for.

I’ll put the full code below for everyone’s convenience:

public class SegmentTree3 {

    static class Node {

        // left 和 right 不再表示区间范围而是表示左右子节点在 tree 中的索引位置
        int left, right;

        int val;

        int add;
    }

    // 记录当前节点数
    int count;

    Node[] tree;

    public SegmentTree3() {
        count = 1;
        tree = new Node[(int) 5e6];
        tree[count] = new Node();
    }

    public int query(int pos, int left, int right, int l, int r) {
        if (l <= left && right <= r) {
            return tree[pos].val;
        }

        lazyCreate(pos);

        pushDown(pos, right - left + 1);

        int res = 0;
        int mid = left + right >> 1;
        if (l <= mid) {
            res += query(tree[pos].left, left, mid, l, r);
        }
        if (r > mid) {
            res += query(tree[pos].right, mid + 1, right, l, r);
        }

        return res;
    }

    /**
     * 修改区间的值
     *
     * @param pos   当前节点的索引值
     * @param left  当前线段树节点表示的范围下界
     * @param right 当前线段树节点表示的范围上界
     * @param l     要修改的区间下界
     * @param r     要修改的区间上界
     * @param val   区间值变化的大小
     */
    public void update(int pos, int left, int right, int l, int r, int val) {
        // 当前区间被要修改的区间全部包含
        if (l <= left && right <= r) {
            tree[pos].val += (right - left + 1) * val;
            tree[pos].add += val;
            return;
        }

        lazyCreate(pos);

        pushDown(pos, right - left + 1);

        int mid = left + right >> 1;
        if (l <= mid) {
            update(tree[pos].left, left, mid, l, r, val);
        }
        if (r > mid) {
            update(tree[pos].right, mid + 1, right, l, r, val);
        }

        pushUp(pos);
    }

    // 为该位置创建节点
    private void lazyCreate(int pos) {
        if (tree[pos] == null) {
            tree[pos] = new Node();
        }
        // 创建左子树节点
        if (tree[pos].left == 0) {
            tree[pos].left = ++count;
            tree[tree[pos].left] = new Node();
        }
        // 创建右子树节点
        if (tree[pos].right == 0) {
            tree[pos].right = ++count;
            tree[tree[pos].right] = new Node();
        }
    }

    private void pushDown(int pos, int len) {
        if (tree[pos].left != 0 && tree[pos].right != 0 && tree[pos].add != 0) {
            // 计算左右子树的值
            tree[tree[pos].left].val += (len - len / 2) * tree[pos].add;
            tree[tree[pos].right].val += len / 2 * tree[pos].add;

            // 子节点懒惰标记
            tree[tree[pos].left].add += tree[pos].add;
            tree[tree[pos].right].add += tree[pos].add;

            tree[pos].add = 0;
        }
    }

    private void pushUp(int pos) {
        tree[pos].val = tree[tree[pos].left].val + tree[tree[pos].right].val;
    }
}


giant's shoulders

Author: JD Logistics Wang Yilong

Source: JD Cloud Developer Community Ziyuanqishuo Tech Please indicate the source when reprinting

The author of the open source framework NanUI switched to selling steel, and the project was suspended. The first free list in the Apple App Store is the pornographic software TypeScript. It has just become popular, why do the big guys start to abandon it? TIOBE October list: Java has the biggest decline, C# is approaching Java Rust 1.73.0 Released A man was encouraged by his AI girlfriend to assassinate the Queen of England and was sentenced to nine years in prison Qt 6.6 officially released Reuters: RISC-V technology becomes the key to the Sino-US technology war New battlefield RISC-V: Not controlled by any single company or country, Lenovo plans to launch Android PC
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10116569
Recommended