KD-Tree

游戏中常对物体进行空间划分,对于均匀分布的划分一般用四叉树(八叉树),动态不均匀的分布可以采用kd-tree

构建kd-tree

构建思路:

  • 1.对节点进行各维度的方差分析,选取方差最大(即离散程度最高)的维度进行排序。取中值节点作为分割点。并将其放入构建树的节点中
  • 2.被排序的节点按不同维度分割后,划分为力左空间(划分维度下小于分割节点的值)与右空间(划分维度下大于分割节点的值)。对两个空间重复步骤1,并且两个空间的分割点分别作为上一个分割点的左右子树加入构建树中。
    (设置维度除了分析方差,有时会选择所有维度循环设置)

树的节点的结构

struct TreeNode {
    int id; //节点唯一id
    Vec2 pos;
    int split; //分割维度
    struct TreeNode* left;
    struct TreeNode* right;
    struct TreeNode* parent;
};

获取分割维度

//计算所有节点各维度的方差,确定分割维度
int KDTreeScene::chooseSplit(vector<Vec2> posData) {
    float xEx1 = 0;
    float xEx2 = 0;
    float xDx = 0;
    float size = posData.size();
    for (auto v : posData) {
        xEx1 += 1.0 / size * v.x * v.x;
        xEx2 += 1.0 / size * v.x;
    }
    xDx = xEx1 - xEx2 * xEx2;
    float yEx1 = 0;
    float yEx2 = 0;
    float yDx = 0;
    for (auto v : posData) {
        yEx1 += 1.0 / size * v.y * v.y;
        yEx2 += 1.0 / size * v.y;
    }
    yDx = yEx1 - yEx2 * yEx2;
    return xDx >= yDx ? 0 : 1;
}

构建树

//rect是画分割线的辅助参数,实际应用可无视
TreeNode* KDTreeScene::buildKdTree(vector<Vec2> posData, Rect rect, int splitIdx) {
    if (posData.size() == 0) return nullptr;
    int split = chooseSplit(posData); //根据方差设置维度
    //split = splitIdx; //循环设置维度
    if (posData.size() == 1) {
    	//空间只剩一个节点,无需再划分
        TreeNode* TNode = new TreeNode();
        TNode->id = _id;
        _id++;
        TNode->pos = posData[0];
        TNode->left = nullptr;
        TNode->right = nullptr;
        TNode->split = split;
        //根据维度画分割线
        drawClipLine(rect, split, posData[0]);
        return TNode;
    }
    int mid = posData.size() / 2;
    vector<Vec2> leftPosData;
    vector<Vec2> rightPosData;

	//根据设置的维度进行排序
    if (split == 0) sort(posData.begin(), posData.end(), cmpX); //垂直x轴分割,则根据x值排序
    else sort(posData.begin(), posData.end(), cmpY); //垂直y轴分割,则根据y值排序

	//获取中值作为分割点
    Vec2 midPos = posData[mid];
    //小于中值的放入左空间
    for (int i = 0; i < mid; i++) {
        leftPosData.push_back(posData[i]);
    }
    //大于中值的放入右空间
    for (int j = mid + 1; j < posData.size(); j++) {
        rightPosData.push_back(posData[j]);
    }
    //创建树的节点
    TreeNode* TNode = new TreeNode();
    TNode->id = _id;
    _id++;
    TNode->pos = midPos;
    drawClipLine(rect, split, midPos);
    // ---------------------
    //用来画分割线的辅助参数,实际应用可无视
    Rect r1, r2;
    if (split == 0) {
        r1 = Rect(rect.getMinX(), rect.getMinY(), midPos.x - rect.getMinX(), rect.getMaxY() - rect.getMinY());
        r2 = Rect(midPos.x, rect.getMinY(), rect.getMaxX() - midPos.x, rect.getMaxY() - rect.getMinY());
    }
    else {
        r1 = Rect(rect.getMinX(), rect.getMinY(), rect.getMaxX() - rect.getMinX(), midPos.y - rect.getMinY());
        r2 = Rect(rect.getMinX(), midPos.y, rect.getMaxX() - rect.getMinX(), rect.getMaxY() - midPos.y);
    }
    // ---------------------
    //splitIdx++;  //循环遍历设置维度的方法
    //对左空间进行kd树的构建步骤,并把顶点作为当前节点的左子树
    TNode->left = buildKdTree(leftPosData, r1, splitIdx % 2);
    if (TNode->left != nullptr) TNode->left->parent = TNode;
    //对右空间进行kd树的构建步骤,并把顶点作为当前节点的右子树
    TNode->right = buildKdTree(rightPosData, r2, splitIdx % 2);
    if (TNode->right != nullptr) TNode->right->parent = TNode;
    TNode->split = split;
    //返回的节点为当前空间构造的kd树的顶点
    return TNode;
}

