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

目录

什么是线段树

线段树基础表示

创建线段树(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;}@Overridepublic 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);}@Overridepublic 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 + 3System.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位置的值更新为epublic 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);elsereturn 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.mzph.cn/news/241736.shtml

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

相关文章

Python----静态Web服务器-返回指定页面数据

1. 静态Web服务器的问题 目前的Web服务器&#xff0c;不管用户访问什么页面&#xff0c;返回的都是固定页面的数据&#xff0c;接下来需要根据用户的请求返回指定页面的数据 返回指定页面数据的实现步骤: 获取用户请求资源的路径根据请求资源的路径&#xff0c;读取指定文件…

VUE element组件生成的全选框如何获取值

//先声明 const Selection ref([]);//获取 const handleSelectCodeForTicket (val) > {console.log(val);// values.value val;Selection.value [];val.forEach((v) > {Selection.value.push(v);});console.log(Selection.value); }; <el-table selection-change…

docker-compaose部署openldap

前段时间在本地搭建了一套gitlab geo测试环境&#xff0c;因为需要集成ldap&#xff0c;所以特意搭建下&#xff0c;特此作为笔记记录下。 文章目录 1. 前置条件2. 编写docker-openldap.yml文件3. 登录4. 使用创建组创建用户登录测试 1. 前置条件 安装docker-compose 安装docke…

毅速:3D打印随形水路已经逐步向压铸模具普及

随着科技的不断发展&#xff0c;3D打印技术已经逐渐渗透到各个领域。其中&#xff0c;3D打印随形水路在注塑模具中已经广泛应用&#xff0c;目前正逐渐向压铸模具普及。 传统CNC等减材工艺的水路制造&#xff0c;可以在模具中生产出平直的冷却水路&#xff0c;但这种工艺难以加…

FMQL开发环境搭建

FMQL开发环境搭建 一、概述 此篇记录上海复旦微电子JFMQL15T开发板开发环境搭建&#xff0c;包含procise安装、vivado2018.3安装破解、IAR安装&#xff0c;以及vivado2018.3 IP_PATCH打补丁全过程&#xff0c;为后续开发基础。 二、IAR安装 安装IAR的软件版本是IAR 8.32.1,…

多用户商城系统哪个好,我的B2B2C电商系统选型之路

选择适合自己的B2B2C电商系统需要考虑多个因素&#xff0c;包括系统功能、易用性、扩展性、安全性和成本等。以下是一些常见的多用户商城系统供您参考&#xff1a; 1. 商淘云 基本情况&#xff1a;广州商淘信息科技有限公司旗下品牌&#xff0c;这家起步过程在国内商户中算比较…

【经典LeetCode算法题目专栏分类】【第9期】深度优先搜索DFS与并查集:括号生成、岛屿问题、扫雷游戏

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能AI、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推荐--…

Opencv中的滤波器

一副图像通过滤波器得到另一张图像&#xff0c;其中滤波器又称为卷积核&#xff0c;滤波的过程称之为卷积。 这就是一个卷积的过程&#xff0c;通过一个卷积核得到另一张图片&#xff0c;明显发现新的到的图片边缘部分更加清晰了&#xff08;锐化&#xff09;。 上图就是一个卷…

攻防世界——Hello, CTF

运行可以发现这是输入型的flag &#xff08;re题目分为两类&#xff0c;一种你直接输入flag&#xff0c;还有一种就是你完成某个操作后&#xff0c;给你flag&#xff09; 可以发现关键字符串就是wrong 和 input 32位 IDA打开 进入直接进入字符串界面&#xff0c;发现关键字符…

Java小案例-讲一下Nacos、OpenFeign、Ribbon、loadbalancer组件协调工作的原理

目录 前言 Nacos 如何进行服务自动注册&#xff1f; 服务自动注册三板斧 服务实例数据封装--Registration 服务注册--ServiceRegistry 服务自动注册--AutoServiceRegistration Ribbon OpenFeign 总结 前言 注册中心要集成SpringCloud&#xff0c;想实现SpringCloud的…

驱动开发-1

