小孩也能看懂的算法笔记,排序day3-图解快速排序内存流转

算法的时间是怎么得出的?

计算机执行所有算法时,都在重复:取数据+运算+算完存回去

算法的时间复杂度就是计算机读取数据+运算+算完再存回去的次数。

还记得为什么要先了解数据结构吗?小孩能看懂的算法笔记首篇

那数据在存储中是怎么变化的?数据结构的栈和堆在内存中长啥样呢?
在这里插入图片描述

存储层级:内存,缓存,寄存器

在内存够用时,要处理的全部数据存在内存RAM(内部存储),数据流转从RAM→缓存→寄存器。内存,缓存,寄存器存取速度不同,内存最慢,寄存器最快,所以倾向尽量把数据让速度
更快的寄存器和缓存,少访问低速的内存。

又由于寄存器最快,但容量极小(几百字节),而内存容量最大(GB),读取数据时,是从内存按需+少量多次地拿局部数据

内存不够用的情况先不赘述。
下面展示原地快速排序(内存够用)的流转过程。

原地快速排序(In-place QuickSort)

排序过程中没有辅助数组,就叫原地快速排序。

原地快速排序步骤:
选基准元素,
把数组分区成一组大的一组小的,
分别递归排好了接起来。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

栈和堆在内存中什么样?

操作系统和程序运行时将内存逻辑上划分为不同区域,包括“栈(Stack)”和“堆(Heap)”和其他。

  • 栈(Stack):是一块连续的内存区域,专门用于存储函数调用时的局部变量、参数、返回地址等。由编译器自动管理,规则是自动清理,后进先出(LIFO)。

    • 分配/释放:函数调用时,其局部变量(如快速排序中的指针 ij)被“压入”栈顶,图里把栈顶放下面了,注意看图步骤22,最底下是栈帧1,往上依次是栈帧2,3等。函数返回时,这些变量被自动“弹出”释放。速度极快。
    • Winndows和Linux地址都从高地址FEF1向低地址FEE1增长。
    • 大小:较小且固定(例如几MB),由操作系统或编译器预设。如果递归太深(如快速排序最坏情况),可能导致栈溢出(Stack Overflow)
  • 堆(Heap):是另一块内存区域,用于动态内存分配(如 mallocnew)。由程序员手动管理(或由垃圾回收器管理),更灵活但也更复杂:

    • 分配/释放:分配时在堆中找到一块足够大的空闲内存;释放时需要显式调用 freedelete
    • 地址:空间不连续,通过指针链接。
    • 大小:远大于栈,通常只受系统可用物理内存和虚拟内存限制。

Python 代码示例

以下是一个完整、可运行的 Python 代码示例,演示了原地快速排序(In-place Quicksort)算法。代码包含了详细的注释,并指出了关键变量(如指针 i, j)在内存中的可能位置,以呼应前文对存储层级的讨论。

def quicksort_inplace(arr, low, high):
    """
    原地快速排序的主递归函数。
    参数:
        arr: 待排序的列表(Python list),在内存中连续存储。
        low: 当前子数组的起始索引(左边界)。
        high: 当前子数组的结束索引(右边界)。
    """
    if low < high:
        # 分区操作,返回基准元素的最终位置
        pivot_index = partition(arr, low, high)
        
        # 递归排序左半部分 (low 到 pivot_index-1)
        quicksort_inplace(arr, low, pivot_index - 1)
        # 递归排序右半部分 (pivot_index+1 到 high)
        quicksort_inplace(arr, pivot_index + 1, high)


def partition(arr, low, high):
    """
    分区函数:选取基准元素,并将数组重新排列,使得基准左侧元素都小于等于基准,右侧元素都大于基准。
    返回基准元素的最终索引位置。
    
    注意:指针变量 `i` 和 `j` 是函数的局部变量。
         在理想情况下,编译器/解释器会尝试将它们存储在快速的 CPU 寄存器中。
         如果寄存器不足或需要取地址,它们则会被分配在函数的栈帧(RAM 中的栈区域)中。
    """
    # 选择最右侧的元素作为基准 (pivot)
    pivot = arr[high]
    
    # i 指针:指向“小于等于基准”区域的最后一个元素的下一个位置。
    # 初始时,这个区域为空,所以 i 指向 low。
    i = low - 1
    
    # j 指针:遍历从 low 到 high-1 的所有元素。
    for j in range(low, high):
        # 如果当前元素 arr[j] 小于等于基准值
        if arr[j] <= pivot:
            # 扩展“小于等于基准”的区域:i 右移一位
            i += 1
            # 将 arr[j] 交换到该区域内
            arr[i], arr[j] = arr[j], arr[i]
    
    # 循环结束后,所有 <= pivot 的元素都在 arr[low..i] 中
    # 所有 > pivot 的元素都在 arr[i+1..high-1] 中
    # 将基准元素 pivot (arr[high]) 交换到正确位置 (i+1)
    arr[i + 1], arr[high] = arr[high], arr[i + 1]
    
    # 返回基准元素的最终索引
    return i + 1


