如何对一组点进行排序,以便它们一个接一个地设置?

Posted

技术标签:

【中文标题】如何对一组点进行排序,以便它们一个接一个地设置?【英文标题】:How to sort a collection of points so that they set up one after another? 【发布时间】:2014-10-06 21:52:41 【问题描述】:

我有一个包含点坐标的 ArrayList:

class Point

   int x, y;

ArrayList<Point> myPoints;

以这样的图像为例:

问题是这些点在 ArrayList 中设置混乱,我想对它们进行排序,以便图像上彼此相邻的 2 个点在 ArrayList 中也是一个接一个。我想不出一些好主意或算法来解决这样的排序......有没有一些已知的方法来解决这些问题?

编辑: 形状不能与自身交叉,我们假设只会出现类似这样的形状。

【问题讨论】:

你试过什么?这是你唯一能得到的输入吗?如果形状越过自身会发生什么?这个问题对于 *** 来说太宽泛了,可能会被关闭。 形状不能跨越自身,我们假设只有看起来像这样的形状才会出现。怎么可能太宽泛了?……求编程算法太宽泛了? 使用像上面这样的形状,您可以选择任意点,然后找到最近的点(在欧几里得空间中),依此类推。对于更复杂的形状,这可能还不够,在这种情况下,您可能需要考虑当前的渐变。即使这样也会有限制。 来吧,近距离投票者,我在发布包含 MVCE 的答案前一分钟... :-( ... 为什么一小部分想法不同的人能够关闭一个实际上引起很多人兴趣的线程?这是错误的 【参考方案1】:

我的想法是,您首先需要对排序进行数学定义。我建议(注意,这个定义在原始问题中并不清楚,为了完整起见,留在这里):

从在序列中放置任何点开始,然后将最接近当前点且尚未附加到序列中的点永久附加到序列中,直到所有点都附加到序列中。

因此,有了这个排序的定义,你可以推导出一个简单的算法

ArrayList<point> orderedList = new ArrayList<point>();

orderedList.add(myList.remove(0)); //Arbitrary starting point

while (myList.size() > 0) 
   //Find the index of the closest point (using another method)
   int nearestIndex=findNearestIndex(orderedList.get(orderedList.size()-1), myList);

   //Remove from the unorderedList and add to the ordered one
   orderedList.add(myList.remove(nearestIndex));

以上内容非常普遍(无论寻找下一个点的算法如何)。那么“findNearestIndex”方法可以定义为:

//Note this is intentially a simple algorithm, many faster options are out there
int findNearestIndex (point thisPoint, ArrayList listToSearch) 
    double nearestDistSquared=Double.POSITIVE_INFINITY;
    int nearestIndex;
    for (int i=0; i< listToSearch.size(); i++) 
        point point2=listToSearch.get(i);
        distsq = (thisPoint.x - point2.x)*(thisPoint.x - point2.x) 
               + (thisPoint.y - point2.y)*(thisPoint.y - point2.y);
        if(distsq < nearestDistSquared) 
            nearestDistSquared = distsq;
            nearestIndex=i;
        
    
    return nearestIndex;

更新: 由于修改了问题以在很大程度上采用我使用的定义,因此我删除了一些警告。

【讨论】:

【参考方案2】:

这是一个可能的解决方案:我们的目标是构建一个路径,该路径在循环返回之前只访问列表中的每个点一次。我们可以递归地构造路径:我们可以从原始列表中选择任何点作为我们的起点,并创建一条仅由单个节点组成的普通路径。然后我们可以通过附加一个我们还没有访问过的点来扩展已经构建的路径。

然后我们假设我们可以通过确保选择具有最小长度的路径来找到原始点列表的良好顺序。这里,长度不是指路径中的点数,而是路径上每对相邻点之间的欧几里得距离的总和。

唯一的问题是:给定这样的路径,接下来我们应该追加哪个点?理论上,我们必须尝试所有的可能性,看看哪一种会导致最佳的整体路径。

以下代码使用的主要技巧是它使用以下启发式方法:在我们必须将新点附加到目前构建的路径的每个步骤中,选择最小化两个相邻点之间的平均距离的点。

应该注意的是,在此包含路径上最后一个点和第一个点之间的“循环距离”是一个坏主意:随着我们不断添加点,我们会远离第一个路径点和更多。如果我们包括两个端点之间的距离,这将严重影响所有相邻对之间的平均距离,从而损害我们的启发式算法。

这里有一个简单的辅助类来实现上述路径构造:

/**
 * Simple recursive path definition: a path consists 
 * of a (possibly empty) prefix and a head point.
 */
class Path 
    private Path prefix;
    private Point head;
    private int size;
    private double length;

    public Path(Path prefix, Point head) 
        this.prefix = prefix;
        this.head = head;

        if (prefix == null) 
            size = 1;
            length = 0.0;
         else 
            size = prefix.size + 1;

            // compute distance from head of prefix to this new head
            int distx = head.x - prefix.head.x;
            int disty = head.y - prefix.head.y;
            double headLength = Math.sqrt(distx * distx + disty * disty);

            length = prefix.length + headLength;
        
    

这是实际的启发式搜索算法。

/**
 * Implements a search heuristic to determine a sort
 * order for the given <code>points</code>.
 */
public List<Point> sort(List<Point> points) 
    int len = points.size();

    // compares the average edge length of two paths
    Comparator<Path> pathComparator = new Comparator<Path>() 
        public int compare(Path p1, Path p2) 
            return Double.compare(p1.length / p1.size, p2.length / p2.size);
        
    ;

    // we use a priority queue to implement the heuristic
    // of preferring the path with the smallest average
    // distance between its member points
    PriorityQueue<Path> pq = new PriorityQueue<Path>(len, pathComparator);
    pq.offer(new Path(null, points.get(0)));

    List<Point> ret = new ArrayList<Point>(len);
    while (!pq.isEmpty()) 
        Path path = pq.poll();

        if (path.size == len) 
            // result found, turn path into list
            while (path != null) 
                ret.add(0, path.head);
                path = path.prefix;
            
            break;
        

        loop:
        for (Point newHead : points) 
            // only consider points as new heads that
            // haven't been processed yet
            for (Path check = path; check != null; check = check.prefix) 
                if (newHead == check.head) 
                    continue loop;
                
            

            // create new candidate path
            pq.offer(new Path(path, newHead));
        
    

    return ret;

如果您在问题的样本点上运行此代码,然后从返回的列表中连接每对相邻的点,您会得到以下图片:

【讨论】:

哇,似乎是另一个复杂的解决方案!很多测试摆在我面前。顺便说一句,您的代码中的“newPath”是什么?我认为这只是一个俯瞰并用上面定义的“路径”替换它在我看来是合理的,但代码给出了内存异常。我会试着弄清楚,明天我会更深入地研究所有的解决方案,并进行一些性能测试 是的,你是完全正确的:我对我的答案进行了一些编辑,但实际上并没有编译它,这就是出现 newPath 工件的原因。我相应地修复了代码。 但是,再三考虑,我认为您应该选择Jared 的答案。我认为与他的答案相比,我的答案引入的额外开销是有道理的,因为我的版本可以更好地处理某些特殊情况。但现在我认为这不值得,你最好用他的方法。【参考方案3】:

这不是Sort 算法 - 它更多的是重新排列以最小化度量(连续点之间的距离)。

我会尝试某种启发式算法——比如:

    选择三个连续的点 a、b、c。 如果距离(a,c) 从 1 开始重复。

应该可以计算出您需要循环多少次才能实现最小排列,或者您可以通过在运行期间发现没有交换来检测最小排列。

您可能需要像冒泡排序的经典优化那样交替扫描方向。

已添加

实验表明该算法不起作用,但我找到了一个。本质上,为列表中的每个条目找到最近的其他点并将其向上移动到下一个位置。

private static class Point 

    final int x;
    final int y;

    public Point(int x, int y) 
        this.x = x;
        this.y = y;
    

    public String toString() 
        return "(" + x + "," + y + ")";
    

    public double distance(Point b) 
        int dx = x - b.x;
        int dy = y - b.y;
        // Simple cartesian distance.
        return Math.sqrt(dx * dx + dy * dy);
    


// Sample test data - forms a square.
Point[] points = new Point[]
    new Point(0, 0),
    new Point(0, 1),
    new Point(0, 2),
    new Point(0, 3),
    new Point(0, 4),
    new Point(0, 5),
    new Point(0, 6),
    new Point(0, 7),
    new Point(0, 8),
    new Point(0, 9),
    new Point(1, 9),
    new Point(2, 9),
    new Point(3, 9),
    new Point(4, 9),
    new Point(5, 9),
    new Point(6, 9),
    new Point(7, 9),
    new Point(8, 9),
    new Point(9, 9),
    new Point(9, 8),
    new Point(9, 7),
    new Point(9, 6),
    new Point(9, 5),
    new Point(9, 4),
    new Point(9, 3),
    new Point(9, 2),
    new Point(9, 1),
    new Point(9, 0),
    new Point(8, 0),
    new Point(7, 0),
    new Point(6, 0),
    new Point(5, 0),
    new Point(4, 0),
    new Point(3, 0),
    new Point(2, 0),
    new Point(1, 0),;

public void test() 
    System.out.println("Hello");
    List<Point> test = Arrays.asList(Arrays.copyOf(points, points.length));
    System.out.println("Before: " + test);
    Collections.shuffle(test);
    System.out.println("Shuffled: " + test);
    List<Point> rebuild = new ArrayList<>(test);
    rebuild.add(0, new Point(0, 0));
    rebuild(rebuild);
    rebuild.remove(0);
    System.out.println("Rebuilt: " + rebuild);


private void rebuild(List<Point> l) 
    for (int i = 0; i < l.size() - 1; i++) 
        Point a = l.get(i);
        // Find the closest.
        int closest = i;
        double howClose = Double.MAX_VALUE;
        for (int j = i + 1; j < l.size(); j++) 
            double howFar = a.distance(l.get(j));
            if (howFar < howClose) 
                closest = j;
                howClose = howFar;
            
        
        if (closest != i + 1) 
            // Swap it in.
            Collections.swap(l, i + 1, closest);
        
    

打印:

Before: [(0,0), (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8), (0,9), (1,9), (2,9), (3,9), (4,9), (5,9), (6,9), (7,9), (8,9), (9,9), (9,8), (9,7), (9,6), (9,5), (9,4), (9,3), (9,2), (9,1), (9,0), (8,0), (7,0), (6,0), (5,0), (4,0), (3,0), (2,0), (1,0)]
Shuffled: [(9,6), (0,9), (0,8), (3,9), (0,5), (9,4), (0,7), (1,0), (5,0), (9,3), (0,1), (3,0), (1,9), (8,9), (9,8), (2,0), (2,9), (9,5), (5,9), (9,7), (6,0), (0,3), (0,2), (9,1), (9,2), (4,0), (4,9), (7,9), (7,0), (8,0), (6,9), (0,6), (0,4), (9,0), (0,0), (9,9)]
Rebuilt: [(0,0), (0,1), (0,2), (0,3), (0,4), (0,5), (0,6), (0,7), (0,8), (0,9), (1,9), (2,9), (3,9), (4,9), (5,9), (6,9), (7,9), (8,9), (9,9), (9,8), (9,7), (9,6), (9,5), (9,4), (9,3), (9,2), (9,1), (9,0), (8,0), (7,0), (6,0), (5,0), (4,0), (3,0), (2,0), (1,0)]

看起来像你要找的东西。

算法的效率并不好 - 大约在 O(n log n) 左右 - 我希望你不需要这样做数百万次。

如果您希望点以可预测的顺序出现(比如最左边的一个),您可以在重建之前在列表的开头添加一个假点,然后将其删除。该算法将始终不理会第一个点。

【讨论】:

【参考方案4】:

我在提出问题后不久就开始了这个,但由于问题被搁置而被推迟。这是同时在 cmets 和其他答案中也提到过的简单方法,但无论如何我都会在这里发布:

这是一个MCVE,展示了最简单、最直接的方法。该方法简单地包括选择一个任意点,然后总是选择最接近前一个点的点。当然,这是有局限性的:

当有尖角或尖角时,它可能会选择错误的点 效率不高,因为它反复搜索最近的点

加速它的一种方法是根据 x 坐标对点进行排序,然后利用这种偏序来在寻找下一个邻居时跳过大部分点。但只要您不想在时间紧迫的上下文中将其应用于万个点,这应该不是问题。

反过来,可能的歧义可能是一个问题,但考虑到这一点,人们不得不说这个问题无论如何都没有明确说明。在某些情况下,甚至人类都无法确定哪个点是合适的“下一个”点——至少,当问题没有扩展到检测形状的“内部/外部”时(这有点类似于@987654322 的问题@你只是不知道意图路径是什么)。

请注意,大多数代码对您的实际问题并不重要,但是……您没有提供这样的“存根”实现。相关部分为=== marked ===

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class SortShapePoints

    public static void main(String[] args)
    
        SwingUtilities.invokeLater(new Runnable()
        
            @Override
            public void run()
            
                createAndShowGUI();
            
        );
    

    private static void createAndShowGUI()
    
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        Shape shape = createExampleShape();
        List<Point2D> points = computePoints(shape, 6);
        Collections.shuffle(points);

        List<Point2D> sortedPoints = sortPoints(points);
        Path2D path = createPath(sortedPoints, true);
        f.getContentPane().add(new ShapePanel(points, path));

        f.setSize(800, 800);
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    

    //=== Relevant part starts here =========================================

    private static List<Point2D> sortPoints(List<Point2D> points)
    
        points = new ArrayList<Point2D>(points);
        List<Point2D> sortedPoints = new ArrayList<Point2D>();
        Point2D p = points.remove(0);
        sortedPoints.add(p);
        while (points.size() > 0)
        
            int index = indexOfClosest(p, points);
            p = points.remove(index);
            sortedPoints.add(p);
        

        return sortedPoints;
    

    private static int indexOfClosest(Point2D p, List<Point2D> list)
    
        double minDistanceSquared = Double.POSITIVE_INFINITY;
        int minDistanceIndex = -1;
        for (int i = 0; i < list.size(); i++)
        
            Point2D other = list.get(i);
            double distanceSquared = p.distanceSq(other);
            if (distanceSquared < minDistanceSquared)
            
                minDistanceSquared = distanceSquared;
                minDistanceIndex = i;
            
        
        return minDistanceIndex;
    

    //=== Relevant part ends here ===========================================


    private static Shape createExampleShape()
    
        Area a = new Area();
        a.add(new Area(new Ellipse2D.Double(200, 200, 200, 100)));
        a.add(new Area(new Ellipse2D.Double(260, 160, 100, 500)));
        a.add(new Area(new Ellipse2D.Double(220, 380, 180, 60)));
        a.add(new Area(new Rectangle2D.Double(180, 520, 260, 40)));
        return a;
    

    private static List<Point2D> computePoints(Shape shape, double deviation)
    
        List<Point2D> result = new ArrayList<Point2D>();
        PathIterator pi = shape.getPathIterator(null, deviation);
        double[] coords = new double[6];
        Point2D newPoint = null;
        Point2D previousMove = null;
        Point2D previousPoint = null;
        while (!pi.isDone())
        
            int segment = pi.currentSegment(coords);
            switch (segment)
            
            case PathIterator.SEG_MOVETO:
                previousPoint = new Point2D.Double(coords[0], coords[1]);
                previousMove = new Point2D.Double(coords[0], coords[1]);
                break;

            case PathIterator.SEG_CLOSE:
                createPoints(previousPoint, previousMove, result, deviation);
                break;

            case PathIterator.SEG_LINETO:
                newPoint = new Point2D.Double(coords[0], coords[1]);
                createPoints(previousPoint, newPoint, result, deviation);
                previousPoint = new Point2D.Double(coords[0], coords[1]);
                break;

            case PathIterator.SEG_QUADTO:
            case PathIterator.SEG_CUBICTO:
            default:
                // Should never occur
                throw new AssertionError("Invalid segment in flattened path!");
            
            pi.next();
        
        return result;
    

    private static void createPoints(Point2D p0, Point2D p1,
        List<Point2D> result, double deviation)
    
        double dx = p1.getX() - p0.getX();
        double dy = p1.getY() - p0.getY();
        double d = Math.hypot(dx, dy);
        int steps = (int) Math.ceil(d / deviation);
        for (int i = 0; i < steps; i++)
        
            double alpha = (double) i / steps;
            double x = p0.getX() + alpha * dx;
            double y = p0.getY() + alpha * dy;
            result.add(new Point2D.Double(x, y));
        
    

    public static Path2D createPath(Iterable<? extends Point2D> points,
        boolean close)
    
        Path2D path = new Path2D.Double();
        Iterator<? extends Point2D> iterator = points.iterator();
        boolean hasPoints = false;
        if (iterator.hasNext())
        
            Point2D point = iterator.next();
            path.moveTo(point.getX(), point.getY());
            hasPoints = true;
        
        while (iterator.hasNext())
        
            Point2D point = iterator.next();
            path.lineTo(point.getX(), point.getY());
        
        if (close && hasPoints)
        
            path.closePath();
        
        return path;
    



class ShapePanel extends JPanel

    private final List<Point2D> points;
    private final Shape shape;

    public ShapePanel(List<Point2D> points, Shape shape)
    
        this.points = points;
        this.shape = shape;
    

    @Override
    protected void paintComponent(Graphics gr)
    
        super.paintComponent(gr);
        Graphics2D g = (Graphics2D) gr;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON);
        g.setColor(Color.RED);
        g.draw(shape);
        g.setColor(Color.BLACK);
        for (Point2D p : points)
        
            g.fill(new Ellipse2D.Double(p.getX() - 1, p.getY() - 1, 2, 2));
        
    

【讨论】:

这与Jared的回答有何不同? @Thomas 如上所述:当“这个问题已关闭”横幅弹出时,我已经开始(并且已经几乎完成了)这个。 (相当烦人,考虑到这个问题当时已经有 3 个赞成票,现在有 9 个赞成票!)。我不想让它丢失,即使在此期间已经发布了类似的答案。它是一个 MVCE,也许这也鼓励其他人通过相应地实现 sortPoints 方法来尝试其他(更好的)方法。【参考方案5】:

这是一个非常开放的问题,但是如果您希望它们以某种方式存储,则需要定义排序而不是“使它们在数组中彼此相邻”您需要有一个函数可以取两点,说 A 点小于 B 点,反之亦然,或者它们相等。

如果你有,那么你需要对它们进行排序的算法已经实现,你可以通过实现一个比较器来使用它,就像 SANN3 所说的那样。

附带说明,您可能不想将形状存储为一组点。我想您可能想将它们存储为一条线?您可以使用三次样条获得几乎任何您想要的形状,然后您可以节省存储空间...

【讨论】:

【参考方案6】:

我的任务是对点进行排序以表示一条线。我决定保留路径的全部权重并相应地根据标准Collection 操作对其进行更新。该解决方案也应该适用于您的情况。只需将这个 LinkedList ps 的元素连接起来即可。此外,您可以添加更多操作,如 PointXY get(int index) 等,并在此组合中向底层 LinkedList 进行更多转发。最后,您应该在必要时使用过多的防御性副本来保护集合。为了简洁起见,我尽量保持简单。

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedList;

