python函数式编程

本文详细介绍了Python编程中的列表推导式,包括其语法、使用场景和示例。此外,还探讨了递归的概念,讲解了递归函数的基础情况、递归情况,并通过斐波那契数列和打家劫舍问题举例说明。最后,通过解决两数相加的链表问题,展示了如何运用递归解决实际问题。文章深入浅出地阐述了列表推导式和递归在Python中的应用,帮助读者更好地理解和掌握这些概念。
摘要由CSDN通过智能技术生成

python列表推导式

列表推导式的语法格式:[f(x) for x in L],其中 f(x) 表示一个函数,作用于列表 L 的每一个元素。所以整体来看,就是将函数f映射到列表 L 中的每个元素上。其返回值是一个新的列表

例如,将列表中每个元素加1

[x+1 for x in [10,20,30]] # [11, 21, 31]

L 也可以是一个字符串

[x for x in 'string'] # ['s', 't', 'r', 'i', 'n', 'g']

当然也可以加if 的判断条件,用于对元素进行条件判断。

一般语法格式为:[f(x) for x in L if ...],返回值是哪些符合 if 条件的元素所组成的新列表。

# ['javascript', 'python']
[str for str in ['java','javascript', 'python'] if len(str) > 4] 

lambda表达式

lambda表达式是根据丘奇的lambda演算定义的,它是最纯粹的函数式编程。

语法格式为:(lambda arg : state)(arg)。其中 lambda 是一个关键字,: 前的 arg 是参数,而 state 是要执行的语句,第二个括号里的 arg 是外部传入的参数。

可以看下下面的例子🌰 :

(lambda x:x+1)(10) # 11

当然可以接收任意的参数:

(lambda x,y : x+y)(10,20) # 30

如果要进行逻辑判断呢?当然也是可以的,不过有一点是需要注意的,那就是不能使用 if 语句,因为它并不是一个表达式。纯粹的函数式编程是不能有 if 存在的。

 (lambda x : x % 2 == 0)(20) # True

如果想到一个列表进行筛选,那岂不是也需要判断条件吗?让我们再来看一个🌰 :

加入我们想筛选出一个列表中的偶数,返回一个符合条件的所有元素组成的新列表,可以这样做

list(filter(lambda x : x % 2 == 0,[10,20,1,2,3,8])) # [10, 20, 2, 8]

看到里面使用到了filter函数,我们就顺便说一下filter

filter

filter 是用来进行筛选的函数,接收一个函数和一个列表,在上面的例子中,filter(func, list),我们传入的是一个 lambda表达式,它是一个匿名函数,[10,20,1,2,3,8]即是这个lambda要处理的元素集合,而filter 将函数映射到了列表中的每一个元素上,最后我们将返回的结果转化为一个list列表。

看一个接收具名函数的例子🌰 :

def isOdd(x):
    return x%2 == 0
    
 list(filter(isOdd,[1,2,3,4,5,6])) # [2, 4, 6]

像这种简单的函数,也许使用匿名函数会更简洁,也不影响代码的可读性。但需要注意的是,filter 所接收的函数只能接收一个参数,且需要返回一个布尔值,这个函数本身被称为谓词,我认为是因为它执行一个动作,所以称为谓词。

map

通过 filter 很容易想到,如果我们不是做函数,而是做一个集合的完全映射呢?即把一个集合通过一个函数完全映射为了另外一个函数,这样的说法似乎很熟悉,是不是很像中学时候学到的函数,f(x) -> y,python的内置函数map可以帮我们做到这一点。

同样的,它接收一个单参数函数,但是这个函数不需要像filter那样返回布尔值,事实上我们完全可以用map替代filter,但是filter本身值得单独存在。

看下面一个例子🌰 :

def add(x):
    return x + 1
    
list(map(add, [1,2,3])) # [2,3,4]

同样我们可以将具名函数 add 替换为匿名函数,我们使用lambda来做到这一点:

list(map(lambda x : x+1, [1,2,3])) # [2,3,4]

我们也可以传入内置单参函数:

list(map(len, ['java', 'javascript', 'python'])) # [4,10,6]

reduce

假如我们要将一个列表中的所有元素相加求和,应该怎么做?嗯,可以这样子:

sum([1,2,3,4,5]) # 15

但,我相信你知道我不是这个意思。我想说的是,如果我们使用reduce帮忙我实现呢?

