使用红黑树模拟实现map和set
在STL的源代码中,map和set的底层原理都是红黑树。但这颗红黑树跟我们单独写的红黑树不一样,它需要改造一下:
改造红黑树
节点的定义
因为map和set的底层都是红黑树。而且map是拥有键值对pair<K,V>的,而set是没有键值对,只有一个K。因此,为了应对这两种不同的情况,就使用模板参数T。
当map使用这棵红黑树的时候,T就会变成pair<K,V>。当set使用时,T就会变成K。
//使用枚举
enum Colour
{RED,BLACK,
};// 如果是map,则为pair<K, V>; 如果是set,则为k
//因此,对于节点来说,需要再套一层模板,来应付两种不同的情况
template<class T>
struct RBTreeNode
{T _data;RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Colour _col;RBTreeNode(const T& data):_data(data), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) //默认是红色{}
};
改版的红黑树的插入操作
红黑树的模板参数有三个:K、valueType和仿函数KeyOfValue。
K是key的类型,ValueTyp要么是set的K,要么是map的pair<const K,V>。
而仿函数是为了比较节点的值的大小的,因为官方库中键值对pair<K,V>比较大小的方法是比较first和second。但是红黑树中,新增节点是通过比较first,因此我们需要自己写一个仿函数用于单独比较first。
//跟节点的定义一样
//因为关联式容器中存储的是<key, value>的键值对,因此
// k为key的类型,
// ValueType: 如果是map,则为pair<K, V>; 如果是set,则为k。如下:
// map->RBTree<K,pair<const K,V>,MapKeyOfT> _t;
// set->RBTree<K,K,SetKeyOfT> _t
//
// KeyOfValue: 通过value来获取key的一个仿函数类。其作用就是用于比较。
template<class K,class ValueType,class KeyOfValue>
class RBTree
{typedef RBTreeNode<ValueType> Node;
public: //迭代器typedef __RBTreeIterator<ValueType> iterator;//......
private:Node* _root = nullptr;
};
迭代器
红黑树的迭代器不能使用原生指针,因为它是一棵树,节点地址不连续,因此需要封装起来。
//set的K,map的pair<K,V>
template<class ValueType>
struct __RBTreeIterator
{typedef RBTreeNode<ValueType> Node;typedef __RBTreeIterator<ValueType> Self;Node* _node;__RBTreeIterator(Node* node):_node(node){}
};
1.operator++()
红黑树是二叉搜索树,采用的是中序遍历的方法。因此,迭代器每次向前走,是按照中序遍历的顺序走的。
因此,对于往前++,有两种情况:第一种情况是当前节点的右孩子不为空,那么按照中序遍历的顺序,下一个节点便是右子树的最小值。第二种情况是当前节点的右孩子为空:这种情况下,又分两种情况:①当前节点是父节点的左孩子,那么下一个节点肯定是当前节点的父节点。②当前节点是父节点的右孩子,这意味着当前节点的父节点也被遍历了,又没有右孩子节点,只能往上找了,找的节点是祖先节点,而这个祖先节点是特征是:孩子节点是它的左节点。
//左 根 右Self& operator++(){//当当前节点的右子树不为空,根据中序遍历的顺序,那就找右子树最小值if (_node->_right){Node* minValue = _node->_right;while (minValue->_left){minValue = minValue->_left;}//找到最小值的节点后,更新节点_node = minValue;}else //当当前节的的右子树为空,说明它是叶子节点。此时需要判断情况{//判断当前节点是父亲节点的左孩子还是右孩子//第一种情况:如果是cur==parent->_right,即当前节点是父节点的右孩子的话,就//去找祖先节点(这个祖先节点的特征:孩子节点是它的左节点)//第二种情况:当前节点是父亲节点的左孩子,那么下一个节点就是父亲节点了Node* cur = _node;//定义父亲节点。Node* parent = cur->_parent;//如果当前节点不是父亲的右孩子,并且父亲是不存在,那么就是根节点或者是左孩子节点了//那就不进入循环。while (parent && cur == parent->_right){//如果是,那就往上找到下一个节点。cur = cur->_parent;parent = parent->_parent;}//更新节点_node = parent;}return *this;}
2.begin()和end();
迭代器的begin和end,是左闭右开的区间。那么begin就是最左的节点,end则是最右节点的下一个节点。这里不使用哨兵来作为end节点。
对于begin,就找到最左的节点就好啦!
iterator begin(){Node* left = _root;while (left && left->_left){left = left->_left;}return iterator(left);}
对于end,这里选择直接置为空,传入匿名对象。这样子做的话,当遍历到最后一个节点后,它会往回遍历,一直到了根节点的上面一个节点,也就是空。此时就会结束循环。
iterator end(){return iterator(nullptr);}
迭代器整体代码:
template<class ValueType>
struct __RBTreeIterator
{typedef RBTreeNode<ValueType> Node;typedef __RBTreeIterator<ValueType> Self;Node* _node;__RBTreeIterator(Node* node):_node(node){}ValueType& operator*(){return _node->_data;}ValueType* operator->(){return &_node->_data;}Self& operator++(){if (_node->_right){Node* minValue = _node->_right;while (minValue->_left){minValue = minValue->_left;}_node = minValue;}else {Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_right){cur = cur->_parent;parent = parent->_parent;}_node = parent;}return *this;}bool operator!=(const Self& s){return _node != s._node;}
};
set的模拟实现
仿函数
由于set只有Key,因此就让仿函数直接返回Key值就可以了。
template<class K>class set{//set的仿函数,返回set的K值struct SetKeyOfValue{const K& operator()(const K& key){return key;}};
整体都是复用了红黑树的接口,并没有什么技术含量,问题都在红黑树中解决了。
namespace my_set
{template<class K>class set{//set的仿函数,返回set的K值struct SetKeyOfValue{const K& operator()(const K& key){return key;}};public:typedef typename RBTree<K, K, SetKeyOfValue>::iterator iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end();}bool insert(const K& key){return _t.Insert(key);}private:RBTree<K, K, SetKeyOfValue> _t;};
map的模拟实现:
仿函数
由于比较的是first,因此我们返回键值对中的first即可。
template<class K, class V>class map{struct MapKeyOfValue{const K& operator()(const pair<const K, V>& kv){return kv.first;}};
整体代码,一样的都是复用了红黑树的接口,并没有什么技术含量:
namespace my_map
{template<class K, class V>class map{struct MapKeyOfValue{const K& operator()(const pair<const K, V>& kv){return kv.first;}};public:typedef typename RBTree<K, pair<const K, V>, MapKeyOfValue>::iterator iterator;iterator begin(){return _t.begin();}iterator end(){return _t.end();}bool insert(const pair<const K, V>& kv){return _t.Insert(kv);}private:RBTree<K, pair<const K, V>, MapKeyOfValue> _t;};
最后是红黑树的整体代码,这段代码跟单独写的红黑树的代码差不多,就是在新增节点的操作那里,比较节点的值使用的是仿函数。
#pragma once//使用枚举
enum Colour
{RED,BLACK,
};// 如果是map,则为pair<K, V>; 如果是set,则为k
//因此,对于节点来说,需要再套一层模板,来应付两种不同的情况
template<class T>
struct RBTreeNode
{T _data;RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Colour _col;RBTreeNode(const T& data):_data(data), _left(nullptr), _right(nullptr), _parent(nullptr), _col(RED) //默认是红色{}
};template<class ValueType>
struct __RBTreeIterator
{typedef RBTreeNode<ValueType> Node;typedef __RBTreeIterator<ValueType> Self;Node* _node;__RBTreeIterator(Node* node):_node(node){}ValueType& operator*(){return _node->_data;}ValueType* operator->(){return &_node->_data;}//左 根 右Self& operator++(){//当当前节点的右子树不为空,根据中序遍历的顺序,那就找右子树最小值if (_node->_right){Node* minValue = _node->_right;while (minValue->_left){minValue = minValue->_left;}//找到最小值的节点后,更新节点_node = minValue;}else //当当前节的的右子树为空,说明它是叶子节点。此时需要判断情况{//判断当前节点是父亲节点的左孩子还是右孩子//第一种情况:如果是cur==parent->_right,即当前节点是父节点的右孩子的话,就//去找祖先节点(这个祖先节点的特征:孩子节点是它的左节点)//第二种情况:当前节点是父亲节点的左孩子,那么下一个节点就是父亲节点了Node* cur = _node;//定义父亲节点。Node* parent = cur->_parent;//如果当前节点不是父亲的右孩子,并且父亲是不存在,那么就是根节点或者是左孩子节点了//那就不进入循环。while (parent && cur == parent->_right){//如果是,那就往上找到下一个节点。cur = cur->_parent;parent = parent->_parent;}//更新节点_node = parent;}return *this;}bool operator!=(const Self& s){return _node != s._node;}
};
//跟节点的定义一样
//因为关联式容器中存储的是<key, value>的键值对,因此
// k为key的类型,
// ValueType: 如果是map,则为pair<K, V>; 如果是set,则为k。如下:
// map->RBTree<K,pair<const K,V>,MapKeyOfT> _t;
// set->RBTree<K,K,SetKeyOfT> _t
//
// KeyOfValue: 通过value来获取key的一个仿函数类。其作用就是用于比较。
template<class K,class ValueType,class KeyOfValue>
class RBTree
{typedef RBTreeNode<ValueType> Node;
public:typedef __RBTreeIterator<ValueType> iterator;iterator begin(){Node* left = _root;while (left && left->_left){left = left->_left;}return iterator(left);}iterator end(){return iterator(nullptr);}bool Insert(const ValueType& data){//先按二叉搜索树的规矩来创建一棵二叉搜索树if (_root == nullptr){_root = new Node(data);//因为红黑树的根节点是黑色的_root->_col = BLACK;return true;}KeyOfValue kot;Node* parent = nullptr;Node* cur = _root;while (cur){if (kot(cur->_data) < kot(data)){parent = cur;cur = cur->_right;}else if (kot(cur->_data) > kot(data)){parent = cur;cur = cur->_left;}else{return false;}}cur = new Node(data);cur->_col = RED;//多写一步,防止写错代码。if (kot(parent->_data) < kot(data)){parent->_right = cur;cur->_parent = parent;}else{parent->_left = cur;cur->_parent = parent;}//创建完二叉搜索树//开始创建红黑树,使用颜色来判断是否需要调整//循环往上走,循环条件:当走到的parent不为空,并且parent是红色的//即我们列举是三种情况,parent都是红的,就需要重新调整//如果parent是黑色的,那就不需要了。直接就是一棵红黑树,不进入循环while (parent && parent->_col == RED){//保存祖先节点,即g节点Node* grandfther = parent->_parent;//判断父节点是在祖先节点的哪边if (parent == grandfther->_left){//父节点在左边,那么叔叔节点就在右边Node* uncle = grandfther->_right;//情况一:uncle存在且为红。改变颜色即可if (uncle && uncle->_col == RED){//变色。parent->_col = uncle->_col = BLACK;grandfther->_col = RED;//往上走cur = grandfther;parent = cur->_parent;}else //uncle不存在 或者 存在但是黑色{//情况二 p是g的左孩子,cur是p的左孩子,以g为轴右单旋if (cur == parent->_left){//右单旋RotateR(grandfther);//变色 右单旋后,parent为根节点,变黑色。cur和g节点为红色parent->_col = BLACK;grandfther->_col = RED;}else //情况三 p是g的左孩子,cur是p的右孩子.{//先以p为轴左旋转RotateL(parent);//变成情况二,再以g为轴右单旋RotateR(grandfther);//变色 cur变成根节点,为黑色。p和g是红色cur->_col = BLACK;grandfther->_col = RED;}break;}}else //parent是在grandfther的右边{//叔叔节点就在祖先节点的左边Node* uncle = grandfther->_left;//情况一:uncle存在且为红。改变颜色即可if (uncle && uncle->_col == RED){//变色。parent->_col = uncle->_col = BLACK;grandfther->_col = RED;//往上走cur = grandfther;parent = cur->_parent;}else //uncle不存在 或者 存在但是黑色{//情况二 p是g的右孩子,cur是p的右孩子。if (cur == parent->_right){//左单旋RotateL(grandfther);//变色 右单旋后,parent为根节点,变黑色。cur和g节点为红色parent->_col = BLACK;grandfther->_col = RED;}else //情况三 p是g的右孩子,cur是p的左孩子.{//先以p为轴右旋转RotateR(parent);//变成情况二,再以g为轴左单旋RotateL(grandfther);//变色 cur变成根节点,为黑色。p和g是红色cur->_col = BLACK;grandfther->_col = RED;}break;}}}//最后将根节点置为黑_root->_col = BLACK;return true;}void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL){subRL->_parent = parent;}Node* ppNode = parent->_parent;subR->_left = parent;parent->_parent = subR;if (ppNode == nullptr){_root = subR;_root->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;}}void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}Node* ppNode = parent->_parent;subL->_right = parent;parent->_parent = subL;if (ppNode == nullptr){_root = subL;_root->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;}}void Inorder(){_Inorder(_root);}void _Inorder(Node* root){if (root == nullptr)return;_Inorder(root->_left);std::cout << root->_kv.first << ": " << root->_kv.second << std::endl;_Inorder(root->_right);}bool Check(Node* root, int blackNum, const int ref){if (root == nullptr){//cout << blackNum << endl;if (blackNum != ref){cout << "违反规则:本条路径的黑色节点的数量跟最左路径不相等" << endl;return false;}return true;}if (root->_col == RED && root->_parent->_col == RED){cout << "违反规则:出现连续红色节点" << endl;return false;}if (root->_col == BLACK){++blackNum;}return Check(root->_left, blackNum, ref)&& Check(root->_right, blackNum, ref);}bool IsBalance(){if (_root == nullptr){return true;}if (_root->_col != BLACK){return false;}int ref = 0;Node* left = _root;while (left){if (left->_col == BLACK){++ref;}left = left->_left;}return Check(_root, 0, ref);}
private:Node* _root = nullptr;
};
相关文章:

使用红黑树模拟实现map和set
在STL的源代码中,map和set的底层原理都是红黑树。但这颗红黑树跟我们单独写的红黑树不一样,它需要改造一下: 改造红黑树 节点的定义 因为map和set的底层都是红黑树。而且map是拥有键值对pair<K,V>的,而set是没有键值对&a…...

【django项目开发】用户登录后缓存权限到redis中(十)
这里写目录标题一、权限的数据的特点二、首先settings.py文件中配置redis连接redis数据库一、权限的数据的特点 需要去数据库中频繁的读和写,为了项目提高运行效率,可以把用户的权限在每次登录的时候都缓存到redis中。这样的话,权限判断的中…...

算法总结c++
文章目录基本概念时间复杂度空间复杂度基本结构1. 数组前缀和差分数组快慢指针(索引)左右指针(索引)盛水容器三数之和最长回文子串2. 链表双指针删除链表的倒数第 n 个结点翻转链表递归将两个升序链表合并为一个新的 升序 链表链表翻转3. 散列表twoSum无…...

Python 之 NumPy 切片索引和广播机制
文章目录一、切片和索引1. 一维数组2. 二维数组二、索引的高级操作1. 整数数组索引2. 布尔数组索引三、广播机制1. 广播机制规则2. 对于广播规则另一种简单理解一、切片和索引 ndarray 对象的内容可以通过索引或切片来访问和修改(),与 Pytho…...

Redis【包括Redis 的安装+本地远程连接】
Redis 一、为什么要用缓存? 缓存定义 缓存是一个高速数据交换的存储器,使用它可以快速的访问和操作数据。 程序中的缓存 在我们程序中,如果没有使用缓存,程序的调用流程是直接访问数据库的; 如果多个程序调用一个数…...

深度学习训练营_第P3周_天气识别
🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍦 参考文章:Pytorch实战 | 第P3周:彩色图片识别:天气识别**🍖 原作者:K同学啊|接辅导、项目定制**␀ 本次实验有两个新增任务&…...
“华为杯”研究生数学建模竞赛2006年-【华为杯】C题:维修线性流量阀时的内筒设计问题(附获奖论文及matlab代码)
赛题描述 油田采油用的油井都是先用钻机钻几千米深的孔后,再利用固井机向四周的孔壁喷射水泥砂浆得到水泥井管后形成的。固井机上用来控制砂浆流量的阀是影响水泥井管质量的关键部件,但也会因磨损而损坏。目前我国还不能生产完整的阀体,固井机仍依赖进口。由于损坏的内筒已…...

数据结构:带环单链表基础OJ练习笔记(leetcode142. 环形链表 II)(leetcode三题大串烧)
目录 一.前言 二.leetcode160. 相交链表 1.问题描述 2.问题分析与求解 三.leetcode141. 环形链表 1.问题描述 2.代码思路 3.证明分析 下一题会用到的重要小结论: 四.leetcode142. 环形链表 II 1.问题描述 2.问题分析与求解 Judgecycle接口…...
数模美赛如何找数据 | 2023年美赛数学建模必备数据库
2023美赛资料分享/思路答疑群:322297051 欧美相关统计数据(一般美赛这里比较多) 1、http://www.census.gov/ 美国统计局(统计调查局或普查局)官方网站 The Census Bureau Web Site provides on-line access to our …...

SSTI漏洞原理及渗透测试
模板引擎(Web开发中) 是为了使 用户界面 和 业务数据(内容)分离而产生的,它可以生成特定格式的文档, 利用模板引擎来生成前端的HTML代码,模板引擎会提供一套生成HTML代码的程序,之后…...

【算法基础】高精度除法
👦个人主页:Weraphael ✍🏻作者简介:目前是C语言 算法学习者 ✈️专栏:【C/C】算法 🐋 希望大家多多支持,咱一起进步!😁 如果文章对你有帮助的话 欢迎 评论💬…...
optimizer.zero_grad(), loss.backward(), optimizer.step()的理解及使用
optimizer.zero_grad,loss.backward,optimizer.step用法介绍optimizer.zero_grad():loss.backward():optimizer.step():用法介绍 这三个函数的作用是将梯度归零(optimizer.zero_grad())&#x…...

融资、量产和一栈式布局,这家Tier 1如此备战高阶智驾决赛圈
作者 | Bruce 编辑 | 于婷从早期的ADAS,到高速/城市NOA,智能驾驶的竞争正逐渐升级,这对于车企和供应商的核心技术和产品布局都是一个重要的考验。 部分智驾供应商已经在囤积粮草,响应变化。 2023刚一开年,智能驾驶领域…...

centos7.8安装oralce11g
文章目录环境安装文件准备添加用户操作系统环境配置解压安装问题解决创建用户远程连接为了熟悉rman备份操作,参照大神的博客在centos中安装了一套oracle11g,将安装步骤记录如下环境安装文件准备 这里准备一台centos7.8 虚拟机 配置ip 192.168.18.100 主…...
【蓝桥杯集训·每日一题】AcWing 3956. 截断数组
文章目录一、题目1、原题链接2、题目描述二、解题报告1、思路分析2、时间复杂度3、代码详解三、知识风暴一维前缀和一、题目 1、原题链接 3956. 截断数组 2、题目描述 给定一个长度为 n 的数组 a1,a2,…,an。 现在,要将该数组从中间截断,得到三个非空子…...

万丈高楼平地起:Linux常用命令
目录 系统管理命令 man命令 ls命令 cd命令 useradd命令 passwd命令 free命令 whoami命令 ps命令 date命令 pwd命令 shutdown命令 文件目录管理命令 touch命令 cat命令 mkdir命令 rm命令 cp命令 mv命令 find命令 more指令 less指令 head指令 tail指令 …...

Linux(Linux的连接使用)
连接Linux我们一般使用CRT或者Xshell工具进行连接使用。 如CRT使用SSH的方式 输出主机,账户,密码那些就可以连接上了。 Linux系统是一个文件型操作系统,有一句话说Linux的一切皆是文件。Linux系统的启动大致有下面几个步骤 Linux系统有7个运…...

Unity中画2D图表(2)——用XChart包绘制散点分布图 + 一条直线方程
散点图用于显示关系。 对于 【相关性】 ,散点图有助于显示两个变量之间线性关系的强度。 对于 【回归】 ,散点图常常会添加拟合线。 举例1:你可以展示【年降雨量】与【玉米亩产量】的关系 举例2:你也可以分析各个【节假日】与【大…...
Go 排序包 sort
写在前面的使用总结: 排序结构体 实现Len,Less,Swap三个函数 package main import ( "fmt" "sort") type StuScore struct { name string score int } type StuScores []StuScore func (s StuScores) Len(…...

Java Email 发HTML邮件工具 采用 freemarker模板引擎渲染
Java Email 发HTML邮件工具 采用 freemarker模板引擎 1.常用方式对比 Java发送邮件有很多的实现方式 第一种:Java 原生发邮件mail.jar和activation.jar <!-- https://mvnrepository.com/artifact/javax.mail/mail --> <dependency><groupId>jav…...

网络六边形受到攻击
大家读完觉得有帮助记得关注和点赞!!! 抽象 现代智能交通系统 (ITS) 的一个关键要求是能够以安全、可靠和匿名的方式从互联车辆和移动设备收集地理参考数据。Nexagon 协议建立在 IETF 定位器/ID 分离协议 (…...

VB.net复制Ntag213卡写入UID
本示例使用的发卡器:https://item.taobao.com/item.htm?ftt&id615391857885 一、读取旧Ntag卡的UID和数据 Private Sub Button15_Click(sender As Object, e As EventArgs) Handles Button15.Click轻松读卡技术支持:网站:Dim i, j As IntegerDim cardidhex, …...
IGP(Interior Gateway Protocol,内部网关协议)
IGP(Interior Gateway Protocol,内部网关协议) 是一种用于在一个自治系统(AS)内部传递路由信息的路由协议,主要用于在一个组织或机构的内部网络中决定数据包的最佳路径。与用于自治系统之间通信的 EGP&…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

网站指纹识别
网站指纹识别 网站的最基本组成:服务器(操作系统)、中间件(web容器)、脚本语言、数据厍 为什么要了解这些?举个例子:发现了一个文件读取漏洞,我们需要读/etc/passwd,如…...

初探Service服务发现机制
1.Service简介 Service是将运行在一组Pod上的应用程序发布为网络服务的抽象方法。 主要功能:服务发现和负载均衡。 Service类型的包括ClusterIP类型、NodePort类型、LoadBalancer类型、ExternalName类型 2.Endpoints简介 Endpoints是一种Kubernetes资源…...

Kafka入门-生产者
生产者 生产者发送流程: 延迟时间为0ms时,也就意味着每当有数据就会直接发送 异步发送API 异步发送和同步发送的不同在于:异步发送不需要等待结果,同步发送必须等待结果才能进行下一步发送。 普通异步发送 首先导入所需的k…...

解析奥地利 XARION激光超声检测系统:无膜光学麦克风 + 无耦合剂的技术协同优势及多元应用
在工业制造领域,无损检测(NDT)的精度与效率直接影响产品质量与生产安全。奥地利 XARION开发的激光超声精密检测系统,以非接触式光学麦克风技术为核心,打破传统检测瓶颈,为半导体、航空航天、汽车制造等行业提供了高灵敏…...

【Linux】自动化构建-Make/Makefile
前言 上文我们讲到了Linux中的编译器gcc/g 【Linux】编译器gcc/g及其库的详细介绍-CSDN博客 本来我们将一个对于编译来说很重要的工具:make/makfile 1.背景 在一个工程中源文件不计其数,其按类型、功能、模块分别放在若干个目录中,mak…...

Java数组Arrays操作全攻略
Arrays类的概述 Java中的Arrays类位于java.util包中,提供了一系列静态方法用于操作数组(如排序、搜索、填充、比较等)。这些方法适用于基本类型数组和对象数组。 常用成员方法及代码示例 排序(sort) 对数组进行升序…...