二叉树基础知识及遍历方式

参考自:1、link.
参考自:2、link.

目录

  • 一、特殊的二叉树及特点
    • 1、斜树
    • 2、满二叉树
    • 3、完全二叉树
  • 三、二叉树的性质
    • 1、一般二叉树的性质
    • 2 完全二叉树性质
  • 四 二叉树的遍历
    • 1 递归遍历
    • 2 非递归遍历
    • 3 层序遍历
    • 4 复杂度总结:
  • 五 其他的树的知识
    • 1 构建二叉树
    • 2 二叉搜索树的删除插入操作
    • 3平衡二叉树
    • 4 红黑树

二叉树是树的特殊一种,具有如下特点:
1、每个结点最多有两颗子树,结点的度最大为2。
2、左子树和右子树是有顺序的,次序不能颠倒。
3、即使某结点只有一个子树,也要区分左右子树。

一、特殊的二叉树及特点

1、斜树

所有的结点都只有左子树(左斜树),或者只有右子树(右斜树)。这就是斜树,应用较少
二叉树基础知识及遍历方式_第1张图片

2、满二叉树

所有的分支结点都存在左子树和右子树,并且所有的叶子结点都在同一层上,这样就是满二叉树。就是完美圆满的意思,关键在于树的平衡。
二叉树基础知识及遍历方式_第2张图片
根据满二叉树的定义,得到其特点为:

  • 叶子只能出现在最下一层。
  • 非叶子结点度一定是2.
  • 在同样深度的二叉树中,满二叉树的结点个数最多,叶子树最多。

3、完全二叉树

对一棵具有n个结点的二叉树按层序排号,如果编号为i的结点与同样深度的满二叉树编号为i结点在二叉树中位置完全相同,就是完全二叉树。满二叉树必须是完全二叉树,反过来不一定成立。

其中关键点是按层序编号,然后对应查找。
二叉树基础知识及遍历方式_第3张图片
上图就是一个完全二叉树。

结合完全二叉树定义得到其特点:

  • 叶子结点只能出现在最下一层(满二叉树继承而来)
  • 最下层叶子结点一定集中在左 部连续位置。
  • 倒数第二层,如有叶子节点,一定出现在右部连续位置。
  • 同样结点树的二叉树,完全二叉树的深度最小(满二叉树也是对的)。

根据下图加深理解,什么时候是完全二叉树。
二叉树基础知识及遍历方式_第4张图片

三、二叉树的性质

1、一般二叉树的性质

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;
二叉树基础知识及遍历方式_第5张图片
上图中结点总数是10,n2为4,n1为1,n0为5。

2 完全二叉树性质

二叉树基础知识及遍历方式_第6张图片

二叉树基础知识及遍历方式_第7张图片
在上图中验证

  • 第一条: 当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 递归遍历

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);
    
}

关于递归遍历的复杂度;

  • 时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(log⁡n),最坏情况下树呈现链状,为 O(n)。

2 非递归遍历

虽然递归实现简单,但是递归函数有自己特定的问题,比如递归调用会耗费很多的栈空间,也就是内存,同时该过程较为耗时,因此其性能通常不及非递归版本。

下面的非递归遍历写法都是在当节点出栈的时候,处理节点!!

1)非递归先序遍历
二叉树基础知识及遍历方式_第8张图片

算法流程:
步骤:
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)非递归后序遍历

上面小节完成的是根左右的先序遍历,如果将上面的步骤四换为:先左后右,并把弹出的元素先放到另一个收集栈中,最后再弹出这个辅助栈的元素,就得到了后序遍历!!!

即:根右左----->左右根

后序遍历的非递归:
二叉树基础知识及遍历方式_第9张图片
步骤:
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)非递归中序遍历

二叉树基础知识及遍历方式_第10张图片
步骤:
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;
    }
};

本质:
二叉树基础知识及遍历方式_第11张图片

3 层序遍历

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();  //已经遍历过的节点出队列
    }
}

4 复杂度总结:

树的递归遍历相对简单且容易理解,但是递归调用实际上隐藏了相对复杂的遍历过程,要想以非递归的方式来遍历二叉树就需要仔细理解递归调用过程。

递归的复杂度:

  • 时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(log⁡n),最坏情况下树呈现链状,为 O(n)。

非递归方式的复杂度:
时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(log⁡n),最坏情况下树呈现链状,为 O(n)。

层序遍历的复杂度:
记树上所有节点的个数为 n。

  • 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
  • 空间复杂度:队列中元素的个数不超过 n个,故渐进空间复杂度为 O(n)。

五 其他的树的知识

链接: link

1 构建二叉树

1)已知前序、中序遍历结果,还原二叉树
2)已知后序、中序遍历结果,还原二叉树

见上链接

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;
    }
};

3平衡二叉树

平衡二叉树的搜索、插入、删除操作的时间复杂度都是O(log2(N))。N是节点的数量。也可以说是O(h),其中h是树的高度。

插入操作:就是先按二叉搜索树的规则插入,若插入后树失去平衡,再进行左旋右旋,得到平衡二叉树。

  • 1 当插入后是LL型:右旋操作
  • 2 插入后是RR型:左旋操作
  • 3 插入后是LR型:先左旋再右旋
  • 4 插入后是RL型,先右旋再左旋

删除操作:

先按照二叉搜索树的规则进行节点的删除。然后再对失衡的节点进行左右旋以达到重新平衡。
至于如何知道那个节点的是否失衡?怎么记录等等信息?都是很复杂的,没搞清楚。

4 红黑树

红黑树和AVL(平衡二叉树)的区别:

  • avl的每个节点的左右子树的高度查不超过1
  • 红黑树的平衡条件没有avl那么严格,红黑树保证最长路径不超过最短路径的二倍,因而近似平衡。
  • 红黑树的优势:红黑树能够以O(log2(N))的时间复杂度进行搜索、插入、删除操作。此外,任何不平衡都会在3次旋转之内解决。这一点是AVL所不具备的!

红黑树首先是一个二叉搜索树,其次他还要满足以下的条件:

  • 每个节点不是黑色就是红色
  • 根节点是黑色
  • 任何相邻的节点都不能同时为红色,也就是说,红色节点是不连续的。
  • 每个节点到树的尾端(尾端是null,而不是叶子结点)的任何路径,所包含的黑色节点的数量是相同的。
  • (将末尾的null节点看做是黑色的)
  • (新插入的节点,必须是红色的。但是最后是啥不一定,要根据规则再做调整)

红黑树的插入删除操作:

  • 红黑树的插入删除操作就是将先按二叉搜索树的规则插入,并且红黑树规定,新插入的节点必须是红色的。当新插入元素后,可能会破坏红黑树的定义规则,这个时候,要进行一定的调整,调整的过程包含两个基础操作:左右旋转、改变颜色。
  • 红黑树的删除操作:就是先进行二叉搜索树的删除操作,在进行节点的调整,使其重新变成红黑树。

你可能感兴趣的:(数据结构与算法,b树,算法,数据结构)