python版数据结构与算法-递归和双指针算法

递归

什么是递归?

递归是一种算法或函数的编程技巧,它通过直接或间接调用自身来解决问题。在递归过程中,一个问题被分解为更小的子问题,直到问题的规模变得足够小,可以直接求解。
在Python中,我们可以使用递归来解决许多问题,例如计算阶乘、斐波那契数列等。递归的关键在于找到一个可以将问题分解为更小规模的相同类型问题的解法。

递归的基本思想

递归的基本思想是将一个大问题分解为若干个相同类型的子问题,然后逐层解决这些子问题。当子问题可以直接求解时,递归终止;否则,继续对子问题进行递归求解。

递归通常有两个重要的概念:基本情况(base case)和递归情况(recursive case)。
  1. 基本情况:也称为边界条件,是指递归过程中不再需要继续递归的条件。如果一个问题的规模足够小,以至于可以直接求解,那么这个问题就满足基本情况。例如,计算阶乘的问题,当n为0或1时,阶乘值为1,此时问题已经满足基本情况,无需再进行递归。
  2. 递归情况:也称为递推关系,是指将问题分解为更小的子问题,并通过递归调用自身来解决这些子问题。例如,计算斐波那契数列的问题,我们可以将第n个斐波那契数表示为前两个斐波那契数之和,即fib(n) = fib(n-1) + fib(n-2)。这里,fib(n-1)和fib(n-2)就是递归情况。
在使用递归时,需要注意以下几点:
  1. 确保所有递归调用都要有控制语句。如果一个递归函数没有结束语句,那么它将永远不会停止递归调用,导致无限循环。最后导致电脑崩溃.
  2. 尽量使递归深度保持在可接受的范围内。过深的递归深度可能导致栈溢出错误。可以通过设置递归限制或者使用尾递归优化来避免这个问题。
  3. 注意避免重复计算。在递归过程中,可能会遇到多次计算相同的子问题。为了避免这种情况,可以将已经计算过的子问题的答案存储起来,下次需要时直接使用。这可以通过使用字典或者集合来实现。
    比如说要计算5的阶乘
def factorial(n):
    # 基本情况:当n为0或1时,阶乘值为1
    if n == 0 or n == 1:
        return 1
    # 递归情况:计算n的阶乘等于n乘以(n-1)的阶乘
    else:
        return n * factorial(n-1)
# 测试
print(factorial(5))  # 输出:120
递归通常被用来解决需要重复划分的问题,如树结构、排序等。递归有助于简化代码,提高可读性,但需要注意递归深度和性能问题。

双指针算法

什么是双指针算法

双指针算法是一种高效的解决数组或链表问题的算法。它通过使用两个指针,一个快指针和一个慢指针,来遍历数组或链表,从而找到满足特定条件的元素或节点。

实现思想

在数组中,我们通常使用快指针来遍历数组,而慢指针则用来标记已经访问过的元素。当快指针到达数组的末尾时,慢指针指向的就是最后一个未被访问的元素。这种方法的时间复杂度为O(n),因为每个元素只会被访问一次。
在链表中,快指针通常用于遍历链表的头部,而慢指针则用于跟随链表的尾部。当快指针到达链表的末尾时,慢指针将指向链表的最后一个节点。这种方法的时间复杂度也为O(n),因为每个节点只会被访问一次。

双指针算法通常有三种形式:快慢指针、左右指针、滑动窗口。
快慢指针通常用于链表中寻找链表中点或判断链表是否有环等问题。
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None

def hasCycle(head: ListNode) -> bool:
    slow = head
    fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False

在上面的例子中,我们定义了一个 ListNode 类来表示链表节点,然后定义了一个 hasCycle 函数来判断链表是否有环。在函数中,我们设置了两个指针 slow 和 fast,其中 slow 每次移动一个节点,而 fast 每次移动两个节点。如果链表中存在环,那么这两个指针最终会相遇;否则,它们会在某个时刻相遇之前停止移动。

左右指针通常用于数组中寻找两个数之和等问题。

比如leetcode的第一题两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。


示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]
def twoSum(nums: list[int], target: int) -> list[int]:
    left = 0
    right = len(nums) - 1
    while left < right:
        current_sum = nums[left] + nums[right]
        if current_sum == target:
            return [left, right]
        elif current_sum < target:
            left += 1
        else:
            right -= 1
    return []

这个题很明显,用双指针是很好的实现思路,你也可以用单循环实现,但是单循环太慢了,时间复杂度更高,当数组的长度很大的时候,单循环很慢.

滑动窗口通常用于字符串或数组中寻找一段连续子串或子序列等问题。
如何使用滑动窗口算法查找一个字符串中最长的无重复字符子串?

先看代码

def max_length_of_substring(s):
    n = len(s)
    if n == 0:
        return 0
    # 定义双指针left和right,以及一个字典来存储字符出现的位置
    left, right = 0, 0
    char_dict = {}
    max_len = 0
    while right < n:
        # 如果当前字符已经在字典中出现过,则更新左指针的位置
        if s[right] in char_dict and char_dict[s[right]] >= left:
            left = char_dict[s[right]] + 1
        # 更新当前字符在字典中的位置
        char_dict[s[right]] = right
        # 更新最大长度
        max_len = max(max_len, right - left + 1)
        # 右指针向右移动一位
        right += 1
    return max_len

解决这个问题的思路是,创建一个滑动窗口,使得窗口中的字符都是不重复的。
具体来说,我们定义两个指针left和right,分别指向滑动窗口的左右边界。当右指针向右移动时,如果当前字符已经在窗口中出现过,则需要更新左指针的位置,以便保证窗口中的字符都是不重复的。
如果当前字符已经在字典中出现过,并且其位置大于等于左指针的位置,则说明需要将左指针移动到当前字符上一次出现的位置的下一位。否则,说明当前字符是第一次出现,可以继续扩展窗口。

需要注意的是,在这个题目中,我们需要使用一个字典来存储每个字符最近一次出现的位置。具体来说,字典的键是字符,值是该字符最近一次出现的位置。当右指针向右移动时,我们需要将当前字符加入字典中,并更新其出现的位置。

Python双指针算法是一种非常实用的算法,它可以帮助我们在处理数组和链表问题时提高效率。
如果你有什么问题,欢迎给我留言

你可能感兴趣的:(python,算法,python,leetcode)