使用二分查找在有重复元素的已排序数组中

31

我被分配创建一种方法,将打印出在已排序数组中发现值x的所有索引。

我知道,如果我们只是从0到N(数组长度)扫描数组,它的运行时间将是最坏情况下的O(n)。由于将传递到该方法中的数组是排序的,我假设可以利用二分查找,因为这将是O(log n)。然而,这仅适用于具有唯一值的数组。由于二分查找将在第一个特定值的“查找”后完成。我想过在已排序数组中进行二进制搜索以查找x,然后检查此索引之前和之后的所有值,但是如果数组包含所有x值,似乎不会更好。

我的问题是,是否有一种更好的方法可以查找排序数组中特定值的所有索引,而不是O(n)?

public void PrintIndicesForValue42(int[] sortedArrayOfInts)
{
    // search through the sortedArrayOfInts

    // print all indices where we find the number 42. 
}

例如: sortedArray = { 1, 13, 42, 42, 42, 77, 78 } 将打印: "42出现在索引2、3、4"


你的解决方案听起来不错,如果数组包含所有的x值,无论如何你都必须查看它们中的所有值。 - jlordo
@JonSkeet - 很抱歉那个打字错误。我已经将数组更新为排序后的。 - 5StringRyan
10个回答

45

您将在O(lg n)时间内获得结果。

public static void PrintIndicesForValue(int[] numbers, int target) {
    if (numbers == null)
        return;

    int low = 0, high = numbers.length - 1;
    // get the start index of target number
    int startIndex = -1;
    while (low <= high) {
        int mid = (high - low) / 2 + low;
        if (numbers[mid] > target) {
            high = mid - 1;
        } else if (numbers[mid] == target) {
            startIndex = mid;
            high = mid - 1;
        } else
            low = mid + 1;
    }

    // get the end index of target number
    int endIndex = -1;
    low = 0;
    high = numbers.length - 1;
    while (low <= high) {
        int mid = (high - low) / 2 + low;
        if (numbers[mid] > target) {
            high = mid - 1;
        } else if (numbers[mid] == target) {
            endIndex = mid;
            low = mid + 1;
        } else
            low = mid + 1;
    }

    if (startIndex != -1 && endIndex != -1){
        for(int i=0; i+startIndex<=endIndex;i++){
            if(i>0)
                System.out.print(',');
            System.out.print(i+startIndex);
        }
    }
}

27

如果您实际上拥有一个已排序的数组,那么您可以进行二进制搜索,直到找到其中一个索引,然后从那里开始,其余的应该很容易找到,因为它们都挨着。

一旦找到第一个,然后再找到它之前的所有实例,然后再找到它之后的所有实例。

使用该方法,您应该获得大约 O(lg(n)+k),其中k是您正在查找的值的出现次数。

编辑:

不,您将永远无法在少于O(k)的时间内访问所有k个值。


第二次编辑:为了让我感觉自己真正做出了一些有用的贡献:

与其仅搜索X的第一个和最后一个出现位置,不如对第一个出现和最后一个出现分别进行二进制搜索。这将导致总共O(lg(n))。一旦完成,您将知道所有介于索引之间的值也包含X(假设已排序)。

您可以通过检查值是否等于x,并检查左侧(或右侧,具体取决于您正在寻找第一个出现还是最后一个出现)的值是否等于x来执行此操作。


4
这个解决方案已经在问题中说明了,对吧? - akaIDIOT
@akaIDIOT 不,他在问题中提出的解决方案是从索引0开始进行线性扫描,直到找到他正在寻找的值之后的最后一个索引,这是O(n)并且是线性搜索,而不是二进制搜索。此答案中的解决方案是二进制搜索,然后是线性扫描,但线性扫描仅发生在数组的子部分上。 - Brian
1
@Brian,从问题中可以看出:“我在考虑使用二分查找来查找已排序数组中的x值,然后检查该索引之前和之后的所有值,但是如果数组包含所有x值,似乎并没有那么好。”--听起来就像你发布的内容。 - akaIDIOT
@akaIDIOT,实际上第二次阅读后,是的,他在问题中确实提到了那一点。我现在会进行编辑以反映这一点。 - Sam I am says Reinstate Monica
+1 如果能在 O(lg(n)) 的时间复杂度内完成,我应该多加思考。 - user789327

