参考自:1、link.
参考自:2、link.
二叉树是树的特殊一种,具有如下特点:
1、每个结点最多有两颗子树,结点的度最大为2。
2、左子树和右子树是有顺序的,次序不能颠倒。
3、即使某结点只有一个子树,也要区分左右子树。
所有的结点都只有左子树(左斜树),或者只有右子树(右斜树)。这就是斜树,应用较少
所有的分支结点都存在左子树和右子树,并且所有的叶子结点都在同一层上,这样就是满二叉树。就是完美圆满的意思,关键在于树的平衡。
根据满二叉树的定义,得到其特点为:
对一棵具有n个结点的二叉树按层序排号,如果编号为i的结点与同样深度的满二叉树编号为i结点在二叉树中位置完全相同,就是完全二叉树。满二叉树必须是完全二叉树,反过来不一定成立。
其中关键点是按层序编号,然后对应查找。
上图就是一个完全二叉树。
结合完全二叉树定义得到其特点:
1)在非空二叉树的i层上,至多有2^(i-1)个节点(i >=1)。通过归纳法论证。
2)在深度为K的二叉树上最多有2^K - 1个结点(k>=1)。通过归纳法论证。
3)对于任何一棵非空的二叉树,如果叶节点个数为n0,度数为2的节点个数为n2,则有: n0 = n2 + 1
在一棵二叉树中,除了叶子结点(度为0)之外,就剩下度为2(n2)和1(n1)的结点了。则树的结点总数为T = n0+n1+n2;在二叉树中结点总数为T,而连线数为T-1.所有:n0+n1+n2-1 = 2*n2 +n1;最后得到n0 = n2+1;
上图中结点总数是10,n2为4,n1为1,n0为5。
第一条: 当i=1时,为根节点。当i>1时,比如结点为7,他的双亲就是7/2= 3;结点9双亲为4.
第二条:结点6,6 * 2 = 12>10,所以结点6无左孩子,是叶子结点。结点5,5*2 = 10,左孩子是10,结点4,为8.
第三条:结点5,2*5+1>10,没有右孩子,结点4,则有右孩子。
二叉树遍历:从树的根节点出发,按照某种次序依次访问二叉树中所有的结点,使得每个结点被访问仅且一次。
这里有两个关键词:访问和次序。
在遍历之前,首先要知道二叉树的节点的定义:
struct tree {
struct tree* left;
struct tree* right;
int value;
};
1 ) 前序递归遍历
基本思想:先访问根结点,再先序遍历左子树,最后再先序遍历右子树即根—左—右。
代码实现,如下所示
//前序递归遍历
void PreOrderTraverse(struct tree* t)
{
//注意跳出条件
if(t == NULL)
{
return;
}
//注意访问语句顺序
printf("%d ", t->data);
PreOrderTraverse(t->lchild);
PreOrderTraverse(t->rchild);
}
2 ) 中序递归遍历:
基本思想:先中序遍历左子树,然后再访问根结点,最后再中序遍历右子树即左—根—右。
//中序递归遍历
void InOrderTraverse(struct tree* t)
{
//递归结束条件
if(t == NULL)
return;
InOrderTraverse(t->lchild);
printf("%d ", t->data);
InOrderTraverse(t->rchild);
}
3 ) 后序递归遍历:
基本思想:先后序遍历左子树,然后再后序遍历右子树,最后再访问根结点即左—右—根。
//后序递归遍历
void PostOrderTraverse(BiTree t)
{
if(t == NULL)
return;
PostOrderTraverse(t->lchild);
PostOrderTraverse(t->rchild);
printf("%d", t->data);
}
关于递归遍历的复杂度;
虽然递归实现简单,但是递归函数有自己特定的问题,比如递归调用会耗费很多的栈空间,也就是内存,同时该过程较为耗时,因此其性能通常不及非递归版本。
下面的非递归遍历写法都是在当节点出栈的时候,处理节点!!
算法流程:
步骤:
1)先把根节点压入栈中
2)从栈中弹出一个元素
3)处理这个元素(打印)
4)将这个元素的节点压入栈,先右后左(如果有的话)
5)转到步骤2,直到栈空
class Solution2 {
public:
vector<int> preorderTraversal(TreeNode* root)
{
if(root==nullptr) return {};
stack<TreeNode*> stk;
vector<int > res;
stk.push(root);
while(!stk.empty())
{
TreeNode * Node=stk.top();
stk.pop();
res.push_back(Node->val);//处理节点
if(Node->right) stk.push(Node->right);
if(Node->left) stk.push(Node->left);
}
return res;
}
};
2)非递归后序遍历
上面小节完成的是根左右的先序遍历,如果将上面的步骤四换为:先左后右,并把弹出的元素先放到另一个收集栈中,最后再弹出这个辅助栈的元素,就得到了后序遍历!!!
即:根右左----->左右根
后序遍历的非递归:
步骤:
1)先把根节点放入栈中
2)从栈中弹出元素,放入收集栈中
3)将弹出的这个元素的左右节点放入栈中,先左再右!(如果有的话)
4)回到步骤2直到栈空
5)将元素一个一个的从收集栈弹出,并处理,直到收集栈栈空
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root)
{
if(root==nullptr) return {};
vector<int> res;
stack<TreeNode *> stk;
stack<TreeNode *> stk2;
stk.push(root);
while(!stk.empty())
{
TreeNode * Node=stk.top();
stk.pop();
stk2.push(Node);
if(Node->left) stk.push(Node->left);
if(Node->right) stk.push(Node->right);
}
while(!stk2.empty())
{
TreeNode* temp=stk2.top();
stk2.pop();
res.push_back(temp->val);//处理节点
}
return res;
}
};
3)非递归中序遍历
步骤:
1)对每颗子树,整个树的左边界入栈
2)依次弹出的过程中,打印(处理)
3)对弹出的节点的右树重复1,周而复始
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root)
{
if(root==nullptr) return {};
stack<TreeNode* > stk;
vector<int> res;
while(root)
{
stk.push(root);
root=root->left;
}
while(!stk.empty())
{
TreeNode * Node=stk.top();
res.push_back(Node->val);//处理节点
stk.pop();
TreeNode *rightnode=Node->right;
while(rightnode)
{
stk.push(rightnode);
rightnode=rightnode->left;
}
}
return res;
}
};
void FloorPrint_QUEUE(TreeNode *Tree) //层序遍历_队列实现
{
queue < pTreeNode> q;
if (Tree != NULL)
{
q.push(Tree); //根节点进队列
}
while (!q.empty()) //队列不为空判断
{
cout << q.front()->data << " → ";
if (q.front()->leftPtr != NULL) //如果有左孩子,leftChild入队列
{
q.push(q.front()->leftPtr);
}
if (q.front()->rightPtr != NULL) //如果有右孩子,rightChild入队列
{
q.push(q.front()->rightPtr);
}
q.pop(); //已经遍历过的节点出队列
}
}
树的递归遍历相对简单且容易理解,但是递归调用实际上隐藏了相对复杂的遍历过程,要想以非递归的方式来遍历二叉树就需要仔细理解递归调用过程。
递归的复杂度:
非递归方式的复杂度:
时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
层序遍历的复杂度:
记树上所有节点的个数为 n。
链接: link
1)已知前序、中序遍历结果,还原二叉树
2)已知后序、中序遍历结果,还原二叉树
见上链接
1)插入操作简单
2)删除操作
删除操作的话,都是一个套路—递归删除。
删除二叉搜索树的最大节点或最小节点:
//递归法删除二叉树的最小节点
class Solution
{
public:
//递归法
TreeNode * removeminnode(TreeNode* root)
{
//没有右子树
if(root->left==nullptr && root->right==nullptr)
return nullptr;
//有右子树
if(root->left==nullptr && root->right!=nullptr)
return root->right;
root->left=removeminnode(root->left);
return root;
}
};
//删除二叉搜索树的最大节点
class Solution
{
public:
TreeNode * removeminnode(TreeNode* root)
{
//没有左子树
if(root->right==nullptr && root->left==nullptr)
return nullptr;
//有左子树
if(root->right==nullptr && root->left!=nullptr)
return root->left;
root->right=removeminnode(root->right);
return root;
}
};
删除二叉搜索树的某一个特定的节点:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
//递归 函数,在以root为根节点的树上进行删除key,返回删除key后的这个树的根节点
TreeNode* deleteNode(TreeNode* root, int key)
{
if(root==nullptr) return nullptr;
if(root->val==key)//找到这个待删除的节点
{
if(root->left==nullptr && root->right==nullptr)
{
//delete root;
return nullptr;
}
else if(root->left==nullptr)
{
auto it=root->right;
delete root;
return it;
}
else if(root->right==nullptr)
{
auto it=root->left;
delete root;
return it;
}
else
{
TreeNode * cur=root->right;
//找到右子树最左下的节点
while(cur->left!=nullptr)
{
cur=cur->left;
}
cur->left=root->left;
TreeNode* node=root->right;
delete root;
return node;
}
}
if(root->val>key) root->left=deleteNode(root->left,key);
if(root->val<key) root->right=deleteNode(root->right,key);
return root;
}
};
平衡二叉树的搜索、插入、删除操作的时间复杂度都是O(log2(N))。N是节点的数量。也可以说是O(h),其中h是树的高度。
插入操作:就是先按二叉搜索树的规则插入,若插入后树失去平衡,再进行左旋右旋,得到平衡二叉树。
删除操作:
先按照二叉搜索树的规则进行节点的删除。然后再对失衡的节点进行左右旋以达到重新平衡。
至于如何知道那个节点的是否失衡?怎么记录等等信息?都是很复杂的,没搞清楚。
红黑树和AVL(平衡二叉树)的区别:
红黑树首先是一个二叉搜索树,其次他还要满足以下的条件:
红黑树的插入删除操作: