python经典面试算法题4.1:如何找出数组中唯一的重复元素

本题目摘自《Python程序员面试算法宝典》,我会每天做一道这本书上的题目,并分享出来,统一放在我博客内,收集在一个分类中。

【百度面试题】

难度系数:⭐⭐⭐
考察频率:⭐⭐⭐⭐

题目描述:数字1 ~ 1000放在含有1001个元素的数组中,其中只有唯一的一个元素重复,其他数字均只出现一次。设计一个算法,将重复元素找出来,要求每个数组元素只能访问一次。

进阶:在上面题目描述中,如果不使用辅助空间,能否设计一个算法实现?

方法一:空间换时间
首先分析题目所要达到的目标以及其中的限定条件。从题目中可以发现,本题的目标是在一个有且仅有一个元素重复的数组中找到这个唯一的重复元素,限定条件是每个数组元素只能访问一次。
在不考虑进阶条件的情况下,我们可以通过字典的key来进行去重,我们可以把数字当作key,出现的次数记作value

from collections import defaultdict

def find_elem(array):
	dic = defaultdict(lambda : 0)  # 构造一个缺省字典,当出现KeyError的时候压制,并创建键值对为key - 0
    for elem in array:
        if dic[elem] == 1:   # 这一步如果dic中没有elem不会报错因为dic是defaultdict
            return elem
        dic[elem] += 1
    return "can't find it"

print(find_elem([1, 3, 4, 2, 5, 3]))   # 3


# 如果defaultdict看不懂可以看下面使用get方法的版本

def find_elem(array):
	dic = {}   # 使用普通字典  
	for elem in array:
		dic[elem] = dic.get(elem, 0) + 1   # get不到elem会返回0, 然后再+1,再创建 k - v 对
		if dic[elem] == 2:  # 重复的元素
			return elem
	return "can't find it"

这种方法很容易想到,时间复杂度为O(n),空间复杂度也是O(n),这种思想在工作中可以使用,比如一个项目急着上线,没有什么时间给你雕琢程序,那么我们可以考虑使用空间来换取程序运行的时间。

方法二:累加求和法
计算机技术与数学本身是一家,抛开计算机专业知识不提,这个问题可以回归成一个数学问题。数学问题的目标是在一个数字序列中寻找重复的那个数。题目描述是1 ~ 1000个数,有一个重复,那么我们把这1001个数加起来再减去 (1 + 2 + … + 1000)得到的就是重复的那个数了。

def find_it(array):
	sum = 0
	one_to_thousand = -1001
	for i, v in enumerate(array, 1):
		sum += v
		one_to_thousand += i
	return sum - one_to_thousand 

print(find_it([i for i in range(1, 1001)] + [56]))

分析:时间复杂度O(n), 空间复杂度O(1),但是计算也挺费时间的。 其实这里可以使用python的第三方库,科学计算库numpy进行并行计算,numpy.array.sum()

方法三:异或法
根据异或运算的性质可以直到,当相同元素异或时,运算结果为0,当相异元素异或时,运算结果非0,任何数字与数字0进行异或运算,其运算结果为该数。因为1001个数字是1 ~ 1000再加上一个大于0小于等于1000的数字,所以我们把这1001个数字和1到1000异或,最后会转变成0和重复的那个数字进行异或,得到的就是重复的数字。
例如数组(1, 3, 4, 2, 5, 3),进行运算:(1, 3, 4, 2, 5, 3)^ (1, 2, 3, 4, 5) = 1 ^ 1 ^ 2 ^ 2 ^ 3 ^ 3 ^ 4 ^ 4 ^ 5 ^ 5 ^ 3 = 0 ^ 3 = 3。

def find_it(array):
   elem = 0
   for i, v in enumerate(array):
       elem ^= i ^ v
   return elem

print(find_it([1, 3, 4, 2, 5, 3]))

这种方法的时间复杂度是O(n), 没有申请额外的存储空间,进行位运算速度还算快。

方法四:数据映射法
数据的取值操作可以看作是一个特殊的函数f:D —> R, 定义域D为下标值 0 ~ 1000,值域为1 ~ 1000, 如果对任意一个数 i, 把f(i)叫做它的后继, i叫做f(i)的前驱。重复的那个数字有两个前驱,所以我们可以把每个数的前驱置为负数,当第二次遇到重复的数字时,它的前驱已经被置为负数了,这个就是重复的数值,返回即可。

# 数据影射法
def find_it(array):
    p = 0
    while array[p] >= 0:
        p = array[p]
        array[p] *= -1
    return abs(array[p])

print(find_it([1, 2, 4, 2, 5, 3]))  # 2
print(find_it([1, 3, 4, 2, 5, 3]))  # 3

这个算法的时间复杂度是O(n),没有申请辅助的空间。但是这种方法修改了原本列表中的元素。



欢迎小伙伴们加入我创建的python交流群:625988679



在这里插入图片描述

posted @ 2019-10-26 09:12  段明  阅读(1691)  评论(0编辑  收藏  举报