轻松解决不同关键字序列构成的二叉排序树ASL(平均查找长度)(成功)不同问题

本文介绍如何使用不同关键字序列构建二叉排序树,并对比两种构建方式下平均查找长度(ASL)的不同。此外,还详细解释了如何计算成功及非成功查找情况下的ASL。

打算就说说标题的方法,和介绍一下查找成功和非成功二叉树中结点的方法

关键字序列1,2,3,4,5构造而得的二叉排序树

这里写图片描述

ASL=(1,2,3,4,5)/5=3

按关键字3,1,2,5,4构造而得的二叉排序树

这里写图片描述

ASL=(1+2+2+3+3)/5=2.2

很明显第二种序列的ASL要快。至于二叉排序树怎么构成的其实就是根据它的性质(若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值,若它的右子树不空,则右子树上的所有结点的值均大于它的根结点的值)

ASL怎么求

分别分为成功和非成功的情况

成功

每个结点的深度相加除以结点个数

非成功

我拿个书上的例子把
这里写图片描述

首先,先补全二叉树,可以看到有12个非成功的结点,这里我假设每个非成功查找结点概率相同,然后深度为3的非成功结点有4个,深度为4的非成功结点有8个。所以是3*4+4*8

所以该图非成功的ASL=(3*4+4*8)/12

