红黑树 - c++

news2024/11/19 5:48:08

文章目录:

  • 红黑树的介绍
  • 红黑树节点定义
  • 红黑树的插入操作
  • 红黑树的删除
  • 红黑树的验证
  • 红黑树 vs AVL树

红黑树的介绍

红黑树(Red-Black-Tree),通常写为 R-B Tree。它是一种特殊的二叉搜索树。红黑树的每个节点上都有一个存储位来标识节点的颜色,可以是红(Red)或者黑(Black)。通过对任意一条从根到叶子的路径上各个节点颜色的限制,红黑树确保没有一条路径比其它路径长出两倍,因此是接近平衡的。
在这里插入图片描述

红黑树的特性:

  1. 每个节点只能是黑色或者红色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子节点必须是黑色的
  4. 对于每一个节点,从该节点到其它所有后代叶节点的简单路径上均包含相同数目的黑色节点。
  5. 每一个叶子节点(NIL)是黑色的,注意:这里的叶子节点指的是空节点

为什么满足以上性质就能确保树中最长的路径中节点个数不会超过最短路径中节点个数的两倍?

由性质3和性质4可以看出,红黑树中不会出现连续的红节点,且从某一节点到其所有叶子节点路径上包含的黑色节点数目相等。

最短路径:全部由黑色节点组成
最长路径:一黑一红节点组成,红色节点的数量等于黑色节点的数量

红黑树节点定义

这里的红黑树我们实现为 kv 模型,为了便于后续的操作,我们将红黑树定义为三叉链结构。使用枚举的方法标识节点的颜色,增加代码的可读性。

enum Color
{
	RED,
	BLACK,
};

template<class K,class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;

	// 存储数据的键值对
	pair<K, V> _kv;
	// 存储节点颜色
	Color _col;

	RBTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED)
	{}
};

为什么在节点的构造函数中将节点初始化为红色,而不是黑色呢?

  • 若新增到红黑树中的节点为红色,此时它的父节点也正好是红色节点,即出现了连续的红色节点,破坏了规则3。若其父节点本身是黑色的,那么我们就不需要进行调整。
  • 若新增到红黑树中的节点为黑色,那么插入新增节点的这条路径上就比其它的路径多了一个黑色节点,破坏了规则4。因此我们必须对其进行调节。

总结插入红色节点可能破坏规则3,插入黑色节点一定破坏规则4。规则4被破坏即影响其它路径,影响面积非常大,若要使其再次满足红黑树的规则将很难进行调节。因此我们插入新的节点都初始化为红色,便于后续调节使其满足红黑树的规则。

红黑树的插入操作

将一个节点插入到红黑树中,需要执行哪些操作步骤呢?

1️⃣ 按照二叉搜索树的规则,将节点插入到对应的位置。
2️⃣ 检测插入新节点之后树是否满足红黑树的规则。
3️⃣ 若插入节点的父节点是红色,则需要对树进行一系列的旋转和变色操作,使其满足红黑树的规则。


🎯新插入的节点颜色默认为红色。因此:若其父节点的颜色是黑色,没有违反红黑树的规则,不需要进行调整;但当插入的节点的父节点颜色是红色时,就违反了规则3不能有连续的红节点。这时需要对红黑树分情况来处理。具体如何对其进行处理,主要看新增节点的叔叔节点(即新增节点父节点的兄弟节点),根据新增节点叔叔颜色的不同,可将红黑树的调整分为三种情况。

注意:插入节点的父节点是红色,说明了其父节点不是根节点,因此插入节点的父节点的父节点(即祖父节点)就一定存在。
规定:cur为新增节点,p为其父节点,g为其祖父节点(即p的父节点),u为叔叔节点。

情况1:新增节点的叔叔存在且为红色;cur为红,p为红,g为黑,u为红

情况1的抽象图如下所示:
在这里插入图片描述