reduce(lambda x,y : x+y, [1,2,3,4,5]) # 15

发现什么不同了么?对比前面的filtermap来说,它接收的是两个参数,是的,只能接收两个参数的函数。为什么这样呢?因为它的功能是做规约,即将列表中所有的元素规约为一个值。

它的工作过程大概是这样的:reduce 从列表中获取前两个元素,并使用传入的函数将其规约为单个值;然后这一过程继续进行,将得到的单个值与列表中的下一个元素结合,再次规约为单个值,这个过程一直持续,一直到仅剩单个值。

mapReduce

mapreduce 组合在一起会组成强大的功能,先使用 map 转换列表中的值,然后使用 reducce 将结果合并为单个值。

reduce(reduceFunction, map(mapFunction, yourList))
def add(x, y):
    return x + y
    
reduce(add, map(lambda x : x + 1, [1,2,3,4,5])) # 20

我们可以直接封装为 mapReduce 函数:

def mapReduce(mapFunction, reduceFunction, yourList):
    return reduce(reduceFunction, mapFunction(yourList))

递归

递归和函数调用

递归就是这样,让人以为清楚又不清楚,在解释什么是递归之前,先说说什么不是递归。

def func1(x):
    result = 2*x
    return result

def func2(x):
    y = x + 1
    z = func1(y)
    return x + y + z

print(func2(5))

函数 func2 里调用了 func1,请问这是递归吗?这不是,那函数调用和递归有关系吗?答案是有关系。

image.png

image.png

让我们再来看一个递归的经典例子:斐波那契数列。

从斐波那契数列说开来

斐波那契数列:

def factorial(n):
    if n <= 1:
        return 1

    return n * factorial(n-1)

factorial(5)

image.png

image.png

所以,递归就是一个函数调用另一个函数,只是恰好被调用的函数与调用函数的名称相同而已。

但是我们如果对一个问题进行递归算法的设计呢?

我们想一想,一般的函数调用,函数A调用函数B,调用完之后就结束了。就算是有多层嵌套,那么函数A调用函数B,函数B调用函数C,函数C再调用函数D,调用到最后还是会结束,然后逐一向外返回结果。而递归这种自己调用自身的呢?在没有任何限制的情况下,会无限的进行嵌套调用,不会停止,导致爆栈。可是应该何时判断递归调用结束,避免爆栈呢?当然是在函数自身内部进行判断,也是到达所谓的基本情况。因此使用递归最基本的,也是非常重要的地方,就是找到基本情况

我们来分析一下这个斐波那契数列递归算法的基本情况:

到哪里停止?像我们上面的例子来说,输入5,递归调用自身,需要f(4),参数 n 的限制是什么?到了这个限制的边界上,应该如何处理?我们看到它的基本情况是 n=1,只有这样一个基本情况。然后递归函数只有一个基本情况吗?针对斐波那契来说是这样的,我们来看另外一个例子,也是常说的打家劫舍问题。

认真说说打家劫舍问题

小偷去偷东西,他所带的包只能放下42个单位的东西,他可以偷到的东西重量分别是5、10、18、23、30、45,在这种情况下,小偷所能够偷到的货物重要最多是多少呢?

我们直观来看,如果他选择18和23,那么总重量为41,是目前最好的组合。但是他所能偷到的东西增加了一个2单位的物品,情况就不一样了,这时候他选择2、10、30三种物品,总重量刚好就是42。

在这个例子里,就存在两种基本情况,一个是小偷所能带走的容量,另一个是他所选择物品的总重量。我们来设计这样一个函数,他接收两个容量和总重量两个参数,返回能够选择的物品之间的最大重量:

subset(42,[5,10,1823,30,45]) // 41

subset(42,[2,5,10,18,23,30,45]) // 42

如何设计这样一个递归函数呢?我们从考虑基本情况说起。先考虑容量,容量最小是什么情况呢?

容量最小肯定只能是0,虽然现实中肯定不可能小于0,但是作为参数判断,肯定要考虑小于0的情况,因此当输入的容量 <= 0 的时候,直接返回0,容量为0根本没法偷。

因此:

if capacity <= 0:
    return 0

这时候我们来考虑第二种情况,如果能偷的物品列表为空呢?也是没得偷的情况,这种情况的话能偷的情况肯定也是0,因为没得偷:

if items == []:
    return 0

