查找1
查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。本文为查找的第一部分,包括 查找表 和 二分查找 两种基本的查找方法。
查找表,考虑基本的数据结构 字符串(str) 、列表(list) 、集合(set) 和 字典(dict)。常用collections的Counter等Python内建集合模块辅助处理问题。
二分查找,也称折半搜索算法,是一种在有序数组中查找某一特定元素的搜索算法。常见的有 按下标搜索(low = 0, high = len(nums)-1)和 按值搜索(low = min(nums), high =max(nums))。大多数情况下,当数组是有序或部分有序时是按照数组的下标进行搜索,当数组是无序的时候,就是按照数值的范围进行搜索。
本文将涉及以下题目:
分类 | 题号 | 题名 | 难度 | 标签 |
---|---|---|---|---|
查找表(str) | 205 | 同构字符串 | 简单 | 哈希表 |
查找表(str) | 242 | 有效的字母异位词 | 简单 | 排序 哈希表 |
查找表(list) | 202 | 快乐数 | 简单 | 哈希表 数学 |
查找表(list) | 290 | 单词规律 | 简单 | 哈希表 |
查找表(set) | 349 | 两个数组的交集 | 简单 | 排序 哈希表 双指针 二分查找 |
查找表(dict) | 350 | 两个数组的交集 II | 简单 | 排序 哈希表 双指针 二分查找 |
查找表(dict) | 451 | 根据字符出现频率排序 | 中等 | 堆 哈希表 |
二分查找(按下标) | 35 | 搜索插入位置 | 简单 | 数组 二分查找 |
二分查找(按下标) | 540 | 有序数组中的单一元素 | 中等 | 二分查找 |
二分查找(按值) | 410 | 分割数组的最大值 | 困难 | 二分查找 动态规划 |
注意:
- 本文的题解语言全部采用Python3,所以有可能会用到Python的库。
- 部分题目可能多种查找算法都能做,这里只选择一种作为分类。
- 博主能力有限,若有错误之处请指出,谢谢!
查找表
str:字符串是 Python 中最常用的数据类型,常用以下方法辅助查找:
方法 | 功能 |
---|---|
string.find(str, beg=0, end=len(string)) | 检测 str 是否包含在 string 中,如果 beg 和 end 指定范围,则检查是否包含在指定范围内,如果是返回开始的索引值,否则返回-1 |
string.rfind(str, beg=0,end=len(string) ) | 类似于 find()函数,不过是从右边开始查找. |
string.index(str, beg=0, end=len(string)) | 跟find()方法一样,只不过如果str不在 string中会报一个异常 |
string.rindex( str, beg=0,end=len(string)) | 类似于 index(),不过是从右边开始 |
list:列表Python中最基本的数据结构,常用以下方法辅助查找:
方法 | 功能 |
---|---|
list.append(obj) | 在列表末尾添加新的对象 |
list.count(obj) | 统计某个元素在列表中出现的次数 |
list.index(obj) | 从列表中找出某个值第一个匹配项的索引位置 |
list.insert(index, obj) | 将对象插入列表 |
list.pop([index=-1]) | 移除列表中的一个元素(默认最后一个元素),并且返回该元素的值 |
list.sort(cmp=None, key=None, reverse=False) | 对原列表进行排序 |
set:可使用set() 函数创建一个无序不重复元素集,可进行关系测试,删除重复数据,还可以计算交集、差集、并集等。set只存储键,而不需要对应其相应的值。set中的键不允许重复。和数学中的集合概念类似。
dict:字典是一种可变容器模型,且可存储任意类型对象。常用于统计频数。可用dict(Counter(list)) 或者 dict(Counter(str)) 快速生成字典统计频数。
205. 同构字符串
原题传送:LeetCode 205. 同构字符串
给定两个字符串 s 和 t,判断它们是否是同构的。
如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。
所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。
示例 1:
输入: s = "egg", t = "add"
输出: true
示例 2:
输入: s = "foo", t = "bar"
输出: false
示例 3:
输入: s = "paper", t = "title"
输出: true
说明:
你可以假设 s 和 t 具有相同的长度。
思路:
判断每个字符的索引相同,如果后面有重复的字符,index就会直接索引到第一次出现的位置。相当于同一字符第一个出现的位置映射到字符串中所有相同的字符。
Python:
class Solution:
def isIsomorphic(self, s: str, t: str) -> bool:
for i in range(len(s)):
if s.index(s[i]) != t.index(t[i]):
return False
return True
242. 有效的字母异位词
给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
示例 1:
输入: s = "anagram", t = "nagaram"
输出: true
示例 2:
输入: s = "rat", t = "car"
输出: false
说明:
你可以假设字符串只包含小写字母。
进阶:
如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?
思路:
只要满足各字符串的组成元素相同就可以,使用Counter对字符的出现次数进行统计。
Python:
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
return collections.Counter(s) == collections.Counter(t)
202. 快乐数
原题传送:LeetCode 202. 快乐数
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。
如果 n
是快乐数就返回 True
;不是,则返回 False
。
示例 :
输入:19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
思路:
创建一个列表用于存放出现过的数,如果有重复的返回False,因为有重复数字出现说明已经进入死循环了。
Python:
class Solution:
def isHappy(self, n: int) -> bool:
lst = []
while n != 1:
if n in lst:
return False
sm = 0
for i in str(n):
sm += int(i)**2
lst.append(n)
n = sm
return True
290. 单词规律
原题传送:LeetCode 290. 单词规律
给定一种规律 pattern
和一个字符串 str
,判断 str
是否遵循相同的规律。
这里的 遵循 指完全匹配,例如, pattern
里的每个字母和字符串 str
中的每个非空单词之间存在着双向连接的对应规律。
示例 1:
输入: pattern = "abba", str = "dog cat cat dog"
输出: true
示例 2:
输入:pattern = "abba", str = "dog cat cat fish"
输出: false
示例 3:
输入: pattern = "aaaa", str = "dog cat cat dog"
输出: false
示例 4:
输入: pattern = "abba", str = "dog dog dog dog"
输出: false
说明:
你可以假设 pattern
只包含小写字母, str
包含了由单个空格分隔的小写字母。
思路:
把str转成列表,然后通过index判断是否同构。
Python:
class Solution:
def wordPattern(self, pattern: str, str: str) -> bool:
lst = str.split()
if len(lst) != len(pattern):
return False
for i in range(len(lst)):
if lst.index(lst[i]) != pattern.index(pattern[i]):
return False
return True
349. 两个数组的交集
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
说明:
- 输出结果中的每个元素一定是唯一的。
- 我们可以不考虑输出结果的顺序。
思路:
把列表转成集合求交集。
Python:
class Solution:
def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
return list(set(nums1).intersection(set(nums2)))
350. 两个数组的交集 II
给定两个数组,编写一个函数来计算它们的交集。
示例 1:
输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2,2]
示例 2:
输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[4,9]
说明:
- 输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
- 我们可以不考虑输出结果的顺序。
进阶:
- 如果给定的数组已经排好序呢?你将如何优化你的算法?
- 如果 nums1 的大小比 nums2 小很多,哪种方法更优?
- 如果 nums2 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?
回答:
- 将两个数组进行排序,随后用双指针顺序查找相同的元素。时间复杂度为 O ( m a x ( n l o g n , m l o g m , n + m ) ) O(max(nlogn, mlogm, n+m)) O(max(nlogn,mlogm,n+m))O,空间复杂度为 O ( 1 ) O(1) O(1)。( n n n, m m m 分别为两个数组的长度)
- 将较小的数组哈希计数,随后在另一个数组中根据哈希来寻找。时间复杂度为 O ( m a x ( n , m ) ) O(max(n, m)) O(max(n,m)),空间复杂度为 O ( m i n ( n , m ) ) O(min(n, m)) O(min(n,m))。
- 通过归并外排将两个数组排序后再使用排序双指针查找。
思路:
把nums1转成字典,遍历nums2,找出共有的元素,通过字典的value控制元素的个数。
Python:
class Solution:
def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
res = []
dic = dict(Counter(nums1))
for i in nums2:
if i in dic and dic[i] > 0:
res.append(i)
dic[i] -= 1
return res
451. 根据字符出现频率排序
给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
示例 1:
输入:
"tree"
输出:
"eert"
解释:
'e'出现两次,'r'和't'都只出现一次。
因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。
示例 2:
输入:
"cccaaa"
输出:
"cccaaa"
解释:
'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。
注意"cacaca"是不正确的,因为相同的字母必须放在一起。
示例 3:
输入:
"Aabb"
输出:
"bbAa"
解释:
此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。
注意'A'和'a'被认为是两种不同的字符。
思路:
存入字典排序。
Python:
class Solution:
def frequencySort(self, s: str) -> str:
res = ""
dic = dict(Counter(s))
for k, v in sorted(dic.items(),key=lambda x:x[1],reverse=True):
for i in range(v):
res += k
return res
二分查找
二分查找只对有序数组有效。二分查找先比较数组中间值和目标值。如果目标值与中间值相等,则返回其在数组中的位置;如果目标值小于中间值,则搜索继续在前半部分的数组中进行。如果目标值大于中间值,则搜索继续在数组上部分进行。由此,算法每次排除掉至少一半的待查数组。
步骤:
给予一个包含
n
n
n 个带值元素的数组
A
A
A 或是记录
A
0
A_{0}
A0 ,
⋯
\cdots
⋯ ,
A
n
−
1
A_{n-1}
An−1 ,使
A
0
≤
⋯
≤
A
n
−
1
A_0 \le \cdots \le A_{n-1}
A0≤⋯≤An−1,以及目标值
T
T
T,还有下列用来查找
T
T
T 在
A
A
A 中位置的子程序。
- 令 L L L 为 0 0 0, R R R 为 n − 1 n-1 n−1。
- 如果 L > R L > R L>R,则查找以失败告终。
- 令 m m m (中间值元素)为 ⌊ ( L + R ) / 2 ⌋ \lfloor (L+R)/2 \rfloor ⌊(L+R)/2⌋。(具体实现中,为防止算术溢出,一般采用 ⌊ L + ( R − L ) / 2 ⌋ \lfloor L+(R-L)/2 \rfloor ⌊L+(R−L)/2⌋ 代替。)
- 如果 A m < T A_m < T Am<T,令 L L L 为 m + 1 m+1 m+1 并回到步骤二。
- 如果 A m > T A_m > T Am>T,令 R R R 为 m − 1 m-1 m−1 并回到步骤二。
- 当 A m = T A_m = T Am=T,查找结束;回传值 m m m。
Python3 版本 while循环:
def binary_search(arr, left, right, hkey):
while left <= right:
mid = left + (right - left) // 2
if arr[mid] == hkey:
return mid
elif arr[mid] < hkey:
left = mid + 1
elif arr[mid] > hkey:
right = mid - 1
return -1
- 时间复杂度:折半搜索每次把搜索区域减少一半,时间复杂度为 O ( log n ) O(\log n) O(logn)。(n代表集合中元素的个数)
- 空间复杂度:循环为 O ( 1 ) O(1) O(1),递归形式为 O ( log n ) O(\log n) O(logn)。
注意:
mid = left + (right - left) // 2
这一段代码可以改成
mid = (left + right) // 2
因为在Python中不用担心整数溢出,其他语言可能要防止算术溢出。不用位运算代替除法是因为会自动优化,效率不会提升。
35. 搜索插入位置
原题传送:LeetCode 35. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
你可以假设数组中无重复元素。
示例 1:
输入: [1,3,5,6], 5
输出: 2
示例 2:
输入: [1,3,5,6], 2
输出: 1
示例 3:
输入: [1,3,5,6], 7
输出: 4
示例 4:
输入: [1,3,5,6], 0
输出: 0
思路 1:
因为数组是升序排列的,所以可以从小到大遍历寻找插入位置,时间复杂度为 O ( N ) O(N) O(N)。
Python:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
## 遍历寻找大于等于 target 的数,找到返回下标
for i in range(len(nums)):
if target <= nums[i]:
return i
## 若没有找到,说明 target 是最大的数,返回列表长度
return len(nums)
思路 2:
因为所给的数组是有序的,所以可以采用二分查找返回左边界,时间复杂度为 O ( log n ) O(\log n) O(logn)。
Python:
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
## 左右边界初始化
l, r = 0, len(nums)
## 二分查找
while l < r:
mid = (l + r) // 2
if nums[mid] >= target:
r = mid
else:
l = mid + 1
## 返回左边界
return l
540. 有序数组中的单一元素
给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。
示例 1:
输入: [1,1,2,3,3,4,4,8,8]
输出: 2
示例 2:
输入: [3,3,7,7,10,11,11]
输出: 10
注意: 您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
思路:
题目要求O(log n)时间复杂度和 O(1)空间复杂度,可以选择二分查找。本题相对于一般的二分查找来说,数组的长度肯定是奇数的,而且每个元素都会出现两次,唯有一个数只会出现一次,因此可以分奇偶讨论,单一元素肯定在奇数的一侧。
步骤如下:
- 当mid为偶数时,mid两边的数字个数为偶数个;当mid为奇数时,mid两边的数字个数为奇数个。
- 当mid为奇数的时候,将mid左移一位。
- 之后比较mid与mid+1的值是否相等,相等的话在mid的右边,不相等的话在mid的左边。
- 循环直到只剩一个元素。
Python:
class Solution:
def singleNonDuplicate(self, nums: List[int]) -> int:
## 左右边界初始化
l, r = 0, len(nums)-1
## 二分查找
while l < r:
mid = (l + r) // 2
if mid % 2 == 1:
mid -= 1
if nums[mid] == nums[mid+1]:
l = mid + 2
else:
r = mid
## 返回单一元素
return nums[l]
410. 分割数组的最大值
给定一个非负整数数组和一个整数 m,你需要将这个数组分成 m 个非空的连续子数组。设计一个算法使得这 m 个子数组各自和的最大值最小。
注意:
数组长度 n 满足以下条件:
- 1 ≤ n ≤ 1000
- 1 ≤ m ≤ min(50, n)
示例 :
输入:
nums = [7,2,5,10,8]
m = 2
输出:
18
解释:
一共有四种方法将nums分割为2个子数组。
其中最好的方式是将其分为[7,2,5] 和 [10,8],
因为此时这两个子数组各自的和的最大值为18,在所有情况中最小。
思路:
本题给出的数组是无序的,也就是不能用一般按下标搜索的二分法。这里采用按值搜索的二分法,因此我们要找的值在max(nums)和sum(nums)内,而这两个值中间是连续的。返回值为下界,即子数组各自和的最大值。当我们选定一个值 x,我们可以线性地验证是否存在一种分割方案,满足其最大分割子数组和不超过 x。
步骤如下:
- 贪心地模拟分割的过程,从前到后遍历数组,用 tot 表示当前分割子数组的和,cnt 表示已经分割出的子数组的数量(包括当前子数组)
- 每当 sum 加上当前值超过了 x,我们就把当前取的值作为新的一段分割子数组的开头,并将 cnt 加 1
- 遍历结束后验证是否 cnt 不超过 m。
Python:
class Solution:
def splitArray(self, nums: List[int], m: int) -> int:
## 左右边界初始化
l, r = max(nums), sum(nums)
## 按值搜索的二分法
while l < r:
mid = (l + r) // 2
tot, cnt = 0, 1
# 判断当前和 tot 与 mid 的大小
for i in nums:
tot += i
if tot > mid:
cnt += 1
tot = i
# 判断当前区间数 cnt 与指定区间数 m 的大小
if cnt > m:
l = mid + 1
else:
r = mid
## 返回子数组各自和的最大值
return l