C++ STL学习之【优先级队列】

news2024/11/24 3:12:24

✨个人主页: 北 海
🎉所属专栏: C++修行之路
🎃操作环境: Visual Studio 2019 版本 16.11.17


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、优先级队列的使用
      • 1.1、基本功能
      • 1.2、优先级模式切换
      • 1.3、相关题目
    • 2、模拟实现优先级队列
      • 2.1、构造函数
      • 2.2、基本功能
      • 2.3、仿函数的使用
      • 2.4、特殊场景
    • 3、源码
  • 🌆总结


🌇前言

优先级队列 priority_queue 是容器适配器中的一种,常用来进行对数据进行优先级处理,比如优先级高的值在前面,这其实就是初阶数据结构中的 ,它俩本质上是一样东西,底层都是以数组存储的完全二叉树,不过优先级队列 priority_queue 中加入了 泛型编程 的思想,并且属于 STL 中的一部分

这就是一个堆,最顶上的石头 优先级最高优先级最低

堆


🏙️正文

1、优先级队列的使用

首先需要认识一下优先级队列 priority_queue

简介

1.1、基本功能

优先级队列的构造方式有两种:直接构造一个空对象通过迭代器区间进行构造

图解
直接构造一个空对象

#include <iostream>
#include <vector>
#include <queue>	//注意:优先级队列包含在 queue 的头文件中

using namespace std;

int main()
{
	priority_queue<int> pq;	//直接构造一个空对象,默认为大堆
	cout << typeid(pq).name() << endl;	//查看类型
	return 0;
}

结果
注意: 默认比较方式为 less,最终为 优先级高的值排在上面(大堆

通过迭代器区间构造对象

#include <iostream>
#include <vector>
#include <queue>	//注意:优先级队列包含在 queue 的头文件中

using namespace std;

int main()
{
	vector<char> vc = { 'a','b','c','d','e' };
	priority_queue<char, deque<char>, greater<char>> pq(vc.begin(), vc.end());	//现在是小堆
	cout << typeid(pq).name() << endl;	//查看类型
	cout << "==========================" << endl;
	while (!pq.empty())
	{
		//将小堆中的堆顶元素,依次打印
		cout << pq.top() << " ";
		pq.pop();
	}
	return 0;
}

结果
注意: 将比较方式改为 greater 后,生成的是 小堆,并且如果想修改比较方式的话,需要指明模板参数2 底层容器,因为比较方式位于模板参数3,不能跳跃缺省(遵循缺省参数规则)

测试数据:27,15,19,18,28,34,65,49,25,37 分别生成大堆与小堆

大堆

vector<int> v = { 27,15,19,18,28,34,65,49,25,37 };
priority_queue<int, vector<int>, less<int>> pq(v.begin(), v.end());
//priority_queue<int> pq(v.begin(), v.end());	//两种写法结果是一样的,默认为大堆

图示

小堆

vector<int> v = { 27,15,19,18,28,34,65,49,25,37 };
priority_queue<int, vector<int>, greater<int>> pq(v.begin(), v.end());	//生成小堆

图示

接下来使用优先级队列(以大堆为例)中的各种功能:入堆出堆查看堆顶元素查看堆中元素个数

图示

#include <iostream>
#include <vector>
#include <queue>	//注意:优先级队列包含在 queue 的头文件中

using namespace std;

void Print(const priority_queue<int>& pq)
{
	cout << "是否为空:" << pq.empty() << endl;
	cout << "堆中的有效元素个数:" << pq.size() << endl;
	cout << "堆顶元素:" << pq.top() << endl;
	cout << "=================" << endl;
}

int main()
{
	vector<int> v = { 27,15,19,18,28,34,65,49,25,37 };
	priority_queue<int> pq(v.begin(), v.end());	//默认生成大堆
	Print(pq);

	pq.push(10);
	pq.push(100);
	Print(pq);

	pq.pop();
	pq.pop();
	pq.pop();
	Print(pq);

	return 0;
}

结果

1.2、优先级模式切换

创建优先级队列时,默认为 大堆,因为比较方式(仿函数)缺省值为 less,这个设计比较反人类,小于 less 是大堆,大于 greater 是小堆…

如果想要创建 小堆,需要将比较方式(仿函数)改为 greater

注意: 因为比较方式(仿函数) 位于参数3,而参数2也为缺省参数,因此如果想要修改参数3,就得指明参数2

讲人话就是想改变比较方式的话,需要把参数2也写出来,这个设计也比较反人类,明明只改一个比较方式,为什么要写明底层容器…

priority_queue<int> pqBig;	//大堆
priority_queue<int, vector<int>, greater<int>> pqSmall;	//小堆

1.3、相关题目

优先级队列(堆)可以用来进行排序和解决 Top-K 问题,比如 查找第 k 个最大的值 就比较适合使用优先级队列

215. 数组中的第K个最大元素

题目

思路:利用数组建立大小为 k 的小堆,将剩余数据与堆顶值比较,如果大于,就入堆

  • 为什么建小堆?因为此时需要的是最大的值,建大堆可能会导致次大的值无法入堆
#include <queue>

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        //建堆
        priority_queue<int, vector<int>, greater<int>> pq(nums.begin(), nums.begin() + k);

        //将剩余元素判断入堆
        auto it = nums.begin() + k;
        while(it != nums.end())
        {
            if(*it > pq.top())
            {
                pq.pop();   //出小的值
                pq.push(*it);   //入大的值
            }

            it++;
        }

        //此时的堆顶元素,就是第 k 个最大元素
        return pq.top();
    }
};

