这个月每日一题有很多没做的,主要重心放在并查集和二分查找的题。
4. 寻找两个正序数组的中位数
题目要求:
给定两个大小为 m 和 n 的正序(从小到大)数组nums1
和nums2
。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为(O(log{m+n}))。
你可以假nums1
和nums2
不会同时为空。
思路:
时间复杂度限制在对数级别,显然是要使用二分查找。用二分查找将两个数组A, B
分为两组,第一组包含A[:imid], B[:jmid]
,第二组包含A[imid:], B[jmid:]
,这两组恰好将他们“平分”。当两个数组的长度和为奇数时,中位数即为第二组中最小的数;当长度和为偶数时,中位数即第一组中最大的数和第二组中最小的数的平均值。通过二分查找找到imid
和jmid
。
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m = len(nums1)
n = len(nums2)
imin, imax = 0, m # 此处的初始化是为了后续计算jmid比较方便
while(imin <= imax):
imid = imin + (imax - imin) // 2
jmid = (m + n - 2 * imid) // 2
if(imid > 0 and nums1[imid - 1] > nums2[jmid]):
imax = imid - 1
elif(imid < m and nums2[jmid - 1] > nums1[imid]):
imin = imid + 1
else:
if(imid == m):
minright = nums2[jmid]
elif(jmid == n):
minright = nums1[imid]
else:
minright = min(nums1[imid],nums2[jmid])
if(imid == 0):
maxleft = nums2[jmid - 1]
elif(jmid == 0):
maxleft = nums1[imid - 1]
else:
maxleft = max(nums1[imid - 1],nums2[jmid - 1])
if((m + n) % 2) == 1:
return minright
return (maxleft + minright) / 2
7. 整数反转
题目要求:
给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
class Solution:
def reverse(self, x: int) -> int:
s = str(x)
if s[0] == '-':
ans = int('-' + s[1:][::-1])
else:
ans = int(s[::-1])
return ans if -2147483648 <= ans <= 2147483647 else 0
14. 最长公共前缀
题目要求:
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串""
。
思路一:
第一时间想到的思路是根据第一个字符串作为基准,逐字符地跟之后的字符串进行比较。
class Solution:
def longestCommonPrefix(self, strs: List[str]) -> str:
n = len(strs)
if n == 0:
return ""
# 以第一个字符串为基准
benchmark = strs[0]
length = [len(s) for s in strs]
m = length[0]
ans = ""
for i in range(m):
for j in range(1, n):
if length[j] < i + 1:
return ans
if strs[j][i] != benchmark[i]:
return ans
ans += benchmark[i]
return ans
思路二:
python中字符串是可以比较大小的,看了评论区才想起来。字符大小的比较是根据ascII值的大小和字符串的长度,因此只需要求最大字符串和最小字符串的最大公共前缀即可。
class Solution:
def longestCommonPrefix(self, strs: List[str]) -> str:
if not strs:
return ""
smin = min(strs)
smax = max(strs)
ans = ""
for i in range(len(smin)):
if smin[i] != smax[i]:
return ans
ans += smin[i]
return ans
16. 最接近的三数之和
题目要求:
给定一个包括n
个整数的数组nums
和 一个目标值target
。找出nums
中的三个整数,使得它们的和与target
最接近。返回这三个数的和。假定每组输入只存在唯一答案。
思路:
排序+双指针。先对数组进行排序,再遍历数组。用双指针(头尾)枚举当前元素之后的元素,当这三数之和大于target
时,尾指针左移;当小于target
时,头指针右移。
class Solution:
def threeSumClosest(self, nums: List[int], target: int) -> int:
nums.sort()
bestMatch = float('inf')
n = len(nums)
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
lo, hi = i + 1, n - 1
while lo < hi:
s = nums[i] + nums[lo] + nums[hi]
if s == target:
return target
if abs(s - target) < abs(bestMatch - target):
bestMatch = s
if s > target:
hi -= 1
while lo < hi and nums[hi] == nums[hi + 1]:
hi -= 1
else:
lo += 1
while lo < hi and nums[lo] == nums[lo - 1]:
lo += 1
return bestMatch
20. 有效的括号
题目要求:
给定一个只包括'(',')','{','}','[',']'
的字符串,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
思路:
栈。遇到左括号就进栈,遇到右括号就出栈并判断是否可以匹配。注意栈不空才能出栈。
class Solution:
def isValid(self, s: str) -> bool:
stack = []
hashmap = {'(': ')', '[': ']', '{': '}'}
for ch in s:
if ch in hashmap:
stack.append(ch)
elif not stack:
return False
elif hashmap[stack.pop()] != ch:
return False
return not stack
24. 两两交换链表中的节点
题目要求:
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
思路:
跟之前做过的25. K 个一组翻转链表有点类似,不过这题简单多了。需要记录一个前驱结点,以便交换之后可以重新将链表连接起来。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def swapPairs(self, head: ListNode) -> ListNode:
L = ListNode(0)
L.next = head
pre = L
# 正偶数个结点才需要交换,只剩一个就不用交换了
while head and head.next:
p, q = head, head.next
pre.next = q
p.next = q.next
q.next = p
pre = p
head = head.next
return L.next
28. 实现 strStr()
题目要求:
实现strStr()
函数。
给定一个haystack
字符串和一个needle
字符串,在haystack
字符串中找出needle
字符串出现的第一个位置 (从0开始)。如果不存在,则返回 -1。
思路一:
这题考察字符匹配算法,先用比较简单的顺序匹配。
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
if not needle:
return 0
m, n = len(haystack), len(needle)
for i in range(m - n + 1):
# 防止数组索引越界
for j in range(n):
flag = True
if haystack[i + j] != needle[j]:
flag = False
break
if flag:
return i
return -1
思路二:
KMP算法。复习一哈
34. 在排序数组中查找元素的第一个和最后一个位置
题目要求:
给定一个按照升序排列的整数数组nums
,和一个目标值target
。找出给定目标值在数组中的开始位置和结束位置。
你的算法时间复杂度必须是(O(log n))级别。
如果数组中不存在目标值,返回[-1, -1]
。
class Solution:
def lower(self, nums: List[int], target: int):
n = len(nums)
left, right = 0, n
while left < right:
mid = left + (right - left) // 2
if nums[mid] >= target:
right = mid
elif nums[mid] < target:
left = mid + 1
return left if left < n and nums[left] == target else -1
def higher(self, nums: List[int], target: int):
n = len(nums)
left, right = 0, n
while left < right:
mid = left + (right - left) // 2
if nums[mid] > target:
right = mid
elif nums[mid] <= target:
left = mid + 1
return left - 1
def searchRange(self, nums: List[int], target: int) -> List[int]:
if not nums:
return [-1, -1]
lo = self.lower(nums, target)
# 二分查找的次数少一半,大概能快点?
if lo == -1:
return [-1, -1]
hi = self.higher(nums, target)
return [lo, hi]
35. 搜索插入位置
题目要求:
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
n = len(nums)
left, right = 0, n
while left < right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] > target:
right = mid
elif nums[mid] < target:
left = mid + 1
return left
38. 外观数列
题目要求:
「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。前五项如下:
1. 1
2. 11
3. 21
4. 1211
5. 111221
1
被读作"one 1"
,即11
。
11
被读作"two 1s"
,即21
。
21
被读作"one 2", "one 1"
,即1211
。
给定一个正整数 n(1 ≤ n ≤ 30),输出外观数列的第 n 项。注意:整数序列中的每一项将表示为一个字符串。
思路:
写一个辅助函数,用于生成一个字符串的外观数列。该辅助函数遍历整个字符串,遇到当前元素与前一个元素相同时,计数加一,否则将计数和之前的元素加入字符串中。将ans
初始化为'1'
,将这个辅助函数运行n - 1
次即可。
class Solution:
def count(self, s: str):
n = len(s)
ans = ""
cnt = 1
temp = s[0]
for i in range(1, n):
if s[i] == temp:
cnt += 1
else:
ans += str(cnt)
ans += temp
cnt = 1
temp = s[i]
ans += str(cnt)
ans += temp
return ans
def countAndSay(self, n: int) -> str:
ans = "1"
for i in range(1, n):
ans = self.count(ans)
return ans
67. 二进制求和
题目要求:
给你两个二进制字符串,返回它们的和(用二进制表示)。输入为非空字符串且只包含数字1
和0
。
提示:
- 每个字符串仅由字符
'0'
或'1'
组成。 1 <= a.length, b.length <= 10^4
- 字符串如果不是
"0"
,就都不含前导零。
思路:
字符串求和,会出现一种可能性,就是最后会出现进位,字符串需要增加一位。一个办法就是将字符串倒过来,就比较好处理多出的那一位。(其实Python也不需要想这么多,字符串往上加就完事了)
class Solution:
def addBinary(self, a: str, b: str) -> str:
if a[0] == '0':
return b
if b[0] == '0':
return a
ans = ''
a, b = a[::-1], b[::-1]
i = j = carry = 0
while i < len(a) or j < len(b) or carry:
n1 = int(a[i]) if i < len(a) else 0
n2 = int(b[j]) if j < len(b) else 0
curBit = (n1 + n2 + carry) % 2
carry = (n1 + n2 + carry) // 2
ans += str(curBit)
i += 1
j += 1
return ans[::-1]
70. 爬楼梯
题目要求:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。
思路:
这题就是斐波那契数列,空间复杂度还可以优化成(O(1)),不过懒得写了。
class Solution:
def climbStairs(self, n: int) -> int:
if n == 1:
return 1
dp = [1] * (n + 1)
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[-1]
125. 验证回文串
题目要求:
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
思路:
双指针。本题忽略大小写,于是可以先把字符串中的大写字母统一转为小写。之后左右指针遇到不是字母或者数字的字符串就跳过,遇到了再判断是否相等。
class Solution:
def isPalindrome(self, s: str) -> bool:
if not s:
return True
s = s.lower()
n = len(s)
left, right = 0, n - 1
while left < right:
while left < right and not ('a' <= s[left] <= 'z' or '0' <= s[left] <= '9'):
left += 1
while right > left and not ('a' <= s[right] <= 'z' or '0' <= s[right] <= '9'):
right -= 1
if left < right:
if s[left] == s[right]:
left += 1
right -= 1
else:
return False
return True
这题用了s = s.lower()
,而python的字符串是不可变的,于是空间复杂度还是(O(n)),顶级白忙活。不过稍微修改一下就可以保证空间复杂度还是(O(1))了。
class Solution:
def isPalindrome(self, s: str) -> bool:
if not s:
return True
n = len(s)
left, right = 0, n - 1
while left < right:
while left < right and not ('a' <= s[left].lower() <= 'z' or '0' <= s[left].lower() <= '9'):
left += 1
while right > left and not ('a' <= s[right].lower() <= 'z' or '0' <= s[right].lower() <= '9'):
right -= 1
if left < right:
if s[left].lower() == s[right].lower():
left += 1
right -= 1
else:
return False
return True
139. 单词拆分
题目要求:
给定一个非空字符串 s 和一个包含非空单词列表的字典wordDict
,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
思路:
动态规划。dp[i]
指的是以第i
个字符为结尾的字符串是否能拆分为单词。而状态转移方程应该为dp[i] = dp[j]&&([j, i - 1]是否为单词)
。
直觉上来说,dp[0] = True
,因为dp[i] = dp[0] and dp[i]
。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp = [False]*(len(s)+1)
dp[0] = True
for i in range(1,len(s)+1):
for j in range(i,-1,-1):
# 转移公式
if dp[j] == True and s[j:i] in wordDict:
dp[i] = True
break
return dp[-1]
162. 寻找峰值
题目要求:
峰值元素是指其值大于左右相邻值的元素。
给定一个输入数组nums
,其中nums[i] ≠ nums[i+1]
,找到峰值元素并返回其索引。
数组可能包含多个峰值,在这种情况下,返回任何一个峰值所在位置即可。
你可以假设nums[-1] = nums[n] = -∞
。
思路:
二分。
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
n = len(nums)
if n == 1:
return 0
left, right = 0, n - 2
# 循环结束后left就是山顶
while left <= right:
top = left + (right - left) // 2
if nums[top] > nums[top + 1]:
right = top - 1
else:
left = top + 1
return left
297. 二叉树的序列化与反序列化
题目要求:
序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。
请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。
思路一:
看了一下这个函数的调用方式,可以取巧(不过这么做也没什么意义,正常应该是用某种顺序遍历二叉树,再将这个遍历顺序恢复成二叉树)。
class Codec:
def serialize(self, root):
"""Encodes a tree to a single string.
:type root: TreeNode
:rtype: str
"""
return root
def deserialize(self, data):
"""Decodes your encoded data to tree.
:type data: str
:rtype: TreeNode
"""
return data
思路二:
首先进行层次遍历,将二叉树序列化,如果要加入队列中的结点为空,也将其加入;当在队列中遇到空的结点时,就把null
放入结果中。
在进行反序列化时,同样地也要用到队列。当队列不为空时,弹出一个元素,若当前列表中对应的元素为null
,则其左指针为空...这么一来恰好对应的层次遍历的顺序。
class Codec:
def serialize(self, root):
"""Encodes a tree to a single string.
:type root: TreeNode
:rtype: str
"""
if not root:
return "[]"
queue = collections.deque()
queue.append(root)
ans = []
while queue:
node = queue.popleft()
if node:
ans.append(str(node.val))
queue.append(node.left)
queue.append(node.right)
else:
ans.append("null")
return '[' + ','.join(ans) + ']'
def deserialize(self, data):
"""Decodes your encoded data to tree.
:type data: str
:rtype: TreeNode
"""
if data=='[]':
return None
vals, i = data[1: -1].split(','), 1
root = TreeNode(int(vals[0]))
queue = collections.deque()
queue.append(root)
while queue:
node = queue.popleft()
if vals[i] != "null":
node.left = TreeNode(int(vals[i]))
queue.append(node.left)
i += 1
if vals[i] != "null":
node.right = TreeNode(int(vals[i]))
queue.append(node.right)
i += 1
return root
209. 长度最小的子数组
题目要求:
给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和(geq s)的长度最小的连续子数组,并返回其长度。如果不存在符合条件的连续子数组,返回 0。
思路:
滑动窗口。用subSum
记录两个指针之间子数组的和,lo
指针每右移一次,subSum
就减去对应的元素;hi
指针每右移一次,subSum
就加上对应的元素。当subSum>=s
就记录子数组的长度。
class Solution:
def minSubArrayLen(self, s: int, nums: List[int]) -> int:
n = len(nums)
ans = n + 2 # 要保证ans足够大
lo = hi = subSum = 0
while hi < n:
subSum += nums[hi]
while subSum >= s:
ans = min(ans, hi - lo + 1)
subSum -= nums[lo]
lo += 1
hi += 1
return 0 if ans == n + 2 else ans
319. 灯泡开关
题目要求:
初始时有 n 个灯泡关闭。 第 1 轮,你打开所有的灯泡。 第 2 轮,每两个灯泡你关闭一次。 第 3 轮,每三个灯泡切换一次开关(如果关闭则开启,如果开启则关闭)。第 i 轮,每 i 个灯泡切换一次开关。 对于第 n 轮,你只切换最后一个灯泡的开关。找出 n 轮后有多少个亮着的灯泡。
思路:
可以用模拟法来模拟每一轮灯泡的开关,但是太耗时了。找规律可以发现只有完全平方数处的灯泡最终才会是亮的。
class Solution:
def bulbSwitch(self, n: int) -> int:
cnt = 1
while cnt ** 2 <= n:
cnt += 1
return cnt - 1
392. 判断子序列
题目要求:
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
你可以认为 s 和 t 中仅包含英文小写字母。字符串 t 可能会很长(长度 ~= 500,000),而 s 是个短字符串(长度 <=100)。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
思路:
遍历长的字符串,用一个指针指向短字符串中的字符。当两个字符相等时,指针右移。若最终指针移至最后,说明是子序列。
class Solution:
def isSubsequence(self, s: str, t: str) -> bool:
m, n = len(s), len(t)
if m == 0:
return True
ptr = 0
for i in range(n):
if t[i] == s[ptr]:
ptr += 1
if ptr == m:
return True
return False
540. 有序数组中的单一元素
题目要求:
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
思路:
看到有序数组,显然要想到二分查找。整体思路确定了,但是有很多细节需要推敲。比如,要怎么判断mid
处的元素是不是单一元素呢?如果要分别判断mid
左右的元素就很冗余,可以让mid
是偶数,这样可以保证mid
左侧一定有偶数个元素。如果mid
右侧的元素与其不相等,则单一元素一定在左侧,否则就在右侧。而右指针的初始化也需要调整,详见代码注释。
class Solution:
def singleNonDuplicate(self, nums: List[int]) -> int:
n = len(nums)
# 将right赋值为n - 1,一是为了不超出数组边界
# 二是当nums[n-1]只出现一次时,也可以正确返回
left, right = 0, n - 1
while left < right:
mid = left + (right - left) // 2
if mid % 2 == 1:
mid -= 1
# 令mid为偶数,则mid左边就有偶数个元素
if nums[mid] == nums[mid + 1]:
left = mid + 2
else:
right = mid
return nums[left]
547. 朋友圈
班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果(M_{ij}=1),表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
思路:
只要直接套用以上的并查集模板即可,最后统计其中树根的个数即可。由上面的假设可知,树根结点满足path[i]=i
。感觉这种不那么复杂的题也不需要按秩合并或者路径压缩了,直接来还快一些。
class Solution:
def findCircleNum(self, M: List[List[int]]) -> int:
def find(x: int, path: List[int]) -> int:
root = x
while path[root] != root:
root = path[root]
return root
def unionSet(x: int, y: int, path: List[int], rank: List[int]) -> bool:
x_root = find(x, path)
y_root = find(y, path)
if x_root == y_root: # 未发生合并,返回False
return False
else: # 需要合并,返回True
if rank[x_root] > rank[y_root]:
path[y_root] = x_root
elif rank[x_root] < rank[y_root]:
path[x_root] = y_root
else:
path[y_root] = x_root
rank[x_root] += 1
return True
N = len(M)
path = [i for i in range(N)]
rank = [0] * N
for i in range(N):
for j in range(i, N):
if M[i][j] == 1 and i != j:
unionSet(i, j, path, rank)
cnt = 0
for i in range(N):
if path[i] == i:
cnt += 1
return cnt
684. 冗余连接
题目要求:
在本问题中, 树指的是一个连通且无环的无向图。
输入一个图,该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。
结果图是一个以边组成的二维数组。每一个边的元素是一对[u, v]
,满足u < v
,表示连接顶点u
和v
的无向图的边。
返回一条可以删去的边,使得结果图是一个有着N个节点的树。如果有多个答案,则返回二维数组中最后出现的边。答案边[u, v]
应满足相同的格式u < v
。
思路:
显然要使用并查集,当边的两个顶点不在集合中时,将它们加入集合;在集合中时,说明有环,就将这条边返回。
class Solution:
def find(self, u: int, path: List[int]):
u_root = path[u]
while u != u_root:
u = u_root
u_root = path[u_root]
return u_root
def union(self, u: int, v: int, path: List[int]):
u_root = self.find(u, path)
v_root = self.find(v, path)
if u_root == v_root:
return False
else:
path[v_root] = u_root
return True
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
path = [i for i in range(n + 1)]
for edge in edges:
if not self.union(edge[0], edge[1], path):
return edge
704. 二分查找
题目要求:
给定一个 n 个元素有序的(升序)整型数组nums
和一个目标值target
,写一个函数搜索nums
中的`target,如果目标值存在返回下标,否则返回 -1。
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
left, right = 0, n - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
721. 账户合并
题目要求:
给定一个列表accounts
,每个元素accounts[i]
是一个字符串列表,其中第一个元素accounts[i][0]
是 名称 (name),其余元素是emails
表示该帐户的邮箱地址。
现在,我们想合并这些帐户。如果两个帐户都有一些共同的邮件地址,则两个帐户必定属于同一个人。请注意,即使两个帐户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。一个人最初可以拥有任意数量的帐户,但其所有帐户都具有相同的名称。
合并帐户后,按以下格式返回帐户:每个帐户的第一个元素是名称,其余元素是按顺序排列的邮箱地址。accounts
本身可以以任意顺序返回。
思路:
并查集。用两个字典,记录邮箱地址和ID之间的关系(ID->邮箱,邮箱->ID)。当遇到相同的邮箱对应两个ID时,就将两个ID放入同一个集合。最后根据邮箱->ID的字典,组织要返回的结果。(如果在合并操作的同时将邮箱列表合并时间复杂度可能会更好,而且代码会好看一些)并不行,一开始给的accounts
就不是有序的。
class Solution:
def find(self, x, path):
if x != path[x]:
path[x] = self.find(path[x], path)
return path[x]
def union(self, x, y, path):
x_root, y_root = self.find(x, path), self.find(y, path)
if x_root != y_root:
path[y_root] = x_root
def accountsMerge(self, accounts) -> List[List[str]]:
n = len(accounts)
path = [i for i in range(n)]
ans = []
# 邮箱 -> 用户编号
email2id = {}
for i, acc in enumerate(accounts):
for email in acc[1:]:
if email not in email2id:
email2id[email] = i
else:
self.union(i, email2id[email], path)
# 用户编号 -> 邮箱
id2email = {}
for email, personId in email2id.items():
root = self.find(personId, path)
if root not in id2email:
id2email[root] = []
id2email[root].append(email)
for root, emailList in id2email.items():
tmp = []
tmp.append(accounts[root][0])
emailList.sort()
tmp.extend(emailList)
ans.append(tmp)
return ans
744. 寻找比目标字母大的最小字母
题目要求:
给你一个排序后的字符列表letters
,列表中只包含小写英文字母。另给出一个目标字母target
,请你寻找在这一有序列表里比目标字母大的最小字母。在比较时,字母是依序循环出现的。举个例子:
如果目标字母target = 'z'
并且字符列表为letters = ['a', 'b']
,则答案返回'a'
思路:
按照题中的示例,如果不存在比目标字母大的最小字母,则应返回letters[0]
。
class Solution:
def nextGreatestLetter(self, letters: List[str], target: str) -> str:
n = len(letters)
left, right = 0, n - 1
while left <= right:
mid = left + (right - left) // 2
if letters[mid] <= target:
left = mid + 1
else:
right = mid - 1
return letters[left] if left < n else letters[0]
990. 等式方程的可满足性
题目要求:
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程equations[i]
的长度为 4,并采用两种不同的形式之一:"a==b"
或"a!=b"
。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回true
,否则返回false
。
思路:
并查集。首先遍历等式,把相等的变量都放在同一个集合中。接着遍历不等式,如果发现不相等的变量在同一个集合中,则一定是不可满足的。
class Solution:
def find(self, x, path):
if x != path[x]:
path[x] = self.find(path[x], path)
return path[x]
def equationsPossible(self, equations: List[str]) -> bool:
path = [i for i in range(26)]
for equation in equations:
if equation[1] == '=':
x = ord(equation[0]) - 97
y = ord(equation[3]) - 97
x_root, y_root = self.find(x, path), self.find(y, path)
if x_root != y_root:
path[y_root] = x_root
for equation in equations:
if equation[1] == '!':
x = ord(equation[0]) - 97
y = ord(equation[3]) - 97
x_root, y_root = self.find(x, path), self.find(y, path)
if x_root == y_root:
return False
return True
1014. 最佳观光组合
题目要求:
给定正整数数组A
,A[i]
表示第i
个观光景点的评分,并且两个景点i
和j
之间的距离为j - i
。
一对景点(i < j
)组成的观光组合的得分为A[i] + A[j] + i - j
:景点的评分之和减去它们两者之间的距离。
返回一对观光景点能取得的最高分。
思路:
这题要求的就是A[i] + A[j] + i - j
的最大值,可以转变一下思路,将前面的式子写成A[i] + i + A[j] - j
,而每个A[i] + i
和A[j] - j
实际上都是固定的,由于题目要求i < j
,只要记录A[i] + i
的最大值,遍历j
即可。
class Solution:
def maxScoreSightseeingPair(self, A: List[int]) -> int:
n = len(A)
lePart = A[0]
ans = A[0] + A[1] - 1
for i in range(1, n):
ans = max(lePart + A[i] - i, ans)
lePart = max(A[i] + i, lePart)
return ans
1300. 转变数组后最接近目标值的数组和
题目要求:
给你一个整数数组 arr 和一个目标值 target ,请你返回一个整数 value ,使得将数组中所有大于 value 的值变成 value 后,数组的和最接近 target (最接近表示两者之差的绝对值最小)。
如果有多种使得和最接近 target 的方案,请你返回这些整数中的最小值。
请注意,答案不一定是 arr 中的数字。
思路:
枚举。用二分查找找到每次要改变的元素的最右边的索引,然后把数组在这个索引右边的所有元素都看作是这个元素。
class Solution:
def biSrch(self, arr, tgt, length):
left, right = 0, length
while left < right:
mid = (left + right) // 2
val = arr[mid]
if val < tgt:
left = mid + 1
elif val >= tgt:
right = mid
return left
def findBestValue(self, arr: List[int], target: int) -> int:
n = len(arr)
arr.sort()
preSum = [0] * (n + 1)
for i in range(n):
preSum[i + 1] = preSum[i] + arr[i]
ans = 0
diff = target
for i in range(1, arr[-1] + 1):
index = self.biSrch(arr, i, n)
curSum = preSum[index] + (n - index) * i
if abs(curSum - target) < diff:
ans, diff = i, abs(curSum - target)
return ans
1319. 连通网络的操作次数
题目要求:
用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用connections
表示,其中connections[i] = [a, b]
连接了计算机 a 和 b。
网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。
给你这个计算机网络的初始布线connections
,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。
思路:
并查集。只要求出有几个连通子图,最终需要的操作次数就是连通子图数减一。
class Solution:
def find(self, x, path):
if x != path[x]:
path[x] = self.find(path[x], path)
return path[x]
def makeConnected(self, n: int, connections: List[List[int]]) -> int:
# 如果边的数量小于n - 1,则一定无法将图联通起来
if len(connections) < n - 1:
return -1
path = [i for i in range(n)]
for x, y in connections:
x_root, y_root = self.find(x, path), self.find(y, path)
if x_root != y_root:
path[y_root] = x_root
cnt = 0
for i in range(n):
if i == path[i]:
cnt += 1
return cnt - 1
剑指 Offer 53 - II. 0~n-1中缺失的数字
题目要求:
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0~n-1之内。在范围0~n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
思路:
认真看题目要求,会发现如果没有缺失值,则有nums[i] == i
。而如果缺失了一个数字,那么就会发生错位。因此就把以上的式子作为二分查找的判断条件。
class Solution:
def missingNumber(self, nums: List[int]) -> int:
n = len(nums)
left, right = 0, n - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == mid:
left = mid + 1
else:
right = mid - 1
return left
面试题 02.01. 移除重复节点
题目要求:
编写代码,移除未排序链表中的重复节点。保留最开始出现的节点。
思路:
遍历一遍,再加个字典,可以快速地找到已经遇到过的元素。
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def removeDuplicateNodes(self, head: ListNode) -> ListNode:
if not head:
return head
p = head
hashmap = {}
hashmap[p.val] = 1
while p.next:
if p.next.val in hashmap:
p.next = p.next.next
else:
hashmap[p.next.val] = 1
p = p.next
return head