【数据结构与算法】最适合新手小白的教程——master定理和两种进阶算法(包你看懂!)

Hello大家好!额思来想去,还是给排序这块收个尾,机器学习的内容还是这周后面再写吧,大家有看不懂或者觉得有问题的地方可以直接评论区打出来呀,我看到会马上回复的~

这篇文章主要内容就是两种进阶的排序算法:归并和快速,以及master公式(计算递归时间复杂度),也是面向萌新盆友,所以写的会详细一点,希望对大家有所帮助~

好啦,咱们话不多说,直接进入正题!

1、master定理(计算递归时间复杂度)

master定理是一种特别好用的计算递归时间复杂度的方法,在满足其前提条件的情况下,我们可以根据公式直接得出其时间复杂度。(可以说是站在前人的肩膀上操作了哈哈哈)在递归算法中,问题通常被分解成若干子问题,解决这些子问题后,再将结果合并以得到原问题的解。Master定理用于分析这种递归关系,它有三个主要部分:分解、递归、合并。

下面是具体公式:

T(n)=aT\left ( \frac{n}{b} \right )+f(n)

        

其中:

  • n 是问题的规模(就是所有递归调用的范围
  • a 是每次递归调用的子问题数(就是你写了几个递归,不是指递归了几次
  • n/b 是每个子问题的规模(这里要求每个子问题的规模必须要相等,否则无法使用此公式
  • f(n) 是在分解和合并过程中花费的时间(也可以理解为除去递归这块代码外其他部分代码所消耗的时间

#在这里提一嘴,所谓规模可以理解为递归调用数的范围,比如总的范围的数组是9个数,然后可以分为三个子问题,每个问题规模是三个数,三个子问题分别内部递归,就可以使用我们的定理。此时a=3,b=3,至于f(n)还要看具体有什么其他操作。

Master定理分三种情况来给出递归关系的渐进时间复杂度:

情况一:      f\left ( n \right )=O\left ( n^{c} \right )   并且  c< \log_{b} a

在这种情况下,T(n) 的渐进时间复杂度为:T(n)=O(n^{\log_{b}a})

情况二:     f\left ( n \right )=O\left ( n^{c} \right )   并且  c=\log_{b} a

在这种情况下,T(n) 的渐进时间复杂度为:T(n)=O(n^{\log_{b}a}\log n)

情况三:    f\left ( n \right )=O\left ( n^{c} \right )  并且  c>\log_{b} a

在这种情况下,T(n) 的渐进时间复杂度为:T(n)=O(n^{c})

#上述定理的证明要通过数学公式来求证,内容很复杂,这里就暂且不展示了。大家只要会用就行,记下来就好,前人种树好乘凉嘛~

以下举一个代码实例,帮助大家理解:

def find_max(arr, left, right):
    # 如果子数组长度为1,直接返回该元素
    if left == right:
        return arr[left]
    
    # 计算子数组的中点
    mid = (left + right) // 2
    
    # 分别递归求解左半部分和右半部分的最大值
    left_max = find_max(arr, left, mid)
    right_max = find_max(arr, mid + 1, right)
    
    # 返回左半部分和右半部分的最大值中的较大值
    return max(left_max, right_max)

#这是一个简单的分治算法,我们可以以他为例子,计算一下时间复杂度

写这个公式太麻烦了,我直接给你们看看AI辅助的分析过程吧

#上述过程我们就可以很清楚的看到整个定理的使用方法,下面还会再给一个例子,这个公式还是很好用的,大家可以自己去找几个来练练手~

2、归并排序

归并排序(Merge Sort)是一种经典的分治算法(我上面提了一半),用于将一个无序数组排序为有序数组。归并排序的基本思想是将数组递归地分成两半,然后将这两半分别排序,最后将排序后的两半合并成一个有序数组,也就是我们说的:分解、递归、合并。

def merge_sort(arr):
    # 如果数组长度小于等于1,直接返回数组,因为它已经是有序的
    if len(arr) <= 1:
        return arr
    
    # 计算数组的中间索引,将数组分成两半
    mid = len(arr) // 2
    
    # 递归地对左半部分进行归并排序
    left = merge_sort(arr[:mid])
    
    # 递归地对右半部分进行归并排序
    right = merge_sort(arr[mid:])
    
    # 合并排序后的左半部分和右半部分
    return merge(left, right)

def merge(left, right):
    result = []  # 用于存储合并后的结果
    i = j = 0  # 初始化两个指针,分别指向左半部分和右半部分的起始位置
    
    # 合并两个有序数组
    while i < len(left) and j < len(right):
        # 如果左半部分的当前元素小于右半部分的当前元素
        if left[i] < right[j]:
            result.append(left[i])  # 将左半部分的当前元素添加到结果中
            i += 1  # 移动左半部分的指针到下一个位置
        else:
            result.append(right[j])  # 将右半部分的当前元素添加到结果中
            j += 1  # 移动右半部分的指针到下一个位置
    
    # 如果左半部分还有剩余元素,添加到结果中
    result.extend(left[i:])
    
    # 如果右半部分还有剩余元素,添加到结果中
    result.extend(right[j:])
    
    return result  # 返回合并后的结果

#递归的过程就是不断的通过去中间数的办法,把数组不断分开,最后成一个数都分成一个数(有点像二叉树的展开),然后再开始通过合并的函数往回带,把数组一块一块排好序,然后小块递归回溯成一个有序的大块。

下面举一个实例来帮助大家理解

初始数组:[3, 1, 4, 1, 5, 9, 2, 6]

(1)分成两半:[3, 1, 4, 1] 和 [5, 9, 2, 6]

(2)对每一半继续分解:[3, 1] 和 [4, 1],[5, 9] 和 [2, 6]

(3)继续分解直到每个子数组长度为1:[3] 和 [1],[4] 和 [1],[5] 和 [9],[2] 和 [6]

(4)合并 [3][1]: [1, 3]

(5)合并 [4][1]:[1, 4]

(6)合并 [5][9]:[5, 9]

(7)合并 [2][6]:[2, 6]

(8)合并 [1, 3][1, 4]:[1, 1, 3, 4]

(9)合并 [5, 9][2, 6]:[2, 5, 6, 9]

(10)最后合并 [1, 1, 3, 4][2, 5, 6, 9]:[1, 1, 2, 3, 4, 5, 6, 9]

#每一步会排好顺序的原因是因为在每一个数都会经过合并的函数,里面有带排序的方法,所以出来的数组都是有序的。

#合并的过程基本思想就是给左右数组两个指针,依次往下滑,谁小取谁,相等取左边,把取出来的数加入到自己设置的result数列表里面。到左右有一个指针过界(就是超过他的那部分数组的长度)的时候,取剩下没过界的一边的数组加入列表,完成合并(这里不用if语句是为了简化代码,而且指针越界的情况肯定会出现,必然会有一边有剩余一边没有的情况)

#这个算法稍有复杂,大家多花点时间,静下心来,多画画图,一定可以拿下!

下面以他为例子,使用master公式计算时间复杂度:

#因为每次都是取中点平均分,所以b=2,有左右数组两个递归调用,所以a=2,合并两个子数组的时间复杂度为线性时间,所以c=n。

下面再举一个力扣经典问题——“求小和”

题干类似于:给定一个数组 arr,我们称 arr 的小和为所有 arr[i] 的左侧所有比 arr[i] 小的元素之和的总和。请编写一个函数来计算数组 arr 的小和。

具体来说,对于一个数组中的每个元素,找到所有比它小的元素之和。这可以通过归并排序的思想来高效解决。归并排序在合并两个子数组时,记录小和,这样可以极大减少时间复杂度,提高代码效率。

def small_sum(arr):
    """
    主函数,用于计算数组的小和。
    如果数组为空或长度小于2,直接返回0。
    否则,调用递归排序并计算小和的辅助函数。
    """
    if not arr or len(arr) < 2:
        return 0
    return merge_sort_and_count(arr, 0, len(arr) - 1)

def merge_sort_and_count(arr, left, right):
    """
    递归函数,进行归并排序并计算小和。
     arr: 输入数组
     left: 左边界索引
     right: 右边界索引
     return: 小和
    """
    # 如果左右索引相等,表示只有一个元素,返回0
    if left == right:
        return 0
    
    # 计算中间索引
    mid = (left + right) // 2
    
    # 递归计算左半部分的小和,其实可以理解为继续拆分数组的动作
    left_sum = merge_sort_and_count(arr, left, mid)
    
    # 递归计算右半部分的小和,这个和上面的也是一样
    right_sum = merge_sort_and_count(arr, mid + 1, right)
    
    # 合并左右部分并计算跨越中间点的小和,这就是在合并的过程中同时计算小和的步骤了
    merge_sum = merge_and_count(arr, left, mid, right)
    
    # 返回总的小和
    return left_sum + right_sum + merge_sum

def merge_and_count(arr, left, mid, right):
    """
    合并并计算小和的函数。
     arr: 输入数组
     left: 左边界索引
     mid: 中间索引
     right: 右边界索引
     return: 小和
    """
    temp = []  # 临时数组,用于存储合并后的结果
    i, j = left, mid + 1  # 初始化指针
    small_sum = 0  # 初始化小和
    
    # 合并两个有序数组,并计算小和
    while i <= mid and j <= right:
        if arr[i] < arr[j]:
            # 如果左边元素小于右边元素,计算小和
            # arr[i] 对应的小和是 arr[i] 乘以右边剩余元素的数量
            small_sum += arr[i] * (right - j + 1)
            temp.append(arr[i])
            i += 1
        else:
            temp.append(arr[j])#单向比较,所以这里没有小和,不然会重复
            j += 1
    
    # 如果左半部分还有剩余元素,添加到临时数组
    while i <= mid:
        temp.append(arr[i])
        i += 1
    
    # 如果右半部分还有剩余元素,添加到临时数组
    while j <= right:
        temp.append(arr[j])
        j += 1
    
    # 将排序后的临时数组内容复制回原数组(原地址修改,这样就可以同步修改数组中的数的排序了)
    for i in range(len(temp)):
        arr[left + i] = temp[i]
    
    return small_sum  # 返回小和

#从函数的定义可以看出,求小和的过程就是在归并的基础上,在merge的时候多了一步计算小和的过程,并且在merge的时候,与归并的定义不同的是,左右两块数组相等的时候,取右边数组的元素加入临时数组中,因为此时你不知道左边这个数有多少右边的数比他大,这样会产生错误

#使用归并排序不会有重复计算或者漏算,因为每次数组只计算一次小和,排序好的数组就不计算小和

#求小和的时候我们只关注右侧有多少数比左侧大,对应的,当右侧数比左侧小时,不计入小和!!!只是单纯把他放到临时数组里面,也就是说计算小和的有效数只有左侧数组!!!

假设以数组[1, 3, 4, 2, 5]为例子,我们可以看一下他的计算过程:

#这一部分确实有点复杂,大家可以多结合简单实际例子多想多画图,也可以详细看看我在代码里面的注释,里面标注了一些关键的点,希望大家能轻松把他拿下!

3、快速排序

快速排序(Quick Sort)是一种高效的排序算法,使用分治法(Divide and Conquer)来排序。其核心思想是通过选择一个枢轴元素,将数组划分成三部分,中间的轴元素自为一部分,剩下两部分里,其中一部分的元素都小于枢轴,另一部分的元素都大于枢轴,然后递归地对这两部分进行排序。

下面是代码展示:

import random

def quick_sort(arr):
    """
    快速排序的主函数。
     arr: 输入数组
     return: 已排序的数组
    """
    if len(arr) <= 1:
        return arr  # 递归结束条件:如果数组为空或只有一个元素,直接返回数组
    
    # 调用辅助函数进行排序
    quick_sort_helper(arr, 0, len(arr) - 1)
    return arr

def quick_sort_helper(arr, low, high):
    """
    快速排序的辅助函数,使用递归对数组进行排序。
     arr: 输入数组
     low: 当前排序子数组的左边界
     high: 当前排序子数组的右边界
    """
    if low < high:
        # 获取划分点
        lt, gt = partition(arr, low, high)
        # 对划分点左边的子数组进行排序
        quick_sort_helper(arr, low, lt )
        # 对划分点右边的子数组进行排序
        quick_sort_helper(arr, gt + 1, high)

def partition(arr, low, high):
    """
    三向切分函数,重新排列数组,使得所有小于枢轴的元素位于枢轴左边,
    所有等于枢轴的元素位于枢轴中间,所有大于枢轴的元素位于枢轴右边。
     arr: 输入数组
     low: 当前子数组的左边界
     high: 当前子数组的右边界
     return: 小于和大于枢轴的分界点
    """
    # 随机选择一个元素作为枢轴,并将其与第一个元素交换
    pivot_index = random.randint(low, high)
    pivot = arr[pivot_index]  # 选择随机元素作为枢轴
    arr[pivot_index], arr[low] = arr[low], arr[pivot_index]  # 将枢轴移到开头
    
    lt, i, gt = low, low + 1, high  # 初始化三个指针
    # lt指向小于枢轴的部分,i用于遍历数组,gt指向大于枢轴的部分
    
    while i <= gt:
        if arr[i] < pivot:
            arr[lt], arr[i] = arr[i], arr[lt]  # 交换小于枢轴的元素到前面
            lt += 1  # 移动lt指针
            i += 1  # 移动i指针
        elif arr[i] > pivot:
            arr[i], arr[gt] = arr[gt], arr[i]  # 交换大于枢轴的元素到后面
            gt -= 1  # 移动gt指针
        else:
            i += 1  # 等于枢轴的元素保持在中间,移动i指针
     
    
    return lt, gt  # 返回小于和大于枢轴的分界点

# 示例数组
arr = [3, 6, 8, 10, 1, 2, 1, 8]

# 执行快速排序
sorted_arr = quick_sort(arr)

# 输出结果
print(f"Sorted array: {sorted_arr}")

#这里导入了random库,旨在随机选取一个元素作为枢轴(也就是我们的划分点),可以使代码的时间复杂度降到O(n*logn),这里证明要用数学里面的长期期望,从概率的角度去证,大家把结果记住就好

#算法的基本思想就是利用递归,重复把数组拆成两个部分,再定义一个函数更新拆分的那个枢轴元素,同时会在原有数组的地址上面更新数组的顺序,做到数组顺序与枢轴元素同步更新

#在循环的时候利用三个指针,i作为主要指针,循环比较的位置,lt与gt两个指针作为小于和大于的边界根据具体情况不断变化,最终将数组分成(小于x,x,大于x)这种形式

#大家可以着重看一下我的注释,里面写的挺详细的了

下面举一个实例,帮助大家理解:

#可以看到,在进行一次排序后,虽然整个数组还未完全有序,但是,大于小于等于的区间已经完全出来了,而我们的程序在排列的基础上进行递归,每次排好一个元素,保证每个元素都排列到位,最后整个数组有序(也可以看成本次递归,保证了所有大于6 的数都在右部分,所有小于6的数都在左部分,等于6的数都在中间,然后递归这种方法在左右两个部分,逐渐减小无序部分,直到类似于只剩三个数,左右中各一个数,他们有序后再反推回来,这三个数一定是小于或者大于某个数a的,这样的话,只要a的两个部分有序,a的这个小数组就有序,接着就可以递归回溯去推更大的数组直达全都有序)

#时刻注意lt与gt的值,他们和i一样是数组下标,要和数组一起看,不然非常容易混淆成第几个数,注意数组是从0开始计数的;还要注意,大于的情况,i是不增加的!!!相等的情况只增加i!!

#光看不练假把式,一定要自己去实现一遍才有可能发现问题所在,多想多练多画图!

4、一些小tip        

#篇幅受限,后面的快速排序就不给例题啦,大家可以自己去网上找找~

#这篇文章的内容确实不太好理解,大家也不要灰心,一定要坚持,总有一天会明白的!!

#一定要画图,一步一步想清楚,人脑是有极限的,一定也要自己去实现,看懂到写出来到能运行,天差地别

#最后,本次笔记就到这里啦,希望能给大家一点点帮助,有任何问题也可以在评论区打出,我看到会马上回复哒~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值