【高阶数据结构】二叉树搜索树 {概念;实现:核心结构,增删查,默认成员函数;应用:K模型和KV模型;性能分析;相关练习}

二叉搜索树

一、二叉搜索树的概念

在这里插入图片描述

二叉搜索树又称二叉排序树,它可以是一棵空树,若果不为空则满足以下性质:

  1. 若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
  2. 若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
  3. 它的左右子树也分别为二叉搜索树
  4. 二叉搜索树中不允许出现相同的键值

提示:二叉搜索树的附加功能是排序和去重。对于一个二叉搜索树,按照左子树-根节点-右子树的顺序进行中序遍历,得到的序列就是有序的。


二、二叉树搜索树的实现

2.1 核心结构

template <class K>
struct BSTreeNode{ //搜索二叉树的节点
  typedef BSTreeNode<K> Node;
  Node *_left;
  Node *_right;
  K _key;

  BSTreeNode(const K &key = K())
    :_left(nullptr), //必须将指针初始化为nullptr,防止出现野指针问题。
    _right(nullptr),
    _key(key)
  {}
}; 

template <class K>
class BSTree{ //搜索二叉树
  typedef BSTreeNode<K> Node;
  Node *_root = nullptr;
    
public:
  BSTree() = default; //C++11的用法:使用了default关键字,表示使用编译器自动生成的默认构造函数
    
  void InOrder(){  //中序遍历多套一层是因为递归调用需要传递根节点指针,而根节点指针_root是private权限,在类外不能访问。
    _InOrder(_root);  
    cout << endl;
  }
    
  //......
};
template <class K>
  void BSTree<K>::_InOrder(Node *root){ //中序遍历
    if(root == nullptr) return;
    _InOrder(root->_left);
    cout << root->_key << " ";
    _InOrder(root->_right);
  }

2.2 查找

  1. 从根开始比较,如果key比根大则到右树去找;如果key比根小则到左树去找;
  2. 最多查找高度次。如果走到空节点还未找到,则说明这个键值不存在。
//迭代法查找:
template <class K>
   bool BSTree<K>::Find(const K &key){
    //如果是空树,直接返回false;
    if(_root == nullptr) return false;
       
    Node *cur = _root;
    while(cur != nullptr)
    {
      if(key > cur->_key) //如果键值大,就到右树去找
      {
        cur = cur->_right;
      }
      else if(key < cur->_key) //如果键值小,就到左树去找
      {
        cur = cur->_left;
      }
      else{
        //找到返回true
        return true;
      }
    }
    //如果走到空节点还未找到,则说明这个键值不存在,返回false
    return false;
  }

//递归法查找:
template <class K>
  bool BSTree<K>::_rFind(Node *root, const K &key){
    if(root == nullptr) return false; //如果是空树或者走到空节点还未找到,返回false
    if(key > root->_key) return _rFind(root->_right, key); //如果键值大,就到右树去找
    else if(key < root->_key) return _rFind(root->_left, key); //如果键值小,就到左树去找
    else return true;//找到返回true
  }

2.3 插入

插入的具体过程如下:

  1. 树为空,则直接新增节点,赋值给root指针
  2. 树不为空,按二叉搜索树性质查找插入位置,插入新节点:
  3. 如果找到相同的键值,则不进行插入。
  4. 直到找到合适的空位置,才能进行插入操作。
//迭代法插入:
template <class K>
bool BSTree<K>::Insert(const K &key){
    //树为空,直接进行插入:
    if(_root == nullptr)
    {
      _root = new Node(key);
      return true;
    }
    
    //树不为空,按二叉搜索树性质查找插入位置,插入新节点:
    Node *cur = _root;
    Node *parent = _root;
    while(cur != nullptr)
    {
      if(key > cur->_key) //如果键值大,就到右树去找
      {
        parent = cur; //移动cur指针前记录父节点指针parent,便于下一步插入操作的连接。
        cur = cur->_right;
      }
      else if(key < cur->_key) //如果键值小,就到左树去找
      {
        parent = cur;
        cur = cur->_left;
      }
      else{
        return false; //如果找到相同的键值,则插入失败。
      }
    }
    
    //直到找到合适的空位置,才能进行插入操作:
    if(key > parent->_key) //判断该位置是父节点的左节点还是右节点
    {
      parent->_right = new Node(key);
    }
    else{
      parent->_left = new Node(key);
    }
    return true;
}

