java 找出重复的数字

这是一个经典的题型,比如:有1亿个数字,找出其中重复的。

最直观的写法就是双重循环了,但是效率过低。再就是先排序再遍历,又总感觉不太直接。后来偶然查到BitSet有相应的api来处理这个问题,查了下源码,还挺有意思的,记录一下。

直接上代码

public class Test05 {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[] arr = new int[10000000];
		for(int i = 0; i < arr.length; i++) {
			arr[i] = i;
		}
		arr[arr.length - 1] = arr.length - 10;//构造一个有重复数字的数组
		
		aaa(arr);
		
	}
	
	public static void aaa(int[] arr) {
		long time1 = System.currentTimeMillis();
		
		int max = 1;
		for(int i: arr) {
			max = i > max ? i : max;//查找最大值
		}
		
		long[] temp = new long[(max >> 6) + 1]; 
		
		for(int i: arr) {
			if((temp[i >> 6] & (1l << i)) == 0) {//判断是否重复
				temp[i >> 6] |= 1l << i;
			}else {
				System.out.println(i + " is duplicate");
				//break;
			}
		}
		
		long time2 = System.currentTimeMillis();
		
		System.out.println((time2 - time1) + "ms");
	}

}

(相对于源码,仅省略了自动扩容和校验)

全是位运算很吓人,但是实际上比冒泡排序还简单。

原理
如果说桶排序是投机取巧,那这个算法就是投机取巧上继续投机取巧。首先找到最大的数,然后除以64(也就是移6位)以确定桶的数量(源码中是动态改变桶数组的,每加入一个新数字,都会算出它所应该在的位置,如果比当前数组长度大,则复制数组进行扩容。由于与算法本身无关,写起来又很麻烦,省略掉)。对于每一个数字,它所在的桶数组的下标是除以64后的整数部分,它所对应的值是1l << 除以64的余数 。举几个例子:

对于数字3,它除以64为0,余数是3。那么它应该在桶数组下标0的位置,它的值用2进制表示为1000。
对于数字4,它除以64为0,余数是4。那么它应该在桶数组下标0的位置,它的值用2进制表示为10000。由于插入数组时用了或运算符,最终结果应该为11000。

在查找重复时也是完全一样的套路。如果我要查找数字4是否存在,则需要查出下标为0的数字,然后通过与运算符来判断第五位是否为1。

最终,上面的temp数组中的数字用2进制表示的话可能是这样的(64位太长了,用4位来表示)

[1000, 0001, 0011]

它实际表示,已经存在数字3,4,8,9。

也就是说,它就是用每一位的1来表示所对应的数字是否存在。

一些细节的解释:
为什么是移6位?因为移6位相当于除以64,而64是长整形的长度。
长整形每左移64位后值不变。1l << i 实际上相当于1l << (i % 64)

缺点:与桶排序一样,如果最大值过大,那么效率可能会很低。

你可能感兴趣的:(算法)