跳转至

Python算法基础

算法概述

算法是解决特定问题的一系列步骤。好的算法应该具备以下特征: - 输入:有零个或多个输入 - 输出:至少有一个输出 - 有穷性:在有限步骤内完成 - 确定性:每个步骤都是明确的 - 可行性:每个步骤都是可执行的

1 时间复杂度

时间复杂度分析统计的不是算法运行时间,而是算法运行时间随着数据量变大时的增长趋势,“时间增长趋势”这个概念比较抽象,通过一个例子来加以理解。假设输入数据大小为 𝑛 ,给定三个算法 A、B 和 C :

# 算法 A 的时间复杂度:常数阶
def algorithm_A(n: int):
    print(0)
# 算法 B 的时间复杂度:线性阶
def algorithm_B(n: int):
    for _ in range(n):
        print(0)
# 算法 C 的时间复杂度:常数阶
def algorithm_C(n: int):
    for _ in range(1000000):
        print(0)
- 算法 A 只有 1 个打印操作,算法运行时间不随着 𝑛 增大而增长。我们称此算法的时间复杂度为“常数阶”。 - 算法 B 中的打印操作需要循环 𝑛 次,算法运行时间随着 𝑛 增大呈线性增长。此算法的时间复杂度被称为“线性阶”。 - 算法 C 中的打印操作需要循环 1000000 次,虽然运行时间很长,但它与输入数据大小 𝑛 无关。因此 C 的时间复杂度和 A 相同,仍为“常数阶”。

1.1 常数阶 𝑂(1)

常数阶的操作数量与输入数据大小 𝑛 无关,即不随着 𝑛 的变化而变化。在以下函数中,尽管操作数量 size 可能很大,但由于其与输入数据大小 𝑛 无关,因此时间复杂度仍为 𝑂(1) :

def constant(n):
    """常数阶"""
    count = 0
    size = 100000
    for _ in range(size):
        count += 1
    return count

 #实例:
 print(constant(3)) #100000
 print(constant(50)) #100000
 #说明常数阶的操作数量与输入数据大小n无关

1.2 线性阶 𝑂(𝑛)

线性阶的操作数量相对于输入数据大小 𝑛 以线性级别增长。线性阶通常出现在单层循环中:

def linear(n):
    """线性阶"""
    count = 0
    for _ in range(n):
        count += 1
    return count
 #实例:
print(linear(3)) #3
print(linear(50)) #50
 #说明常数阶的操作数量与输入大小n有关   

1.3 平方阶 𝑂(𝑛^2)

平方阶的操作数量相对于输入数据大小 𝑛 以平方级别增长。平方阶通常出现在嵌套循环中,外层循环和内层循环的时间复杂度都为 𝑂(𝑛) ,因此总体的时间复杂度为 𝑂(𝑛^2) :

def quadratic(n):
    """平方阶"""
    count = 0
    # 循环次数与数据大小 n 成平方关系
    for i in range(n):
        for j in range(n):
            count += 1
    return count


  #实例:
print(quadratic(4))   #16
print(quadratic(15)). #25

1.4 指数阶 𝑂(2^𝑛)

生物学的“细胞分裂”是指数阶增长的典型例子:初始状态为 1 个细胞,分裂一轮后变为 2 个,分裂两轮后变为 4 个,以此类推,分裂 𝑛 轮后有 2^𝑛 个细胞。 以下代码模拟了细胞分裂的过程,时间复杂度为 𝑂(2^𝑛) 。请注意,输入 𝑛 表示分裂轮数,返回值 count 表示总分裂次数。

def exponential(n: int) -> int:
    """指数阶(循环实现)"""
    count = 0
    base = 1
    # 细胞每轮一分为二,形成数列 1, 2, 4, 8, ..., 2^(n-1)
    for _ in range(n):
        for _ in range(base):
            count += 1
        base *= 2
    # count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1
    return count