//递归法插入:
/*传引用的好处:root是父节点内_left,_right指针的引用(对于空树就是_root的引用),修改root就是修改父节点的_left,_right指针。所以我们不需要再记录父节点的指针,也不需要再判断该位置是父节点的左节点还是右节点。*/
template <class K>
  bool BSTree<K>::_rInsert(Node* &root, const K &key){
    //如果树为空或者找到了合适的空位置,进行插入操作:
    if(root == nullptr)
    {
      root = new Node(key);
      return true;
    }
      
    if(key > root->_key)  //如果键值大,就到右树去插入
        return _rInsert(root->_right, key);
    else if(key < root->_key)  //如果键值小,就到左树去插入
        return _rInsert(root->_left, key);
    else  //如果找到相同的键值,则插入失败。
        return false;
  }

2.4 删除

首先查找元素是否在二叉搜索树中,如果不存在,则返回, 否则要删除的结点可能分下面四种情况:

  1. 要删除的结点是叶节点
  2. 要删除的结点只有左结点
  3. 要删除的结点只有右结点
  4. 要删除的结点有左、右结点

看起来待删除节点有4中情况,实际情况1可以与情况2或者3合并起来(使父节点指向nullptr),因此真正的删除过程如下:

  1. 使父结点指向被删除节点的左结点;然后删除该结点;–直接删除
  2. 使父结点指向被删除节点的右结点;然后删除该结点;–直接删除
  3. 找到被删除节点左树的最大值(最右)或右树的最小值(最左);将其key值与被删除节点的key值交换,保证搜索树的结构;最后删除该节点(最大或最小节点)–替换法删除
//迭代法删除:
template <class K>
  bool BSTree<K>::Erase(const K &key){
    Node *cur = _root;
    Node *parent = _root; 
    while(cur != nullptr)
    {
      if(key > cur->_key)
      {
        parent = cur; //移动cur指针前记录父节点指针parent,便于下一步删除操作的连接。
        cur = cur->_right;
      }
      else if(key < cur->_key)
      {
        parent = cur;
        cur = cur->_left;
      }
      else{
        //情况1,2,3 直接删除
        if(cur->_left == nullptr || cur->_right == nullptr)
        {
          DelNode(parent, cur);
        }
        else{
          //情况4 替换法删除
          Node *lmaxp = cur; //如果cur->left就是左树的最大值,lmaxp应该指向cur
          Node *lmax = cur->_left; //lmax找左树的最大值,即左树的最右节点。
          while(lmax->_right != nullptr) 
          {
            lmaxp = lmax; //移动lmax指针前记录父节点指针lmaxp,便于下一步删除操作的连接。
            lmax = lmax->_right;
          } 
          swap(cur->_key, lmax->_key); //将其key值与被删除节点的key值交换,保证搜索树的结构
        
          //最后删除该节点。
          //注意:lmax指向最右节点,但该节点可能有左树;即使是叶节点,删除后也要接nullptr;
          //因此要调用DelNode删除前进行连接。替换法删除实际是将问题转化为情况1或2或3
          DelNode(lmaxp, lmax); 
        }
        return true; //完成删除马上返回
      }
    }
     //如果是空树,或者找不到要删除的元素,删除失败。
     return false; 
  }

//直接删除节点
template <class K>
  void BSTree<K>::DelNode(Node *parent, Node *cur){
    if(cur->_left == nullptr) //要删除的结点只有右结点(或者是叶节点);使其父结点指向其右结点(叶节点指向空);
    {
      if(cur == _root) //如果要删除的是根节点,需要特殊处理。
      {
        _root = cur->_right;
      }
      else if(parent->_left == cur) //判断要删除的结点是父节点的左节点还是右节点
      {
        parent->_left = cur->_right;
      }
      else{
        parent->_right = cur->_right;
      }
    }
    else if(cur->_right == nullptr) //要删除的结点只有左结点;使父结点指向其左结点;
    {
      if(cur == _root)
      {
        _root = cur->_left;
      }
      else if(parent->_left == cur)
      {
        parent->_left = cur->_left;
      }
      else{
        parent->_right = cur->_left;
      }
    }
    //最后释放删除节点
    delete cur;
  }