插入 cur 节点之后,出现了连续的红色节点,因此我们对此进行了处理。将其父节点变为黑色,为了保证每条路径上黑色节点的数量相等,需要将其叔叔节点(u)也变为黑色,然后将祖父节点(g)变为红色。但是调整并没有结束,这里需要对祖父(g)节点进行判断:

若 g 是根节点,调整完成之后,需要将其变为黑色。
若 g 是一棵子树的根节点,需要将祖父当作新插入的节点,继续按照相应的规则向上调整。

叔叔(u)存在且为红时,我们不需要考虑新增节点是父亲的左孩子还是右孩子,因为红黑树以颜色标识节点,所以调整方法是一样的。

情况2:新增节点的叔叔存在且为黑色;cur为红,p为红,g为黑,u为黑

这种情况是在情况1向上调整的过程中出现的,cur 不可能是新增节点,而是在情况1调节过程中更新的祖父节点(g)。因为新增节点之前,此二叉搜索树是满足红黑树的规则的,若 cur 是新增节点,那么该树在新增节点之前已经违反了红黑树的规则了(从任意一个节点出发,以它为根节点的所有路径上的黑色节点相等),即路径上黑色节点的数量不相等。因此,cur 不可能是新增节点,
在这里插入图片描述
若叔叔(u)存在且为黑色时,仅仅是对节点进行变色已经无法处理了,这时我们需要按照相应的规则对其进行旋转处理。因为涉及到旋转,因此我们需要考虑 cur 是在父节点的左孩子和右孩子两种情况。

✔️若 cur 为 p 的左孩子,需要先进行右单旋,然后进行颜色的调整。调整完成之后这棵子树的根节点变成了黑色,则不需要继续往上进行处理了。

其抽象图如下所示:
在这里插入图片描述
处理步骤:
step1:以 g 为旋转点进行右单旋
step2:将 g 变成红色,p 变成黑色

注意:若 g 的右孩子是 p , p的右孩子是 cur 时,这时呈现出的是一条向左倾斜的直线,这是我们先以 p 为旋转点进行左单旋,然后以同样的方式进行颜色的调节。

✔️若 cur 为 p 的右孩子,需要先进行左右双旋,然后进行颜色的调整。调整完成之后这棵子树的根节点变成了黑色,则不需要继续往上进行处理了。

其抽象图如下所示:
在这里插入图片描述
处理步骤:
step1:以 p 为旋转点进行左单旋
step2:以 g 为旋转点进行右单旋
step3:将 g 变成红色,cur 变成黑色

注意:若 g 的右孩子是 p , p的左孩子是 cur 时,这时呈现出的是一条形状类似于大于符号的折线,这时我们先以 g 为旋转点进行右单旋,再以 p 为旋转点进行左单旋,然后以同样的方式进行颜色的调节。

情况3:新增节点的叔叔不存在

若 u 不存在,则 cur 一定是新增节点,若 cur 不是新增节点,则 cur 和 p 一定有一个节点的颜色是黑色,不满足性质4:每条路径黑色节点个数相同。

✔️若新增节点在 p 的左边,即呈现出来的是一条向右倾斜的直线。这时我们需要对其进行右单旋然后再进行变色处理。

其抽象图如下所示:
在这里插入图片描述
处理步骤:
step1:以 g 为旋转点进行右单旋
step2:将 g 变成红色,p 变成黑色

注意:若 g 的右节点为 p , p 的右节点为 cur 。此时呈现出一条向左倾斜的直线,这是我们需要进行左单旋处理,然后调整节点颜色。

✔️若新增节点在 p 的右边,即呈现出来的是一条类似于小于符号的折线。这时我们需要对其进行左右双旋然后再进行变色处理。
在这里插入图片描述
处理步骤:
step1:以 p 为旋转点进行左单旋
step2:以 g 为旋转点进行右单旋
step3:将 g 变成红色,cur 变成黑色

