【问题标题】:Binary search when a part of array was reversed当数组的一部分被反转时的二进制搜索
【发布时间】:2024-04-27 19:50:02
【问题描述】:

给定一个数组(大小 N)(已排序,但其中的某些部分被颠倒了),以及一系列元素(Q),我们必须根据元素是否存在于数组中来输出是/否。

我想出的解决方案如下:

  1. 进行线性遍历并找出数组反转的索引。
  2. 对于元素的每个查询,对 3 个子数组(第一个子数组、反转子数组和剩余子数组)调用二分查找

时间复杂度是O(N + Q* log N).不知道我们能不能在O(Q * Log N)中做到避免第一步?

【问题讨论】:

  • Q 是要检查的元素个数是否在数组中?
  • 你在什么情况下不得不处理这种奇怪的有序数据?还是这是作业?
  • @MrSmith42 是的。 Q是数量元素。这是一道面试题
  • 如果Q >= N,则对数组进行排序将花费[N * log N],所有Q 搜索的总成本将为[Q * log N]。个人搜索 = [log N]
  • 可以重复吗?

标签: arrays algorithm search binary-search


【解决方案1】:

可以在log(n)中完成:

主要有 3 种情况:反转子数组共享两半(1 5 4 3 2 6 7)或完全位于前半部分(1 3 2 4 5 6 7) 或下半场 (1 2 3 4 6 5 7)。

1) 最初,只需进行正常的二分搜索并继续搜索直到任一

(i) 你找到了需要的元素或
(ii) 您遇到反转子数组(即,当索引 (l+h)/2 处的元素小于 prev_element 且大于 next_element 时)。
如果遇到反向子数组,则继续2)。

2) 当反向子数组共享两半时,即切穿中间索引时,这就是 枢轴数组方法的情况。对数组的前半部分和后半部分应用下面提到的透视数组方法,并查看是否找到元素。
2*log(n) ~ log(n)。
确保也处理极端情况。

总体时间复杂度:log(n)


透视数组方法:

请记住,数组的一半仍将被排序。 所以,你仍然可以通过稍微修改二分搜索找到log(n)中的元素。

Input arr[] = {3, 4, 5, 1, 2}
Element to Search = 1
 1) Find middle point mid = (l + h)/2
 2) If key is present at middle point, return mid.
 3) Else If arr[l..mid] is sorted
    a) If key to be searched lies in range from arr[l]
       to arr[mid], recur for arr[l..mid].
    b) Else recur for arr[mid+1..r]
 4) Else (arr[mid+1..r] must be sorted)
    a) If key to be searched lies in range from arr[mid+1]
       to arr[r], recur for arr[mid+1..r].
    b) Else recur for arr[l..mid] 

关于www.geeksforgeeks.org/search-an-element-in-a-sorted-and-pivoted-array/的非常好的文章

【讨论】:

  • 如果数组是 {1, 2, 4, 3 , 5}
  • 不读取区间内所有元素怎么知道“如果arr[l..mid]是排序的”
  • 'reversed''rotated' 不同。我们也不知道 lh。问题是我们是否真的需要搜索它们,因为这可能需要 O(n) 时间。
  • @MrSmith42 我们通过二分搜索找到 l 和 h。查看更新的答案
  • @JerryGoyal:我不明白如何使用二分搜索找到 l 和 h。您无法知道它们中的每一个是枢轴元素的左侧还是右侧。
【解决方案2】:

我想我找到了解决办法。首先,我们需要某种找到断点的方法,我的意思是:假设我们有一个数组 {1,2,3,4,9,8,7,6} 注意数字 9在索引 4 处,这是一个断点,因为现在更改了排序方向,重要的部分是在 log(n) 中找到它。为此,我创建了以下类。为了让这个类工作,他需要获取一个数组以下类型 {sorted,reverseSorted}