//递归法删除:
/*传引用的好处:root是父节点内_left,_right指针的引用,修改root就是修改父节点的_left,_right指针。所以我们不需要再记录父节点的指针,也不需要再判断该位置是父节点的左节点还是右节点。*/
template <class K>
  bool BSTree<K>::_rErase(Node* &root, const K &key){
    //如果是空树,或者找不到要删除的元素,删除失败。
    if(root == nullptr) return false;
      
    if(key > root->_key) 
        _rErase(root->_right, key);
    else if(key < root->_key) 
        _rErase(root->_left, key);
    else{
      Node *del = root; //记录要删除的节点
      //情况1,2,3 直接删除
      if(root->_left == nullptr)
      {
        root = root->_right;
      }
      else if(root->_right == nullptr)
      {
        root = root->_left; 
      }
      else{
        //情况4 替换法删除 
        Node *rmin = root->_right; //rmin找右树的最小值,即右树的最左节点。
        while(rmin->_left!=nullptr)
        {
          rmin = rmin->_left;
        }
        swap(rmin->_key, root->_key); //将其key值与被删除节点的key值交换,保证搜索树的结构
        
        //最后删除该节点。
        //注意:rmin指向最左节点,但该节点可能有右树;即使是叶节点,删除后也要接nullptr;
        //因此要递归调用_rErase删除前进行连接。替换法删除实际是将问题转化为情况1或2或3
        //交换后,已经不能从根节点开始找key了(key的位置已经不符合搜索树结构);
        //应该从右子树的根节点开始找(key的位置在右子树中仍符合搜索树结构);
        return _rErase(root->_right, key);
      }
      //释放节点
      delete del;
      return true;
    }
  }

2.5 默认成员函数

template <class K>
class BSTree{
  typedef BSTreeNode<K> Node;
  Node *_root = nullptr;
public:
  BSTree() = default; //使用编译器自动生成的默认构造函数

  //多套一层是因为递归调用需要传递根节点指针,而根节点指针_root是private权限,在类外不能访问。析构同理。
  BSTree(const BSTree<K> &bst){
    _root = _Copy(bst._root);
  }

  BSTree<K>& operator=(BSTree<K> bst){ //复用拷贝构造
    swap(bst._root, _root);
    return *this;
  }
  
  ~BSTree(){
    _Destroy(_root);
  }
  
  //......
};

拷贝构造

  template <class K>
  Node* BSTree<K>::_Copy(Node *root){
    if(root == nullptr) return nullptr;
    Node *copyroot = new Node(root->_key); //前序遍历拷贝,保证两棵树的结构顺序完全相同
    copyroot->_left = _Copy(root->_left);
    copyroot->_right = _Copy(root->_right);
    return copyroot;
  }

析构

  template <class K>
  void BSTree<K>::_Destroy(Node *root){
    if(root == nullptr) return;
    _Destroy(root->_left); //析构必须采用后序遍历
    _Destroy(root->_right);
    delete root;
  } 

三、二叉搜索树的应用

3.1 K模型

K模型:K模型即只有key作为关键码,结构中只需要存储Key即可,关键码即为需要搜索到的值。

比如:给一个单词word,判断该单词是否拼写正确,具体方式如下:(示例代码:wordchecker.cc)

  1. 以词库中所有单词集合中的每个单词作为key,构建一棵二叉搜索树
  2. 在二叉搜索树中检索该单词是否存在,存在则拼写正确,不存在则拼写错误。
//wordchecker.cc
#include "BSTree_K.hpp"
#include <iostream>
#include <string>
using namespace std;

int main(){
  string tmp[] = {"search", "word", "vector", "string", "dictionary", "list", "binary"};
  BSTree<string> lib;
  for(string &e : tmp)
  {
    lib.Insert(e);
  }
  lib.InOrder();
  string input;
  while(cin >> input)
  {
    bool ret = lib.Find(input);
    if(ret)
    {
      cout << "拼写正确!" << endl;
    }
    else{
      cout << "拼写错误!" << endl;
    }
  }
  return 0;
}

3.2 KV模型

KV模型:每一个关键码key,都有与之对应的值Value,即<Key, Value>的键值对。

KV模型中key为关键码,二叉搜索树构建过程中的比较仍以key值为准。value只是一个对应值,附加值。

