剑指 Offer 03. 数组中重复的数字

在长度为n的数组中,所有数字在0~n-1范围内,存在重复数字。本文提供了两种方法找到任意一个重复数字:1) 使用哈希表,遍历数组并返回重复数字;2) 原地交换元素,使得元素值等于其索引,遇到重复数字即返回。同时,还介绍了在O(n)时间和O(1)空间复杂度内统计不同元素出现次数的方法,通过正负数表示原始值和计数值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

找出数组中重复的数字。

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。

示例 1:

输入:
[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3 

限制:

2 <= n <= 100000


方法一:哈希表 / Set
        利用数据结构特点,容易想到使用哈希表(Set)记录数组的各个数字,当查找到重复数字则直接返回。

算法流程:

  • 初始化: 新建 HashSet ,记为 dic ;
  • 遍历数组 nums 中的每个数字 num :
    • 当 num 在 dic 中,说明重复,直接返回 num;
    • 否则将 num添加至 dic 中;
  • 返回 -1 。题中一定有重复数字,因此这里返回多少都可以。

复杂度分析:

  • 时间复杂度 O(N) : 遍历数组使用 O(N),HashSet 添加与查找元素皆为 O(1) 。
  • 空间复杂度 O(N) : HashSet 占用 O(N)大小的额外空间。
class Solution {
    public int findRepeatNumber(int[] nums) {
        // 使用set存放不重复元素
        Set<Integer> dic = new HashSet<>();
        // 遍历数组进行比较,如果set中不包含该元素,放入
        for(int num : nums) {
            if(dic.contains(num)) return num;
            dic.add(num);
        }
        return -1;
    }
}

        在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此题意表示:数组元素的 索引 和 值 是 一对多 的关系。—— 遍历数组,通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i] = i)。因而,就能通过索引映射对应的值,起到与字典等价的作用。

数组的数字都在 0~n-1的范围内。如果这个数组中没有重复的数字,当数组排序之后数字 i将出现在下标为 i 的位置。由于数组中有重复的数字,有些位置可能存在多个数字,同时有些位置可能没有数字。

方法二:原地交换

Picture0.png

        遍历数组时,第一次遇到数字 x 时,将其交换至索引 x 处;而当第二次遇到数字 x 时,一定有 nums[x] = x ,此时即可得到一组重复数字。

以2,3,1,0,2,5,3为例
        1)数组第0号位置为2,2!=0,将0号位置的2与2号位置的1交换,变成 1,3,2,0,2,5,3
        2)数组第0号位置为1,1!=0,将0号位置的1与1号位置的3交换,变成 3,1,2,0,2,5,3
        3)数组第0号位置为3,3!=0,将0号位置的3与3号位置的0交换,变成 0,1,2,3,2,5,3
        4)数组第0号位置为0,0==0,游标右移至1号位置,1==1,继续右移,直到第4号位置
        5)数组第4号位置为2, 2!=4, 2==array[2],说明2重复了,输出2,算法结束

算法流程:

  • 遍历数组 nums,设索引初始值为 i = 0:
    • 若 nums[i] = i: 说明此数字已在对应索引位置,无需交换,因此跳过;
    • 若 nums[nums[i]] = nums[i]: 代表索引 nums[i] 处和索引 i 处的元素值都为 nums[i] ,即找到一组重复值,返回此值 nums[i];
    • 否则: 交换索引为 i 和 nums[i] 的元素值,将此数字交换至对应索引位置。
  • 若遍历完毕尚未返回,则返回 -1 。

复杂度分析:

  • 时间复杂度 O(N) : 遍历数组使用 O(N),每轮遍历的判断和交换操作使用 O(1) 。
  • 空间复杂度 O(1) : 使用常数复杂度的额外空间。
