二叉搜索树的模拟实现

news2024/9/25 11:19:07
基础的二叉树用的其实不多,二叉树的重点在二叉树的延伸:二叉搜索树。二叉搜索树又延伸出了平衡二叉搜索树。搜索数的特点是:查找效率极高。
二叉搜索树的作用:
1. map和set特性需要先铺垫二叉搜索树,而二叉搜索树也是一种树形结构
2. 二叉搜索树的特性了解,有助于更好的理解map和set的特性
3. 二叉树中部分面试题稍微有点难度,在前面讲解大家不容易接受,且时间长容易忘
4. 有些OJ题使用C语言方式实现比较麻烦,比如有些地方要返回动态开辟的二维数组,非常麻烦
搜索二叉树的概念
二叉搜索树又称二叉排序树(称为排序树是因为如果采用中序遍历二叉树,得到的是升序排列,正常排序还是排序算法更适合),它或者是一棵空树,或者是具有以下性质的二叉树:
    若它的左子树不为空,则左子树上所有节点的值都小于根节点的值
    若它的右子树不为空,则右子树上所有节点的值都大于根节点的值
    它的左右子树也分别为二叉搜索树
总结一下就是任意一个子树都要满足左子树中的值< 根 < 右子树中的值。
以图示搜索树为例,如果要找8,8比6大,必然在6的右子树,8比9小,必然在9的左子树,8又比7大,在7的右子树,找到8。效率很高。最多查找高度次。有一个误区:虽然搜索树查找效率很高,但是时间复杂度不是O(logN),而是O(N)。因为存在二叉树只有左子树,左节点的情况。如果要达到O(logN)的效果,就要尽量让根节点的左右子树个数差不多,方案有两种:AVL树和红黑树,如何达到这样的效果之后会讲到。
下面先实现搜索树:
用上图作样例。
BinarySearchTree.h中:
#pragma once
#include<iostream>
#include<vector>
using namespace std;
template<class K>
struct BinarySearchTreeNode
{
        BinarySearchTreeNode<K>* _left;
        BinarySearchTreeNode<K>* _right;
        K _key;
        BinarySearchTreeNode(const K& key)
               :_left(nullptr)
               , _right(nullptr)
               , _key(key)
        {}
};
template<class K>
class BinarySearchTree
{
public:
        typedef BinarySearchTreeNode<K> Node; //编译器向上查找,typedef必须在Destory上面
private:
        void Destroy(Node* root)
        {
               if (root == nullptr)
                       return;
               Destroy(root->_left);
               Destroy(root->_right);
               delete root;
        }
        Node* construct(Node* root) //逻辑不理解就画递归展开图,在文章结尾,有各个函数的递归展开图
        {
               if (root == nullptr)
                       return nullptr;
                //第一次写的失败作
               //root->_left = construct(root->_left);
               //root->_right = construct(root->_right);
               //return new Node(root->_key);
               Node* copyNode = new Node(root->_key);
               copyNode->_left = construct(root->_left);
               copyNode->_right = construct(root->_right);
               return copyNode;
        }
public:
         //有了拷贝构造,编译器不会自动生成默认构造函数,需要自己手写;
        //或者可以通过c++11提供的关键字default完成显示生成默认构造函数BinarySearchTree() = default;
        //BinarySearchTree()
        //      :_root(nullptr)
        //{}
        BinarySearchTree() = default; //作用是强制编译器自己生成默认构造函数
        BinarySearchTree(BinarySearchTree<K>& tree) //拷贝构造,需要深拷贝,且无法使用现代写法
        {
               _root = construct(tree._root); //拷贝构造自身的参数是BinarySearchTree,只有一个成员参数,不适合递归传参
        }
        BinarySearchTree<K>& operator=(BinarySearchTree<K> tree)
         //拷贝构造完成后,赋值运算符重载一定能使用现代写法
        {
               swap(_root, tree._root);
               return *this;
        }
        ~BinarySearchTree() //任何递归都需要参数,这里要递归释放空间,就需要传参,所以要通过封装完成空间释放
        {
               Destroy(_root);
               _root = nullptr;
        }
        bool Insert(const K& key)
               //搜索二叉树的结果和插入的顺序有关,如果以一个相对有序的顺序插入数据,树的高度就会接近数据的个数
        {
               Node* tmp = new Node(key);
               if (_root == nullptr)
               {
                       _root = tmp;
                       return true;
               }
               else
               {
                       Node* cur = _root; //cur找key节点所在的位置
                       Node* parent = nullptr; //parent记录cur的父节点,在key找到对应位置后,进行对key的链接
                       while (cur) //cur停下来时一定在一个空节点
                       {
                              if (cur->_key > key)
                              {
                                      parent = cur;
                                      cur = cur->_left;
                              }
                              else if (cur->_key < key)
                              {
                                      parent = cur;
                                      cur = cur->_right;
                              }
                              else //不能插入相同的数,搜索二叉树默认不允许重复的值。
                                      return false;
                               //也存在允许重复的情况,这时候就统一将重复的值放在左节点/右节点(左右任意,但必须统一)
                       }
                       if (parent->_key > key) //cur找到位置后,需要再确定是parent的左节点还是右节点。
                       {
                              parent->_left = tmp;
                       }
                       else
                       {
                              parent->_right = tmp;
                       }
               }
               return true;
        }
        bool find(const K& key)
                //find也可以直接返回节点,但必须被const修饰,原因在于搜索二叉树节点一旦被修改,新的树就可能不是搜索二叉树
        {
               Node* tmp = _root;
               while (tmp) //逻辑和插入相同
               {
                       if (tmp->_key == key)
                       {
                              return true;
                       }
                       else
                       {
                              if (tmp->_key > key)
                              {
                                      tmp = tmp->_left;
                              }
                              else
                              {
                                      tmp = tmp->_right;
                              }
                       }
               }
               return false;
        }
        void InOrder() //中序遍历需要套一层,因为c++实现封装,外部无法拿到_root的数据
        {
               _InOrder(_root);
               cout << endl;
        }
        bool Erase(const K& key) //非递归版本
                //搜索二叉树的删除比较复杂,需要画图理解
               //搜索二叉树的删除主要分三种情况:删除的节点没有左子树,删除的节点没有右子树,删除的节点有左右子树
               //没有左右子树可以归为没有左子树或者没有右子树的情况
        {
               Node* cur = _root;
               Node* parent = nullptr;
                //如果_root只有左子树或者只有右子树,且要删除的是根节点,parent的值就不会改变,
               //如果parent被初始化成nullptr,在下面parent->_key就会报错,所以进入左为空,或者右为空的判断时,
               //要添一部分代码,保证不会走到parent->_key
               while (cur) //首先找到要删除的节点
               {
                       if (cur->_key > key)
                       {
                              parent = cur;
                              cur = cur->_left;
                       }
                       else if (cur->_key < key)
                       {
                              parent = cur;
                              cur = cur->_right;
                       }
                       else
                              break;
               }
               if (cur == nullptr) //没找到删除的节点,或者树中没有节点
                       return false;
               if (cur->_left == nullptr)
                        //删除的节点没有左子树,此时让parent节点的左指针或者右指针指向删除节点的右子树就可以
               {
                       if (cur == _root) //左为空,且删除的是根节点的情况,也是添加代码的部分
                       {
                              _root = cur->_right; //改变根节点的位置
                              delete cur;
                              return true;
                       }
                       else //删除的不是根节点
                       {
                              if (cur->_key > parent->_key)
                              {
                                      parent->_right = cur->_right;
                                      delete cur; //节点是new出来的
                                      return true;
                              }
                              else
                              {
                                      parent->_left = cur->_right;
                                      delete cur;
                                      return true;
                              }
                       }
               }
               else if (cur->_right == nullptr) //删除的节点没有右子树,逻辑和左子树相同
               {
                       if (cur == _root) //右为空,且删除的是根节点的情况,添加代码的部分
                       {
                              _root = cur->_left; //改变根节点的位置
                              delete cur;
                              return true;
                       }
                       else //删除的不是根节点
                       {
                              if (cur->_key > parent->_key)
                              {
                                      parent->_right = cur->_left;
                                      delete cur; //节点是new出来的
                                      return true;
                              }
                              else
                              {
                                      parent->_left = cur->_left;
                                      delete cur;
                                      return true;
                              }
                       }
               }
               else //删除的节点具有左子树和右子树,这时需要通过特殊的方式删除节点
                       //找到删除节点左子树中最大的节点,或者右子树中最小的节点,替换原来节点中_key的值,再删除找到的节点
                       //只有左树的最大节点才能比所有左树节点大,才能替换掉被删除的节点;右树的最小节点同理。
                       //找到的节点一定存在一个子树为空,此时情况和上面情况相同
               {
                       Node* parentTmp = cur; //cur本来就是tmp的父节点,直接赋值给parentTmp
                       Node* tmp = cur->_left; //找到左子树中的最大节点,tmp此时为左子树的根节点
                       while (tmp->_right != nullptr)
                               //根据搜索二叉树的特性,子树中右节点一定比左节点和根节点大,最大节点就是左子树中右节点为空的节点
                       {
                              parentTmp = tmp;
                              tmp = tmp->_right;
                       }
                       cur->_key = tmp->_key; //如果这里选择交换节点,而不是节点中的值,那树整个都会改变,因为节点中还有
                       //节点与子树的关系,比如将根节点与根的左节点交换,根节点就会变成原树的左节点,树会变成原树的左子树
                       //而原来的根节点直接无法通过现在的根节点找到。即_root指向的位置改变了。
                       //如果将根的左节点和右节点互换,那么根存储左节点的内容变成了之前右节点空间的内容,
                       //储存右节点内容的空间同理,就像是树的左右子树互换了。树将不再是搜索二叉树
                       
                       //在上一步完成后,有些人会将下面部分的内容替换成return Erase(tmp);目的是,此时tmp代表要删除的节点,
                       //直接通过Erase删除。这其实是弄混了,Erase的参数类型是const K&,而不是Node*,参数都不对应。
                       //或者return Erase(tmp->_key),这样也不对,cur->_key = tmp->_key;执行后,树已经不是原来的树了,
                       //Erase(tmp->_key)找到的是cur->_key的位置(cur->_key = tmp->_key后cur->_key和tmp->_key相同,
                       //且cur->_key离根节点更近,会先被找到)
                       if (tmp->_key > parentTmp->_key) //parentTmp是子树根节点和不是子树根节点,parentTmp的链接位置不同
                       {
                              parentTmp->_right = tmp->_left;
                              delete tmp;
                              return true;
                       }
                       else
                       {
                              parentTmp->_left = tmp->_left;
                              delete tmp;
                              return true;
                       }
               }
        }
/
//下面为各个成员函数的递归版本
        bool findR(const K& key) //递归写起来比非递归简单,但理解复杂,如果有非递归的选择,尽量写非递归。
               //如果树的高度太高,会栈溢出(栈空间很小)
        {
               return _findR(_root, key);
        }
        bool InsertR(const K& key) //递归逻辑和finR相同,重点是对引用的理解
        {
               return _InsertR(_root, key);
        }
        bool EraseR(const K& key) //递归逻辑和finR相同
        {
               return _EraseR(_root, key);
        }
private:
        bool _EraseR(Node*& root, const K& key)
        {
               if (root == nullptr) //没找到节点返回false
                       return false;
               if (root->_key > key)
                       return _EraseR(root->_left, key);
               else if (root->_key < key)
                       return _EraseR(root->_right, key);
               else //找到节点开始删,还是要分三种情况,递归只是查找节点的位置
               {
                       Node* tmp = root; //要删除的节点先保存起来
                       if (root->_left == nullptr) //左子树为空的情况
                       {
                              root = root->_right; //和非递归复杂的判断情况不同,这里的递归,利用引用,一句代码就完成了目的
                              //原因在于root引用的是要删除节点的父节点,指向要删除节点的指针,且左子树为空已经确定
                              //所以和InsertR一样,root就是链接删除节点右子树的指针,不需要额外的判断。
                              //还不理解的话看最后的图
                       }
                       else if (root->_right == nullptr) //右子树为空的情况
                       {
                              root = root->_left; //与左子树同理
                       }
                       else
                       {
                               //以下是选择将要删除节点中的值和找到的值互换,然后删除交换后的目标节点,但有更合适的方法
                              //tmp = root->_left;
                              //Node* parentTmp = root;
                              //while (tmp->_right)
                              //{
                              //      parentTmp = tmp;
                              //      tmp = tmp->_right;
                              //}
                              //swap(tmp->_key, root->_key);
                              //if (parentTmp->_left == tmp)
                              //      parentTmp->_left = tmp->_left;
                              //else
                              //      parentTmp->_right = tmp->_left;
                              Node* minRight = tmp->_right;
                              while (minRight->_left)
                              {
                                      minRight = minRight->_left;
                              }
                              swap(minRight->_key, tmp->_key);
                              return _EraseR(root->_right, key);
                       //之前讲过,return Erase(key);无法删除key,但是这里是_EraseR,可以传树的根节点,在新的树中搜索key
                      //数值交换后,新的树依然满足搜索二叉树,交换的值是右子树的最小值,且比key大,key一定还是新树的最小值
                      //要删除的节点也只有右子树。
                       }
                       delete tmp;
                       return true;
               }
        }
         //bool _InsertR(Node* root, const K& key)
        bool _InsertR(Node*& root, const K& key) //递归中的神奇之处:root类型改为引用解决父节点问题
        {
               if (root == nullptr)
                        //在root为空时,就是创建节点的时候,但是新节点要和树链接上,就需要父节点的左/右指针指向新节点
                       //父节点可以通过新增一个参数来得到,但是存在更简单的方法:将root参数类型改为引用。在_InsertR的传参中,
                       //第一个参数是root->_left/root->_right,root是对他们的引用,即root本身就是父节点中指向新节点的指针,
                       //直接对root赋值,就能将新节点链接上树。
                       //就算树为空树,root也是对_root的引用,对root的赋值,就可以改变_root。
               {
                       root = new Node(key);
                       return true;
               }
               if (root->_key > key)
                       return _InsertR(root->_left, key);
               else if (root->_key < key)
                       return _InsertR(root->_right, key);
               else
                       return false;
        }
        bool _findR(Node* root, const K& key) //最后同样有递归展开图
        {
               if (root == nullptr)
                       return false;
               if (root->_key > key)
                       return _findR(root->_left, key);
               else if (root->_key < key)
                       return _findR(root->_right, key);
               else
                       return true;
        }
        void _InOrder(Node* root) //中序遍历是递归实现的,必须要有参数
        {
               if (root == nullptr)
                       return;
               _InOrder(root->_left);
               cout << root->_key << " ";
               _InOrder(root->_right);
        }
        Node* _root = nullptr;
};
test.cpp中
//都是测试内容
#define _CRT_SECURE_NO_WARNINGS 1
#include"BinarySearchTree.h"
void BinarySerachTreeTest1()
{
        BinarySearchTree<int> t;
        vector<int> v;
        v.push_back(8);
        v.push_back(3);
        v.push_back(1);
        v.push_back(10);
        v.push_back(6);
        v.push_back(4);
        v.push_back(7);
        v.push_back(14);
        v.push_back(13);
        for (auto e : v)
        {
               t.Insert(e);
        }
        t.InOrder();
        t.Insert(16);
        t.Insert(9);
        cout << endl;
        t.InOrder();
        cout << endl;
        bool ret = t.find(0);
        cout << ret << endl;
        ret = t.find(10);
        cout << ret << endl;
        ret = t.find(13);
        cout << ret << endl;
}
void BinarySerachTreeTest2()
{
        BinarySearchTree<int> t;
        int a[] = { 8,3,1,10,6,4,7,14,13 };
        for (auto e : a)
        {
               t.Insert(e);
        }
        t.InOrder();
        cout << endl;
        t.Erase(3);
        t.InOrder();
        cout << endl;
        t.Erase(8);
        t.InOrder();
        cout << endl;
        for (auto e : a)
        {
               t.Erase(e);
        }
        t.InOrder();
        cout << endl;
        t.Insert(9);
        t.Insert(16);
        t.InOrder();
        cout << endl;
}
void BinarySerachTreeTest3()
{
        BinarySearchTree<int> t;
        int a[] = { 8,3,1,10,6,4,7,14,13 };
        for (auto e : a)
        {
               t.Insert(e);
        }
        BinarySearchTree<int> copy = t;
        BinarySearchTree<int> cp;
        cp = copy;
}
void BinarySerachTreeTest4()
{
        BinarySearchTree<int> t;
        int a[] = { 8,3,1,10,6,4,7,14,13 };
        for (auto e : a)
        {
               t.InsertR(e);
        }
        t.InOrder();
        bool ret = t.findR(10);
        cout << ret << endl;
        ret = t.findR(7);
        cout << ret << endl;
        ret = t.findR(11);
        cout << ret << endl;
        t.InOrder();
        t.EraseR(8);
        t.InOrder();
        t.EraseR(3);
        t.InOrder();
        for (auto e : a)
        {
               t.EraseR(e);
        }
        t.InOrder();
}
int main()
{
        BinarySerachTreeTest4();
        return 0;
}

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

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

