前言
树形数据结构是应用非常广泛的结构,包括二叉树、平衡二叉树、完全二叉树、红黑树、B树和B+树等,本文先介绍基础的树形结构——二叉树。
二叉树
二叉树是指节点可以有两个孩子节点的树型结构,其中左边的孩子节点称为左孩子,右边的孩子节点称为右孩子。
可以通过下面的结构来描述一个二叉树节点,为了数据结构的完整性,这里把构造方法也加上。
struct treeNode{
treeNode *left;
treeNode *right;
int value;
//构造函数
treeNode():value(0),left(nullptr),right(nullptr){};
treeNode(int val):value(val),left(nullptr),right(nullptr){};
treeNode(int val, treeNode *lchild, treeNode *rchild):value(val), left(lchild), right(rchild){};
}
二叉树的构造
既然我们有了节点的构造函数,下面就来构造一颗二叉树。
假设我们要构造这样一颗二叉树,只需要逐个构造节点,然后将节点和二叉树按图中关系连接起来就行了:
treeNode* buildTree(const std::vector<std::string>& vals){
if(vals.empty()||vals[0]=="null")return nullptr; //null表示当前节点为空节点
int total_length = vals.size();
std::queue<treeNode*> m_queue; //用于储存尚未连接孩子节点的二叉树节点
treeNode* head = new treeNode(stoi(vals[0]));
m_queue.push(head);
int cur = 1; //用于指示当前构造的元素的位置
while(!m_queue.empty()){
treeNode* node = m_queue.front();
m_queue.pop();
treeNode* tmp;
if(cur < total_length && vals[cur] != "null"){
tmp = new treeNode(stoi(vals[cur]));
node->left = tmp;
m_queue.push(tmp);
}
++cur;
if(cur < total_length && vals[cur] != "null"){
tmp = new treeNode(stoi(vals[cur]));
node->right = tmp;
m_queue.push(tmp);
}
++cur;
}
return head;
}
二叉树的遍历
遍历数组或链表时,只需要朝某一方向逐个访问每个节点就行,而二叉树是一种分叉的树状结构,如果只向一个方向访问,显然不能遍历所有元素,按照我们构造二叉树的思路,可以用相似的代码按层来遍历一颗二叉树。
void levelOrder(treeNode* root){
std::queue<treeNode*> m_queue;
if(!root)return;
m_queue.push(root);
while(!m_queue.empty()){
int cur_length = m_queue.size(); //这一层的元素个数
for(int i=0;i<cur_length;++i){ //访问这一层的每个元素
treeNode* node = m_queue.front();
m_queue.pop();
std::cout << node->value << std::endl;
//将下一层的元素放入队列中
if(node->left)m_queue.push(node->left);
if(node->right)m_queue.push(node->right);
}
}
}
运行下面的程序,看看层序便历是否正确:
int main(){
std::vector<std::string> vals = {"3", "2", "5", "null", "1", "9", "4"};
treeNode* root = buildTree(vals);
levelOrder(root);
return 0;
}
运行结果如下,与预期结果相符。
层序遍历被称为BFS(Breadth First Search)广度优先搜索,特点是与树的路径无关,按层序来遍历,与之相对的是DFS(Depth First Search)深度优先搜索,DFS按某种规律沿着路径搜索,分为先序、中序和后序。
先序遍历
先序遍历每次访问节点时,先访问当前元素,再访问左孩子节点,最后访问右孩子节点,还是以我们上面的二叉树为例,先序遍历访问的结果应该是:3, 2, 1, 5, 9, 4,下面的代码用于先序遍历二叉树。
void preOrder(treeNode *root){
if(!root)return;
std::cout << root->value << std::endl;
preOrder(root->left);
preOrder(root->right);
}
在组织树形结构的代码时,使用递归的思路是非常实用的,递归可以认为是高中学过的数学归纳法的思路,假设我们现在遍历到了某个节点node,我们不必考虑具体是哪个node,我们之需要考虑递归的结束条件和我们应该如何访问下一个元素即可。在先序遍历中,当访问到叶子节点时就已经遍历结束一条路径,因此递归的结束条件是!root
,当访问当前节点时,先序遍历先访问当前节点,因此先打印当前节点的值,然后分别访问左右孩子即可,下面是先序遍历代码的运行结果:
中序遍历和后序遍历
中序遍历指的是先访问左孩子,再访问当前元素,最后访问右孩子;后续遍历指的是先访问左孩子,再访问右孩子,最后访问自己,按照先序遍历的思路组织代码:
//中序遍历
void inOrder(treeNode *root){
if(!root)return;
preOrder(root->left); //先访问左孩子
std::cout << root->value << std::endl; //再访问自己
preOrder(root->right); //最后访问右孩子
}
//后序遍历
void inOrder(treeNode *root){
if(!root)return;
preOrder(root->left); //先访问左孩子
preOrder(root->right); //再访问右孩子
std::cout << root->value << std::endl; //最后访问自己
}
总结
本文简单介绍了二叉树的结构、层序构造、循环BFS和递归DFS的方法,有兴趣的朋友可以尝试一下DFS构造二叉树、递归BFS和循环DFS,灵活使用递归和循环,DFS和BFS是使用树状结构的基础。下文将介绍基于二叉树的二叉搜索树、平衡二叉树等数据结构。