一、驱动课程大纲 内核模块字符设备驱动中断 二、ARM裸机代码和驱动有什么区别&#xff1f; 1、共同点&#xff1a; 都能够操作硬件 2、不同点&#xff1a; 1&#xff09;裸机就是用C语言给对应的寄存器里面写值&#xff0c;驱动是按照一定的套路往寄存器里面写值 2&#xff09…

c++11--强枚举类型,智能指针

1.枚举 1.1. c11之前的枚举 实例 #include <iostream>enum Type{ONE,TWO,THREE };int main(){printf("sizeof_%d, ONE_%d\n", sizeof(ONE), ONE);return 0; }具备以下特点&#xff1a; (1). 枚举值直接在父作用域可见。 (2). 枚举底层类型由编译器结合枚举成员…

爬虫工作量由小到大的思维转变---<第二十二章 Scrapy开始很快,越来越慢(诊断篇)>

前言: 相信很多朋友在scrapy跑起来看到速度200/min开心的不得了;可是,越跑到后面,发现速度变成了10-/min;刚开始以为是ip代理的问题,结果根本不得法门... 新手跑3000 ~ 5000左右数据,我相信大多数人没有问题,也不会发现问题; 可一旦数据量上了10W,你是不是就能明显感觉到速度…

Unity PlayerPrefs存储数据在Windows环境中本地存储的位置

Unity PlayerPrefs存储数据在Windows环境中本地存储的位置 一、编辑器模式下的PlayerPrefs存储位置1.Win r 输入regedit进入注册表界面2. HKEY_CURRENT_USER/Software/Unity3.CompanyName和ProjectName可以在Unity->Edit->Project Settings->Player中查看和设置 二、…

华为设备文件系统基础

华为网络设备的配置文件和VRP系统文件都保存在物理存储介质中&#xff0c;所以文件系统是VRP正常运行的基础。只有掌握了对文件系统的基本操作&#xff0c;网络工程师才能对设备的配置文件和VRP系统文件进行高效的管理。 基本查询命令 VRP基于文件系统来管理设备上的文件和目录…

【低照度图像增强系列(1)】传统方法(直方图、图像变换)算法详解与代码实现

前言 ☀️ 在低照度场景下进行目标检测任务&#xff0c;常存在图像RGB特征信息少、提取特征困难、目标识别和定位精度低等问题&#xff0c;给检测带来一定的难度。 &#x1f33b;使用图像增强模块对原始图像进行画质提升&#xff0c;恢复各类图像信息&#xff0c;再使用目标检…

【Spring实战】04 Lombok集成及常用注解

文章目录 0. 集成1. Data2. Getter 和 Setter3. NoArgsConstructor&#xff0c;AllArgsConstructor和RequiredArgsConstructor4. ToString5. EqualsAndHashCode6. NonNull7. Builder总结 Lombok 是一款 Java 开发的工具&#xff0c;它通过注解的方式简化了 Java 代码的编写&…

Quartz.NET 事件监听器

1、调度器监听器 调度器本身收到的一些事件通知&#xff0c;接口ISchedulerListener&#xff0c;如作业的添加、删除、停止、挂起等事件通知&#xff0c;调度器的启动、关闭、出错等事件通知&#xff0c;触发器的暂停、挂起等事件通知&#xff0c;接口部分定义如下&#xff1a…

算数平均数、调和平均数、几何平均数的计算方法与应用场合

一 定义 1、算数平均数&#xff1a;又称均值&#xff0c;是统计学中最基本&#xff0c;最常用的一种平均指标&#xff0c;分为简单算术平均数、加权算术平均数。 2、调和平均数&#xff1a;又称倒数平均数&#xff0c;是总体各统计变量倒数的算数平均数的倒数。分为数学调和平…

深度学习中的池化

1 深度学习池化概述 1.1 什么是池化 池化层是卷积神经网络中常用的一个组件&#xff0c;池化层经常用在卷积层后边&#xff0c;通过池化来降低卷积层输出的特征向量&#xff0c;避免出现过拟合的情况。池化的基本思想就是对不同位置的特征进行聚合统计。池化层主要是模仿人的…