每日一题——Python实现PAT甲级1029 Median(举一反三+思想解读+逐步优化)


一个认为一切根源都是“自己不够强”的INTJ

个人主页:用哲学编程-CSDN博客
专栏:每日一题——举一反三
Python编程学习
Python内置函数

Python-3.12.0文档解读

目录

我的方法

代码功能和结构点评

时间复杂度分析

空间复杂度分析

优化建议

我要更强!

代码详解:

时间和空间复杂度

示例解释

示例输入

合并后的数组

中位数位置

详细步骤

哲学和编程思想

编程思想

哲学思想

示例的哲学与编程思想

举一反三

技巧1:化繁为简

技巧2:递归的自相似性

技巧3:二分查找

技巧4:抽象

技巧5:分治法和递归结合(归并排序)

总结


题目链接


我的方法

nums1=list(map(int,input().split()))
N1=nums1[0]
nums1=nums1[1:]
nums2=list(map(int,input().split()))
N2=nums2[0]
nums2=nums2[1:]

nums1+=nums2
nums1.sort()

if (N1+N2)%2==0:
    print(nums1[(N1+N2)//2-1])
else:
    print(nums1[(N1+N2)//2])

这段代码的功能是读取两个列表(nums1 和 nums2),合并它们,排序,然后根据合并后的列表长度的奇偶性,输出中位数。

代码功能和结构点评

  1. 输入处理:
    • 两次调用 input().split() 读取输入,将其转换为整型列表。
    • nums1 和 nums2 的第一个元素被分别赋值给 N1 和 N2,表示两个列表的长度。
    • 余下的元素分别保存在 nums1 和 nums2 中。
  2. 合并与排序:
    • 将 nums2 合并到 nums1 中,然后对合并后的列表进行排序。
  3. 中位数计算:
  • 根据合并后列表长度的奇偶性,计算并输出中位数。

时间复杂度分析

  1. 读取输入:
    • map(int, input().split()) 的时间复杂度是 O(N),这里 N 是输入的元素总数。
  2. 合并列表:
    • nums1 += nums2 是 O(N2),其中 N2 是 nums2 的长度。
  3. 排序:
    • 使用 sort() 方法对列表排序,时间复杂度是 O((N1 + N2) log(N1 + N2)),因为采用的是 Timsort 算法。
  4. 中位数查找:
  • 访问列表元素的时间复杂度是 O(1)。

综上所述,总的时间复杂度是: [ O(N1 + N2 + (N1 + N2) \log(N1 + N2)) ]

空间复杂度分析

  • 需要额外的空间来存储输入的整数列表,空间复杂度是 O(N1 + N2)。
  • sort() 方法在最差情况下使用 O(N1 + N2) 的额外空间(Timsort 的空间复杂度)。

综上所述,总的空间复杂度是: [ O(N1 + N2) ]

优化建议

合并和排序的时间复杂度是这段代码的主要瓶颈。如果只是为了找中位数,可以采用更高效的算法(如归并排序中的选择算法),其时间复杂度为 O(N1 + N2),无需对整个列表排序。


我要更强!

要优化这段代码的时间复杂度和空间复杂度,可以使用一种称为“二分查找”的方法来找到两个有序数组的中位数,而无需将它们合并和排序。这个方法的时间复杂度是 O(log(min(N1, N2))),空间复杂度是 O(1)。

以下是实现这个方法的完整代码和注释:

def findMedianSortedArrays(nums1, nums2):
    def find_kth_element(arr1, arr2, k):
        # 如果 arr1 比 arr2 长,交换它们
        if len(arr1) > len(arr2):
            arr1, arr2 = arr2, arr1
        # 如果 arr1 为空,直接返回 arr2 中的第 k 个元素
        if len(arr1) == 0:
            return arr2[k - 1]
        # 如果 k == 1,返回两个数组第一个元素中较小的一个
        if k == 1:
            return min(arr1[0], arr2[0])

        # 取两个数组的第 k//2 个元素进行比较
        i = min(len(arr1), k // 2)
        j = min(len(arr2), k // 2)

        if arr1[i - 1] > arr2[j - 1]:
            return find_kth_element(arr1, arr2[j:], k - j)
        else:
            return find_kth_element(arr1[i:], arr2, k - i)

    total_len = len(nums1) + len(nums2)
    if total_len % 2 == 1:
        # 如果总长度是奇数,返回第 (total_len // 2 + 1) 个元素
        return find_kth_element(nums1, nums2, total_len // 2 + 1)
    else:
        # 如果总长度是偶数,返回第 (total_len // 2) 个元素
        return find_kth_element(nums1, nums2, total_len // 2)

# 读取输入
import sys
input = sys.stdin.read
data = input().split()

# 解析输入
n1 = int(data[0])
nums1 = list(map(int, data[1:n1+1]))

n2 = int(data[n1+1])
nums2 = list(map(int, data[n1+2:]))

# 调用函数并输出结果
print(findMedianSortedArrays(nums1, nums2))
  • 代码详解:

  1. 定义 find_kth_element 函数:
    • 该函数用于查找两个有序数组中的第 k 个元素。
    • 如果数组 arr1 比 arr2 长,则交换它们,以确保 arr1 是较短的数组。
    • 如果 arr1 为空,则直接返回 arr2 中的第 k 个元素。
    • 如果 k == 1,返回两个数组第一个元素中较小的一个。
    • 通过比较 arr1 和 arr2 的第 k//2 个元素来缩小查找范围。
  2. 计算总长度 total_len:
    • 如果总长度是奇数,则返回第 (total_len // 2 + 1) 个元素。
    • 如果总长度是偶数,则返回第 (total_len // 2) 个元素(即中间两个元素偏左的那个)。
  3. 读取和解析输入:
    • 使用 sys.stdin.read 读取输入,并根据输入格式解析成两个数组。
  4. 调用函数并输出结果:
  • 调用 findMedianSortedArrays 函数计算中位数,并输出结果。

这样,通过二分查找,我们能以 O(log(min(N1, N2))) 的时间复杂度找到两个有序数组的中位数。

时间和空间复杂度

  • 时间复杂度:O(log(min(N1, N2))),因为我们对较短的数组进行二分查找。
  • 空间复杂度:O(1),只使用了常数级别的额外空间。

这段代码高效地找到了两个有序数组的中位数,避免了合并和排序的高时间复杂度,并且只使用了常数级别的额外空间。

示例解释

示例输入

数组1:[11, 12, 13, 14]
数组2:[9, 10, 15, 16, 17]

合并后的数组

合并并排序后,我们得到一个新的排序数组:[9, 10, 11, 12, 13, 14, 15, 16, 17]

中位数位置

由于合并后的数组长度为 9(奇数),中位数是第 5 个元素(偏左的那个):13

详细步骤

因为 arr1[1] > arr2[1],所以排除 arr2 的前 j = 2 个元素,并递归查找剩下的第 k - j = 3 个元素。

因为 arr1[0] <= arr2[0],所以排除 arr1 的前 i = 1 个元素,并递归查找剩下的第 k - i = 2 个元素。

因为 arr1[0] <= arr2[0],所以排除 arr1 的前 i = 1 个元素,并递归查找剩下的第 k - i = 1 个元素。

  1. 调用 findMedianSortedArrays(nums1, nums2):
    • nums1 = [11, 12, 13, 14]
    • nums2 = [9, 10, 15, 16, 17]
    • total_len = 9(奇数)。
  2. 查找第 (9 // 2 + 1) = 5 个元素。
  3. 调用 find_kth_element(nums1, nums2, 5):
    • k = 5,初始时 arr1 = [11, 12, 13, 14] 和 arr2 = [9, 10, 15, 16, 17]。
  4. 比较两个数组的第 k // 2 = 2 个元素:
    • i = min(len(arr1), k // 2) = 2
    • j = min(len(arr2), k // 2) = 2
    • arr1[1] = 12
    • arr2[1] = 10
  5. 调用 find_kth_element(nums1, nums2[2:], 3),即:
    • arr1 = [11, 12, 13, 14]
    • arr2 = [15, 16, 17]
    • k = 3。
  6. 比较两个数组的第 k // 2 = 1 个元素:
    • i = min(len(arr1), k // 2) = 1
    • j = min(len(arr2), k // 2) = 1
    • arr1[0] = 11
    • arr2[0] = 15
  7. 调用 find_kth_element(nums1[1:], nums2, 2),即:
    • arr1 = [12, 13, 14]
    • arr2 = [15, 16, 17]
    • k = 2。
  8. 比较两个数组的第 k // 2 = 1 个元素:
    • i = min(len(arr1), k // 2) = 1
    • j = min(len(arr2), k // 2) = 1
    • arr1[0] = 12
    • arr2[0] = 15
  9. 调用 find_kth_element(nums1[1:], nums2, 1),即:
    • arr1 = [13, 14]
    • arr2 = [15, 16, 17]
    • k = 1。
  10. 由于 k == 1,直接返回两个数组的第一个元素中较小的一个,即:
  • min(arr1[0], arr2[0]) = min(13, 15) = 13

所以,合并后数组的中位数为 13。


哲学和编程思想

编程思想

  1. 分治法(Divide and Conquer):
    • 该方法通过将问题分成更小的子问题,然后递归地解决这些子问题。具体来说,二分查找的方法将两个数组的中位数问题分解为对较短数组的一部分和较长数组的一部分进行递归查找。
    • 分治法的核心在于将一个复杂问题分解成更小、更容易解决的部分,然后组合这些部分的解来解决整个问题。
  2. 递归(Recursion):
    • 递归是一种直接或间接调用自身的编程技术。这种方法通过不断地缩小问题规模,最终解决最小规模的问题来达到解决整个问题的目的。
    • 递归的核心思想在于找到基准情况(base case)和递归步骤(recursive step),基准情况是问题的最小实例,它可以直接解答,而递归步骤则是将问题缩小并继续递归求解。
  3. 二分查找(Binary Search):
  • 二分查找是一种在有序数组中查找元素的高效算法,其时间复杂度为 O(log n)。在这个方法中,二分查找用于确定数组中第 k 个元素,从而显著减少了查找的时间复杂度。
  • 二分查找的核心思想在于每次比较时将搜索范围缩小一半,从而快速定位目标元素。

哲学思想

  1. 化繁为简(Reductionism):
    • 这种思想主张将复杂的问题分解为更小、更简单的问题来解决。在这个算法中,通过将两个数组的中位数问题分解为查找第 k 个元素的问题,我们可以更容易地处理问题。
    • 化繁为简的哲学在于相信任何复杂的问题都可以通过适当的分解和简化来解决。
  2. 递归的自相似性(Self-Similarity in Recursion):
    • 递归过程中的每一层调用看起来都与其他层次类似,只是处理的规模不同。这种自相似性是许多自然界和数学现象的共同特征。
    • 递归的自相似性在编程中的应用体现了问题的结构和解决方案之间的一致性。
  3. 抽象(Abstraction):
  • 抽象是一种只关注问题的高层次视角,而忽略具体实现细节的方法。在这个算法中,通过定义 find_kth_element 函数,我们把查找第 k 个元素的具体实现细节封装起来,使得主函数的逻辑更加清晰。
  • 抽象的核心在于从复杂的现实中提取出关键的本质部分,从而简化问题的解决过程。

示例的哲学与编程思想

通过运用上述思想,能够高效地解决合并两个有序数组并找到中位数的问题:

  1. 分治法使我们能够递归地缩小问题规模,避免了直接合并两个数组的高昂时间复杂度。
  2. 递归允许我们自然地处理分治法分解出的子问题。
  3. 二分查找提供了一种高效的方式来确定数组的中位数位置。
  4. 化繁为简的哲学思想帮助我们将复杂问题分解,使其更易于解决。
  5. 递归的自相似性和抽象使得代码结构清晰,逻辑简洁。

通过结合这些编程和哲学思想,不仅能够高效地解决问题,还能够提高代码的可读性和可维护性。


举一反三

理解这些编程和哲学思想后,您可以应用这些思想解决其他复杂问题。以下是一些技巧和示例代码,帮助您举一反三:

技巧1:化繁为简

问题:给定一个整数数组,找到数组中的第 k 小的元素(不包括重复元素)。

技巧:可以利用分治法和递归来解决这个问题。

def findKthSmallest(arr, k):
    def quickselect(left, right, k_smallest):
        if left == right:
            return arr[left]

        pivot_index = partition(left, right)

        if k_smallest == pivot_index:
            return arr[k_smallest]
        elif k_smallest < pivot_index:
            return quickselect(left, pivot_index - 1, k_smallest)
        else:
            return quickselect(pivot_index + 1, right, k_smallest)

    def partition(left, right):
        pivot = arr[right]
        store_index = left
        for i in range(left, right):
            if arr[i] < pivot:
                arr[i], arr[store_index] = arr[store_index], arr[i]
                store_index += 1
        arr[store_index], arr[right] = arr[right], arr[store_index]
        return store_index

    unique_arr = list(set(arr))
    return quickselect(0, len(unique_arr) - 1, k - 1)

# 示例使用
arr = [3, 2, 1, 5, 6, 4, 3, 2]
k = 2
print(findKthSmallest(arr, k))  # 输出 3

技巧2:递归的自相似性

问题:计算斐波那契数列的第 n 个数。

技巧:递归的自相似性可以自然地解决这种问题。

def fibonacci(n):
    if n <= 1:
        return n
    else:
        return fibonacci(n - 1) + fibonacci(n - 2)

# 示例使用
n = 10
print(fibonacci(n))  # 输出 55

技巧3:二分查找

问题:在一个旋转排序数组中找到一个目标值。

技巧:二分查找可以高效地解决这个问题。

def search_rotated_array(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid
        if nums[left] <= nums[mid]:
            if nums[left] <= target < nums[mid]:
                right = mid - 1
            else:
                left = mid + 1
        else:
            if nums[mid] < target <= nums[right]:
                left = mid + 1
            else:
                right = mid - 1
    return -1

# 示例使用
nums = [4, 5, 6, 7, 0, 1, 2]
target = 0
print(search_rotated_array(nums, target))  # 输出 4

技巧4:抽象

问题:计算字符串的所有可能子集。

技巧:抽象出递归的核心部分,使得代码更清晰。

def subsets(s):
    def backtrack(start, path):
        result.append(path[:])
        for i in range(start, len(s)):
            path.append(s[i])
            backtrack(i + 1, path)
            path.pop()

    result = []
    backtrack(0, [])
    return result

# 示例使用
s = "abc"
print(subsets(s))  # 输出 [[''], ['a'], ['a', 'b'], ['a', 'b', 'c'], ['a', 'c'], ['b'], ['b', 'c'], ['c']]

技巧5:分治法和递归结合(归并排序)

问题:对一个整数数组进行排序。

技巧:利用分治法和递归实现归并排序。

def merge_sort(arr):
    if len(arr) <= 1:
        return arr

    mid = len(arr) // 2
    left_half = merge_sort(arr[:mid])
    right_half = merge_sort(arr[mid:])

    return merge(left_half, right_half)

def merge(left, right):
    sorted_array = []
    while left and right:
        if left[0] < right[0]:
            sorted_array.append(left.pop(0))
        else:
            sorted_array.append(right.pop(0))

    sorted_array.extend(left or right)
    return sorted_array

# 示例使用
arr = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print(merge_sort(arr))  # 输出 [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]

总结

通过理解这些技巧,可以在解决各种问题时应用相应的编程和哲学思想:

  • 化繁为简:将复杂问题分解为简单子问题。
  • 递归的自相似性:利用递归解决具有自相似性的复杂问题。
  • 二分查找:在有序或部分有序的数据结构中快速查找元素。
  • 抽象:将复杂的逻辑封装在函数内,使代码更简洁清晰。

分治法和递归结合:通过分治法和递归解决需要多步骤处理的问题。


感谢阅读,关注我每日一题提升自己。

  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

用哲学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值