def print_array_with_memory_view(arr, description=""):
    """
    辅助函数:打印数组及其内存地址视图(模拟)。
    用于直观展示数组元素在内存中的连续存储。
    """
    print(f"\n{description}")
    print("索引: ", end="")
    for idx in range(len(arr)):
        print(f"{idx:>4}", end=" ")
    print("\n值  : ", end="")
    for val in arr:
        print(f"{val:>4}", end=" ")
    print()
   
   


# ========== 主程序:演示排序过程 ==========
if __name__ == "__main__":
    # 1. 初始化一个待排序的数组
    original_array = [38, 27, 43, 9, 15, 82, 52]
    print("=== 原地快速排序演示 ===")
    print_array_with_memory_view(original_array, "排序前数组:")
    
    # 2. 创建数组副本用于排序(保持原数组不变以便对比)
    array_to_sort = original_array.copy()
    
    # 3. 调用原地快速排序函数
    quicksort_inplace(array_to_sort, 0, len(array_to_sort) - 1)
    
    # 4. 打印排序结果
    print_array_with_memory_view(array_to_sort, "排序后数组:")
    
    # 5. 验证排序结果
    print(f"\n验证: 数组是否已排序? {array_to_sort == sorted(original_array)}")
    
    # 6. 解释与存储层级的关联
    print("\n" + "="*50)
    print("【代码与存储层级的关联说明】")
    print("1. 数组 `arr`: 存储在内存(RAM)的连续区域,排序过程直接在此区域交换元素,符合'原地'定义。")
    print("2. 指针 `i`, `j`: 函数局部变量。")
    print("   - 理想情况: 由编译器分配到 CPU 寄存器,访问延迟约 0.5 纳秒(最快)。")
    print("   - 寄存器不足时: 存储在函数栈帧(RAM 中的栈区域),访问需经过缓存或直接读内存。")
    print("3. 递归调用 `quicksort_inplace`: 每次调用产生一个栈帧,存储参数 low/high 和返回地址。")
    print("   递归深度 O(log n) 决定了栈空间的使用量,这也是算法空间复杂度为 O(log n) 的原因。")
    print("4. 缓存友好性: 由于数组内存连续,分区操作顺序访问元素,有利于缓存行(Cache Line)加载,")
    print("   提高缓存命中率,从而提升速度。")

运行结果示例:

=== 原地快速排序演示 ===

排序前数组:
索引:    0    1    2    3    4    5    6
值  :   38   27   43    9   15   82   52
地址: 0x1000 0x1004 0x1008 0x100C 0x1010 0x1014 0x1018

排序后数组:
索引:    0    1    2    3    4    5    6
值  :    9   15   27   38   43   52   82
地址: 0x1000 0x1004 0x1008 0x100C 0x1010 0x1014 0x1018

验证: 数组是否已排序? True

==================================================
【代码与存储层级的关联说明】
1. 数组 `arr`: 存储在内存(RAM)的连续区域,排序过程直接在此区域交换元素,符合'原地'定义。因此每个基准元素对比过程中,每层空间复杂度一直是O(1),最后递归了O(logn)层,总空间复杂度为O(1)*O(logn)即O(logn)。
2. 指针 `i`, `j`: 函数局部变量。
   - 理想情况: 由编译器分配到 CPU 寄存器,访问延迟约 0.5 纳秒(最快)。
   - 寄存器不足时: 存储在函数栈帧(RAM 中的栈区域),访问需经过缓存或直接读内存。
3. 递归调用 `quicksort_inplace`: 每次调用产生一个栈帧,存储参数 low/high 和返回地址。
   递归深度 O(log n) 决定了栈空间的使用量,这也是算法空间复杂度为 O(log n) 的原因。
4. 缓存友好性: 由于数组内存连续,分区操作顺序访问元素,有利于缓存行(Cache Line)加载,
   提高缓存命中率,从而提升速度。

如何运行:

  1. 将上述代码复制到一个 .py 文件(例如 quicksort_demo.py)。
  2. 在终端或命令行中执行:python quicksort_demo.py
  3. 你将看到排序前后的数组、模拟的内存地址视图以及算法与存储层级的关联分析。

现在指针存储(寄存器/栈)栈帧与递归内存连续性与缓存这些概念有没有变清晰呢?

这是@fivebliss

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值