二叉树:
是由N个结点组成的有限集合,该集合可为空(空二叉树),或者由一个根节点和两个互不相交的子节点组成,也就是说除了根节点,每一个子节点都必须且仅有一个父节点。
二叉树的特点:
-
最多有两棵子树,左子树和右子树;
-
对于任意一个i层二叉树而言:
a .它的任意第i层最多有2^(i-1)个节点
比如; 第一层:最多1个
第二层:最多2个
第三层:最多4个
第四层:最多8个
最五层:最多16个
…
第i层:最多2^(i-1)个
b.它的所有层节点之和最多有2^i - 1个节点;
证明如下:
-
五种基本形态:
A. 空二叉树;
B. 只有一个根节点的二叉树;
C. 根节点只有左子树;
D. 根节点只有右子树;
E. 根节点既有左子树又有右子树;
(树不能带环,子节点不能拥有多个父节点;)
特殊的二叉树: -
满二叉树:
在一颗二叉树中,所有的分支节点都存在左子树和右子树,并且所有的叶子都在同一层上,这样的二叉树称为满二叉树;
如下图:
-
完全二叉树:
对一棵有N个结点的二叉树按照层序编号,如果编号为i的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,这样的二叉树称为完全二叉树。(说白了其实就是将结点层序遍历存放在数组中,结点之间没有空缺。)
如果是完全二叉树,对他的每个节点进行顺序编号,任意第 i 个子节点都有以下特性:
001.每个子节点的父节点的下标是 i / 2;(该特性同样适用于满二叉树)
比如:上方完全二叉树例图中第三层,
下标是5的子节点,父节点的下标就是 i / 2 = 2 (舍去小数)
同理下标是4的子节点,父节点的下标就是 i / 2 = 2 (舍去小数)
(说明二者拥有共同的父节点)002.当父节点的下标是i,左子节点的下标就是 2*i, 右子节点的下标是 2*i + 1 (该特性同样适用于满二叉树) 003.如果左子节点的下标值大于总节点数,代表没有左子节点,右子节点同理
通过以上特性可以看出:如果使用完全二叉树按照某种约定(比如从小到大)进行数据的存储,那么在我们进行数据的删除和添加操作时,每一次都要进行一次树结构的调整,除去直接在末尾插入。
- 斜树;
所有结点都在左子树的二叉树为左斜树;
所有结点都在右子树的二叉树为右斜树;
二者统称为斜树;
4)二叉搜索树:
要求左子树的所有子节点都比父节点要小,右子树的所有子节点都比父节点大;
如果通过中序遍历(左中右,下面会详细介绍)二叉搜索树会得到一个有序的列表。
5)平衡二叉树:
它是一颗空树或者它的左右两个子树的高度差的绝对值不会超过1,并且左右两个子树都是一颗平衡二叉树,并且满足二叉搜索树的规则。
二叉树的表示方法:
- 数组表示:
缺点:浪费空间,对于没有孩子的也要占用空间,只适用于完全二叉树和满二叉树; - 链表表示:
A. 二叉链表:有左孩子,右孩子
B. 三叉链表:有左孩子,右孩子,父节点
遍历方式:
前序遍历,中序遍历,后序遍历,主要根据父节点(当前节点)为主,然后左右固定的顺序来区分。
比如:如果是先输入父节点,然后在左节点,再右节点,就是前序遍历,父亲输入在最前面;
比如:如果是先输入左节点,然后在父节点,再右节点,就是中序遍历,父亲输入在中间;
比如:如果是先输入左节点,然后在右节点,再输入父节点,就是后序遍历,父亲在最后输入;
(以上是个人一个小总结,一种记忆推荐,具体还是根据个人理解)
A. 前序遍历:根结点—》左孩子—》右孩子(中左右)
B. 中序遍历:左孩子—》根节点—》右孩子(左中右)
C. 后序遍历:左孩子—》右孩子—》根节点(左右中)
代码表示三种遍历方式:
递归遍历:
//三种遍历方式
void printfTreePrev(Node* root)//打印tree,使用前序----根左右
{
if(root == NULL)
return;
cout<<root->_data<<" ";
printfTreePrev(root->_leftChild);
printfTreePrev(root->_rightChild);
}
void printfTreeMid(Node* root)//打印tree,使用中序----左根右
{
if(root == NULL)
return ;
printfTreeMid(root->_leftChild);
cout<<root->_data<<" ";
printfTreeMid(root->_rightChild);
}
void printfTreeBack(Node* root)//打印tree,使用后序----左右根
{
if(NULL == root)
return;
printfTreeBack(root->_leftChild);
printfTreeBack(root->_rightChild);
cout<<root->_data<<" ";
}
非递归遍历:
任何算法的递归形式都是可以转换成非递归的形式,因为递归实际是一种不断压栈的过程(这时候要小心栈溢出的问题),所以也可以借助堆栈来帮助我们进行非递归。
思想:这里以先序遍历(中左右)为主我们简单分析一下:
单层循环:
首先我们先将根节点push进入栈中,然后再从栈中将它取出打印,然后再将它的右节点push进入栈中,再将左节点push进入栈中(因为栈的逻辑是先进后出,所以要先打印左节点要最后push左节点),然后循环继续pop出栈的栈顶节点,打印数据,然后继续重复push右节点,左节点,循环往复(直到栈为空),借助一个栈来将我们的数据打印出来。
借个图来描述下:
代码如下:
思想:这里以中序遍历(中左右)为主我们简单分析一下:
将当前的节点的左节点一直push进入栈中,直到遇到叶子节点,没有左子节点后,从栈顶pop节点并打印,然后再从它的右子节点为父节点开始,继续push左子节点。
代码如下:
后序非递归遍历(左右中):
//后序循环遍历和前序循环遍历类似,只不过改变push的顺序,
//我们优先push 左子树,再push右子树
void ListTree::LoopBackOrder(TreeNode* node)
{
if (nullptr == node)
{
return;
}
stack<TreeNode*> st1;
stack<TreeNode*> st2;
TreeNode* curNode;
st1.push(node);
while (!st1.empty())
{
curNode = st1.top();
st1.pop();
st2.push(curNode);
if(nullptr != curNode->leftNode)
{
st1.push(curNode->leftNode);
}
if(nullptr != curNode->rightNode)
{
st1.push(curNode->rightNode);
}
}
while (!st2.empty())
{
curNode = st2.top();
cout << curNode->value << " ";
st2.pop();
}
}
双层循环:
//采用非递归的方式---利用栈的后进先出的思想---
//先访问根节点,访问完了以后就push,然后访问左子树,直到访问的左子树为NULL,然后就pop取出栈顶元素访问其右子树,以此类推
//先序遍历----根左右
void PreOrder()
{
vector<Node*> v;
cout<<"中序遍历:"<<endl;
Node* cur = _root;
while(cur || !v.empty())
{
while(cur)
{
v.push_back(cur);
cout<<cur->_data<<" ";
cur = cur->_leftChild;
}
Node* top = v.back();
cur = top->_rightChild;
v.pop_back();
}
cout<<endl;
}
//中序遍历----左根右
void MidOrder()
{
vector<Node*> v;
cout<<"中序遍历:"<<endl;
Node* cur = _root;
while(cur || !v.empty())
{
while(cur)
{
v.push_back(cur);
cur = cur->_leftChild;
}
Node* top = v.back();
cout<<top->_data<<" ";
cur = top->_rightChild;
v.pop_back();
}
cout<<endl;
}
//后序遍历----左右根
void BackOrder()
{
vector<Node*> v;
cout<<"后序遍历:"<<endl;
Node* cur = _root;
Node* pre = NULL;
while(cur || !v.empty())
{
while(cur)
{
v.push_back(cur);
cur = cur->_leftChild;
}
Node* top = v.back();
if(top->_rightChild == NULL || top->_rightChild == pre)
{
cout<<top->_data<<" ";
pre = top;
v.pop_back();
}
else
{
cur = top->_rightChild;
}
}
cout<<endl;
}
二叉树代码补充完整:
template <class T>
struct TreeNode
{
T _data;
TreeNode<T>* _leftChild;
TreeNode<T>* _rightChild;
TreeNode(const T& data)
:_data(data),_leftChild(NULL),_rightChild(NULL)
{}
};
template <class T>
class BinaryTree
{
typedef TreeNode<T> Node;
protected:
Node* _root;
public:
BinaryTree()
:_root(NULL)
{}
BinaryTree(T* arr, size_t n, const T& invalid)
{
int a = 0;
_root = CreateTree(arr, n, invalid, a);
}
~BinaryTree()//析构函数
{
Destory(_root);
}
//采用递归的方式
//销毁结点
void Destory(Node* root)//删除节点采用后序的遍历方式----左右根
{
if(root == NULL)
return;
Destory(root->_leftChild);
Destory(root->_rightChild);
delete root;
}
//创建一颗树
Node* CreateTree(T* arr, size_t n, const T& invalid, int& index)
{
assert(arr);
Node* root = NULL;
if(index < n && arr[index] != invalid)
{
root = new Node(arr[index]);
root->_leftChild = CreateTree(arr, n, invalid, ++index);
root->_rightChild = CreateTree(arr, n, invalid, ++index);
}
return root;
}
//计算叶子节点的个数=======左 + 右
size_t CountLeaf()
{
return _CountLeaf(_root);
}
size_t _CountLeaf(Node* root)
{
if(NULL == root)
{
return 0;
}
if(root->_leftChild == NULL && root->_rightChild == NULL)
return 1;
return _CountLeaf(root->_leftChild) + _CountLeaf(root->_rightChild);
}
//计算第K层的结点个数
size_t CountKLeaf(size_t k)
{
return _CountKLeaf(_root,k);
}
size_t _CountKLeaf(Node* root, size_t k)
{
assert(k > 0);
if(NULL == root)
return 0;
if(k == 1)
return 1;
return _CountKLeaf(root->_leftChild, k-1) + _CountKLeaf(root->_rightChild, k-1);//切勿使用--k,因为此时k是在同一层的左右子树
}
//求tree的深度-----树的高度
size_t HeightTree()
{
return _HeightTree(_root);
}
size_t _HeightTree(Node* root)
{
if(NULL == root)
return 0;
size_t left = _HeightTree(root->_leftChild) + 1;
size_t right = _HeightTree(root->_rightChild) + 1;
return left > right ? left : right;
}
//找到某个节点
void Find(const T& data)
{
Node* node = _Find(_root,data);
if(NULL == node)
cout<<"find number: NULL"<<endl;
else
cout<<"find number: "<<node->_data<<endl;
}
Node* _Find(Node* root,const T& data)
{
if(NULL == root)
return NULL;
if(root->_data == data)
return root;
Node* left = _Find(root->_leftChild,data);
if(left)
return left;
return _Find(root->_rightChild,data);
}