效果演示

KD-Tree_第1张图片

近邻搜寻

搜寻思路

维护一个最近邻居的优先级队列,以及一个记录访问过节点的表

  • 1.从根节点开始,将目标点递归往下查询。与插入思路一样,当前维度小于当前节点值的查询左子树,大于当前节点值的查询右子树。直到移动到叶子节点,将叶子节点进行访问。
  • 2.访问叶子节点。如果最近邻居优先级队列里的值小于所需邻居值,或者该叶子节点到目标点的距离小于队列中最远的邻居节点到目标点的距离。则将该叶子节点更新到最近邻居的队列,记录访问过的当前节点
  • 3.从叶子节点往上查询,(获取该叶子节点的父节点或着解开递归等方式)。对父节点进行访问处理(步骤2)。如果父节点已经访问过,则继续往上(重复步骤3)
  • 4.对该父节点对应维度的分割边进行距离判定:
    1.如果最近邻居优先级队列里的值小于所需邻居值或者目标点到分割边的垂直距离小于队列中最远的邻居节点到目标点的距离,则对该父节点的另一半空间进行从步骤1开始往下的查询
    2.否则,直接忽略该父节点的另一半空间,继续往上查询(回到步骤3)

往上的查询直到树的顶点即停止

void KDTreeScene::searchNearestPoint(Vec2 pos, TreeNode* tNode) {
    while (!_nearestQueue.empty()) {
        _nearestQueue.pop();
    }
    _searchDrawNode->clear();
    _exploredMap.clear();
    _searchDrawNode->drawDot(pos, 5, Color4F(0, 1, 0, 1));

//-----------------
	//进入搜寻
    TreeNode* leaf = searchLeafNode(pos, _kdTree);
    backCheck(pos, leaf);
//-----------------

//下面是画出搜寻结果
    while (!_nearestQueue.empty()) {
        _searchDrawNode->drawDot(_nearestQueue.top().second->pos, 5, Color4F(0, 0, 0, 1));
        if (_model) {
            delete _nearestQueue.top().second;
        }
        _nearestQueue.pop();
    }
}

访问对应节点

上述步骤2

bool KDTreeScene::checkInsertQueue(Vec2 pos, TreeNode* tNode) {
	//记录以访问过的节点
    _exploredMap[tNode->id] = true;
    //_searchNode是需要获取的最近邻居节点数
    //近邻居优先级队列里的值小于所需邻居值,则直接插入队列
    if (_nearestQueue.size() < _searchNode) {
        _nearestQueue.emplace(pos.getDistance(tNode->pos), tNode);
        return true;
    }
    else {
        auto data = _nearestQueue.top();
        float dist = pos.getDistance(tNode->pos);
        //该叶子节点到目标点的距离小于队列中最远的邻居节点到目标点的距离,则更新队列
        if (dist < data.first) {
            _nearestQueue.pop();
            _nearestQueue.emplace(dist, tNode);
            return true;
        }
    }
    return false;
}

递归查询到叶节点

上述步骤1

TreeNode* KDTreeScene::searchLeafNode(Vec2 pos, TreeNode* tNode) {
    if (tNode->split == 0) {
        if (pos.x < tNode->pos.x) {
            if (tNode->left == nullptr) {
                checkInsertQueue(pos, tNode);
                return tNode;
            }
            else return searchLeafNode(pos, tNode->left);
        }
        else {
            if (tNode->right == nullptr) {
                checkInsertQueue(pos, tNode);
                return tNode;
            }
            else return searchLeafNode(pos, tNode->right);
        }
    }
    else {
        if (pos.y < tNode->pos.y) {
            if (tNode->left == nullptr) {
                checkInsertQueue(pos, tNode);
                return tNode;
            }
            else return searchLeafNode(pos, tNode->left);
        }
        else {
            if (tNode->right == nullptr) {
                checkInsertQueue(pos, tNode);
                return tNode;
            }
            else return searchLeafNode(pos, tNode->right);
        }
    }
}