相关文章

3节点linux服务器集群搭建

一&#xff0c;目的 由于当前集群部署已经成为主流&#xff0c;适当研究一些集群部署的基本操作&#xff0c;有助于后续像k8s集群、doris集群的部署。 大部分集群都是一主两从这种三节点配置。故本文也是采用三节点完成相关学习和记录。 二&#xff0c;说明 因为会关闭防火…

webScoket

webScoket是什么&#xff1f; 支持端对端通讯可以由客户端发起&#xff0c;也可以有服务端发起用于消息通知、直播间讨论区、聊天室、协同编辑等 做一个简单的webScoket 客户端配置&#xff1a; 1、新建一个页面叫web-scoket.html <!DOCTYPE html> <html lang"…

P7243 最大公约数

题目 思路 利用曼哈顿原理求离&#xff08;x&#xff0c;y&#xff09;最远的点 代码 #include<bits/stdc.h> using namespace std; #define int long long #define INF 0x3f3f3f3f const int maxn2005; int gcd(int a,int b) { return b?gcd(b,a%b):a; } int n,m; i…

华为OD机试真题 JavaScript 实现【输入整型数组和排序标识,对其元素按照升序或降序进行排序】【牛客练习题】

目录 一、题目描述二、输入描述三、输出描述四、解题思路五、JavaScript算法源码 华为OD机试 2023B卷题库疯狂收录中&#xff0c;刷题点这里 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试&#xff0c;发现新题目&#x…

