基于决策树算法对良/恶性乳腺癌肿瘤预测
就本课程的数据结构教学而言,在题所述的基础上展开讨论。针对本课程的数据结构教学内容,请提供具体的设计思路及实现过程的详细讲解。旨在帮助学生更好地理解和掌握相关知识的同学可参考上述内容,并在学习过程中相互探讨问题(其中部分实现细节参考了 GitHub 上的相关优秀开源项目)。
内容简介:
基于已知的概率分布构建决策树以计算净现值期望值大于等于零的概率是一种有效的方法用于评估项目风险并判断其可行性这一分析方法具有直观的图解特性
其结构图形特征与树枝延展相仿在机器学习领域中作为一种预测模型它描述了输入特征与输出结果之间的对应关系
信息熵(Entropy )= 系统的凌乱程度本课研究用算法ID3生成决策树这一度量基于信息论中的熵概念
决策树属于一种具有层次结构的数据模型,在其中的每一个中间节点都对应于对特定属性的评估过程,在其每一条分支则导出相应的分类结果或预测值;而每一个终端节点则归类到特定的目标类别中。
决策树是一种广泛应用的分类技术。它属于被监督的学习模式,在这一过程中需要处理一批数据样本,在每个样本中包含一组特征和预设的类别类型,在经过训练后生成一个分类器来识别新出现的对象所属的具体类别类型。这种基于规则的学习算法被称为被监督学习
决策树是一种广泛应用的分类技术
决策树是一种通过一系列标准对数据进行分类的机制;它呈现一种条件导向的结果的方式;这种方法构建一个系统化的方法论基础;广泛应用于多个领域;其中一类应用是通过建立分类模型系统对潜在风险进行识别;另一类则是深入研究特定问题:如根据乳腺癌细胞的各种特征来判断该肿瘤性质的问题
(一)需求和规格说明
本次课程设计基于UCI网站上的Breast-Cancer-Wisconsin数据集进行开发。该数据集包含共10个特征、其中一个是分类变量。实验中将4至10号特征用于训练与测试,并以第十一号变量作为目标进行预测。

部分数据集如下图所示:

基于题目设定,在给定的训练数据集上构建了决策树模型。随后评估了预测系统的性能优劣,并通过将位置类别属性的数据样本进行分类处理,并将其与真实结果进行对比分析后得出了该预测系统的学习效果及其泛化能力的表现信息。
在交叉验证时,输入 590 个数据作为训练数据,98 个数据作为测试数据。
训练数据集与测试数据集的获取途径为:https://pan.baidu.com/s/1BcOY6YzlodazVYNyFXPDKg?pwd=y6af;提取密码则为y6af
(二)设计
2.1 设计初衷
作为人工智能与大数据校企共建实验室的一员,我对机器学习算法有浓厚的兴趣,熟悉结构化数据下的机器学习处理方法,因而选择了基于决策树算法的课程设计课题。同时,我亦兼顾国家大学生创新创业计划项目的申报需求,所开展的研究工作是以基因表达数据为基础进行多种癌症特征基因的选择与识别系统的开发。在这一集成学习框架中,仅作为基础 learners之一的是决策树算法。而本课程的设计则聚焦于乳腺癌细胞特征数据的研究,采用经典的 ID3 分类算法实现了预测模型的核心构建,并对测试集进行了相应的性能评估
在机器学习领域中通常采用Python语言来开发相关算法与模型,在本课程设计中我们面临一个特定挑战:即需要用C++语言基于ID3算法构建决策树模型并完成性能评估。因此整个开发过程是一个较为复杂而耗时的任务。为了实现这一目标需要对决策树的基本原理有深入理解同时还需要掌握编程计算信息熵与信息增益的方法并能灵活运用递归策略来构造决策树数据结构
接下来我将详细介绍,我最终课程设计的设计背景与设计细节。
2.2 设计背景
H.Simon在1983年提出了一种关于学习的基本理论框架:若某一系统经由特定流程的实施而得以提升自身效能,则可视为实现了学习。机器学习作为一个跨学科领域,则专注于探索计算机如何模仿或模拟人类的学习机制,并以此积累新知识与技能。重新组织现有知识体系,并通过持续优化使其性能得到提升。如今机器学习广泛应用于多个领域(如交通、医疗等),其重要性不言而喻。
李航的《统计学习方法》为我们系统地介绍了机器学习中监督式学习的三种决策树算法:ID3、C4.5、CART。通过这些系统的介绍与深入阅读该书的内容后,在本次数据结构课程设计中,我尝试用代码实现了ID3算法的基本框架,并对其实现过程有了更加深入的理解
在谈设计细节前,我们需要对ID3算法进行简单介绍。
ID3 算法作为决策树算法中最为基础的一种方法,在特征选取上主要依据的是信息增益来进行特征的选取。基于公式的计算选择最优子项(纯度最高的节点)继续进行分支扩展以构建决策树。其核心采用了贪心策略:即每一步都做出当前局部最优的选择以期达到全局最优的目标。其中关键在于计算这一指标的基础——信息熵。香农将其引入到通信领域,并用作衡量数据不确定性的重要指标。当系统的信息熵越低时,表明系统的不确定性越小,在决策过程中能够提供更为确定的信息。
信息熵(Entropy)