往上查询

上述步骤3和4

void KDTreeScene::backCheck(Vec2 pos, TreeNode* tNode) {
	//到顶点停止
    if (tNode->parent == nullptr) {
        return;
    }
    else {
    	//步骤3
        tNode = tNode->parent;
        //已经访问过的节点直接跳过,往上查询
        if (!_exploredMap[tNode->id]) {
        	//访问父节点
            checkInsertQueue(pos, tNode);
            if (tNode->split == 0) {
            	//步骤4
                if (_nearestQueue.size() < _searchNode || abs(pos.x - tNode->pos.x) < _nearestQueue.top().first) {
                	//步骤4 情况1
                	//----------------------
                    if (pos.x < tNode->pos.x) {
                        if(tNode->right) tNode = searchLeafNode(pos, tNode->right);
                    }
                    else {
                        if (tNode->left) tNode = searchLeafNode(pos, tNode->left);
                    }
                    //----------------------
                }
            }
            else {
                if (_nearestQueue.size() < _searchNode || abs(pos.y - tNode->pos.y) < _nearestQueue.top().first) {
                    if (pos.y < tNode->pos.y) {
                        if (tNode->right) tNode = searchLeafNode(pos, tNode->right);
                    }
                    else {
                        if (tNode->left) tNode = searchLeafNode(pos, tNode->left);
                    }
                }
            }
        }
        backCheck(pos, tNode);
    }
}

效果演示

KD-Tree_第2张图片

完整效果

以下是对2000个物体查询最近的6个邻居的效果:
KD-Tree_第3张图片

对比了下对所有物体进行遍历找寻最近值的方式
搜寻3个最近物体时,在1000个时差不多开始出现1ms的延迟,在5000个3到4ms内的延迟,在10000个时7ms延迟, 在100000时70ms延迟, 就是成正比的延迟,
而kd树的查询到10000000仍是0,因为是类似二叉树的log的复杂度
主要耗时在构造树,1000个物体差不多5ms,5000个物体30ms,10000个物体50ms,100000个物体550ms,1000000个物体6400ms,5000000个物体37000ms,10000000个物体76000ms,差不多也是正比
不同机器效率不同,但是趋势一样

代码

KDTreeScene.h

#ifndef __KDTREE_SCENE_H__
#define __KDTREE_SCENE_H__

#include "cocos2d.h"
USING_NS_CC;
using namespace std;

struct TreeNode {
    int id;
    Vec2 pos;
    int split;
    struct TreeNode* left;
    struct TreeNode* right;
    struct TreeNode* parent;
};

struct cmp {
    bool operator()(pair<float, TreeNode*>& a, pair<float, TreeNode*>& b) {
        return a.first < b.first;
    }
};

class KDTreeScene : public Scene
{
public:
    static Scene* createScene();

    virtual bool init();

    virtual bool onTouchBegan(Touch* touch, Event* unused_event);

    TreeNode* buildKdTree(vector<Vec2> posData, Rect rect, int splitIdx);
    int chooseSplit(vector<Vec2> posData);
    void drawClipLine(Rect rect, int split, Vec2 pos);
    void searchNearestPoint(Vec2 pos, TreeNode* tNode);
    TreeNode* searchLeafNode(Vec2 pos, TreeNode* tNode);
    void backCheck(Vec2 pos, TreeNode* tNode);
    bool checkInsertQueue(Vec2 pos, TreeNode* tNode);

    // implement the "static create()" method manually
    CREATE_FUNC(KDTreeScene);

    void update(float dt);

protected:
    EventListenerTouchOneByOne* _touchListener;
    Vec2 _touchBeganPosition;
    DrawNode* _mapDrawNode;
    DrawNode* _clipDrawNode;

    vector<Vec2> _posData;
    TreeNode* _kdTree;
    bool _isDrawSplitLine = false;

