算法挑战记录——Python

算法挑战记录——Python

  • 这里是算法挑战记录的Python版本,flag总是要实现的,描述和思路大多和java版本统一,只是实现的语言为Python,如果想查看Java版本的实现,请看算法挑战记录——Java。于2020-11-10.

1.旋转字符串挑战

描述

  • 给定一个字符串(以字符数组的形式给出)和一个偏移量,根据偏移量原地旋转字符串(从左向右旋转)。

    在数组上原地旋转,使用O(1)的额外空间

思路

  • 原地旋转并使用O(1)的额外空间,意味着只能在字符数组上进行操作,通过交换索引位置来进行交换。
  • 交换次数为数组长度-1,代码如下:
class Solution:
    """
    @param str: An array of char
    @param offset: An integer
    @return: nothing
    """
    def rotateString(self, s, offset):
        # write your code here
        lenth = len(s)
        if lenth < 2 or offset == 0 ://字符数组长度小于2时无需交换
            return
        
        offset %= lenth//超过长度取余数
        if offset == 0://交换位置为0时无需操作
            return
        
        temp = ' '//临时字符位置
        pos = 0 //需要进行替换的元素位置
        check = 0//如果字符数组长度不是素数,会出现循环
        for i in s[1:]://只需要进行length-1次
            newPos = (pos+offset)%lenth
            temp = s[newPos]
            s[newPos] = s[check]
            s[check] = temp
            pos += offset
            if pos % lenth == check://check最多为offset-1,循环就可结
                check += 1
                pos = check

2.尾随零

描述

  • 给定一个整数n,返回n!(n的阶乘)的尾随零的个数。

    您的解法时间复杂度应为对数级别。

思路

  • 尾随0以为着计算结果有多少个10,10 =2×5。也就是说寻找阶乘中因数2和5的个数。由于阶乘中含有因数2的数量将远多于5的个数,因此只需要统计因数为5的个数。
  • 如果针对5的数量直接进行统计,时间复杂度将不是对数级别。因此如何统计5的个数就是解决问题的关键。
  • 我们不妨针对所给的n进行考虑,思考5 25 125 情况下包含5的个数规律,不难发现,每隔5倍,因子中包含5的数量都为n/5个。
  • 综合上述分析,循环次数仅需次数为log5n,代码如下。
class Solution:
    """
    @param: n: An integer
    @return: An integer, denote the number of trailing zeros in n!
    """
    def trailingZeros(self, n):
        # write your code here, try to do it without arithmetic operators.
        res = 0
        while n >=5 :
            res += n // 5
            n //=5
        return res

3.落单的数Ⅰ

描述

  • 给出 2 * n + 1个数字,除其中一个数字之外其他每个数字均出现两次,找到这个数字。(n≤100)

    挑战: 一次遍历,常数级的额外空间复杂度

思路

  • 最为直观的思路是利用set的特性来进行去重操作,但无法做到常数级别的空间复杂度,代码如下。
class Solution:
    """
    @param A: An integer array
    @return: An integer
    """
    def singleNumber(self, A):
        # write your code here
        hs = set()
        for i in A:
            if i in hs:
                hs.remove(i)
            else:
                hs.add(i)
        
        return hs.pop()

  • 利用位操作异或的特性,即a ^ a ^ b = b,可以实现空间复杂度为常数的目标。
class Solution:
    """
    @param A: An integer array
    @return: An integer
    """
    def singleNumber(self, A):
        # write your code here
        res = A[0]
        for i in A[1:]:
            res ^= i
        
        return res


4.统计数字

描述

  • 计算数字 k 在 0 到 n 中的出现的次数,k 可能是 0~9 的一个值。

样例

输入:
k = 1, n = 12
输出:
5
解释:
在 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 中,我们发现 1 出现了 5 次 (1, 10, 11, 12)(注意11中有两个1)。

思路

在不进行分析的情况下,使用二维for循环暴力计算显然会耗费大量的时间。

因此我们需要对数字出现次数分布进行分析,寻找更好的方法。
  • 寻找规律

    • 我们很容易想到数字本身就是从0-9循环累加,每一位都是从0到9之后进位。

    • 不妨对数字首位进行补零。补零后全部个位数、十位数、百位数…的总位数和分别为10 2×102 3×103…。

    • 设位数为n,n位数以内的全部数字出现的次数为n×10n

    • 因此每个数字出现的次数为n×10n/10=n×10n-1

    • 由于除个位外最高位数字不能为零,需要去掉除个位0之外的多余的零,此时需要分别统计k=0和k!=0两种情况。

    • k!=0时:
      在n位以内的全部数字中k出现的次数为: s u m k = n × 1 0 n − 1 sum_k =n×10^{n-1} sumk=n×10n1

    • k=0时:
      在n位以内的全部数字中k出现的次数为: s u m k = n × 1 0 n − 1 − ∑ 1 n 1 0 n − 1 + 1 sum_k =n×10^{n-1} - \sum_1^n 10^{n-1}+1 sumk=n×10n11n10n1+1最末尾的+1是指个位数时0可以为最高位数字

  • 总结上述规律之后,我们在统计时可以将统计数字简化为
    1.统计当前位所含数字k数量
    2.统计之前位所含数字k数量

  • 统计当前位数字k数量时需要对比当前位数字和k之间的关系,分类进行统计。

  • 综合上述分析,采用n/10>0循环来作为判断条件,当n为零时需要单独考虑,该算法循环次数仅需次数为log10n,空间复杂度为O(1),代码如下。