public class ContinuousLineSet implements Collection<PointXY> 
  LinkedList<PointXY> ps = new LinkedList<>(); // Exposed for simplicity
  private int fullPath = 0; // Wighted sum of all edges in ps

  @Override
  public int size() 
    return ps.size();
  

  @Override
  public boolean isEmpty() 
    return ps.isEmpty();
  

  @Override
  public boolean contains(Object o) 
    return ps.contains(o);
  

  @Override
  public Iterator<PointXY> iterator() 
    return ps.iterator();
  

  @Override
  public Object[] toArray() 
    return ps.toArray();
  

  @Override
  public <T> T[] toArray(T[] a) 
    return ps.toArray(a);
  

  private int dist(PointXY a, PointXY b) 
    return (a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y);
  

  @Override
  public boolean add(PointXY e) 
    if (isEmpty())
      return ps.add(e);

    if (ps.getFirst().equals(e))
      return false;

    Iterator<PointXY> it = ps.iterator();
    PointXY previous = it.next();
    int asFirst = fullPath + dist(e, previous);
    int minPath = asFirst;
    int iMin = 0;
    int i = 0;
    while (it.hasNext()) 
      i++;
      PointXY next = it.next();
      if (next.equals(e))
        return false;

      int asBetween = fullPath - dist(previous, next) + dist(previous, e) + dist(e, next);
      if (asBetween < minPath) 
        iMin = i;
        minPath = asBetween;
      

      previous = next;
    

    int asLast = fullPath + dist(e, previous);
    if (asLast < minPath) 
      minPath = asLast;
      iMin = size();
    

    fullPath = minPath;
    ps.add(iMin, e);
    return true;
  

  public void reverse() 
    Collections.reverse(ps);
  

  @Override
  public boolean remove(Object o) 
    PointXY last = null;
    for (Iterator<PointXY> it = iterator(); it.hasNext();) 
      PointXY p = it.next();
      if (o.equals(p)) 
        int part1 = last != null ? dist(last, p) : 0;
        int part2 = it.hasNext() ? dist(p, it.next()) : 0;
        fullPath -= part1 + part2;
        break;
      

      last = p;
    

    return ps.remove(o);
  

  @Override
  public boolean containsAll(Collection<?> c) 
    return ps.containsAll(c);
  

  @Override
  public boolean addAll(Collection<? extends PointXY> c) 
    boolean wasAdded = false;
    for (PointXY p : c) 
      wasAdded |= add(p);
    

    return wasAdded;
  

  @Override
  public boolean removeAll(Collection<?> c) 
    boolean wasRemoved = false;
    for (Object o : c) 
      if (o instanceof PointXY) 
        PointXY p = (PointXY) o;
        wasRemoved |= remove(p);
      
    

    return wasRemoved;
  

  @Override
  public boolean retainAll(Collection<?> c) 
    ContinuousLineSet cls = new ContinuousLineSet();

    for (Object o : c) 
      if (o instanceof PointXY && ps.contains(o)) 
        PointXY p = (PointXY) o;
        cls.add(p);
      
    

    int oldSize = ps.size();
    ps = cls.ps;
    fullPath = cls.fullPath;

    return size() != oldSize;
  

  @Override
  public void clear() 
    ps.clear();
    fullPath = 0;
     


class PointXY  
  public static PointXY of(int x, int y) 
    return new PointXY(x, y);
  

  public final int x, y;
  private int hash;
  private boolean wasHashInit = false;

  private PointXY(int x, int y) 
    this.x = x;
    this.y = y;
  

  @Override
  public boolean equals(Object obj) 
    if (!(obj instanceof PointXY))
      return false;
    PointXY p = (PointXY) obj;
    return x == p.x && y == p.y;
  

  @Override
  public int hashCode() 
    if (!wasHashInit) 
      hash = 17;
      hash = 31 * hash + y;
      hash = 31 * hash + x;
      wasHashInit = true;
    

    return hash;
  

  @Override
  public String toString() 
    return String.format("(%d, %d)", x, y);
  

【讨论】:

【参考方案7】:

公共类 Point 实现 Comparable

...

... @Override

public int compareTo(Pointarg0)

    ....

...

...

【讨论】:

以上是关于如何对一组点进行排序,以便它们一个接一个地设置?的主要内容,如果未能解决你的问题,请参考以下文章

在numpy中使用旋转矩阵有效地旋转一组点

将点列表排序为多边形

如何对一组 IP 地址 进行排序?

如何使用一个 VBO 绘制一组点?

如何通过 created_at 对一组行进行排序?

C语言链表中如何实现对一组数据进行排序?