    DrawNode* _searchDrawNode;
    int _searchNode = 6;
    priority_queue<pair<float, TreeNode*>,  vector<pair<float, TreeNode*>>, cmp> _nearestQueue;
    int _id;
    unordered_map<int, bool> _exploredMap;

    bool _model = false;
};

#endif

KDTreeScene.cpp

#include "KDTreeScene.h"
#include 
using namespace std::chrono;

Scene* KDTreeScene::createScene()
{
    return KDTreeScene::create();
}

static void problemLoading(const char* filename)
{
    printf("Error while loading: %s\n", filename);
    printf("Depending on how you compiled you might have to add 'Resources/' in front of filenames in KDTreeScene.cpp\n");
}

// on "init" you need to initialize your instance
bool KDTreeScene::init()
{
    //
    // 1. super init first
    if (!Scene::init())
    {
        return false;
    }
    auto visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    auto layer = LayerColor::create(Color4B(255, 255, 255, 255));
layer:setContentSize(visibleSize);
    this->addChild(layer);

    _clipDrawNode = DrawNode::create();
    this->addChild(_clipDrawNode);
    _mapDrawNode = DrawNode::create();
    this->addChild(_mapDrawNode);
    _searchDrawNode = DrawNode::create();
    this->addChild(_searchDrawNode);

    auto drawNode = DrawNode::create();
    this->addChild(drawNode);
    auto vecData = vector<Vec2>{ Vec2::ZERO, Vec2(100,0), Vec2(100,640), Vec2(0,640) };
    Vec2* ptr = reinterpret_cast<Vec2*>(vecData.data());
    drawNode->drawPolygon(ptr, vecData.size(), Color4F(0, 0, 0, 1), 0, Color4F(0, 0, 0, 0));
    auto vecData1 = vector<Vec2>{ Vec2(1300,0), Vec2(1400,0), Vec2(1400,640), Vec2(1300,640) };
    Vec2* ptr1 = reinterpret_cast<Vec2*>(vecData1.data());
    drawNode->drawPolygon(ptr1, vecData1.size(), Color4F(0, 0, 0, 1), 0, Color4F(0, 0, 0, 0));

    _touchListener = EventListenerTouchOneByOne::create();
    _touchListener->setSwallowTouches(true);
    _touchListener->onTouchBegan = CC_CALLBACK_2(KDTreeScene::onTouchBegan, this);
    this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(_touchListener, layer);

    this->scheduleUpdate();
    return true;
}

bool KDTreeScene::onTouchBegan(Touch* touch, Event* event)
{
    _touchBeganPosition = touch->getLocation();
    CCLOG("xxxxxxxxx==========>> %f? %f", _touchBeganPosition.x, _touchBeganPosition.y);
    if (_touchBeganPosition.x < 100) {
        _posData.clear();
        _mapDrawNode->clear();
        _clipDrawNode->clear();
        _searchDrawNode->clear();
        _id = 1;
        for (int i = 0; i < 5000000; i++) {
            float x = RandomHelper::random_real<float>(100, 1300);
            float y = RandomHelper::random_real<float>(0, 640);
            _posData.push_back(Vec2(x, y));
//            _mapDrawNode->drawDot(Vec2(x, y), 3, Color4F(0, 1, 1, 1));
        }
        auto last = std::chrono::system_clock::now();
        auto timestamp = std::chrono::time_point_cast<std::chrono::milliseconds>(last).time_since_epoch().count();
        _kdTree = buildKdTree(_posData, Rect(0, 0, 1400, 640), 0);
        _kdTree->parent = nullptr;
        auto now = std::chrono::system_clock::now();
        auto timestamp1 = std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count();
        CCLOG("build timestamp dif      %d", timestamp1 - timestamp);
    }
    else if (_touchBeganPosition.x > 1300) {
        _model = !_model;
    }
    else {
        if (_posData.empty()) {
            _id = 1;
            for (int i = 0; i < 2000; i++) {
                float x = RandomHelper::random_real<float>(100, 1300);
                float y = RandomHelper::random_real<float>(0, 640);
                _posData.push_back(Vec2(x, y));
//                _mapDrawNode->drawDot(Vec2(x, y), 3, Color4F(0, 1, 1, 1));
            }
            auto last = std::chrono::system_clock::now();
            auto timestamp = std::chrono::time_point_cast<std::chrono::milliseconds>(last).time_since_epoch().count();
            _kdTree = buildKdTree(_posData, Rect(0, 0, 1400, 640), 0);
            _kdTree->parent = nullptr;
            auto now = std::chrono::system_clock::now();
            auto timestamp1 = std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count();
            CCLOG("build timestamp dif       %d", timestamp1 - timestamp);
            return true;
        }
        searchNearestPoint(_touchBeganPosition, _kdTree);
    }
    return true;
}

