先抛开leetcode 求重复数的各种限制而言,
题目
给定一个数组,求出数组中重复的数,并打印出来
解决方案有哪些呢?
方法1. 暴力求解
思路:
第1个数,和后面 n-1个数比较,是否有重复
第2个数,和后面n-2个数比较,是否有重复
一共执行n轮
有点像 选择排序,一直拿前面的数和后面的数做比较
特殊处理:
需要有个list存储已经重复的数,下次比较如果出现在list中,直接跳过,比如数组 [1,3,2,3,5,3],如果不过滤, 会 第2个3和 第3个3 都多判断一次
时间复杂度:
O(n^2)
代码如下:
def same_number(a):
n=len(a)
list=[]
if n>1:
for i in range(0,n-1):
for j in range(i+1,n):
if a[j]==a[i]:
if a[i] in list: # 如果下一个数在list中,代表已经被记录过,程序进入下一个for 循环,不再打印
continue
else:
print(a[i])
else:
continue
list.append(a[j]) # 每次重复的数存储起来
else:
print("数组长度不足2,肯定没有重复的")
a=[1,3,4,3,5,3,5,7,2]
b=[1]
same_number(b)
方法2: 先排序,再比较
思路:
先进行排序,然后比较相邻的数即可
不足:会改变list
时间复杂度
sort排序的 O(n.logn)
# 方法5: 先排序,再比较,会改变list,时间复杂度为排序的 O(n.logn)
def same_number_sort(a):
n=len(a)
if n > 1:
a.sort()
for i in range(0,n-1):
if a[i+1]==a[i]:
print(a[i])
else:
print("数组长度不足2,肯定没有重复的")
a=[1,3,4,5,3,5,7,2]
same_number_sort(a)
方法3: 使用哈希表
思路:
先设置一个空的集合,用来存储重复的数据
比如:第1次,set为空,a[0]不在set中,set将a[0]存储进来
第2次,如果 a[1]在 set中,说明有重复,和a[0]相同
其实就是开辟一个新空间,然后让每个数和里面存储的已经遍历过的数做比较。
为啥用set不用list?
刚开始我用的list,也可以实现效果,但是需要注意 set和list 插入和查找元素的时间复杂度的问题
for循环中,每次in操作(即查找)在list中复杂度线性是O(n),在set和dict数据结构中复杂度为O(1)
为了实现整体线性时间复杂性,我们需要能够在恒定时间内将元素插入数据结构,并查找它们,set 很好地满足这些约束
时间复杂度:
O(n)
空间复杂度:
O(n)
def same_number_hash(a):
n=len(a)
s=set()
for i in range(0,n):
if a[i] in s: # 查找,时间复杂度O(1),如果是列表,则时间复杂度O(n)
print("重复的数为:",a[i])
s.add(a[i])
a=[1,3,4,3,5,5,7,2]
same_number_hash(a)
方法4: 使用二分法 (适用于只有一个重复的数时)
思路:
对要定位的“数”做二分,而不是对数组的索引做二分。要定位的“数”根据题意在 1和 n 之间,每一次二分都可以将搜索区间缩小一半。
以 [1, 2, 2, 3, 4, 5, 6, 7] 为例,一共有 88个数,每个数都在 1和 7 之间。
11和 7 的中位数是 4,遍历整个数组,统计小于 44的整数的个数,至多应该为 3个,如果超过 3个就说明重复的数存在于区间 [1,4)[1,4) (注意:左闭右开)中;否则,重复的数存在于区间 [4,7][4,7](注意:左右都是闭)中。
这里小于 4的整数有 4个(它们是 1, 2, 2, 3),因此砍掉右半区间,连中位数也砍掉。
以此类推,最后区间越来越小,直到变成 1 个整数,这个整数就是我们要找的重复的数。
二分法详细介绍:链接
时间复杂度:
O(nlog(n))
空间复杂度:
O(1)
# 方法2: 二分查找,
def same_number_binary(nums):
left = 1
right = len(nums) - 1
while (left < right):
mid = (left + right) // 2
count = 0
for num in nums:
if (num <= mid):
count += 1
if (count <= mid):
left = mid + 1
else:
right = mid
print("二分法查找结果是:",left)
a = [1, 3, 4, 5, 3, 5, 7, 2]
方法5: 快慢指针 (比较难理解)
思路:
分为两步:
1.找到环
2.找到环的入口(即重复元素)
找环:
1.定义快慢指针
slow=0
fast=0
2.进入循环:
slow每次走一步,即slow=nums[slow]
fast每次走两步,即fast=nums[nums[fast]]
3.当slow = = fast时,退出循环。
当快慢指针相遇时,一定在环内。此时假设slow走了k步,则fast走了2k步。
找环的入口:
1.定义新的指针find=0
2.进入循环:
find每次走一步,即find=nums[find]
slow每次走一步,即slow=nums[slow]
当两指针相遇时,即find==slow,返回find
为何相遇时,找到的就是入口:
假设起点到环的入口(重复元素),需要m步。此时slow走了n+mn+m步,其中n是环的周长c的整数倍,所以相当于slow走了m步到达入口,再走了n步。所以相遇时一定是环的入口。
快慢指针计算: 链接
时间复杂度:
O(n)
空间复杂度:
O(1)
# 方法4: 快慢指针
def same_number_point(nums):
# 把nums看成是顺序存储的链表,nums中每个元素的值是下一个链表节点的地址
# 那么如果nums有重复值,说明链表存在环,本问题就转化为了找链表中环的入口节点,因此可以用快慢指针解决
# 初始时,都指向链表第一个节点nums[0]
slow, fast = 0, 0
# 慢指针走一步,快指针走两步
slow, fast = nums[slow], nums[nums[fast]]
# 循环退出时,slow与fast相遇,相遇节点必在环中
while slow != fast:
slow, fast = nums[slow], nums[nums[fast]]
# 让before,after分别指向链表开始节点,相遇节点
before, after = 0, slow
# before与after相遇时,相遇点就是环的入口节点
while before != after:
before, after = nums[before], nums[after]
print("快慢指针实现的方法为:",before)
a=[1,3,4,5,3,5,7,2]
same_number_point(a)
leetcode287. 寻找重复数 要求:
只有上面的方法4和5符合时间和空间复杂度的要求
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。
扩展1: leetcode 136. 只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
说明:
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
示例 1:
输入: [2,2,1]
输出: 1
示例 2:
输入: [4,1,2,1,2]
输出: 4
解决方案1:使用列表 处理重复的数
思路:
定义一个列表,
相同的,remove 出去, 不同的,append 进来
最后list剩下的是只出现一次是数
pop和remove区别:
注意这里是remove ,remove是删除一个元素值为x,对值进行操作
pop弹出操作的是下标元素 ,对索引进行操作
时间复杂度:
O(n^2)
遍历 nums 花费 O(n)的时间,在列表中遍历判断是否存在这个数字也是O(n)
空间复杂度:
O(n)
实现方式:
# 方法1 :使用列表,相同的,remove 出去, 不同的,append 进来
def same_number_hash(a):
list = []
for x in a:
if x in list: # 查找,时间复杂度O(1),如果是列表,则时间复杂度O(n)
list.remove(x)
else:
list.append(x)
print("使用列表存储只出现一次的数为:",list) # 如果只有一个重复的数,就用list.pop弹出最后一个
a=[1,3,4,3,5,5,7,2]
same_number_hash(a)
解决方案2: 使用dic或者set ,降低时间复杂度
思路和1一样,不过时间复杂度降低了
思路:
dic查找,时间复杂度O(1)
先定义dic={}
如果 元素在dic中,说明已经存在,dic移除这个key值
否则,就添加到dic中
dic的添加和删除:
添加:直接赋值 dic[x]=1 value是出现的次数,这里只有不重复的key才存进来,所以value都是1
移除:dic.pop(x)
dic操作:
1.取key值:
for c in cc.keys(): 或者 for c in cc:
2.取value值:
for c in cc.values(): 或者 for c in cc:
3.弹出最后一个(key,value) :
dic.popitem() 是(2,1)
4.弹出最后一个的key
dic.popitem()[0]
时间复杂度:
O(n)
空间复杂度:
O(n)
实现方式:
# # 方法2 :使用set或dic哈希表都可以,
def same_number_hash(a):
dic={}
for x in a:
if x in dic: # 查找,时间复杂度O(1),如果不存在这个值,这个key的value存为1
dic.pop(x)
else:
dic[x]=1
# for c in dic.keys(): 如果不止一个重复的数,获取字典所有的key可以用 for c in dic.keys(): 或者 for c in dic:
# print(c)
print("使用dic弹出只出现一次的数为:",dic) # dic.popitem() 弹出的是最后一个(key,value) 是(2,1)
a=[1,3,4,3,5,5,7,2]
same_number_hash(a)
方法3: 使用异或 (比较特殊)
思路:
使用异或 ^ ,非常巧妙
所有相同的两个数异或之后等于0.
而0与唯一一个只出现一次的数异或,等于这个数本身,
所以所有的数按顺序进行异或,得到的就是这个唯一的数,非常简单
a⊕0=a
a⊕a=0
a⊕b⊕a=b
时间复杂度:
O(n)
空间复杂度:
O(1)
实现方式:
# 方法3: 使用异或 ^ 所有相同的两个数异或之后等于0.而0与唯一一个只出现一次的数异或,等于这个数本身,所以所有的数进行异或,得到的就是这个唯一的数,非常简单
def same_number_yihuo(list):
a=0
for i in list:
a=a^i
print("异或得到的结果是:",a)
a=[1,3,1,3,2,7,2]
same_number_yihuo(a)
扩展2: 142. 环形链表 II
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
方法1: set存储节点,判断节点是否已存在
思路:
使用set存储节点
如果节点已在set中,说明有环,直接返回true
不管在不在都添加当前元素,并节点后移
时间复杂度:
O(n)
空间复杂度:
O(n)
实现方法:
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
def have_clcle(head):
s=set()
while head: # 当节点不为空时
if head in s: # 节点在set中,说明有环
return True
s.add(head)
head=head.next
return False
# 先添加各个node节点
node1=ListNode(3)
node2=ListNode(2)
node3=ListNode(0)
node4=ListNode(-4)
# 按照要求声明节点之间的关系
node1.next=node2
node2.next=node3
node3.next=node4
node4.next=node2
result=have_clcle(node1)
print("使用哈希结构 判断是否有环的结果是:",result)
方法2: 快慢指针
特殊场景考虑:
1.没有节点
2.只有一个节点
3. while fast :
进行指针后移,快的和慢的重合,说明有环,相遇点就是环的入口
# 2.快慢指针
def have_clcle_by_pointer(head):
# 如果头结点为空,没有元素,直接输出没有环
if head ==None:
return False
slow,fast=head # 快慢指针都指向第一个节点
while fast: # 因为每次fast走两步,所以以fast为临界条件
if fast.next ==None: # 如果第一个节点的下一个节点为空,只有一个节点,直接返回false
return False
else:
slow=slow.next
fast=fast.next.next
if slow == fast:
return True
return False
# 先添加各个node节点
node1=ListNode(3)
node2=ListNode(2)
node3=ListNode(0)
node4=ListNode(-4)
# 按照要求声明节点之间的关系
node1.next=node2
node2.next=node3
node3.next=node4
node4.next=node2
result1=have_clcle_by_pointer(node1)
print("使用快慢指针判断是否有环的结果是:",result1)
总结:
像这种是否有重复数 和 是否有环 属于同一类 题目,考察的是对数组或链表 值的判断比较
这一类题一般需要 遍历 for a in b
这里特殊强调:
dic和set 查找,和删除,时间复杂度都是O(1)
list 为O(n)
set(集合)
set和dict类似,也是一组key的集合,但不存储value。
由于key不能重复,所以,在set中,没有重复的key。