【高阶数据结构】二叉搜索树 {概念;实现:核心结构,增删查,默认成员函数;应用: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链接 (翻转“分层遍历1”中的二维数组)
  4. 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先 。OJ链接
  5. 二叉树搜索树转换成排序双向链表。OJ链接

根据前、中序或中、后序遍历构造二叉树

  1. 根据一棵树的前序遍历与中序遍历构造二叉树。 OJ链接
  2. 根据一棵树的中序遍历与后序遍历构造二叉树。OJ链接

非递归实现二叉树的前中后序遍历(重点)

  1. 二叉树的前序遍历,非递归迭代实现 。OJ链接
  2. 二叉树中序遍历 ,非递归迭代实现。OJ链接
  3. 二叉树的后序遍历 ,非递归迭代实现。OJ链接

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

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

相关文章

JavaScript关于函数的小挑战

题目 回到两个体操队&#xff0c;即海豚队和考拉队! 有一个新的体操项目&#xff0c;它的工作方式不同。 每队比赛3次&#xff0c;然后计算3次得分的平均值&#xff08;所以每队有一个平均分&#xff09;。 只有当一个团队的平均分至少是另一个团队的两倍时才会获胜。否则&…

企业主流全链路监控系统 - OpenTelemetry(二)

OpenTelemetry 二 4. 部署&#xff08;python&#xff09;准备工作&#xff08;1/5&#xff09;创建 HTTP Server&#xff08;2/5&#xff09;Automatic instrumentation&#xff08;3/5&#xff09;增加观测项&#xff08;Manual&#xff09;&#xff08;4/5&#xff09;向 Co…

Zabbix 5.0 媒体介质 邮箱配置例子

QQ企业邮箱 参考&#xff1a;zabbix 腾讯企业邮箱配置图_harveymomo的博客-CSDN博客

uniapp - 全平台兼容实现上传图片带进度条功能,用户上传图像到服务器时显示上传进度条效果功能(一键复制源码,开箱即用)

效果图 uniapp小程序/h5网页/app实现上传图片并监听上传进度,显示进度条完整功能示例代码 一键复制,改下样式即可。 全部代码 记得改下样式,或直接

java八股文面试[多线程]——自旋锁

优点&#xff1a; 1. 自旋锁尽可能的减少线程的阻塞&#xff0c;这对于锁的竞争不激烈&#xff0c;且占用锁时间非常短的代码块来说性能能大幅度的提升&#xff0c;因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗 &#xff0c;这些操作会导致线程发生两次上下文切换&…

C# textBox 右键菜单 contextMenuStrip

需求&#xff1a; 想在上图空白处可以右键弹出菜单&#xff0c;该怎么做呢&#xff1f; 1.首先&#xff0c;拖出一个 ContextMenuStrip。 随便放哪里都行&#xff0c;如下: 2.在textBox里关联这个“右键控件”即可&#xff0c;如下&#xff1a; 最终效果如下&#xff1a; 以上…

Linux:ansible-playbook配置文件(剧本)

如果你还没有配置基础的ansible和一些基础用法可以去下面的链接 playbook是基于ansible的 Linux&#xff1a;ansible自动化运维工具_鲍海超-GNUBHCkalitarro的博客-CSDN博客 Linux&#xff1a;ansible自动化运维工具_鲍海超-GNUBHCkalitarro的博客-CSDN博客 Linux&…

计算机网络-笔记-第一章-计算机网络概述

目录 一、第一章——计算机网络概述 1、因特网概述 &#xff08;1&#xff09;网络、互联网、因特网 &#xff08;2&#xff09;因特网发展的三个阶段 &#xff08;3&#xff09;因特网服务的提供者&#xff08;ISP&#xff09; &#xff08;4&#xff09;因特网标准化工…

肿瘤科医师狂喜,15分RNA修饰数据挖掘文章

Biomamba荐语 与这个系列的前面一些论文类似&#xff0c;这次给大家推荐的是一篇纯生物信息学数据挖掘的文章&#xff0c;换句话说&#xff0c;这又是一篇不需要支出科研经费&#xff08;白嫖&#xff09;的论文(当然&#xff0c;生信分析用的服务器还是得掏点费用的)。一般来…

MySQL数据库学习【基础篇】

&#x1f4c3;基础篇 下方链接使用科学上网速度可能会更加快一点哦&#xff01; 请点击查看数据库MySQL笔记大全 通用语法及分类 DDL: 数据定义语言&#xff0c;用来定义数据库对象&#xff08;数据库、表、字段&#xff09;DML: 数据操作语言&#xff0c;用来对数据库表中的…

LabVIEW开发灭火器机器人

LabVIEW开发灭火器机器人 如今&#xff0c;自主机器人在行业中有着巨大的需求。这是因为它们根据不同情况的适应性。由于消防员很难进入高风险区域&#xff0c;自主机器人出现了。该机器人具有自行检测火灾的能力&#xff0c;并通过自己的决定穿越路径。 由于消防安全是主要问…

LoRA学习笔记

Background 全参微调 全量微调指的是&#xff0c;在下游任务的训练中&#xff0c;对预训练模型的每一个参数都做更新。例如图中&#xff0c;给出了Transformer的Q/K/V矩阵的全量微调示例&#xff0c;对每个矩阵来说&#xff0c;在微调时&#xff0c;其d*d个参数&#xff0c;都…

Android 实现资源国际化

前言 国际化指的是当Android系统切换语言时&#xff0c;相关设置也随之改变&#xff0c;从而使用不同的国家地区&#xff1b; 简而言之&#xff0c;就是我们的Android App中的文字和图片会随着不同国家的地区变化从而切换为不同语言文字和不同国家的图片 文字图片国际化 只要…

鸿鹄企业工程项目管理系统 Spring Cloud+Spring Boot+前后端分离构建工程项目管理系统源代码

鸿鹄工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离构建工程项目管理系统 1. 项目背景 一、随着公司的快速发展&#xff0c;企业人员和经营规模不断壮大。为了提高工程管理效率、减轻劳动强度、提高信息处理速度和准确性&#xff0c;公司对内部工程管…

Java 中数据结构LinkedList的用法

LinkList 链表&#xff08;Linked list&#xff09;是一种常见的基础数据结构&#xff0c;是一种线性表&#xff0c;但是并不会按线性的顺序存储数据&#xff0c;而是在每一个节点里存到下一个节点的地址。 链表可分为单向链表和双向链表。 一个单向链表包含两个值: 当前节点…

c语言每日一练(12)

前言&#xff1a;每日一练系列&#xff0c;每一期都包含5道选择题&#xff0c;2道编程题&#xff0c;博主会尽可能详细地进行讲解&#xff0c;令初学者也能听的清晰。每日一练系列会持续更新&#xff0c;暑假时三天之内必有一更&#xff0c;到了开学之后&#xff0c;将看学业情…

浅谈AI浪潮下的视频大数据发展趋势与应用

视频大数据的发展趋势是多样化和个性化的。随着科技的不断进步&#xff0c;人们对于视频内容的需求也在不断变化。从传统的电视节目到现在的短视频、直播、VR等多种形式&#xff0c;视频内容已经不再是单一的娱乐方式&#xff0c;更是涉及到教育、医疗、商业等各个领域。 为了…

day03_注释丶关键字丶标识符丶常量

​注释 注释就是使用人类的自然语言对代码的解释和说明。 代码本身和人类的自然语言相比&#xff0c;可读性肯定是要差一些&#xff0c;所以为了更快能够知道代码的含义、作用、需要注意地方&#xff0c;所有程序员都应该养成写注释的好习惯。 由于注释的内容是给程序员看的&…

网络编程

1. 网络编程入门 1.1 网络编程概述 计算机网络 是指将地理位置不同的具有独立功能的多台计算机及其外部设备&#xff0c;通过通信线路连接起来&#xff0c;在网络操作系统&#xff0c;网络管理软件及网络通信协议的管理和协调下&#xff0c;实现资源共享和信息传递的计算机系统…

C# 使用NPOI操作EXCEL

1.添加NOPI 引用->管理NuGet程序包->添加NOPI 2.相关程序集 3.添加命名空间 using NPOI.HSSF; using NPOI.XSSF; using System.IO; using NPOI.XSSF.UserModel; using NPOI.HSSF.UserModel; 4.从Excel导入的dgv样例 //NPOI读入dgv private void button1_Click(object s…