python刷题--学习笔记

8.20

1.好数对的题目

给你一个整数数组 nums 。
如果一组数字 (i,j) 满足 nums[i] == nums[j] 且 i < j ,就可以认为这是一组 好数对 。
返回好数对的数目。
示例 1:
输入:nums = [1,2,3,1,1,3]
输出:4
解释:有 4 组好数对,分别是 (0,3), (0,4), (3,4), (2,5) ,下标从 0 开始
示例 2:
输入:nums = [1,1,1,1]
输出:6
解释:数组中的每组数字都是好数对

当时,本人用了一种暴力的解法:

number=0
        for i in range(0,len(nums)):
            for j in range(i+1,len(nums)):
                if nums[i]==nums[j]:
                    number=number+1
        
        return number

这种算法虽然简单,但是效率不高。
标准答案给了另外一种解法,效率很高:

用哈希表统计每个数在序列中出现的次数,假设数字 k 在序列中出现的次数为 v,那么满足题目中所说的nums[i]=nums[j]=k(i

其代码为:

 m = collections.Counter(nums)
        return sum(v * (v - 1) // 2 for k, v in m.items())

最后附上collections.Counter的使用方法:
https://www.jianshu.com/p/d8a921f61214

8.22

1.合并排序的数组

给定两个排序后的数组 A 和 B,其中 A 的末端有足够的缓冲空间容纳 B。 编写一个方法,将 B 合并入 A 并排序。
初始化 A 和 B 的元素数量分别为 m 和 n。
示例:
输入:
A = [1,2,3,0,0,0], m = 3
B = [2,5,6], n = 3
输出: [1,2,2,3,5,6]
A.length == n + m

当时,本人用了一种很笨的方式,将数组B插入数组A,还十分麻烦。

for i in range(0,n):
            if m==0:
                A[0]=B[i]
                m=m+1
                continue
            for j in range(0,m):
                if B[i]<=A[j]:
                    for k in range(m,j-1,-1):
                        A[k]=A[k-1]
                    A[j]=B[i]
                    m=m+1
                    break
                if j==m-1:
                    break
            if j==m-1 :
                A[m]=B[i]
                m=m+1

这种答案很麻烦,效率也不高。
标准答案给出了另外一种解法:

逆向双指针
我们可以观察到,A的后半部分时空的,可以直接覆盖而不会影响结果,因此可以设置两个指针从后向前遍历,每次取两个之中较大者放进A的最后面。
严格来说,在此遍历中的任意时刻,A数组中有m-pa-1个元素被放入A的后半部,B数组中有n-pb-1个元素被放入A的后半部,而在指针pa的后面,A数组有m+n-pa-1个位置。由于m+n-pa-1>=m-pa-1+n-pb-1等价于pb>=-1永远成立,因此pa后面永远有足够容纳被插入的元素,不会产生覆盖。

其代码为:

pa, pb = m - 1, n - 1
tail = m + n - 1
while pa >= 0 or pb >= 0:
            if pa == -1:
                A[tail] = B[pb]
                pb -= 1
            elif pb == -1:
                A[tail] = A[pa]
                pa -= 1
            elif A[pa] > B[pb]:
                A[tail] = A[pa]
                pa -= 1
            else:
                A[tail] = B[pb]
                pb -= 1
            tail -= 1

8.23

数字范围按位与

给定范围 [m, n],其中 0 <= m <= n <= 2147483647,返回此范围内所有数字的按位与(包含 m, n 两端点)。
示例 1: 输入: [5,7] 输出: 4
示例 2:输入: [0,1] 输出: 0

本人想了一种很简单暴力的方法,就是将每个数相与从而求出最终结果。
但这个方法运行时间超出了时间限制。

答案提供了多种方法:
首先我们可以对范围内的每个数字用二进制的字符串表示,例如9=00001001(2)9=00001001_{(2)}9=00001001(2)​,然后我们将每个二进制字符串的位置对齐。

python刷题--学习笔记_第1张图片在上述例子中,我们可以发现:对所有数字执行按位与运算的结果是所有对应二进制字符串的公共前缀再用零补上后面的剩余位。
那么这个规律是否正确呢?我们可以进行简单的证明。假设对于所有这些二进制串,前 i 位均相同,第 i+1 位开始不同,由于 [m,n] 连续,所以第 i+1 位在 [m,n] 的数字范围从小到大列举出来一定是前面全部是 0,后面全部是 1,在上图中对应 [9,11] 均为 0,[12,12] 均为 1。并且一定存在连续的两个数 x 和 x+1,满足 x 的第 i+1 位为 0,后面全为 1,x+1 的第 i+1 位为 1,后面全为 0,对应上图中的例子即为 11 和 12。这种形如 0111… 和1000的二进制串的按位与的结果一定为 0000…,因此第 i+1位开始的剩余位均为 0,前 i 位由于均相同,因此按位与结果不变。最后的答案即为二进制字符串的公共前缀再用零补上后面的剩余位。
因此,最终我们可以将问题重新表述为:给定两个整数,我们要找到它们对应的二进制字符串的公共前缀。

  1. 位移
    将两个数字不断向右移动,直到数字相等,即数字被缩减为它们的公共前缀。然后,通过将公共前缀向左移动,将零添加到公共前缀的右边以获得最终结果。
    python刷题--学习笔记_第2张图片算法由两步骤组成:
  • 通过右移,将两个数字压缩为它们的公共前缀。在迭代过程中,我们计算执行的右移操作数。
  • 将得到的公共前缀左移相同的操作数得到结果。
shift = 0   
        # 找到公共前缀
        while m < n:
            m = m >> 1
            n = n >> 1
            shift += 1
        return m << shift
  1. Brian Kernighan 算法
    Brian Kernighan算法关键在于我们每次对 number 和 number−1之间进行按位与运算后,number中最右边的 1 会被抹去变成 0。
    python刷题--学习笔记_第3张图片
    基于上述技巧,我们可以用它来计算两个二进制字符串的公共前缀。
    其思想是,对于给定的范围 [m,n](m python刷题--学习笔记_第4张图片在上图所示的示例(m=9,n=12)中,公共前缀是 00001。在对数字 n 应用 Brian Kernighan 算法后,后面三位都将变为零,最后我们返回 n 即可。
        while m < n:
            # 抹去最右边的 1
            n = n & (n - 1)
        return n

9.10

1.数组中的重复数字

在一个长度为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。
示例 1:
输入:[2, 3, 1, 0, 2, 5, 3]
输出:2 或 3

当时,本人的解法是将数组排序之后,再采用二分法进行查找,现在看来这个方法真的蠢到家了。

        nums.sort()
        for i in nums:
            a=0
            b=len(nums)-1
            j=nums[int((a+b)/2)]
            while nums[i]!=j and a<=b:
                if nums[i]>j:
                    a=int((a+b)/2)
                else:
                    b=int((a+b)/2)
                
                j=nums[int((a+b)/2)]

            if i!=int((a+b)/2):
                break
        
        return nums[i]

答案里给出了两种较为典型的解法:

  • 哈希表 / Set

利用数据结构特点,容易想到使用哈希表(Set)记录数组的各个数字,当查找到重复数字则直接返回。
算法流程:

  1. 初始化: 新建 HashSet ,记为 dicdicdic ;
  2. 遍历数组nums中的每个数字num:
    当num再dic中,说明重复,直接返回num;
    将num添加到dic中
    3.返回-1。本题中一定有重复的数字,所以这里返回什么都可以。
        dic = set()
        for num in nums:
            if num in dic: return num
            dic.add(num)
        return -1
  • 原地交换
    题目说明尚未被充分使用,即 在一个长度为 n 的数组 nums 里的所有数字都在 0 ~ n-1 的范围内 。 此说明含义:数组元素的 索引 和 值 是 一对多 的关系。
    因此,可遍历数组并通过交换操作,使元素的 索引 与 值 一一对应(即 nums[i]=i)。因而,就能通过索引映射对应的值,起到与字典等价的作用。
    python刷题--学习笔记_第5张图片遍历中,第一次遇到数字 xxx 时,将其交换至索引 xxx 处;而当第二次遇到数字 xxx 时,一定有 nums[x]=xnums[x] = xnums[x]=x ,此时即可得到一组重复数字。

算法流程:

  1. 遍历数组nums:
    若num[i]=i,则说明数字已在所对应的索引位置,无需交换,因此跳过。
    若nums[num[i]]=num[i],则说明索引num[i]处和索引i处元素的值相等,都是num[i]。即找到了重复的数字,返回num[i]。
    否则,交换索引为i和nums[i]的元素值,将此数字交换至对应索引位置。

  2. 若遍历完毕尚未返回,则返回 −1 。

        i = 0
        while i < len(nums):
            if nums[i] == i:
                i += 1
                continue
            if nums[nums[i]] == nums[i]: return nums[i]
            nums[nums[i]], nums[i] = nums[i], nums[nums[i]]
        return -1

10.10

青蛙跳台阶问题

一只青蛙一次可以跳上1级台阶,也可以跳上2级台阶。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。
答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

本人用了一种很笨的方法,把n分成奇数和偶数,然后分别列出奇数:跳一次一级台阶、三次一级台阶、五次一级台阶;偶数:跳两次一级台阶、四次一次台阶等。。果不其然,超时了。
答案提供了一种思路:

设跳上 n 级台阶有 f(n) 种跳法。在所有跳法中,青蛙的最后一步只有两种情况: 跳上 1级或 2级台阶。
当为 1 级台阶: 剩 n−1 个台阶,此情况共有 f(n−1) 种跳法;
当为 2 级台阶: 剩 n−2 个台阶,此情况共有 f(n−2) 种跳法。

f(n)=f(n-1)+f(n-2),所以这个问题的本质是求斐波那契数列。与斐波那契数列唯一不同的是起始的数字不同。
青蛙跳台阶:f(0)=1 f(1)=1 f(2)=2
斐波那契数列:f(0)=0 f(1)=1 f(2)=1

下面用动态规划法实现:

        a, b = 1, 1
        for _ in range(n):
            a, b = b, a + b
        return a % 1000000007

这里要注意的是此类求 多少种可能性的题目一般都有递推性质 ,即 f(n) 和 f(n−1)…f(1) 之间是有联系的。

10.11

顺时针打印矩阵

输入一个矩阵,按照从外向里以顺时针的顺序依次打印出每一个数字。
示例1:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

当时笔者考虑的是先从左到右打印,再从上到下打印,再从右到左打印,最后从下到上打印。但这样设计的问题是不知道什么时候结束打印,因为有时候不需要从上到下打印或者从右到左打印或者从下到上打印。
此时设计在四个角设计四个边界就可以很容易地解决此问题。
算法的流程如下:

1.空值处理: 当 matrix 为空时,直接返回空列表 [] 即可。
2.初始化: 矩阵 左、右、上、下 四个边界 l , r , t , b ,用于打印的结果列表 res 。
3.循环打印: “从左向右、从上向下、从右向左、从下向上” 四个方向循环,每个方向打印中做以下三件事 (各方向的具体信息见下表)
①根据边界打印,即将元素按顺序添加至列表 res 尾部;
②边界向内收缩 1(代表已被打印);
③判断是否打印完毕(边界是否相遇),若打印完毕则跳出。
4.返回值: 返回 res 即可。

打印方向 1.根据边界打印 2.边界向内收缩 3.是否打印完毕
从左向右 左边界"l",右边界"r" 上边界t加一 是否t>b
从上向下 上边界"t",下边界"b" 右边界r减一 是否l>r
从右向左 右边界”r",左边界"l’ 下边界b减一 是否t>b
从下向上 下边界“b",上边界"t" 左边界l加1 是否l>r

由于边界的加入,判定打印结束也十分简单,代码也很简单,如下:

        if not matrix:
            return []
        l,r,t,b=0,len(matrix[0])-1,0,len(matrix)-1
        res=[]
        while True:
            for i in range(l,r+1):
                res.append(matrix[t][i])
            t=t+1
            if t>b:
                break
            for i in range(t,b+1):
                res.append(matrix[i][r])
            r=r-1
            if l>r:
                break
            for i in range(r,l-1,-1):
                res.append(matrix[b][i])
            b=b-1
            if t>b:
                break
            for i in range(b,t-1,-1):
                res.append(matrix[i][l])
            l=l+1
            if l>r:
                break
        return res

你可能感兴趣的:(总结,python)