Структура данных для хранения тысяч векторов - PullRequest
9 голосов
/ 17 декабря 2009

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

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

Ответы [ 7 ]

6 голосов
/ 17 декабря 2009

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

Диаграмма Вороного может быть создана с использованием алгоритма Фортуны , который выполняет O (n log n) вычислительных шагов и стоит O (n) пространства. На этом сайте показано, как сделать трапециевидную карту и как ее запросить. Вы также можете найти некоторые границы там:

  • Ожидаемое время создания: O (n log n)
  • Ожидаемая сложность пространства: O (n) Но
  • самое главное, ожидаемый запрос время: O (log n).
    (Это (теоретически) лучше, чем O (& radic; n) kD-дерева.)
  • Обновление будет линейным (O (n)), я думаю.

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

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

5 голосов
/ 17 декабря 2009

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

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

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

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

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

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

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

Универсальный раствор

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

  • Если пробел не является проблемой, наиболее эффективным способом может быть предварительный расчет ближайшего соседа для каждой точки на экране, а затем сохранение уникального идентификатора ближайшего соседа в двумерном массиве, представляющем экран.
  • Если время не является проблемой, сохраняя 10K точек в простом двумерном массиве и выполняя простой поиск каждый раз, то есть цикл по каждой точке и вычисление расстояния может быть хорошим и простым и простым в обслуживании вариантом.
  • Для ряда компромиссов между ними, вот хорошее представление о различных доступных вариантах поиска ближайшего соседа: 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;
   }
}
5 голосов
/ 17 декабря 2009

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

2 голосов
/ 17 декабря 2009

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

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

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

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

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

1 голос
/ 17 декабря 2009

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

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

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

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

0 голосов
/ 19 декабря 2009

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

0 голосов
/ 18 декабря 2009

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...