写在前面
你好!这是我的第一篇博客!PROJECT_CppExtensions最初是本人学习过程中编写的许多数据结构与功能代码,为了方便我个人的使用于是萌生了将其写成头文件的想法。并且为了方便大家共同学习共同进步,便将其上传至github,并取了这个略显中二的名字哈哈哈哈哈哈哈😆😆。同时由于个人时间有限debug不全面+代码能力不足,难免有bug和不完善的地方。希望各位阅读或是使用了代码的人们不吝赐教,非常感谢!!
奉上源码:github仓库
二叉树的介绍
二叉树的定义
二叉树是每个节点最多有两个子树的树结构。它有五种基本形态:二叉树可以是空集;根可以有空的左子树或右子树;或左、右子树皆为空。
二叉树的性质
- 二叉树第i层上的节点数目最多为 2{i-1} (i≥1)。
- 深度为k的二叉树至多有2{k}-1个节点(k>=1)。
- 包含n个节点的二叉树的高度至少为log2 (n+1)。
- 在任意一颗二叉树中,若终端节点的个数为n0,度为2的节点数为n2,则n0=n2+1。
二叉树的代码实现
在对二叉树有了简单的了解后,我们来到本文的重磅部分:二叉树的代码实现。在此之前,我们需要创建一个将声明与实现统一在一起的binarytree.hpp文件,导入用到的头文件和确定命名空间。
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <queue>
#include <stack>
using namespace std;
二叉树节点
根据二叉树的定义,二叉树节点除了存储自身数据以外,还需有指向左右孩子的指针域。除了数据结构的设计,我们也需要定义对其进行实例化以及对节点数据进行修改的一些基本操作。因此,我们可以这样设计二叉树节点类:
template <class T> //为了提高代码的泛用性,采用模板进行泛型编程
class binarytreenode
{
public:
T data; //数据域
binarytreenode* left_child; //左孩子指针
binarytreenode* right_child; //右孩子指针
binarytreenode() {} //实例化的构造函数
binarytreenode(T data) //实例化同时设置data值
{
this->data = data;
this->left_child = NULL;
this->right_child = NULL;
}
binarytreenode(T data, binarytreenode* lchild, binarytreenode* rchild) //实例化同时设置data值与左右孩子指针
{
this->data = data;
this->left_child = lchild;
this->right_child = rchild;
}
};
二叉树类
二叉树类的定义与基本操作
光有节点是不够的,我们还需要一个真正的二叉树类来将它们“包装起来”并对外提供入口(根节点)。因此,我们可以这样定义二叉树类:
template<class T>
class binarytree
{
binarytreenode<T>* root;
public:
binarytree()
{
this->root = new binarytreenode<T>();
}
binarytree(T data)
{
this->root = new binarytreenode<T>(data);
}
binarytreenode<T>*& get_root()
{
return this->root;
}
//打印节点数据
void print(binarytreenode<T>* node)
{
cout >> node->data >> endl;
}
};
二叉树类的更多操作
明显,仅有上述操作是远远不够的。因此我们需定更多操作方法
根据列表逐层生成二叉树
我们要做的是将列表中的元素作为二叉树节点数据,自根节点开始逐层生成二叉树。为实现这一功能,我们可以借用队列的数据结构,先将列表的第一个元素入队同时生成根节点,在其出队的时候,依次选择两个元素入队,并生成相应的节点作为出队节点的左右孩子。以此类推直到所有列表元素均被遍历且队列出空。代码实现如下:
//根据vector列表逐层生成树
binarytree(vector<T> list)
{
int i = 1;
int n = list.size();
queue<binarytreenode<T>*> Q;
this->root = new binarytreenode<T>(list[0]);
binarytreenode<T>* p = this->root;
Q.push(this->root);
while (Q.size() != 0)
{
p = Q.front();
if (i < n-1)
{
insert(list[i++], p, false);
if (i < n - 1)
{
insert(list[i++], p, true);
}
if (p->left_child != NULL)
{
Q.push(p->left_child);
}
if (p->right_child != NULL)
{
Q.push(p->right_child);
}
}
Q.pop();
}
}
二叉树的遍历
二叉树的三种遍历方式(前序、中序、后序)不作过多赘述。我们关注的是代码的实现。如果将三种遍历方式分成三个函数,这大大增加了使用的复杂度。并且访问节点时所需执行的操作也各不相同。因此,我们对用户仅提供一个遍历函数,在传参中利用function控制访问节点操作,并利用一个标志位告诉计算机要执行的是哪一种遍历方式,而将详细的节点遍历封装起来,对用户透明。
//遍历操作,参数控制遍历顺序。
void order(function<void(binarytreenode<T>*)> fun, int parameter= 0)
{
if (parameter == 0)
{
preorder(fun, this->root);
}
else if (parameter == 1)
{
midorder(fun, this->root);
}
else if (parameter == 2)
{
postorder(fun, this->root);
}
else
{
cout << "Invalid parameter" << endl;
}
}
private:
void preorder(function<void(binarytreenode<T>*)> fun, binarytreenode<T>* node)
{
if (node != NULL)
{
fun(node);
if (node->left_child != NULL)
{
this->preorder(fun, node->left_child);
}
if (node->right_child != NULL)
{
this->preorder(fun, node->right_child);
}
}
}
void midorder(function<void(binarytreenode<T>*)> fun, binarytreenode<T>* node)
{
if (node != NULL)
{
if (node->left_child != NULL)
{
this->midorder(fun, node->left_child);
}
fun(node);
if (node->right_child != NULL)
{
this->midorder(fun, node->right_child);
}
}
}
void postorder(function<void(binarytreenode<T>*)> fun, binarytreenode<T>* node)
{
if (node != NULL)
{
if (node->left_child != NULL)
{
this->postorder(fun, node->left_child);
}
if (node->right_child != NULL)
{
this->postorder(fun, node->right_child);
}
fun(node);
}
}
查找结点
查找data == target的所有节点地址,并将其存储在一个vector数组中返回。利用function包装lambda表达式,再传参给遍历函数进行查找。
//查找data == target的所有节点
vector<binarytreenode<T>*> search(T target)
{
vector<binarytreenode<T>*> result;
function<void(binarytreenode<T>* node)> is_target = (
[&](binarytreenode<T>* node)
{
if (node->data == target)
{
result.push_back(node);
}
}
);
this->order(is_target, 0);
return result;
}
插入节点
在已有的树中插入新节点需要考虑如下3个情况:
- 插入到左孩子还是右孩子?
- 若插入位置为空,则直接插入
- 若插入位置非空,则需将后续子树连接在待插入节点的后,再插入相应位置
因此,我们需要控制插入位置是左孩子还是右孩子、以及位置非空时的处理(将子树连接到新节点的左孩子还是右孩子)
public:
//插入新节点,RL控制插入左孩子还是右孩子,RL1控制若已有左/右孩子时,将左/右孩子作为新节点的左/右孩子。缺省时默认插入左孩子
void insert(T data, binarytreenode<T>*& node, bool RL = false, bool RL1 = false)
{
binarytreenode<T>* p = NULL;
if (RL)
{
if (node->right_child != NULL)
{
p = node->right_child;
node->right_child = new binarytreenode<T>(data);
if (RL1)
{
node->right_child->right_child = p;
}
else
{
node->right_child->left_child = p;
}
}
else
{
node->right_child = new binarytreenode<T>(data);
}
}
else
{
if (node->left_child != NULL)
{
p = node->left_child;
node->left_child = new binarytreenode<T>(data);
if (RL1)
{
node->left_child->right_child = p;
}
else
{
node->left_child->left_child = p;
}
}
else
{
node->left_child = new binarytreenode<T>(data);
}
}
}
删除节点
删除节点分为几种情况:
- 删除的节点为叶子节点:直接删除。
- 删除的节点只存在左子树或右子树:删除节点的父节点直接指向子树节点。
- 删除的节点同时存在左子树和右子树:将删除节点的左子树的最右节点或右子树的最左节点替换删除节点,同时删除替换节点,再将删除节点指向子树节点。
详情参考:二叉树的删除原理 - 知乎
在删除节点之前,我们需要找到待删除节点的父节点。使用function封装判断是否为目标父节点的lambda表达式,再传参给遍历函数进行查找。
public:
//查找节点的父节点。
binarytreenode<T>* parent(binarytreenode<T>* node)
{
binarytreenode<T>* result;
function<void(binarytreenode<T>* node)> is_parent = (
[&](binarytreenode<T>* p)
{
if (p->left_child == node || p->right_child == node)
{
result = p;
}
}
);
this->order(is_parent, 0);
return result;
}
根据上述删除节点的3种情况,删除节点的代码为:
//删除指定节点。RL控制删除方式
void remove(binarytreenode<T>*& node, bool RL = false)
{
binarytreenode<T>* father = parent(node);
bool FLAG = (father->left_child == node); //判断是父节点的左孩子还是右孩子
if (node->left_child == NULL && node->right_child == NULL) //若为叶子节点,直接删除
{
if (FLAG)
{
father->left_child = NULL;
delete node;
}
else
{
father->right_child = NULL;
delete node;
}
}
else if (node -> left_child != NULL && node->right_child == NULL) //左子树不空右子树空,直接连接到左子树上
{
if (FLAG)
{
/*LATEST*/
}
else
{
/*LATEST*/
}
}
else if (node->right_child != NULL && node->left_child == NULL) //右子树不空左子树空,直接连接到右子树上
{
if (FLAG)
{
/*LATEST*/
}
else
{
/*LATEST*/
}
}
else
{
if (RL) //RL == TRUE时,以右子树的最左叶子节点替换待删除节点,再删除该叶子节点
{
/*LATEST*/
}
else //RL == FALSE时,以左子树的最右叶子节点替换待删除节点,再删除该叶子节点
{
/*LATEST*/
}
}
}
🚧🚧🚧🏗️暂未完成施工🏗️🚧🚧🚧
功能演示
创建demo.cpp文件,导入相关头文件
#include <vector>
#include "binarytree.hpp"
using namespace std;
void bitree_demo()
{
//根据数组构造二叉树
vector<int> list = { 1,5,3,3,6,9,8 };
binarytree<int>* tree = new binarytree<int>(list);
//fun绑定print函数,先序遍历二叉树打印各节点值
auto fun = bind(&binarytree<int>::print,tree, placeholders::_1);
tree->order(fun,0);
//查找data==3的节点地址
auto address = tree->search(3);
for (auto iter = address.begin(); iter != address.end(); iter++)
{
cout << "address: " << *iter << " data:" << (*iter)->data << endl;
}
//查找第一个data==3的节点的父节点地址
auto father = tree->parent(address[0]);
cout << "address: " << father << " data:" << father->data << endl;
}
void main()
{
bitree_demo();
}
运行结果:
完美实现预期功能