当前位置:   article > 正文

Java中实现几种搜索算法一二分法、深度优先、广度优先、插值搜索算法_java 二分法查找算法

java 二分法查找算法

一、 高效搜索的必要性

假设我们从事葡萄酒销售业务,每天都有数百万买家访问我们的应用程序。

通过我们的应用程序,客户可以过滤掉价格低于 n 美元的商品,从搜索结果中选择一瓶,然后将它们添加到他的购物车中。我们有数以百万计的用户每秒都在寻找有价格限制的葡萄酒。结果需要快速。
在后端,我们的算法对整个葡萄酒列表进行线性搜索,将客户输入的价格限制与列表中每个葡萄酒瓶的价格进行比较。
然后,它返回价格小于或等于价格限制的项目。这种线性搜索的时间复杂度为 O(n)。
这意味着我们系统中的酒瓶数量越多,所需的时间就越多。搜索时间与引入的新项目数量成比例地增加。

如果我们开始按排序顺序保存项目并使用二进制搜索搜索项目,我们可以实现 O(log n) 的复杂度。

使用二进制搜索时,搜索结果所花费的时间会随着数据集的大小而自然增加,但不会成比例地增加。

二、Java 中的二分搜索算法

简单地说,算法将键值与数组的中间元素进行比较;如果它们不相等,则删除键不能属于的一半,并继续搜索剩余的一半,直到成功。

请记住 - 这里的关键方面是数组已经排序。

如果搜索结束时剩余的一半为空,则键不在数组中。

2.1. 迭代实现

