一、前言
大家好,本文章属于《剑指 Offer 每日一题》中的系列文章中的第 1 篇。
在该系列文章中我将通过刷题练手的方式来回顾一下数据结构与算法基础,同时也会通过博客的形式来分享自己的刷题历程。如果你刚好也有刷算法题的打算,可以互相鼓励学习。我的算法基础薄弱,希望通过这两三个月内的时间弥补这块的漏洞。本次使用的刷题语言为 Java ,预计后期刷第二遍的时候,会采用 Python 来完成。
-
刷题平台为 leetcode 的剑指 Offer 专题
-
码云仓库地址:gitee.com/xiaoleiStud…
二、题目
找出数组中重复的数字
在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
实例1:
输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3
复制代码
限制:
2<= n <= 100000
三、解题思路
3.1 思路1:排序后比较
首先,我们要理解题意,该题目的意思十分的明确,就是在一个数组中找出任意一个重复的数字即可。
那么我们想到的一个简单的方式就是将这个数组排序,然后遍历这个数组,两两比较,找到重复的元素即可。
3.1.1 代码实现
Java 代码实现:
private static int solove1(int[] nums) {
quickSort(nums,0,nums.length-1);
int repeat = -1;
for (int i = 1; i < nums.length; i++) {
if (nums[i] == nums[i - 1]) {
repeat=nums[i];
break;
}
}
return repeat;
}
/**
* 快速排序
* @param arr
* @param L 指向数组第一个元素
* @param R 指向数组最后一个元素
*/
public static void quickSort(int[] arr, int L, int R) {
int i = L;
int j = R;
//支点
int pivot = arr[(L + R) / 2];
//左右两端进行扫描,只要两端还没有交替,就一直扫描
while (i <= j) {
//寻找直到比支点大的数
while (pivot > arr[i])
i++;
//寻找直到比支点小的数
while (pivot < arr[j])
j--;
//此时已经分别找到了比支点小的数(右边)、比支点大的数(左边),它们进行交换
if (i <= j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;
j--;
}
}
//上面一个while保证了第一趟排序支点的左边比支点小,支点的右边比支点大了。
//“左边”再做排序,直到左边剩下一个数(递归出口)
if (L < j)
quickSort(arr, L, j);
//“右边”再做排序,直到右边剩下一个数(递归出口)
if (i < R)
quickSort(arr, i, R);
}
复制代码
3.1.2 代码执行结果
该方式的实现效果如下
3.1.3 复杂度分析
本次使用快速排序先进行排序,并没有申请额外的空间,快排的时间复杂度为O(nlogn),空间复杂度O(1)
3.2 思路2:哈希表实现
哈希表来解决这个问题。从头到尾扫描数组的每一个数,用O(1) 的时候来判断哈希表中是否包含这个数,如果哈希表中没有这个数,就把它加入哈希表;如果哈希表中已经存在这个数,就找到了一个重复的数,并返回。
这个算法的时间复杂度是O(n) ,但它提高时间效率是以一个大小为O(n) 的哈希表为代价的。
3.2.1 代码实现
Java 版:
// 方法二: 遍历数组。由于 只需要找出数组中任意一个重复的数字,则可与用 set 集合来处理
// 时间复杂度 :O(n)
private static int solove2(int[] nums) {
Set<Integer> set = new HashSet<Integer>();
int repeat = -1;
for (int num : nums) {
if (!set.add(num)) {
repeat = num;
break;
}
}
return repeat;
}
复制代码
3.2.2 执行效果
该方式的执行效果如下,可以发现内存消耗提升了少许。
3.3 思路3:原地交换
这种方法是利用了题目给出的条件来完成。
注意到数组中的数字都在 0 ~ n-1 的范围内,如果这个数组中没有重复的数字,那么当数组排序之后数字 i 将出现在下标为 i 的位置。当然数组中有重复的数字,有些位置可能存在多个数字,同时有些位置可能没有数字。
那么突破口就来了,我们可以遍历数组中的 x 元素,把它放在数组中的 索引为 x 的位置上,当第二次遇到数字 x 的时候,那么就会出现索引为 x 的元素 == x,即当前的 x 是重复元素。
3.3.1 算法流程
遍历数组 nums,设置索引初始值为 i=0;
-
1、若 nums[i] !=i ,说明此数字未放到我们的目标位置上,进入观察它
-
2、若 nums[nums[i]] = nums[i] :代表索引 nums [i] 处和索引 i 处的元素值都为 nums [i] ,即找到一组重复值,返回此值 nums[i]。
-
3、否则:交换索引 为 i 和 nums[i] 的元素值,将此数字交换到对应索引位置。
-
4、若遍历完毕都未找到,返回 -1
这里的第二步,我们可以举个例子来理解下:假如 索引0 处的第一个值是0 即nums[ 0 ] =0 ,然后遍历第二个值也是0,
即nums[0] = 0,这个条件满足,无需交换,开始进入第二个条件,nums[nums[0]] == 0 这个条件也满足,说明 找到重复的值了。
3.3.2 代码实现
Java 版:
// 方法三:原地置换
private static int solove3(int[] nums) {
int temp;
for(int i=0;i<nums.length;i++){
while (nums[i]!=i){
if(nums[i]==nums[nums[i]]){
return nums[i];
}
temp=nums[i];
nums[i]=nums[temp];
nums[temp]=temp;
}
}
return -1;
}
复制代码
3.3.3 执行效果
只能说原地置换算法是真的强!另一方面也告诉我们多利用题目中的条件。
四、小结
本题考察了对一维数组的理解及编程能力。一维数组在内存中占据连续的空间,因此我们可以根据下标定位对应的元素。
三种典型的解法在这边做了个总结,剑指 Offer 刷题之旅后续继续展开,欢迎一起交流学习。