【问题标题】:How can I improve this search algorithms runtime?如何改进此搜索算法运行时?
【发布时间】:2021-01-13 21:33:22
【问题描述】:

我正在尝试解决几年前为准备即将进行的面试而遇到的面试问题。该问题在 pdf here 中进行了概述。我使用 DFS 编写了一个简单的解决方案,该解决方案适用于文档中概述的示例,但我无法让程序满足标准

您的代码应该在一秒钟内产生正确的答案 10,000 x 10,000 Geo GeoBlock,包含 10,000 个被占用的 Geo。

为了测试这一点,我生成了一个包含 10000 个随机条目的 CSV 文件,当我针对它运行代码时,平均只需 2 秒多一点就能找到其中最大的地理块。除了在更快的笔记本电脑上运行它之外,我不确定可以对我的方法进行哪些改进以将运行时间减少一半以上。从我的调查来看,搜索本身似乎只需要大约 8 毫秒,所以也许我将数据加载到内存中的方式是效率低下的部分?

我非常感谢有关如何改进这一点的建议。见以下代码:

GeoBlockAnalyzer

package analyzer.block.geo.main;

import analyzer.block.geo.model.Geo;
import analyzer.block.geo.result.GeoResult;

import java.awt.*;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.*;

public class GeoBlockAnalyzer {

  private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
  private final int width;
  private final int height;
  private final String csvFilePath;
  private GeoResult result = new GeoResult();

  // Map of the geo id and respective geo object
  private final Map<Integer, Geo> geoMap = new HashMap<>();
  // Map of coordinates to each geo in the grid
  private final Map<Point, Geo> coordMap = new HashMap<>();

  /**
   * Constructs a geo grid of the given width and height, populated with the geo data provided in
   * the csv file
   *
   * @param width the width of the grid
   * @param height the height of the grid
   * @param csvFilePath the csv file containing the geo data
   * @throws IOException
   */
  public GeoBlockAnalyzer(final int width, final int height, final String csvFilePath)
      throws IOException {

    if (!Files.exists(Paths.get(csvFilePath)) || Files.isDirectory(Paths.get(csvFilePath))) {
      throw new FileNotFoundException(csvFilePath);
    }

    if (width <= 0 || height <= 0) {
      throw new IllegalArgumentException("Input height or width is 0 or smaller");
    }

    this.width = width;
    this.height = height;
    this.csvFilePath = csvFilePath;

    populateGeoGrid();
    populateCoordinatesMap();
    calculateGeoNeighbours();
    // printNeighbours();
  }

  /** @return the largest geo block in the input grid */
  public GeoResult getLargestGeoBlock() {
    for (final Geo geo : this.geoMap.values()) {
      final List<Geo> visited = new ArrayList<>();
      search(geo, visited);
    }
    return this.result;
  }

  /**
   * Iterative DFS implementation to find largest geo block.
   *
   * @param geo the geo to be evaluated
   * @param visited list of visited geos
   */
  private void search(Geo geo, final List<Geo> visited) {
    final Deque<Geo> stack = new LinkedList<>();
    stack.push(geo);
    while (!stack.isEmpty()) {
      geo = stack.pop();
      if (visited.contains(geo)) {
        continue;
      }
      visited.add(geo);

      final List<Geo> neighbours = geo.getNeighbours();
      for (int i = neighbours.size() - 1; i >= 0; i--) {
        final Geo g = neighbours.get(i);
        if (!visited.contains(g)) {
          stack.push(g);
        }
      }
    }
    if (this.result.getSize() < visited.size()) {
      this.result = new GeoResult(visited);
    }
  }

  /**
   * Creates a map of the geo grid from the csv file data
   *
   * @throws IOException
   */
  private void populateGeoGrid() throws IOException {
    try (final BufferedReader br = Files.newBufferedReader(Paths.get(this.csvFilePath))) {
      int lineNumber = 0;
      String line = "";
      while ((line = br.readLine()) != null) {
        lineNumber++;
        final String[] geoData = line.split(",");
        LocalDate dateOccupied = null;

        // Handle for empty csv cells
        for (int i = 0; i < geoData.length; i++) {
          // Remove leading and trailing whitespace
          geoData[i] = geoData[i].replace(" ", "");

          if (geoData[i].isEmpty() || geoData.length > 3) {
            throw new IllegalArgumentException(
                "There is missing data in the csv file at line: " + lineNumber);
          }
        }
        try {
          dateOccupied = LocalDate.parse(geoData[2], formatter);
        } catch (final DateTimeParseException e) {
          throw new IllegalArgumentException("There input date is invalid on line: " + lineNumber);
        }
        this.geoMap.put(
            Integer.parseInt(geoData[0]),
            new Geo(Integer.parseInt(geoData[0]), geoData[1], dateOccupied));
      }
    }
  }

