线索二叉树(C语言详解----画图理解)

1. 前序

  线索二叉树之所以加上线索,是因为能够存储存储前驱结点的指针和后继的指针叫线索过程,加上线索二字的二叉树称之为线索二叉树。
  为什么需要线索二叉树?对于一个二叉树而言,每一个存储结构都有左孩子指针域和右孩子指针域,它可以按照一定的次序进行遍历,但每一个根结点只能找到它的左右孩子不能找到上一个结点,如果需要找到上一个结点又只能从上到下顺序遍历,线索二叉树便于查找每一个结点的上一个结点是谁也可以找到下一个结点是谁。这就是普通二叉树与加上线索的二叉树的区别所在。
  如何实现线索呢?对于n个结点的二叉树,必定含有n+1个空的指针域,利用空的指针域存储前驱结点或者后继结点,吧空的指针域充分利用起来。对于n个结点,每一个结点包含左右孩子,所以有2n个指针域和n-1个非空的指针域,所以空的指针域 = 总指针域个数 - 非空指针域,所以空的指针域 = 2n - (n-1) = 2n - n +1 = n+1。

2.定义数据结构

在这里插入图片描述

lchild:左孩子指针域
LTag:前驱索引标志
data:数据域
RTag:后继索引标志
rchiild:右孩子指针域
 
LTag = 1的时候,表示左孩子指针域存储的是前驱结点
LTag = 0的时候,表示左孩子指针域存储的是左孩子结点
 
RTag = 1的时候,表示右孩子指针域存储的是后继结点
RTag = 0的时候,表示右孩子指针域存储的是右孩子结点

3.线索二叉树的规律

  对于中序线索而言

后继规律:该结点的后继应是该结点的右子树时访问的第一个结点,或者说,该节点的右子树的最左下的结点
前驱规律:该节点的前驱应是该结点的左子树最后访问的结点,或者说该结点的左子树的最右下的结点

  对于后序线索而言

分3种情况
1.若结点x是二叉树的根,则后继是空
2.若结点x是双亲的右孩子或者说是其双亲的左孩子且其双亲没有右子树,则其后继即为双亲结点
3.若结点x是双亲的左孩子且其双亲有右孩子,则其后继为其双亲的右子树按后序遍历的第一个结点

3.1 图示

               在这里插入图片描述

中序遍历顺序:a+bc-d-e/f
后序遍历顺序:abcd-
+ef/-

根据中序索引规律,例如根结点的"-“的前驱是其左子树的最右下角,那就是结点d
根据中序索引规律,例如根结点的”-"的后继是其右子树的最左下角,那就是e

4.手算线索过程

需要一个前驱指针和一个根指针,先判断根指针指向结点里面的的左孩子是否为NULL,如果是NULL,那么将根所指向的结点里面的LTag = 1,左孩子存储的是前驱指针,然后判断前驱指针所指向的结点的右孩子是否为NULL,如果右孩子为NULL,那么将前驱指针里面的RTag = 1,并且右孩子存储的是根指针

中序线索伪算法

设置pre为全局作用域前驱指针
pre = NULL
void InThread(BTree root)
{
  如果root不等于NULL
  {
    1.左子树搜索

    2.线索过程
      2.1.root没有左孩子
        设置前驱索引为1,并且左孩子存储pre
      2.1.pre!=NULL && pre没有右孩子
        设置后继索引为1,并且右孩子存储root
      pre = root用来保存上一次根节点的位置

    3.右子树搜索
  }
}

中序索引图解
中序遍历:DGBHEACF

1.第一次,root指向的是D,pre = NULL,根没有左孩子,设置左索引LTag = 1,左指针域存储前驱pre
在这里插入图片描述

2.第二次,根没有左孩子,设置左索引LTag = 1,左指针域存储前驱pre,pre有右孩子,不管它

在这里插入图片描述
3.第三次,根有左孩子,不管,pre没有右孩子,设置RTag = 1, rchild存储root
在这里插入图片描述
4.第四次,root没有左孩子,设置LTag = 1,左孩子存储pre, pre有右孩子,不管
在这里插入图片描述
5.第五次,root有左孩子,不管,pre没有右孩子,设置RTag = 1,右孩子存储root
在这里插入图片描述
6.第六次,root有左孩子,不管,pre没有右孩子,设置RTag = 1,右孩子存储root
在这里插入图片描述
7.第七次,root没有左孩子,设置LTag = 1, 左孩子存储pre,pre有右孩子,不管
在这里插入图片描述
8.第八次,root没有左孩子,设置LTag = 1, 左孩子存储pre,pre有右孩子,不管
在这里插入图片描述
9.第九次,最终如下图所示,pre指向最后一个结点
在这里插入图片描述

