【活动打卡】【Datawhale】第16期 编程实践(LeetCode 分类练习) Task03:查找1

查找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. 同构字符串

给定两个字符串 st,判断它们是否是同构的。

如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。

所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。

示例 1:

输入: s = "egg", t = "add"
输出: true

示例 2:

输入: s = "foo", t = "bar"
输出: false

示例 3:

输入: s = "paper", t = "title"
输出: true

说明:
你可以假设 st 具有相同的长度。

思路:

判断每个字符的索引相同,如果后面有重复的字符,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. 有效的字母异位词

原题传送:LeetCode 242. 有效的字母异位词

给定两个字符串 st ,编写一个函数来判断 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. 两个数组的交集

原题传送:LeetCode 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

原题传送:LeetCode 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 的元素存储在磁盘上,内存是有限的,并且你不能一次加载所有的元素到内存中,你该怎么办?

回答:

  1. 将两个数组进行排序,随后用双指针顺序查找相同的元素。时间复杂度为 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 分别为两个数组的长度)
  2. 将较小的数组哈希计数,随后在另一个数组中根据哈希来寻找。时间复杂度为 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))
  3. 通过归并外排将两个数组排序后再使用排序双指针查找。

思路:

把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. 根据字符出现频率排序

原题传送:LeetCode 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} An1 ,使 A 0 ≤ ⋯ ≤ A n − 1 A_0 \le \cdots \le A_{n-1} A0An1,以及目标值 T T T,还有下列用来查找 T T T A A A 中位置的子程序。

  1. L L L 0 0 0 R R R n − 1 n-1 n1
  2. 如果 L > R L > R L>R,则查找以失败告终。
  3. 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+(RL)/2 代替。)
  4. 如果 A m < T A_m < T Am<T,令 L L L m + 1 m+1 m+1 并回到步骤二。
  5. 如果 A m > T A_m > T Am>T,令 R R R m − 1 m-1 m1 并回到步骤二。
  6. 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. 有序数组中的单一元素

原题传送:LeetCode 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)空间复杂度,可以选择二分查找。本题相对于一般的二分查找来说,数组的长度肯定是奇数的,而且每个元素都会出现两次,唯有一个数只会出现一次,因此可以分奇偶讨论,单一元素肯定在奇数的一侧。

步骤如下:

  1. 当mid为偶数时,mid两边的数字个数为偶数个;当mid为奇数时,mid两边的数字个数为奇数个。
  2. 当mid为奇数的时候,将mid左移一位。
  3. 之后比较mid与mid+1的值是否相等,相等的话在mid的右边,不相等的话在mid的左边。
  4. 循环直到只剩一个元素。
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. 分割数组的最大值

原题传送:LeetCode 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。

步骤如下:

  1. 贪心地模拟分割的过程,从前到后遍历数组,用 tot 表示当前分割子数组的和,cnt 表示已经分割出的子数组的数量(包括当前子数组)
  2. 每当 sum 加上当前值超过了 x,我们就把当前取的值作为新的一段分割子数组的开头,并将 cnt 加 1
  3. 遍历结束后验证是否 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

参考文章

  1. 第16期 Datawhale 组队学习活动
  2. Datawhale课程——查找
  3. 菜鸟教程-Python 字符串
  4. 菜鸟教程-Python 列表(List)
  5. LeetCode 350. 两个数组的交集 II 题解区-进阶三问 by Alien-Leon
  6. 二分查找–按值搜索和按下标搜索总结
  7. 二分查找算法-维基百科
  8. LeetCode 410. 分割数组的最大值 题解区-分割数组的最大值 by 官方题解
  9. LeetCode 540. 有序数组中的单一元素 题解区-二分搜索 by 惠朦朦
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值