3
public void PrintIndicesForValue42(int[] sortedArrayOfInts) {
    int index_occurrence_of_42 = left = right = binarySearch(sortedArrayOfInts, 42);
    while (left - 1 >= 0) {
        if (sortedArrayOfInts[left-1] == 42)
            left--;
    }
    while (right + 1 < sortedArrayOfInts.length) {
        if (sortedArrayOfInts[right+1] == 42)
            right++;
    }
    System.out.println("Indices are from: " + left + " to " + right);
}

这将在O(log(n) + #出现次数)内运行 阅读并理解代码。它足够简单。

3
如果数组中的每个元素都是42,则假设此算法的时间复杂度为O(log n + n) = O(n)。但这将是一个非常有限的最坏情况。因此,可以安全地假设在更“平均”的情况下时间复杂度为O(log n + k),其中k是一些常数出现次数,可能是O(log n)。只是想知道,因为这是我最初计划的内容,但由于它基于可能重复的变量数量,所以我好奇是否有一种算法可以保证比O(n)更好的时间复杂度。但是看到答案似乎不会有更好的解法。 - 5StringRyan
1
你没有跳出那些while循环,所以如果你找到了第一个非42的数字,循环不会停止。 - nawfal

3

对于寻找左侧目标和右侧目标的log(n)二分搜索,这是用C++编写的代码,但我认为它非常易读。

思路是我们总是在 left = right + 1 时结束。因此,为了找到左侧目标,如果我们可以将right移动到小于目标的最右侧数字,那么左侧目标就在左侧。

对于左侧目标:

int binary_search(vector<int>& nums, int target){
    int n = nums.size();
    int left = 0, right = n - 1;

    // carry right to the greatest number which is less than target.
    while(left <= right){
        int mid = (left + right) / 2;
        if(nums[mid] < target)
            left = mid + 1;
        else
            right = mid - 1;
    }
    // when we are here, right is at the index of greatest number
    // which is less than target and since left is at the next, 
    // it is at the first target's index
    return left;
}

对于最右边的目标,思路非常相似:

int binary_search(vector<int>& nums, int target){
    while(left <= right){
        int mid = (left + right) / 2;
        // carry left to the smallest number which is greater than target.
        if(nums[mid] <= target)
            left = mid + 1;
        else
            right = mid - 1;
    }
    // when we are here, left is at the index of smallest number
    // which is greater than target and since right is at the next, 
    // it is at the first target's index
    return right;
}

2

我使用二分搜索提出了解决方案,唯一需要做的就是在找到匹配项后在两侧进行二分搜索。

public static void main(String[] args) {
    int a[] ={1,2,2,5,5,6,8,9,10};
    System.out.println(2+" IS AVAILABLE  AT = "+findDuplicateOfN(a, 0, a.length-1, 2));
    System.out.println(5+" IS AVAILABLE  AT = "+findDuplicateOfN(a, 0, a.length-1, 5));
    int a1[] ={2,2,2,2,2,2,2,2,2};
    System.out.println(2+" IS AVAILABLE  AT = "+findDuplicateOfN(a1, 0, a1.length-1, 2));

    int a2[] ={1,2,3,4,5,6,7,8,9};
    System.out.println(10+" IS AVAILABLE  AT = "+findDuplicateOfN(a2, 0, a2.length-1, 10));
}

public static String findDuplicateOfN(int[] a, int l, int h, int x){
    if(l>h){
        return "";
    }
    int m = (h-l)/2+l;
    if(a[m] == x){
        String matchedIndexs = ""+m;
        matchedIndexs = matchedIndexs+findDuplicateOfN(a, l, m-1, x);
        matchedIndexs = matchedIndexs+findDuplicateOfN(a, m+1, h, x);
        return matchedIndexs;
    }else if(a[m]>x){
        return findDuplicateOfN(a, l, m-1, x);
    }else{
        return findDuplicateOfN(a, m+1, h, x);
    }
}


2 IS AVAILABLE  AT = 12 
5 IS AVAILABLE  AT = 43 
2 IS AVAILABLE  AT = 410236578 
10 IS AVAILABLE  AT =

我认为这仍然以O(logn)的复杂度提供结果。

在所有重复项最坏的情况下,例如[5, 5, 5, 5, 5, 5, 5, 5],这是否访问了数组的每个成员并变为O(n)? - Erich

2
以下是Java代码,它返回给定排序数组中搜索键所涵盖的范围:
public static int doBinarySearchRec(int[] array, int start, int end, int n) {
    if (start > end) {
        return -1;
    }
    int mid = start + (end - start) / 2;

    if (n == array[mid]) {
        return mid;
    } else if (n < array[mid]) {
        return doBinarySearchRec(array, start, mid - 1, n);
    } else {
        return doBinarySearchRec(array, mid + 1, end, n);
    }
}

/**
 * Given a sorted array with duplicates and a number, find the range in the
 * form of (startIndex, endIndex) of that number. For example,
 * 
 * find_range({0 2 3 3 3 10 10}, 3) should return (2,4). find_range({0 2 3 3
 * 3 10 10}, 6) should return (-1,-1). The array and the number of
 * duplicates can be large.
 * 
 */
public static int[] binarySearchArrayWithDup(int[] array, int n) {

    if (null == array) {
        return null;
    }
    int firstMatch = doBinarySearchRec(array, 0, array.length - 1, n);
    int[] resultArray = { -1, -1 };
    if (firstMatch == -1) {
        return resultArray;
    }
    int leftMost = firstMatch;
    int rightMost = firstMatch;

    for (int result = doBinarySearchRec(array, 0, leftMost - 1, n); result != -1;) {
        leftMost = result;
        result = doBinarySearchRec(array, 0, leftMost - 1, n);
    }

    for (int result = doBinarySearchRec(array, rightMost + 1, array.length - 1, n); result != -1;) {
        rightMost = result;
        result = doBinarySearchRec(array, rightMost + 1, array.length - 1, n);
    }

    resultArray[0] = leftMost;
    resultArray[1] = rightMost;

    return resultArray;
}

1

它使用修改后的二分查找。时间复杂度为O(LogN),空间复杂度为O(1)。 我们将调用BinarySearchModified两次,一次用于查找元素的起始索引,另一次用于查找元素的结束索引。

private static int BinarySearchModified(int[] input, double toSearch)
    {
        int start = 0;
        int end = input.Length - 1;

        while (start <= end)
        {
            int mid = start + (end - start)/2;
            if (toSearch < input[mid]) end = mid - 1;
            else start = mid + 1;
        }

        return start;
    }


    public static Result GetRange(int[] input, int toSearch)
    {
        if (input == null) return new Result(-1, -1);

        int low = BinarySearchModified(input, toSearch - 0.5);

        if ((low >= input.Length) || (input[low] != toSearch)) return new Result(-1, -1);

        int high = BinarySearchModified(input, toSearch + 0.5);

        return new Result(low, high - 1);
    } 

 public struct Result
    {
        public int LowIndex;
        public int HighIndex;

        public Result(int low, int high)
        {
            LowIndex = low;
            HighIndex = high;
        }
    }

1
Find_Key(int arr[], int size, int key){
int begin = 0;
int end = size - 1;
int mid = end / 2;
int res = INT_MIN;

while (begin != mid)
{
    if (arr[mid] < key)
        begin = mid;
    else
    {
        end = mid;
        if(arr[mid] == key)
            res = mid;
    }
    mid = (end + begin )/2;
}
return res;
}

假设整数数组按升序排列;返回关键字第一次出现的索引或INT_MIN。运行时间为O(lg n)。

只有当 begin-1(而不是 0)开始,endsize(而不是 size - 1)开始时,这才有效。此外,您还需要注意空数组。 - nawfal

1
如果您不需要使用二分搜索,那么哈希表可能是可行的。
创建一个哈希表,其中“键”是值本身,“值”是该值在数组中的索引数组。循环遍历数组,为每个值更新哈希表中的数组。
查找每个值的索引的时间将约为O(1),创建哈希表本身将约为O(n)。

1
如果你已经有一个排序好的数组,那么这个方法可以工作,但听起来有点过度设计。 - jlordo
真的。只要排序,SamIam的解决方案仍然更好。 - Michael
@jlordo,对于查找重复项,如何通过对数组进行排序来帮助呢? - Shark
@Shark 当数组被排序后,所有重复的元素都在一起;)因此,如果你找到了其中一个,只需向左右查找即可找到所有其他的重复元素。 - jlordo
@jlordo 但是你怎么知道要查找哪个值?如果你最初就知道了重复的值,那么你可以找到一个并向左右两侧查找直到找到另一个值。 - Shark
看看 OP 的问题。他不是在寻找重复项。但是如果他正在寻找的数字有多个出现,他希望找到它们所有的位置(分别为它们的索引)。 - jlordo

