数据结构-树

    • 什么是树

      树是一种层次型数据结构,其中每个元素都只有一个父节点,但可以有多个子节点。在计算机科学中,树是一种抽象数据类型,它的主要优点是能够高效地插入和删除元素,且可以按照层级进行遍历。

      树的每个节点代表一个元素,每个节点都包含一个值以及一个指向子节点的列表。树的节点可以分为根节点(root)、内部节点(internal node)和叶子节点(leaf node)。根节点没有父节点,叶子节点没有子节点。

    • 树的基本术语

      1. 节点(Node):树中的每个元素称为一个节点。
      2. 根节点(Root Node):没有父节点的节点称为根节点。
      3. 父节点(Parent Node):指向某个节点的节点称为该节点的父节点。
      4. 子节点(Child Node):某个节点指向的节点称为该节点的子节点。
      5. 兄弟节点(Sibling Node):具有相同父节点的节点互为兄弟节点。
      6. 叶子节点(Leaf Node):没有子节点的节点称为叶子节点。
      7. 路径(Path):从根节点到某个节点的所有节点组成的序列称为从根到该点的路径。
      8. 层次(Level):节点在树中的层级,根节点为第0层,其他节点层数是其父节点层数加1。
      9. 深度(Depth):树中节点的最大路径长度称为树的深度。
      10. 高度(Height):叶子节点到根节点的最长路径长度称为树的高度。
  • 二叉树

    • 什么是二叉树

      • 二叉树是一种特殊的树,每个节点最多有两个子节点。

      • 二叉树中的节点可以分为:

        根节点(Root Node):二叉树的顶端的节点。

        左子节点(Left Child):指向左子节点的链接。

        右子节点(Right Child):指向右子节点的链接。

      • 二叉树可以是:

        • 满二叉树:每一层的节点数都是最大节点数。
        • 完全二叉树:除了最后一层外,每一层都是满的,并且最后一层的节点都集中在左侧。
        • 平衡二叉树:每一个节点的两个子树的高度差都不超过1。
        • 二叉搜索树(Binary Search Tree,BST):左子树的所有节点都小于根节点,右子树的所有节点都大于根节点。
    • 二叉树的存储结构

      • 二叉树的顺序存储

        二叉树的顺序存储是指使用数组来表示二叉树,其中数组的每个元素存储二叉树中的一个节点。顺序存储的二叉树通常用于实现特殊的二叉树,例如顺序二叉树和堆。

      • 二叉树的链式存储

        二叉树的链式存储是指使用链表来表示二叉树,其中每个节点存储二叉树中的一个节点。链式存储的二叉树通常用于实现普通的二叉树。

        //代码实现链式二叉树
        class Node {
            char value;
            Node left;
            Node right;
        
            public Node(char value) {
                this.value = value;
                this.left = null;
                this.right = null;
            }
        }
        
        Node root = new Node('A');
        root.left = new Node('B');
        root.right = new Node('C');
        root.left.left = new Node('D');
        root.left.right = new Node('E');
        root.right.left = new Node('F');
        root.right.right = new Node('G');
        
    • 二叉树的遍历(序列化)

      //节点定义
      public class TreeNode {
          int val;
          TreeNode left;
          TreeNode right;
          TreeNode(int x) { val = x; }
      }
      
      • 深度优先搜索

        定义:深度优先搜索(DFS,Depth-First Search)是一种用于遍历或搜索树或图的算法。在这种算法中,我们会从根节点开始,沿着一条路径尽可能深入,直到达到某个叶节点或目标节点,然后回溯,沿着另一条路径深入,直到遍历完所有节点。树的深度优先搜索遍历包括:

        • 前序遍历:前序遍历是一种遍历二叉树的方法,按照"根-左-右"的顺序访问每个节点。

          //非递归方式
          public class FrontDemo1 {
              public static void preOrderTree(TreeNode root) {
                  if (root == null){
                      return;
                  }
                  //用栈来模拟递归
                  Stack<TreeNode> stack = new Stack<>();
                  stack.push(root);
                  while (!stack.isEmpty()){
                      TreeNode node = stack.pop();
                      System.out.println(node.val+"");
                      //右节点先入栈后出栈
                      if (node.right != null){
                          stack.push(node.right);
                      }
                      //左节点后入栈先出栈
                      if (node.left != null){
                          stack.push(node.left);
                      }
                  }
              }
          }
          
          //递归方式
          public class FrontDemo2 {
              public void preOrder(TreeNode root){
                  if (root == null){
                      return;
                  }
                  System.out.println(root.val+"");
                  preOrder(root.left);//递归左子树
                  preOrder(root.right);//递归右子树
              }
          }
          
        • 中序遍历:中序遍历二叉树是指按照"左-根-右"的顺序访问每个节点。

          //非递归方式
          public class MidDemo1 {
              public static void midOrderTree(TreeNode root){
                  if (root==null){
                      return;
                  }
                  //用栈来模拟递归
                  Stack<TreeNode> stack = new Stack<>();
                  TreeNode current=root;
                  while (current!=null||!stack.isEmpty()){
                      while (current!=null){
                          //左子树全部入栈
                          stack.push(current);
                          current=current.left;
                      }
                      //左子树节点和根节点出栈
                      current=stack.pop();
                      System.out.println(current.val+"");
                      /*若current是叶子节点就让current指向null
                      以便执行第二次循环让根节点出栈*/
                      current=current.right;
                  }
              }
          }
          
          //递归方式
          public class MidDemo2 {
              public static void midOrderTree(TreeNode root){
                  if(root == null){
                      return;
                  }
                  midOrderTree(root.left);
                  System.out.print(root.val+" ");
                  midOrderTree(root.right);
              }
          }
          
        • 后续遍历:

          //非递归方式
          public class LastDemo1 {
              public static void LastOrder(TreeNode root){
                  if (root == null){
                      return;
                  }
                  Stack<TreeNode> stack = new Stack<>();
                  TreeNode prev=null;
                  TreeNode current=root;
                  while (current!=null||!stack.isEmpty()){
                      if (current!=null){
                          stack.push(current);
                          current=current.left;
                      }else {
                          TreeNode topNode = stack.peek();
                          //防止重复遍历右子树(若右子树非空且没有被访问过则访问右子树)
                          if (topNode.right!=null&&topNode.right!=prev){
                              current=topNode.right;
                          //若右子树为空或已被访问过则直接输出父节点;
                          }else {
                              System.out.println(topNode.val+" ");
                              prev=topNode;
                              stack.pop();
                          }
                      }
                  }
              }
          }
          
          //递归方式
          public class LastDemo2 {
              public static void lastOrder(TreeNode root){
                  if (root == null){
                      return;
                  }
                  //先遍历左子树
                  lastOrder(root.left);
                  //再遍历右子树
                  lastOrder(root.right);
                  //最后访问根节点
                  System.out.print(root.val + " ");
              }
          }
          
      • 广度优先搜素

        • 层序遍历:层序遍历是一种二叉树的遍历方式,也称为广度优先遍历。它按照从左到右、从上到下的顺序来访问每一个节点。层序遍历通常使用队列来实现。

          public class WideDemo {
              public List<List<Integer>> levelOrder(TreeNode root) {
                  //空树则返回空集合
                  if (root == null) {
                      return new ArrayList<>();
                  }
                  //创建嵌套list存储结果,其中内层每个list集合存储的是每一层级的结果
                  List<List<Integer>> result = new ArrayList<>();
                  //创建队列,用于广度优先遍历
                  Queue<TreeNode> queue = new LinkedList<>();
                  //将根节点入队
                  queue.offer(root);
                  while (!queue.isEmpty()) {
                      int size= queue.size();
                      List<Integer> currentLevel = new ArrayList<>();
                      //遍历当前层级的节点
                      for (int i = 0; i < size; i++) {
                          TreeNode currentNode = queue.poll();
                          currentLevel.add(currentNode.val);
                          if (currentNode.left != null) {
                              queue.offer(currentNode.left);
                          }
                          if (currentNode.right != null) {
                              queue.offer(currentNode.right);
                          }
                      }
                      //将当前层级的节点值添加到结果list中
                      result.add(currentLevel);
                  }
                  return result;
              }
          }
          
    • 二叉查找(搜索)树/二叉排序树

      • 定义:二叉排序树(Binary Sort Tree)是一种特殊的二叉树,它满足以下性质:

        1. 每个节点都有一个键(和通常一个关联的值),并且节点的键可以用于进行比较。
        2. 对于任何节点n,左子树的所有键都小于n的键。
        3. 对于任何节点n,右子树的所有键都大于n的键。
        4. 对二叉排序树进行中序遍历后能得到一个递增的有序序列
      • 实现

        public class BSTDemo {
            private TreeNode root;
        
            public BSTDemo(TreeNode root) {
                this.root = root;
            }
        
            //1.二叉排序树的插入
            public void insert(int key){
                root = insert(root, key);
            }
            private TreeNode insert(TreeNode node,int key){
                //如果为空树,则创建一个新节点
                if (node==null){
                    return new TreeNode(key);
                }
                //如果插入的值小于当前节点的值,则插入到左子树
                if (key<node.val){
                    node.left=insert(node.left,key);
                 // 如果插入的值大于当前节点的值,则插入到右子树
                } else if (key>node.val) {
                    node.right=insert(node.right,key);
                }
        /*        当key == node.key时,我们不需要在二叉排序树中插入一个新的节点,
                 但是仍然需要将当前节点返回给上一层递归,
                 以便在上一层递归中继续处理当前节点的兄弟节点。*/
                return node;
        
            }
            //2.二叉排序树的搜索
            public List<Integer> search(int key){
                return search(root,key);
            }
            private List<Integer> search(TreeNode node, int key){
                //如果为空树,则返回空列表
                if (node==null){
                    return new ArrayList<>();
                }
                if (key>node.val){
                    return search(node.right,key);
                }else if (key<node.val){
                    return search(node.left,key);
                }else {
                    //找到节点,返回不可变列表
                    return Collections.singletonList(node.val);
                }
            }
            //3.判断更小节点
            //找到以node为根的二叉排序树中的最小值
            private int minValue(TreeNode node){
                int minValue = node.val;
                while (node.left!=null){
                    minValue=node.left.val;
                    node=node.left;
                }
                return minValue;
            }
            //4.二叉排序树的删除
            public void delete(int key){
                root=delete(root,key);
            }
            //删除掉以node为根的二叉排序树中值为key的节点
            private TreeNode delete(TreeNode node,int key){
                if (node==null){
                    return null;
                }
                //如果key小于node的值,那么就继续在左子树中删除
                if (key<node.val){
                    node.left=delete(node.left,key);
                }else if (key>node.val){
                    //如果key大于node的值,那么就继续在右子树中删除
                    node.right=delete(node.right,key);
                }else {
                    //如果node的值等于key,那么就需要删除node
                    //如果node的左子树为空,那么就直接返回node的右子树
                    if (node.left==null){
                        return node.right;
                    }else if (node.right==null){
                        //如果node的右子树为空,那么就直接返回node的左子树
                        return node.left;
                    }
                    //如果node的左右子树都不为空,那么就找到右子树中的最小值,
                    //然后将node的值赋值为这个最小值,然后继续在右子树中删除这个最小值
                    node.val = minValue(node.right);
                    node.right=delete(node.right,node.val);
                }
                return node;
            }
        }
        
      • 效率

        • 在最好的情况下,平衡二叉树的时间复杂度为O(log(2)n),最坏为O(n)。
      • 应用

        • 数据库索引:在数据库中,二叉排序树经常被用来构建索引,加快数据的检索速度。例如,MySQL使用B-Tree作为其索引数据结构,而B-Tree是一种扩展的二叉排序树,具有更高的效率。
        • 文件索引:在文件系统中,二叉排序树可以用来建立索引,提高文件检索效率。例如,Linux文件系统使用B-Tree作为其索引数据结构。
        • 排序算法:二叉排序树本身就是一种排序算法,可以用来对一组数据进行排序。
        • 查找算法:二叉排序树中的查找算法,时间复杂度为O(log(n)),是一种非常高效的查找方法,可以应用于需要快速查找数据的场景。
        • 平衡二叉搜索树:AVL树和红黑树等平衡二叉搜索树,除了具有二叉排序树的所有优点外,还可以保持树的平衡,使得各种操作的时间复杂度稳定保持在O(log(n))。这使得它们在项目中的使用更加广泛。
    • 平衡二叉树

      • 定义:平衡二叉树(Balanced Binary Tree),也称为AVL树(Adelson-Velsky and Landis tree),是一种自平衡的二叉搜索树。在AVL树中,任何节点的两个子树的高度差最大为1,从而保证了树的平衡。AVL树是一种特殊的二叉搜索树,它的插入、删除和查找操作都是O(log n)。

      • 什么是平衡因子:平衡因子是用于描述二叉树平衡性的一个值,它的定义是:平衡因子 = 左子树的高度 - 右子树的高度。若平衡因子为-1、0或1,则该二叉树是平衡的;若平衡因子为-2或2,则该二叉树不是平衡的。

      • 四种导致二叉树不平衡的旋转方式

        • LL(左左)旋转:
          当新插入的节点位于根节点的左子树的左子树上时,需要进行LL旋转。
        • RR(右右)旋转:
          当新插入的节点位于根节点的右子树的右子树上时,需要进行RR旋转。
        • LR(左右)旋转:
          当新插入的节点位于根节点的左子树的右子树上时,需要进行LR旋转。
        • RL(右左)旋转:
          当新插入的节点位于根节点的右子树的左子树上时,需要进行RL旋转。
      • 判断二叉树是否平衡

        public class BalanceNode {
            int val;
            TreeNode left;
            TreeNode right;
        
            public BalanceNode(int val) {
                this.val = val;
            }
        }
        
        public class BalanceTreeDemo {
            //返回当前节点的深度
            private int depth(TreeNode node){
                if (node==null){
                    return 0;
                }
                int leftDepth = depth(node.left);
                int rightDepth = depth(node.right);
                return Math.max(leftDepth,rightDepth)+1;
            }
            //判断是否为平衡二叉树
            public boolean isBalanced(TreeNode root){
                if (root==null){
                    return true;
                }
                int ldepth = depth(root.left);
                int rdepth = depth(root.right);
                if (Math.abs(ldepth-rdepth)>1){
                    return false;
                }
                return isBalanced(root.left)&&isBalanced(root.right);
            }
        }
        
      • 应用

        • 搜索引擎:搜索引擎中的索引结构通常采用平衡二叉树,如B-Tree和其变种B+Tree。这些树结构在磁盘上存储数据,并通过平衡二叉树的结构保证检索效率。
        • 数据库:数据库中的索引结构也常常使用平衡二叉树,如B-Tree和其变种B+Tree。它们可以有效地提高数据的检索速度,提高数据库的性能。
        • 排序和搜索算法:在计算机科学中,平衡二叉树也被用于实现各种排序和搜索算法,如AVL树、红黑树等。这些算法在插入和删除操作后能自动保持树的平衡,从而保证操作的效率。
        • 路由算法:在计算机网络中,路由算法常常使用平衡二叉树来计算最短路径。这种树结构能快速地找到目标节点的路由信息,提高网络传输效率。
        • 数据压缩:在数据压缩领域,平衡二叉树也被用于实现哈夫曼编码和哈夫曼解码算法。这些算法通过构建平衡二叉树来优化数据的存储和传输效率。
    • 红黑树:红黑树讲解

  • N叉树

    • 定义:N叉树是树的一种,其中每个节点可以有0个或多个子节点。

    • 创建N叉树

      public class NTreeNode {
          int value;
          List<NTreeNode> children;
      
          public NTreeNode(int value) {
              this.value = value;
              this.children=new ArrayList<>();
          }
          public void addChildren(NTreeNode child) {
              this.children.add(child);
          }
      }
      
      public class Main {
          public static void main(String[] args) {
              TreeNode root = new TreeNode(1);
              TreeNode child1 = new TreeNode(2);
              TreeNode child2 = new TreeNode(3);
              TreeNode child3 = new TreeNode(4);
              TreeNode child4 = new TreeNode(5);
      
              root.addChild(child1);
              root.addChild(child2);
              child1.addChild(child3);
              child1.addChild(child4);
          }
      }
      
    • N叉树的遍历

      • 前序遍历

          public void preOrderTravel(NTreeNode root) {
                //如果节点为空,则返回
                if (root == null){
                    return;
                }
                //输出节点值
                System.out.println(root.value);
                //遍历子节点
                for (NTreeNode child : root.children) {
                    //递归调用前序遍历
                    preOrderTravel(child);
                }
            }
        
      • 后序遍历

         //后序遍历
         public void postOrderTravel(NTreeNode root) {
             if (root==null){
                 return;
             }
             for (NTreeNode child : root.children) {
                 postOrderTravel(child);
             }
             System.out.println(root.value);
         }
        
    • N叉树的高度

       public int height(NTreeNode root) {
           if (root == null){
               return 0;
           }else {
               int maxHeight=0;
               for (NTreeNode child: root.children) {
                   int childHeight = height(child);
                   if (childHeight > maxHeight){
                       maxHeight=childHeight;
                   }
               }
               return maxHeight+1;
           }
      
    • 应用

      • 文件系统:N叉树可以用来表示文件系统中的目录结构,每个节点表示一个文件或目录,子节点表示其下的文件或子目录。
      • 路由算法:在路由器中,N叉树可以用来表示网络拓扑结构,每个节点表示一个路由器或网络设备,子节点表示其下的路由器或网络设备。
      • 图形处理:在图形处理中,N叉树可以用来表示场景中的物体和光源,每个节点表示一个物体或光源,子节点表示物体中的部分或光源的属性。
      • 人工智能:在人工智能中,N叉树可以用来表示游戏中的棋盘,每个节点表示棋盘的一个状态,子节点表示棋盘下一步可能的状态。
      • 数据结构:N叉树是一种灵活的数据结构,可以用来表示各种复杂的数据结构,例如堆、二叉搜索树等。
    • 定义:
      每个节点都大于等于(或小于等于)其子节点的值(对于最大堆来说);
      每个节点都小于等于(或大于等于)其子节点的值(对于最小堆来说);
      堆顶元素是堆中最大的元素(对于最大堆来说);
      堆顶元素是堆中最小的元素(对于最小堆来说);
      对于当前节点i,其父节点的索引为i / 2。

    • 大根堆:大根堆(也称为最大堆)是一种完全二叉树,满足任一非叶子节点的值均大于或等于其左右孩子节点的值。大根堆通常用于实现优先队列(也称为堆排序)。

    • 小根堆:最小堆(也称为小根堆)是一种特殊的堆数据结构,它的每个节点的值都小于等于其子节点的值。

    • 大根堆的实现

      public class MaxHeap {
          //整型数组,用于存储堆中的元素。由于堆是完全二叉树,所以可以使用数组来表示。
          // heap.length表示堆的容量,size表示堆中当前元素的个数
          private int[] heap;
          //堆中当前元素的个数
          private int size;
          //初始化堆
          public MaxHeap(int capacity) {
              heap = new int[capacity+1];
              size = 0;
          }
      
          //1.向堆中添加新元素
          public void insert(int value) {
              //由于我们将堆的根节点(heap[1])作为堆顶元素,所以heap[0]的位置会一直空缺。
              // 在实际使用中,我们可以将heap[0]设置为一个特殊值,例如Integer.MIN_VALUE,或者直接忽略heap[0]的位置。
              heap[++size]=value;
              swim(size);
          }
          //2.元素上浮(增加的节点大于其父节点,则交换它与父节点的位置,直到满足堆的性质)
          private void swim(int k){
              //新加入的节点的值大于其父节点
              while (k>1&&heap[k]>heap[k/2]){
                  //交换两者位置
                  swap(heap,k,k/2);
                  //继续向上比较
                  k/=2;
              }
          }
          //3.从堆中删除并返回最大元素
          /*在最大堆中,我们通常拿最小的元素来下沉。
          这是因为最大堆的性质是每个节点的值都大于等于其子节点的值。
          当我们从堆中删除最大元素时,我们将堆中最后一个元素移动到堆顶,
          此时堆顶元素的值可能小于其子节点的值,从而破坏了堆的性质。
          为了恢复堆的性质,我们需要将堆顶元素与较大的子节点交换位置,然后将交换后的位置继续下沉。*/
          public int removeMax(){
              int max=heap[1];
              //将堆中最后一个元素移动到堆顶
              heap[1]=heap[size--];
              sink(1);
              return max;
          }
          //4.元素下沉
          private void sink(int k){
              while (2*k<=size){
                  int j=2*k;
                  //获取左右节点中最大的那个
                  if (j<size&&heap[j]<heap[j+1]){
                      j++;
                  }
                  //若果当前节点大于子节点,则下沉结束
                  if (heap[k]>=heap[j]){
                      break;
                  }
                  swap(heap,k,j);
                  k=j;
              }
          }
          //5.交换数组中两个元素的位置
          private static void swap(int[]arr,int i,int j){
              int temp=arr[i];
              arr[i]=arr[j];
              arr[j]=temp;
          }
      }
      
  • B树和B+树

    • B树的定义

      B树,又称多路平衡查找树,B树中孩子个数的最大值称为B树的阶,通常用m表示一颗m阶B树满足如下性质:

      • 树中的每个节点至多有m颗子树,至多含有m-1个关键字

      • 若根节点不是终端节点,则至少含有两颗子树

      • 除根节点外的所有非叶结点至少含有(m/2)向上取整颗子树,即至少含有(m/2)向上取整颗-1个关键字(保证了查找效率)

      • 所有非叶结点都出现在同一层次,并且不带任何信息(实际这些节点不存在,指向这些节点的指针为空)

    • B+树的定义

      B+树是一种改进的B树,它的所有键值都存储在叶子节点,并且所有的叶子节点通过指针连接成一个有序链表。B+树利用了链表的特性,可以实现高效的区间查询和遍历操作。一颗m阶B+树满足如下性质:

      • 每个分支节点至多有m颗子树
      • 非叶根节点至少含有两颗子树,其他分支节点至少含有m/2向上取整颗子树(保证了查找效率)
      • 节点的子树个数与关键字个数相等
      • 所有分支节点中仅包含它的各个子节点的关键字的最大值以及指向其子节点的指针
      • 所有叶子结点包含全部关键字以及指向相应记录的指针,叶节点中按关键字大小顺序排列,并且相邻叶节点按照大小顺序相互链接起来.
    • B树和B+树的主要差异

      • 在B+树中具有n个关键字的节点只含有n个子树,即每个关键字对应一颗子树,而在B树中,具有n个关键字的节点包含n+1个子树
      • 在B+树中叶节点包含了全部关键字信息,即在非叶结点中出现的关键字也会出现在叶节点中,而在B树中,叶节点包含的关键字和其他节点包含的关键字是不重复的
      • 在B+树中,叶节点包含信息,所有非叶结点仅仅起索引作用,非叶结点的每个索引项只含有最大的关键字和指向子节点的指针,并不包含数据记录的存储地址.
    • 应用

      B树的应用

      • 数据库索引:B树由于其平衡性和多路搜索的特性,可以高效地实现数据库中数据的快速查找和排序。许多数据库如MySQL、PostgreSQL等都使用B树作为其索引结构。
      • 文件系统索引:B树在文件系统中也被广泛应用,用于实现文件的快速查找和访问。例如,Windows的NTFS文件系统和Linux的Ext4文件系统都使用B树作为其索引结构。

      B+树的应用:

      • 数据库索引:B+树由于其更加高效的区间查询和遍历操作,在数据库中也被广泛使用。例如,Oracle数据库就使用B+树作为其索引结构。
      • 文件系统索引:B+树在文件系统中同样被使用,特别是在需要高效区间查询和遍历操作的场景下。例如,Linux的XFS文件系统和MacOS的HFS+文件系统都使用B+树作为其索引结构。
相关推荐
qq_4419960525 分钟前
Mybatis官方生成器使用示例
java·mybatis
巨大八爪鱼32 分钟前
XP系统下用mod_jk 1.2.40整合apache2.2.16和tomcat 6.0.29,让apache可以同时访问php和jsp页面
java·tomcat·apache·mod_jk
爱吃生蚝的于勒2 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
失落的香蕉4 小时前
C语言串讲-2之指针和结构体
java·c语言·开发语言
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
wclass-zhengge4 小时前
SpringCloud篇(配置中心 - Nacos)
java·spring·spring cloud
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet