Структура данных для хранения тысяч векторов

У меня есть до 10 000 случайно расположенных точек в пространстве, и я должен быть в состоянии сказать, какой курсор ближе всего в любой момент времени. Чтобы добавить некоторый контекст, точки имеют форму векторного рисунка, поэтому они могут быть постоянно и быстро добавлены и удалены пользователем, а также потенциально несбалансированы по пространству холста..

поэтому я пытаюсь найти наиболее эффективную структуру данных для хранения и запроса этих точек. Я хотел бы сохранить это вопрос на языке агностиков, если это возможно.

7 ответов


после обновления вопроса

  1. использовать два Красно-Черное Дерево или Skip_list карты. Оба являются компактными самобалансирующимися структурами данных, дающими вам O (log n) время для операций поиска, вставки и удаления. Одна карта будет использовать координату X для каждой точки в качестве ключа и саму точку в качестве значения, а другая-координату Y в качестве ключа и саму точку в качестве значения.

  2. в качестве компромисса I предложите изначально ограничить область поиска вокруг курсора квадратом. Для идеального соответствия квадратная сторона должна быть равна диаметру вашего "круга чувствительности"вокруг курсора. Т. е. если вас интересует только ближайший сосед в радиусе 10 пикселей от курсора, то квадратная сторона должна быть 20px. В качестве альтернативы, если вы ищете ближайшего соседа независимо от близости, вы можете попытаться найти границу динамически, оценив пол и потолок относительно указатель.

  3. затем извлеките два подмножества точек из карт, которые находятся в пределах границ, объедините, чтобы включить только точки в оба подмножества.

  4. цикл через результат, вычислить близость к каждой точке (dx^2+dy^2, Избегайте квадратного корня, так как вас не интересует фактическое расстояние, просто близость), найти ближайшего соседа.

  5. взять корень квадратный из таких, чтобы измерить расстояние для ближайшего соседа посмотрите, больше ли он радиуса "круга чувствительности", если это так, это означает, что внутри круга нет точек.

  6. Я предлагаю делать некоторые бенчмарки каждый подход; это два легко перейти на вершину с оптимизациями. На моем скромном оборудовании (Duo Core 2) наивный однопоточный поиск ближайшего соседа в пределах 10k точек, повторенный тысячу раз, занимает 350 миллисекунд в Java. Пока общий интерфейс повторное действие время находится под 100 миллисекунды это будет казаться мгновенным для пользователя, имея в виду, что даже наивный поиск может дать вам достаточно быстрый ответ.

Универсального Решения

наиболее эффективная структура данных зависит от алгоритма, который вы планируете использовать, пространственно-временного компромисса и ожидаемого относительного распределения точек:

  • если пространство не является проблемой, наиболее эффективным способом может быть предварительный расчет ближайшего соседа для каждого укажите на экран, а затем сохраните уникальный идентификатор ближайшего соседа в двумерном массиве, представляющем экран.
  • если время не является проблемой хранения 10k точек в простом 2D-массиве и делает наивный поиск каждый раз, т. е. цикл через каждую точку и расчет расстояния может быть хорошим и простым простым в обслуживании вариантом.
  • для ряда компромиссов между ними, вот хорошая презентация о различных вариантах поиска ближайших соседей, доступных: http://dimacs.rutgers.edu/Workshops/MiningTutorial/pindyk-slides.ppt
  • куча хороших подробных материалов для различных алгоритмов поиска ближайших соседей:http://simsearch.yury.name/tutorial.html, как раз выберите одно которое одевает ваши потребности самые лучшие.

таким образом, действительно невозможно оценить структуру данных-это изоляция от алгоритма, который, в свою очередь, трудно оценить без хорошего представления об ограничениях задачи и приоритеты.

Пример Реализации Java

import java.util.*;
import java.util.concurrent.ConcurrentSkipListMap;

class Test
{

  public static void main (String[] args)
  {

      Drawing naive = new NaiveDrawing();
      Drawing skip  = new SkipListDrawing();

      long start;

      start = System.currentTimeMillis();
      testInsert(naive);
      System.out.println("Naive insert: "+(System.currentTimeMillis() - start)+"ms");
      start = System.currentTimeMillis();
      testSearch(naive);
      System.out.println("Naive search: "+(System.currentTimeMillis() - start)+"ms");


      start = System.currentTimeMillis();
      testInsert(skip);
      System.out.println("Skip List insert: "+(System.currentTimeMillis() - start)+"ms");
      start = System.currentTimeMillis();
      testSearch(skip);
      System.out.println("Skip List search: "+(System.currentTimeMillis() - start)+"ms");

  }

  public static void testInsert(Drawing d)
  {
      Random r = new Random();
      for (int i=0;i<100000;i++)
            d.addPoint(new Point(r.nextInt(4096),r.nextInt(2048)));
  }

  public static void testSearch(Drawing d)
  {
      Point cursor;
      Random r = new Random();
      for (int i=0;i<1000;i++)
      {
          cursor = new Point(r.nextInt(4096),r.nextInt(2048));
          d.getNearestFrom(cursor,10);
      }
  }


}

// A simple point class
class Point
{
    public Point (int x, int y)
    {
        this.x = x;
        this.y = y;
    }
    public final int x,y;

    public String toString()
    {
        return "["+x+","+y+"]";
    }
}

// Interface will make the benchmarking easier
interface Drawing
{
    void addPoint (Point p);
    Set<Point> getNearestFrom (Point source,int radius);

}


class SkipListDrawing implements Drawing
{

    // Helper class to store an index of point by a single coordinate
    // Unlike standard Map it's capable of storing several points against the same coordinate, i.e.
    // [10,15] [10,40] [10,49] all can be stored against X-coordinate and retrieved later
    // This is achieved by storing a list of points against the key, as opposed to storing just a point.
    private class Index
    {
        final private NavigableMap<Integer,List<Point>> index = new ConcurrentSkipListMap <Integer,List<Point>> ();

        void add (Point p,int indexKey)
        {
            List<Point> list = index.get(indexKey);
            if (list==null)
            {
                list = new ArrayList<Point>();
                index.put(indexKey,list);
            }
            list.add(p);
        }

        HashSet<Point> get (int fromKey,int toKey)
        {
            final HashSet<Point> result = new HashSet<Point> ();

            // Use NavigableMap.subMap to quickly retrieve all entries matching
            // search boundaries, then flatten resulting lists of points into
            // a single HashSet of points.
            for (List<Point> s: index.subMap(fromKey,true,toKey,true).values())
                for (Point p: s)
                 result.add(p);

            return result;
        }

    }

    // Store each point index by it's X and Y coordinate in two separate indices
    final private Index xIndex = new Index();
    final private Index yIndex = new Index();

    public void addPoint (Point p)
    {
        xIndex.add(p,p.x);
        yIndex.add(p,p.y);
    }


    public Set<Point> getNearestFrom (Point origin,int radius)
    {


          final Set<Point> searchSpace;
          // search space is going to contain only the points that are within
          // "sensitivity square". First get all points where X coordinate
          // is within the given range.
          searchSpace = xIndex.get(origin.x-radius,origin.x+radius);

          // Then get all points where Y is within the range, and store
          // within searchSpace the intersection of two sets, i.e. only
          // points where both X and Y are within the range.
          searchSpace.retainAll(yIndex.get(origin.y-radius,origin.y+radius));


          // Loop through search space, calculate proximity to each point
          // Don't take square root as it's expensive and really unneccessary
          // at this stage.
          //
          // Keep track of nearest points list if there are several
          // at the same distance.
          int dist,dx,dy, minDist = Integer.MAX_VALUE;

          Set<Point> nearest = new HashSet<Point>();

          for (Point p: searchSpace)
          {
             dx=p.x-origin.x;
             dy=p.y-origin.y;
             dist=dx*dx+dy*dy;

             if (dist<minDist)
             {
                   minDist=dist;
                   nearest.clear();
                   nearest.add(p);
             }
             else if (dist==minDist)
             {
                 nearest.add(p);
             }


          }

          // Ok, now we have the list of nearest points, it might be empty.
          // But let's check if they are still beyond the sensitivity radius:
          // we search area we have evaluated was square with an side to
          // the diameter of the actual circle. If points we've found are
          // in the corners of the square area they might be outside the circle.
          // Let's see what the distance is and if it greater than the radius
          // then we don't have a single point within proximity boundaries.
          if (Math.sqrt(minDist) > radius) nearest.clear();
          return nearest;
   }
}

// Naive approach: just loop through every point and see if it's nearest.
class NaiveDrawing implements Drawing
{
    final private List<Point> points = new ArrayList<Point> ();

    public void addPoint (Point p)
    {
        points.add(p);
    }