  /** Create a map of each coordinate in the grid to its respective geo */
  private void populateCoordinatesMap() {
    // Using the geo id, calculate its point on the grid
    for (int i = this.height - 1; i >= 0; i--) {
      int blockId = (i * this.width);
      for (int j = 0; j < this.width; j++) {
        if (this.geoMap.containsKey(blockId)) {
          final Geo geo = this.geoMap.get(blockId);
          geo.setCoordinates(i, j);
          this.coordMap.put(geo.getCoordinates(), geo);
        }
        blockId++;
      }
    }
  }

  private void calculateGeoNeighbours() {
    for (final Geo geo : this.geoMap.values()) {
      addNeighboursToGeo(geo);
    }
  }

  private void addNeighboursToGeo(final Geo geo) {
    final int x = geo.getCoordinates().x;
    final int y = geo.getCoordinates().y;

    final Point[] possibleNeighbours = {
      new Point(x, y + 1), new Point(x - 1, y), new Point(x + 1, y), new Point(x, y - 1)
    };

    Geo g;
    for (final Point p : possibleNeighbours) {
      if (this.coordMap.containsKey(p)) {
        g = this.coordMap.get(p);
        if (g != null) {
          geo.getNeighbours().add(g);
        }
      }
    }
  }

  private void printNeighbours() {
    for (final Geo geo : this.geoMap.values()) {
      System.out.println("Geo " + geo.getId() + " has the following neighbours: ");
      for (final Geo g : geo.getNeighbours()) {
        System.out.println(g.getId());
      }
    }
  }
}

地理结果

package analyzer.block.geo.result;

import analyzer.block.geo.model.Geo;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class GeoResult {

    private final List<Geo> geosInBlock = new ArrayList<>();

    public GeoResult() {
    }

    public GeoResult(final List<Geo> geosInBlock) {
        this.geosInBlock.addAll(geosInBlock);
    }

    public List<Geo> getGeosInBlock() {
        this.geosInBlock.sort(Comparator.comparingInt(Geo::getId));
        return this.geosInBlock;
    }

    public int getSize() {
        return this.geosInBlock.size();
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("The geos in the largest cluster of occupied Geos for this GeoBlock are: \n");
        for(final Geo geo : this.geosInBlock) {
            sb.append(geo.toString()).append("\n");
        }
        return sb.toString();
    }
}

地理位置

package analyzer.block.geo.model;

import java.awt.Point;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class Geo {

    private final int id;
    private final String name;
    private final LocalDate dateOccupied;
    private final Point coordinate;
    private final List<Geo> neighbours = new ArrayList<>();

    public Geo (final int id, final String name, final LocalDate dateOccupied) {
        this.id = id;
        this.name = name;
        this.dateOccupied = dateOccupied;
        this.coordinate = new Point();
    }

    public int getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public LocalDate getDateOccupied() {
        return this.dateOccupied;
    }

    public void setCoordinates(final int x, final int y) {
        this.coordinate.setLocation(x, y);
    }

    public Point getCoordinates() {
        return this.coordinate;
    }

    public String toString() {
        return this.id + ", " + this.name + ", " + this.dateOccupied;
    }

    public List<Geo> getNeighbours() {
        return this.neighbours;
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.id, this.name, this.dateOccupied);
    }

    @Override
    public boolean equals(final Object obj) {
        if(this == obj) {
            return true;
        }

        if(obj == null || this.getClass() != obj.getClass()) {
           return false;
        }

        final Geo geo = (Geo) obj;
        return this.id == geo.getId() &&
                this.name.equals(geo.getName()) &&
                this.dateOccupied == geo.getDateOccupied();
    }
}

