目录
手写实现二叉树
今天来试着今天试着自己写一个二叉树。包括各种遍历,求树高,节点数的接口的实现。
(1)二叉树节点的定义
下面是二叉树的节点定义
typedef struct Node
{
int val;
struct Node* left;
struct Node* right;
Node(int a = 0, Node* b = nullptr, Node* c = nullptr) :val(a), left(b), right(c) {}
}Node;
(2)二叉树类及其接口
定义相关接口和构造函数
class binary_tree
{
public:
Node* root;
binary_tree():root(nullptr){}
//还原数组存储的二叉树
binary_tree(int arr[],int size);
//前序遍历
void prePrint();
//中序遍历
void inPrint();
//后序遍历
void posPrint();
//层序遍历
void print();
//判断树是否为空
bool isEmpty() { return root==nullptr;}
//树的节点个数
int size();
//树的高度
int height();
//查找节点
Node findkey(int tmp);
//非递归前序遍历
void prePrintStack();
//非递归中序遍历
void inPrintStack();
//非递归后序遍历
void posPrintStack();
(3)接口和其构造函数
1.构造函数的实现
空构造就不必说了,我们知道数组也是可以来存储二叉树的。
1.1数组存储二叉树的原理
对于完全二叉树,我们以 i 表示节点序号,如上图,i 从上至下,从左至右编号。那么这个完全二叉树满足以下性质:
1.如果i > 1,那么它的父节点为 i/2 (整除)。
2.与上一点相反,如果一个节点存在分支节点,那么它的左子节点为 2i,右子节点为2i + 1。
3.上一点继续推导可以得出,如果2i > n 那么这个结点没有分支节点,它是一个叶子点。
4.如果 2i + 1 > n 那么这个结点没有右分支节点。
5.如果2i == n,这个二叉树就不是完全二叉树。
有了这些性质我们可以把一棵二叉树装到数组中存储,方法如下:
我们可以假定任意一棵树为完全二叉树,根节点下标为1,不存在的节点我们可以存特殊值(这里我用的-1),如此我们可以根据以上性质,在一个数组中存储二叉树,以节点编号为数组下标即可!
如上图的二叉树,我们可以把它存在数组中,如下图所示
下面就可以来写通过数组来还原构造二叉树了,通过递归的方式还原出这棵树,代码如下:
//递归函数的实现
Node* create_tree(int arr[],int i, int size)
{
//判断是否越界或不存在节点
if (i >= size||arr[i]==-1)
{
return nullptr;
}
//开始递归构造二叉树
Node*tmp= new Node(arr[i]);
tmp->left = create_tree(arr, 2 * i, size);
tmp->right = create_tree(arr, 2 * i + 1, size);
return tmp;
}
binary_tree::binary_tree(int arr[],int size)
{
int i = 1;
root=create_tree(arr,i,size);
}
2.各种遍历的实现
2.1前序遍历两种写法(递归与非递归)
这里提供两种写法(其实还可以通过Morris遍历)
传统的递归写法:
//前序遍历
void preprint(Node* root)
{
if (root == nullptr)return;
//根左右的顺序
cout << root->val;
preprint(root->left);
preprint(root->right);
}
void binary_tree::prePrint()
{
preprint(root);
}
通过栈来实现的迭代写法:
//非递归的先序遍历
//根左右,由于栈先进后出,所以我们push完根,要先push右,再push左
void binary_tree::prePrintStack()
{
stack<Node*>st;
if(root)st.push(root);
while (!st.empty())
{
Node* t = st.top();
st.pop();
cout << t->val;
if(t->right)st.push(t->right);
if(t->left)st.push(t->left);
}
}
这里由于栈是先进后出,我们如果把根出栈后,然后先push左子树,再push右子树,这样的话,出栈的顺序就是右左,显然不行。我们要想实现根左右,那我们根出栈以后,因该先push右子树,再push左子树。
2.2中序的两种写法
传统的递归写法
//中序遍历
void inprint(Node* root)
{
if (root == nullptr)return;
inprint(root->left);
cout << root->val;
inprint(root->right);
}
void binary_tree::inPrint()
{
inprint(root);
}
通过栈来实现的迭代写法:
void binary_tree::inPrintStack()
{
stack<Node*>st;
Node* cur = root;
//定义一个指针从root开始
while (cur||!st.empty())
{
if (cur != NULL)
{
//如果不为空,一直往左边走,并入栈
st.push(cur);
cur = cur->left;
}
else
{
//如果走到空了,开始输出根节点。
cur = st.top();
st.pop();
cout << cur->val;
//开始处理右子树
cur = cur->right;
}
}
}
中序遍历的顺序是左根右,我们想通过迭代来模拟递归。那么开始处理根节点的时机,就是处理完左子树,才开始输出根节点,再开始去处理右子树。那么迭代可以这样去模仿:我们定义一个cur变量,它的是左子树不为空时,就不断把cur入栈,当它左子树为空时,我们输出根节点,并把cur移动到右子树并入栈,当栈为空且cur为NULL时结束。
2.3后序遍历的两种写法
后序的递归写法
void posprint(Node* root)
{
if (root == NULL) return;
posprint(root->left);
posprint(root->right);
cout << root->val;
}
void binary_tree:: posPrint()
{
posprint(root);
}
后序的非递归写法
oid binary_tree::posPrintStack()
{
stack<Node*> st;
Node* node = root;
Node* mark = NULL;
while (!st.empty() || node != NULL)
{
if (node != NULL)
{
//存在左子树,向左子树走
st.push(node);
node = node->left;
}
else
{
//不存在左子树,开始回溯到根
Node* t = st.top();
//判断右子树存在,且被没被mark标记(说明这是第一次走到父节点,并不是回溯到父节点)
if (t->right != NULL && mark !=t->right)
{
node = t->right;
}
else
{
//如果不存在右子树或该右子树被mark标记(说明这是已经处理的右子树,回溯到父节点)
cout << t->val;
mark = t;
st.pop();
}
}
}
}
后序遍历就比较复杂了,一般不会涉及。想学习的可以看下面这个视频,讲的挺清楚的。
2.4 层序遍历
利用队列进行层序遍历
void binary_tree::print()
{
//利用队列实现层序遍历
queue<Node*>q;
if (root != NULL) q.push(root);
while (!q.empty())
{
//记录上一层结点的个数
int size = q.size();
for (int i = 0; i < size; i++)
{
//取出队头元素
Node* t = q.front();
q.pop();
cout << t->val;
if (t->left) q.push(t->left);
if (t->right) q.push(t->right);
}
//输出完一层
cout << endl;
}
}
3. 其他接口的实现
3.1 获取节点个数
采用递归向下搜索,回溯带回节点个数。
树的节点数=左子树节点数+右子树节点数+1(根节点)
//树的节点个数
int getsize(Node*root)
{
if (root == nullptr) return 0;
//如果是树叶
if (root->left == nullptr && root->right ==nullptr) return 1;
return getsize(root->left) + getsize(root->right)+1;
}
int binary_tree::size()
{
return getsize(root);
}
3.2 获取树高
树高的定义:二叉树结点层次的最大值,也就是其左右子树的最大高度+1
height=max(heightmax左子树+heightmax右子树)+1;
这样我们就可以递归处理左右子树,最后求出树高。
//树的高度
int getheight(Node* root)
{
if (root == nullptr) return 0;
return max(getheight(root->left), getheight(root->right)) + 1;
}
int binary_tree::height()
{
return getheight(root);
}
3.3查找节点
//查找结点
void dfs(Node* root,Node&t,int tmp)
{
if (root == nullptr) return;
if (root->val = tmp)
{
t = *root;
return;
}
dfs(root->left,t,tmp);
dfs(root->right,t,tmp);
}
Node binary_tree::findkey(int tmp)
{
Node t;
dfs(root,t,tmp);
return t;
}
(4)总结
二叉树的大部分问题可以通过递归解决。当树的节点多时,递归会出现,函数大量压栈,占用栈空间出现爆栈,所以出现了非递归的前中后序遍历。但本质上时间复杂度和空间复杂度都是O(N),没有变化,仅解决了爆栈的问题。那有没有更好的算法呢?
答案是有的,Morris遍历可以实现前中后序的遍历,且时间复杂度为O(N),空间复杂度为O(1),优化了空间复杂度。