Python 这两个O(n^2)算法之间的实际性能差异是否来自缓存/内存访问?

Python 这两个O(n^2)算法之间的实际性能差异是否来自缓存/内存访问?,python,algorithm,caching,memory,time-complexity,Python,Algorithm,Caching,Memory,Time Complexity,我编写了两种方法来对数字列表进行排序,它们具有相同的时间复杂度:O(n^2),但实际运行时间相差3倍(第二种方法使用的时间是第一种方法的3倍) 我猜差异来自内存层次结构(寄存器/缓存/内存获取的计数非常不同),对吗 具体来说:第一种方法是将一个列表元素与一个变量进行比较并在它们之间赋值,第二种方法是比较两个列表元素并在它们之间赋值。我猜这意味着第二种方法比第一种方法有更多的缓存/内存获取。对吧? 当列表有10000个元素时,循环计数和运行时间如下: # TakeSmallestRecursive

我编写了两种方法来对数字列表进行排序,它们具有相同的时间复杂度:O(n^2),但实际运行时间相差3倍(第二种方法使用的时间是第一种方法的3倍)

我猜差异来自内存层次结构(寄存器/缓存/内存获取的计数非常不同),对吗

具体来说:第一种方法是将一个列表元素与一个变量进行比较并在它们之间赋值,第二种方法是比较两个列表元素并在它们之间赋值。我猜这意味着第二种方法比第一种方法有更多的缓存/内存获取。对吧?

当列表有10000个元素时,循环计数和运行时间如下:

# TakeSmallestRecursivelyToSort   Loop Count: 50004999
# TakeSmallestRecursivelyToSort   Time: 7861.999988555908       ms
# CompareOneThenMoveUntilSort     Loop Count: 49995000
# CompareOneThenMoveUntilSort     Time: 17115.999937057495      ms
代码如下:

# first method
def TakeSmallestRecursivelyToSort(input_list: list) -> list:
    """In-place sorting, find smallest element and swap."""
    count = 0
    for i in range(len(input_list)):
        #s_index = Find_smallest(input_list[i:]) # s_index is relative to i
        if len(input_list[i:]) == 0:
            raise ValueError
        if len(input_list[i:]) == 1:
            break
        index = 0
        smallest = input_list[i:][0]
        for e_index, j in enumerate(input_list[i:]):
            count += 1
            if j < smallest:
                index = e_index
                smallest = j
        s_index = index
        input_list[i], input_list[s_index + i] = input_list[s_index + i], input_list[i]
    print('TakeSmallestRecursivelyToSort Count', count)
    return input_list


# second method
def CompareOneThenMoveUntilSort(input_list: list) -> list:
    count = 0
    for i in range(len(input_list)):
        for j in range(len(input_list) - i - 1):
            count += 1
            if input_list[j] > input_list[j+1]:
                input_list[j], input_list[j+1] = input_list[j+1], input_list[j]
    print('CompareOneThenMoveUntilSort Count', count)
    return input_list
