在集合中查找第一个缺少的号码

时间:2018-08-30 09:28:38

标签: java collections

我需要从HashSet中找到第一个缺少的号码,例如:

Set<Integer> h = new TreeSet<>(Arrays.asList(1, 2, 3, 4, 6, 8, 9, 10));

在此示例中,如果我们是第一次迭代,则将获得int freeNumber = 5;

很显然,我可以使用while循环进行排序和迭代,直到找到一个缺失的数字。 但这似乎不是优化或优雅的方法来完成此操作。

int i = 1;
for (Integer number : allFileNumbers) {
    if(number != i) {
        missing = number;
        break;
    }
    i++;
}

4 个答案:

答案 0 :(得分:2)

问题标题表明解决方案不应取决于所使用的Set的实现。在那种情况下,迭代Set的值不是最佳选择:例如HashSet不能保证遵循插入顺序或自然顺序的迭代。

您最好的选择是迭代整数并检查它们在集合中的存在。这是一种简单的方法,将在O(k*p)中运行,其中k是集合中没有的最小值,而p是调用Set.contains()的代价。如果您的集合具有O(1)个读访问权限,那么您将获得一个O(k)复杂度算法,该算法是线性的。

示例:

public int findFirstNotInSet(Set<Integer> values) {
    for (int i = 1; i < Integer.MAX_VALUE; i++) {
        if (!values.contains(i)) {
            return i;
        }
    }

    // handle edge case for Integer.MAX_VALUE here
}

如果您可以对集合中的值(范围,缺失值的数量,...)做出更多假设,则可以加快该算法的速度。

答案 1 :(得分:2)

当您拥有TreeSet或任何NavigableSet时,可以使用 Binary Search 的变体来查找第一个缺失值:

static Integer firstMissing(NavigableSet<Integer> set) {
    if(set.size() <= 1) return null;
    Integer first = set.first(), last = set.last();
    if(set.size() == last - first + 1) return null; // no gaps at all
    while(true) {
        int middle = (first+last)>>>1;
        NavigableSet<Integer> sub = set.headSet(middle, false);
        if(sub.size() < middle - first) {// gap before middle
            set = sub;
            last = sub.last();
        }
        else {
            set = set.tailSet(middle, true);
            first = set.first();
            if(first != middle) return middle;
        }
    }
}

被称为

NavigableSet<Integer> set = new TreeSet<>(Arrays.asList(1, 2, 3, 4, 6, 7, 8, 9, 10));
System.out.println(firstMissing(set));

首先,由于Set不包含重复项,因此我们可以使用最小和最大数字来计算一组连续数字应具有的大小。如果集合具有该大小,则我们知道没有间隙,可以立即返回。否则,我们将计算中间数字并将集合分成两半。对于前半部分,我们可以执行相同的测试来确定它是否有间隙,仅继续处理上半部分以找到第一个间隙。否则,我们就考虑下半部分,因为已经知道必须存在差距。当该集合不包含我们的中间号码时,搜索结束。

如果您有任意的Set,但是没有保证的顺序,则没有最佳方法,因为每种方法对某些输入都有效,而对其他输入则更差。

  • 您可以简单地使用TreeSet将集合复制到new TreeSet<>(set)并使用上述方法

  • 您可以遍历数字范围,以测试是否存在数字

        static Integer firstMissing(Set<Integer> set) {
            if(set.size() <= 1) return null;
            Integer firstPresent = null, firstAbsent = null;
            for(int i = Integer.MIN_VALUE; firstPresent == null; i++)
                if(set.contains(i)) firstPresent = i;
            for(int i = firstPresent+1; firstAbsent == null; i++)
                if(!set.contains(i)) firstAbsent = i;
            return firstAbsent-firstPresent == set.size()? null: firstAbsent;
        }
    

    循环条件利用了预测试的优势,可以确保集合中至少有两个数字。

    一个明显的问题是数字范围很大,我们必须探究。如果我们知道所有数字都是正数,则可以将Integer.MIN_VALUE替换为零。

  • 您可以遍历集合的内容,以将所有遇到的值记录在可搜索的数据结构中。这类似于上面的复制方法,但是例如,如果所有数字均为正,则可以使用以下测试:

        static Integer firstMissing(Set<Integer> set) {
            if(set.size() <= 1) return null;
            BitSet bs = new BitSet();
            set.forEach(bs::set);
            int firstPresent = bs.nextSetBit(0), firstAbsent = bs.nextClearBit(firstPresent);
            return firstAbsent-firstPresent == set.size()? null: firstAbsent;
        }
    

    如果仅缺少几个数字或根本没有数字,则它比TreeSet的工作要好得多,但如果值确实稀疏,则效果会更糟。

答案 2 :(得分:1)

我认为您可以在流中找到它。就像这样;

Set<Integer> h = new LinkedHashSet<>(Arrays.asList(1, 2, 3, 4, 6, 8, 9, 10));

    h.stream().anyMatch(isMissed -> {
        if (!h.contains(isMissed + 1)) {
            System.out.println(isMissed + 1);
            return true;
        }
        return false;
    });

答案 3 :(得分:0)

只是个主意...

Set<Integer> h = new HashSet<>(Arrays.asList(1, 2, 3, 4, 6, 8, 9, 10));
Set<Integer> k = IntStream.rangeClosed(Collections.min(h),Collections.max(h)).boxed().collect(Collectors.toSet());
k.removeAll(h);
System.out.println(k.stream().findFirst().orElse(-1));