一、题目
在一个长度为n+1的数组里面的所有数字都在1~n的范围内,所以数组中至少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的数组。例如,如果输入长度为9的数组{2,3,5,4,3,2,6,7},那么对应的输出是重复的数字2或者3。
二、思路
方法一
创建一个新n+1的数组data,遍历原来的数组如 2 将2存到 data[2]中, 3存到data[3]中…. 等下下次一遍历到3 发现data[3]=3 说明重复了。
算法时间复杂度为 O(n),空间复杂度O(n)
方法二
使用二分法。 如:{2,3,5,4,3,2,6,7} 先二分,先统计在1-4里面的数字个数如果大于4则说明1~4里面有重复数字,否则5-7里面有重复数字。重复上面操作。
分析:二分查找logn ,但是getCount每个数组遍历一变 n,时间复杂度为O(nlogn),空间复杂度为O(1)。但是这个方法有个问题不能找出所有的重复元素。
如{2,2,4,4,5,5} 找到的元素会是4。
start=1,end=5,middle=3. 1-3中元素有2个则4~5有重复
start=4,end=5,middle=4。 4有2个元素
start=4,end=4,middle=4, 返回4。
三、解决问题
3.1 代码实现
方法一
/**
* 创建一个新n+1的数组data,遍历原来的数组如 2 将2存到 data[2]中, 3存到data[3]中…. 等下下次一遍历到3 发现data[3]=3 说明重复了。
* 算法时间复杂度为 O(n),空间复杂度O(n)
* @param arr
* @return
*/
public int getDuplicate1(int[] arr) {
if(null == arr || arr.length <= 0){
System.out.println("数组输入无效!");
return -1;
}
for(int num : arr){
if (num < 1 || num > arr.length - 1){
System.out.println("数组大小超出范围");
return -1;
}
}
int[] newArr = new int[arr.length];
Map<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < newArr.length; i++) {
newArr[i] = -1;
}
for (int i = 0; i < arr.length; i++) {
// 即将该数组放到他在数组中和他角标相等的位置
if (newArr[arr[i]] == arr[i]){
map.put(arr[i],map.get(arr[i]) + 1);//重复的次数
}else{
newArr[arr[i]] = arr[i];
map.put(arr[i],1);
}
}
// 通用的Map迭代方式
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() > 1){
System.out.println(entry.getKey() +" : "+entry.getValue() );
return entry.getKey();
}
}
// JDK8的迭代方式
/*map.forEach((key,value)->{
if (value > 1){
System.out.println(key +" : "+value);
}
});*/
return -1;
}
方法二
/**
* 找到数组中一个重复的数字
* 返回-1代表无重复数字或者输入无效
*
* 采用二分思想。我们把1~n的数字从中间的数字middle分为两部分,
* 统计输入数组中1~middle的数字的数目,如果这部分数目大于middle,则说明此部分中含有重复数字,否则就是另外的一部分含有重复数字。
* 依此类推,将区间内的数目和区间大小比较,直到区间的头尾相等,就找到了重复数字。
*
*
* 二分查找logn ,但是getCount每个数组遍历一变 n,时间复杂度为O(nlogn),空间复杂度为O(1)。但是这个方法有个问题不能找出所有的重复元素。
*/
public int getDuplicate(int[] arr) {
if(null == arr || arr.length <= 0){
System.out.println("数组输入无效!");
return -1;
}
for(int num : arr){
if (num < 1 || num > arr.length - 1){
System.out.println("数组大小超出范围");
return -1;
}
}
int low = 1;
int high = arr.length - 1;// high即为题目的n
int mid,count;
while (low <= high){
mid = ((high - low) >> 1) + low;//右移动n位,相当于除以2^n
//统计每区间里数字的数目
count = countRange(arr,low,mid);
/*System.out.println("low: " + low);
System.out.println("mid: " + mid);
System.out.println("high: " + high);*/
//直到区间的头尾相等,就找到了重复数字
if (low == high){
if (count > 1){
return low;
}else {
break;// 必有重复,应该不会出现这种情况吧?
}
}
if (count > mid - low + 1){
high = mid;//说明这个区间有重复元素
}else {
low = mid + 1;
}
}
return -1;
}
/**
* 返回在[low,high]范围中数字的个数
* 查找数组中值位于low和high之间的元素个数
* 函数countRange()将被调用O(logn)次,每次需要O(n)的时间。
* @return
*/
private int countRange(int[] arr, int low, int high) {
if (null == arr){
return 0;
}
int count = 0;
for(int num : arr){
if (num >= low && num <= high){
count++;
}
}
return count;
}
3.2 单元测试
1.数组中带一个或多个重复数字
2.数组中不包含重复的数字
3.无效输入测试用例(空数组,数组数字越界等)
package SwordOffer;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
/**
* @Description 不修改数组找出重复的数字
*
* @author kankan
* @creater 2019-10-28 9:20
*/
/*
* 题目:在一个长度为n+1的数组里的所有数字都在1到n的范围内,所以数组中至
* 少有一个数字是重复的。请找出数组中任意一个重复的数字,但不能修改输入的
* 数组。例如,如果输入长度为8的数组{2, 3, 5, 4, 3, 2, 6, 7},那么对应的
* 输出是重复的数字2或者3。
*/
public class Solution3 {
/**
* 创建一个新n+1的数组data,遍历原来的数组如 2 将2存到 data[2]中, 3存到data[3]中…. 等下下次一遍历到3 发现data[3]=3 说明重复了。
* 算法时间复杂度为 O(n),空间复杂度O(n)
* @param arr
* @return
*/
public int getDuplicate1(int[] arr) {
if(null == arr || arr.length <= 0){
System.out.println("数组输入无效!");
return -1;
}
for(int num : arr){
if (num < 1 || num > arr.length - 1){
System.out.println("数组大小超出范围");
return -1;
}
}
int[] newArr = new int[arr.length];
Map<Integer,Integer> map = new HashMap<>();
for (int i = 0; i < newArr.length; i++) {
newArr[i] = -1;
}
for (int i = 0; i < arr.length; i++) {
// 即将该数组放到他在数组中和他角标相等的位置
if (newArr[arr[i]] == arr[i]){
map.put(arr[i],map.get(arr[i]) + 1);//重复的次数
}else{
newArr[arr[i]] = arr[i];
map.put(arr[i],1);
}
}
// 通用的Map迭代方式
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
if (entry.getValue() > 1){
System.out.println(entry.getKey() +" : "+entry.getValue() );
return entry.getKey();
}
}
// JDK8的迭代方式
/*map.forEach((key,value)->{
if (value > 1){
System.out.println(key +" : "+value);
}
});*/
return -1;
}
/**
* 找到数组中一个重复的数字
* 返回-1代表无重复数字或者输入无效
*
* 采用二分思想。我们把1~n的数字从中间的数字middle分为两部分,
* 统计输入数组中1~middle的数字的数目,如果这部分数目大于middle,则说明此部分中含有重复数字,否则就是另外的一部分含有重复数字。
* 依此类推,将区间内的数目和区间大小比较,直到区间的头尾相等,就找到了重复数字。
*
*
* 二分查找logn ,但是getCount每个数组遍历一变 n,时间复杂度为O(nlogn),空间复杂度为O(1)。但是这个方法有个问题不能找出所有的重复元素。
*/
public int getDuplicate(int[] arr) {
if(null == arr || arr.length <= 0){
System.out.println("数组输入无效!");
return -1;
}
for(int num : arr){
if (num < 1 || num > arr.length - 1){
System.out.println("数组大小超出范围");
return -1;
}
}
int low = 1;
int high = arr.length - 1;// high即为题目的n
int mid,count;
while (low <= high){
mid = ((high - low) >> 1) + low;//右移动n位,相当于除以2^n
//统计每区间里数字的数目
count = countRange(arr,low,mid);
/*System.out.println("low: " + low);
System.out.println("mid: " + mid);
System.out.println("high: " + high);*/
//直到区间的头尾相等,就找到了重复数字
if (low == high){
if (count > 1){
return low;
}else {
break;// 必有重复,应该不会出现这种情况吧?
}
}
if (count > mid - low + 1){
high = mid;//说明这个区间有重复元素
}else {
low = mid + 1;
}
}
return -1;
}
/**
* 返回在[low,high]范围中数字的个数
* 查找数组中值位于low和high之间的元素个数
* 函数countRange()将被调用O(logn)次,每次需要O(n)的时间。
* @return
*/
private int countRange(int[] arr, int low, int high) {
if (null == arr){
return 0;
}
int count = 0;
for(int num : arr){
if (num >= low && num <= high){
count++;
}
}
return count;
}
//1.无效输入测试用例(空数组,数组数字越界等)
@Test
public void test1(){
System.out.println("test1:");
int[] arr = null;
int dup = getDuplicate(arr);
if (dup >= 0){
System.out.println("重复数字为:" + dup);
}
}
//2.数组中不包含重复的数字
@Test
public void test2(){
System.out.println("test2:");
int[] arr = {1,2,3,4};
int dup = getDuplicate(arr);
if (dup >= 0){
System.out.println("重复数字为:" + dup);
}
}
//3.数组中带一个或多个重复数字
@Test
public void test3() {
System.out.print("test3:");
int[] a = {2,3,5,4,3,2,6,7};
int dup = getDuplicate1(a);
if (dup >= 0)
System.out.println("重复数字为:" + dup);
}
}