6-12 二叉搜索树的操作集(30 point(s))

本文介绍了一种二叉搜索树的基本操作实现方法,包括插入、删除、查找、寻找最小值和最大值等功能。通过递归的方式实现了这些操作,并提供了完整的代码示例。
6-12 二叉搜索树的操作集(30 point(s))

本题要求实现给定二叉搜索树的5种常用操作。

函数接口定义:

BinTree Insert( BinTree BST, ElementType X );
BinTree Delete( BinTree BST, ElementType X );
Position Find( BinTree BST, ElementType X );
Position FindMin( BinTree BST );
Position FindMax( BinTree BST );

其中BinTree结构定义如下:

typedef struct TNode *Position;
typedef Position BinTree;
struct TNode{
    ElementType Data;
    BinTree Left;
    BinTree Right;
};
  • 函数InsertX插入二叉搜索树BST并返回结果树的根结点指针;
  • 函数DeleteX从二叉搜索树BST中删除,并返回结果树的根结点指针;如果X不在树中,则打印一行Not Found并返回原树的根结点指针;
  • 函数Find在二叉搜索树BST中找到X,返回该结点的指针;如果找不到则返回空指针;
  • 函数FindMin返回二叉搜索树BST中最小元结点的指针;
  • 函数FindMax返回二叉搜索树BST中最大元结点的指针。

裁判测试程序样例:

#include <stdio.h>
#include <stdlib.h>

typedef int ElementType;
typedef struct TNode *Position;
typedef Position BinTree;
struct TNode{
    ElementType Data;
    BinTree Left;
    BinTree Right;
};

void PreorderTraversal( BinTree BT ); /* 先序遍历,由裁判实现,细节不表 */
void InorderTraversal( BinTree BT );  /* 中序遍历,由裁判实现,细节不表 */

BinTree Insert( BinTree BST, ElementType X );
BinTree Delete( BinTree BST, ElementType X );
Position Find( BinTree BST, ElementType X );
Position FindMin( BinTree BST );
Position FindMax( BinTree BST );

int main()
{
    BinTree BST, MinP, MaxP, Tmp;
    ElementType X;
    int N, i;

    BST = NULL;
    scanf("%d", &N);
    for ( i=0; i<N; i++ ) {
        scanf("%d", &X);
        BST = Insert(BST, X);
    }
    printf("Preorder:"); PreorderTraversal(BST); printf("\n");
    MinP = FindMin(BST);
    MaxP = FindMax(BST);
    scanf("%d", &N);
    for( i=0; i<N; i++ ) {
        scanf("%d", &X);
        Tmp = Find(BST, X);
        if (Tmp == NULL) printf("%d is not found\n", X);
        else {
            printf("%d is found\n", Tmp->Data);
            if (Tmp==MinP) printf("%d is the smallest key\n", Tmp->Data);
            if (Tmp==MaxP) printf("%d is the largest key\n", Tmp->Data);
        }
    }
    scanf("%d", &N);
    for( i=0; i<N; i++ ) {
        scanf("%d", &X);
        BST = Delete(BST, X);
    }
    printf("Inorder:"); InorderTraversal(BST); printf("\n");

    return 0;
}
/* 你的代码将被嵌在这里 */

输入样例:

10
5 8 6 2 4 1 0 10 9 7
5
6 3 10 0 5
5
5 7 0 10 3

输出样例:

Preorder: 5 2 1 0 4 8 6 7 10 9
6 is found
3 is not found
10 is found
10 is the largest key
0 is found
0 is the smallest key
5 is found
Not Found
Inorder: 1 2 4 6 8 9