5.问题以及改进

1.为什么4模块中的图解,root按照这种规律进行遍历呢?
 这是因为它是按照中序的方式搜索,所以按照中序方式回退,而pre每次指向的是上一次root的位置

2.pre前驱指针得设置全局作用域,初始化NULL,如果不用全局作用域,那么也可以将pre设置static,静态。

3.改进,根据图解中,发现第一个结点D中的左孩子是NULL,以及最后一个结点的右孩子是NULL,可以在此二叉树加一个头结点,该头节点的左孩子存储的是root,root中序遍历第一个结点左孩子存储的是头结点。
root中序遍历最后一个结点的右孩子存储头结点。头结点的右孩子存储最后一个结点

如图
在这里插入图片描述

6.查找前驱和后继算法

查找前驱

叶子结点
   判断LTag是否等于1,如果是,那么左孩子存储的是该结点的前驱
非叶子结点
   该结点的左孩子,然后一直查找右孩子,当RTag = 1就退出循环,当RTag = 0就继续循环查找右孩子

查找后继

叶子结点
   判断RTag是否等于1,如果是,那么右孩子存储的是该结点的后继
非叶子结点
   该结点的右孩子,然后一直查找左孩子,当LTag = 1就退出循环,当LTag = 0就继续循环查找左孩子

前驱查找代码

//前驱查找
void InPre(BiTree root, BiTree* pre) {
	//叶子结点前驱查找
	   //只需要判断左索引是否是1,如果是,那么该结点的前驱存储在该结点的左孩子
	if (root->ltag == 1)
		*pre = root->Lchild;

	//非叶子结点前驱查找
	  //该节点的左孩子的右孩子最后一个结点,最后一个结点肯定是叶子结点,既然是叶子结点,那么ltag,rtag 都是 1
	  //查找该结点的左孩子的右孩子然后一直查找右孩子最终肯定是一个叶子结点, 如果不是叶子结点rtag = 0
	  //当查找到某一个结点的结点的rtag = 1,那么必定是叶子结点,就是它的前驱结点
	else {
		BiTree p;
		for (p = root->Lchild; p->rtag == 0; p = p->Rchild);
		*pre = p;
	}
}

后继查找代码

//后继查找
void InSucc(BiTree root, BiTree* pre) {
	//叶子结点后继查找
	   //右索引是否是1,如果是,那么该结点的右孩子必定是该结点的后继
	if (root->rtag == 1)
		*pre = root->Rchild;

	//非叶子结点后继查找
	   //该结点的右孩子的左孩子的第一个结点,必定是叶子结点,同时也是该结点的后继
	   //查找该结点的右孩子然后一直查找左孩子,最终该结点肯定是一个叶子结点,就找到它的后继
	else {
		BiTree p;
		for (p = root->Rchild; p->ltag == 0; p = p->Lchild);
		*pre = p;
	}
}

7.代码

7.1建立二叉树

//先序建立一棵树
void Create_Tree(BiTree* root) {
	char ch;
	scanf("%c", &ch);
	if (ch == '#') (*root) = NULL;
	else {
		(*root) = (BiTree)malloc(sizeof(Tree));
		(*root)->data = ch;
		(*root)->ltag = 0;
		(*root)->rtag = 0;
		Create_Tree(&((*root)->Lchild));
		Create_Tree(&((*root)->Rchild));
	}
}

7.2 中序线索

//中序线索
void InThread(BiTree root) {
	if (root != NULL) {
		//左搜索
		InThread(root->Lchild);

		//索引
		   //根的左孩子索引判断
		if (root->Lchild == NULL) {
			root->ltag = 1;
			root->Lchild = pre;
		}

		   //前驱的右孩子索引判断
		if (pre && pre->Rchild == NULL) {
			pre->rtag = 1;
			pre->Rchild = root;
		}
		   
		   //更新前驱结点
		pre = root;

		//右搜索
		InThread(root->Rchild);
	}
}