信息增益(Information gain)

深入理解了决策树算法的核心内容及其信息熵与信息增益的计算方法之后, 我们就可以着手运用ID3算法来构建决策树模型
2.3 设计细节
2.3.1信息熵的计算
基于属性的不同取值情况下,在信息论的基础上我们需要首先计算该特征变量的信息熵这一步骤是后续计算的前提条件。因此输入参数是来自不同数据组中某类属性或分类的具体取值集合例如classes:(2 2 4 4 2 2 4 4 2……)。
我们需通过去除重复元素来增强模型的泛化能力。最终将遍历每一个classes中的取值,并按照前面所述的信息熵计算公式进行处理:首先计算各概率值与其对数值相乘的结果;然后将这些结果累加到entropy中,并将其定义为该属性的信息熵。当完成所有遍历时即可得到entropy值。
2.3.2信息增益的计算
此外我们必须计算数据集合中全部属性的信息增益作为输入使用二维vector阵列
为了实现信息增益的计算目标,在处理数据时需要使用动态数组来存储各属性的信息增益数据。具体而言,在训练集中各个数据的最后一列为classes类别特征,并无需进行计算处理。随后针对每个属性存储其可能取值集合,并对每个属性逐一分析其在各数据中的分布情况;同时为classes特征单独建立标签索引集合,并将这些标签信息放入labels变量中进行后续处理操作。随后遍历每一个属性特征(其中最后一列特指类别标签特征),无需计算该类特征的信息增益指标。通过计算第i个属性取itr概率以及该属性各个可能取值对应的条件熵之和的方式更新H(D|A)的计算结果
最后得到信息增益:
gain[i] =compute_entropy(labels) - gain[i];//g(D,A)=H(D)-H(D|A)
2.3.3决策树的构建
为了更好地实现这一目标, 我们需要对决策树的结构体进行封装. 在此过程中, 我参考教材中介绍的一种较为简便的方法完成了这一封装工作.
struct Tree {
unsigned root;//节点属性值
vector
vector
};
这样利用递归来实现,并结合分治思想以及基于信息增益进行数据分析支撑我们可以顺利构建决策树模型
递归构建决策树:
第一步:识别所有实例是否全部属于同一类别;若所有实例均属于同一类别,则无需进行划分;决策树仅包含一个节点(递归出口);
在第二阶段中进行检查,在确定是否还存在未被考虑到的属性时,请评估当前的情况。如果没有任何新的特征被发现,则意味着无法进一步划分数据集。此时可选特征的数量降为零,在此情况下,默认采用训练数据中出现频率最高的分类标签确定为此节点的标准分类依据(递归出口)。
步骤三:当上一步骤的条件均未满足时,在当前数据集中计算各候选属性的信息增益,并选取具有最大信息增益的属性作为根节点;随后确定该属性的所有可能取值
在第四阶段:基于节点取值情况,在此前提下将examples按照节点取值划分成多个子集(每个子树都基于分支条件构建)。
第五步:对每一个子集递归调用递归构建决策树函数。
2.3.4决策树的打印
基于决策树数据的结构特性,我们利用递归的方法将它的各个结点逐一输出。对于根节点以及内部节点而言,则可分别输出其标签及对应的取值;而当处理至叶子结点时,则可直接获取其属性信息(良性对应编号为2;恶性对应编号为4)。
2.3.5决策树的性能评估
基于现有的决策树模型结构, 我们对所有训练样本与测试样本进行预测, 并获得训练集与测试集的成功率计算结果, 从而完成对决策树性能的评估.
(三)实例
由于决策树的高度较大,限于篇幅,仅截部分实例如图所示。****