这两种情况肯定是并列的,所以我们可以放在一起:

if capacity <=0 or items == []:
    return 0

这就是基础的情况,接下来我们来考虑如何设计递归呢?

我们这样来考虑,面对一个一个的物品(因为列表是有顺序)的,所以对于当前物品,我们面临两种选择,偷或者不偷。首先有一种特殊的情况是需要考虑的,那就是如果第一个物品的重量就是大于容量的话,我们的选择是不偷。

if capacity <=0 or items == []:
    return 0
    
first = items[0] # 第一个物品
rest = items[1:] # 剩下的物品

if first > capacity:
    return subset(capacity, rest)

如果第一个物品的重量是等于容量的话,我们的选择是直接偷:

if capacity <=0 or items == []:
    return 0
    
first = items[0] # 第一个物品
rest = items[1:] # 剩下的物品

if first > capacity:
    return subset(capacity, rest)

if first == capacity:
    return first

如果第一个物品重量小于剩余容量的话,则我们要考虑偷或者不偷,但是事实上却没有这么简单。

因为我们的目标是偷到的物品总重量最大,因此可能存在这两种不同的情况:

1.假如剩余容量capacity为10,物品重量列表为items[8,4,6],那么这种情况下应该不偷第一个物品,我们选择偷后两种物品,这样能够偷到的总重量为10。

2.但是如果同样容量为10的情况下,物品重量列表为[4,8,6],这种情况下我们应该取第一个和第三个物品,这样子偷的物品重量最大。

所以不能只看当前物品重量是否小于容量来决定是不是要偷,而是要比较不同的情况下偷到的总重量

你或许会有这样一个疑问,那我岂不是手动要比较每一次?那不是需要全部比较完才知道结果?事实上递归是这样运作的:本次在A偷与不偷,我们全部走一次,偷的情况下,偷了以后再看剩余的容量和剩余的物品列表,再继续看当前物品偷不偷,然后这时候又是两种情况都进行,偷的情况下走了一个分支,继续下一次偷与不偷的判断,然后不偷的情况走一个分支,继续下一次偷与不偷的判断。到了最后基本的情况开始一层一层的返回,返回每一次偷的结果和不偷的结果,对比得到偷与不偷,这个结果再继续返回,在对比上一层偷与不偷的结果,只到返回到最外层,判断最初的那个物品的选择,偷与不偷的情况。

是不是有点烧脑🤯 ?递归就是这样,你以为你理解了,但细细考虑,你发现你还是没考虑情况,总是把自己陷进去,这可能是由于计算机是用栈来记录递归调用情况的,而我们的大脑并不是的原因吧。

让我们用代码来描述这个过程:

if first < capacity:
    # 选择当前物品
    use = first + subset(capacity - first, rest)
    # 舍弃当前物品
    lose = subset(capacity, rest)
    # 比较两种情况能够获取的总重量
    return max(use, lose)

由于每一次都有两种情况,所以如果物品列表items中有n个物品,那么最多可以产生2的n次方次调用。

这里最难理解的,可能是每一次的两种选择,需要再假定当前取或不取情况下两种选择分别的情况,然后这个过程持续进行,可能脑中很难去演义这整个过程。我觉得好的方式是,不在大脑中去演示这种一直递归的情况,因为我们的脑子没有一个栈来保存调用记录,而是理解清楚每一个选择都需要简化的一个选择,直到最基本的情况。

让我们来看下全部的代码:

def subset(capacity, items):
    if capacity <=0 or items == []:
     return 0

    first = items[0] # 第一个物品
    rest = items[1:] # 剩下的物品
    
    if first > capacity:
     return subset(capacity, rest)

    if first == capacity:
     return first

    if first < capacity:
        # 选择当前物品
        use = first + subset(capacity - first, rest)
        # 舍弃当前物品
        lose = subset(capacity, rest)
        # 比较两种情况能够获取的总重量
        return max(use, lose)

我们来验证一下:

maxVal = subset(10,[3,4,2,9,10])
print(maxVal) # 10

我们来总结一下:

首先递归就是一个函数调用另一个函数,只不过是函数调用的恰好是它自身。我们在设计递归函数时,使用的策略是解决较小版本的问题让我们更接近解决原来版本的问题,这个较小版本的问题更加接近基本情况。我们需要清楚的就是:这个问题必须处理的最简单或者说最基本的输入是什么?