0
public void printCopies(int[] array)
{
    HashMap<Integer, Integer> memberMap = new HashMap<Integer, Integer>();
    for(int i = 0; i < array.size; i++)
       if(!memberMap.contains(array[i]))
           memberMap.put(array[i], 1);
       else
       {
           int temp = memberMap.get(array[i]); //get the number of occurances
           memberMap.put(array[i], ++temp); //increment his occurance
       }

    //check keys which occured more than once
    //dump them in a ArrayList
    //return this ArrayList
 }

或者,你可以将它们的索引放入一个ArrayList中,并将其放入映射中,而不是计算出现次数。

   HashMap<Integer, ArrayList<Integer>> 
   //the integer is the value, the arraylist a list of their indices

public void printCopies(int[] array)
{
    HashMap<Integer, ArrayList<Integer>> memberMap = new HashMap<Integer, ArrayList<Integer>>();
    for(int i = 0; i < array.size; i++)
       if(!memberMap.contains(array[i]))
       {
           ArrayList temp = new ArrayList();
           temp.add(i);
           memberMap.put(array[i], temp);
       }
       else
       {
           ArrayList temp = memberMap.get(array[i]); //get the lsit of indices
           temp.add(i);
           memberMap.put(array[i], temp); //update the index list
       }

    //check keys which return lists with length > 1
    //handle the result any way you want
 }

嘿,我猜这个得发布了。

 int predefinedDuplicate = //value here;
 int index = Arrays.binarySearch(array, predefinedDuplicate);
 int leftIndex, rightIndex;
 //search left
 for(leftIndex = index; array[leftIndex] == array[index]; leftIndex--); //let it run thru it
 //leftIndex is now the first different element to the left of this duplicate number string
 for(rightIndex = index; array[rightIndex] == array[index]; rightIndex++); //let it run thru it

 //right index contains the first different element to the right of the string
 //you can arraycopy this [leftIndex+1, rightIndex-1] string or just print it
 for(int i = leftIndex+1; i<rightIndex; i++)
 System.out.println(array[i] + "\t");

这将为您获取数字出现的次数,而不是他想要的数字位置。 - Michael
@Michael看一下修改,随时可以取消踩的哦 ;) - Shark
你的代码仍然存在很多问题。你的基本想法是正确的,但我之前在我的回答中已经建议过这个想法了。 - Michael
@Michael 是“TON”吗?请指出一些,我在这里看不出任何问题.... :/ 它将每个值映射到其索引列表。查找“x”的出现只需*return memberMap.get(x);*并打印它。 - Shark
@jlordo 我希望你是在开玩笑... - Shark
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接