【问题讨论】:

  • 请在问题中包含问题描述,而不是链接。
  • 我认为这会使问题变得臃肿,因为这是一个相当长的问题描述,还需要需要上传的图表。这真的是首选方法吗?
  • 当然。因为链接可能会过期。看起来也规定不发布,但这个问题似乎已经违反了这一点。我想你可以试着掩盖一下语言。
  • 我发布它的唯一原因是因为我认为如果我复制并粘贴问题,我会因为使问题过于庞大而不是重点而被否决,所以我会这样做,谢谢。大约 3 年前我也收到了这个问题,所以我对分享这个问题并不感到难过,因为它现在已经很老了。
  • @ldog 为什么推荐位设置?我不熟悉它,也不知道它的作用或它对列表的优缺点。我也已经在问题中概述了搜索本身需要 8 毫秒,其余时间用于创建网格的内存表示。所以我已经为你完成了基本的分析和指示......

标签: java algorithm search optimization


【解决方案1】:

未经测试,在我看来,这里的主要块是地图的文字创建,最多可能有 100,000,000 个单元格。如果我们改为 labeled 每个 CSV 条目并有一个函数 getNeighbours(id, width, height) 返回可能的邻居 ID 列表(想想模块化算法),就没有必要这样做了。当我们依次迭代每个 CSV 条目时,如果 (1) 个邻居 ID 已经看到都具有相同的标签,我们将使用该标签标记新 ID;如果 (2) 没有看到邻居,我们将为新 ID 使用新标签;并且如果 (3) 在看到的邻居 ID 之间存在两个或更多不同的标签,我们会将它们组合成一个标签(比如最小标签),方法是将标签映射到其“最终”标签的哈希值。还存储每个标签的总和和大小。您当前的解决方案是O(n),其中nwidth x height。这里的想法是O(n),其中n 是被占用的地理区域的数量。

这里有一些 Python 中非常粗糙的东西,我不希望它处理所有场景,但希望能给你一个想法(抱歉,我不懂 Java):

def get_neighbours(id, width, height):
  neighbours = []

  if id % width != 0:
    neighbours.append(id - 1)
  if (id + 1) % width != 0:
    neighbours.append(id + 1)
  if id - width >= 0:
    neighbours.append(id - width)
  if id + width < width * height:
    neighbours.append(id + width)

  return neighbours

def f(data, width, height):
  ids = {}
  labels = {}
  current_label = 0
        
  for line in data:
    [idx, name, dt] = line.split(",")
    idx = int(idx)
    this_label = None
    neighbours = get_neighbours(idx, width, height)
    no_neighbour_was_seen = True

    for n in neighbours:
      # A neighbour was seen
      if n in ids:
        no_neighbour_was_seen = False

        # We have yet to assign a label to this ID
        if not this_label:
          this_label = ids[n]["label"]
          ids[idx] = {"label": this_label, "data": name + " " + dt}
          final_label = labels[this_label]["label"]
          labels[final_label]["size"] += 1
          labels[final_label]["sum"] += idx
          labels[final_label]["IDs"] += [idx]

        # This neighbour has yet to be connected
        elif ids[n]["label"] != this_label:
          old_label = ids[n]["label"]
          old_obj = labels[old_label]
          final_label = labels[this_label]["label"]
          ids[n]["label"] = final_label
          labels[final_label]["size"] += old_obj["size"]
          labels[final_label]["sum"] += old_obj["sum"]
          labels[final_label]["IDs"] += old_obj["IDs"]
          del labels[old_label]

    if no_neighbour_was_seen:
      this_label = current_label
      current_label += 1
      ids[idx] = {"label": this_label, "data": name + " " + dt}
      labels[this_label] = {"label": this_label, "size": 1, "sum": idx, "IDs": [idx]}

  for i in ids:
    print i, ids[i]["label"], ids[i]["data"]
  print ""
  for i in labels:
    print i
    print labels[i]

  return labels, ids
  
          
data = [
  "4, Tom, 2010-10-10",
  "5, Katie, 2010-08-24",
  "6, Nicole, 2011-01-09",
  "11, Mel, 2011-01-01",
  "13, Matt, 2010-10-14",
  "15, Mel, 2011-01-01",
  "17, Patrick, 2011-03-10",
  "21, Catherine, 2011-02-25",
  "22, Michael, 2011-02-25"
]

f(data, 4, 7)
print ""
f(data, 7, 4)

输出:

