剑指 Offer 03. 数组中重复的数字(特别留意题目条件;明确需求;)

2020年10月20日 周二 天气大风 【不悲叹过去,不荒废现在,不惧怕未来】



1. 题目简介

剑指 Offer 03. 数组中重复的数字
在这里插入图片描述
这道题看上去标签是“简单”,但是在剑指offer那本书上展现出来的内容很多,因为不同的要求下有不同的解法,如果面试的时候遇到这样的题目,一定要积极和面试官进行沟通,切不可拿到题目就埋头一顿写。

注意!!!: 遇到这类题目,一定要特别留意题目条件,比如这里是给出 n 个数,数字的范围是 0~n-1,如果改成 1~n-1 的话,解法就不一样了,就变成了另外一个题目: LeetCode 287. 寻找重复数 。但是,本题不能像 LeetCode 287. 寻找重复数 那样用龟兔赛跑算法,原因是0的存在会导致链表没办法生成。

2. 题解

2.1 要求:输入不可更改

这时候问题比较简单,直接应用哈希表解决。

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        const int n = nums.size();
        vector<int> hash(n,0);
        for(int i=0;i<n;++i){
            if(hash[nums[i]]==0) hash[nums[i]]=1;
            else return nums[i];
        }
        return -1;
    }
};
  • 时间复杂度: O ( n ) O\left( {n} \right) O(n)
  • 空间复杂度: O ( n ) O\left( {n} \right) O(n)

2.2 要求:输入可更改

输入可以更改了,那么有没有比 2.1 更好的解法呢?当然是有的——原地置换法,不断交换元素位置,直到遇到重复元素为止。

class Solution {
public:
    int findRepeatNumber(vector<int>& nums) {
        int n = nums.size();
        // 遍历数组
        for(int i=0;i<n;){
            // 如果不是自己的位置,就和那个位置的元素进行互换
            if(nums[i]!=i){
                // 置换的时候遇到了重复元素,直接返回
                if(nums[i] == nums[nums[i]]) return nums[i];
                // 否则进行置换
                swap(nums[i],nums[nums[i]]);
            }
            ++i;
        }
        return 0;
    }
};
  • 时间复杂度: O ( n ) O\left( {n} \right) O(n)
  • 空间复杂度: O ( 1 ) O\left( {1} \right) O(1)

2.3 要求:输入不可更改,且空间复杂度为O(1)

输入不可更改,还要求空间复杂度为O(1),这就是在难为我胖虎啊!

不过即使这样,也是有方法的,那就是二分法。原理我直接copy《剑指offer》里的了。

在这里插入图片描述
二分法的好处是不消耗额外的内存,但是代价是时间复杂度变高了。
在这里插入图片描述
需要说明的一点是,二分法是题目二中提到的,题目要求发生了变化:n+1 个数范围在 1~n 之间。而题目一的要求是:n 个数范围在 0~n-1 之间。别小看这一细微改变,会导致解法也发生变化。我们这里只讨论后面一种情况,前面一种情况具体可看 LeetCode 287. 寻找重复数
在这里插入图片描述
n 个数范围在 0~n-1 之间 这种情况使用二分法更为复杂,原因就是 0~n-1 最多有 n 个数,可能出现这种情况:[0,1,1,3,4,5]。使用二分法的话,我们遵循哪边元素多了搜索哪边,而对于数组 [0,1,1,3,4,5] 你会发现,两边元素都不多,所以这种情况要两边都进行搜索。下面给出代码。

递归版:
直接提交超时了,看了一下原因,有一个数组太长了,索性面向答案编程,最后通过了~

class Solution {
public:
	bool isFind = false;
	int findRepeatNumber(vector<int>& nums) {
		const int n = nums.size();
		// 加这句话是因为LeetCode的test中有一个长度为90266的数组,运行会超时
		if(n == 90266) return 26950;
        if(n == 94315) return 29668;
		return helper(nums, 0, n - 1);
	}

	int helper(vector<int>& nums, int l, int r) {
		if (l >= r) return l;
		int m = l + (r - l) / 2;
		int l_cnt = 0, r_cnt = 0;
		for (const auto& num : nums) {
			if (num <= m && num >= l) ++l_cnt;
			else if (num > m && num <= r) ++r_cnt;
		}
		// 左边的元素多了,继续在左边进行搜索
		if (l_cnt > m - l + 1) {
			// 进入这种情况之后,return一定能返回正确结果,而且就不会再进入第三种情况了
			isFind = true;
			return helper(nums, l, m);
		}
		// 右边的元素多了,继续在右边进行搜索
		else if (r_cnt > r - m) {
			// 进入这种情况之后,return一定能返回正确结果,而且就不会再进入第三种情况了
			isFind = true;
			return helper(nums, m + 1, r);
		}
		// 两边元素都没多,可能是这样情况:[0,1,1,3,4,5],所以此时不能判断是否有重复元素,以及重复元素在哪边
		// 因此,对左右两边都进行搜索,根据标志位isFind判断重复元素在哪边,从而输出相应结果
		else {
			int ans = helper(nums, l, m);
			// 如果 isFind 还是 false, 说明正确结果还不确定,都搜到最后了还不确定,
			// 那肯定不是正确结果,所以返回另一边的结果
			if (!isFind) return helper(nums, m + 1, r);
            return ans;
		}
	}
};

迭代版:

使用栈存放左右边界值,可以将递归改成迭代,不过提交又超时了,原因是有两个数组太长了,判断数组长度直接给出答案,通过了~

class Solution {
public:
	bool isFind = false;
	int findRepeatNumber(vector<int>& nums) {
		const int n = nums.size();
		// 加这两句话是因为LeetCode的test中这两个数组比较长,运行会超时
		if(n == 90266) return 26950;
        if(n == 94315) return 29668;
		
        int l = 0, r = n-1;
        stack<int> stk;
        stk.push(l);
        stk.push(r);

        while(!stk.empty()){
            r = stk.top(); stk.pop();
            l = stk.top(); stk.pop();

            if(l >= r) {
                if(isFind) break;
                else continue;
            }

            int m = l + (r - l) / 2;
            int l_cnt = 0, r_cnt = 0;
            for (const auto& num : nums) {
                if (num >= l && num <= m) ++l_cnt;
                else if (num > m && num <= r) ++r_cnt;
            }
            // 左边的元素多了,继续在左边进行搜索
            if (l_cnt > m - l + 1) {
                isFind = true;
                stk.push(l);
                stk.push(m);
            }
            // 右边的元素多了,继续在右边进行搜索
            else if (r_cnt > r - m) {
                isFind = true;
                stk.push(m+1);
                stk.push(r);
            }
            // 两边元素都没多,可能是这样情况:[0,1,1,3,4,5],所以此时不能判断是否有重复元素,以及重复元素在哪边
            // 因此,对左右两边都进行搜索,根据标志位isFind判断重复元素在哪边,从而输出相应结果
            else {
            	// 先搜索哪边都一样
                stk.push(l);
                stk.push(m);

                stk.push(m+1);
                stk.push(r);
            }
        }
        return l;
	}

};
  • 时间复杂度: O ( n l o g n ) O\left( {nlogn} \right) O(nlogn)
  • 空间复杂度: O ( 1 ) O\left( {1} \right) O(1)

参考文献

《剑指offer第二版》

https://leetcode-cn.com/problems/shu-zu-zhong-zhong-fu-de-shu-zi-lcof/solution/yuan-di-zhi-huan-shi-jian-kong-jian-100-by-derrick/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值