zoukankan      html  css  js  c++  java
  • 数据结构与算法习题总结——查找与排序算法

    本篇为Datawhale编程实践项目的学习笔记,会随着学习进程不断更新,题目都借鉴自网络或书籍,仅用作个人学习。由于水平实在有限,不免产生谬误,欢迎读者多多批评指正。如需要转载请与博主联系,谢谢


    查找算法

    例题:

    1. 搜索插入位置
      给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。你可以假设数组中无重复元素。
    class Solution:
        def searchInsert(self, nums: List[int], target: int) -> int:
            '''
            # 解法一:暴力查找
            if not nums or target <= nums[0]:return 0
            if target > nums[-1]:return len(nums)
            for i in range(1,len(nums)):
                if target <= nums[i] and target > nums[i-1]:  # 小于等于同时达到了查找和差值的目的
                    return i
            '''
            # 解法二:二分查找
            left = 0
            right = len(nums)    # 考虑了target可能大于最后一个值的情况
            while left < right:
                mid = (left + right) // 2
                if nums[mid] < target:
                    left = mid + 1     # 如果没有相同值,最终一定得到nums[left-1]<target<nums[left]
                elif nums[mid] > target:
                    right = mid
                elif nums[mid] == target:
                    return mid     # 查到结果,直接输出
            return left
    
    1. 快乐数
      编写一个算法来判断一个数 n 是不是快乐数。
      「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为  1,那么这个数就是快乐数。
      如果 n 是快乐数就返回 True ;不是,则返回 False 。
    class Solution:
        def isHappy(self, n: int) -> bool:
            # 思路很简单,就是迭代计算看是否陷入循环(不是快乐数)还是收敛到1(是快乐数)
            def cal(m:int):
                l = list(str(m))
                return sum([pow(int(i),2) for i in l])
            s = set([int(n)])   # 这里利用集合(HashSet)存储已出现过的数很关键,可以大大提升查找效率
            while n != 1:
                n = cal(n)
                if n in s:
                    return False
                s.add(n)
            return True
    
    1. 同构字符串
      给定两个字符串 s 和 t,判断它们是否是同构的。
      如果 s 中的字符可以被替换得到 t ,那么这两个字符串是同构的。
      所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。
      示例1:
      输入: s = "egg", t = "add"
      输出: true
      示例2:
      输入: s = "foo", t = "bar"
      输出: false
    class Solution:
        def isIsomorphic(self, s: str, t: str) -> bool:
            # 用哈希表存储已经出现过的对应替换关系,有重复元素出现时检查此时两个字符间的映射是否与之前存储的匹配
            if not s and not t: return True
            if len(s) != len(t): return False
            dic = {}
            for i in range(len(s)):
                if t[i] in dic:
                    if dic[t[i]] != s[i]:
                        return False
                else:
                    if s[i] in dic.values():
                        return False
                    dic[t[i]] = s[i]
            return True
    
    1. 有效的字母异位词
      给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
    class Solution:
        def isAnagram(self, s: str, t: str) -> bool:
            # 先生成一个字典用键来存储字符,值记录字符出现次数;然后根据字典对照另一个字符串的字符出现情况即可
            dic = {}
            if len(s) != len(t): return False
            for i in range(len(s)):
                if s[i] in dic:
                    dic[s[i]] += 1
                else:
                    dic[s[i]] = 1
            for j in range(len(t)):
                if t[j] not in dic:
                    return False
                else:
                    dic[t[j]] -= 1
                if dic[t[j]] == 0:
                    del dic[t[j]]
            return True if not dic else False
    
    1. 单词规律
      给定一种规律 pattern 和一个字符串 str ,判断 str 是否遵循相同的规律。
      这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 str 中的每个非空单词之间存在着双向连接的对应规律。
    class Solution:
        def wordPattern(self, pattern: str, str: str) -> bool:
            # 思路很简单,还是用哈希表来存储对应关系
            s = str.split()
            if len(s) != len(pattern):return False
            dic = {}
            for i in range(len(pattern)):
                if pattern[i] not in dic:
                    if s[i] in dic.values():
                        return False
                    dic[pattern[i]] = s[i]
                else:
                    if dic[pattern[i]] != s[i]:
                        return False
            return True
    
    1. 两个数组的交集 I
      给定两个数组,编写一个函数来计算它们的交集。
    class Solution:
        def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]:
            '''
            # 解法一:暴力查找
            res = []
            for i in nums1:
                if i in nums2 and i not in res:
                    res.append(i)
            return res
            '''
            # 解法二:投机取巧使用Python集合的求交集方法(不想这样的话可以用哈希表做元素的存储和查找也很简单)
            return set(s1) & set(s2)
    
    1. 两个数组的交集 II
      还是上题的要求,但输出结果中每个元素出现的次数,应与元素在两个数组中出现次数的最小值一致。
    class Solution:
        def intersect(self, nums1: List[int], nums2: List[int]) -> List[int]:
            # 还是利用哈希表记录一个列表中出现的元素及元素出现的次数,然后另一个列表出现的元素与之比较并记录结果
            dic = {}
            ans = []
            for i in range(len(nums1)):
                if nums1[i] not in dic:
                    dic[nums1[i]] = 1
                else:
                    dic[nums1[i]] += 1
            for i in range(len(nums2)):
                if nums2[i] in dic:
                    if dic[nums2[i]] == 0:
                        continue
                    dic[nums2[i]] -= 1
                    ans.append(nums2[i])
            return ans
    
    1. 根据字符出现频率排序
      给定一个字符串,请将字符串里的字符按照出现的频率降序排列。
    class Solution:
        def frequencySort(self, s: str) -> str:
            # 核心思路是字典存储元素及出现的次数,根据次数排序,然后按出现次数重新在新列表中打印出来
            N = len(s)
            dic = {}
            for i in range(N):
                if s[i] not in dic:
                    dic[s[i]] = 1
                else:
                    dic[s[i]] += 1
            l = sorted(zip(dic.values(),dic.keys()))    # 将键值对打包成元组,注意值要排在前面,因为要按值排序
            ans = [None]*N
            n = 0
            for j in range(len(l)-1,-1,-1):   # 降序排列打印
                for m in range(l[j][0]):
                    ans[n] = l[j][1]
                    n += 1
            return ''.join(ans)
    
    1. 有序数组中的单一元素
      给定一个只包含整数的有序数组,每个元素都会出现两次,唯有一个数只会出现一次,找出这个数。注意您的方案应该在 O(log n)时间复杂度和 O(1)空间复杂度中运行。
    class Solution:
        def singleNonDuplicate(self, nums: List[int]) -> int:
            # 这个题麻烦在时间复杂度要求log n,因此不能遍历查找。应该用二分法解决。
            if len(nums) == 1:
                return nums[0]
            left = 0
            right = len(nums)-1
            while left <= right:
                mid = (left + right) // 2
                if mid % 2 == 0 and mid + 1 < len(nums):  # mid是偶数(nums[mid]前面有偶数个元素)
                    if nums[mid] == nums[mid+1]:   # mid前面没有单一元素
                        left = mid + 1
                    else:   # mid前面有单一元素
                        right = mid - 1
                elif mid % 2 != 0 and mid + 1 < len(nums):   # mid是奇数(nums[mid]前面有奇数个元素)
                    if nums[mid] == nums[mid+1]:  # mid前面有单一元素
                        right = mid - 1
                    else:   # mid前面没有单一元素
                        left = mid + 1
                else:
                    return nums[mid]
            return nums[left]
    
    1. 两数之和(力扣的第一题)
      给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
      你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
    class Solution:
        def twoSum(self, nums: List[int], target: int) -> List[int]:
            '''
            # 解法一:暴力法,时间复杂度O(n^2),空间复杂度O(1)
            for i in range(len(nums)-1):
                for j in range(i+1,len(nums)):
                    if nums[i] + nums[j] == target:
                        return [i,j]
            '''
            # 解法二:空间换时间,利用哈希表存储每个数与目标值之间的差值,再用一次循环查找此差值在数组中的位置
            dic = {}
            for i in range(len(nums)):
                dic[target-nums[i]] = i   # 这里差值为键,当前元素的索引为值
            for j in range(len(nums)):
                if nums[j] in dic:
                    if j != dic[nums[j]]:   # 注意排除一个数与自己相加得到目标值的情况
                        return [j,dic[nums[j]]]
    
    1. 三数之和
      给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。注意:答案中不可以包含重复的三元组。
    class Solution:
        def threeSum(self, nums: List[int]) -> List[List[int]]:
            '''
            # 解法一:暴力求解,时间复杂度O(N^3),超时
            dic2 = {}
            answer = []
            for i in range(len(nums)):
                for j in range(i+1, len(nums)):
                    if (nums[i],nums[j]) in dic2:
                        continue
                    else:
                        dic2[(nums[i],nums[j])] = [nums[i],nums[j]]
                        sums = -nums[i]-nums[j]
                        if (sums in nums[j+1:len(nums)]) and (sorted([nums[i],nums[j],sums]) not in answer):
                            #print(dic1[-i-j])
                            answer.append(sorted([nums[i],nums[j],sums]))
            return answer
            
            # 解法二:利用哈希表,但这样很难处理重复三元组的情况(没想出解决方法)
            ans = []
            s = set()
            dic = {}
            for i in range(len(nums)):
                dic[-nums[i]] = i
            for i in range(len(nums)-1):
                for j in range(i+1,len(nums)):
                    if nums[i]+nums[j] in dic and i != dic[nums[i]+nums[j]] and j != dic[nums[i]+nums[j]]:
                        ans.append([nums[i],nums[j],-nums[i]-nums[j]])
            return ans
            '''
            # 解法三:排序+双指针,可以好好理解下这个思路,时间复杂度O(N^2)
            if not nums or len(nums)<3:
                return []
            if len(nums)==3 and sum(nums)==0:
                return [nums]
    
            nums = sorted(nums)
            answer = []
            for i in range(len(nums)):  # 设定nums[i]为三元组中最小元素
                m = i+1
                n = len(nums)-1
                if nums[i] > 0:   # nums[i]大于0后就不用找了,和肯定大于0
                    return answer
                if i > 0 and nums[i] == nums[i-1]:   # 跳过重复的元素,避免找到相同三元组
                    continue
                while m < n:   
                    sums = nums[i]+nums[m]+nums[n]  # 以下分三种情况讨论
                    if sums==0:
                        answer.append([nums[i],nums[m],nums[n]])
                        while m<len(nums)-1 and nums[m] == nums[m+1]:  # 重复元素跳过(前一个条件为了防止索引越界)
                            m+=1
                        while n > 0 and nums[n] == nums[n-1]:
                            n-=1
                        m+=1
                        n-=1
                    elif sums>0:
                        n-=1
                    else:
                        m+=1
            return answer
    
    1. 最接近的三数之和
      给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。
    class Solution:
        def threeSumClosest(self, nums: List[int], target: int) -> int:
            # 解法一:排序+双指针,与上一题思路类似
            nums = sorted(nums)
            aver = target/3
            if aver >= nums[-1]:    # 当数组最大值小于target/3或最小值大于target/3时可以单独处理
                return nums[-1]+nums[-2]+nums[-3]
            if aver <= nums[0]:
                return nums[0]+nums[1]+nums[2]
            loss = float('inf')    # loss用于记录当前最小差距
            answer = 0             # answer用于记录当前最小差距对应的三个数
            for i in range(len(nums)):
                m = i+1
                n = len(nums)-1
                while m < n:
                    temp = abs(target - nums[i] - nums[m] - nums[n])  # temp为当前三个数的差距,以下分为三种情况考虑
                    if temp == 0: return target        # 情况一
                    if temp < loss:
                        loss = temp
                        answer = nums[i] + nums[m] + nums[n]
                    if (nums[m] + nums[n]) < (target - nums[i]):   # 情况二和三
                        m += 1
                    else:
                        n -= 1
            return answer
    
    1. 四数之和
      给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。注意答案中不可以包含重复的四元组。
    class Solution:
        def fourSum(self, nums: List[int], target: int) -> List[List[int]]:
            # 这个还是排序加双指针,就是固定前两个小数写起来好麻烦,直接参考了leetcode上网友的答案(笔试遇到就按这个思路慢慢想吧。。)
            nums = sorted(nums)
            n = len(nums)
            res = []
            p = 0        # p, k, i, j 依次为从小到大的四个元素的索引
            while p < n - 3:  
                if nums[p] + 3 * nums[p + 1] > target: break     # 这种情况下已经不可能相加等于target
                if nums[p] + 3 * nums[-1] < target:           # p所指向的元素太小,需要向后移动
                    while p < n - 4 and nums[p] == nums[p + 1]: p += 1
                    p += 1
                    continue
                k = p + 1
                while k < n - 2:   # k 和 p 的判断是一样的
                    if nums[p] + nums[k] + 2 * nums[k + 1] > target: break
                    if nums[p] + nums[k] + 2 * nums[-1] < target: 
                        while k < n - 3 and nums[k] == nums[k + 1]:
                            k += 1
                        k += 1
                        continue
                    i = k + 1
                    j = n - 1
                    new_target = target - nums[p] - nums[k]
                    while i < j:
                        if nums[i] + nums[j] > new_target: j -= 1
                        elif nums[i] + nums[j] < new_target: i += 1
                        else:
                            res.append([nums[p],nums[k],nums[i],nums[j]])
                            i += 1
                            j -= 1
                            while i < j and nums[i] == nums[i - 1]: i += 1 # 避免结果重复
                            while i < j and nums[j] == nums[j + 1]: j -= 1 # 避免结果重复
                    while k < n - 3 and nums[k] == nums[k + 1]: k += 1   # 与上一题中类似,跳过重复元素避免构成重复四元组
                    k += 1
                while p < n - 4 and nums[p] == nums[p + 1]: p += 1
                p += 1
            return res
    
    1. 存在重复元素 II
      给定一个整数数组和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,使得 nums [i] = nums [j],并且 i 和 j 的差的 绝对值 至多为 k。
    class Solution:
        def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
            '''
            # 解法一:暴力循环,在leetcode上会超时
            for i in range(len(nums)):
                for j in range(i+1,min(i+k+1,len(nums))):
                    if nums[i] == nums[j]:
                        return True
            return False
            '''
            # 解法二:哈希表查找,很巧妙,时间复杂度O(N)
            dic = {}
            for i in range(len(nums)):
                if nums[i] not in dic:  # 判断当前元素是否出现过,没有就记录下来
                    dic[nums[i]] = i
                else:
                    if i - dic[nums[i]] <= k:  # 判断上次出现时索引与当前索引相距是否在k以内
                        return True
                    else:
                        dic[nums[i]] = i   # 如果相距太远,则更新此元素值对应的索引
            return False
    
    1. 存在重复元素 III
      在整数数组 nums 中,是否存在两个下标 i 和 j,使得 nums [i] 和 nums [j] 的差的绝对值小于等于 t ,且满足 i 和 j 的差的绝对值也小于等于 ķ 。如果存在则返回 true,不存在返回 false。
    class Solution:
        def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
            '''
            # 解法一:暴力循环,时间复杂度O(n*min(k,n)),leetcode中会超时
            for i in range(len(nums)):
                for j in range(i + 1, len(nums)):
                    if abs(nums[i] - nums[j]) <= t and j - i  <= k:
                        return True
            return False
            '''
            # 解法二:桶排序,按元素数值大小扔到固定间隔切分的桶中,保证每个桶内部容量刚好满足题目要求差值的最大间隔,然后判断新的元素在桶中是否有满足题意的伙伴
            bucket = {}
            if t < 0: return False       # 这一句是因为leetcode的样例中竟然有t<0的情况,因此单独处理
            for i in range(len(nums)):
                n = nums[i] // (t + 1)   # 判断新元素将被放入的桶的序号
                if n in bucket:          # 桶中已有一个元素,则查找成功(新元素与该元素间隔必定小于t)
                    return True
                elif n - 1 in bucket and abs(nums[i] - bucket[n-1]) <= t:    # 如果前后两个桶中有元素,则需要单独判断下;更远处的桶中元素与新元素间隔过大,不必判断
                    return True
                elif n + 1 in bucket and abs(nums[i] - bucket[n+1]) <= t:
                    return True
                bucket[n] = nums[i]                 # 未找到伙伴,新元素入桶
                if i >= k: bucket.pop(nums[i-k]//(t+1))   # 每时刻仅维护当前元素及前k个元素,将之前保存的桶及其中的元素抛弃掉
            return False 
    
    1. 回旋镖的数量
      给定平面上 n 对不同的点,“回旋镖” 是由点表示的元组 (i, j, k) ,其中 i 和 j 之间的距离和 i 和 k 之间的距离相等(需要考虑元组的顺序)。
      找到所有回旋镖的数量。你可以假设 n 最大为 500,所有点的坐标在闭区间 [-10000, 10000] 中。
    class Solution:
        def numberOfBoomerangs(self, points: List[List[int]]) -> int:
            '''
            # 最简单的思路就是固定一个点,然后遍历其他点到该点的距离,把每种出现的距离值记录在哈希表中,最后计算总“回旋镖”元组数
            ans = 0
            for i in range(len(points)):
                dic = {}
                for j in range(len(points)):   # 注意这里为什么还要从头遍历,因为以i为起点j为某个终点和以j为起点以i为某个终点的三元组是不一样的(好好思考下)
                    if i == j:
                        continue
                    if (points[i][0]-points[j][0])**2+(points[i][1]-points[j][1])**2 not in dic:
                        dic[(points[i][0]-points[j][0])**2+(points[i][1]-points[j][1])**2] = 1
                    else:
                        dic[(points[i][0]-points[j][0])**2+(points[i][1]-points[j][1])**2] += 1
                for m in dic.values():
                    ans += m*(m-1)     # 到当前点距离相同的点数n与构成的“回旋镖”元组数s之间的关系为s=n*(n-1)
            return ans
            '''
            # 大佬的两行版答案,利用了高性能容器Collection,用组合公式求组合数目,执行速度更快
            f = lambda x1, y1: sum(t * t - t for t in collections.Counter((x2 - x1) ** 2 + (y2 - y1) ** 2 for x2, y2 in points).values())
            return sum(f(x1, y1) for x1, y1 in points)
    
    1. 字母异位词分组
      给定一个字符串数组,将字母异位词组合在一起。字母异位词指字母相同,但排列不同的字符串。
    class Solution:
        def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
            # 利用哈希表存储排序后的字符串作为键,排序后相同的原始字符串以列表的形式储存在值中
            dic = {}
            for i in range(len(strs)):
                temp = ''.join(sorted(strs[i]))   # 注意这里必须转换回字符串的形式,因为python中字典不接受列表作为键
                if temp not in dic:
                    dic[temp] = [strs[i]]
                else:
                    dic[temp].append(strs[i])
            return list(dic.values())
    
    1. 求平方根
      实现函数 int sqrt(int x).
      计算并返回x的平方根
    class Solution:
        def sqrt(self , x ):
            # 典型的二分查找问题,注意处理特殊的边界情况
            if x <= 1:
                return x
            left = 1
            right = x - 1
            mid = (left + right)//2
            while left < right:
                pows = mid * mid
                if pows == x:
                    return mid
                elif pows > x:
                    right = mid
                    mid = (left + right)//2
                else:
                    if (mid+1)*(mid+1) > x:
                        return mid
                    left = mid
                    mid = (left + right)//2
            return left
    

    排序算法

    例题:

    1. 反转链表(最基础的题,但却常考)
      输入一个链表,反转链表后,输出新链表的表头。
    # class ListNode:
    #     def __init__(self, x):
    #         self.val = x
    #         self.next = None
    class Solution:
        def ReverseList(self, pHead):
            # 循环解法,注意需要一个临时变量来存储节点原始的下一个元素,以免.next修改后找不到
            if not pHead: return None
            node = pHead
            nextn = node.next
            while nextn != None:
                temp = nextn.next
                nextn.next = node
                node = nextn
                nextn = temp
            pHead.next = None
            return node
    
    1. 寻找第K大的数
      有一个整数数组,请你根据快速排序的思路,找出数组中第K大的数。
      给定一个整数数组a,同时给定它的大小n和要找的K(K在1到n之间),请返回第K大的数,保证答案存在。
    class Finder:
        def findKth(self, a, n, K):
            # 经典的快排算法,应当牢记于心
            l = self.quick(a)
            return l[n-K]   # 注意是找第K大而不是第K小的数,因此需要倒着数,或者快排时直接由大到小排列
        def quick(self,lists):
            if len(lists) < 2: return lists
            pivot = lists[0]
            less = [i for i in lists[1:] if i <= pivot]
            more = [i for i in lists[1:] if i > pivot]
            return self.quick(less)+[pivot]+ self.quick(more)
    

    参考资料:

    1. https://github.com/datawhalechina/team-learning-program/blob/master/LeetCodeClassification/1.分治.md Datawhale小组学习资料
    2. https://leetcode-cn.com/ 力扣leetcode
    3. https://www.nowcoder.com/ 牛客网
  • 相关阅读:
    [bzoj1076]奖励关
    [bzoj1085]骑士精神
    [bzoj1082]栅栏
    [bzoj1084]最大子矩阵
    [bzoj1072]排列
    [bzoj1071]组队
    [bzoj1068]压缩
    [bzoj1061]志愿者招募
    [bzoj1059]矩阵游戏
    [bzoj1052]覆盖问题
  • 原文地址:https://www.cnblogs.com/liugd-2020/p/13551349.html
Copyright © 2011-2022 走看看