7.4 前驱查找

//前驱查找
void InPre(BiTree root, BiTree* pre) {
	//叶子结点前驱查找
	   //只需要判断左索引是否是1,如果是,那么该结点的前驱存储在该结点的左孩子
	if (root->ltag == 1)
		*pre = root->Lchild;

	//非叶子结点前驱查找
	  //该节点的左孩子的右孩子最后一个结点,最后一个结点肯定是叶子结点,既然是叶子结点,那么ltag,rtag 都是 1
	  //查找该结点的左孩子的右孩子然后一直查找右孩子最终肯定是一个叶子结点, 如果不是叶子结点rtag = 0
	  //当查找到某一个结点的结点的rtag = 1,那么必定是叶子结点,就是它的前驱结点
	else {
		BiTree p;
		for (p = root->Lchild; p->rtag == 0; p = p->Rchild);
		*pre = p;
	}
}

7.5 后继查找

//后继查找
void InSucc(BiTree root, BiTree* pre) {
	//叶子结点后继查找
	   //右索引是否是1,如果是,那么该结点的右孩子必定是该结点的后继
	if (root->rtag == 1)
		*pre = root->Rchild;

	//非叶子结点后继查找
	   //该结点的右孩子的左孩子的第一个结点,必定是叶子结点,同时也是该结点的后继
	   //查找该结点的右孩子然后一直查找左孩子,最终该结点肯定是一个叶子结点,就找到它的后继
	else {
		BiTree p;
		for (p = root->Rchild; p->ltag == 0; p = p->Lchild);
		*pre = p;
	}
}

7.6 加入头节点线索二叉树

//加入头结点中序线索二叉树
void InOrderThreading(BiTree& Thrt, BiTree root) {
	if (!(Thrt = (BiTree)malloc(sizeof(Tree)))) exit(OVERFLOW);
	Thrt->ltag = 0; Thrt->rtag = 1;
	Thrt->Rchild = Thrt;  //回指自己
	if (!Thrt) Thrt->Lchild = Thrt; //二叉树不存在
	else {
		Thrt->Lchild = root; pre = Thrt; //前驱指针指向头结点
		InThread(root);  //中序线索
		pre->Rchild = Thrt; pre->rtag = 1; //最后一个结点线索
		Thrt->Rchild = pre;
	}
}

7.7 非递归中序遍历线索二叉树

//非递归遍历线索二叉树
void InOrderTraverse_Thr(BiTree thrt) {
	BiTree p = thrt->Lchild; //找到根结点
	while (p != thrt) {
		while (p->ltag == 0) p = p->Lchild;
		printf("%c ", p->data);
		while (p->rtag == 1 && p->Rchild != thrt) {
			p = p->Rchild; printf("%c ", p->data);
		}
		p = p->Rchild;
	}
}

8.测试案例

Tree.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#define OVERFLOW -1
typedef char ElemType;
typedef int PointerType;
typedef struct BTree {
	ElemType data;
	struct BTree* Lchild, * Rchild; //指向左右孩子指针
	PointerType ltag, rtag;         //左右索引标志 为1的时候进行索引,为0指向孩子
}Tree, *BiTree;

extern BiTree pre;

//先序建立一棵树
void Create_Tree(BiTree* root);

//中序线索
void InThread(BiTree root);

//前驱查找
void InPre(BiTree root, BiTree* pre);

//后继查找
void InSucc(BiTree root, BiTree* pre);

//加入头结点中序线索二叉树
void InOrderThreading(BiTree& Thrt, BiTree root);

//非递归遍历线索二叉树
void InOrderTraverse_Thr(BiTree thrt);

TreeOperation.cpp

#include "Tree.h"
//先序建立一棵树
void Create_Tree(BiTree* root) {
	char ch;
	scanf("%c", &ch);
	if (ch == '#') (*root) = NULL;
	else {
		(*root) = (BiTree)malloc(sizeof(Tree));
		(*root)->data = ch;
		(*root)->ltag = 0;
		(*root)->rtag = 0;
		Create_Tree(&((*root)->Lchild));
		Create_Tree(&((*root)->Rchild));
	}
}