public class BreakPointSearcher {
private int array[];
private BiFunction<Integer, Integer, Boolean> compare;
private Map<Boolean, Direction> directions;

public BreakPointSearcher(int[] array, BiFunction<Integer, Integer, Boolean> compare) {
    this.array = array;
    this.compare = compare;
}

public int findTheBreackingPoint(int start, int end) {
    if (start >= end || end >= array.length)
        return -1;

    loadDirectionMap(start, end);
    int mid = (start + end) / 2;
    if (isBreackingPoint(mid))
        return mid;

    Direction direction = getDirection(mid);
    //Using recursive call by that making the algorithm run in O(log(n))
    if (GoRight == direction) {
        return findTheBreackingPoint(mid + 1, end);
    } else if (GoLeft == direction) {
        return findTheBreackingPoint(start, mid - 1);
    }

    return -1;
}

/**
 * I assume that two sides of the array have opposite sorting
 * that is if at the start we have ascending at the end we have descending
 */
private void loadDirectionMap(int start, int end) {
    directions = new HashMap<>();
    directions.put(compare.apply(array[start], array[start + 1]), GoRight);
    directions.put(compare.apply(array[end - 1], array[end]), GoLeft);
}

private Direction getDirection(int mid) {
    return directions.get(compare.apply(array[mid], array[mid + 1]));
}

private boolean isBreackingPoint(int mid) {
    boolean inBetween = compare.apply(array[mid - 1], array[mid]) && compare.apply(array[mid + 1], array[mid]); // a > b < c
    boolean inBetweenTypeTwo = compare.apply(array[mid], array[mid - 1]) && compare.apply(array[mid], array[mid + 1]); // a < b > c
    return inBetween || inBetweenTypeTwo;
}

enum Direction {
    GoLeft,
    GoRight
}

public static void main(String... args) {
    int[] arrayNums = {1, 2, 3, 4, 5, 6, 7, 20, 9, 8};
    BiFunction<Integer, Integer, Boolean> compareAsc = (a, b) -> a > b;
    BreakPointSearcher sbs = new BreakPointSearcher(arrayNums, compareAsc);
    System.out.println(sbs.findTheBreackingPoint(0, arrayNums.length - 1)); //output is 7

    arrayNums = new int[]{252, 48, 22, 10, 12, 13, 16};
    sbs = new BreakPointSearcher(arrayNums, compareAsc);
    System.out.println(sbs.findTheBreackingPoint(0, arrayNums.length - 1)); //output 3

    arrayNums = new int[]{22, 56, 13};
    sbs = new BreakPointSearcher(arrayNums, compareAsc);
    System.out.println(sbs.findTheBreackingPoint(0, arrayNums.length - 1)); //output 1

    arrayNums = new int[]{22, 56, 78, 33};
    sbs = new BreakPointSearcher(arrayNums, compareAsc);
    System.out.println(sbs.findTheBreackingPoint(0, arrayNums.length - 1)); //output 2

    arrayNums = new int[]{1, 2, 3, 4};
    sbs = new BreakPointSearcher(arrayNums, compareAsc);
    System.out.println(sbs.findTheBreackingPoint(0, arrayNums.length - 1)); //output -1

    arrayNums = new int[]{4, 3, 2, 1};
    sbs = new BreakPointSearcher(arrayNums, compareAsc);
    System.out.println(sbs.findTheBreackingPoint(0, arrayNums.length - 1)); //output -1

}

}

现在我们有了这样的类,我们可以开始二进制搜索我要做的是搜索键,如果找到它,则函数完成如果在某个时候算法注意到排序被颠倒它会去在该点的左侧和右侧,然后将搜索反转数组的边界,然后我们在每个数组中都有 3 个数组,我们执行二进制搜索(其中一个是反向的),就是这样。这不是一个完美的解决方案并且可能会在某些特殊情况下,但作为一般 idia,我认为这是正确的

public class SpecialBinarySearch {

private static BiFunction<Integer, Integer, Boolean> ascComp = (a, b) -> a > b;
private static BiFunction<Integer, Integer, Boolean> dscComp = (a, b) -> a < b;

public static int search(int[] array, int key, int start, int end, BiFunction<Integer, Integer, Boolean> compare) {
    int mid = (start + end) / 2;
    if (array[mid] == key)
        return mid;
    if (start == end)
        return -1;

    boolean opositeSorted = compare.apply(array[mid], array[mid + 1]);
    if (opositeSorted) {
        BreakPointSearcher breakPointSearcher = new BreakPointSearcher(array, compare);
        int leftBound = breakPointSearcher.findTheBreackingPoint(0, mid);
        int rightBound = breakPointSearcher.findTheBreackingPoint(mid, array.length - 1);

        leftBound = leftBound == -1 ? 0 : leftBound; //
        rightBound = rightBound == -1 ? array.length - 1 : rightBound;

        int opt1 = search(array, key, leftBound, rightBound, getOpositeCompare(compare));
        int opt2 = search(array, key, start, leftBound, compare);
        int opt3 = search(array, key, rightBound, end, compare);

        return Math.max(Math.max(opt1, opt2), opt3);
    }

    if (compare.apply(key, array[mid])) {
        return search(array, key, mid + 1, end, compare);
    } else {
        return search(array, key, start, mid - 1, compare);
    }
}

private static BiFunction<Integer, Integer, Boolean> getOpositeCompare(BiFunction<Integer, Integer, Boolean> compare) {
    return compare == ascComp ? dscComp : ascComp;
}

public static void main(String... args) {
    int[] array = {1, 2, 3, 20, 25};
    System.out.println(search(array, 20, 0, array.length - 1, ascComp)); //output 3

    array = new int[]{10, 9, 8, 7, 6, 3};
    System.out.println(search(array, 6, 0, array.length - 1, dscComp));//output 4

    array = new int[]{1, 2, 3, 4, 5, 20, 19, 18, 17, 16};
    System.out.println(search(array, 18, 0, array.length - 1, ascComp));//output 7

    array = new int[]{1, 2, 3, 4, 5, 20, 19, 18, 22, 25};
    System.out.println(search(array, 22, 0, array.length - 1, ascComp));//output 8

    array = new int[]{1, 2, 3, 4, 5, 20, 19, 18, 17, 16};
    System.out.println(search(array, 16, 0, array.length - 1, ascComp));//output 9

    array = new int[]{1, 2, 3, 4, 5, 20, 19, 18, 17, 16};
    System.out.println(search(array, 2, 0, array.length - 1, ascComp));//output 1

}

}

【讨论】:

    【解决方案3】:

    你知道数组由三部分组成,升序、降序、升序。所以你可以通过采样找到这两个临界点。这有点棘手,一旦你在反转部分找到一个点,它就很容易了(只需两个二进制搜索)。找到反向部分更难。如果我们采样并且 array[i+1]

    【讨论】:

    • 如果我们不走运,这会导致我们查看每个元素 O(n)。因此,在最坏的情况下,线性查看数组以找到 reveres 区间没有任何改进。