数据结构与算法面试要点简明教程(六)—— 树

本文简要介绍了树的概念及其重要类型,包括树的定义、二叉树、二叉搜索树、平衡二叉树(如红黑树)、哈夫曼树以及B树、B+树和B*树。还探讨了树的相关面试题目,如如何根据遍历顺序复原二叉树。这些知识对于理解和解决数据结构与算法面试问题至关重要。
摘要由CSDN通过智能技术生成

参考:https://blog.csdn.net/jiaoyangwm/article/details/80808235

https://blog.csdn.net/a2392008643/article/details/81781766

https://mp.weixin.qq.com/s/vn3KiV-ez79FmbZ36SX9lg

本文仅是将他人博客经个人理解转化为简明的知识点,供各位博友快速理解记忆,并非纯原创博客,如需了解详细知识点,请查看参考的各个原创博客。

目录

第六章  树

6.1  树的定义

6.2  二叉树的定义

6.3  二叉树的遍历

6.4  二叉搜索树

6.5  平衡二叉树

6.6  红黑树

6.7  哈夫曼树

6.8  B树、B+树和B*树

6.9  相关面试题


第六章  树

树是n个结点的有限集(n=0时,称为空树),在任意一颗非空树中:

  • 有且仅有一个根结点(Root)
  • 当n>1时,其余结点可分为m个互不相交的有限集合,其中每个集合本身又是一棵树,并且称为根的子树(Subtree)

6.1  树的定义

1、树的度

树的结点包含一个数据元素及若干个指向其子树的分支,结点拥有的子树数量称为结点的度。

树的度

 

2、树结点间的关系

树中结点的关系包括:双亲、兄弟、孩子,下面用一张图简明表达:

树结点的关系

 

3、树的层次

树中有深度和高度两种定义,深度定义是从上往下的,高度定义是从下往上的。(此处约定深度和高度均从1开始,空树为0)

树的层次

 

6.2  二叉树的定义

二叉树(Binary tree)是n个结点的有限集合(该集合或为空集),由一个根节点和两棵互不相交、分别称为根节点的左子树和右子树的二叉树组成,二叉树中不存在度大于2的结点。

二叉树

 

6.2.1  特殊二叉树

1、斜二叉树

所有结点都只有左子树的二叉树叫左斜树,所有结点都只有右子树的二叉树叫右斜树。

2、满二叉树

所有分支节点都存在左子树和右子树,并且所有叶子都在同一层上的二叉树。

3、完全二叉树

高度为h的二叉树,除了h层外其余层次的结点数均达到最大,且h层的所有结点都连续集中在最左边。

完全二叉树

 

6.2.2  二叉树的重要性质

1、在二叉树的第i层,至多有2^{_{i-1}}个结点

2、深度为k的二叉树最多有2^{k} -1个结点

3、对任何非空二叉树T,若n_{0}表示叶结点的个数,n_{2}是度为2的非叶节点的个数,那么两者满足关系n_{0}=n_{2}+1

4、具有n个结点的完全二叉树高度为\log_{2} (n+1)

5、给定n个结点,能构成h(n)=\frac{C_{n}^{2n}}{n+1}种不同的二叉树

6、完全二叉树的任意结点i,其父节点为[i/2],左孩子为2i,右孩子为2i+1。

 

6.2.3  二叉树的存储结构

1、顺序存储结构

用一维数组存储二叉树中的结点,并且结点的存储位置,也就是数组的下标要能体现结点之间的逻辑关系。

  • 完全二叉树
完全二叉树——顺序存储结构

 

  • 一般二叉树

可以将其按完全二叉树来编号,仅是把不存在的结点设置为空,但会造成空间浪费。

一般二叉树——顺序存储结构

 

2、链式存储结构

二叉树每个结点最多有两个孩子,所以为其设计一个数据域和两个指针域,称这样的链表为二叉链表。

二叉链表

 

6.3  二叉树的遍历

定义:从根节点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次,且仅被访问一次。

1、前序遍历

若二叉树为空,则空操作返回,否则先访问根节点,然后前序遍历左子树,再前序遍历右子树,下图遍历顺序为:ABDGHCEIF

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    //递归法
    void dfs(TreeNode* root, vector<int> res){
        if (root) {
            res.push_back(root->val);
            dfs(root.left, res);
            dfs(root.right, res);
        }
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        dfs(root, res);
        return res;
    }
    /*-----------------------------------------------*/
    //非递归
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> st;    //设置一个栈
        st.push(root);    //根结点入栈
        while(!st.empty()){    //栈中有元素就一直循环
            TreeNode* node = st.top();
            st.pop();    //取结点,并出栈
            if(!node) continue;
            res.push_back(node->val);    //添加进结果
            st.push(node->right);    //右孩子入栈(先进后出)
            st.push(node->left);    //左孩子入栈
        }
        return res;
    }
};