//中序线索
void InThread(BiTree root) {
	if (root != NULL) {
		//左搜索
		InThread(root->Lchild);

		//索引
		   //根的左孩子索引判断
		if (root->Lchild == NULL) {
			root->ltag = 1;
			root->Lchild = pre;
		}

		   //前驱的右孩子索引判断
		if (pre && pre->Rchild == NULL) {
			pre->rtag = 1;
			pre->Rchild = root;
		}
		   
		   //更新前驱结点
		pre = root;

		//右搜索
		InThread(root->Rchild);
	}
}

//前驱查找
void InPre(BiTree root, BiTree* pre) {
	//叶子结点前驱查找
	   //只需要判断左索引是否是1,如果是,那么该结点的前驱存储在该结点的左孩子
	if (root->ltag == 1)
		*pre = root->Lchild;

	//非叶子结点前驱查找
	  //该节点的左孩子的右孩子最后一个结点,最后一个结点肯定是叶子结点,既然是叶子结点,那么ltag,rtag 都是 1
	  //查找该结点的左孩子的右孩子然后一直查找右孩子最终肯定是一个叶子结点, 如果不是叶子结点rtag = 0
	  //当查找到某一个结点的结点的rtag = 1,那么必定是叶子结点,就是它的前驱结点
	else {
		BiTree p;
		for (p = root->Lchild; p->rtag == 0; p = p->Rchild);
		*pre = p;
	}
}

//后继查找
void InSucc(BiTree root, BiTree* pre) {
	//叶子结点后继查找
	   //右索引是否是1,如果是,那么该结点的右孩子必定是该结点的后继
	if (root->rtag == 1)
		*pre = root->Rchild;

	//非叶子结点后继查找
	   //该结点的右孩子的左孩子的第一个结点,必定是叶子结点,同时也是该结点的后继
	   //查找该结点的右孩子然后一直查找左孩子,最终该结点肯定是一个叶子结点,就找到它的后继
	else {
		BiTree p;
		for (p = root->Rchild; p->ltag == 0; p = p->Lchild);
		*pre = p;
	}
}

//加入头结点中序线索二叉树
void InOrderThreading(BiTree& Thrt, BiTree root) {
	if (!(Thrt = (BiTree)malloc(sizeof(Tree)))) exit(OVERFLOW);
	Thrt->ltag = 0; Thrt->rtag = 1;
	Thrt->Rchild = Thrt;  //回指自己
	if (!Thrt) Thrt->Lchild = Thrt; //二叉树不存在
	else {
		Thrt->Lchild = root; pre = Thrt; //前驱指针指向头结点
		InThread(root);  //中序线索
		pre->Rchild = Thrt; pre->rtag = 1; //最后一个结点线索
		Thrt->Rchild = pre;
	}
}

//非递归遍历线索二叉树
void InOrderTraverse_Thr(BiTree thrt) {
	BiTree p = thrt->Lchild; //找到根结点
	while (p != thrt) {
		while (p->ltag == 0) p = p->Lchild;
		printf("%c ", p->data);
		while (p->rtag == 1 && p->Rchild != thrt) {
			p = p->Rchild; printf("%c ", p->data);
		}
		p = p->Rchild;
	}
}


main.cpp

#include "Tree.h"
BiTree pre = NULL;

int main() {
	BiTree root;
	BiTree Thrt = NULL; //头结点
	//先序建立一颗二叉树
	Create_Tree(&root);
	getchar();

	InOrderThreading(Thrt, root); //中序带头结点线索
	InOrderTraverse_Thr(Thrt);    //非递归遍历线索二叉树
	getchar();
	
	
	
	getchar();
	return 0;
}
/*
ABD#G##EH###C#F##
*/

9.运行结果

在这里插入图片描述

10.增加一个功能

利用中序遍历线索二叉树非递归算法,查找并且输出每一个结点的前驱和后继结点,直接在非递归线索二叉树输出的printf那里改一下,将每次的结点调用查找前驱结点和后继结点的接口,就能实现一个输出线索二叉树中的每一个结点的前驱结点和后继结点的信息。