class Solution {
    public int findRepeatNumber(int[] nums) {
        //初始化
        /*HashMap map = new HashMap<Integer,Integer>();

        for(int i = 0; i < nums.length; i++){
            if(map.get(nums[i]) == null){
                map.put(nums[i], 1);
            }else{
                return nums[i];
            }
        }*/

        //置换
        int length = nums.length;

        int i = 0;

        while(i < length){
            if(nums[i] == i){
                i++;
                continue;
            }
            
            if(nums[nums[i]] == nums[i]){
                return nums[i];
            }else{

                /*记num[ i ] =a, num[num[ i ]]=num[ a ] = b,
                那么 交换后,num[ i ]=b, num[num[i]]=num[a]=a,
                这时候下标 a 对应的元素也是a,达到目的
                */

                int temp;
                temp = nums[i];
                nums[i] = nums[temp];
                nums[temp] = temp;
            }
        }

        return -1;
    }
}

补充题:

一个长度大小为n的数组,数组中的每个元素的取值范围在[1,n],且为正整数。问:如何在时间复杂度为O(n),空间复杂度为O(1)的条件下,统计数组中不同元素出现的次数。

  • 数组按序扫描,通过当前元素的值作为下标,找到下一个元素。
  • 最后得到的数组中,下标(因为下标从0开始的,故输出时需要+1)为数组中出现的元素,每个下标对应的值取反输出即是该元素出现的频率。
    • 若当前元素小于0,则跳过;
    • 若当前元素大于0,则判断其作为下标索引到的元素是否大于0,
      • 若大于0,则索引到的元素赋值给当前元素,索引到的元素置为-1;
      • 若小于0,则索引到的元素自减1,当前元素置为0;
  • 用正数/负数来区分a[i]上是原来的值,还是用于计数的值

   举个例子,比如 { 2, 5, 5, 2, 3 }。看到第一个是 2,即2有1个,所以先将这个“1”保存到a[2-1]的位置上。但a[2-1]有有效数,将该数移动。假设不用负数,而是用中括号来表示计数,步骤依次是
    { 2, 5, 5, 2, 3 }
    5, [1], 5, 2, 3
    3, [1], 5, 2, [1]
    5, [1], [1], 2, [1]
    [0], [1], [1], 2, [2]
    [0], [2], [1], [0], [2]

    结果是 1有0个, 2有2个, 3有1个, 4有0个, 5有2个

    此类以值做下标的方法适用条件:正整数、取值范围[1,n]、n个数

package DaliyPractise;
import java.util.*;

/*一个长度大小为n的数组,数组中的每个元素的取值范围在[1,n],且为正整数。
问:如何在时间复杂度为O(n),空间复杂度为O(1)的条件下,统计数组中不同元素出现的次数。
*/
public class UnOrderedArrayNumberCount {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt(); // 数组的长度
		int[] arr = new int[n]; // 数组元素的取值范围在【1,n】,正整数
		for(int i = 0; i < n; i ++) {
			arr[i] = sc.nextInt();
		}
		
		calNumberAppear(n, arr);
	}

	private static void calNumberAppear(int n, int[] arr) {
		// TODO Auto-generated method stub
		if(arr == null || n < 0 || arr.length == 0) {
			return;
		}
		
		int index = 0; // 从第一位开始遍历
		while(index < n) {
			// 计算当下元素的在数组下标中的位置 [0 , n - 1]
			int temp = arr[index] - 1;
			// 如果出现 负数,相当于数字已经被替换变为计数表示,继续向后执行
			if(temp < 0) {
				index ++;
				continue;
			}
			
			// 让该位置的元素放置到 与当前元素位置 值相同的数组位置上
			// 将该位置的数值变为 -1,代表 当前元素位置相同的数出现了一次
			if(arr[temp] > 0) {
				arr[index] = arr[temp];
				arr[temp] = -1;
			}
			else {
				// 如果当下下标的元素 <= 0,相当于已经出现过,次数++即 --
				arr[temp] --;
				arr[index] = 0;
			}
		}
		
		// 输出结果,将负数取反即为最后的结果
		for(int i = 0; i < n; i ++) {
			System.out.print(Math.abs(arr[i]) + " ");
		}
	}

}

/*
5
2 5 5 2 3

0 2 1 0 2 
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值