C++从零开始 ------ 二叉树与 轮子的故事
C++从零开始 ------ 二叉树与 轮子的故事
轮子
引入
一个优秀的程序员拿到一个新语言的第一件事往往就是造轮子
,没有轮子的程序就像没有轮子的汽车,怎么跑都跑不动 。大家都喜欢Python,为什么呢?因为Python全部是轮子,C++跟Python一比,就是婴儿车对战高达(就便利性来说),当然,C++优秀的性能也是不容小觑的,想象一下全副武装的C++,那就是无敌的感觉啦。
类
我们一直说的轮子就是类
,自从类
这个概念被发明以来,人类的bug就以指数级下降,毫不夸张的说,没有类就没有今天的3A大作。最简单的类就长下面这个亚子。
class myclass{
// private和publlic 是类中特有的两个关键字,新手可以全部用public
// private一般存储不会被外界使用的变量,只在类中使用
private:
int data;
public:
// public中写函数
// 以类名命名的函数被称为构造函数,它将会在类被创建的时候调用
myclass(){data = 0};
// 你可以写多个构造函数,类被创建的时候会根据传入的参数调用合适的构造函数
myclass(int data){this->data = data}
}
然后我们可以在main函数中这样构造一个类
// 这个main请跟在上面的class底下写
int main()
{
// 创建一个myclass的指针a
myclass * a = new myclass();
myclass * b = new myclass(2);
}
下面是最关键的,也是类能消灭诸多bug的神奇之处,它是松耦合的,也就是说你可以把它单独拿出来给另一个程序使用,他有一个好兄弟叫做.h文件
,如果你把class单独拎出来放到一个叫filename.h
的地方,那么你的main函数可以不紧跟着下面写,而可以写到另一个cpp里面,像这样(.h和.cpp文件请放在同一个文件夹中)
// 加载头文件
#include "filename.h"
// 这个main函数写在main.cpp中
int main()
{
// 创建一个myclass的指针a
myclass * a = new myclass();
myclass * b = new myclass(2);
}
所以在类的概念被提出 来之后,编程行业基本上就是上github上搜一堆.h文件
,include一下,调几个函数,一个项目就完成了,它不香吗?
另外,除了类可以放在.h
文件中,什么#include<iostream>之类
的也可以放在.h文件
中,让main.cpp
变得更加清爽,由此衍生出来的QT项目有时候mian.cpp
就只有一句话(当然这也应该是我们的追求)
#include <QT>
qt.start();
二叉树
创建一个.h文件
,写入以下东西,直接看代码,要说的都在代码里
#include <iostream>
#include <vector>
using namespace std;
// 这里节点我没有使用类,而是使用C风格的struct,与类是一样的效果
struct node
{
char data; // 数据
node * lnext; // 左节点 这里为什么要用指针?想一想!
node * rnext; // 右节点
bool done; // 用于dfs判断是否访问过
node(char d) // 构造函数(用来初始化一个节点)
{
data = d;
lnext = NULL; //NULL 是C++保留字,表示空
rnext = NULL;
done = false;
}
};
// 一棵二叉树
class Tree
{
//新手最好一律public
public:
node * root = NULL; // 树根
// 构造函数,r代表树根
Tree(){root = NULL;};
Tree(node * root)
{
this->root = root; // this->root 特指类中的root,root指传入参数
}
// root 代表树上的某一节点(不是类中本有的变量root), n表示新节点,isriaght 表示向左插入还是向右插入
void insert(node *& root, node * n, bool isright)
{
// 保证节点右节点是空
if(isright & root->rnext == NULL) root ->rnext = n;
else if(root->lnext == NULL) root ->lnext =n;
else printf("insert faill!\n");
};
// 输出前序遍历结果
void outputPreoder(node * root)
{
if (root!=NULL)
{
printf("%c ",root->data);
outputPreoder(root->lnext);
outputPreoder(root->rnext);
}
}
// 依据前序遍历建树,root代表根节点,i代表进行到的前序遍历数组的下标,input代表前序遍历数组
void preOrder(node *& root,int &i,vector<char> input) // *&的意思是再次函数内所有对root的改变都将·生效·, 如果想让root的改变只在该函数内·生效·,就用*,&i是一样的意思(仔细想一想为什么要&i)
{
if(i>=input.size())return ; // 已经到头
if(input[i]=='#') // 空节点,没必要继续
{
i++;
root=NULL;
}
else // 建树
{
root=new node(input[i]); // 每个节点都需要空间
// 简单的说就是一个节点要从NULL变为一个有实际意义的节点,就必须要分配空间,即new
i++;
preOrder(root->lnext,i,input); //递归
preOrder(root->rnext,i,input);
}
}
// 这里dfs将输出每个节点的数据
// 什么问题适用于dfs解决?父节点需要解决的事情依赖于子节点的解决
void dfs(node *& root)
{
if(root == NULL) return;
root->done = true;
if (root->lnext!=NULL) //NULL数据是不可访问的,所以必须先判断
if (!root->lnext->done)
{
dfs(root->lnext);
// TODO:在这里你可以进行数据处理
// 并且由于dfs的特性,左子节点的数据已经处理好
// 子节点的数据可以这样获得root->lnext->data(左子节点)
}
if (root->rnext!=NULL)
if(!root->rnext->done)
{
dfs(root->rnext);
// TODO:同上
}
// 经过数据处理后的data,如果进行了数据处理的话
printf("%c",root->data);
}
};
要想学好树,学好递归很重要,我甚至可以大胆的说,递归是程序员质检师。我大二的算法老师一句话道破天机
递归就是耍流氓,我不会写怎么办?递归。我相信这个函数可以做到我让他做的,一直相信,那这个函数就真的做到了。
学递归没有什么太好的办法,只能多写程序,多练,多想,直到想表达一件复杂的事情的时候想到递归,那基本就可以了。
还有一个问题:指针
,怎么学好指针?这个倒是有捷径
1. 写几个有指针的程序,然后开启单步调试,看看这些指针究竟指到哪里去了。
2. 牢记一点:在编程里等于号永远不是等于号!它是·赋值·!!!
上述代码可以自行在main.cpp中include并测试,这里给出一个测试方案:
#include "tree.h"
int main()
{
vector <char> a = {'A','B','C','#','#','D','E','#','G','#','#','F','#','#','#'};
Tree *mytree = new Tree();
int i = 0;
mytree->preOrder(mytree->root,i,a);
mytree->outputPreoder(mytree->root);
mytree->dfs(mytree->root);
}