void KDTreeScene::update(float dt) {

}

bool cmpX(Vec2 a, Vec2 b) {
    return a.x < b.x;
}

bool cmpY(Vec2 a, Vec2 b) {
    return a.y < b.y;
}

int KDTreeScene::chooseSplit(vector<Vec2> posData) {
    float xEx1 = 0;
    float xEx2 = 0;
    float xDx = 0;
    float size = posData.size();
    for (auto v : posData) {
        xEx1 += 1.0 / size * v.x * v.x;
        xEx2 += 1.0 / size * v.x;
    }
    xDx = xEx1 - xEx2 * xEx2;
    float yEx1 = 0;
    float yEx2 = 0;
    float yDx = 0;
    for (auto v : posData) {
        yEx1 += 1.0 / size * v.y * v.y;
        yEx2 += 1.0 / size * v.y;
    }
    yDx = yEx1 - yEx2 * yEx2;
    return xDx >= yDx ? 0 : 1;
}

TreeNode* KDTreeScene::buildKdTree(vector<Vec2> posData, Rect rect, int splitIdx) {
    if (posData.size() == 0) return nullptr;
    int split = chooseSplit(posData);
    //split = splitIdx;
    if (posData.size() == 1) {
        TreeNode* TNode = new TreeNode();
        TNode->id = _id;
        _id++;
        TNode->pos = posData[0];
        TNode->left = nullptr;
        TNode->right = nullptr;
        TNode->split = split;
        drawClipLine(rect, split, posData[0]);
        return TNode;
    }
    int mid = posData.size() / 2;
    vector<Vec2> leftPosData;
    vector<Vec2> rightPosData;

    if (split == 0) sort(posData.begin(), posData.end(), cmpX);
    else sort(posData.begin(), posData.end(), cmpY);

    Vec2 midPos = posData[mid];
    for (int i = 0; i < mid; i++) {
        leftPosData.push_back(posData[i]);
    }
    for (int j = mid + 1; j < posData.size(); j++) {
        rightPosData.push_back(posData[j]);
    }
    TreeNode* TNode = new TreeNode();
    TNode->id = _id;
    _id++;
    TNode->pos = midPos;
    drawClipLine(rect, split, midPos);
    Rect r1, r2;
    if (split == 0) {
        r1 = Rect(rect.getMinX(), rect.getMinY(), midPos.x - rect.getMinX(), rect.getMaxY() - rect.getMinY());
        r2 = Rect(midPos.x, rect.getMinY(), rect.getMaxX() - midPos.x, rect.getMaxY() - rect.getMinY());
    }
    else {
        r1 = Rect(rect.getMinX(), rect.getMinY(), rect.getMaxX() - rect.getMinX(), midPos.y - rect.getMinY());
        r2 = Rect(rect.getMinX(), midPos.y, rect.getMaxX() - rect.getMinX(), rect.getMaxY() - midPos.y);
    }
    splitIdx++;
    TNode->left = buildKdTree(leftPosData, r1, splitIdx % 2);
    if (TNode->left != nullptr) TNode->left->parent = TNode;
    TNode->right = buildKdTree(rightPosData, r2, splitIdx % 2);
    if (TNode->right != nullptr) TNode->right->parent = TNode;
    TNode->split = split;
    return TNode;
}

void KDTreeScene::drawClipLine(Rect rect, int split, Vec2 pos) {
//    if (split == 0) {
//        _clipDrawNode->drawSegment(Vec2(pos.x, rect.getMinY()), Vec2(pos.x, rect.getMaxY()), 1, Color4F(1, 0, 0, 0.5));
//    }
//    else {
//        _clipDrawNode->drawSegment(Vec2(rect.getMinX(), pos.y), Vec2(rect.getMaxX(), pos.y), 1, Color4F(0, 0, 1, 0.5));
//    }
}