"""
4 0  Tom  2010-10-10
5 0  Katie  2010-08-24
6 0  Nicole  2011-01-09
11 1  Mel  2011-01-01
13 2  Matt  2010-10-14
15 1  Mel  2011-01-01
17 2  Patrick  2011-03-10
21 2  Catherine  2011-02-25
22 2  Michael  2011-02-25

0
{'sum': 15, 'size': 3, 'IDs': [4, 5, 6], 'label': 0}
1
{'sum': 26, 'size': 2, 'IDs': [11, 15], 'label': 1}
2
{'sum': 73, 'size': 4, 'IDs': [13, 17, 21, 22], 'label': 2}

---

4 0  Tom  2010-10-10
5 0  Katie  2010-08-24
6 0  Nicole  2011-01-09
11 0  Mel  2011-01-01
13 0  Matt  2010-10-14
15 3  Mel  2011-01-01
17 2  Patrick  2011-03-10
21 3  Catherine  2011-02-25
22 3  Michael  2011-02-25

0
{'sum': 39, 'size': 5, 'IDs': [4, 5, 6, 11, 13], 'label': 0}
2
{'sum': 17, 'size': 1, 'IDs': [17], 'label': 2}
3
{'sum': 58, 'size': 3, 'IDs': [21, 22, 15], 'label': 3}
"""

【讨论】:

  • @ciamej 这正是我的答案第二句中“标记”这个词所链接的内容。
  • 我正在尝试将您编写的内容重写为 Java,但我不熟悉 Python 语法。 final_label = labels[this_label]["label"] 是什么意思?我的解释是在数组labels 中获取索引this_label 处的元素并获取字段"label" 的值?但这听起来不对,因为this_label 是一个字符串,因此我不能用它来获取数组中的元素。你能解释一下这个语法到底是什么意思吗?
  • @Eoin 我猜labels 理论上可以构建为一个数组,其长度限制为占用的最大地理数。我们在 Python 代码中拥有的是 labels 是一个字典(我认为它需要一个 Map 类型),而 this_label 是地图上的键。那么键指向的值是另一个字典(所以另一个 Map),它有键、“标签”、“大小”、“ID”等。这有意义吗?
  • @Eoin final_label 的实际值是一个字符串,它是字典(映射)的键“标签”处的值,即labels[this_label] 的值。字典词典(Map of Maps)。
【解决方案2】:

这里可用的主要优化是概念性的。不幸的是,这种类型的优化不容易教授,也不容易在某处的参考资料中查找。这里使用的原理是:

使用分析公式计算已知结果(几乎总是)比(预)计算它便宜。 [1]

从您的代码和问题的定义中可以清楚地看出,您没有利用此原则和问题规范。特别是,直接从问题规范中提取的关键点之一是:

对于包含 10,000 个被占用的 Geos 的 10,000 x 10,000 GeoBlock,您的代码应该在一秒钟内产生正确的答案。

当您阅读此声明时,您应该会想到一些事情(考虑运行时效率时):

  • 10,000^2 比 10,000 大得多(正好大 10,000 倍!)如果您可以维持 O(n) 而不是 O(n^2) 的算法(在预期的情况,因为使用了散列。)
  • 触摸整个网格(即计算任何 O(1) 操作)将立即产生 O(n^2) 算法;显然,这是必须尽可能避免的事情
  • 从问题陈述中,我们不应该期望需要接触 O(n^2) 地理区域。这应该是编写问题的人在寻找什么的主要提示。 BFS 或 DFS 是一种 O(N+M) 算法,其中 N,M 是所触及的节点和边的数量。因此,我们应该期待 O(n) 搜索。
  • 基于以上几点,很明显,对于网格大小为 10,000 x 10,000 和 10,000 个地理区域的问题输入,此处寻找的解决方案应该是 O(10,000)

您提供的解决方案是 O(n^2),因为,

  1. 您使用visited.contains,其中visited 是一个列表。这在您的测试中没有显示为问题区域,因为我怀疑您使用的是小型地理集群。尝试使用大型地理集群(一个具有 10,000 个地理区域的集群)。与拥有 3 个地理区域的最大集群相比,您应该会看到速度大幅下降。这里的解决方案是为visited 使用有效的数据结构,我想到的是bit set(我不知道Java 是否有任何可用的,但任何体面的语言都应该)或哈希集(显然Java 有一些可用。)因为您在测试中没有注意到这一点,这向我表明您没有通过足够多的不同示例来充分审查/测试您的代码。这应该在对您的代码进行任何彻底的测试/分析时立即出现。根据我的评论,我希望在问题发布之前看到这种类型的基础工作/分析。
  2. 您触摸了函数/成员 populateCoordinatesMap 中的整个 10,000 x 10,000 网格。这显然已经是 O(n^2),其中 n=10,000。请注意,在populateCoordinatesMap 之外使用coordMap 的唯一位置是在addNeighboursToGeo 中。这是一个主要瓶颈,并且没有任何理由,addNeighboursToGeo 可以在 O(1) 时间内计算出来,而无需 coordMap。但是,我们仍然可以按原样使用您的代码,只需在下面进行小幅修改。