该种方式在现实生活中非常常见:

  • 比如英汉词典就是英文与中文的对应关系,通过英文可以快速找到与其对应的中文,英文单词与其对应的中文<word, chinese>就构成一种键值对;(示例代码:dictionary.cc)
  • 再比如统计单词次数,统计成功后,给定单词就可快速找到其出现的次数,单词与其出现次数就是<word, count>就构成一种键值对。(示例代码:wordcounter.cc)

将二叉搜索树改造成KV模型:

//将二叉搜索树改造成KV模型
//只有少量改动,不变的内容省略不写。
#include <iostream>
using namespace std;

template <class K, class V>
struct BSTreeNode{
  typedef BSTreeNode<K, V> Node;
  Node *_left;
  Node *_right;
  K _key;
  V _val; //加了一个value

  BSTreeNode(const K &key = K(), const V &val = V())
    :_left(nullptr),
    _right(nullptr),
    _key(key),
    _val(val)
  {}
}; 

template <class K, class V>
class BSTree{
  typedef BSTreeNode<K, V> Node;
  Node *_root = nullptr;
    
public:
  bool Insert(const K &key, const V &val);
  
  Node* Find(const K &key);

  //......
  
private:
  Node* _Copy(Node *root);
     
  //......
};

template <class K, class V>
bool BSTree<K,V>::Insert(const K &key, const V &val){
    if(_root == nullptr)
    {
      _root = new Node(key, val); //插入时要初始化value
      return true;
    }
    //......
    if(key > parent->_key)
    {
      parent->_right = new Node(key, val);
    }
    else{
      parent->_left = new Node(key, val);
    }
    return true;
}
  
template <class K, class V>
    BSTreeNode<K,V>* BSTree<K,V>::Find(const K &key){
    if(_root == nullptr) return nullptr;
    Node *cur = _root;
    while(cur != nullptr)
    {
      //......
      else{
        return cur; //找到返回节点指针,便于查看和修改value
      }
    }
    return nullptr;
  }
  
template <class K, class V>
  BSTreeNode<K,V>* BSTree<K,V>::_Copy(Node *root){
    if(root == nullptr) return nullptr;
    Node *copyroot = new Node(root->_key, root->_val);  //拷贝也要复制value
    //......
  }

template <class K, class V>
  void BSTree<K,V>::_InOrder(Node *root){
    if(root == nullptr) return;
    _InOrder(root->_left);
    cout << root->_key << "-->" << root->_val << endl;
    _InOrder(root->_right);
  }

KV模型的应用示例:

//dictionary.cc
#include "BSTree_KV.hpp"
#include <iostream>
#include <string>
using namespace std;

int main(){
  BSTree<string, string> dct;
  dct.Insert("buffer", "缓冲器");
  dct.Insert("error", "错误");
  dct.Insert("derive", "继承自,来自");
  dct.Insert("soldier", "士兵");
  dct.Insert("column", "列");
  dct.Insert("row", "行");

  dct.InOrder();

  string input;
  while(cin >> input){
    BSTreeNode<string,string> *ret = dct.Find(input);
    if(ret != nullptr)
    {
      cout << ret->_val << endl;
    }
    else{
      cout << "词库中无此单词" << endl;
    }
  }
}

//wordcounter.cc
int main(){
  string tmp[] = {"orange", "orange","orange","strawberry", "pear","apple", "apple","apple","apple","peach","peach"};
  BSTree<string, int> counter;
  for(string &e : tmp)
  {
    BSTreeNode<string, int> *ret = counter.Find(e);
    if(ret)
    {
      ++ret->_val;
    }
    else{
      counter.Insert(e, 1);
    }
  }
  counter.InOrder();
  return 0;
}

四、二叉搜索树的性能分析

插入和删除操作都必须先查找,查找效率代表了二叉搜索树中各个操作的性能。

对有n个结点的二叉搜索树,若每个元素查找的概率相等,则二叉搜索树平均查找次数是结点在二叉搜索树的深度的函数,即结点越深,则比较次数越多。时间复杂度:O(h),h是树的深度。

但对于同一个关键码集合,如果各关键码插入的次序不同,可能得到不同结构的二叉搜索树:

在这里插入图片描述

最优情况下,二叉搜索树为完全二叉树(或者接近完全二叉树),其平均比较次数为:log_2 N
最差情况下,二叉搜索树退化为单支树(或者类似单支),其平均比较次数为:N

问题:如果退化成单支树,二叉搜索树的性能就失去了。那能否进行改进,不论按照什么次序插入关键码,二叉搜索树的性能都能达到最优?那么我们后续章节学习的AVL树和红黑树就可以上场了。