#实例:
print(exponential(4))  #15
print(exponential(5))  #31

2 排序算法

2.1 冒泡排序

冒泡排序(Bubble Sort)算法,它的核心思想是重复遍历待排序的数列,一次比较两个元素,如果顺序错误就把它们交换过来,直到整个数列有序。 冒泡排序的基本步骤是: 1. 比较相邻元素:从第一个元素开始,依次比较相邻的两个元素。 2. 交换元素:如果顺序错误(例如升序排列时前一个元素大于后一个元素),则交换它们。 3. 重复步骤 1-2:对每一对相邻元素重复上述过程,直到最后一对。 4. 优化:每一轮比较后,最大的元素会 "浮" 到数列末尾,因此下一轮可以少比较一个元素。 冒泡排序算法如下:

def bubble_sort(arr):
    n = len(arr)  # 获取数组长度
    for i in range(n):  # 外层循环:控制排序轮数,共需要n-1轮
        for j in range(n-1-i):  # 内层循环:每轮比较的次数逐渐减少,也可以range(n-1),不过计算量大一些
            if arr[j] > arr[j+1]:  # 比较相邻元素
                arr[j], arr[j+1] = arr[j+1], arr[j]  # 交换元素
    return arr  # 返回排序后的数组

arr = [2,3,6,5,3,2,1]
print(maopao(arr))    
#结果
#[1, 2, 2, 3, 3, 5, 6]

2.2 选择排序

选择排序(Selection Sort)算法,它的核心思想是每一轮从未排序部分中选出最小元素,与未排序部分的第一个元素交换位置,直到整个数组有序。 选择排序的基本步骤是: 1. 划分数组:将数组分为已排序部分(初始为空)和未排序部分。 2. 寻找最小值:在未排序部分中找到最小的元素。 3. 交换元素:将找到的最小值与未排序部分的第一个元素交换。 4. 扩展已排序部分:已排序部分增加一个元素,未排序部分减少一个元素。 5. 重复步骤 2-4:直到未排序部分为空。

def select_sort(arr):
    n = len(arr)  # 获取数组长度
    for i in range(n):  # 外层循环:控制当前未排序部分的起始位置
        min_index = i  # 假设当前位置的元素是最小值
        for j in range(i+1, n):  # 内层循环:在未排序部分中寻找最小值
            if arr[j] < arr[min_index]:  # 如果找到更小的元素
                min_index = j  # 更新最小值的索引
        arr[min_index], arr[i] = arr[i], arr[min_index]  # 交换最小值和当前位置的元素
    return arr  # 返回排序后的数组

# 测试代码
arr = [2, 3, 6, 5, 3, 2, 1]
print(select_sort(arr)) 
# 输出:[1, 2, 2, 3, 3, 5, 6]

2.3 拆入排序

插入排序(Insertion Sort)核心思想是将未排序数据逐个插入到已排序序列的适当位置。它类似于玩扑克牌时整理手牌的方式 —— 每次拿到一张新牌,就将其插入到已有牌的正确位置。 算法步骤: 1. 划分数组:将数组分为已排序部分(初始为第一个元素)和未排序部分。 2. 选择元素:从未排序部分取出第一个元素,称为当前元素。 3. 寻找插入位置:在已排序部分中,从后向前比较,找到第一个小于或等于当前元素的位置。 4. 插入元素:将当前元素插入到该位置,已排序部分长度加 1。 5. 重复步骤 2-4:直到未排序部分为空。

def insertion_sort(arr):
    for i in range(1, len(arr)):  # 从第2个元素开始遍历(索引1)
        key = arr[i]  # 当前要插入的元素
        j = i - 1     # 已排序部分的最后一个元素索引

        # 将比key大的元素后移
        while j >= 0 and arr[j] > key:
            arr[j + 1] = arr[j]  # 元素后移
            j -= 1

        # 插入key
        arr[j + 1] = key

    return arr