结果
优先级队列非常适合用来解决类似问题


2、模拟实现优先级队列

优先级队列 priority_queue 属于容器适配器的一种,像栈和队列一样,没有迭代器,同时也不需要实现自己的具体功能,调用底层容器的功能就行了,不过因为堆比较特殊,需要具备 向上调整向下调整 的能力,确保符合堆的规则

2.1、构造函数

注: 现在实现的是没有仿函数的版本

优先级队列的基本框架为

#pragma once

#include <vector>

namespace Yohifo
{
	//默认底层结构为 vector
	template<class T, class Container = std::vector<T>>
	class priority_queue
	{
	public:
		//构造函数及其他功能
	private:
		Container _con;	//其中的成员变量为底层容器对象
	};
}

默认构造函数:显式调用底层结构的默认构造函数

//默认构造函数
priority_queue()
	:_con()
{}

迭代器区间构造:将区间进行遍历,逐个插入即可

//迭代器区间构造
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
	:_con()
{
	while (first != last)
	{
		push(*first);
		first++;
	}
}

测试:

结果

2.2、基本功能

因为是容器适配器,所以优先级队列也没有迭代器

同时基本功能也比较少,首先来看看比较简单的容量相关函数

容量相关

判断是否为空:复用底层结构的判空函数

//判断是否为空
bool empty() const
{
	return _con.empty();
}

获取优先级队列大小:复用获取大小的函数

//优先级队列的大小(有效元素数)
size_t size() const
{
	return _con.size();
}

获取堆顶元素:堆顶元素即第一个元素(完全二叉树的根)

//堆顶元素(优先级最 高/低 的值)
const T& top() const
{
	return _con.front();
}

注意: 以上三个函数均为涉及对象内容的改变,因此均使用 const 修饰 this 指针所指向的内容

数据修改

因为在插入/删除数据后,需要确保堆能符合要求

  • 大堆:父节点比子节点大
  • 小堆:父节点比子节点小

因此每进行一次数据修改相关操作,都需要检查当前堆结构是否被破坏,这一过程称为 调整

插入数据:尾插数据,然后向上调整

//插入数据
void push(const T& val)
{
	//直接尾插,然后向上调整
	_con.push_back(val);
	adjust_up(size() - 1);	//从当前插入的节点处进行调整
}

向上调整:将当前子节点与父节点进行比较,确保符合堆的特性,如果不符合,需要进行调整