class Solution:
    """
    @param k: An integer
    @param n: An integer
    @return: An integer denote the count of digit k in 1..n
    """
    def digitCounts(self, k, n):
        # write your code here
        if n == 0 :
            if k == 0:
                return 1
            else:
                return 0
        
        unit = 1 //当前位的单位110100、
        place = 0 //当前位所在的前一位
        res = 0 //统计结果
        temp = 0 //存储n每一位的数值
        count = 0 //统计次一位数的总数
        tail = 0 //当k为0时统计过多计算的0
        
        while n > 0 :
            temp = n % 10
            
            if temp > k ://统计当前位所含数字k的数量
                res += unit
            elif temp == k:
                res += count+1
            
            if k == 0://统计之前位所含数字k的数量
                res += temp * place *unit//10 - tail//注意第一次个位时tail = 0
            else:
                res += temp * place * unit//10
                
            count += temp * unit
            place +=1
            unit *= 10
            n = n//10
            tail = unit
        return res
        


5.移除9

描述

  • 从整数1开始,删除任意整数包含9,像是9, 19, 29…
    现在,我们有一串新的整数序列: 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, …
    给定正整数n,你需要返回删除之后序列的第n个整数。注意1将会是第一个整数。

    n 不会超过 9 x 108.

样例

输入:88
输出:107
解释:
移除包含9的数字,第88个数字为107

思路

  • 实质上是进行了进制转换,将十进制的数转化为了9进制
  • 该算法循环次数仅需次数为log9n,空间复杂度为O(1)具体代码如下:
class Solution:
    """
    @param n: an integer
    @return: return an long integer
    """
    def newInteger(self, n):
        # write your code here
        res = 0
        times = 1
        
        while n > 0 :
            res += times*(n%9)
            n = n // 9
            times *=10
        
        return res

6.丑数 II

描述

  • 设计一个算法,找出只含素因子2,3,5 的第 n 小的数。
    符合条件的数如:1, 2, 3, 4, 5, 6, 8, 9, 10, 12…

      我们可以认为 1 也是一个丑数。
    

    挑战: 要求时间复杂度为 O(nlogn) 或者 O(n)。

样例

输入:9
输出:10

思路

  • 分析题目之后,需要明确这道题的关键在于找出只含2,3,5因子的数字。

  • 看到这道题时我很自然想到的是筛法,但筛法无法剔除掉不满足条件的因子。如果采取暴力方式,在筛掉数据之后,显然需要去逐个筛查每个字符是否满足只含2、3、5的因子。这样做无疑会严重增加算法的时间复杂度。

  • 因此我们这里需要将思路从剔除不满足条件的因子 转换到如何利用因子逐个生成满足条件的数字

  • 从数学的角度讲,这道题就是找出由集合{2i3j5k: i,j,k ∈ N}以及乘法运算构成的半群,按从小到大顺序的第n个元素。

  • 可以发现:从1开始,
    当1分别乘2,3,5之后,1就无需使用到
    当2分别乘2,3,5之后,2就无需使用到
    当某一个数字乘2过后,那么只需要考虑该数字乘3和5的情况。

  • 最好的方法就是采用动态规划的方式,逐个生成未使用的最小数字

      使用长度为n的数组存放依次生成的数字。
      
      3个指针指向数组中未被使用到的最小的数字的位置。
      
      从数组第二个位置开始到数组最后位置结束:
      
      	找出三个指针指向的数字生成的最小的数字,存放在数组的空位。
      	该数字对应的指针右移一位。
      	
      数组的最后一位数字即为满足条件的结果。
    
  • 该算法时间复杂度为O(n),空间复杂度为O(n),具体代码如下:

class Solution:
    """
    @param n: An integer
    @return: return a  integer as description.
    """
    
    def nthUglyNumber(self, n):
        # write your code here
        if n<=6: 
            return n
        temp = [0,0,0]
        a,b,c = 0,0,0
        res = [0 for i in range(n)]
        res[0] = 1
        for i in range(1, n):
            a = res[temp[0]]*2
            b = res[temp[1]]*3
            c = res[temp[2]]*5
            res[i] =  min(a,b,c)
            if res[i] == a :
                temp[0]+=1
            if res[i] == b :
                temp[1]+=1
            if res[i] == c :
                temp[2]+=1
                
        return res[n-1]