    public Set<Point> getNearestFrom (Point origin,int radius)
    {

          int prevDist = Integer.MAX_VALUE;
          int dist;

          Set<Point> nearest = Collections.emptySet();

          for (Point p: points)
          {
             int dx = p.x-origin.x;
             int dy = p.y-origin.y;

             dist =  dx * dx + dy * dy;
             if (dist < prevDist)
             {
                   prevDist = dist;
                   nearest  = new HashSet<Point>();
                   nearest.add(p);
             }
             else if (dist==prevDist) nearest.add(p);

          }

          if (Math.sqrt(prevDist) > radius) nearest = Collections.emptySet();

          return nearest;
   }
}

Я хотел бы предложить создание Вороного Схемы и Трапецеидальная Карте (в основном то же самое ответ Как я дал этой вопрос). The Вороного Схемы разделит пространство на полигоны. Каждая точка будет иметь многоугольник, описывающий все точки, которые ближе всего к ней. Теперь, когда вы получаете запрос точки, вам нужно найти, в каком многоугольнике она лежит. Эта проблема называется Местоположение Точки и может решается путем построения Трапецеидальная Карте.

на Вороного Схемы может быть создан с помощью удачи!--2--> который принимает o(n log n) вычислительные шаги и стоит o (n) пространство. этот сайт показывает, как сделать трапециевидную карту и как запросить ее. Вы также можете найти некоторые границы:

  • ожидаемое время создания: O (N log n)
  • ожидаемая сложность пространства: O( n) Но!--26-->
  • самое главное, что ожидается запрос время: O (log n).
    (Это (теоретически) лучше, чем O(√n) KD-дерева.)
  • обновление будет линейным(O (n)) я думаю.

мой источник(кроме ссылки выше): вычислительная геометрия: алгоритмы и приложения, главы шесть и семь.

там вы найдете подробную информацию о двух структурах данных (включая подробные доказательства). поиск книг Google версия имеет только часть того, что вам нужно, но других ссылок должно быть достаточно для ваших целей. Просто купить книгу, если вы заинтересованы в такого рода вещи (это хорошая книга).


наиболее эффективной структурой данных будет KD-tree текст ссылки


равномерно ли распределены точки?

вы можете построить четырехъядерное дерево до определенной глубины, скажем, 8. Вверху у вас есть узел дерева, который делит экран на четыре квадранта. Хранить на каждом узле:

  • верхняя левая и нижняя правая координаты
  • указывает на четыре дочерних узла, которые делят узел на четыре квадранта

постройте дерево на глубину до 8, скажем, и в листовых узлах сохраните список точек связан с этим регионом. Этот список можно искать линейно.

Если вам нужно больше детализации, строить квад-дерево на большую глубину.


Это зависит от частоты обновлений и запросов. Для быстрого запроса, медленных обновлений, Quadtree (который является формой JD-дерева для 2-D), вероятно, будет лучшим. Quadtree очень хороши для неоднородной точки.

Если у вас низкое разрешение, вы можете использовать необработанный массив ширины X высоты предварительно вычисленных значений.

Если у вас очень мало точек или быстрое обновление, достаточно простого массива или может быть простое разбиение (которое идет к дерева квадрантов).

поэтому ответ зависит от параметров вашей динамики. Также я бы добавил, что в настоящее время algo-это не все; использование нескольких процессоров или CUDA может дать огромный толчок.


вы не указали размеры ваших точек, но если это 2D - линейный чертеж, то растровое ведро-2D-массив списков точек в области, где вы сканируете ведра, соответствующие и близкие к курсору, может работать очень хорошо. Большинство систем с радостью будут обрабатывать растровые ведра порядка от 100x100 до 1000x1000, маленький конец которых будет означать одну точку на ведро. Хотя асимптотическая производительность равна O (N), реальная производительность обычно очень хороша. Движущийся отдельные точки между ведрами могут быть быстрыми; перемещение объектов также может быть быстрым, если вы поместите объекты в ведра, а не в точки ( поэтому полигон из 12 точек будет ссылаться на 12 ведер; перемещение становится в 12 раз дороже вставки и удаления списка ведер; поиск ведра-это постоянное время в 2D-массиве ). Основная стоимость-реорганизация всего, если размер холста растет во многих небольших скачках.


Если он находится в 2D, вы можете создать виртуальную сетку, охватывающую все пространство (ширина и высота до вашего фактического пространства точек) и найти все 2D-точки, которые принадлежат каждой ячейке. После этого ячейка будет ведром в хэш-таблице.