完整模型请程序在命令行上阅览。
训练集成功率为1
测试集成功率为0.938776
(四)进一步改进
(1)界面设计较为简单。由于刚接触决策树算法,在程序实现时所构造的决策树架构较为基础,主要依赖于分隔符控制完成输出展示工作。后续建议采用QT等工具来构建更为直观的决策树可视化界面。然而我认为核心目标应放在预测功能上因为模型展示过程是对程序进行可视化的表现也是一个不容忽视的重要环节。
(2)操作空间有限。由于决策树构建与数据处理涉及广泛的技术与流程,其中涵盖了大量必要的前期准备工作,而本课程仅完成了核心部分的开发工作,因此目前还未能充分完成整个系统的建设目标。预计在明年的大创项目中将对整个预测系统进行更加全面的优化与完善。
说明
基于分类型的属性进行分类只能处理离散值数据集的问题。
2.ID3算法对于缺失值没有进行考虑。
3.没有考虑过拟合的问题。
ID3算法在决定根节点和各内部节点分支属性时,以信息增益作为评估标准。该方法存在偏好取值较多的属性的倾向,在某些场景下这类属性可能并不会带来多少有价值的信息。
5.划分过程会由于子集规模过小而造成统计特征不充分而停止。
我会尝试使用别的算法,例如:C4.5、CART算法从而得到更好的预测模型。
(五)心得体会
通过本次课程设计,个人感受是获益良多的.感谢本次课程设计带来的显著帮助.
对于这次课程设计任务,我更加深入地学习了机器学习领域的决策树相关知识,并熟练掌握了递归算法的具体运用方法;在编程实现过程中,我搭建了基于新知识的知识体系框架,并充分利用了这一学期所学的递归、树等技术原理。我认为这次课程设计对我的编程能力提升具有显著效果;这不仅弥补了有限学时带来的不足,在暑期研究机器学习相关课题以及后续大创项目中都发挥了重要作用;特别是在参数调试能力和代码优化方面取得了明显进步
对于大学生而言,网课资源以及各大平台资源(如GitHub、)都是不可或缺的重要资源。它不仅能够帮助你获取课堂之外的知识,并且通过参与这些学习活动还能显著提升了你的编程能力。这也是我们合肥工业大学课程体系设置的一个重要目标。
参数调试与修复功能开发是本专业学生必须掌握的核心技能之一。在课程设计环节中,这些技能将得到显著的提升。通过实践积累经验是提高专业水平的关键。根据时间安排和项目要求,在规定时间内努力完成预期目标。本次课程设计能够很好地展示我的编码能力和技术水平。如果代码和功能存在一些致命性问题,请老师多多批评指正,在此表示虚心接受并愿意改进。未来可期,在更多类似的课程设计机会中不断提升自己 coding skills.
(六)代码实现
为便于深入理解本人的设计思想, 本节将提供具体的代码实现(并包含详细的注释说明), 以帮助读者更好地掌握相关技术细节
/*
该决策树应用的算法为ID3——决策树中选择最优划分属性最基本的一个方法,当然还有CART、C4.5算法
该决策树应用的数据集为判断乳腺癌为良性(classes:2)或为恶性(classes:4)
该数据集的属性共有10个,训练和测试时选择了属性4-10,每个属性的值为1-10(减少属性数量,提高泛化能力)——举例大创工作(小样本,高维度)
代码输出决策树,输出将训练集放入决策树的成功率,交叉验证并输出将测试集放入决策树的成功率
*/
#include <iostream>
#include <cmath>
#include <vector>
#include <string>
#include <algorithm>
#include <fstream>
#include <cstring>
#include <sstream>
using namespace std;
vector<vector<unsigned>> trains;//训练集内容,包含了590个训练样例
vector<vector<unsigned>> tests;//测试集内容,包含了98个测试样例
//属性名称,作用于打印决策树
string attribute_names[] = { "Uniformity of Cell Shape", "Marginal Adhesion", "Single Epithelial Cell Size","Bare Nuclei","Bland Chromatin","Normal Nucleoli","Mitoses" };
//数据集中各个属性的所有可能取值,与attribute_names[]中的元素按顺序对应,用于打印
unsigned attribute_values[] = { 1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,1,2,3,4,5,6,7,8,9,10,2,4 };
//将所有属性和分类的可能取值与0-71一一对应,读取数据集时将数据转化为对应的attribute_number,方便编程(从0开始在print里方便调用数组下标)。(本数组仅作后续讲解,不使用)
unsigned attribute_number[] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71 };
//将数据读入训练集和测试集,并转换为对应的数字,存入动态数组trains和tests
void read_file()
{
vector<unsigned> s;//暂存数据集中的每一行
//10种属性和1种类别,前三种属性忽略
unsigned codeNumber;// 分支节点纯度达到最大,不具有泛化能力,无法对新数据进行预测(过拟合)
unsigned thickness;
unsigned size;
unsigned shape;
unsigned adhesion;
unsigned cellSize;
unsigned nuclei;
unsigned chromatin;
unsigned nucleoli;
unsigned mitoses;
unsigned classes;
//训练集
fstream is;
is.open("breast-cancer.data");
if (is)
{
while (!is.eof())
{
is >> codeNumber >> thickness >> cellSize >> shape >> adhesion >> size >> nuclei >> chromatin >> nucleoli >> mitoses >> classes;
//转换为对应数字
//转换举例:
//转化前:1000025 5 1 1 1 2 1 3 1 1 2
//转化后: 0 10 21 30 42 50 60 70
s.push_back(shape - 1);
s.push_back(adhesion + 9);
s.push_back(size + 19);
s.push_back(nuclei + 29);
s.push_back(chromatin + 39);
s.push_back(nucleoli + 49);
s.push_back(mitoses + 59);
if (classes == 2) //最后一列为分类标签
s.push_back(70);
else
s.push_back(71);
trains.push_back(s);
s.clear();
}
}
is.close();
//测试集
fstream tis;
tis.open("test.data");
if (tis)
{
while (!tis.eof())
{
tis >> codeNumber >> thickness >> cellSize >> shape >> adhesion >> size >> nuclei >> chromatin >> nucleoli >> mitoses >> classes;
s.push_back(shape - 1);
s.push_back(adhesion + 9);
s.push_back(size + 19);
s.push_back(nuclei + 29);
s.push_back(chromatin + 39);
s.push_back(nucleoli + 49);
s.push_back(mitoses + 59);
if (classes == 2)
s.push_back(70);
else
s.push_back(71);
tests.push_back(s);
s.clear();
}
}
tis.close();
}
//计算一个数值得以2为底的对数
double log2(double n)
{
return log10(n) / log10(2.0);
}
//将vector中重复元素合并,只保留一个
template <typename T>
vector<T> unique(vector<T> vals)
{
vector<T> unique_vals;
typename vector<T>::iterator itr;
typename vector<T>::iterator subitr;
int flag = 0;
while (!vals.empty())
{
unique_vals.push_back(vals[0]);
itr = vals.begin();
subitr = unique_vals.begin() + flag;
while (itr != vals.end())
{
if (*subitr == *itr)
itr = vals.erase(itr);
else
itr++;
}
flag++;
}
return unique_vals;
}
//根据属性的取值,计算该属性的信息熵
double compute_entropy(vector<unsigned> v) //输入为不同组数据中某种属性或分类的取值集合,例如classes:(2,2,4,4,2,2,4,4,2......)
{
vector<unsigned> unique_v;
unique_v = unique(v);//去掉重复元素,在本数据集中即(2,4)
vector<unsigned>::iterator itr;
itr = unique_v.begin();
double entropy = 0.0;
auto total = v.size();
while (itr != unique_v.end())//遍历classes的每种取值,计算 概率*log2(概率),取负后加入entropy
{
double cnt = count(v.begin(), v.end(), *itr);//计算每种classes取值在集合中的个数
entropy -= cnt / total * log2(cnt / total);
itr++;
}
return entropy;
}
//计算数据集中所有属性的信息增益
vector<double> compute_gain(vector<vector<unsigned> > truths)//输入为训练集
{
vector<double> gain(truths[0].size() - 1, 0);//存储各个属性的信息增益,规模数为truths的size-1是因为训练集各个数据的最后一列为classes,无需计算
vector<unsigned> attribute_vals;//用来针对每个属性存储其取值,得到该属性在各个数据中的取值集合
vector<unsigned> labels;//用来存储classes的取值
for (unsigned j = 0; j < truths.size(); j++)//将classes取值放入labels
{
labels.push_back(truths[j].back());
}
for (unsigned i = 0; i < truths[0].size() - 1; i++)//遍历每一种属性,最后一列是类别标签,没必要计算信息增益
{
for (unsigned j = 0; j < truths.size(); j++)//将每个数据j中的第i种属性的取值放入attribute_vals
attribute_vals.push_back(truths[j][i]);
vector<unsigned> unique_vals = unique(attribute_vals);//去重,得到第i种属性的所有可能取值
vector<unsigned>::iterator itr = unique_vals.begin();
vector<unsigned> subset;//subset存储对应于数据的第i种属性为某个取值时的classes集合
while (itr != unique_vals.end())//遍历第i种属性的所有可能取值
{
for (unsigned k = 0; k < truths.size(); k++)
{
if (*itr == attribute_vals[k])
{
subset.push_back(truths[k].back());//将数据truth[k]的第i种属性取*itr时的classes放入subset
}
}
double A = (double)subset.size();
gain[i] += A / truths.size() * compute_entropy(subset);//得到第i种属性取*itr的概率以及该属性取值对应classes的信息熵,加到H(D|A)中
itr++;//计算第i种属性的下一个取值
subset.clear();
}
gain[i] = compute_entropy(labels) - gain[i];//g(D,A)=H(D)-H(D|A)
attribute_vals.clear();
}
return gain;
}
//找出数据集中最多的类别属性
template <typename T>
T find_most_common_label(vector<vector<T> > data)
{
vector<T> labels;
for (unsigned i = 0; i < data.size(); i++)
{
labels.push_back(data[i].back());
}
typename vector<T>::iterator itr = labels.begin();
T most_common_label;
unsigned most_counter = 0;
while (itr != labels.end())
{
unsigned current_counter = count(labels.begin(), labels.end(), *itr);
if (current_counter > most_counter)
{
most_common_label = *itr;// 返回最多的类别属性
most_counter = current_counter;
}
itr++;
}
return most_common_label;
}
//根据属性,找出该属性可能的取值
template <typename T>
vector<T> find_attribute_values(T attribute, vector<vector<T> > data)
{
vector<T> values;
for (unsigned i = 0; i < data.size(); i++)
{
values.push_back(data[i][attribute]);
}
return unique(values);
}
/*
在构建决策树的过程中,如果某一属性已经考察过了
那么就从数据集中去掉这一属性,此处不是真正意义
上的去掉,而是将考虑过的属性全部标记为100
*/
template <typename T>
vector<vector<T> > drop_one_attribute(T attribute, vector<vector<T> > data)
{
vector<vector<T> > new_data(data.size(), vector<T>(data[0].size() - 1, 0));
for (unsigned i = 0; i < data.size(); i++)
{
data[i][attribute] = 100;
}
return data;
}
//决策树的结构体封装
struct Tree {
unsigned root;//节点属性值
vector<unsigned> branches;//节点可能取值
vector<Tree> children; //孩子节点
};
//递归构建决策树
void build_decision_tree(vector<vector<unsigned> > examples, Tree& tree)// (子集)集合,(子树)根结点
{
//第一步:判断所有实例是否都属于同一类,如果是,则无需划分,决策树是单节点(递归出口)
vector<unsigned> labels(examples.size(), 0);
for (unsigned i = 0; i < examples.size(); i++)
{
labels[i] = examples[i].back();// 返回数组最后一个单元:classes
}
if (unique(labels).size() == 1)
{
tree.root = labels[0];
return;
}
//第二步:判断是否还有剩余的属性没有考虑,如果所有属性都已经考虑过了,则无法划分
//那么此时属性数量为0,将训练集中最多的类别标记作为该节点的类别标记(递归出口)
if (count(examples[0].begin(), examples[0].end(), 100) == examples[0].size() - 1)//只剩下一列类别未被标记为100
{
tree.root = find_most_common_label(examples);
return;
}
//第三步:在上面两步的条件都判断失败后,计算信息增益,选择信息增益最大的属性作为根节点,并找出该节点的所有取值
vector<double> standard = compute_gain(examples);//计算信息增益
tree.root = 0;
for (unsigned i = 0; i < standard.size(); i++)//选择信息增益最大的属性作为根节点
{
if (standard[i] >= standard[tree.root] && examples[0][i] != 100)
tree.root = i;
}
tree.branches = find_attribute_values(tree.root, examples);//找出该节点的所有取值
//第四步:根据节点的取值,将examples分成若干子集(子树都是在分支的条件下建立)
vector<vector<unsigned> > new_examples = drop_one_attribute(tree.root, examples);//已考察属性要先作处理再开始分
vector<vector<unsigned> > subset;
for (unsigned i = 0; i < tree.branches.size(); i++)//分支
{
for (unsigned j = 0; j < examples.size(); j++)//行数
{
for (unsigned k = 0; k < examples[0].size(); k++)//列数
{
if (tree.branches[i] == examples[j][k])
subset.push_back(new_examples[j]);
}
}
//第五步:对每一个子集递归调用build_decision_tree()函数
Tree new_tree;
build_decision_tree(subset, new_tree);
tree.children.push_back(new_tree);
subset.clear();
}
}
//递归打印决策树
void print_decision_tree(Tree tree, unsigned depth)
{
for (unsigned d = 0; d < depth; d++) cout << "\t";
if (!tree.branches.empty()) //根节点和内部节点
{
cout << attribute_names[tree.root] << endl;
for (unsigned i = 0; i < tree.branches.size(); i++)
{
for (unsigned d = 0; d < depth + 1; d++) cout << "\t";
cout << attribute_values[tree.branches[i]] << endl;// 将转化后的数据转化为转化前的取值(1-10)
print_decision_tree(tree.children[i], depth + 2);
}
}
else //是叶子节点
{
cout << attribute_values[tree.root] << endl;
}
}
//根据已经建立好的决策树模型预测数据集中的每个样例
unsigned classify_tree(Tree tree, vector<unsigned> test)
{
if (tree.branches.empty())//是叶子节点
{
return tree.root;
}
else
{
for (unsigned i = 0; i < tree.branches.size(); i++)
{
if (test[tree.root] == tree.branches[i])
{
return classify_tree(tree.children[i], test);
}
}
}
}
//检测决策树学习效果和泛化性能
void test_tree(Tree tree)
{
unsigned num_right_test = 0;
unsigned num_right_train = 0;
unsigned result;
double right_ratio_test;
double right_ratio_train;
//训练集,得出学习效果
for (unsigned i = 0; i < trains.size(); i++)
{
result = classify_tree(tree, trains[i]);
if (result == trains[i].back())
num_right_train++;
}
right_ratio_train = double(num_right_train) / trains.size();
cout << endl;
cout << "训练集成功率为" << right_ratio_train;
//测试集,得出泛化性能
for (unsigned i = 0; i < tests.size(); i++)
{
result = classify_tree(tree, tests[i]);
if (result == tests[i].back())
num_right_test++;
}
right_ratio_test = double(num_right_test) / tests.size();
cout << endl;
cout << "测试集成功率为" << right_ratio_test;
}
int main()
{
read_file();//读取训练集的数据入动态数组trains,读取测试集的数据入动态数组tests
Tree tree;//创建一棵决策树
build_decision_tree(trains, tree);
print_decision_tree(tree, 0);//打印决策树
test_tree(tree);//将训练集数据和测试集数据分别放入决策树进行测试,输出各自的预测成功率,得到学习效果和推广性能
return 0;
}
cpp

部分代码借鉴GitHub大佬的开源代码,如有不足之处,欢迎指出,共同学习!