2、中序遍历

若二叉树为空,则空操作返回,否则从根节点开始(并不访问),中序遍历左子树,然后访问根节点,最后中序遍历右子树,下图遍历顺序为:GDHBAEICF

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> st;
        TreeNode* cur = root;
        while(cur || !st.empty()){    //当指针存在或栈不为空时
            while(cur){    //将指针指向最左边的叶子节点,一路上的所有结点均入栈
                st.push(cur);
                cur = cur->left;
            }
            TreeNode* node = st.top();
            st.pop();    
            res.push_back(node->val);    //取栈顶结点遍历
            cur = node->right;    //指针指向右孩子
        }
        return res;
    }
};

3、后序遍历

若二叉树为空,则空操作返回,否则从左到右,先叶子后结点的方式遍历访问左右子树,最后访问根节点,,下图遍历顺序为:GHDBIEFCA

前序遍历为 root -> left -> right,后序遍历为 left -> right -> root。可以修改前序遍历成为 root -> right -> left,那么这个顺序就和后序遍历正好相反。

class Solution {
public:
    vector<int> postorderTraversal(TreeNode* root) {
        vector<int> res;
        stack<TreeNode*> st;
        st.push(root);
        while(!st.empty()){
            TreeNode* node = st.top();
            st.pop();
            if(!node) continue;
            res.push_back(node->val);
            st.push(node->left);
            st.push(node->right);
        }
        reverse(res.begin(), res.end());
        return res;
    }
};

4、层序遍历

若二叉树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,同一层中,按从左到右的顺序对结点逐个访问,遍历顺序为:ABCDEFGHI

注意:

1)第一次碰到结点打印出来——先序遍历

2)第二次碰到结点打印出来——中序遍历

3)第三次碰到结点打印出来——后序遍历

其中,前和中、中和后两对遍历序列都可以唯一确定这棵二叉树,二叉树遍历的核心问题:怎么将二维结构转换成一维线性序列

 

6.4  二叉搜索树

二叉搜索树(BST):根节点大于等于左子树所有节点,小于等于右子树所有节点。其具有如下特性:

  • 最左下的端结点一定最小,最右下的端结点一定最大。
  • 二叉搜索树中序遍历有序。

有了二叉搜索树,当你要查找一个值,就不需要遍历整个序列或者说遍历整棵树了,可以根据当前遍历到的结点的值来确定搜索方向(剪枝),就可以使插入、搜索效率大大提高。

1、查找

从根结点开始,根据目标值大小,选择左/右子树查找,直到找到目标。

//查找
Position IterFind(ElementType X, BinTree BST)
{
	while(BST){
	    if(X>BST->Data)
			BST=BST->Right;    //向右子树中移动,继续查找
		else if(x<BST->Data)
			BST=BST->Left;
		else
			return BST;
	}
	return NULL;
}

2、插入

根据目标值,递归的寻找适合结点插入的位置,生成新节点并插入。

//插入
BinTree Insert(ElementType X,BinTree BST)
{
	if(!BST){
		//若原树为空(即BST不存在),生成并返回一个结点
		BST=malloc(sizeof(struct TreeNode));
		BST->Data=X;
		BST->Left= BST->Right = NULL;
	}else{
		if (X<BST->Data)
			//递归插入左子树
			BST->Left = Insert(X,BST->Left);
		else if (X>BST->Data)
			BST->Right=Insert(X,BST->Right);
	}
	return BST;
}

3、删除

先找到要删除的结点,然后进行如下判断:

若结点为叶子结点,直接删除;

若结点只有左子树或只有右子树,直接用左子树或右子树替换该结点;

若结点左右子树均存在,则找左子树中最大(最右)的结点(直接前驱)或右子树中最小(最左)的结点(直接后继)替换该结点。

//删除
BinTree Delete(ElementType X,BinTree BST)
{
	Position Tmp;
	//树为空
	if(!BST) printf("要删除的元素未找到");
	else if (X<BST->Data)
		//左子树递归删除
		BST->Left = Delete(X,BST->Left);
	else if (X>BST->Data)
		//右子树递归删除
		BST->Right = Delete(X,BST->Right);
	else //找到了要删除的结点
		if(BST->Left && BST->Right){
			//如果被删除的结点有左右两个结点
			Tmp = FindMin(BST->Right);    //在右子树中找最小值或左子树中找最大值
			BST->Data =Tmp->Data;    //用该值覆盖要删除结点的值
			BST->Right = Delete(BST->Data,BST->Right);   //在删除结点的右子树中删除最小元素          
		} else {
			//被删除的结点只有一个或无子结点
			Tmp = BST;
			if(!BST->Left)   //有右子结点或无子结点
				BST=BST->Right;
			else if (!BST->Right)
				BST=BST->Left;
			free(Tmp);
		}
		return BST;
}