典型的递归函数应该有两个主要部分:

(1)基本情况:针对函数的“最简单”参数返回的值,这种情况下已经没有自相似性的存在。

(2)递归情况:这是较小版本的问题的解。利用更小或者更简单的参数来调用该函数,然后以某种方式利用较小问题的解来解决原始问题。

另外递归的模式可以总结为:

(1)将输入分解为first和rest,或者类似的东西,即让问题简化

(2)对rest部分进行递归

(3)按照特定于问题的方式,将递归调用的结果和rest组合起来

(4)记住并推理基本情况,有时候可能需要几种基本情况。

那么递归难道仅仅只是如此吗?我的意思是说,难道所有的递归函数都只需要考虑两种情况吗?当然不是这样,接下来我们就看一个问题。

字符串转化的最小距离:给定两个字符串,计算将第一个字符串转换成第二个字符串所需要的最少操作。可以采用三种操作:替换、插入和删除。

举个例子,假如给定的两个字符串分别是 alicecmice,我们要将 alice 转换成 cmice,对比两个字符串,对于alice的第一个字符a,我们可以考虑将在头部插入一个c,也可以直接将a替换成c,也可以将a删除。

我们来分析这三种情况:

如果在头部插入c,那么第一个字符串(以下称为字符串A)变为calice,那么就将问题转化为了将字符串calice变为第二个字符串cmice(以下称为字符串B)。如果将a替换为c,那问题就转化为了将字符串clice变为字符串cmice。如果将a删除,那么问题就转化为了将字符串lice变为字符串cmice

因此每次我们有三种选择,相比上面的例子相对复杂了一些。

这时候我们来看下这个问题所需要考虑的基本问题:

我们所接收的参数有两个,分别是字符串A和字符串B,那么最基本的情况是字符串为空。假如A为空,B不为空,那么A变成B所需要的步骤即为B的长度,将B中每一个字符都依次插入到A;如果A不为空,B为空,那么A变成B所需要的步骤即为A的长度,将A中每一个字符都删除,直至为空;那么如果A和B同时为空的话,那么不需要任何操作,步骤为0。

我们来写下代码:

def translate(s1,s2):
    if len(s1) == 0:
        return len(s2)
  
    if len(s2) == 0:
        return len(s1)

考虑完基本的情况,我们来考虑递归的情况:

我们要逐个对比两个字符串的每个字符是否相同,如果相同,那么直接删除当前字符,然后将问题简化为去掉当前字符的情况。

def translate(s1,s2):
    if len(s1) == 0:
        return len(s2)
    elif len(s2) == 0:
        return len(s1)
    elif s1[0] == s2[0]:
        return translate(s1[1:],s2[1:])

如果字符串当前所比较的两个字符不相同呢?我们可以有三种操作,意味着我们有三种选择,这时候可以像上面的例子一样,我们全部尝试,看哪一个步骤最少。但是我们是要递归进行,在递归时候我们要考虑的就是参数如何变化,每一次的结果是怎样的。上面已经分析过了,我们再来回顾一下:

(1)如果在头部插入c,那么第一个字符串(以下称为字符串A)变为calice,那么就将问题转化为了将字符串calice变为第二个字符串cmice(以下称为字符串B)。当然可以不体现这一步,B去掉首字符,相当于插入之后A和B都去除首字符了,即tranlate(s1,s2[1:]),操作增加了一次(插入操作)。