# 测试示例
arr = [5, 2, 4, 6, 1, 3]
print(insertion_sort(arr))  # 输出: [1, 2, 3, 4, 5, 6]

3 查找算法

3.1 线性查找

线性查找(Linear Search),也称为顺序查找,是一种最简单的搜索算法。它的核心思想是逐个遍历数据结构中的元素,直到找到目标值或遍历完所有元素。 算法步骤: 1. 从第一个元素开始:从数据结构(如数组、列表)的起始位置开始遍历。 2. 逐个比较:将当前元素与目标值进行比较。 3. 返回结果: - 如果找到匹配的元素,返回其位置(索引)。 - 如果遍历完所有元素仍未找到,返回特定值(如 -1 或 None)表示未找到。 代码如下所示:

def linear_search(arr, target):
    """
    线性查找算法实现

    参数:
    arr (list): 待查找的列表
    target: 要查找的目标值

    返回:
    int: 目标值在列表中的索引,如果未找到返回 -1
    """
    for i in range(len(arr)):
        if arr[i] == target:
            return i  # 找到目标值,返回索引
    return -1  # 未找到目标值

# 测试示例
arr = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
target = 9
result = linear_search(arr, target)

if result != -1:
    print(f"目标值 {target} 在索引 {result} 处找到")
else:
    print(f"目标值 {target} 未找到")

 #结果为:目标值 9 在索引 5 处找到

3.2 二分查找

二分查找是一种高效的搜索算法,适用于已排序的数据结构(如有序数组)。其核心思想是每次将搜索范围缩小一半,直到找到目标值或确定目标值不存在。 算法步骤: 1. 确定中间点:计算当前搜索范围的中间位置。 2. 比较中间值:将中间位置的元素与目标值比较: - 若相等,则找到目标值。 - 若中间值大于目标值,缩小搜索范围到左半部分。 - 若中间值小于目标值,缩小搜索范围到右半部分。 3. 重复步骤 a-b:直到找到目标值或搜索范围为空。

def binary_search(arr, target):
    left, right = 0, len(arr) - 1

    while left <= right:
        mid = (left + right) // 2  # 计算中间索引

        if arr[mid] == target:
            return mid  # 找到目标值
        elif arr[mid] < target:
            left = mid + 1  # 搜索右半部分
        else:
            right = mid - 1  # 搜索左半部分

    return -1  # 未找到目标值

# 测试示例
arr = [1, 3, 4, 5, 9, 12, 15]
target = 9
print(binary_search(arr, target))  # 输出: 4(目标值的索引位置)

4 递归算法

递归是一种 “自己调用自己” 的编程技巧,需注意 终止条件 和 递推关系。

4.1 阶乘

递归解决阶乘的关键在于递推关系和终止条件: 1. 终止条件:当 n = 0 时,直接返回 1,不再继续递归。 2. 递推关系:n! = n × (n-1)!,即把 n! 的计算分解为 n 乘以 (n-1)! 的子问题。 代码实例如下所示:

def factorial(n):
    if n==0:
        return 1
    return n*factorial(n-1)

print(factorial(5)) #输出:120
print(factorial(10)) #输出:3628800

4.2 斐波那契数列

def fibonacci(n):
    if n == 0:  # 终止条件1:n=0时返回0
        return 0
    elif n == 1:  # 终止条件2:n=1时返回1
        return 1
    else:  # 递推关系:f(n) = f(n-1) + f(n-2)
        return fibonacci(n-1) + fibonacci(n-2)

#斐波那契数列的前10项为[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# 测试
print(fibonacci(5))  # 输出: 5(序列:0,1,1,2,3,5)
print(fibonacci(10)) # 输出: 55

5 字符串处理算法

5.1 反转字符串

# 切片法(一行实现)
reversed_str = s[::-1]
#实例:
s = 'hello'
print(s[::-1])
#输出:olleh


