题目3:找出数组中任意重复的数字

在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,如果输入长度为7的数组{2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复的数字2或者3。

思路:

看到这道题后我的第一个想法是使用hash表对数组按顺序从头到尾进行扫描,扫描到不包含的数字就将其加入hash表中,扫描到hash表中包含的数字则代表该数字为重复的数字,则返回该重复的数字。这个算法的时间复杂度是O(n),但是它提高时间效率的代价是必须要有一个大小为O(n)的hash表。

在面试中,面试官除了会考察程序员是否能解决一个问题,还会关注他是否能考虑周全,比如使用尽量低的时间复杂度和空间复杂度的算法。因此,有时候我们仅仅只是做到能够解题是不够的,我们还需考虑有没有更好的办法。

第二个方法是对数组进行排序,然后对数组进行相邻两个数字的对比,找出相同的数字。排序一个长度为n的数组需要O(nlogn)的时间。这个方法的时间复杂度也比较高,再看看还有没有更优解。

重复查看题目,注意到所有数字都在0到n-1的范围,这样的话如果该数组不包含重复的数字,那么在排完序后数字i应出现在下角标为i的位置。
那么可以这样排序,对数组从头到尾扫描,扫描到n[i]时,判断n[i]是否等于i,是的话代表它在正确的位置,扫描下一个,不是的话先判断下角标为n[i]的数字是否等于i,是的话那么我们就找到这个重复的数字了,返回该数字退出循环即可,不是的话就将n[i]中的数字与下角标为n[i]的数字交换。一直循环直到扫描完整个数组。若循环完整个数组后仍然没有返回值,则代表该数组没有重复的数字。该方法虽然有个两重循环,但每个数字最多只需要交换两次就可以找到自己的位置,因此时间复杂度为O(n),至于空间复杂度,由于该方法全程都是在对原数组操作,故其空间复杂度为O(1)。故该方法为最优解。

注:在编写解决问题的函数之前先写单元测试用例,这是一种很好的习惯,在面试中会给面试官带来很好的印象。

关键代码实现:

private static int checkDuplicate(int[] ar) {
        int duplicate;

        if (ar == null) {// 避免数组为空的情况
            return -2;
        }

        for (int i = 0; i < ar.length; i++) {// 避免出现数组内容格式错误
            if (ar[i] < 0 || ar[i] > ar.length - 1) {
                return -3;
            }
        }

        for (int i = 0; i < ar.length; i++) {
            while (ar[i] != i) {
                if (ar[i] == ar[ar[i]]) {
                    duplicate = ar[i];
                    System.out.println(
                            "The array contains duplicate numbers." 
                    + "One of the duplicate number is: " + duplicate);
                    return duplicate;
                }
                swap(ar, i, ar[i]);
            }
        }
        return -1;
    }
  //对数组中下角标为c和d的数字交换
    private static void swap(int[] ar, int c, int d) {
        int temp = ar[c];
        ar[c] = ar[d];
        ar[d] = temp;
    }

下面对代码进行测试

测试用例及完整代码:

public class Duplication3 {
    public static void main(String[] args) {
        int[] a = { 3, 4, 1, 0, 2, 5, 6};// 数组中不包含重复数字
        int[] b = { 3, 4, 1, 0, 2, 5, 3};// 数组中含有一个重复数字
        int[] c = { 4, 6, 1, 0, 2, 5, 0};//数组中重复数字为最小值
        int[] d = { 4, 6, 1, 0, 2, 6, 5};//数组中重复数字为最大值
        int[] e = { 2, 3, 1, 0, 2, 5, 3 };// 数组中含有多个重复数字
        int[] f = { 2, 3, 1, 0, -1, 5, 3};// 数组中的数字格式错误
        int[] g = { 2, 3, 1, 0, 8, 5, 3};//数组中含有数字大于数组长度-1
        int[] h = null;
        
        test("test1",a);
        test("test2", b);
        test("test3", c);
        test("test4", d);
        test("test5", e);
        test("test6", f);
        test("test7", g);
        test("test8", h);
    }

    private static void test(String s, int[] ar) {
        System.out.println(s + ":");
        int k = checkDuplicate(ar);
        if (k < 0) {
            switch (k) {
            case -1:
                System.out.println("No duplicate numbers in this array!");
                break;
            case -2:
                System.out.println("This array is null!");
                break;
            case -3:
                System.out.println("This array content is malformed!");
            }
        }
    }

    private static int checkDuplicate(int[] ar) {
        int duplicate;

        if (ar == null) {// 避免数组为空的情况
            return -2;
        }

        for (int i = 0; i < ar.length; i++) {// 避免出现数组内容格式错误
            if (ar[i] < 0 || ar[i] > ar.length - 1) {
                return -3;
            }
        }

        for (int i = 0; i < ar.length; i++) {
            while (ar[i] != i) {
                if (ar[i] == ar[ar[i]]) {
                    duplicate = ar[i];
                    System.out.println(
                            "The array contains duplicate numbers." 
                    + "One of the duplicate number is: " + duplicate);
                    return duplicate;
                }
                swap(ar, i, ar[i]);
            }
        }
        return -1;
    }

    private static void swap(int[] ar, int c, int d) {
        int temp = ar[c];
        ar[c] = ar[d];
        ar[d] = temp;
    }
}