(2)如果将a替换为c,那问题就转化为了将字符串clice变为字符串cmice。这时候字符串A和B都可以将当前字符(首字符)删除,即tranlate(s1[1:],s2[1:])`,操作增加了一次(修改操作)。

(3)如果将a删除,那么问题就转化为了将字符串lice变为字符串cmice这时候A字符串都能将当前字符(首字符)剔除,而B字符串不变,即tranlate(s1[1:],s2),操作增加了一次(删除操作)。

转化为代码:

insert = 1 + translate(s1,s2[1:])
replace = 1 + translate(s1[1:], s2[1:])
delete = 1 + translate(s1[1:],s2)
return min(insert,replace,delete)

我们看下完整的代码:

def translate(s1,s2):
    if len(s1) == 0:
        return len(s2)
    elif len(s2) == 0:
        return len(s1)
    elif s1[0] == s2[0]:
        return translate(s1[1:],s2[1:])
    else:
        insert = 1 + translate(s1,s2[1:])
        replace = 1 + translate(s1[1:], s2[1:])
        delete = 1 + translate(s1[1:],s2)
        return min(insert,replace,delete)

我们来验证一下:

print(translate('alcei','clsgt')) # 4

讲了这么多的递归,我们何不来练习一把?就像上学时候,总是要做大量的练习来巩固知识的。

刻意练习 —— 从leetcode上的递归问题开始叭

两数相加

【题目】给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

【解题】我们考虑用递归来解决这个问题。首先考虑基本情况,给定的输入是两个链表,要做的事情是把链表节点上的值相加,那么边界条件便是链表的某一个节点是不是存在呢?那既然有两个链表,可能的情况有三种了:两个链表的节点都不存在;链表A的节点存在,链表B的节点不存在;链表A的节点不存在,链表B的节点存在。如果是第一种情况,就没有什么好说的了,应该直接返回None。而如果是后面两种情况的话,也就是两个链表不一样长的情况下,还是要相加的,只是不存在的那个节点的值要把它变成0。

我们来将以上的分析转化为代码:

# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        if l1 == None and l2 == None:
            return None
        if l1 == None:
            l1 = ListNode(0)
        if l2 == None:
            l2 = ListNode(0)

那么接下来便可以考虑递归的情况了:

递归进行的便是两个节点的值相加,这样子就会存在进位的情况:两数之和如果大于10,那便要进位。所以我们要对是否进位进行判断。可是如果要考虑进位的话,那么进位应该保存在哪里呢?因为这一次的进位要加到下一节点上去,那么我们便可以直接给下一个节点上的值加1,无论加到哪个链表的节点上都可以,因为和是不会变的。在进行这一步操作的时候,又要考虑一个问题,那便是下一个节点存在吗?如果存在的话直接加1就是了,如果不存在呢?我们需要初始化这个节点,将其值设置为1,即可。

我们来看下👀代码:

# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        if l1 == None and l2 == None:
            return None
        if l1 == None:
            l1 = ListNode(0)
        if l2 == None:
            l2 = ListNode(0)
            
        sum = l1.val + l2.val
        if sum  >= 10:
            sum = sum % 10
            if l1.next == None:
                l1.next = ListNode(1)
            else:
                l1.next.val += 1
             

进位是处理完了,可是递归呢?当然我们刚才写的就是递归的内容,但是递归调用呢?递归调用当然是调用自身了,也就是addTwoNumbers这个函数,可是参数呢?参数如何动态的变化呢?当然你可能会说不就是传入l1.nextl2.next吗?是这样的,这个时候我们是进行了递归:addTwoNumbers(l1.next,l2.next)。可是,我们又返回值吗?我们返回什么呢?难道返回addTwoNumbers(l1.next,l2.next)吗?那它的结果是什么呢?我们现在来看一看,我们刚才写的代码里,return出现在了哪些地方?发现只有一个,那就是当两个链表的节点都不存在的时候,返回了None。是不是很奇怪?那我们该如何处理呢?

这个时候再想想题目里要求我们返回的是什么?是一个链表!那我们每一个去初始化一个链表返回不就可以了吗?那这个链表的值是什么,以及下一个节点该指向什么呢?首先说值,应该是两个链表对应节点值的和,如果需要进位那就将进位加到下一个节点上去,而本身的值应该是和 % 10的结果。

那我们要返回的链表的下一个指向呢?我们目前还是没有进行递归调用,我们可以在这里进行递归调用,那便是:

return ListNode(sum, addTwoNumbers(l1.next,l2.next))

是不是感觉会有一些惊奇😂

但其实如果仔细想想,我们这个函数的目标就是生成一个新的链表,返回值当然应该是一个链表了。

我们来看一下整体的代码:

# class ListNode:
#     def __init__(self, val=0, next=None):
#         self.val = val
#         self.next = next
class Solution:
    def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
        if l1 == None and l2 == None:
            return None
        if l1 == None:
            l1 = ListNode(0)
        if l2 == None:
            l2 = ListNode(0)

        sum = l1.val + l2.val
        if sum >= 10:
            sum = sum % 10
            if l1.next == None:
                l1.next = ListNode(1)
            else:
                l1.next.val += 1
        return ListNode(sum,self.addTwoNumbers(l1.next,l2.next))
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值