code:
BinTree Insert( BinTree BST, ElementType X ){
    //如果是一个空节点
    if(!BST){
        BST = (BinTree)malloc(sizeof(struct TNode));//既然为空所以要生成一个
        BST->Data = X;
        BST->Left = NULL;
        BST->Right = NULL;
    }
    else{//一般情况
        if(X < BST->Data){//插入值小于节点,应该往左子树中找位置
            BST->Left = Insert(BST->Left,X);//递归插入左子树
        }
        else if(X > BST->Data){//插入值大于节点,应该往右子树中找
            BST->Right = Insert(BST->Right,X);//递归插入右子树
        }
        //如果相等说明X已经存在,什么也不做
    }
    return BST;
}
Position Find( BinTree BST, ElementType X ){
    while(BST){//直接循环查找,类似链表
        if(X < BST->Data){
            BST = BST->Left;//小于节点,找左子树
        }
        else if(X > BST->Data){//大于节点,找右子树
            BST = BST->Right;
        }
        else{//相等则找到
            return BST;
        }
    }
    return NULL;
}
Position FindMin( BinTree BST ){
    if(!BST){
        return NULL;
    }
    else if(!BST->Left)
        return BST;
    else return FindMin(BST->Left);
}
Position FindMax( BinTree BST ){
    if(!BST)return NULL;
    else if(!BST->Right)return BST;
    else return FindMax(BST->Right);
}
BinTree Delete( BinTree BST, ElementType X ){
    Position temp;
    if(!BST){
        printf("Not Found\n");//如果最终树为空,说明没有
    }
    else{//这里类似于插入重点在于找到后怎么办
        if(X < BST->Data){
            BST->Left = Delete(BST->Left,X);//从左子树递归删除
        }
        else if(X > BST->Data){
            BST->Right = Delete(BST->Right,X);//从右子树递归删除
        }
        else{//当前BST就是要删除的节点
              if(BST->Left && BST->Right){//要被删除的节点有左右两个孩子,就从右子树中找最小的数填充删除的节点
                temp = FindMin(BST->Right);//找最小
                BST->Data = temp->Data;//填充删除的节点
                BST->Right = Delete(BST->Right,temp->Data);//删除拿来填充的那个节点
              }
              else{//只有一个子节点
                temp = BST;
                if(!BST->Left){//只有右节点
                    BST = BST->Right;//直接赋值就可以
                }
                else if(!BST->Right){//只有左节点
                    BST = BST->Left;//直接赋值就可以
                }
                free(temp);//如果啥也没有直接删除就可以,当然上面两种情况赋值后也要删除
              }
        }
    }
    return BST;
}

 
<think>我们讨论的是KD树在KNN算法中的应用,特别是在scikit-learn中的实现和详细推导过程。下面我将步骤进行讲解,包括KD树的构建、搜索过程、在scikit-learn中的使用方式以及背后的数学原理。###1.KD树的基本概念KD树(k-dimensionaltree)是一种用于组织k维空间中点的数据结构,用于范围搜索和最近邻搜索。它是一种二叉搜索树,每个节点代表一个超矩形区域,通过交替选择不同维度进行划。**关键特性**:-每个节点表示一个数据点和一个划的维度-左子树包含在当前划维度上小于该点的点-右子树包含在当前划维度上大于或等于该点的点###2.KD树的构建算法给定一个k维数据集$X$,包含$n$个点,每个点是一个k维向量:$x_i=(x_i^{(0)},x_i^{(1)},\dots,x_i^{(k-1)})^T$####构建步骤(递归过程):1.**选择划维度**:通常有两种策略:-轮流选择:第j层选择维度$l=j\modk$(最常用)-最大方差维度:选择方差最大的维度进行划(计算更复杂,但可能产生更平衡的树)2.**选择切点**:在选定维度上,选择中位数作为切点(此时构建的树是平衡的,树高$O(\logn)$)。也可以选择其他点,但中位数可以保证树的平衡。3.**递归构建左右子树**:将数据划为两个子集,别构建左子树和右子树。####数学描述对于节点$N$,其对应数据集$S$,当前划维度$d$:-点$p$:$p=\text{median}\{x^{(d)}|x\inS\}$-左子树:$S_{\text{left}}=\{x\inS|x^{(d)}< p^{(d)}\}$-右子树:$S_{\text{right}}=\{x\inS|x^{(d)}\geqp^{(d)}\}$####构建的终止条件当节点包含的数据点个数不超过预设的`leaf_size`参数时,该节点成为叶子节点。**时间复杂度析**:-找中位数:平均$O(n)$,使用快速选择算法-总时间复杂度:$O(n\logn)$###3.KD树的最近邻搜索(KNN)KD树的最近邻搜索是一个递归过程,使用了剪枝策略(pruning)来减少搜索的节点数。####基本步骤(以单个查询点$q$为例):1.**向下递归**:从根节点开始,根据当前节点的划维度比较,递归进入左子树或右子树(类似二叉搜索树查找),直到叶子节点。记录当前最近邻点(candidate)和最近距离(min_dist)。2.**回溯路径**:递归向上回溯,检查每个节点:-a.计算查询点$q$与当前节点代表的点的距离$d$,更新最近邻-b.**剪枝条件**:检查当前节点的另一个子树是否可能存在更近的点。判断方法是:以$q$为中心,当前最小距离$r$为半径的超球面是否与当前节点的割超平面相交。-若相交(即$|q^{(d)}-N_{\text{split}}|< r$),则需要搜索另一个子树-否则,跳过该子树####剪枝的几何意义对于节点$N$的划维度$d$,割点为$s$。则割超平面为$x^{(d)}=s$。在回溯过程中,若当前最近邻距离为$r$,只有当$q^{(d)}$到割超平面的距离小于$r$时,超球面才可能与另一个子树区域相交,因此需要搜索另一个子树。否则,另一个子树不可能有更近的点(因为整个子树都在超球面外)。####K近邻搜索(K>1)与最近邻类似,但是需要维护一个优先队列(按距离从大到小排序)来保存当前找到的K个最近邻点。剪枝条件改为:与当前第K远的点的距离$r_k$进行比较。###4.KD树在scikit-learn中的实现在scikit-learn中,KDTree的实现在`sklearn.neighbors.KDTree`类中。其核心方法包括:-`__init__`:构建KD树-`query`:查询K近邻####关键参数-`leaf_size`:控制叶节点包含的最大样本数(影响树的深度和搜索效率)-`metric`:距离度量(默认欧氏距离$L_2$)####构建树的代码框架以下是简化版的构建流程(展示关键逻辑):```pythonclassKDTree:def__init__(self,X,leaf_size=40,metric='euclidean'):self.leaf_size=leaf_sizeself.metric=metricself.root=self._build_tree(X,depth=0)def_build_tree(self,X,depth):n_samples,n_features=X.shapeifn_samples<=self.leaf_size:returnNone#标记为叶子节点,具体存储数据#选择划维度:按层轮流选择axis=depth%n_features#找到该维度的中位数median=np.median(X[:,axis])median_idx=np.where(X[:,axis]==median)[0][0]#实际实现通过快速选择避免全排序#划数据left_mask=X[:,axis]< medianright_mask=~left_maskleft_data=X[left_mask]right_data=X[right_mask]#构建节点和子树node={'axis':axis,'position':X[median_idx],'left':self._build_tree(left_data,depth+1),'right':self._build_tree(right_data,depth+1)}returnnode```####查询过程(回溯+剪枝)查询伪代码:```pythondefquery(self,q,k=1):#初始化优先级队列(大顶堆,存距离的负值)neighbors=[]self._query_recursive(self.root,q,k,neighbors,depth=0)#返回最近的k个邻居returnsorted(neighbors,key=lambdax:x[0])[:k]def_query_recursive(self,node,q,k,neighbors,depth):ifnodeisNone:#叶子节点,处理其存储的数据点#这里遍历该叶子节点中的所有点,更新最近邻#(代码略)returnaxis=node['axis']#根据划维度进入左子树或右子树ifq[axis]< node['position'][axis]:go_first=node['left']go_second=node['right']else:go_first=node['right']go_second=node['left']#递归进入第一个子树self._query_recursive(go_first,q,k,neighbors,depth+1)#检查当前节点dist=self.distance(q,node['position'])#更新邻居列表(维护大小为k的堆)#(具体更新代码略)#计算查询点到当前割平面的距离plane_dist=np.abs(q[axis]-node['position'][axis])#如果当前第k近的距离(或者当前最远的距离)大于割面距离,则另一个子树可能存在更近的点ifkisNoneorlen(neighbors)< kor(neighborsanddist_to_plane< neighbors[-1][0]):self._query_recursive(go_second,q,k,neighbors,depth+1)```###5.KD树搜索的时间复杂度-**构建**:$O(n\logn)$-**查询**:-最坏情况$O(n)$(当数据布使得剪枝失效时)-平均情况$O(\logn)$(适用于低维空间)-高维情况下效率可能退化到接近$O(n)$(维度灾难)###6.在KNN类中的应用在scikit-learn的`KNeighborsClassifier`中,当设置`algorithm='kd_tree'`时,使用KDTree来加速最近邻搜索。训练时构建KDTree索引,预测时对每个测试样本使用该索引快速找到最近邻。####示例代码```pythonfromsklearn.neighborsimportKNeighborsClassifierfromsklearn.datasetsimportload_irisfromsklearn.model_selectionimporttrain_test_splitdata=load_iris()X,y=data.data,data.targetX_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.3)#使用KDTree的KNN类器knn=KNeighborsClassifier(n_neighbors=5,algorithm='kd_tree',leaf_size=30)knn.fit(X_train,y_train)score=knn.score(X_test,y_test)print(f"Accuracy:{score:.4f}")```###7.数学推导:KD树剪枝的正确性为什么剪枝条件成立?设当前最近邻距离为$r$,查询点为$q$,待检查节点$N$的割维度$d$,割点$s$。我们需要证明:如果$|q^{(d)}-s|\geqr$,那么另一侧的子树中不可能存在比当前最近邻更近的点。**证明**:设割点$s$将空间为两个半空间:$x^{(d)}< s$和$x^{(d)}\geqs$。假设$q$在左半空间(即$q^{(d)}<s$),且$|q^{(d)}-s|\geqr$。那么,对于右子树中的任意一点$p$,都有$p^{(d)}\geqs$。因此,$|p^{(d)}-q^{(d)}|\geqs-q^{(d)}\geqr$。由勾股定理,$q$与$p$的欧氏距离:$$d(q,p)=\sqrt{\sum_{i=0}^{k-1}(q^{(i)}-p^{(i)})^2}\geq|p^{(d)}-q^{(d)}|\geqr$$所以,$p$不可能比当前最近邻更近。因此,剪枝操作是安全的。###8.总结与注意事项-**适用维度**:KD树在处理低维数据(例如维度<20)时效率很高,但高维时退化为线性扫描(称为“维度灾难”)。此时可考虑替代方法如BallTree或LSH(局部敏感哈希)。-**数据量影响**:对于样本量$n$,当$n$较大(如$n>1000$)时,KDTree比暴力搜索快很多。-**参数`leaf_size`**:需要权衡。增大`leaf_size`会减少树的深度,减少递归开销,但可能增加回溯时的距离计算次数。§§相关问题§§1.为什么在高维空间中KD树搜索效率会退化?如何改进高维数据的最近邻搜索?2.比较KD树和Ball树在结构、构建和查询上的区别,以及在什么情况下选择哪种树?3.如何通过选择不同的割策略来优化KD树的平衡性?4.KD树在范围搜索(半径搜索)中的应用和实现?5.KD树在非欧几里得距离度量(如曼哈顿距离)下的适用性?</think> ### KD树在KNN算法中的详细推导与scikit-learn实现 #### 一、KD树的数学原理与构建过程 KD树是一种二叉搜索树,通过递归割k维数据空间实现高效检索。给定n个k维数据点 $\{\mathbf{x}_i\}_{i=1}^n$,其中 $\mathbf{x}_i = (x_{i1}, x_{i2}, \dots, x_{ik})$: 1. **划维度选择**: 在深度为$d$的节点上,选择割维度 $l = d \mod k$(维度轮换策略)。实践中可选方差最大的维度: $$l = \arg\max_{j} \text{Var}(X^{(j)})$$ $X^{(j)}$ 表示第$j$维特征向量。 2. **切点选择**: 在维度$l$上取中位数 $\text{median}(X^{(l)})$,满足: $$\left|\{ \mathbf{x}_i | x_{il} < \text{median} \}\right| \approx \left|\{ \mathbf{x}_i | x_{il} \geq \text{median} \}\right|$$ 3. **递归构建**: 割平面为 $x_l = \text{median}$,左右子树别处理对应子集: $$\begin{cases} \text{左子树:} \{\mathbf{x}_i | x_{il} < \text{median}\} \\ \text{右子树:} \{\mathbf{x}_i | x_{il} \geq \text{median}\} \end{cases}$$ 4. **终止条件**: 当节点样本数 $\leq$ `leaf_size` 时停止递归(控制树深与精度平衡)。 --- #### 二、最近邻搜索的剪枝机制 给定查询点 $\mathbf{q}$,搜索最邻近点 $\mathbf{x}^*$ 的核心思想是**空间距离剪枝**: 1. **向下递归**: 从根节点开始,按当前节点割维度$l$比较 $q_l$ 与节点值,选择支路径,直到叶节点。初始暂存候选点 $\mathbf{x}_c$ 和距离 $d_c = \|\mathbf{q} - \mathbf{x}_c\|_2$ 2. **回溯剪枝**: 计算查询点到割超平面的距离: $$\delta = |q_l - \text{node.split\_value}|$$ - 若 $\delta \geq d_c$ → 另一侧子树无更优点(超球与割面不相交) - 若 $\delta < d_c$ → 需搜索另一子树 几何解释: <div align=center> <img src="https://ai-studio-static-online.cdn.bcebos.com/5d8f07c01f4a476e8a4f6dc1ebf3cdd15f7b5f48bd1c4a50a5ffd4a3f4c8bf11" width="50%"> </div> --- #### 三、KD树在scikit-learn中的实现 在scikit-learn中,KDTree的核心实现位于 `sklearn/neighbors/_kd_tree.pyx`,关键流程: 1. **数据结构**: ```python cdef class KDTree: cdef: np.ndarray data # 数据矩阵 (n_samples, n_features) int leaf_size # 叶节点最大样本数 NodeType root # 树根节点 cdef class NodeType: int idx_start, idx_end # 节点数据在数组中的起止索引 int split_dim # 割维度 float split_threshold # 割阈值 NodeType left, right # 左右子树 ``` 2. **构建过程简化代码**: ```python def _build_tree(int node_id, int depth, indices): if len(indices) <= leaf_size: # 叶节点终止 return LeafNode(indices) dim = depth % k # 维度轮换 median = _fast_median(data[indices, dim]) left_idx = indices[data[indices, dim] < median] right_idx = indices[data[indices, dim] >= median] node = InternalNode( split_dim=dim, split_threshold=median, left=_build_tree(2*node_id+1, depth+1, left_idx), right=_build_tree(2*node_id+2, depth+1, right_idx) ) return node ``` 3. **搜索过程优化**: - **优先级队列**:维护候选邻居的堆结构 - **双树遍历**:当 `dualtree=True` 时同时遍历查询树和目标树 - **距离计算优化**:使用距离下界进行剪枝 --- #### 四、数学推导:剪枝策略的正确性 **定理**:若 $\delta \geq d_c$,则割平面另一侧不存在更近邻。 **证明**: 设查询点 $\mathbf{q}$,候选点 $\mathbf{x}_c$,割平面 $H: x_l = s$,右侧任一点 $\mathbf{x}_r$。 由割平面性质 $x_{rl} \geq s$,且由 $\delta = |q_l - s| \geq d_c$: $$\begin{align*} \|\mathbf{q} - \mathbf{x}_r\|_2^2 &\geq |q_l - x_{rl}|^2 & \text{(距离量关系)} \\ &\geq (x_{rl} - q_l)^2 & \text{(右侧点特性)} \\ &\geq (s - q_l)^2 & \text{(因 $x_{rl} \geq s$)} \\ &= \delta^2 \geq d_c^2 \end{align*}$$ 故 $\|\mathbf{q} - \mathbf{x}_r\|_2 \geq d_c$,证毕。 --- #### 五、scikit-learn中的调用实例 ```python from sklearn.neighbors import KDTree import numpy as np # 生成样本数据(500个3维点) X = np.random.rand(500, 3) # 初始化KDTree(叶节点大小=20) kdt = KDTree(X, leaf_size=20, metric='euclidean') # 查询点与K近邻搜索 query_point = np.array([0.5, 0.5, 0.5]) distances, indices = kdt.query(query_point.reshape(1, -1), k=5) print(f"最近邻索引:{indices[0]}") print(f"对应距离:{distances[0]}") ``` **关键参数说明**: - `leaf_size=20`:平衡树深与距离计算次数 - `metric='minkowski'`:支持欧氏/曼哈顿距离等 - `k=5`:返回5个最近邻 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值