6.5  平衡二叉树

定义:平衡二叉树又称为AVL树,是一种特殊的二叉搜索树,其任意结点左右子树高度差的绝对值不超过1。

将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子,那么平衡二叉树上的所有结点的平衡因子只可能是-1、0和1。

引入AVL树的原因:如果插入的序列越有序,生成的二叉搜索树越像一个链表,查找的时间复杂度就接近O(n)了。为了避免这种情况,引入了平衡二叉树,即让树的结构看起来尽量“均匀”,左右子树的节点数尽量一样多。

判定二叉树是否平衡

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {    //使用递归判定二叉树是否平衡
private:
    bool res = true;
    int maxDepth(TreeNode* root){
        if(root == nullptr) return 0;
        int l = maxDepth(root->left);
        int r = maxDepth(root->right);
        if(abs(l-r) > 1) res = false;
        return max(l, r) + 1;
    }
public:
    bool isBalanced(TreeNode* root) {
        maxDepth(root);
        return res;
    }
};

6.6  红黑树

1、定义

红黑树是一种二叉搜索树,但其在每个结点上增加了一个存储位表示结点的颜色,可以是Red或Black。

通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的(弱平衡)。下图即为一颗红黑树:

红黑树

 

红黑树在二叉搜索树的基础上增加了着色,并通过相关性质使得红黑树相对平衡,这些性质具体包括:

1)每个结点要么是红的要么是黑的;

2)根结点是黑的;

3)每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的;

4)如果一个结点是红的,那么它的两个儿子都是黑的;

5)对于任意结点而言,其到树尾端NULL结点的每条路径都包含相同数目的黑结点。

正是红黑树的这5条性质,使一棵n个结点的红黑树始终保持了log n的高度,从而保证了红黑树的查找、插入、删除的时间复杂度最坏为O(log n)。

 

2、红黑树的旋转

红黑树的旋转是一种能保持二叉搜索树性质的搜索树局部操作。有左旋和右旋两种旋转,通过改变树中某些结点的颜色以及指针结构来保持对红黑树进行插入和删除操作后的红黑性质。

左旋:对某个结点x做左旋操作时,假设其右孩子为y而不是NULL:以x到y的链为“支轴”进行。使y成为该子树新的根结点,x成为y的左孩子,y的左孩子成为x的右孩子。

左旋示例

 

右旋:对某个结点x做右旋操作时,假设其左孩子为y而不是NULL:以x到y的链为“支轴”进行。使y成为该子树新的根结点,x成为y的右孩子,y的右孩子成为x的左孩子。

右旋示例

 

3、红黑树相较AVL树的优点

AVL 树是高度平衡的,频繁的插入和删除,会引起频繁的rebalance,导致效率下降;红黑树高度弱平衡,算是一种折中,插入最多两次旋转,删除最多三次旋转。红黑树查找、插入、删除的性能都是O(logn),且性能稳定,所以STL里面很多结构包括map底层实现都是使用的红黑树。

 

6.7  哈夫曼树

1、概念

结点的带权路径:该结点到树根之间的路径长度与结点上权的乘积

树的带权路径:树中所有结点的带权路径长度之和

哈夫曼树:带权路径长度(Weighted Path Length, WPL)最小的二叉树,称为哈夫曼树,也叫最优二叉树

如上图所示,二叉树a的WPL为:

WPL=5\times 1+15\times 2+40\times 3+30\times 4+10\times 4=315

二叉树b的WPL为:

WPL=5\times 3+15\times 3+40\times 2+30\times 2+10\times 2=220

这意味着,如果此时有10000个学生需要判断五级分制成绩,用二叉树a需要做31500次比较,用二叉树b只需要做22000次比较。

2、哈夫曼树的构造过程

哈夫曼树的构造算法步骤如下:

对于刚才的五级分制问题,构造出的哈夫曼树如下图所示:

可以看到,其WPL=205,此时才是最优的哈夫曼树。

3、哈夫曼树的特点

1)没有度为1的结点;

2)n个叶子结点的哈弗曼树共有2n-1个结点;

3)对同一组权值存在不同构的哈夫曼树。

4、哈夫曼编码

哈夫曼编码是哈夫曼树的一种应用,广泛用于数据文件压缩。其根据字符使用频率进行编码,以最大化节省字符的存储空间。