五、相关练习

这些题目更适合使用C++完成,难度也更大一些

  1. 二叉树创建字符串。OJ链接
  2. 二叉树的分层遍历1。OJ链接
  3. 二叉树的分层遍历2。OJ链接
  4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。OJ链接
  5. 二叉树搜索树转换成排序双向链表。OJ链接
  6. 根据一棵树的前序遍历与中序遍历构造二叉树。 OJ链接
  7. 根据一棵树的中序遍历与后序遍历构造二叉树。OJ链接
  8. 二叉树的前序遍历,非递归迭代实现 。OJ链接
  9. 二叉树中序遍历 ,非递归迭代实现。OJ链接
  10. 二叉树的后序遍历 ,非递归迭代实现。OJ链接

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

前端简介(HTML+CSS+JS)

学习Django过程中遇到一些前端相关的内容&#xff0c;于是整理了一下相关概念。 前端开发是创建WEB页面或APP等前端界面呈现给用户的过程。 如果只是想要入门前端&#xff0c;只要学习网页三剑客(HTML、CSS、JavaScript)即可。 如果把网页比喻成一个房子&#xff0c;HTML就是…

ReactiveApi

reactivity api: https://v3.vuejs.org/api/reactivity-api 1. 获取响应式数据 API传入返回备注reactiveplain-object对象代理深度代理对象中的所有成员readonlyplain-object or proxy对象代理只能读取代理对象中的成员&#xff0c;不可修改refany{ value: ... }对value的访问…

[uniapp] scroll-view 简单实现 u-tabbar效果

文章目录 方案踩坑1.scroll-view 横向失败2.点击item不滚动?3. scrollLeft从哪里来? 效果图 方案 官方scroll-view 进行封装 配合属性 scroll-left Number/String 设置横向滚动条位置 即可 scroll-into-view 属性尝试过,方案较难实现 踩坑 1.scroll-view 横向失败 安装…

阿里云服务器和腾讯云服务器折扣对比_哪家折扣低?

阿里云服务器和腾讯云服务器根据购买时长可以享受一定的优惠折扣&#xff0c;综合对比下来腾讯云折扣更低&#xff0c;阿腾云来对比下阿里云和腾讯云的云服务器根据购买时长可以享受的常规折扣对比&#xff1a; 目录 阿里云和腾讯云折扣对比 阿里云服务器常规折扣 腾讯云服…

架构评估-架构师之路(十二)

软件系统质量属性 软件系统质量熟悉分为 开发期质量属性 和 运行期质量属性。 质量属性 性能&#xff1a;指 系统的响应能力&#xff0c;如 响应时间&#xff0c;吞吐率。 设计策略&#xff1a;优先级队列、增加计算资源、减少计算开销、引入并发机制、采用资源调度。 可靠…

html学习第2篇---标签(1)

html学习第2篇---标签 1、标题标签h1---h62、段落标签p3、换行标签br4、文本格式化标签5、div标签和span标签6、图像标签img6.1、图像属性6.2、相对路径、绝对路径 7、超链接标签a7.1、属性7.2、分类 8、注释标签和特殊字符8.1、注释8.2、特殊字符 1、标题标签h1—h6 为了使网…

数学——七桥问题——图论

当涉及数学&#xff0c;有很多不同的话题可以讨论。你是否有特定的数学领域、概念或问题想要了解更多&#xff1f;以下是一些常见的数学领域和主题&#xff0c;你可以选择一个或者告诉我你感兴趣的具体内容&#xff0c;我将很乐意为你提供更多信息&#xff1a; 代数学&#xff…

django开发流程

设计model django采用ORM映射&#xff0c;可以在代码中描述数据库的布局 只需要导入from django.db import models 并使类继承models.Model&#xff0c;models中的一个类对应数据库中的一个表&#xff0c;类的变量对应表字段。 创建数据库 $ python manage.py makemigration…

SpringIoC基于注解配置

目录 一、Bean注解标记和扫描 (IoC) 二、组件&#xff08;Bean&#xff09;作用域和周期方法注解 三、Bean属性赋值&#xff1a;引用类型自动装配 (DI) 四、Bean属性赋值&#xff1a;基本类型属性赋值 (DI) 一、Bean注解标记和扫描 (IoC) 一、注解方式介绍 1.注解介绍 和…