#第一种方法
def TAKESMALLESTRUCERSIVELYTORT(输入列表:列表)->列表:
“”“就地排序,查找最小元素并交换。”“”
计数=0
对于范围内的i(len(输入列表)):
#s_索引=查找_最小值(输入_列表[i:])s_索引相对于i
如果len(输入列表[i:])==0:
升值误差
如果len(输入列表[i:])==1:
打破
索引=0
最小值=输入_列表[i:[0]
对于e_索引,枚举中的j(输入_列表[i:]):
计数+=1
如果j<最小值:
索引=e_索引
最小=j
s_指数=指数
输入列表[i],输入列表[s\U索引+i]=输入列表[s\U索引+i],输入列表[i]
打印('takesmallesturecurivelytosort Count',Count)
返回输入列表
#第二种方法
def CompareOneThenMoveUntilSort(输入\列表:列表)->列表:
计数=0
对于范围内的i(len(输入列表)):
对于范围内的j(len(输入列表)-i-1):
计数+=1
如果输入列表[j]>输入列表[j+1]:
输入列表[j],输入列表[j+1]=输入列表[j+1],输入列表[j]
打印('CompareoThenMoveUntillSort Count',Count)
返回输入列表

您的第一个算法可能进行O(N^2)比较,但它只进行O(N)交换。正是这些掉期交易花费了最多的时间。如果从第二个算法中删除交换,您将看到它所需的时间明显减少:

def CompareOneThenMoveUntilSortNoSwap(input_list: list) -> list:
    for i in range(len(input_list)):
        for j in range(len(input_list) - i - 1):
            if input_list[j] > input_list[j+1]:
                pass
仅仅因为两个算法的渐近顺序相同,并不意味着它们的速度会一样快。当比较同一order类中算法的实现时,这些恒定成本仍然很重要。因此,虽然这两个实现将显示与您绘制排序元素数量所用时间相同的指数曲线,但
compareonethenmovuntilsort
实现将在所用时间图表的上方绘制一条线

请注意,通过在
takesmallesturecurivelytosort
实现中添加4个额外的O(N)循环,您增加了每个
N
循环的固定成本。每个
inputlist[i:://code>切片创建一个新的列表对象,将索引
i
中的所有引用复制到新列表中。它的速度可能会更快:

def TakeSmallestRecursivelyToSortImproved(input_list: list) -> list:
    """In-place sorting, find smallest element and swap."""
    l = len(input_list)
    for i in range(l - 1):
        index = i
        smallest = input_list[i]
        for j, value in enumerate(input_list[i + 1:], i + 1):
            if value < smallest:
                smallest, index = value, j
        input_list[i], input_list[index] = input_list[index], input_list[i]
    return input_list
def takesmallestrecursivelytortimproved(输入列表:列表)->列表:
“”“就地排序,查找最小元素并交换。”“”
l=len(输入列表)
对于范围(l-1)内的i:
指数=i
最小=输入_列表[i]
对于j,枚举中的值(输入_列表[i+1:],i+1):
如果值<最小值:
最小,索引=值,j
输入列表[i],输入列表[index]=输入列表[index],输入列表[i]
返回输入列表

这一个大约需要3秒钟。

inputlist[i:][/code>创建列表的副本,需要O(N)个时间。
input\u list[i:[0]
是一种非常昂贵的拼写方法
input\u list[i]
。可能会重复常数因子(以及常数偏移量)总是包含在大O中。它所讨论的是给定算法的大输入的渐近复杂性增长。没有对算法/实现之间的实际运行时间进行任何比较。Big-O与实际运行时间没有任何关系。除了效率低下的实现之外,第一个示例包含的分支数量是原来的3倍,这通常会降低执行时间。但要点是:永远不要用Big-O来总结实际运行时的任何情况。第一种方法使用input_list[i:][0],但第二种方法使用的时间是第一种方法的3倍……所以你的评论并不能真正解释差异。@Yoyo:你的问题不清楚。我知道你做错了什么,我会更新的。谢谢你的解释。我认为在第二种方法中,更多的列表[索引]操作意味着更多的缓存未命中,这会导致更多的内存访问时间,但在这里,您的推断更有意义,我在第二种方法中使用了太多的交换。即使它们都在缓存中,第二种方法仍然会占用更多的时间。@Yoyo:在用Python编码时,你真的不需要担心CPU缓存;CPU与Python代码的距离太远;中间有一个完整的字节码解释器循环。
def TakeSmallestRecursivelyToSortImproved(input_list: list) -> list:
    """In-place sorting, find smallest element and swap."""
    l = len(input_list)
    for i in range(l - 1):
        index = i
        smallest = input_list[i]
        for j, value in enumerate(input_list[i + 1:], i + 1):
            if value < smallest:
                smallest, index = value, j
        input_list[i], input_list[index] = input_list[index], input_list[i]
    return input_list