注意:若 g 的右节点为 p , p 的左节点为 cur 。这时呈现出的是一条形状类似于大于符号的折线,这是我们先以 p 为旋转点进行右单旋,再以 g 为旋转点进行左单旋,然后以同样的方式进行颜色的调节。

实际上情况三的处理和情况二是一样的

红黑树新增节点代码实现:

pair<Node*, bool> insert(const pair<K, V>& kv)
{
	// 一开始插入新节点时树为空,则直接让新节点作为根节点
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return make_pair(_root, true);
	}

	Node* cur = _root;
	Node* parent = _root;
	while (cur)
	{
		if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
			// 待插入的节点已经存在,则返回该节点的指针,插入失败
			return make_pair(cur, false);
	}

	// 走到这里就已经找到了将要插入的位置
	Node* newnode = new Node(kv);
	newnode->_col = RED;
	// 判断待插入节点与parent指向值的大小,将待插入节点插入到正确的位置
	if (parent->_kv.first < kv.first)
	{
		parent->_right = newnode;
		newnode->_parent = parent;
	}
	else
	{
		parent->_left = newnode;
		newnode->_parent = parent;
	}
	cur = newnode; // newnode节点不动,便于后续返回

	// 若父亲存在且为红色就需要进行处理
	while (parent&& parent->_col == RED)
	{
		// 若父节点为红色,则祖父节点一定存在,不需要进行判断
		Node* grandfather = parent->_parent;
		if (parent == grandfather->_left)
		{
			Node* uncle = grandfather->_right;
			//情况1:uncle存在且为红
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				// 继续向上进行处理
				cur = grandfather;
				parent = cur->_parent;
			}
			else // 情况2+3:uncle不存在或者uncle存在且为黑
			{
				// 情况2:右单旋
				if (cur == parent->_left)
				{
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else // 左右双旋
				{
					RotateL(parent);
					RotateR(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break; // 这两种情况处理完成已经符合红黑树规则了,不需要继续往上处理了
			}
		}
		else // grandfather->_right == parent;
		{
			Node* uncle = grandfather->_left;
			// 情况1
			if (uncle&& uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				// 继续向上进行处理
				cur = grandfather;
				parent = cur->_parent;
			}
			else // 情况2+3
			{
				if (cur == parent->_righ)
				{
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else // cur == parent->_left
				{
					RotateR(parent);
					RotateL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break; // 这两种情况处理完成已经符合红黑树规则了,不需要继续往上处理了
			}
		}
	}

	// 将根节点的颜色处理为黑色
	_root->_col = BLACK;
	return make_pair(newnode, true);
}

// 旋转逻辑和AVL树的旋转是一样的
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* parentParent = parent->_parent;

	// 让parent的右指针指向subRL,判断一下subRL是否为空
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	// subR的左指针链接parent
	subR->_left = parent;
	parent->_parent = subR;

	// parent为根的情况,更新根节点,让根节点指向空
	if (_root == parent)
	{
		_root = subR;
		_root->_parent = nullptr;
	}
	else //若parent为一棵子树,则链接与parentParent的关系
	{
		if (parentParent->_right == parent)
			parentParent->_right = subR;
		else
			parentParent->_left = subR;

		subR->_parent = parentParent;
	}
}

void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* parentParent = parent->_parent;

	// 将subLR链接到parent的左边,这里注意subLR可能为空的情况,需要判断一下
	parent->_left = subLR;
	if (subLR)
	{
		subLR->_parent = parent;
	}

	// 将parent这棵子树链接到subL的右指针
	subL->_right = parent;
	parent->_parent = subL;

	// 若parent为根,则更新新的根节点
	if (parent == _root)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else //若parent为一棵子树,则链接与parentParent的关系
	{
		if (parentParent->_right == parent)
			parentParent->_right = subL;
		else
			parentParent->_left = subL;

		subL->_parent = parentParent;
	}
}

红黑树的删除

红黑树的删除情况比较复杂,这里就不做讲解了,具体详解及其实现可参考:红黑树删除
在这里插入图片描述

红黑树的验证

红黑树是从二叉搜索树变换过来的,因此它是满足二叉搜索树规则的,因此可检测其是否符合二叉搜索树的规则(中序遍历为有序序列)。然后检测其是否符合红黑树的特性。

1.使用中序遍历去检测其中序遍历是否有序。

void _InOrder(Node* root)
{
	if (root == nullptr)
		return;

	_InOrder(root->_left);
	cout << root->_kv.first << " -> " << root->_kv.second << endl;
	_InOrder(root->_right);
}

void InOrder()
{
	_InOrder(_root);
}

2.检测树中最长路径是否超过最短路径的两倍。

// 计算树中最长路径的长度
int _maxHeight(Node* root)
{
	if (root == nullptr)
		return 0;

	int leftHeight = _maxHeight(root->_left);
	int rightHeight = _maxHeight(root->_right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

// 计算树中最短路径的长度
int _minHeight(Node* root)
{
	if (root == nullptr)
		return 0;

	int leftHeight = _minHeight(root->_left);
	int rightHeight = _minHeight(root->_right);

	return leftHeight < rightHeight ? leftHeight + 1 : rightHeight + 1;
}

void height()
{
	cout << "最长路径:" << _maxHeight(_root) << endl;
	cout << "最短路径:" << _minHeight(_root) << endl;
}

3.检测该树是否符合红黑树的性质。

bool _CheckBlance(Node* root, int blackNum, int count)
{
	// 走到nullptr之后,判断count和blackNum是否相等
	if (root == nullptr)
	{
		if (count != blackNum)
		{
			cout << "违反性质4:每条路径上的黑色节点个数必须相同" << endl;
			return false;
		}
		return true;
	}

	// 检测当前节点与其父节点是否都为红色
	if (root->_col == RED && root->_parent && root->_parent->_col == RED)
	{
		cout << "存在连续红色节点,违反性质3" << endl;
		return false;
	}

	// 统计黑色节点数量
	if (root->_col == BLACK)
		++count;

	return _CheckBlance(root->_left, blackNum, count) && _CheckBlance(root->_right, blackNum, count);
}

bool CheckBlance()
{
	// 空树也是红黑树
	if (_root == nullptr)
		return true;
	if (_root->_col == RED)
	{
		cout << "根节点为红色,违反性质二" << endl;
		return false;
	}

	// 找最左路径黑色节点数量做参考值 - 比较基准值
	int blackNum = 0;
	Node* left = _root;
	while (left)
	{
		if (left->_col == BLACK)
			blackNum++;
		left = left->_left;
	}

	// 检测是否满足红黑树性质,count用来记录路径中黑色节点数量
	int count = 0;
	return _CheckBlance(_root, blackNum, count);
}

红黑树 vs AVL树

红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O(logN),红黑树是一种弱平衡二叉搜索树,其只需要保证最长路径不超多最短路径的2倍。相对于要求严格的AVL树而言,降低了插入旋转的次数,所以在经常进行增删查改的结构中红黑树比AVL树性能更优,且红黑树的实现相较而言更加简单,所以实际运用中红黑树更加广泛。红黑树广泛用于C++的STL中,map 和 set 都是红黑树实现的。

🌍增删查改的复杂度:
AVL树:可以稳定在O(logN)
红黑树:一般情况下是O(logN),极端情况下是O(log2N)。它们基本上没有差别。

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

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

相关文章

积木报表—JimuReport v1.5.4版本发布,免费的可视化Web报表工具

项目介绍 一款免费的低代码可视化报表&#xff0c;像搭建积木一样在线拖拽设计&#xff01;低代码开发必备&#xff0c;功能涵盖&#xff0c;数据报表、打印设计、图表报表、大屏设计等&#xff01; 秉承“简单、易用、专业”的产品理念&#xff0c;极大的降低报表开发难度、缩…

【css伪类选择器及透明度——附项目图片及代码】

不知不觉&#xff0c;又鸽了好长时间了&#xff0c;非常抱歉&#xff0c;没办法&#xff0c;毕竟开学了&#xff0c;今天课少&#xff0c;抽出了两个小时写了一篇css的&#xff0c;每天不是被催更&#xff0c;就是在催更的路上。放心&#xff0c;小陈陈有时间一定会给大家分享好…

SVM 支持向量机

SVM 支持向量机SVM 原理最优化问题线性不可分sklearn 调用 SVM核函数SVM 原理 前置知识&#xff1a;用迭代策略来划分样本&#xff0c;请猛击《神经元的计算》。 SVM 也是用一条迭代的直线来划分不同数据之间的边界&#xff1a; .- 是一条直线&#xff08;线性函数&#xff09…

数据结构c语言版第二版(严蔚敏)第五章笔记

目录 树和二叉树的定义 树的定义 树的基本术语 二叉树的定义 二叉树的性质和存储结构 二叉树的性质 二叉树的存储结构 顺序存储结构 链式存储结构 遍历二叉树和线索二叉树 遍历二叉树 先序遍历 中序遍历 后序遍历 前序遍历的递归算法 中序遍历的递归算法 后序…

SARScape中用sentinel-1数据做SBAS-InSAR完整流程(2/2)

书接上回&#xff1a;SARScape中用sentinel-1数据做SBAS-InSAR完整流程&#xff08;1/2&#xff09; SARScape中用sentinel-1数据做SBAS-InSAR完整流程&#xff08;2/2&#xff09;7 反演第一步Inversion&#xff1a;First Step7.1 导入设置7.2 optional file7.3 parameters参数…

齐博x1用户登录接口

用户的登录主要涉及到小程序登录、APP的帐号密码登录、APP的微信开发平台帐号登录。 相应的地址是&#xff1a;http://qb.net/index.php/index/wxapp.login/index.html 涉及到的方法如下 上面的地址&#xff0c;默认是小程序的登录与注册。 http://qb.net/index.php/index/wxa…

matlab/simulink电力电子仿真傅里叶变换模块(fourier)测幅值相角的设置与使用

matlab/simulink电力电子仿真傅里叶变换模块&#xff08;fourier&#xff09;测幅值相角的设置与使用 今天要说的是一个可以测量信号的幅值和相角的模块&#xff0c;fourier&#xff0c;长下面这样&#xff1a; 有时候我们需要求某个信号的幅值或者相位&#xff0c;或求两个…

用文字描述给黑白照上色,这个免费网站火了!网友:比其他同类都好用

金磊 Alex 发自 凹非寺量子位 | 公众号 QbitAI这是清朝末代皇后婉容广为流传的一张老照片&#xff1a;如果让照片变成彩色的&#xff0c;会是什么样子&#xff1f;竟然没有什么违和感&#xff0c;百年前的老照片似乎在此刻变得鲜活了起来。而这张图上色的背后&#xff0c;并没有…

BUUCTF NewStarCTF 公开赛赛道Week5 Writeup

文章目录WEBGive me your photo PLZBabySSTI_ThreeUnsafe ApacheSo Baby RCE AgainFinal roundMISC最后的流量分析奇怪的PDF 2奇怪的文本Yesec no drumsticks 5qsdzs girlfriend 5WEB Give me your photo PLZ 可上传.htaccess AddType application/x-httpd-php .jpg然后上传…

干货!手把手教你穿透内网

干货&#xff01;手把手教你穿透内网干货&#xff01;手把手教你穿透内网cpolar内网穿透使用场景如何使用cpolar内网穿透&#xff1f; ↓↓1. 注册cpolar账号2. 安装cpolar内网穿透2.1 Windows系统2.2 Linux系统2.2.1 安装2.2.2 向系统添加服务2.2.3 启动服务2.2.4 查看服务状态…

生成二维码或条形码JavaScript脚本库

二维码或条形码在日常生活中现在应用已经非常普遍了&#xff0c;文章分享生成条形码和二维码的JavaScript库。 条形码 条形码是日常生活中比较常见的&#xff0c;主要用于商品。通俗的理解就是一串字符串的集合&#xff08;含字母、数字及其它ASCII字符的集合应用&#xff09…

【机器学习基础】 线性回归

线性回归1、线性回归定义2、线性回归题目示例3、推导公式4、误差5、似然函数6、线性回归评价指标7、梯度下降1、线性回归定义 经典统计学习技术中的线性回归和softmax回归可以视为 线性神经⽹络。给定训练数据特征 X 和对应的已知标签 y &#xff0c;线性回归的⽬标是找到⼀组权…

Seata安装启动

一、下载 https://github.com/seata/seata/releases/download/v1.4.2/seata-server-1.4.2.zip 二、启动 在安装路径下cmd seata-server.bat -h 127.0.0.1 -m file 三、作用 Seata是分布事务解决方案&#xff0c;seata保证微服务远程调用业务的原子性 Seata将为用户提供了 …

Spring Cloud LoadBalancer--负载均衡的原理(源码分析)

原文网址&#xff1a;Spring Cloud LoadBalancer--负载均衡的原理&#xff08;源码分析&#xff09;_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍Spring Cloud LoadBalancer负载均衡的原理。 SpringCloud从2020版本开始移除了对Ribbon的依赖&#xff0c;官方使用Spring Cl…

VsCode中一些可以让工作“事半功倍”的插件

1.GitLens — Git supercharged 这个插件可以查看代码修改的消息&#xff0c;比如是谁修改的以及修改时间 2.Chinese (Simplified) (简体中文) 简体中文&#xff0c;这个可以说是装的最多的一款插件了 3.Auto Close Tag 标签自动补全 4.Auto Rename Tag&#xff1a;自动完…

2021第7届中国大学生程序设计竞赛CCPC广州站, 签到题4题

文章目录I.Pudding StoreH.Three IntegersC.NecklaceF.Cactus补题链接&#xff1a;https://codeforces.com/gym/103415 I.Pudding Store I. Pudding Store time limit per test2.0 s memory limit per test512 megabytes inputstandard input outputstandard output 159 is a…

快速排序图解(两种思想)

七大排序之快速排序 文章目录七大排序之快速排序前言一、《算法导论》中的分区思想1.1 算法思想1.2 代码实现二、Hoare挖坑法2.1 算法思想2.2 代码实现三、算法分析四、注意事项总结前言 博主个人社区&#xff1a;开发与算法学习社区 博主个人主页&#xff1a;Killing Vibe的博…

【每天学习一点新知识】网络安全--截获攻击

截获攻击原理和后果 原理 若正常传输路径为终端A到终端B&#xff0c;黑客首先改变传输路径为终端A—黑客终端—终端B&#xff0c;使得传输信息必须经过黑客终端&#xff0c;黑客终端就可以截获终端A传输给终端B的消息。 后果 目前很多访问过程采用明码方式传输登录的用户名和密…

C++入门基础(下)

目录 引用 引用概念 引用特性 1.引用在定义时必须初始化 2.一个变量可以有多个引用 3.引用一旦引用一个实体&#xff0c;再不能引用其他实体. 常引用 使用场景 1.作为参数使用 2.作为返回值使用 引用和指针的区别 内联函数 内联函数的概念 内联函数特性 宏的优缺点 auto关键字 …

scala spark dataframe 时间加减

参考Adding 12 hours to datetime column in Spark 只针对标准化时间戳 yyyy-MM-dd HH:mm:ss 如果是 yyyy-MM-dd HH:mm 转换后会自动补到 HH:mm:ss ss位补0 时间英文简写查询 HOUR 代表小时 MINUTE 代表分钟 SECOND 代表秒 DAY MONTH YEAR 正数代表向后 负数代表向前 …