具体来说,哈夫曼编码算法用字符(结点)在文件中出现的频率(权值)来建立哈夫曼树,路径上使用0,1编码表示该字符。下面举一个例子来直观表达:

假设有A、B、C、D、E五个字符,出现的频率分别为5、4、3、2、1,那么我们根据哈夫曼树的构造方法,即可构造出如下哈夫曼树:

构造过程中,我们默认左路径用0表示,右路径用1表示,那么最终5个字符的哈夫曼编码为:A->11,B->10,C->00,D->011,E->010

 

6.8  B树、B+树和B*树

6.8.1  B树

背景:通常来说,数据库的索引会采用哈希表或者树结构来存储,这是因为树的查询效率高且可以保持有序。

矛盾:但是我们并不采用二叉搜索树来存储,是因为在进行索引查询时计算机需要多次进行IO操作,每次IO操作只能加载一个磁盘页(对应一个结点),而二叉搜索树中进行查询时最坏情况下需要进行和树的高度一样次数的IO操作,当结点海量时,二叉搜索树的高度极高,会大幅降低效率。

解决方案:因此,为了减少IO次数,我们把原本瘦高的树结构设计成矮胖的样式,提出了多路平衡搜索树(即B树),一棵m阶的B树具有如下特征:

  • 根结点至少有两个子女;
  • 每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m;
  • 每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m;
  • 所有的叶子结点都位于同一层;
  • 每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分界。

下面,我们举个例子来直观了解B树的查找过程:

假设我们要搜索5这个元素,第一次IO加载入【9】这个结点,然后在内存中将5和结点元素进行比较;第二次IO加载入【2 6】这个结点,同样在内存中进行比较定位;第三次IO加载入【3 5】这个结点,定位到5这个元素。

可以看出,当单一结点元素较多时,需要在内存中进行多次比较,但相比于磁盘IO的速度可以忽略不计,所以,只要树的高度足够低,IO次数够少,就可以提升查找性能。

 B树的插入和删除比较麻烦,但可以实现自平衡,具体可参考如下链接:https://www.jianshu.com/p/8b653423c586

6.8.2  B+树

B+树是B树的一种变体,有着比B树更高的查询性能。一棵m阶的B+树在B树基础上多出如下几个特征:

  • 有k个子树的中间节点包含有k个元素(B树中是k-1个元素),每个元素不保存数据,只用来索引,所有数据都保存在叶子节点;
  • 所有的叶子结点中包含了全部元素的信息,及指向含这些元素记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接;
  • 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

下面,我们举个例子来直观了解B+树:

我们可以看到,每个父亲结点的元素都会出现在子结点中,并且是子结点中最大/最小的元素;叶子结点包含了所有元素,并且每个叶子结点都有指向下一个叶子结点的指针,形成了一个有序链表。

注意:

  • 根结点的最大元素即为B+树的最大元素,以后无论插入删除多少元素,始终要保持最大元素在根结点中
  • 卫星数据是指索引元素指向的数据记录,B树中所有结点均带有卫星数据,B+树中只有叶子结点带有卫星数据,中间结点仅是索引。
B树——卫星数据

 

B+树——卫星数据

 

1、B+树的单行查询

B+树的单行查询与B树类似,区别在于:

  • B+树的中间结点没有卫星数据,所以同样大小的磁盘页可以容纳更多的结点元素,因此数据相同情况下,B+树更加矮胖,查询IO次数更少;
  • B+树的查询必须最终查找到叶子结点,而B树只要找到匹配元素即可,因此B树的查找性能并不稳定,B+树每一次查找都是稳定的。

2、B+树的范围查询

在B树中,我们如果要做范围查询,只能依靠中序遍历,假设我们要查找[3,11]的元素,首先我们自顶向下查到下限3,然后中序遍历到6、8、9、11,遍历才能结束。而在B+树中,我们只需要先查到下限3,然后在链表上做遍历即可:

综上所述,B+树相对B树的优势有三点:

  • IO次数更少
  • 查询性能更稳定
  • 范围查询更简便

至于B+树的插入和删除,则与B树类似。

6.8.3  B*树

B*树是B+树的变体,在B+树的基础上(所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针),B*树中中间结点再增加指向兄弟的指针;B*树定义了非叶子结点关键字个数至少为(2/3)*m,即块的最低使用率为2/3(代替B+树的1/2)。给出了一个简单实例,如下图所示:

特点:B*树中插入一个元素时,当所在结点满了,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了)。因此,B*树分配新结点的概率比B+树要低,空间使用率更高。

6.9  相关面试题

Q:知道树的前序和中序遍历结果(或者中序和后序遍历结果),如何复原二叉树?

A:前序/后序可知道根节点,中序可划分左右子树,根据这个原则即可复原二叉树。

Q:

A:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值