public int runBinarySearchIteratively(
  int[] sortedArray, int key, int low, int high) {
    int index = Integer.MAX_VALUE;
    
    while (low <= high) {
        int mid = low  + ((high - low) / 2);
        if (sortedArray[mid] < key) {
            low = mid + 1;
        } else if (sortedArray[mid] > key) {
            high = mid - 1;
        } else if (sortedArray[mid] == key) {
            index = mid;
            break;
        }
    }
    return index;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

runBinarySearchIteratively 方法将 sortedArray、key 以及 sortedArray 的低索引和高索引作为参数。当该方法首次运行时,low(sortedArray 的第一个索引)为 0,而 high(sortedArray 的最后一个索引)等于其长度 – 1。

中间是 sortedArray 的中间索引。现在,该算法运行一个 while 循环,将键与 sortedArray 的中间索引的数组值进行比较。

**注意中间索引是如何生成的 (int mid = low + ((high – low) / 2)。这是为了适应非常大的阵列。**如果仅通过获取中间索引 (int mid = (low + high) / 2) 生成中间索引,则包含 2 的数组可能会发生溢出30或更多元素,因为低 + 高的总和很容易超过最大正 int 值。

2.2. 递归实现

现在,让我们看一下一个简单的递归实现:

public int runBinarySearchRecursively(
  int[] sortedArray, int key, int low, int high) {
    int middle = low  + ((high - low) / 2);
        
    if (high < low) {
        return -1;
    }

    if (key == sortedArray[middle]) {
        return middle;
    } else if (key < sortedArray[middle]) {
        return runBinarySearchRecursively(
          sortedArray, key, low, middle - 1);
    } else {
        return runBinarySearchRecursively(
          sortedArray, key, middle + 1, high);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

runBinarySearchRecursively 方法接受 sortedArray、键、sortedArray 的低索引和高索引。

2.3. 使用数组。binarySearch()

int index = Arrays.binarySearch(sortedArray, key);
  • 1

sortedArray 和 int 键(将在整数数组中搜索)作为参数传递给 Java Arrays 类的 binarySearch 方法。

2.4. 使用集合。binarySearch()

int index = Collections.binarySearch(sortedList, key);
  • 1

sortedList 和 Integer 键(将在 Integer 对象列表中搜索)作为参数传递给 Java Collections 类的 binarySearch 方法。

2.5. 性能

是使用递归还是迭代方法来编写算法,主要取决于个人喜好。 但是,我们仍然应该注意以下几点:

  1. 由于维护堆栈的开销,递归可能会变慢,并且通常会占用更多内存
    2.递归对堆栈不友好。在处理大数据集
    时可能会导致 StackOverflowException 3.递归增加了代码的清晰度,因为与迭代方法相比,它使代码更短

理想情况下,与线性搜索大值 n 相比,二叉搜索将执行较少的比较次数。对于较小的 n 值,线性搜索的性能可能优于二叉搜索。

人们应该知道,这种分析是理论性的,可能会因上下文而异。

此外,**二进制搜索算法需要一个排序的数据集,这也有其成本。**如果我们使用合并排序算法对数据进行排序,则代码中会添加 n log n 的额外复杂性。

因此,首先我们需要很好地分析我们的需求,然后决定哪种搜索算法最适合我们的需求。

三、Java 中的深度优先搜索

3.1. 概述

在本教程中,我们将探讨 Java 中的深度优先搜索。

深度优先搜索 (DFS) 是一种遍历算法,用于树形和图形数据结构。深度优先搜索深入每个分支,然后再探索另一个分支。

在接下来我们将首先了解 Tree 的实现,然后是 Graph。

要了解如何在 Java 中实现这些结构,请查看我们之前关于二叉树和图形的教程。

3.2. 树深度优先搜索

使用 DFS 遍历树有三种不同的顺序:

  1. 预购遍历
  2. 无序遍历
  3. Postorder 遍历

3.2.1. 预序遍历

在预序遍历中,我们首先遍历根,然后遍历左右子树。

我们可以简单地使用递归实现预序遍历:

  • 访问当前节点
  • 遍历左侧子树
  • 遍历右子树
public void traversePreOrder(Node node) {
    if (node != null) {
        visit(node.value);
        traversePreOrder(node.left);
        traversePreOrder(node.right);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们还可以实现无需递归的预序遍历。

**为了实现迭代的预订单遍历,我们需要一个堆栈,**我们将完成以下步骤:

  • 在我们的大头钉中推根
  • 虽然堆栈不为空
    • 弹出当前节点
    • 访问当前节点
    • 先推右子,再推左子堆叠
public void traversePreOrderWithoutRecursion() {
    Stack<Node> stack = new Stack<Node>();
    Node current = root;
    stack.push(root);
    while(!stack.isEmpty()) {
        current = stack.pop();
        visit(current.value);
        
        if(current.right != null) {
            stack.push(current.right);
        }    
        if(current.left != null) {
            stack.push(current.left);
        }
    }        
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3.2.2. 无序遍历

对于无序遍历,我们首先遍历左边的子树,然后是根,最后遍历右边的子树。

二叉搜索树的无序遍历意味着按节点值的递增顺序遍历节点。

我们可以简单地使用递归实现无序遍历:

public void traverseInOrder(Node node) {
    if (node != null) {
        traverseInOrder(node.left);
        visit(node.value);
        traverseInOrder(node.right);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

我们也可以在没有递归的情况下实现无序遍历:

  • 使用 root 初始化当前节点
  • 当 current 不为 null 或 stack 不为空时
    - 继续将左边的子节点推到堆栈上,直到我们到达当前节点最左边的子节点
    - 弹出并访问堆栈中最左侧的节点
    - 将 current 设置为弹出节点的右侧子节点
public void traverseInOrderWithoutRecursion() {
    Stack stack = new Stack<>();
    Node current = root;

    while (current != null || !stack.isEmpty()) {
        while (current != null) {
            stack.push(current);
            current = current.left;
        }

        Node top = stack.pop();
        visit(top.value);
        current = top.right;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

3.2.3. Postorder 遍历

最后,在后序遍历中,我们在遍历根之前遍历了左边和右边的子树。

我们可以遵循前面的递归解决方案

public void traversePostOrder(Node node) {
    if (node != null) {
        traversePostOrder(node.left);
        traversePostOrder(node.right);
        visit(node.value);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

或者,我们也可以实现没有递归的后序遍历:

  • 在 stack 中推送根节点
  • 虽然 s大头钉不是空的
    - 检查我们是否已经遍历了左右子树
    - 如果没有,则将右子项和左子项推到堆栈上
public void traversePostOrderWithoutRecursion() {
    Stack<Node> stack = new Stack<Node>();
    Node prev = root;
    Node current = root;
    stack.push(root);

    while (!stack.isEmpty()) {
        current = stack.peek();
        boolean hasChild = (current.left != null || current.right != null);
        boolean isPrevLastChild = (prev == current.right || 
          (prev == current.left && current.right == null));

        if (!hasChild || isPrevLastChild) {
            current = stack.pop();
            visit(current.value);
            prev = current;
        } else {
            if (current.right != null) {
                stack.push(current.right);
            }
            if (current.left != null) {
                stack.push(current.left);
            }
        }
    }   
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

3.3. 图深度优先搜索

图形和树之间的主要区别在于图形可能包含周期。

因此,为了避免循环搜索,我们将在访问每个节点时对其进行标记。

我们将看到图形 DFS 的两种实现,一种是递归,另一种是不带递归。

3.3.1. 使用递归绘制 DFS

首先,让我们从递归开始:

  • 我们将从一个给定的节点开始
  • 将当前节点标记为已访问
  • 访问当前节点
  • 遍历未访问的相邻顶点
public boolean[] dfs(int start) {
    boolean[] isVisited = new boolean[adjVertices.size()];
    return dfsRecursive(start, isVisited);
}

private boolean[] dfsRecursive(int current, boolean[] isVisited) {
    isVisited[current] = true;
    visit(current);
    for (int dest : adjVertices.get(current)) {
        if (!isVisited[dest])
            dfsRecursive(dest, isVisited);
    }
    return isVisited;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

3.3.2. 不递归的图形DFS

我们还可以在没有递归的情况下实现图形 DFS。我们只需使用一个 Stack:

  • 我们将从一个给定的节点开始
  • 将启动节点推送到堆栈中
  • 虽然堆栈不为空
    - 将当前节点标记为已访问
    - 访问当前节点
    - 推送未访问的相邻顶点
public void dfsWithoutRecursion(int start) {
    Stack<Integer> stack = new Stack<Integer>();
    boolean[] isVisited = new boolean[adjVertices.size()];
    stack.push(start);
    while (!stack.isEmpty()) {
        int current = stack.pop();
        if(!isVisited[current]){
            isVisited[current] = true;
            visit(current);
            for (int dest : adjVertices.get(current)) {
                if (!isVisited[dest])
                    stack.push(dest);
            }
    }
    return isVisited;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

3.3.4. 拓扑排序

图深度优先搜索有很多应用。DFS 的著名应用之一是拓扑排序。

有向图的拓扑排序是其顶点的线性排序,因此对于每条边,源节点都位于目标节点之前。

为了进行拓扑排序,我们需要对刚刚实现的 DFS 进行简单的添加:

  • 我们需要将访问的顶点保留在堆栈中,因为拓扑排序是以相反顺序访问的顶点
  • 只有在遍历所有邻居之后,我们才会将访问的节点推送到堆栈中
public List<Integer> topologicalSort(int start) {
    LinkedList<Integer> result = new LinkedList<Integer>();
    boolean[] isVisited = new boolean[adjVertices.size()];
    topologicalSortRecursive(start, isVisited, result);
    return result;
}

private void topologicalSortRecursive(int current, boolean[] isVisited, LinkedList<Integer> result) {
    isVisited[current] = true;
    for (int dest : adjVertices.get(current)) {
        if (!isVisited[dest])
            topologicalSortRecursive(dest, isVisited, result);
    }
    result.addFirst(current);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

四、Java 中的广度优先搜索算法

4.1. 概述

在本文中,我们将学习广度优先搜索算法,该算法允许我们通过遍历广度优先而不是深度优先的节点来搜索树或图中的节点。

首先,我们将介绍一些关于这种树和图算法的理论。之后,我们将深入研究 Java 中算法的实现。最后,我们将介绍它们的时间复杂度。

4.2. 广度优先搜索算法

广度优先搜索 (BFS) 算法的基本方法是通过先探索子节点之前的邻居,在树或图形结构中搜索节点。

首先,我们将看看这个算法是如何用于树木的。之后,我们将它适应图形,这些图形具有有时包含周期的特定约束。最后,我们将讨论该算法的性能。

4.2.1. 树

BFS 树算法背后的思想是**维护一个节点队列,以确保遍历顺序。**在算法开始时,队列仅包含根节点。只要队列仍包含一个或多个节点,我们就会重复这些步骤:

  • 从队列中弹出第一个节点
  • 如果该节点是我们正在搜索的节点,则搜索结束
  • 否则,请将此节点的子节点添加到队列的末尾,然后重复这些步骤

**没有周期可以确保执行终止。**我们将在下一节中看到如何管理周期。

4.2.2. 图表

在图的情况下,我们必须考虑结构中可能的循环。如果我们简单地将前面的算法应用于具有循环的图形上,它将永远循环。因此,我们需要保留访问节点的集合,并确保我们不会访问它们两次:

  • 从队列中弹出第一个节点
  • 检查节点是否已被访问,如果是,请跳过它
  • 如果该节点是我们正在搜索的节点,则搜索结束
  • 否则,将其添加到访问的节点
  • 将此节点的子节点添加到队列中,然后重复这些步骤

4.3.在 Java 中实现

现在已经涵盖了理论,让我们开始编写代码并在 Java 中实现这些算法!

4.3.1. 树

首先,我们将实现树算法。让我们设计一下 Tree 类,它由一个值和由其他 Tree列表表示的子类组成:

public class Tree<T> {
    private T value;
    private List<Tree<T>> children;

    private Tree(T value) {
        this.value = value;
        this.children = new ArrayList<>();
    }

    public static <T> Tree<T> of(T value) {
        return new Tree<>(value);
    }

    public Tree<T> addChild(T value) {
        Tree<T> newChild = new Tree<>(value);
        children.add(newChild);
        return newChild;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

为了避免创建循环,子级由类本身根据给定值创建。

之后,让我们提供一个 search() 方法:

public static <T> Optional<Tree<T>> search(T value, Tree<T> root) {
    //...
}
  • 1
  • 2
  • 3

如前所述,**BFS 算法使用队列来遍历节点。**首先,我们将根节点添加到此队列中:

Queue<Tree<T>> queue = new ArrayDeque<>();
queue.add(root);
  • 1
  • 2

然后,我们必须在队列不为空时循环,每次我们从队列中弹出一个节点时:

while(!queue.isEmpty()) {
    Tree<T> currentNode = queue.remove();
}
  • 1
  • 2
  • 3

如果该节点是我们正在搜索的节点,则返回它,否则将其子节点添加到队列中:

if (currentNode.getValue().equals(value)) {
    return Optional.of(currentNode);
} else {
    queue.addAll(currentNode.getChildren());
}
  • 1
  • 2
  • 3
  • 4
  • 5

最后,如果我们访问了所有节点,但没有找到要搜索的节点,则返回一个空结果:

return Optional.empty();
  • 1

现在让我们想象一个示例树结构:

树示例

这翻译成 Java 代码:

Tree<Integer> root = Tree.of(10);
Tree<Integer> rootFirstChild = root.addChild(2);
Tree<Integer> depthMostChild = rootFirstChild.addChild(3);
Tree<Integer> rootSecondChild = root.addChild(4);
  • 1
  • 2
  • 3
  • 4

然后,如果搜索值 4,我们希望算法按以下顺序遍历值为 10、2 和 4 的节点:

BreadthFirstSearchAlgorithm.search(4, root)
  • 1

我们可以通过记录访问节点的值来验证:

[main] DEBUG  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 10
[main] DEBUG  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 2 
[main] DEBUG  c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 4
  • 1
  • 2
  • 3

4.3.2. 图表

树木的情况到此结束。现在让我们看看如何处理图形。**与树相反,图形可以包含循环。**这意味着,正如我们在上一节中看到的那样,**我们必须记住我们访问的节点以避免无限循环。**我们稍后将看到如何更新算法来考虑这个问题,但首先,让我们定义我们的图结构:

public class Node<T> {
    private T value;
    private Set<Node<T>> neighbors;

    public Node(T value) {
        this.value = value;
        this.neighbors = new HashSet<>();
    }

    public void connect(Node<T> node) {
        if (this == node) throw new IllegalArgumentException("Can't connect node to itself");
        this.neighbors.add(node);
        node.neighbors.add(this);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

现在,我们可以看到,与树相反,我们可以自由地将一个节点与另一个节点连接起来,从而有可能创建循环。唯一的例外是节点无法连接到自身。

还值得注意的是,在这种表示中,没有根节点。这不是问题,因为我们还使节点之间的连接是双向的。这意味着我们将能够从任何节点开始搜索图形。

首先,让我们重用上面的算法,适应新的结构:

public static <T> Optional<Node<T>> search(T value, Node<T> start) {
    Queue<Node<T>> queue = new ArrayDeque<>();
    queue.add(start);

    Node<T> currentNode;

    while (!queue.isEmpty()) {
        currentNode = queue.remove();

        if (currentNode.getValue().equals(value)) {
            return Optional.of(currentNode);
        } else {
            queue.addAll(currentNode.getNeighbors());
        }
    }

    return Optional.empty();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

我们不能像这样运行算法,否则任何循环都会让它永远运行。因此,我们必须添加指令来处理已经访问过的节点:

while (!queue.isEmpty()) {
    currentNode = queue.remove();
    LOGGER.debug("Visited node with value: {}", currentNode.getValue());

    if (currentNode.getValue().equals(value)) {
        return Optional.of(currentNode);
    } else {
        alreadyVisited.add(currentNode);
        queue.addAll(currentNode.getNeighbors());
        queue.removeAll(alreadyVisited);
    }
}

return Optional.empty();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

正如我们所看到的,我们首先初始化一个 Set,它将包含访问的节点。

Set<Node<T>> alreadyVisited = new HashSet<>();
  • 1

然后,当值比较失败时,我们将节点添加到访问的节点中:

alreadyVisited.add(currentNode);
  • 1

最后,在将节点的邻居添加到队列后,我们从中删除已访问的节点(这是检查当前节点是否存在于该集合中的另一种方法):

queue.removeAll(alreadyVisited);
  • 1

通过这样做,我们确保算法不会陷入无限循环。

让我们通过一个例子看看它是如何工作的。首先,我们将定义一个带有循环的图形:

图形示例

在 Java 代码中也是如此:

Node<Integer> start = new Node<>(10);
Node<Integer> firstNeighbor = new Node<>(2);
start.connect(firstNeighbor);

Node<Integer> firstNeighborNeighbor = new Node<>(3);
firstNeighbor.connect(firstNeighborNeighbor);
firstNeighborNeighbor.connect(start);

Node<Integer> secondNeighbor = new Node<>(4);
start.connect(secondNeighbor);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

再说一次,我们要搜索值 4。由于没有根节点,我们可以从任何我们想要的节点开始搜索,我们将选择 firstNeighborNeighbor:

BreadthFirstSearchAlgorithm.search(4, firstNeighborNeighbor);
  • 1

同样,我们将添加一个日志来查看访问了哪些节点,我们预计它们是 3、2、10 和 4,每个节点按该顺序只访问一次:

[main] DEBUG c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 3 
[main] DEBUG c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 2 
[main] DEBUG c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 10 
[main] DEBUG c.b.a.b.BreadthFirstSearchAlgorithm - Visited node with value: 4
  • 1
  • 2
  • 3
  • 4

4.3.3. 复杂性

现在我们已经介绍了 Java 中的这两种算法,让我们来谈谈它们的时间复杂度。我们将使用 Big-O 符号来表达它们。

让我们从树算法开始。它最多向队列添加一次节点,因此最多访问一次。因此,如果 n 是树中的节点数,则算法的时间复杂度将为 O(n)。

现在,对于图算法,事情有点复杂。我们最多遍历每个节点一次,但为此,我们将使用具有线性复杂性的操作,例如 addAll() 和 removeAll()。

让我们考虑 n 个节点数和 c 个图的连接数。然后,在最坏的情况下(没有找到节点),我们可以使用 addAll() 和 removeAll() 方法来添加和删除节点,最多可以添加和删除连接数,从而为这些操作提供 O(c) 的复杂性。**因此,假设 c > n,则整个算法的复杂度将为 O(c)。否则,它将是 O(n)。**这通常被称为 O(n + c),它可以解释为取决于 n 和 c 之间的最大数字的复杂度。

为什么我们在树搜索中没有这个问题?因为树中的连接数受节点数的限制。n 个节点树中的连接数为 n – 1。

五、Java 中的插值搜索

插值搜索是对为均匀分布数据量身定制的二进制搜索的改进。

无论数据分布如何,二进制搜索都会将每一步的搜索空间减半,因此它的时间复杂度始终为 O(log(n))。

另一方面,**插值搜索时间复杂度因数据分布而异。**对于时间复杂度为 O(log(log(n)))) 的均匀分布数据,它比二进制搜索更快。然而,在最坏的情况下,它的性能可能与 O(n) 一样差。

5.1. 插值搜索

与二进制搜索类似,插值搜索只能对排序数组起作用。它在每次迭代时将探测器放置在计算位置。如果探头正好在我们要找的物品上,则返回位置;否则,搜索空间将限制在探头的右侧或左侧。

探针位置计算是二叉搜索和插值搜索之间的唯一区别。

在二进制搜索中,探测位置始终是剩余搜索空间的最中间项。

相反,插值搜索根据以下公式计算探针位置:
在这里插入图片描述
让我们看一下每个术语:

  • 探头:新的探头位置将分配给此参数。
  • lowEnd:当前搜索空间中最左边的项的索引。
  • highEnd:当前搜索空间中最右边的项的索引。
  • data[]:包含原始搜索空间的数组。
  • 项目:我们要查找的项目。

为了更好地理解插值搜索的工作原理,让我们通过一个示例来演示它。

假设我们想在下面的数组中找到 84 的位置:
在这里插入图片描述

数组的长度为 8,因此最初 highEnd = 7 和 lowEnd = 0(因为数组的索引从 0 开始,而不是从 1 开始)。

在第一步中,探针位置公式将导致探针 = 5:

第 1 步

由于 84(我们正在寻找的项目)大于 73(当前探测位置项目),因此下一步将通过分配 lowEnd = probe + 1 来放弃数组的左侧。现在搜索空间仅由 84 和 101 组成。探针位置公式将设置 probe = 6,这正好是 84 的索引:

步骤 2

由于我们正在寻找的项目已找到,因此将返回位置 6。

5.2. 在 Java 中实现

现在我们已经了解了该算法的工作原理,让我们在 Java 中实现它。

首先,我们初始化 lowEnd 和 highEnd:

int highEnd = (data.length - 1);
int lowEnd = 0;
  • 1
  • 2

接下来,我们设置一个循环,在每次迭代中,我们根据上述公式计算新探针。循环条件通过将项目与 data[lowEnd] 和 data[highEnd] 进行比较来确保我们没有超出搜索空间:

while (item >= data[lowEnd] && item <= data[highEnd] && lowEnd <= highEnd) {
    int probe
      = lowEnd + (highEnd - lowEnd) * (item - data[lowEnd]) / (data[highEnd] - data[lowEnd]);
}
  • 1
  • 2
  • 3
  • 4

我们还会在每次新的探测分配后检查是否找到了该项目。

最后,我们调整 lowEnd 或 highEnd 以减少每次迭代的搜索空间:

public int interpolationSearch(int[] data, int item) {

    int highEnd = (data.length - 1);
    int lowEnd = 0;

    while (item >= data[lowEnd] && item <= data[highEnd] && lowEnd <= highEnd) {

        int probe
          = lowEnd + (highEnd - lowEnd) * (item - data[lowEnd]) / (data[highEnd] - data[lowEnd]);

        if (highEnd == lowEnd) {
            if (data[lowEnd] == item) {
                return lowEnd;
            } else {
                return -1;
            }
        }

        if (data[probe] == item) {
            return probe;
        }

        if (data[probe] < item) {
            lowEnd = probe + 1;
        } else {
            highEnd = probe - 1;
        }
    }
    return -1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/476083
推荐阅读
相关标签
  

闽ICP备14008679号