<think>嗯,用户让我详细介绍C语言中的线索二叉树线索二叉树,这个概念我之前学过,但可能有点忘了。让我先回忆一下。 首先,线索二叉树是什么?好像是为了解决二叉树遍历时的空间效率问题。普通的二叉树在遍历时需要用递归或者栈来维护节点,而线索二叉树通过利用空的指针域来指向遍历顺序的前驱或后继,这样可能可以节省空间。线索化的过程应该是在遍历的时候修改空指针,让它们指向对应的节点。 那线索二叉树的结构应该和普通二叉树不同吧?比如每个节点可能需要额外的标记来区分指针是指向子节点还是线索。比如,每个节点可能有左右两个指针,同时还有两个标志位,用来表示左右指针是子树还是线索。例如,left_tag为0表示左孩子,为1表示前驱线索;right_tag同理,可能是后继线索。 然后,线索二叉树的类型应该分为三种:前序线索二叉树、中序线索二叉树和后序线索二叉树。不同的遍历方式线索化的顺序不同。最常见的是中序线索二叉树,可能用户问的是这个。 接下来需要考虑如何在C语言中实现线索二叉树的结构。结构体应该包含数据、左右指针,以及左右标志。比如: struct ThreadedNode { int data; struct ThreadedNode *left, *right; int left_tag; // 0表示指向左孩子,1表示前驱线索 int right_tag; // 0表示右孩子,1表示后继线索 }; 然后,线索化的过程需要在中序遍历的时候修改空指针。比如,在遍历时保留前一个节点,如果当前节点的左孩子为空,就将其左指针指向前驱节点,并设置标志位。类似地,处理右指针时,如果前一个节点的右孩子为空,就指向当前节点作为后继。 比如,中序线索化的递归函数可能如下: void inThread(struct ThreadedNode *p, struct ThreadedNode **pre) { if (p == NULL) return; inThread(p->left, pre); if (p->left == NULL) { p->left = *pre; p->left_tag = 1; } if (*pre != NULL && (*pre)->right == NULL) { (*pre)->right = p; (*pre)->right_tag = 1; } *pre = p; inThread(p->right, pre); } 不过这里需要注意初始调用时pre应该初始化为NULL。可能需要一个辅助函数来包裹这个递归过程。 然后,线索二叉树遍历的时候不需要递归或者栈,只需要用线索来找到后继节点。例如,中序线索二叉树的遍历可以从最左边的节点开始,然后沿着右指针和后继线索访问节点。具体步骤可能包括找到第一个节点,然后根据right_tag判断是否有右子树,或者直接通过线索找到下一个节点。 比如,中序遍历线索二叉树的代码可能如下: void inOrderTraversal(struct ThreadedNode *root) { struct ThreadedNode *p = root; while (p != NULL) { // 找到最左边的节点 while (p->left_tag == 0) { p = p->left; } printf("%d ", p->data); // 沿着后继线索遍历 while (p->right_tag == 1) { p = p->right; printf("%d ", p->data); } p = p->right; // 转向右子树 } } 不过这个可能需要更多的测试,比如处理存在右子树的情况是否正确。 另外,用户可能想知道线索二叉树的应用场景,比如在需要频繁遍历且希望节省空间或时间的情况下使用。但线索二叉树的缺点可能在于插入和删除操作比较复杂,因为需要维护线索的正确性。 还有,创建线索二叉树的过程可能需要先建立普通二叉树,然后进行线索化处理。比如,用户输入一个二叉树的结构,然后通过线索化函数进行处理。 可能需要注意,在编写代码时如何处理pre指针的引用传递,因为在C语言中需要传递指针的指针,或者使用全局变量。比如,前面的例子中使用的是双指针来维护pre的值。 还有,线索二叉树的结构示意图可能会有帮助。比如,一个中序线索化的二叉树,其左线索指向前驱,右线索指向后继。可能用户需要这样的图解,但在这里只能文字描述。 总结一下,线索二叉树通过利用空指针存储线索,优化了遍历的空间复杂度。C语言实现时需要注意节点结构的标记位,线索化的递归或非递归方法,以及遍历时的处理逻辑。同时,插入和删除操作需要考虑线索的维护,这可能比较复杂。 可能用户会问到线索二叉树相对于非线索化的优势,比如遍历时不需要栈,空间复杂度O(1),而普通的递归遍历需要O(h)的栈空间,h是树的高度。不过线索化的遍历虽然空间效率高,但时间上可能相差不大,因为需要遍历所有节点。 还有可能需要注意,线索二叉树分为前序、中序、后序三种类型,各自的线索化方式和遍历方式不同。比如,前序线索二叉树在遍历时需要考虑子节点和线索的关系,可能会更复杂一些。 比如,前序遍历线索二叉树时,可能需要先访问节点,然后如果有左子树则转向左,否则根据线索转向右。但具体实现可能比中序复杂,因为前序的访问顺序是根左右,线索的指向可能需要更复杂的处理。 总之,在介绍时需要分点,结构清晰,包括定义、结构、线索化过程、遍历方法、优缺点和应用等部分。同时,给出C语言的代码示例,帮助用户理解实现细节。</think>线索二叉树是一种优化二叉树结构的方法,通过利用空指针存储遍历顺序的“线索”,从而提升遍历效率(无需递归或栈)。以下是C语言线索二叉树详解--- ### **一、线索二叉树的核心概念** 1. **背景问题** 普通二叉树遍历需借助栈或递归,空间复杂度为O(h)(h为树高)。线索二叉树利用空指针(原为NULL的左右指针)指向遍历顺序的前驱或后继节点,将空间复杂度降至O(1)。 2. **线索化规则** - **左指针**:若左子树为空,指向遍历顺序的前驱节点(前驱线索)。 - **右指针**:若右子树为空,指向遍历顺序的后继节点(后继线索)。 - **标志位**:用标记区分指针类型(子树或线索)。例如: - `left_tag=0`:左指针指向左孩子;`left_tag=1`:左指针为前驱线索- `right_tag`同理。 --- ### **二、数据结构定义** 在C语言中,线索二叉树的节点结构如下: ```c typedef struct ThreadedNode { int data; struct ThreadedNode *left, *right; int left_tag; // 0: 左孩子,1: 前驱线索 int right_tag; // 0: 右孩子,1: 后继线索 } ThreadedNode; ``` --- ### **三、线索化过程(以中序为例)** 线索化需在遍历中动态修改指针,并维护前驱节点(pre): ```c // 中序线索化递归函数 void inThread(ThreadedNode *p, ThreadedNode **pre) { if (p == NULL) return; // 递归线索化左子树 inThread(p->left, pre); // 处理当前节点的左指针 if (p->left == NULL) { p->left = *pre; p->left_tag = 1; // 标记为线索 } // 处理前驱节点的右指针 if (*pre != NULL && (*pre)->right == NULL) { (*pre)->right = p; (*pre)->right_tag = 1; } *pre = p; // 更新前驱节点 // 递归线索化右子树 inThread(p->right, pre); } // 中序线索化入口函数 void createInThread(ThreadedNode *root) { ThreadedNode *pre = NULL; if (root != NULL) { inThread(root, &pre); // 处理最后一个节点的右指针 pre->right = NULL; pre->right_tag = 1; } } ``` --- ### **四、线索二叉树的遍历(中序)** 遍历时无需递归或栈,直接通过线索访问后继: ```c void inOrderTraversal(ThreadedNode *root) { ThreadedNode *p = root; while (p != NULL) { // 找到最左节点(中序起点) while (p->left_tag == 0) { p = p->left; } printf("%d ", p->data); // 沿后继线索遍历 while (p->right_tag == 1) { p = p->right; printf("%d ", p->data); } p = p->right; // 转向右子树 } } ``` --- ### **五、三种线索二叉树对比** | 类型 | 线索化顺序 | 遍历逻辑 | |------------|------------|--------------------------------------------------------------------------| | **前序线索** | 根--右 | 访问根后,优先左子树;若左为线索,转右子树或线索。 | | **中序线索** | 左--右 | 先找到最左节点,沿线索遍历后继。 | | **后序线索** | 左--根 | 需记录父节点,处理子树关系复杂,应用较少。 | --- ### **六、优缺点分析** - **优点**: - 遍历空间效率高(O(1))。 - 快速查找前驱/后继节点。 - **缺点**: - 插入、删除节点需维护线索,逻辑复杂。 - 结构比普通二叉树占用更多内存(标志位)。 --- ### **七、应用场景** - 频繁遍历但较少修改的二叉树结构(如表达式树)。 - 内存受限环境下需高效遍历的场景。 --- 通过上述内容,可以清晰理解线索二叉树的实现原理及其在C语言中的具体应用。如需进一步探讨前序或后序线索化,可深入分析其遍历逻辑与代码实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值