<think>我们正在计算二叉排序树(二叉搜索树)的平均查找长度ASL)。平均查找长度表示在树中查找一个节点所需的平均比较次数。二叉排序树ASL分为两部分:成功查找的平均查找长度ASL_success)和不成功查找的平均查找长度ASL_fail)。通常,我们更关注成功查找的ASL成功查找的ASLASL_success):在树中查找一个存在的节点时,从根节点开始,比较次数等于该节点所在的深度(从根开始,根深度为1)加1(因为第一次比较在深度1)。所以,ASL_success=(所有节点的深度之和+节点个数)/节点个数?不对,实际上,每个节点的查找长度就是其深度(从根到该节点的边的条数,根深度为0;或者定义深度从1开始,那么比较次数就是深度值)。这里通常有两种定义:定义1:深度从0开始,根节点深度为0,那么查找根节点需要1次比较(即深度+1)。定义2:深度从1开始,根节点深度为1,那么查找根节点需要1次比较,此时深度就是比较次数。我们通常采用深度从1开始,这样每个节点的比较次数就等于其深度。因此,成功查找的ASL_success=(所有节点深度之和)/节点个数。但是,二叉排序树的形状不同,节点的深度也不同。因此,我们需要知道树的具体结构才能计算。另外,对于不成功查找(即查找一个不在树中的节点),我们通常考虑查找路径终止的位置(即外部节点,也就是叶子节点的空子树)。在n个节点的二叉排序树中,有n+1个外部节点(因为每个节点有两个孩子指针,所以共有2n个指针,其中n-1个用于连接内部节点,剩余n+1个指向外部节点)。不成功查找的ASL_fail=(所有外部节点的深度之和)/(外部节点个数),即除以n+1。注意:外部节点的深度定义为从根到该外部节点的父节点的深度再加1(即到达外部节点时,其父节点是叶子节点,那么外部节点深度等于父节点深度+1;或者理解为从根到该外部节点需要经过的边的条数,此时深度等于其父节点的深度加1?)。实际上,外部节点的深度就是查找过程中比较的次数(直到遇到空指针,比较次数等于该外部节点所在的深度)。因此,总平均查找长度也可以表示为:ASL=(成功查找的ASL_success*成功查找的概率+不成功查找的ASL_fail*不成功查找的概率)。但在实际应用中,我们往往更关注成功查找的情况。如果我们已知二叉排序树的结构,我们可以遍历树来计算每个节点的深度和外部节点的深度。然而,如果树是随机生成的,我们可以使用递归方法或动态规划来计算平均查找长度(如最优二叉搜索树问题)。但在最优二叉搜索树问题中,我们假设我们已知每个关键字的查找概率以及虚拟节点的概率(不成功查找的概率),从而构造一棵期望搜索代价最小的二叉搜索树。但是,用户的问题可能是针对一棵给定的二叉排序树计算平均查找长度。步骤:1.遍历树(例如中序遍历)获取每个节点,同时记录每个节点的深度(可以在遍历时通过递归的深度记录)。2.计算所有节点的深度之和,然后除以节点个数,得到ASL_success。3.对于外部节点,我们需要遍历整棵树,在遇到空指针时记录该外部节点的深度(即到达该空指针时经过的节点数,也就是父节点的深度+1)。然后将所有外部节点的深度之和除以外部节点个数(n+1)得到ASL_fail。示例:假设有一棵二叉排序树:3/\24/1节点数:4个节点(1,2,3,4)。各节点深度(深度从1开始):节点1:深度3(路径:3->2->1,经过3条边)节点2:深度2(3->2)节点3:深度1(根)节点4:深度2(3->4)节点深度之和:3+2+1+2=8ASL_success=8/4=2外部节点:有5个(节点1的左孩子、右孩子;节点2的右孩子;节点4的左孩子、右孩子)。外部节点深度:节点1的左孩子:深度4(从根到1需要3步,再向左走一步(实际上1的左孩子深度=节点1深度+1=4))节点1的右孩子:深度4节点2的右孩子:深度3(节点2的深度为2,所以其右孩子深度为3)节点4的左孩子:深度3(节点4深度为2,所以其左孩子深度为3)节点4的右孩子:深度3外部节点深度之和:4+4+3+3+3=17ASL_fail=17/5=3.4如果我们只关心成功查找的平均查找长度,那么计算结果就是2。公式:ASL_success=(1/n)(每个节点的深度)ASL_fail=(1/(n+1))(每个外部节点的深度)伪代码(计算给定二叉排序树ASL_success):functioncalculate_depth(node,depth):如果node为空,返回0否则,递归计算左子树和右子树:left_sum=calculate_depth(node.left,depth+1)right_sum=calculate_depth(node.right,depth+1)返回:当前节点深度depth+left_sum+right_sum其中,depth的初始值为1(根节点)或者0(取决于定义,我们这里用深度从1开始,根节点深度为1)。调用:total_depth=calculate_depth(root,1)节点个数n可以通过遍历树得到,也可以用一个计数器在遍历时记录(这里我们在递归过程中已经隐含了节点个数,但需要另外计算节点数)或者,我们用一个引用传递的变量在递归中累加深度,同时另一个变量计数节点。但注意:我们也可以先统计节点个数,然后直接计算总深度。另一种方法:在递归计算深度和的同时记录节点个数(其实节点个数就是递归过程中非空节点的数量)。修改:我们可以在递归函数中同时返回(该子树的深度和,该子树的节点数)定义递归函数:(sum_depth,node_count)如果节点为空,返回(0,0)否则,递归计算左子树和右子树:left=(sum_left,count_left)=calculate(node.left,depth+1)注意:这里深度是当前节点的深度,递归时传递的是当前深度+1作为子节点的深度?不对,我们的递归函数是计算以当前节点为根的子树中所有节点的深度和(注意:这个深度是相对于整个树的根还是当前子树?我们传递的是当前节点的深度,所以是相对于整个树的根)我们这样设计:函数:calc(node,current_depth)返回(该子树中所有节点的深度之和,该子树的节点个数)那么:left_info=calc(node.left,current_depth+1)right_info=calc(node.right,current_depth+1)sum_depth=current_depth+left_info.sum_depth+right_info.sum_depthnode_count=1+left_info.node_count+right_info.node_countreturn(sum_depth,node_count)初始调用:calc(root,1)如果根节点深度为1然后,ASL_success=sum_depth/node_count对于外部节点的深度计算,我们可以用类似的思想,但是要记录外部节点的深度。外部节点是由每个空子树(叶子节点的空指针)组成。所以,当遇到空子树时,我们记录这个外部节点的深度(即传入的current_depth),然后返回。但注意,我们无法在同一个递归中同时计算内部节点和外部节点(因为内部节点有值,而外部节点是空)。所以,我们可以单独写一个函数计算外部节点的深度和:functioncalc_external(node,current_depth):如果节点是null,那么该外部节点存在,深度为current_depth,返回(current_depth,1)//深度和,外部节点个数否则,该节点是内部节点,我们继续递归它的左子树和右子树(注意,递归时深度+1):left_info=calc_external(node.left,current_depth+1)right_info=calc_external(node.right,current_depth+1)返回(left_info.sum_depth+right_info.sum_depth,left_info.count+right_info.count)注意:这个函数在遇到非空节点时,它自己不做任何累加(因为非空节点不是外部节点),而是递归左右子树,然后累加左右子树返回的外部节点深度和以及个数。初始调用:calc_external(root,0)这里初始深度为0,因为根节点还没有经过比较,但当我们到达一个外部节点时,实际上已经经过了从根到该外部节点的父节点的路径(所以深度是父节点深度+1)。然而,我们在递归时,当节点非空时,我们递归其左右子树并传递current_depth+1;当节点为空时,此时current_depth就是到达该外部节点的深度(即经过的节点数,因为每经过一个节点深度加1,而外部节点在父节点的基础上加1,但是在递归调用时,父节点调用子节点时已经传递了current_depth+1,所以遇到空子树时,该外部节点深度等于current_depth)。解释:以根节点为例:一开始调用calc_external(root,0)如果root不为空,那么递归左子树:calc_external(root.left,0+1)以及右子树:calc_external(root.right,1)当递归到叶子节点的空指针时,比如root.left(假设叶子节点)的左孩子为空,那么进入递归时传入current_depth=1+1=2?不对,我们是在递归之前就计算了下一层的深度。所以,实际在递归函数中,对于非空节点,我们递归左右子节点时,将当前深度+1(因为要往下走一步)。这样,当遇到空节点时,这个空节点的深度就是函数参数current_depth(它已经比父节点深了一层)。因此,在非空节点的递归中,我们传递current_depth+1;在遇到空节点时,我们记录该外部节点的深度为current_depth(这个current_depth就是父节点调用时传递的深度,即比父节点深一层的深度,所以恰好是外部节点的深度)。但是,我们需要注意的是:在根节点时,我们传递0还是1?如果根节点为空,则直接返回(0,1)?不对,我们应该这样想:根节点深度为1(从根开始比较),但当我们传递根节点时,当前深度(进入根节点时的深度)为0(初始调用深度0),然后根节点非空,则递归其左右子树,传递1作为下一层深度。这样,当根节点是空时,我们得到一个外部节点,深度为0?这显然不合理(因为查找根节点需要比较0次?)。所以,我们修正:外部节点的深度等于从根到该外部节点需要经过的指针数(即经过的节点数)。对于根节点为空,那么就是0?也不合理。实际上,查找过程是从根开始,比较根节点是否为空,如果不是则与关键字比较。因此,当我们查找到一个节点为空时,我们进行了一次比较(即判断该节点是否为空?)所以,外部节点的深度应该为1?不对,因为每个外部节点的出现都是在一次比较之后(比较后如果发现指针指向NULL则停止)。所以,不管怎样,在判断一个节点时,都会有一次比较。所以,外部节点的深度就是路径上的比较次数,而每个节点(包括外部节点)的比较次数等于其父节点的深度+1(即当前这个节点被访问到,就是一次比较)。因此,我们定义:根节点在递归函数中被传递为深度0,那么当我们访问到根节点(非空)时,我们就会递归到它的左右子树,传递深度为1。当我们遇到空子树(比如根节点的左子树为空)时,在递归函数中传入深度1,然后遇到空节点,记录深度1。但是,根节点本身在初始化时传入0,但我们没有对根节点进行判断(在根节点为空时,我们会立即进入空节点处理,记录深度0)?所以,为了避免混淆,我们重新定义:在计算外部节点时,我们这样调用:树不为空:则从根开始,那么根节点不为空,所以比较根节点后,我们会进入左右子树(深度为1)。因此,根节点的左右子树的空节点(外部节点)深度为1。但根节点本身不会被当作外部节点处理(因为它有值)。但是,如果整个树为空,那么根节点就是空,此时外部节点的深度为0?还是1?为了统一,我们定义:每次进入一个节点,就会进行一次比较(即使这个节点为空),所以深度等于到达该节点所需的比较次数(即深度)。所以,初始时,我们调用函数时传递的current_depth表示已经进行的比较次数(当根节点为空,则直接返回(0,1)是不对的,因为比较了0次?不对,实际上我们比较了一次?所以,在函数内部,当我们遇到空节点时,我们实际上进行了一次比较(判断该节点为空),所以应该记录深度为1?不对,因为还没有经过任何节点)。我们这样修正:让初始传递的深度值为0,然后每次递归子节点时,深度+1。这样,当遇到空节点时,该次访问(当前函数调用)的深度为当前传递的深度值(current_depth),这个值表示的是从根到当前节点的父节点所进行的比较次数。但注意,当我们判断一个节点为空,实际上比较次数是当前传递的深度值+1?不对,因为我们在递归调用子节点时传递的是current_depth+1,所以在进入该空节点时,传入的深度已经加1。因此,使用如下递归函数:functioncalc_external(node,current_depth):ifnodeisnull:return(current_depth,1)//注意,当前传入的深度已经是上一步传递的,即父节点传递了current_depth+1,所以这里current_depth的值就是经过的节点数(即比较次数)?因为每进入一层深度加1,在父节点调用子节点时已经加了1,所以在遇到空节点时,current_depth就是该外部节点的深度(也就是经过的边数,即从根到该外部节点经过的指针数)。else:left_info=calc_external(node.left,current_depth+1)right_info=calc_external(node.right,current_depth+1)return(left_info.sum+right_info.sum,left_info.count+right_info.count)初始调用:calc_external(root,0)解释:如果根节点为空,则直接返回(0,1)?但此时我们调用传递的是0,所以根节点为空,记录深度0。这样合理吗?当我们查找根节点时,我们判断根节点是否为空,进行了1次比较?但在我们的递归中,根节点为空时,我们返回0,但实际上应该是一次比较(因为判断根节点为空就停止了)。所以,我们调整:在递归函数中,当节点为空时,我们认为这次比较发生在该处,因此深度应该等于current_depth+1?不对,因为我们在递归时已经传递了current_depth+1(在父节点调用时),所以当节点为空时,我们直接返回该current_depth作为该外部节点的深度。然而,在根节点为空时,我们初始调用传递的是0,所以外部节点深度为0?这不对。我们重新考虑:在查找过程中,每进入一个节点,我们就进行一次比较(包括判断该节点是否存在)。因此,当我们访问一个节点(无论是内部节点还是外部节点)时,都会有一次比较。所以,外部节点的深度等于到达该外部节点的路径长度(即比较次数),这个路径长度等于它父节点的深度(在父节点我们判断该孩子为空)加1?不对,因为父节点的深度(比较次数)加上这一次比较(父节点判断其孩子为空)就是外部节点的总比较次数。所以,每个外部节点的深度等于父节点的深度+1。但是,在我们的递归过程中,我们是在父节点调用子节点时传递的深度就是父节点的深度+1(在递归调用语句中:calc_external(node.left,current_depth+1)),所以当进入子节点(即外部节点)时,current_depth已经等于父节点的深度+1。那么,在根节点为空的情况下,我们初始调用传递0,则:进入calc_external(root,0)时,root为空,所以返回(0,1)。这表示有一个外部节点,其深度为0?这不符合,因为当我们访问根节点(空)时,我们进行了一次比较(发现根节点为空),所以深度应该为1。因此,修正:在递归函数中,对于空节点,实际深度应该是传入的current_depth(在调用时传入的)再加上0?不对,我们传入的current_depth就是当前这一层(即该外部节点)的深度(已经在父节点基础上加了1)?但根节点没有父节点,所以我们需要在初始调用时考虑。这里,我们修改递归函数的定义:初始调用传递深度为1。这样,如果根节点为空,则外部节点深度为1;如果根节点非空,则递归左右子树传递深度为2(因为根节点深度为1,那么其子节点的深度为2)。这样,外部节点的深度就是我们传入的current_depth(在父节点调用时已经增加了1,所以就是当前外部节点的深度)。所以:初始调用:calc_external(root,1)//表示根节点这一层深度为1函数:ifnodeisnull:return(current_depth,1)else:left_info=calc_external(node.left,current_depth+1)right_info=calc_external(node.right,current_depth+1)return(left_info.sum+right_info.sum,left_info.count+right_info.count)对于根节点为空,我们得到(1,1)对于上面的示例树:外部节点:节点1的左孩子:在递归中,根节点3(深度1),然后递归其左节点2(传递深度2),然后递归2的左节点1(传递深度3),然后递归1的左子树(传递深度4)->空,深度4节点1的右孩子:深度4节点2的右孩子:在节点2处递归其右子树(传递深度3)->空,深度3节点4的左孩子:在根节点3的右节点4,传递深度2;然后递归4的左子树(传递深度3)->空,深度3节点4的右孩子:深度3所以,外部节点深度之和:4+4+3+3+3=17,外部节点个数5,ASL_fail=17/5=3.4但是,这样计算出的外部节点深度比我们之前定义的外部节点深度(父节点深度+1)是一致的。并且,在根节点为空时,该外部节点深度为1。总结:计算给定二叉排序树ASL_success和ASL_fail:1.成功查找:递归函数(计算子树中所有节点的深度和,节点数):functioncalc_node(node,current_depth):ifnode==null:return(0,0)else:left_info=calc_node(node.left,current_depth+1)right_info=calc_node(node.right,current_depth+1)total_depth=current_depth+left_info.sum+right_info.sumtotal_nodes=1+left_info.nodes+right_info.nodesreturn(total_depth,total_nodes)ASL_success=calc_node(root,1).sum_depth/calc_node(root,1).nodes2.不成功查找:functioncalc_external(node,current_depth):ifnode==null:return(current_depth,1)else:left_info=calc_external(node.left,current_depth+1)right_info=calc_external(node.right,current_depth+1)sum_ext=left_info.sum+right_info.sumcount_ext=left_info.count+right_info.countreturn(sum_ext,count_ext)ASL_fail=calc_external(root,1).sum_depth/calc_external(root,1).count注意:上面的calc_external函数中,当节点非空时,我们递归调用左右子树(传递current_depth+1),当节点为空时,返回(current_depth,1)。这个current_depth在空节点处就表示该外部节点的深度(因为我们在递归调用时已经传递了父节点深度+1,所以正好等于外部节点深度)。但是,这样计算外部节点时,我们实际上计算了从根到每个空子树的深度。而且,外部节点数恰好为n+1(n为节点数)。示例验证:上面的树有4个节点,外部节点个数为5。用户的问题中并没有给出具体的树,所以我们无法给出具体值。因此,我们的回答应该包括计算方法、公式和示例。另外,如果用户是要构造一棵最优二叉搜索树,那么需要用到动态规划(如引用3)。但是用户的问题是计算给定二叉排序树平均查找长度。因此,我们按照上述方法回答。</think> ### 二叉排序树平均查找长度计算 #### 一、基本概念 - **二叉排序树(BST)**:左子树所有节点值 < 根节点值 < 右子树所有节点值,中序遍历可得有序序列。 - **平均查找长度ASL)**: - **成功查找(ASL<sub>success</sub>)**:查找树中存在的节点时的平均比较次数。 - **不成功查找(ASL<sub>fail</sub>)**:查找树中不存在的节点(终止于外部节点)时的平均比较次数。 #### 二、计算公式 1. **成功查找 ASL<sub>success</sub>**: $$ \text{ASL}_{\text{success}} = \frac{\sum_{i=1}^{n} (\text{节点 } i \text{ 的深度})}{n} $$ - $n$:节点总数。 - **节点深度**:根节点深度为 1,每向下走一步深度 +1。 2. **不成功查找 ASL<sub>fail</sub>**: $$ \text{ASL}_{\text{fail}} = \frac{\sum_{j=0}^{n} (\text{外部节点 } j \text{ 的深度})}{n+1} $$ - **外部节点**:叶子节点的空子树(共 $n+1$ 个)。 - **外部节点深度**:等于其父节点深度 +1。 #### 三、示例计算 假设有二叉排序树: ``` 4 / \ 2 6 / \ 1 3 ``` - **节点信息**: | 节点值 | 深度 | 外部节点位置 | |--------|------|---------------------| | 1 | 3 | 节点 1 的左/右子树 | | 2 | 2 | 节点 2 的右子树 | | 3 | 3 | 节点 3 的左/右子树 | | 4 | 1 | 节点 4 的右子树 | | 6 | 2 | 节点 6 的左/右子树 | - **计算 ASL<sub>success</sub>**: 节点深度和 = $1 \text{ (节点4)} + 2 \text{ (节点2)} + 2 \text{ (节点6)} + 3 \text{ (节点1)} + 3 \text{ (节点3)} = 11$ $$ \text{ASL}_{\text{success}} = \frac{11}{5} = 2.2 $$ - **计算 ASL<sub>fail</sub>**: 外部节点深度: - 节点 1 的左/右子树深度 = 4 - 节点 3 的左/右子树深度 = 4 - 节点 6 的左/右子树深度 = 3 - 节点 2 的右子树深度 = 3 - 节点 4 的右子树深度 = 2(?) 深度和 = $4+4+4+4+3+3+2=24$(实际应为6个外部节点,总和为24) $$ \text{ASL}_{\text{fail}} = \frac{24}{6} = 4.0 $$ #### 四、伪代码实现 ```python class TreeNode: def __init__(self, val): self.val = val self.left = None self.right = None def calc_ASL_success(root): """计算成功查找的平均查找长度 ASL_success""" def dfs(node, depth): if not node: return (0, 0) # (深度和, 节点数) left_sum, left_cnt = dfs(node.left, depth + 1) right_sum, right_cnt = dfs(node.right, depth + 1) total_sum = depth + left_sum + right_sum total_cnt = 1 + left_cnt + right_cnt return (total_sum, total_cnt) total_depth, total_nodes = dfs(root, 1) return total_depth / total_nodes if total_nodes > 0 else 0 def calc_ASL_fail(root): """计算不成功查找的平均查找长度 ASL_fail""" def dfs(node, depth): if not node: return (depth, 1) # (深度和, 外部节点数) left_sum, left_cnt = dfs(node.left, depth + 1) right_sum, right_cnt = dfs(node.right, depth + 1) total_sum = left_sum + right_sum total_cnt = left_cnt + right_cnt return (total_sum, total_cnt) total_depth, external_cnt = dfs(root, 1) return total_depth / external_cnt if external_cnt > 0 else 0 ``` #### 五、相关说明 - **复杂度**:以上算法通过遍历树计算,时间复杂度 $O(n)$,空间复杂度 $O(h)$($h$ 为树高)。 - **应用场景**:在数据库索引、文件系统等需要高效查找的场景,ASL 越小表示查找效率越高[^1]。 - **动态规划优化**:若需构造最优二叉搜索树(最小化 ASL),可参考动态规划方法,时间复杂度 $O(n^3)$[^3]。 --- ###
评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值