# 双指针法(原地反转,适合列表)
def reverse_string(s_list):
    left, right = 0, len(s_list)-1
    while left < right:
        s_list[left], s_list[right] = s_list[right], s_list[left]
        left += 1
        right -= 1
    return s_list
 #实例:
string_list = ['hello','world','apple','peach','tap4fun']
print(reverse_string(string_list))
#输出:['tap4fun', 'peach', 'apple', 'world', 'hello']

5.2 判断回文

思想:字符串正序和逆序是否相等,代码如下所示

def is_palindrome(str):
    if str == str[::-1]:
        return True
    return False

#实例:
str_1 = 'string'
str_2 = 'holloh'
print(is_palindrome(str_1)) #输出: Fasle
print(is_palindrome(str_2)) #输出:True

6 简单的算法思想

6.1 模拟算法

模拟算法的思想:按题意模拟过程,如 “猜数字游戏”“银行排队” 等。如下实例,模拟投掷n次骰子后,点数统计情况:

import random
count = [0]*7 #索引0不用,1-6对应点数
def count_sum(n):
    for _ in range(n):
        point = random.randint(1,6)
        count[point] += 1
    return count[1:7]
print(count_sum(180)) #输出:[25, 28, 33, 32, 31, 31]

6.2 贪心算法

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取当前状态下最优(局部最优)的选择,从而希望最终导致全局最优解的算法策略。它不考虑整体情况,仅关注当前步骤的最优解,因此适用于具有贪心选择性质和最优子结构性质的问题。 贪心算法的核心思想如下: 1. 贪心选择:每一步都做出当前看起来最优的选择,不考虑该选择对后续步骤的影响。 2. 局部最优 → 全局最优:通过每一步的局部最优选择,最终达到全局最优解。 经典案例:找零问题 问题描述:给定面额为 1、5、10、25 的硬币,如何用最少的硬币凑出指定金额? 贪心策略:每次选择面值最大的硬币,直到凑出目标金额。 代码如下:

def coin_change(amount):
    coins = [25, 10, 5, 1]  # 硬币面值(降序排列)
    count = [0]*len(coins)  # 各面值硬币数量

    for coin in coins:
        if amount >= coin:
            num = amount // coin  # 当前面值的硬币数量
            count[coins.index(coin)] = num
            amount -= num * coin  # 剩余金额
            if amount == 0:
                break

    return count

# 测试
print(coin_change(63))  
# 输出 [2, 1, 0, 3]
#即(25×2 + 10×1 + 5×0 + 1×3)

7 算法优化技巧

7.1 空间时间权衡

# 使用额外空间换取时间效率
def two_sum(nums, target):
    seen = {}  # 空间换时间
    for i, num in enumerate(nums):
        complement = target - num
        if complement in seen:
            return [seen[complement], i]
        seen[num] = i
    return []

7.2 循环优化

# 减少循环中的计算
def optimize_loop():
    n = 1000
    # 优化前
    for i in range(n):
        result = i * len(range(n))  # 每次都计算len

    # 优化后
    length = len(range(n))  # 提前计算
    for i in range(n):
        result = i * length

7.3 使用适当的数据结构

# 使用集合进行快速查找
def find_duplicate(nums):
    seen = set()
    for num in nums:
        if num in seen:  # O(1)查找
            return num
        seen.add(num)
    return None

练习题

  1. 实现一个函数,判断一个字符串是否是回文
  2. 编写一个函数,找出数组中第k大的元素
  3. 实现一个简单的计算器,支持基本的四则运算和括号

进阶主题

  • 动态规划
  • 贪心算法
  • 回溯算法
  • 图算法
  • 树算法

小结

  • 理解算法的基本概念和特征
  • 掌握时间复杂度分析方法
  • 能够实现基本的查找和排序算法
  • 理解递归的原理和应用
  • 掌握基本的算法优化技巧