RK3566 使能NPU

一、硬件 确定VDD_NPU所用的电源 用的是PMIC的DC-DC2 二、SDK 1、修改板级配置DTS文件 配置vdd_gpu vdd_gpu: DCDC_REG2 {regulator-always-on;regulator-boot-on;regulator-min-microvolt <500000>;regulator-max-microvolt <1350000>;regulator-init-microv…

[threejs]相机与坐标

搞清相机和坐标的关系在threejs初期很重要&#xff0c;否则有可能会出现写了代码&#xff0c;运行时一片漆黑的现象&#xff0c;这种情况就有可能是因为你相机没弄对。 先来看一下threejs中的坐标(世界坐标) 坐标轴好理解&#xff0c;大家只需要知道在three中不同颜色代表的轴…

python-pytorch基础之cifar10数据集使用图片分类

这里写目录标题 总体思路获取数据集下载cifar10数据解压包文件介绍加载图片数字化信息查看数据信息数据读取自定义dataset使用loader加载建模训练测试建测试数据的loader测试准确性测试一张图片读取一张图片加载模型预测图片类型创建一个预测函数随便来张马的图片结果其他打开一…

java实现钉钉群机器人@机器人获取信息后,机器人回复

1.需求 鉴于需要使用钉钉群机器人回复&#xff0c;人们提出的问题&#xff0c;需要识别提出的问题中的关键词&#xff0c;后端进行处理实现对应的业务逻辑 2.实现方式 用户群机器人&#xff0c;附带提出的问题&#xff0c;后端接收消息后识别消息内容&#xff0c;读取到关键…

亚马逊、虾皮、Lazada、速卖通、阿里国际等跨境电商平台怎么获取优质评价?

在跨境电商平台上&#xff0c;产品的评价直接影响卖家账户的评定因素&#xff0c;同时也影响产品页面的曝光量和流量&#xff0c;从而对产品销量产生影响&#xff0c;因此&#xff0c;产品评价的重要性不言而喻&#xff0c;除了产品的图片、描述、详情、广告和站外推广&#xf…

Java throw和throws 关键字

在Java中&#xff0c;异常可以分为两种类型&#xff1a; 未检查的异常&#xff1a;它们不是在编译时而是在运行时被检查&#xff0c;例如&#xff1a;ArithmeticException&#xff0c;NullPointerException&#xff0c;ArrayIndexOutOfBoundsException&#xff0c;Error类下的异…

[Docker实现测试部署CI/CD----相关服务器的安装配置(2)]

目录 6、Jenkins安装配置安装jdk安装maven拉取镜像启动jenkins修改数据卷权限浏览器访问安装插件配置jenkins移动JDK和Maven配置JDK和Maven 6、Jenkins安装配置 Jenkins 是一个开源软件项目&#xff0c;是基于 Java 开发的一种持续集成工具&#xff0c;用于监控持续重复的工作&…

MySQL 的解析器以及 MySQL8.0 做出的改进 | StoneDB技术分享 #2

设计&#xff1a;小艾 审核&#xff1a;丁奇 编辑&#xff1a;宇亭 作者&#xff1a;柳湛宇&#xff08;花名&#xff1a;乌淄&#xff09; 浙江大学-软件工程-在读硕士、StoneDB 内核研发实习生 一、MySQL 的解析器 MySQL 所使用的解析器&#xff08;即 Lexer 和 Parser …

⛳ Java多线程 一,线程基础

线程基础 ⛳ Java多线程 一&#xff0c;线程基础&#x1f43e; 一&#xff0c;线程基础&#x1f4ad; 1.1&#xff0c;什么是程序&#xff0c;进程&#xff0c;线程&#x1f3ed; 1.2&#xff0c;什么是并行和并发&#x1f463; 1.3&#xff0c;线程使用的场景&#x1f3a8; 1.…

Nginx 15分钟入门

1、反向代理和负载均衡 Nginx 反向代理 负载均衡 网站初期用户量较少的时候&#xff0c;一台服务器就够用&#xff0c;但是当大量用户注册&#xff0c;那么显然一台机器就不够了。如下图&#xff0c;我们把同一个项目部署在3台服务器上。那么问题又来了&#xff0c;用户A的请…

连锁门店新零售管理系统服务商,提供新零售商城一体化解决方案|亿发

新零售时代&#xff0c;客户需求和购物方式正在发生翻天覆地的变化&#xff0c;数字化运营服务成为连锁门店增强竞争力的有效工具。那么&#xff0c;我们该如何借助数字化力量&#xff0c;升级连锁门店的新零售运营服务&#xff0c;迎接未来的商业挑战呢&#xff1f;一、智慧新…

emWin - BMP图片显示

BmpCvt.exe 用途 利用BMP图片&#xff0c;进行GUI显示&#xff1b;ICON等图标都是小BMP图片&#xff0c;核心是将BMP图片&#xff0c;转成emWin支持的方式&#xff0c;最终显示到TFT屏上 使用BmpCvt.exe工具&#xff0c;将各个图片转成相应的C文件. emWin有关的工具&#xff…

P4780 Phi的反函数

题目 思路 φ(x)n 当指数均为1时n最小 证明&#xff1a;容斥原理 代码 #include<bits/stdc.h> using namespace std; #define int long long const int maxn1e9; int ansINT_MAX,n; bool f; map<int,bool> mp; bool is_prime(int n){if(n<1) return false;fo…

Web功能测试之表单、搜索测试

初入职场接触功能测试老是碰到以下情况不知道怎么写测试用例&#xff1a; 一个界面很多搜索条件怎么写用例&#xff1f; 下拉框测试如何考虑测试点&#xff1f; 上传要考虑哪些验证点&#xff1f;...... 所以这篇主要是整理关于web测试之表单、搜索测试的相关要点。 一、表…

个性定义轻松掌控,更适合PC玩家的游戏鼠标,雷柏VT350S体验

喜欢玩PC游戏的玩家都知道&#xff0c;一款好的鼠标可以在游戏中更加游刃有余&#xff0c;甚至扭转乾坤。但是&#xff0c;有线鼠标总是让人觉得不够灵活&#xff0c;无线鼠标又担心延迟和续航。那么&#xff0c;有没有一款无线鼠标既能满足游戏需求&#xff0c;操作又能随心所…

话费充值系话费直充系统源码支付快充慢充系统运营商接口

话费充值系话费直充系统源码支付快充慢充系统运营商接口