二叉树
二叉树是另一种树形结构。
二叉树的每个结点至多只有两颗子树(即二叉树中不存在度大于2的结点)。
二叉树的子树有左右之分,是有序树的一种。
定义
- ADT BinaryTree:
- 数据对象 $D$:$D$ 是具有相同特性的数据元素的集合。
- 数据关系 $R$:
- 若 $D=\Phi$,则 $R=\Phi$,称
BinaryTree
为空二叉树。 - 若 $D\ne \Phi$,则 $R=\lbrace H \rbrace$,$H$ 是如下的二元关系:
- 在 $D$ 中存在惟一的称为根的数据元素 $root$,它在关系 $H$ 下无前驱;
- 若 $D-root \ne \Phi$,则存在惟一的 $D-\lbrace root \rbrace = \lbrace D_l, D_r \rbrace$,且 $D_l \cap D_r = \Phi$;
- 若 $D_l \ne \Phi$,则 $D_l$ 中存在惟一的元素 $x_l$,有 $ <root, x_l> \in H$,且存在 $D_l$ 上的关系 $H_l \subset H$;
- 若 $D_r \ne \Phi$,则 $D_r$ 中存在惟一的元素 $x_r$,有 $ <root, x_r> \in H$,且存在 $D_r$ 上的关系 $H_r \subset H$;
- $H=\lbrace <root, x_l>, <root, x_r>, H_l, H_r \rbrace$。
- $(D_l, \lbrace H_l \rbrace)$ 是一棵符合本定义的二叉树,称为根的左子树;$(D_r, \lbrace H_r \rbrace)$ 是一棵符合本定义的右子树。
- 基本操作 $P$:
/* * 操作结果:构造空二叉树T。 */ InitBiTree(&T); /* * 初始条件:二叉树T已存在。 * 操作结果:销毁二叉树T。 */ DestroyBiTree(&T); /* * 初始条件:definition给出二叉树T的定义。 * 操作结果:按definition构造二叉树T。 */ CreateBiTree(&T, definition); /* * 初始条件:二叉树T已存在。 * 操作结果:将二叉树T清为空树。 */ ClearBiTree(&T); /* * 初始条件:二叉树T已存在。 * 操作结果:若T为空树,则返回TRUE,否则返回FALSE。 */ BiTreeEmpty(T); /* * 初始条件:二叉树T已存在。 * 操作结果:返回T的深度。 */ BiTreeDepth(T); /* * 初始条件:二叉树T已存在。 * 操作结果:返回T的根。 */ Root(T); /* * 初始条件:二叉树T已存在,e是T中的某个结点 * 操作结果:返回e的值。 */ Value(T,e); /* * 初始条件:二叉树T已存在,e是T中的某个结点 * 操作结果:结点e赋值为value。 */ Assign(T, &e, value); /* * 初始条件:二叉树T已存在,e是T中的某个结点。 * 操作结果:若e是T的非根结点,则返回它的双亲;否则返回空。 */ Parent(T, e); /* * 初始条件:二叉树T已存在,e是T中的某个结点。 * 操作结果:返回e的左孩子,若e无左孩子,则返回空。 */ LeftChild(T, e); /* * 初始条件:二叉树T已存在,e是T中的某个结点。 * 操作结果:返回e的右孩子,若e无右孩子,则返回空。 */ RightChild(T, e); /* * 初始条件:二叉树T已存在,e是T中的某个结点。 * 操作结果:返回e的左兄弟,若e无左兄弟,则返回空。 */ LeftSibling(T, e); /* * 初始条件:二叉树T已存在,e是T中的某个结点。 * 操作结果:返回e的右兄弟,若e无右兄弟,则返回空。 */ RightSibling(T, e); /* * 初始条件:二叉树T已存在,p指向T中的某个结点,LR为0或1,非空二叉树c与T不相交且右子树为空。 * 操作结果:根据LR为0或1,插入c为T中p所指结点的左或右子树。p所指结点的原有左或右子树则称为c的右子树。 */ InsertChild(T, p, LR, c); /* * 初始条件:二叉树T已存在,p指向T中的某个结点,LR为0或1。 * 操作结果:根据LR为0或1,删除T中p所指节点的左或右子树。 */ DeleteChild(T, p, LR); /* * 初始条件:二叉树T已存在,Visit是对结点操作的应用函数。 * 操作结果:先序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。 */ PreOrderTraverse(T, Visit()); /* * 初始条件:二叉树T已存在,Visit是对结点操作的应用函数。 * 操作结果:中序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。 */ InOrderTraverse(T, Visit()); /* * 初始条件:二叉树T已存在,Visit是对结点操作的应用函数。 * 操作结果:后序遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。 */ PostOrderTraverse(T, Visit()); /* * 初始条件:二叉树T已存在,Visit是对结点操作的应用函数。 * 操作结果:层次遍历T,对每个结点调用函数Visit一次且仅一次。一旦visit()失败,则操作失败。 */ LevelOrderTraverse(T, Visit());
二叉树的5中基本形态
其中,a:空二叉树;b:仅有根节点的二叉树;c:右子树为空的二叉树;d:左右子树均为非空的二叉树;e:左子树为空的二叉树。
性质
性质1
在二叉树的第 $i$ 层至多有 $2^{i-1}$ 个结点($i \ge 1$)。
性质2
深度为 $k$ 的二叉树至多有 $2^k-1$ 个结点,($k \ge 1$)。
深度为 $k$ 的二叉树的最大结点数为
$$ \sum_{i=1}^{k}(第i层上的最大节点数) = \sum_{i=1}^{k}2^{i-1} = 2^k - 1 $$
等比数列求和公式:
$$ S_n=a_1*\frac{1-q^n}{1-q} (a_1为首项,q为公比) $$
性质3
对任何一棵二叉树 $T$ ,如果其叶子结点数为 $n_0$,度为 $2$ 的节点数为 $n_2$,则 $n_0=n_2+1$。
证明:
设 $n_1$ 为二叉树 $T$ 中度为 $1$ 的结点数。因为二叉树中所有结点的度均小于或等于 $2$,所以其结点总数为
$$ n=n_0+n_1+n_2 \tag{1} $$
再看二叉树的分支数。除了根节点之外,其余结点都有一个分支进入,设 $B$ 为分支总数,则 $n=B+1$。
由于这些分支是由度为 $1$ 和 $2$ 的结点映射出的, 所以又有 $B=n_1+2*n_2$。因此可得
$$ n=n_1+2*n_2+1 \tag{2} $$
由公式 $(1)$ 和 $(2)$ 可得:
$$ n_0 = n_2+1 $$
满二叉树
深度为 $k$ 且具有 $2^k-1$个结点的二叉树。
这种树的特点是每一层上的结点数都是最大结点数。
完全二叉树
深度为 $k$ 的,有 $n$ 个结点的二叉树,当且仅当其每一个结点都与深度为 $k$ 的满二叉树从 $1$ 至 $n$ 的结点一一对应时,称为完全二叉树。
- 叶子结点只可能出现在层次最大的两层上。
- 对任一结点,若其右分支下的子孙的最大层次为 $l$,则其左分支下的子孙的最大层次必为 $l$ 或 $l+1$。
性质4
具有 $n$ 个结点的完全二叉树的深度为 $\lfloor log_{2}{n} \rfloor + 1$ 。
证明:
假设深度为 $k$,根据性质2和完全二叉树的定义有
$$ \begin{align} 2^{k-1}-1 < &n \le 2^{k}-1 \\ 或&\\ 2^{k-1} \le &n < 2^{k} \end{align} $$
所以 $k-1 \le log_{2}{n} < k $,$\because k$ 是整数,所以 $k=\lfloor log_{2}{n}\rfloor + 1$。
性质5
如果对一棵有 $n$ 个结点的完全二叉树的结点按层序编号(从第 $1$ 层到第 $\lfloor log_{2}{n} + 1$ 层,每层从左到右),那么对任一结点 $i(i\le i \le n)$ ,有以下3条性质:
- 如果 $i==1$,则结点 $i$ 是二叉树的根,无双亲;如果 $i>1$,则其双亲 $PARENT(i) = \lfloor i/2 \rfloor$ 。
- 如果 $2i > n$,则结点 $i$ 无左孩子,结点 $i$ 为叶子结点;否则其左孩子 $LCHILD(i)=2i$。
- 如果 $2i+1>n$,则节点 $i$ 无右孩子;否则其右孩子 $RCHILD(i)=2i+1$。
证明:
当 $i==1$ 时,由完全二叉树的定义可知,其左孩子时结点 $2$。若 $2>n$,即不存在结点 $2$,此时结点 $i$ 无左孩子。结点 $i$ 的右孩子是结点 $3$,若 $3>n$,即不存在结点 $3$,此时结点 $i$ 无有孩子。(满足1、2、3)
当 $i>1$ 时,
- 设 $i$ 是第 $j(1 \le j \le \lfloor log_{2}{n} \rfloor)$ 层的第 $1$ 个结点。
- 由性质2可知,前 $j-1$ 层的结点总数为 $2^{j-1}-1$,因此 $i=2^{j-1}-1+1=2^{j-1}$。
- 结点 $i$ 的左孩子是第 $j+1$ 层的第 $1$ 个结点,其编号为 $2^{j}-1+1=2^{j}=2i$。
- 结点 $i$ 的右孩子是第 $j+1$ 层的第 $2$ 个结点,其编号是 $2^{j}-1+2=2^{j}+1=2i+1$。
- 因此,若 $2i>n$,则结点 $i$ 无左孩子;若 $2i+1>n$,则结点 $i$ 无右孩子。
- 设 $k$ 是第 $j(1 \le j \le \lfloor log_{2}{n} \rfloor)$ 层的任意$1$个结点且 $k > i$,因此结点 $k$ 是结点 $i$ 的右兄弟或堂兄弟。
- 由于结点 $i$ 的左孩子是 $2i$,因此结点 $k$ 的左孩子是 $2i+2(k-i)=2k$(完全二叉树的定义,结点 $i$ 和结点 $k$ 之间相差 $k-i$ 个结点)。
- 由此,结点 $k$ 的右孩子为 $2k+1$。
- 当 $2k<n$ 时,结点 $k$ 无左孩子;当 $2k+1<n$ 时, 结点 $k$ 无有孩子。
存储
顺序存储结构
#define DEF_MAX_TREE_SIZE 100 // 二叉树的最大结点数
typedef struct //
{ //
int value; //
} TElemType; // 二叉树的结点数据类型
//
typedef struct //
{ //
TElemType elem; //
} SqBiTree[DEF_MAX_TREE_SIZE]; // 顺序存储的二叉树类型
//
SqBiTree bt; // 二叉树存储变量
用一组地址连续的存储单元一次自上而下、自左至右存储完全二叉树的结点元素,即将完全二叉树上编号 $i$ 的结点元素存储在如上定义的一维数组下标为 $i-1$ 的分量中。
I这种顺序存储结构仅适用于完全二叉树。 因为,在最坏的情况下,一个深度为 $k$ 且仅有 $k$ 个结点单支树(树中不存在度为 $2$ 的结点)却需要长度为 $2^k-1$ 的一维数组。
链式存储结构
不同的结点结构可以构成不同形式的链式存储结构。
由二叉树的定义可知,二叉树的结点由一个数据元素和分别指向其左、右子树的两个分支构成,因此二叉树的链表中的结点至少包含3个域:数据域和左、右指针域,如图a所示。
有时为了便于找到结点的双亲,还可以在结点结构中增加一个指向其双亲结点的指针域,如图b所示。
利用这两种结点结构所得二叉树的存储结构分别称为二叉链表和三叉链表。
在含有 $n$ 个结点二叉链表中有 $n+1$ 个空链域。
证明:
$n$ 个结点的二叉链表中共有 $2n$ 个指针域。
二叉树的每个分支占用一个指针域,其中共占用了 $n-1$ 个指针域,剩余指针域为 $2n-(n-1)=n+1$ 。
typedef struct
{
int value;
} TElemType;
typedef struct BiTNode
{
TElemType data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
创建二叉链表
创建如下图所示的二叉链表存储结构的二叉树。
其中前序序列:1234567;中序序列:3256471;后序序列:3657421。
根据前序序列来创建二叉链表,用-1来表示不存在结点,因此数据序列为static int data[] = {1, 2, 3, -1, -1, 4, 5, -1, 6, -1, -1, 7, -1, -1, -1};
下面是创建该二叉树的代码:
typedef struct BiTNode
{
int data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
static int data[] = {1, 2, 3, -1, -1, 4, 5, -1, 6, -1, -1, 7, -1, -1, -1};
static int i = I0;
// 递归创建二叉树
static int create_binary_tree(BiTree* biT)
{
int iRet = -1;
if (data[i] == -1)
{
// 如果结点数值为-1,则表示不存在。
biT = NULL;
i++;
return iRet;
}
// 1. 先为根节点赋值
*biT = (BiTNode*)malloc(sizeof(BiTNode));
if (*biT == NULL)
{
exit(-1);
return iRet;
}
memset(*biT, 0x00, sizeof(BiTNode));
(*biT)->data = data[i];
i++;
// 2. 递归为左子树赋值
create_binary_tree(&(*biT)->lchild);
// 3. 递归为右子树赋值
create_binary_tree(&(*biT)->rchild);
return iRet;
}
遍历
- 先序遍历二叉树:
- 访问根结点;
- 先序遍历左子树;
- 先序遍历右子树。
- 中序遍历二叉树:
- 中序遍历左子树;
- 访问根节点;
- 中序遍历右子树。
- 后序遍历二叉树:
- 后序遍历左子树;
- 后序遍历右子树;
- 访问根节点。
3种遍历算法的不同处仅在于访问根节点和遍历左、右子树的先后关系。
二叉树遍历有两种算法:递归算法和非递归算法,区别在于是否显式使用栈。
下面的例子展示这两种方式:
递归方式遍历二叉树:
// 先序遍历
static void pre_order_traverse(BiTree T, Visit_T visit)
{
if (T != NULL)
{
// 1. 根结点不为空,访问根结点。
visit(T->data);
// 2. 递归遍历左子树。
pre_order_traverse(T->lchild, visit);
// 3. 递归遍历右子树。
pre_order_traverse(T->rchild, visit);
}
}
// 中序遍历
static void in_order_traverse(BiTree T, Visit_T visit)
{
if (T != NULL)
{
// 当前结点不为空
// 1. 中序遍历左子树。
in_order_traverse(T->lchild, visit);
// 2. 访问当前结点。
visit(T->data);
// 3. 中序遍历右子树。
in_order_traverse(T->rchild, visit);
}
}
// 后序遍历
static void post_order_traverse(BiTree T, Visit_T visit)
{
if (T != NULL)
{
// 1. 后序遍历左子树。
post_order_traverse(T->lchild, visit);
// 2. 后序遍历左子树。
post_order_traverse(T->rchild, visit);
// 3. 访问当前结点。
visit(T->data);
}
}
int main()
{
BiTree biT = NULL;
create_binary_tree(&biT);
pre_order_traverse(biT, visit); // 1 2 3 4 5 6 7
in_order_traverse(biT, visit); // 3 2 5 6 4 7 1
post_order_traverse(biT, visit);// 3 6 5 7 4 2 1
}
非递归二叉树遍历算法:
// 用指针数组模拟栈结构
BiTNode* bitNodePre[100];
int indexPre = 0;
static void pre_order_traverse(BiTree T, Visit_T visit)
{
BiTNode* p = T;
while (p != NULL || indexPre > 0)
{
if (p != NULL)
{
// 1. 访问根节点
visit(p->data);
// 2. 将右子树压栈
if (p->rchild != NULL)
{
bitNodePre[indexPre] = p->rchild;
indexPre++;
}
// 2. 当前结点设置为根结点的左子树
p = p->lchild;
}
else
{
// 当左子树为为空或左侧到头时,表示当前子树的左子树和根节点都已经访问过,
// 所以出栈访问右子树。
indexPre--;
p = bitNodePre[indexPre];
}
}
}
BiTNode* bitNodeIn[100];
int indexIn = 0;
static void in_order_traverse(BiTree T, Visit_T visit)
{
BiTNode* p = T;
while (indexIn > 0 || p != NULL)
{
if (p != NULL)
{
// 1. 向左子树走到头,并入栈。
bitNodeIn[indexIn] = p;
indexIn++;
p = p->lchild;
}
else
{
// 2. 没有左子树时,出栈。
indexIn--;
p = bitNodeIn[indexIn];
// 3. 访问根节点
visit(p->data);
// 4.当前结点指向右子树,下一次循环中开始将右子树入栈
p = p->rchild;
}
}
}
BiTNode* bitNodePost[100];
int indexPost = 0;
static void post_order_traverse(BiTree T, Visit_T visit)
{
BiTNode* p = T;
BiTNode* r = NULL; // 用来记录结点是否访问过。
while (indexPost > 0 || p != NULL)
{
if (p != NULL)
{
// 1. 向左子树走到头,并入栈。
bitNodePost[indexPost] = p;
indexPost++;
p = p->lchild;
}
else
{
// 2. 获取栈顶元素,即为最左侧子树的根,判断是否有右子树。
p = bitNodePost[indexPost - 1]; // 获取栈顶元素
if (p->rchild != NULL && p->rchild != r)
{
// 3.当前结点更新为右子树的根节点,右子树不为空,则将其压栈。
p = p->rchild;
bitNodePost[indexPost] = p;
indexPost++;
// 4. 从右子树的左侧结点开始,重新将左侧结点入栈。
p = p->lchild;
}
else
{
// 5.没有右子树,则出栈,访问根节点。
indexPost--;
p = bitNodePost[indexPost]; // 获取栈顶元素
visit(p->data);
// 6. 记录访问过的结点,用以避免右子树访问过并出栈之后,
// 再次判断是否有右子树时又被重新入栈。
r = p;
p = NULL;
}
}
}
}
遍历二叉树是以一定规则将二叉树中的结点排列成一个线性序列,得到二叉树中结点的先序序列或中序序列或后序序列。 这实质上是对一个非线性结构进行线性化操作,使每个结点(除第一个和最后一个之外)在这些线性序列中有且仅有一个直接前驱和直接后继。
线索化
线索化解决的问题是以二叉链表存储作为存储结构时,只能得到当前结点的左右孩子信息,并不能得到得到任一序列的前驱和后继信息(这些信息只有动态遍历时才能得到)。
为了解决这个问题,有两种方法:
其一是在二叉链表的每个结点上增加两个指针域,分别指向遍历时的前驱和后继信息,这种方法会浪费空间,降低存储密度。
另一种方法是,利用二叉链表的 $n+1$ 个空链域来存储前驱和后继信息,这种方法会和存储的左右子树信息冲突,因此需要标记。如图所示:
其中LTag和RTag都是0代表指向结点的左、右孩子;1代表指向结点前驱、后继。
typedef enum
{
Link, // Link==0,表示指针
Thread // Thread==1,表示线索
} PointerTag;
typedef struct BiThrNode
{
int data;
struct BiThrNode *lchild, *rchild;
PointerTag LTag, RTag;
} BiThrNode, *BiThrTree;
以这种结点结构构成的二叉链表作为二叉树的存储结构,叫做线索链表。
指向前驱和后继的指针,叫做线索。带有线索的二叉树称为线索二叉树。
线索化指的是以某种次序遍历使其称为线索二叉树的过程,其实质是将二叉链表中的空指针改为指向前驱或后继的线索,而前驱或后继只有在遍历过程中才能得到,因此线索化的过程即为在遍历过程中修改空指针的过程。
中序线索化
结点的后继是遍历其右子树时访问的第一个结点,即右子树中最左下的结点。
前驱是左子树中最后访问的结点,即最右下的结点。
构建二叉树的双向线索链表:
- 在二叉树的线索链表上添加一个头结点。
- 令其
lchild
域的指针指向二叉树的根结点,其rchild
域的指针指向中序遍历时访问的最后一个结点; - 令二叉树中序序列中第一个结点的
lchild
域指针和最后一个结点的rchild
域指针均指向头节点。
这样可以从双向线索链表的第一个结点起顺后继进行遍历,也可以从最后一个结点起顺前驱进行遍历。
typedef enum
{
Link, // Link==0,表示指针
Thread // Thread==1,表示线索
} PointerTag;
typedef struct BiThrNode
{
int data;
struct BiThrNode *lchild, *rchild;
PointerTag LTag, RTag;
} BiThrNode, *BiThrTree;
static int data[] = {1, 2, 3, -1, -1, 4, 5, -1, 6, -1, -1, 7, -1, -1, -1};
int i = 0;
// 递归创建二叉树
static int create_binary_tree(BiThrTree* biThrT)
{
int iRet = -1;
if (data[i] == -1)
{
// 如果结点数值为-1,则表示不存在。
biThrT = NULL;
i++;
return iRet;
}
// 1. 先为根节点赋值
*biThrT = (BiThrNode*)malloc(sizeof(BiThrNode));
if (*biThrT == NULL)
{
exit(-1);
return iRet;
}
memset(*biThrT, 0x00, sizeof(BiThrNode));
(*biThrT)->data = data[i];
i++;
// 2. 递归为左子树赋值
create_binary_tree(&(*biThrT)->lchild);
// 3. 递归为右子树赋值
create_binary_tree(&(*biThrT)->rchild);
return iRet;
}
typedef void (*Visit_T)(int data);
static void visit(int data)
{
printf("%d ", data);
}
// 中序遍历
static void in_order_traverse(BiThrTree T, Visit_T visit)
{
if (T != NULL)
{
// 当前结点不为空
// 1. 中序遍历左子树。
in_order_traverse(T->lchild, visit);
// 2. 访问当前结点。
visit(T->data);
// 3. 中序遍历右子树。
in_order_traverse(T->rchild, visit);
}
}
// // 中序线索化
static BiThrTree pre = NULL;
static void in_threading(BiThrTree T)
{
if (T != NULL)
{
in_threading(T->lchild);
// 左子树为空,则左指针为线索,指向前一个结点
if (T->lchild == NULL)
{
T->LTag = Thread;
T->lchild = pre;
}
// 右子树为空,则右子树为线索,指向当前结点
if (pre->rchild == NULL)
{
pre->RTag = Thread;
pre->rchild = T;
}
pre = T;
in_threading(T->rchild);
}
}
// 在中序线索二叉链表上进行遍历
static void in_order_traverse_thread(BiThrTree T, Visit_T visit)
{
BiThrTree p = T->lchild; // p指向二叉树的根节点
while (p != T) // 空树或者遍历结束时p==T
{
// 1. 获取最左下结点,为中序遍历的第一个结点
while (p->LTag == Link)
p = p->lchild;
visit(p->data);
// 2. 右子树为线索并且不是最后一个结点,表示指向有效的后继结点。
while (p->RTag == Thread && p->rchild != T)
{
p = p->rchild;
visit(p->data);
}
p = p->rchild;
}
}
int main(int argc, char* argv[])
{
BiThrTree biThrT = NULL;
create_binary_tree(&biThrT);
in_order_traverse(biThrT, visit);
printf("\n");
// 1. 建立头节点并初始化
BiThrTree biThrTHead = (BiThrTree)malloc(sizeof(BiThrNode));
biThrTHead->LTag = Link;
biThrTHead->lchild = biThrTHead;
biThrTHead->RTag = Thread;
biThrTHead->rchild = biThrTHead;
if (biThrT != NULL)
{
// 2. 二叉树不为空,头节点左指针指向二叉树的根节点
biThrTHead->lchild = biThrT;
// 3. pre指针始终指向刚刚访问过的结点
pre = biThrTHead;
// 4. 中序线索化
in_threading(biThrT);
// 5. 最后一个结点的右指针指向头节点
pre->rchild = biThrTHead;
pre->RTag = Thread;
// 6. 头节点的右指针指向最后一个结点,构成双向线索链表
biThrTHead->rchild = pre;
}
in_order_traverse_thread(biThrTHead, visit);
}
后序线索
- 后继节点:
- 结点是二叉树的根,后继为空
- 结点是其双亲的右孩子或者是其双亲的左孩子且双亲无右子树,后继是双亲结点
- 结点是双亲的左孩子且其双亲有右子树,则后继是其双亲右子树后序遍历的第一个结点。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E7%AE%97%E6%B3%95/adt/binary_tree/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。