7.落单的数Ⅱ

描述

  • 给出3*n + 1 个非负整数,除其中一个数字之外其他每个数字均出现三次,找到这个数字。

样例

输入:  [1,1,2,3,3,3,2,2,4,1]
输出:  4

挑战: 一次遍历,常数级的额外空间复杂度

思路

  • 最为直观的思路是对数组进行排序,然后每隔3个数间隔对比之后一个数。但这样做显然无法做到一次遍历,因为在排序时就已经进行过遍历。

  • 仔细观察题目要求,可以试图将思路转变为如何将三个相同的数之和变为0

    由于非负整数,无需考虑补码存储问题,这里我们可以使用一个长度为32的int数组统计每一位1的数量,然后对3取余,然后再将数组转换为int即可。

  • 这样能够满足只遍历一次的要求,并且额外的空间复杂度只有常数级。算法时间复杂度为O(n)。

class Solution:
    """
    @param A: An integer array
    @return: An integer
    """
    def singleNumberII(self, A):
        # write your code here
        temp = [0 for i in range(32)]
        pos = 0
        res = 0
        
        for i in A:
            while i>0 :
                if i%2 == 1:
                    temp[pos] +=1
                pos +=1
                i //=2
            pos = 0
        
        
        for i in range(31,-1,-1):
            res <<= 1
            res += (temp[i] % 3)
        
        return res

  • 上述算法需要每次都进行额外的最差32次的循环,而且还需要将数组重组为int类型。如果能够使用位运算来处理,可以有效提高算法的速度,为此回顾了有限状态机相关的内容。

     构造一个三进制的归零的进制规则 即 00->01->10->00
     这样每三个相同的数字运算之后,都会回到0
     由于对于int来说额外增加一位共需64位的空间,因此这里使用两个int类型的变量分别存储高位和低位。最终高位将都是0,因此结果为低位值。
     前一步的低位^A[i]  & ~高位     得到当前低位值
     前一步的高位^A[i] & ~当前低位   得到当前高位值
    
  • 这样能够满足只遍历一次的要求,并且额外的空间只有2个int大小。算法时间复杂度为O(n),运行时间将小于上面的算法。

class Solution:
    """
    @param A: An integer array
    @return: An integer
    """
    def singleNumberII(self, A):
        # write your code here
        high = 0
        low = 0
        for i in A:
            low = low ^ i & ~high
            high = high ^i & ~low
        return low


8.落单的数Ⅲ

描述

  • 给出2*n + 2个的数字,除其中两个数字之外其他每个数字均出现两次,找到这两个数字。

样例

样例 1:
输入:  [1,2,2,3,4,4,5,3]
输出:  [1,5]

样例 2:
输入: [1,1,2,3,4,4]
输出:  [2,3]

挑战: O(n)时间复杂度,O(1)的额外空间复杂度

思路

  • 最为直观的思路是统计每个数字出现的次数,显然额外空间复杂度时O(n),想要完成挑战目标,还是要从位运算考虑。

  • 很容易发现这道题和Ⅰ相比多了一个数,对数组中的数字按位异或后的数为a^b,我们这里需要对a和b进行区分,区分之后便可以区分出这两个数。

     假设数组为A
     考察a^b
     由于a、b两数不同,那么必然存在1位k为1
     并且该位必定属于a、b其中之一
     那么也就是说我们以这一位k作为判断
     也就是对A[i] & k  ==0 判断 ,同时分别进行按位异或,
     就可以区分出来这两个数。
    
  • 找到k位为1数时可以逐位%2取余判断,也可以按位&1来判断。
    这里可以利用补码的特性,非零数 n & -n 可以得到 n 最后一位非零位。

      以8 bit为例 
      3: 0000 0011   -3: 1111 1101   3&-3:  0000 0001
      6: 0000 0110   -3: 1111 1011   3&-3:  0000 0010
    
  • 这样能够满足O(1)额外的空间复杂度,需要遍历两次算法时间复杂度为O(n)。

class Solution:
    """
    @param A: An integer array
    @return: An integer array
    """
    def singleNumberIII(self, A):
        # write your code here
        temp = 0
        res = [0,0]
        for i in A:
            temp ^= i
        temp  = temp & -temp
        for i in A:
            if (i & temp) == 0:
                res[0] ^= i
            else:
                res[1] ^= i
        
        return res


9.骰子求和

描述

  • 扔 n 个骰子,向上面的数字之和为 S。给定 n,请列出所有可能的 S 值及其相应的概率。

样例