解决Springboot创建工程时,pom.xml文件中的插件spring-boot-maven-plugin报红

在初始创建工程完成之后&#xff0c;发现pom文件中有错误 spring-boot-maven-plugin这一行会报红 解决办法&#xff1a;在代码中添加版本信息 <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-…

Android应用启动流程:从启动到可交互的过程解析

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读1.1 启动知识储备1.2 Zygote进程1.3 SystemServer进程1.4 …

Git gui教程---第八篇 Git gui的使用 创建一个分支

一般情况下一个主分支下代码稳定的情况下会新建出一个分支&#xff0c;然后在分支上修改&#xff0c;修改完成稳定后再合并到主分支上。 或者几个人合作写一份代码&#xff0c;每个人各一个分支&#xff0c;测试稳定再合并到主分支上。 在git gui选择菜单栏“分支”&#xff0…

Python“牵手”易贝(Ebay)商品列表数据,关键词搜索ebayAPI接口数据,ebayAPI接口申请指南

Ebay平台API接口是为开发电商类应用程序而设计的一套完整的、跨浏览器、跨平台的接口规范&#xff0c; EbayAPI接口是指通过编程的方式&#xff0c;让开发者能够通过HTTP协议直接访问Ebay平台的数据&#xff0c;包括商品信息、店铺信息、物流信息等&#xff0c;从而实现Ebay平…

看个电影就能学会类的加载过程?我不信!

O、前言 今天我们来了解一下关于类的加载过程&#xff0c;这个问题在面试过程中属于高频面试题了。 那么回答的时候&#xff0c;我们往往会采取死记硬背的方式&#xff0c;告诉面试官类的加载过程包括&#xff1a;加载、验证、准备、解析和初始化这5个阶段。 但是如果面试官…

数字孪生引领智慧港口新纪元

随着数字化时代的到来&#xff0c;港口行业也在不断寻求创新&#xff0c;以提高运营效率、优化资源分配&#xff0c;并实现可持续发展。数字孪生技术作为一种强大的虚拟仿真工具&#xff0c;正日益成为智慧港口解决方案的核心。本文带大家一起探讨数字孪生在智慧港口领域的应用…

智慧政务,长远布局——AIGC引领,加速推进数字化政府建设

在人工智能、虚拟现实等领域迅猛发展且日益成熟的背景下&#xff0c;AI行业正迈向蓬勃发展的全新阶段&#xff0c;市场规模持续扩张。与此同时&#xff0c;数字服务也正在蓬勃兴起&#xff0c;新一代信息技术为数字政府构建了坚实支撑&#xff0c;重塑了政务信息化管理、业务架…

SQL 大小敏感问题

在SQL中&#xff0c;关键字和函数名 是不区分 大小写的 比如&#xff08;select、where、order by 、group by update 等关键字&#xff09;&#xff0c;以及函数(ABS、MOD、round、min等) window系统默认是大小写不敏感 &#xff08;ZEN文件和zen 文件 不能同时存在&#xff…

方案:AI边缘计算智慧工地解决方案

一、方案背景 在工程项目管理中&#xff0c;工程施工现场涉及面广&#xff0c;多种元素交叉&#xff0c;状况较为复杂&#xff0c;如人员出入、机械运行、物料运输等。特别是传统的现场管理模式依赖于管理人员的现场巡查。当发现安全风险时&#xff0c;需要提前报告&#xff0…

自己搭建Minecraft服务器并通过cpolar内网穿透实现与公网小伙伴联机我的世界

文章目录 1. Java环境搭建2.安装我的世界Minecraft服务3. 启动我的世界服务4.局域网测试连接我的世界服务器5. 安装cpolar内网穿透6. 创建隧道映射内网端口7. 测试公网远程联机8. 配置固定TCP端口地址8.1 保留一个固定tcp地址8.2 配置固定tcp地址 9. 使用固定公网地址远程联机 …

tomcat更改端口号和隐藏端口号

因为默认端口:8080不会自动隐藏&#xff0c;因此为了更显格调需要将其改为:80 进入tomcat的server文件 将其改为80&#xff0c;之后将tomcat重新启动即可 tomcat启动流程 [rootshang ~]# cd /usr/local/tomcat/apache-tomcat-8.5.92 [rootshang apache-tomcat-8.5.92]# cd b…