我希望如何解决 (1) 很明显。要修复 (2),请替换 populateCoordinatesMap

  /** Create a map of each coordinate in the grid to its respective geo */
  private void populateCoordinatesMap() {
   for (Map.Entry<int,Geo> entry : geoMap.entrySet()) {
     int key = entry.getKey();
     Geo value = entry.getValue();
     int x = key % this.width;
     int y = key / this.width;  
     value.setCoordinates(x, y);
     this.coordMap.put(geo.getCoordinates(), geo); 
   }
  }

注意这里使用的原则。不是像以前那样迭代整个网格(O(n^2) 立即),而是只迭代占用的 Geos,并使用分析公式来索引 2D 数组(而不是进行大量计算来计算同样的事情。)实际上,此更改将 populateCoordinatesMap 从 O(n^2) 提高到 O(n)。

以下一些一般的和固执己见的cmets:

  • 总的来说,我强烈反对针对这个问题使用面向对象的方法而不是过程方法。我认为 OO 方法对于这段代码应该多么简单是完全不合理的,但我理解面试官希望看到它。
  • 这是您试图解决的一个非常简单的问题,我认为您在此处采用的面向对象方法非常混乱,因此您无法只见树木不见森林(或者也许只见树木不见森林。) A即使使用面向对象的方法,也可以采用更简单的方法来实现该算法。
  • 从以上几点很清楚,您可以从了解您所使用的语言的可用工具中受益。我的意思是您应该知道哪些容器是现成的,以及在每个容器上使用每个操作的权衡是什么容器。如果您要研究优化代码,您还应该至少了解一种适用于您正在使用的语言的体面分析工具。鉴于您未能发布分析摘要,即使在我要求之后,它也向我表明您不知道这样的 Java 工具。学习一个。

[1] 我没有为这个原则提供参考,因为它是第一个原则,并且可以通过运行较少的恒定时间操作比运行许多更便宜的事实来解释。这里的假设是已知的解析形式需要较少的计算。这条规则偶尔会有例外。但应该强调的是,此类例外几乎总是因为硬件限制或优势。例如,在计算汉明距离时,在不访问 SSE 寄存器/操作的情况下,使用预先计算的 LUT 来计算硬件架构上的人口计数会更便宜。

【讨论】:

  • 非常感谢您的详细回复。为了解决您的一些观点:对于我的测试,我使用了一个包含 10000 个地理信息的网格。不过,我编写了一些代码来随机生成它,因为手动执行此操作需要很长时间,所以我无法控制邻居的间距。我用您建议的 populateCoordinatesMap 方法更新了我的代码,这解决了问题。代码现在运行时间为 246 毫秒,而不是超过 2 秒。
  • 我不明白代码int x = key % this.width; int y = key / this.width; 是如何工作的。也许是因为这是我不知道的数学问题,但是如何通过使用 id 和网格的宽度来计算地理的位置?为什么使用宽度而不是高度?抱歉,这似乎是一个基本问题,但如果我知道这一点,我一开始就不会使用 n^2 方法。
  • 我也很想知道您如何使addNeighboursToGeo O(1) 如您所说?我有什么明显的遗漏吗?
  • @Eoin,抱歉回复晚了。 addNeighboursToGeo 可以简单地进行所需的检查,而不是依赖于 geoMap 中包含的内容和不包含的内容。例如,它可以检查邻居当前是否被占用并且没有超出网格的边界。这将使它成为 O(1)。
  • xy 的公式来自反转此处描述的多维网格索引方案:en.wikipedia.org/wiki/… 在处理计算机中的多维数据时,该方案非常有用,非常有用推荐学习一下。
猜你喜欢
  • 1970-01-01
  • 2011-01-21
  • 2021-05-10
  • 1970-01-01
  • 2016-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多