void KDTreeScene::searchNearestPoint(Vec2 pos, TreeNode* tNode) {
    while (!_nearestQueue.empty()) {
        _nearestQueue.pop();
    }
    _searchDrawNode->clear();
    _exploredMap.clear();
    _searchDrawNode->drawDot(pos, 3, Color4F(1, 0, 0, 1));
    auto last = std::chrono::system_clock::now();
    auto timestamp = std::chrono::time_point_cast<std::chrono::milliseconds>(last).time_since_epoch().count();
    if (_model) {
        for (auto p : _posData) {
            TreeNode* t = new TreeNode();
            t->id = _id;
            _id++;
            t->pos = p;
            checkInsertQueue(pos, t);
        }
    }
    else {
        TreeNode* leaf = searchLeafNode(pos, _kdTree);
        backCheck(pos, leaf);
    }
    auto now = std::chrono::system_clock::now();
    auto timestamp1 = std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count();
    CCLOG("search timestamp dif       %d", timestamp1 - timestamp);
    while (!_nearestQueue.empty()) {
        _searchDrawNode->drawDot(_nearestQueue.top().second->pos, 3, Color4F(0, 0, 0, 1));
        if (_model) {
            delete _nearestQueue.top().second;
        }
        _nearestQueue.pop();
    }
}

bool KDTreeScene::checkInsertQueue(Vec2 pos, TreeNode* tNode) {
    _exploredMap[tNode->id] = true;
    if (_nearestQueue.size() < _searchNode) {
        _nearestQueue.emplace(pos.getDistance(tNode->pos), tNode);
        return true;
    }
    else {
        auto data = _nearestQueue.top();
        float dist = pos.getDistance(tNode->pos);
        if (dist < data.first) {
            _nearestQueue.pop();
            _nearestQueue.emplace(dist, tNode);
            return true;
        }
    }
    return false;
}

TreeNode* KDTreeScene::searchLeafNode(Vec2 pos, TreeNode* tNode) {
    if (tNode->split == 0) {
        if (pos.x < tNode->pos.x) {
            if (tNode->left == nullptr) {
                checkInsertQueue(pos, tNode);
                return tNode;
            }
            else return searchLeafNode(pos, tNode->left);
        }
        else {
            if (tNode->right == nullptr) {
                checkInsertQueue(pos, tNode);
                return tNode;
            }
            else return searchLeafNode(pos, tNode->right);
        }
    }
    else {
        if (pos.y < tNode->pos.y) {
            if (tNode->left == nullptr) {
                checkInsertQueue(pos, tNode);
                return tNode;
            }
            else return searchLeafNode(pos, tNode->left);
        }
        else {
            if (tNode->right == nullptr) {
                checkInsertQueue(pos, tNode);
                return tNode;
            }
            else return searchLeafNode(pos, tNode->right);
        }
    }
}

void KDTreeScene::backCheck(Vec2 pos, TreeNode* tNode) {
    if (tNode->parent == nullptr) {
        return;
    }
    else {
        tNode = tNode->parent;
        if (!_exploredMap[tNode->id]) {
            checkInsertQueue(pos, tNode);
            if (tNode->split == 0) {
                if (_nearestQueue.size() < _searchNode || abs(pos.x - tNode->pos.x) < _nearestQueue.top().first) {
                    if (pos.x < tNode->pos.x) {
                        if(tNode->right) tNode = searchLeafNode(pos, tNode->right);
                    }
                    else {
                        if (tNode->left) tNode = searchLeafNode(pos, tNode->left);
                    }
                }
            }
            else {
                if (_nearestQueue.size() < _searchNode || abs(pos.y - tNode->pos.y) < _nearestQueue.top().first) {
                    if (pos.y < tNode->pos.y) {
                        if (tNode->right) tNode = searchLeafNode(pos, tNode->right);
                    }
                    else {
                        if (tNode->left) tNode = searchLeafNode(pos, tNode->left);
                    }
                }
            }
        }
        backCheck(pos, tNode);
    }
}

你可能感兴趣的:(游戏开发,算法,kd-tree,KD树算法,最邻近搜索)