输入:n = 2
输出:[[2,0.03],[3,0.06],[4,0.08],[5,0.11],[6,0.14],[7,0.17],[8,0.14],[9,0.11],[10,0.08],	[11,0.06],[12,0.03]]

思路

  • 这道题实际上是求掷骰子结果的概率分布,总的投掷可能性是6n,可能的结果范围为 n~6n

     当然我们可以模拟掷骰子的全部可能性,统计各个结果的总数
     但无疑这样做的时间复杂度和空间复杂度都会相当大
     这里应该致力于减少算法的空间复杂度和时间复杂度
     
     通常的解法是使用动态规划来进行处理,这里我并不想这样处理,可以参见 LintCode 20题评论下的解答方法。
    
  • 这次我们从数学的角度考虑,由于每一次投掷的结果都会依赖上一次的掷骰子的结果,实际上就是一种离散卷积。

  • 概率分布如下:
    F ( n ) = ∑ 1 6 F ( n − 1 ) k F(n) = \sum_{1}^6F(n-1)k F(n)=16F(n1)k 边 界 值 F ( 1 ) = k = [ 1 6 , 1 6 , 1 6 , 1 6 , 1 6 , 1 6 ] 边界值F(1)=k=[\frac1 6,\frac1 6,\frac1 6,\frac1 6,\frac1 6,\frac1 6] F1=k=[61,61,61,61,61,61]

  • 或者也可以理解为下式的展开项的系数: ( 1 + x + x 2 + x 3 + x 4 + x 5 ) n 6 n \frac{(1+x+x^2+x^3+x^4+x^5)^n}{6^n} 6n(1+x+x2+x3+x4+x5)n

  • 下面是具体算法,空间复杂度O(n),时间复杂度为O(n2)。

class Solution:
    # @param {int} n an integer
    # @return {tuple[]} a list of tuple(sum, probability)
    def dicesSum(self, n):
        # Write your code here
        if n == 1:
            return [(1,1/6.0),(2,1/6.0),(3,1/6.0),(4,1/6.0),(5,1/6.0),(6,1/6.0)]
        else:
            total = 5*n +1
            temp = [0.0 for i in range(total)]
            for i in range(6):
                temp[i] = 1.0
            current = 5
            nex = 10
            for s in range(1,n):
                for i in range(1,nex//2+1):
                    temp[nex-i] += (temp[nex-i-1]+ temp[nex-i-2]+temp[nex-i-3]+temp[nex-i-4]+temp[nex-i-5])
                temp[nex] = temp[0]
                for i in range(1,nex//2+1):
                    temp[i] = temp[nex-i]
                current = nex
                nex = current +5
            
            s = 6**n
            res = list()
            for i in range(total):
                res.append((n+i,temp[i]/s))
            return res


10 两数之和

描述

  • 给一个整数数组,找到两个数使得他们的和等于一个给定的数 target。
    你需要实现的函数twoSum需要返回这两个数的下标, 并且第一个下标小于第二个下标。注意这里下标的范围是 0 到 n-1。

样例

样例1:
给出 numbers = [2, 7, 11, 15], target = 9, 返回 [0, 1].
样例2:
给出 numbers = [15, 2, 7, 11], target = 9, 返回 [1, 2]。

挑战

  • O(n) 空间复杂度,O(nlogn)时间复杂度,
    O(n) 空间复杂度,O(n)时间复杂度,

思路

  • 很明显这道题想要降低时间复杂度需要使用额外的空间
  • 注意到当一个数a在数组中,那么必然target - a也在数组中
  • 因此我们构造一个存储target - number[i] 的另一个容器类,然后判断是否number[i]在该容器中,即可查找到两个目标数的位置。
  • 这里选用set可以使时间复杂度降为O(n)
  • 下面是具体算法,时间复杂度O(n),空间复杂度O(n).
class Solution:
    """
    @param numbers: An array of Integer
    @param target: target = numbers[index1] + numbers[index2]
    @return: [index1 + 1, index2 + 1] (index1 < index2)
    """
    def twoSum(self, numbers, target):
        # write your code here
        res = [-1,-1]
        check = 0
        temp = {(target - numbers[i]) for i in range(len(numbers))}
        for i in range(len(numbers)):
            if numbers[i] in temp:
                if target == 0:
                    if check == 0:
                        res[0] = i
                        check+=1
                    elif (numbers[i] + numbers[res[0]]) == target:
                        res[1] = i
                        return res
                    elif check == 1:
                        res[1] = i
                        check +=1
                    elif (numbers[res[0]] + numbers) == target:
                        res[1] = i
                        return res
                    else :
                        res[0] = res[1]
                        res[1] = i
                        return res
                else :
                    if check == 0:
                        res[0] = i
                        check +=1
                    else :
                        res[1] = i
                        return res
        return res
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值