本文搬运自极客时间付费课程《数据结构和算法之美》。
这是第二次整理二叉树相关概念。复习前序、中序、后序遍历以及手写递归实现和迭代实现。基础概念十分钟,前序中序后序遍历半小时熟悉,递归实现1小时理解,迭代实现画图辅助理解加代码实现过程耗时2.5小时。希望以后整理的时候能高效一点。数据结构这种东西为什么我没办法焊在脑子里!
带着问题去学习:
1、二叉树有哪几种存储形式?什么样的二叉树适合用数组去存储?
树(Tree)
比如下面这幅图,A 节点就是 B 节点的父节点,B 节点是 A 节点的子节点。B、C、D 这三个节点的父节点是同一个节点,所以它们之间互称为兄弟节点。我们把没有父节点的节点叫做根节点,也就是图中的节点 E。我们把没有子节点的节点叫做叶子节点或者叶节点,比如图中的 G、H、I、J、K、L 都是叶子节点。
关于“树”,还有三个比较相似的概念:高度(Height)、深度(Depth)、层(Level)。
二叉树(Binary Tree)
树结构多种多样,不过我们最常用还是二叉树。二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子节点和右子节点。
满二叉树
叶子节点全都在最底层,除了叶子节点之外,每个节点都有左右两个子节点,这种二叉树就叫做满二叉树。
完全二叉树
叶子节点都在最底下两层,最后一层的叶子节点都靠左排列,并且除了最后一层,其他层的节点个数都要达到最大,这种二叉树叫做完全二叉树。
如何表示或者存储一颗二叉树?
我们有两种方法,一种是基于指针或者引用的二叉链式存储法,一种是基于数组的顺序存储法。
链式存储法
从图中你应该可以很清楚地看到,每个节点有三个字段,其中一个存储数据,另外两个是指向左右子节点的指针。我们只要拎住根节点,就可以通过左右子节点的指针,把整棵树都串起来。这种存储方式我们比较常用。大部分二叉树代码都是通过这种结构来实现的。
顺序存储法
我们把根节点存储在下标 i = 1 的位置,那左子节点存储在下标 2 * i = 2 的位置,右子节点存储在 2 * i + 1 = 3 的位置。以此类推,B 节点的左子节点存储在 2 * i = 2 * 2 = 4 的位置,右子节点存储在 2 * i + 1 = 2 * 2 + 1 = 5 的位置。
我来总结一下,如果节点 X 存储在数组中下标为 i 的位置,下标为 2 * i 的位置存储的就是左子节点,下标为 2 * i + 1 的位置存储的就是右子节点。反过来,下标为 i/2 的位置存储就是它的父节点。通过这种方式,我们只要知道根节点存储的位置(一般情况下,为了方便计算子节点,根节点会存储在下标为 1 的位置),这样就可以通过下标计算,把整棵树都串起来。不过,我刚刚举的例子是一棵完全二叉树,所以仅仅“浪费”了一个下标为 0 的存储位置。如果是非完全二叉树,其实会浪费比较多的数组存储空间。
所以,如果某棵二叉树是一棵完全二叉树,那用数组存储无疑是最节省内存的一种方式。因为数组的存储方式并不需要像链式存储法那样,要存储额外的左右子节点的指针。这也是为什么完全二叉树会单独拎出来的原因,也是为什么完全二叉树要求最后一层的子节点都靠左的原因。当我们讲到堆和堆排序的时候,你会发现,堆其实就是一种完全二叉树,最常用的存储方式就是数组。
二叉树遍历
1、前序遍历:根 左 右。前序遍历的规则是对于树中的任意一个节点先打印它自己,再打印它的左子树,最后打印它的右子树。
2、中序遍历:左 根 右。中序遍历的规则是对于树中的任意节点来说,先打印它的左子树,然后再打印它本身,最后打印它的右子树。
3、后序遍历:左 右 根。后序遍历的规则是对于树中的任意节点来说,先打印它的左子树,然后再打印它的右子树,最后打印这个节点本身。
实际上,二叉树的前、中、后序遍历就是一个递归的过程。
递归实现二叉树前、中、后序遍历
如何深刻理解递归——从最后叶子节点的递归调用开始想比较好理解。
写递归代码的关键,就是看能不能写出递推公式,而写递推公式的关键就是,如果要解决问题 A,就假设子问题 B、C 已经解决,然后再来看如何利用 B、C 来解决 A。
void preOrder(Node * root)
{
if (root == NULL)
{
return;
}
print root;//伪代码
preOrder(root->left);
preOrder(root->right);
}
void inOrder(Node * root)
{
if (root == NULL)
return;
inOrder(root->left);
print root;
inOrder(root->right);
}
void postOrder(Node * root)
{
if (root == NULL)
return;
postOrder(root->left);
postOrder(root->right);
print root;
}
二叉树的顺序遍历—迭代法
这里补充说明迭代和递归的区别:
迭代是一种编程思想,它是指重复执行一定的算法,直到达到某个条件为止。迭代通常使用循环语句来实现,比如for循环或while循环。迭代的优点是简单易懂,效率高,缺点是可能不够优雅,难以处理复杂的问题。
递归是一种编程思想,它是指函数自己调用自己,直到满足一个终止条件。递归通常使用函数调用来实现,比如f(n) = f(n-1) + f(n-2)。递归的优点是优雅,可以解决一些复杂的问题,缺点是可能消耗更多的内存和时间,容易造成栈溢出。
迭代和递归的区别是:
迭代是利用已知的变量值,根据递推公式不断演进得到变量新值的过程,是正向的。
递归是从问题的最终目标出发,逐渐将复杂问题化为简单问题,最终求得问题的过程,是逆向的。
迭代是有去无回,而递归则是有去有回,因为存在终止条件和返回值。
迭代是一种线性的过程,而递归是一种树形的结构。
参考出处:https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/c-san-chong-jie-fa-by-yizhe-shi-3/
1、前序遍历
迭代法实现二叉树的顺序遍历需要用到栈结构的后进先出思想,实现前序遍历先把右节点压入栈中,再把左节点压入栈中。
vector<int> Solution::preOrder(treeNode * root)
{
if (root == NULL)
{
return{};
}
stack<treeNode *> s;
vector<int> ans;
s.push(root);
while (!s.empty())
{
treeNode * node = s.top();
ans.push_back(node->val);
s.pop();
if(node->right) s.push(node->right);
if(node->left) s.push(node->left);
}
return ans;
}
2、中序遍历
vector<int> Solution:: midOrder(treeNode * root)
{
if (root == NULL)
{
return{};
}
stack<treeNode *> s;
vector<int> ans;
treeNode * tmp = root;
while (!s.empty() || tmp != NULL)
{
while (tmp != NULL)//中序遍历:左,根,右,因此每次都先把一个节点到叶子节点路径上的全部左子节点压入栈中
{
s.push(tmp);
tmp = tmp->left;
}
treeNode * node = s.top();
ans.push_back(node->val);
s.pop();
tmp = node->right;//如果出栈的是左叶子节点,tmp==NULL,没有新的节点入栈,因此下一次出栈的就是根节点。
//当根节点出栈时,tmp指向根节点的右子节点,不为空时,在while循环压入栈,因此下一次出栈的是它的右子节点
}
return ans;
}
3、后序遍历
//出处:
//后序:
// 首先我们知道后序是先左子树,再右子树,再根节点,如果我们先根节点,再右子树,再左子树,得到的结果刚好是反过来的,而先根节点,再右子树,再左子树和先序遍历的先根节点,再左子树,再右子树,其实很类似,只要把左右子树交换看就ok了,所以模仿先序的代码,可以写出后序的代码如下:
// 作者:yizhe - shi
// 链接:https ://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/c-san-chong-jie-fa-by-yizhe-shi-3/
//来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
vector<int> Solution::postOrder(treeNode * root)
{
if (root == NULL)
{
return{};
}
stack<treeNode *> s;
vector<int> ans;
s.push(root);
while (!s.empty())
{
treeNode * node = s.top();
s.pop();
ans.push_back(node->val);
if (node->left) { s.push(node->left); }
if (node->right) { s.push(node->right); }
}
reverse(ans.begin() , ans.end());
return ans;
}
下面是我自己测试用的代码:
binaryTree_traversal.h
#pragma once
#include<vector>
struct treeNode {
int val;
treeNode * left;
treeNode * right;
//构造函数
treeNode(int x) :val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
std::vector<int> preOrder(treeNode *);//前序遍历
std::vector<int> midOrder(treeNode *);//中序遍历
std::vector<int> postOrder(treeNode *);//后序遍历
};
binaryTree_traversal.cpp
// binaryTree_traversal.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include"binaryTree_traversal.h"
#include<stack>
#include<iostream>
using namespace std;
vector<int> Solution::preOrder(treeNode * root)
{
if (root == NULL)
{
return{};
}
stack<treeNode *> s;
vector<int> ans;
s.push(root);
//先序遍历利用栈先进后出,
while (!s.empty())
{
treeNode * node = s.top();
ans.push_back(node->val);
s.pop();
if(node->right) s.push(node->right);
if(node->left) s.push(node->left);
}
return ans;
}
vector<int> Solution:: midOrder(treeNode * root)
{
if (root == NULL)
{
return{};
}
stack<treeNode *> s;
vector<int> ans;
treeNode * tmp = root;
while (!s.empty() || tmp != NULL)
{
while (tmp != NULL)//中序遍历:左,根,右,因此每次都先把一个节点到叶子节点路径上的全部左子节点压入栈中
{
s.push(tmp);
tmp = tmp->left;
}
treeNode * node = s.top();
ans.push_back(node->val);
s.pop();
tmp = node->right;//如果出栈的是左叶子节点,tmp==NULL,没有新的节点入栈,因此下一次出栈的就是根节点。
//当根节点出栈时,tmp指向根节点的右子节点,不为空时,在while循环压入栈,因此下一次出栈的是它的右子节点
}
return ans;
}
//出处:
//后序:
// 首先我们知道后序是先左子树,再右子树,再根节点,如果我们先根节点,再右子树,再左子树,得到的结果刚好是反过来的,而先根节点,再右子树,再左子树和先序遍历的先根节点,再左子树,再右子树,其实很类似,只要把左右子树交换看就ok了,所以模仿先序的代码,可以写出后序的代码如下:
// 作者:yizhe - shi
// 链接:https ://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/c-san-chong-jie-fa-by-yizhe-shi-3/
//来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
vector<int> Solution::postOrder(treeNode * root)
{
if (root == NULL)
{
return{};
}
stack<treeNode *> s;
vector<int> ans;
s.push(root);
while (!s.empty())
{
treeNode * node = s.top();
s.pop();
ans.push_back(node->val);
if (node->left) { s.push(node->left); }
if (node->right) { s.push(node->right); }
}
reverse(ans.begin() , ans.end());
return ans;
}
int main()
{
//首先构建一颗二叉树
// 1
// 2 3
// 4 5 6 7
treeNode * Node1 = new treeNode(1);
treeNode * Node2 = new treeNode(2);
treeNode * Node3 = new treeNode(3);
treeNode * Node4 = new treeNode(4);
treeNode * Node5 = new treeNode(5);
treeNode * Node6 = new treeNode(6);
treeNode * Node7 = new treeNode(7);
Node1->left = Node2;
Node1->right = Node3;
Node2->left = Node4;
Node2->right = Node5;
Node3->left = Node6;
Node3->right = Node7;
vector<int> ans;
Solution ss;
ans = ss.preOrder(Node1);
//打印先序遍历结果
cout << "preorder traversal:" << endl;
for (int i = 0; i < ans.size(); i++)
{
cout << ans[i] << endl;
}
//打印中序遍历结果
vector<int> ans2;
ans2 = ss.midOrder(Node1);
cout << "midorder traversal:" << endl;
for (int i = 0; i < ans2.size(); i++)
{
cout << ans2[i] << endl;
}
//打印后序遍历结果
vector<int> ans3;
ans3 = ss.postOrder(Node1);
cout << "postorder traversal:" << endl;
for (int i = 0; i < ans3.size(); i++)
{
cout << ans3[i] << endl;
}
system("pause");
return 0;
}
运行结果:
后序遍历(正向实现)
思考
1、给定一组数据,比如 1,3,5,6,9,10。你来算算,可以构建出多少种不同的二叉树?
2、我们讲了三种二叉树的遍历方式,前、中、后序。实际上,还有另外一种遍历方式,也就是按层遍历,你知道如何实现吗?