//向上调整
void adjust_up(size_t child)
{
	size_t parent = (child - 1) / 2;

	while (child != 0)
	{
		//父 > 子 此时为大堆,如果不符合,则调整
		if (_con[child] > _con[parent])
		{
			std::swap(_con[child], _con[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

注意: 如果在调整过程中,发现遵循堆的特性,那么此时不需要再调整,直接 break 即可

删除数据:将堆顶数据交换至堆底,删除堆底元素,再向下调整堆

//删除堆顶元素(优先级最 高/低 的值)
void pop()
{
	if (empty()) 
		return;

	//将堆顶元素交换至堆底删除,向下调整
	std::swap(_con.front(), _con.back());
	_con.pop_back();
	adjust_down(0);
}

向下调整:将当前父节点与 【较大 / 较小】 子节点进行比较,确保符合堆的特性,如果不符合,需要进行调整

//向下调整
void adjust_down(size_t parent)
{
	size_t child = parent * 2 + 1;	//假设左孩子为 【大孩子 / 小孩子】

	while (child < size())
	{
		//判断右孩子是否比左孩子更符合条件,如果是,则切换为与右孩子进行比较
		if (child + 1 < size() && _con[child + 1] > _con[child])
			child++;

		//父 > 子 此时为大堆,如果不符合,则调整
		if (_con[child] > _con[parent])
		{
			std::swap(_con[child], _con[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;	//满足条件时,一样需要跳出,不再调整
	}
}

注意: 删除时,需要先判断当前堆是否为空,空则不执行删除

测试:

图示

假设先使用 小堆,需要将下图中的三处逻辑判断,改为 <

结果

难道每次使用时都得手动切换吗?而且如果我想同时使用大堆和小堆时该怎么办?

  • 答案是没必要,通过 仿函数 可以轻松解决问题,这也是本文的重点内容

2.3、仿函数的使用

仿函数又名函数对象 function objects,仿函数的主要作用是 借助类和运算符重载,做到同一格式兼容所有函数 这有点像函数指针,相比于函数指针又长又难理解的定义,仿函数的使用可谓是很简单了

下面是两个仿函数,作用是比较大小

template<class T>
struct less
{
	//比较 是否小于
	bool operator()(const T& x, const T& y)
	{
		return x < y;
	}
};

template<class T>
struct greater
{
	//比较 是否大于
	bool operator()(const T& x, const T& y)
	{
		return x > y;
	}
};

此时 priority_queue 中的模板参数升级为3个,而参数3的缺省值就是 less

template<class T, class Container = std::vector<T>, class Comper = less<T>>

当需要进行逻辑比较时(大小堆需要不同的比较逻辑),只需要调用 operator() 进行比较即可

这里采用的是匿名对象调用的方式,当然也可以直接实例化出一个对象,然后再调用 operator() 进行比较

在使用仿函数后,向上调整向下调整 变成了下面这个样子

//向上调整
void adjust_up(size_t child)
{
	size_t parent = (child - 1) / 2;

	while (child != 0)
	{
		//父 > 子 此时为大堆,如果不符合,则调整
		if (Comper()(_con[parent], _con[child]))	//Comper() 为匿名对象
		{
			std::swap(_con[child], _con[parent]);
			child = parent;
			parent = (child - 1) / 2;
		}
		else
			break;
	}
}

//向下调整
void adjust_down(size_t parent)
{
	size_t child = parent * 2 + 1;	//假设左孩子为 【大孩子 / 小孩子】

	while (child < size())
	{
		//判断右孩子是否比左孩子更符合条件,如果是,则切换为与右孩子进行比较
		//同样使用匿名对象
		if (child + 1 < size() && Comper()(_con[child], _con[child + 1]))
			child++;

		//父 > 子 此时为大堆,如果不符合,则调整
		if (Comper()(_con[parent], _con[child]))	//匿名对象调用 operator()
		{
			std::swap(_con[child], _con[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
			break;	//满足条件时,一样需要跳出,不再调整
	}
}

使用仿函数后,可以轻松切换为小堆

图解

注意: 为了避免自己写的仿函数名与库中的仿函数名起冲突,最好加上命令空间,访问指定域中的仿函数

仿函数作为 STL 六大组件之一,处处体现着泛型编程的思想

图解

仿函数给我们留了很大的发挥空间,只要我们设计的仿函数符合调用规则,那么其中的具体比较内容可以自定义(后续在进行特殊场景的比较时,作用很大)

2.4、特殊场景

假设此时存在 日期类(部分)

class Date

class Date
{
public:
	Date(int year = 1970, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	bool operator<(const Date& d)const
	{
		return (_year < d._year) ||
			(_year == d._year && _month < d._month) ||
			(_year == d._year && _month == d._month && _day < d._day);
	}
	bool operator>(const Date& d)const
	{
		return (_year > d._year) ||
			(_year == d._year && _month > d._month) ||
			(_year == d._year && _month == d._month && _day > d._day);
	}
	friend std::ostream& operator<<(std::ostream& _cout, const Date& d)
	{
		_cout << d._year << "-" << d._month << "-" << d._day;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

创建数据为 Date 的优先级队列(大堆),取堆顶元素(判断是否能对自定义类型进行正确调整)

void TestPriorityQueue3()
{
	Yohifo::priority_queue<Date> q1;
	q1.push(Date(2012, 3, 11));
	q1.push(Date(2012, 3, 12));
	q1.push(Date(2012, 3, 13));
	cout << q1.top() << endl;	//取堆顶元素
}

结果:正确,因为在实际比较时,调用的是 Date 自己的比较逻辑,所以没问题

结果

但如果此时数据为 Date*,再进行比较

void TestPriorityQueue4()
{
	//数据类型为指针
	Yohifo::priority_queue<Date*> q1·;
	q1.push(new Date(2012, 3, 11));
	q1.push(new Date(2012, 3, 12));
	q1.push(new Date(2012, 3, 13));
	cout << *(q1.top()) << endl;
}

结果:错误,多次运行结果不一样!因为此时调用的是指针的比较逻辑(地址是随机的,因此结果也是随机的)

结果
解决方法:

  1. 通过再编写指针的仿函数解决
  2. 通过模板特化解决

这里介绍法1,法2在下篇文章《模板进阶》中讲解

仿函数给我们提供了极高的自由度,因此可以专门为 Date* 编写一个仿函数(曲线救国)

//小于
template<class T>
struct pDateLess
{
	bool operator()(const T& p1, const T& p2)
	{
		return (*p1) < (*p2);
	}
};

//大于
template<class T>
struct pDateGreater
{
	bool operator()(const T& p1, const T& p2)
	{
		return (*p1) > (*p2);
	}
};

在构建对象时,带上对对应的 仿函数 就行了

void TestPriorityQueue5()
{
	//数据类型为指针
	Yohifo::priority_queue<Date*, vector<Date*>, pDateLess<Date*>> qBig;
	qBig.push(new Date(2012, 3, 11));
	qBig.push(new Date(2012, 3, 12));
	qBig.push(new Date(2012, 3, 13));
	cout << *(qBig.top()) << endl;

	Yohifo::priority_queue<Date*, vector<Date*>, pDateGreater<Date*>> qSmall;
	qSmall.push(new Date(2012, 3, 11));
	qSmall.push(new Date(2012, 3, 12));
	qSmall.push(new Date(2012, 3, 13));
	cout << *(qSmall.top()) << endl;
}

此时无论是 大堆 还是 小堆 都能进行正常比较

结果

关于 Date* 仿函数的具体调用过程,可以自己下去通过调试观察


3、源码

本文中提及的所有源码都在此仓库中 《优先级队列博客》

结果


🌆总结

以上就是本次关于 C++ STL学习之【优先级队列】的全部内容了,在本文中,我们又学习了一种容器适配器 priority_queue,优先级队列在对大量数据进行 Top-K 筛选时,优势是非常明显的,因此需要好好学习,尤其是向上调整和向下调整这两个重点函数;最后我们还见识了仿函数的强大之处,容器在搭配仿函数后,能做到更加灵活,适应更多需求


星辰大海

相关文章推荐

STL 之 适配器

C++ STL学习之【反向迭代器】

C++ STL学习之【容器适配器】

===============

STL 之 list

C++ STL学习之【list的模拟实现】

C++ STL学习之【list的使用】

===============

STL 之 vector

C++ STL学习之【vector的模拟实现】

C++ STL学习之【vector的使用】

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

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

相关文章

蛋白质界的 ChatGPT:AlphaFold1 论文必备知识,不会有人还不知道吧

你知道 AlphaFold2 吗&#xff1f;它真正解决了蛋白质三维结构预测的算法困境&#xff0c;堪称蛋白质界的 chat-GPT4&#xff0c;甚至它的意义不是 chat-GPT4 所能够匹敌的。它为世界疾病治疗药物开发以及探究生物生命之谜提供了通向天神的一条道路&#xff0c;未来是生物的世纪…

Android开机时间工具分析

背景 android 上面有很多的方法可以分析开机时间 比如打log&#xff0c;通过log 分析。android 的官网上面提供了下面的两种图形化的方式来分析开机时间&#xff0c;一些异常很明显的拖长整个开机时间的活动 可以很容易就看出来。 问题 android 官网和网上的教程很多都不适用于…

【解决方案】基于边缘视频AIBox的校园立体防控解决方案

基于边缘AIBox的校园立体防控解决方案 一、方案背景 智慧校园安全防控系统是一款围绕学校周界安全、出入口安全、人身安全、消防安全、财产安全等校园安全场景打造的主动智能预警防控系统。它利用人工智能技术对校内及周边环境进行全天候24h实时监测和智能分析&#xff0c;对…

Linux最常用的15个基本命令

目录 Linux基本命令 命令1&#xff1a;ls &#xff08;查看指定目录中有哪些内容&#xff09; ls / 相当于查看根目录中的内容&#xff0c;相当于查看我的电脑 ls -l&#xff08;小写l&#xff0c;或者使用ll&#xff09;详细查看目录下所有内容 ls /usr/lib&#xff08…

AI面试必刷算法题 附答案和解析 --持续更新中

面试中发现很多同学一股脑优化、润色项目经历&#xff0c;但聊到基本的算法&#xff0c;反而会一脸懵X&#xff0c;得空整理下算法题给大家&#xff0c;希望对你有帮助。 1. tail(head(tail(C))) ( ) 已知广义表: A(a,b), B(A,A), C(a,(b,A),B), 求下列运算的结果:&#xff08…

vue 做一个文本展示 点击文本弹出element ui的时间选择器 但不会出现element ui时间组件的那个输入框

我们先来创建一个vue2项目 引入element ui 然后 找到一个组件 这样写 <template><div><el-date-pickerv-model"value"type"datetimerange"align"right"unlink-panelsrange-separator"至"start-placeholder"开始日…

ext-3 怎么将PDK的库包添加到CCS工程中

第一次接触ccs和A8这个库&#xff0c;PDK工具包的库是啥后缀&#xff0c;怎么添加到工程里&#xff1f;等等&#xff0c;这些摸索了好久&#xff0c;这里记录一下&#xff01;&#xff08;这里的编译器都选则的是GNU&#xff0c;非TI自带的编译器&#xff09; 目录 1、问题来…

UNIX网络编程卷一 学习笔记 第十一章 名字与地址转换

到目前为止&#xff0c;本书中所有例子都用数值地址表示主机&#xff08;如206.6.226.33&#xff09;&#xff0c;用数值端口号来标识服务器&#xff08;如端口13代表daytime服务器&#xff09;。但出于某些理由&#xff0c;我们应使用名字而非数值&#xff1a;名字比较容易记住…

编译链接再认识+gdb认识+makefile了解

索引 一. 编译链接再认识1.预处理2.编译3.汇编4.链接1.静态链接2.动态链接 二.gdb三.makefile/make 一. 编译链接再认识 主要针对gcc展开 一个文件从源文件编译成可执行文件大致要经历四个步骤 预处理&#xff08;进行宏替换&#xff09;编译&#xff08;生成汇编&#xff09…

[Pandas] 创建透视表与交叉表

1.生成透视表 在使用Python处理数据时&#xff0c;我们希望能够快速地进行排列与计算数据&#xff0c;从而帮助我们更有效的分析数据&#xff0c;pivot_table函数可以实现Excel数据透视表的功能 基本语法格式 pd.pivot_table(data, valuesNone, indexNone, columnsNone, agg…

【51单片机】使用STC烧录软件生成定时器的代码以及注意事项

&#x1f38a;专栏【51单片机】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【Love Story】 &#x1f970;大一同学小吉&#xff0c;欢迎并且感谢大家指出我的问题&#x1f970; 目录 1.点击定时器计算器​编辑 2.每次打开的时…

华为MPLS跨域C1方式RR场景(数据不经过RR)实验配置

目录 配置BGP邻居的建立 配置MPLS LDP 配置RR之间的MP-BGP邻居 配置通过ASBR学习PE路由并为PE分配标签&#xff08;实现Vpnv4路由的传递和数据传输&#xff09; 根据图配置接口的IP地址和IGP协议 BGP邻居用来传递标签和RR、PE的路由 MP-BGP邻居用来传递Vpnv4路由 配置BG…

QGC分析(一)-整体分析和通信流程

参考QGroundControl开发指南 设计理念 QGC用户界面是用QML实现的。 构建 可以按照这一篇来在Windows中下载源码并且编译。QGroungControl在QT中源码编译&#xff08;包括配置环境&#xff09; 通信流程 src/comm/LinkManager.cc LinkManager始终打开UDP端口&#xff0c;…

Vue3中如何实现数字翻牌效果?

一、需求 监听数据的变化&#xff0c;实现数字翻牌效果 本人OS:本想截一个gif&#xff0c;但是一直没找到合适的截gif工具......有好用的截gif工具&#xff0c;跪求戳戳我~ 二、思路 1.设置初始数组&#xff1a;[0] 2. 把获取到的新数据整个数字用逗号隔开&#xff0c;组成…

让Bito帮你写Mokito单元测试

前言 现在稍微大一点的公司应该都有单测覆盖率要求&#xff0c;比如核心工程单测覆盖率95%以上&#xff0c;非核心工程90%以上。单测可以降低开发错误的反馈回路&#xff0c;减少重复工作&#xff0c;提升开发效率。但是写单测对于开发来说需要额外的时间。我们可以用TestMe、…

AdaBoost算法介绍和代码实现

AdaBoost算法介绍和代码实现 算法原理 AdaBoost算法的核心思想是将弱分类器组合成一个强分类器。在每一轮迭代中&#xff0c;AdaBoost会训练一个新的弱分类器并调整每个样本的权重&#xff0c;使得之前分类错误的样本在下一轮迭代中受到更多的关注。最终&#xff0c;AdaBoost…

认识监听器(Listener)

监听器是什么&#xff1f; 监听器&#xff08;Listener&#xff09;是一种运行在后台的程序&#xff0c;它主要用于监控某些事件在系统中的发生&#xff0c;并且根据这些事件做一些特定的处理。在Web应用程序中&#xff0c;监听器可以观察ServletContext、HttpSession以及Serv…

PostgreSQL-数值类型

数值类型是最常用的几种数据类型之一&#xff0c;主要分为&#xff1a; 整型浮点型精确小数 数值类型介绍 数值类型列表 类型名称存储空间描述范围smallint2字节小范围的整数。Oracle中没有此数值类型&#xff0c;使用number代替-2^15 ~ 2^15-1int 或 integer4字节常用的整数…

日常开发为什么需要做Code Review

日常开发为什么需要做Code Review 一、背景 最近在开始一个新的项目&#xff0c;在查看项目中代码及具体细节时&#xff0c;发现这个项目真实一堆乱麻&#xff0c;没有规律可循&#xff0c;可总结下这个项目的缺陷 没有规律可循&#xff0c;没有结构性设计不做公共封装&#…

08_Uboot顶层Makefile分析_make过程

目录 make 过程 make 过程 配置好 uboot 以后就可以直接make 编译了,因为没有指明目标,所以会使用默认目标,主 Makefile 中的默认目标如下: 目标_all 又依赖于all,如下所示: 如果KBUILD_EXTMOD为空的话_all 依 赖 于all 。这 里 不 编 译 模 块,所 以KBUILD_EXTMOD肯定为空,_…