最快删除指定链表节点 :Time = O(1)

本文详细介绍了如何在O(1)时间内删除链表节点,包括O(n)时间删除的原理和O(1)时间删除的思路,通过偷梁换柱的方法避免遍历链表。同时提供了O(1)时间删除指定节点的C++代码实现,考虑了边界条件和特殊情况,确保代码的安全性。

删除链表节点的问题

题目:在 O(1)时间内删除链表节点。

给定单向链表的头指针和一个节点指针,定义一个函数在 O(1)时间内删除该节点,链表节点和函数的定义如下:

struct ListNode{
    int       m_nValue;
    ListNode* m_pNexat;
};

//为了避免删除头节点后实参指针指向内存释放的地址,
//变成野指针,第一个参数使用指针的指针或者指针的引用类型
void DeleteListNode(ListNode** head, ListNode* pToBeDeleted) { 
    
}

O(n) 时间删除与 O(1) 删除节点的原理讲解

  1. O(n) 时间删除

思路:正常情况下,我们删除一个节点,除了删除头节点外,其余应知道这个指定节点的前一个节点才能删除。

删除头节点:

head = head->next;
free(ToBeDeleted)

在这里插入图片描述

删除非头节点


//需要遍历找到 p1
p1 ->next = ToBeDeleted -> next
free(ToBeDeleted)

在这里插入图片描述

也就是说我们需要知道 指定节点 的 Front节点,我们需要遍历链表进行查找该 节点,所以造成了 O(n)
删除节点。

  1. O(1) 删除讲解

思路:由于我们知道删除一个节点需要知道它的前置节点,删除后它的前置节点与它的后置节点相连接, 我们不妨来一个偷梁换柱,将它的后置节点 赋值到自身上,之后直接删除后置节点,因为删除一个指定节点的后置节点是很容易的。

删除指定的3节点

ListNode* temp = 3->next;
3->val = temp ->val;
3->next = temp->next;
free(temp);

在这里插入图片描述

O(1) 时间删除指定节点代码实现

到了写代码的时候我们就要好好分析一下代码的全面性,保证代码安全性

特殊输入参数合法性考虑:头指针与删除节点指针是否为空,为空可以直接return
边界考虑:删除的节点为头节点或者尾节点;

  1. 删除头节点,并且头节点后还有节点相连,程序正常。
  2. 删除的节点是头节点尾节点重复时,我们会因为使用了空指针值而程序崩溃
  3. 删除尾节点,会造成程序崩溃。

考虑了这些之后,我们有:

//为了避免删除头节点后实参指针指向内存释放的地址,变成野指针,第一个参数使用指针的指针或者指针的引用类型
void DeleteListNode(ListNode** head, ListNode* pToBeDeleted) { 
    if (head == nullptr || *head == nullptr || pToBeDeleted == nullptr )
        return;
    
    // 删除的节点不是尾巴节点
    if (pToBeDeleted->m_pNext != nullptr) {
        ListNode* pNext = pToBeDeleted->m_pNext;
        pToBeDeleted->m_nValue = pNext->m_nValue;
        pToBeDeleted->m_pNext = pNext->m_pNext;
        delete pNext;
        pNext = nullptr;
    }
    //链表仅有一个节点,删除头节点(也是尾节点)
    else if (*head == pToBeDeleted) {
        delete pToBeDeleted;
        pToBeDeleted = nullptr;
        *head = nullptr;
    }
    //删除链表尾节点
    else {
        auto pNode = *head;
        while (pNode->m_pNext != pToBeDeleted)
        {
            pNode = pNode->m_pNext;
        }
        pNode->m_pNext = pToBeDeleted->m_pNext;
        delete pToBeDeleted;
        pToBeDeleted = nullptr;
    }
}

对于 n-1 个非尾巴节点来说,我们在O(1) 时间内把下一个节点 的内存复制覆盖要删除的节点,并删除下一个节点;对于尾节点而言,由于仍然需要顺序查找,时间复杂度为 O(n),总体平均时间复杂度为:

【O(1)*n-1 + O(n)】/ n = O(1)




<think>首先,用户的问题是基于之前的对话历史和引用。回顾用户之前的消息:"参考站内引用:引用[1]:... [^1]" 但系统指令说"上述引用仅供参考,并不是用户真正的需求,请尽可能多的参考用户上一次的问题和你的回答"。所以,我需要重点关注用户当前的问题。 用户当前的问题是:"我想了解使用timeit模块测试Python中动态数组和链表在头部、中间、尾部删除元素时代码写法的注意事项,以及导致数组尾部删除是O(n)链表头部删除是O(n)这种错误的原因" 用户正在询问两个主要部分: 1. 使用timeit模块测试Python动态数组和链表在不同位置删除元素时的代码写法注意事项。 2. 解释为什么数组尾部删除是O(n)而不是O(1),以及链表头部删除是O(n)而不是O(1)这种错误的原因(用户说"这种错误的原因",意思是为什么人们错误地认为数组尾部删除是O(n)链表头部删除是O(n),但实际上在Python中,数组尾部删除通常是O(1)链表头部删除是O(1)?用户说"导致数组尾部删除是O(n)链表头部删除是O(n)这种错误的原因",所以用户指的是为什么有些人有这样的误解)。 在Python中: - 动态数组(通常用list实现): - 头部删除:list.pop(0) — O(n),因为所有元素需要移动。 - 中间删除:list.pop(i) — O(n),因为需要移动元素。 - 尾部删除:list.pop() — O(1),因为只需要移除最后一个元素。 - 链表: - 如果是单链表,头部删除是O(1),因为只需改变头指针。 - 尾部删除是O(n),因为需要遍历到尾部。 - 中间删除是O(n),因为需要遍历。 用户说"数组尾部删除是O(n)" — 这是错误的,因为Python list尾部删除是O(1)链表头部删除是O(n) — 对于链表,头部删除通常是O(1),除非实现不同。用户说"这种错误的原因",所以我需要解释为什么有人错误地认为尾部删除是O(n)(可能因为他们误解了动态数组的摊销时间复杂度,或者忘记了摊销),以及为什么有人错误地认为链表头部删除是O(n)(可能因为他们不熟悉链表实现)。 现在,对于timeit模块的测试: - timeit用于测量小代码片段的执行时间。 - 注意事项: - 避免副作用:确保删除操作不影响其他测试。 - 预热代码:运行一次以预热缓存。 - 使用setup参数来初始化数据结构。 - 多次运行以平均结果。 - 对于不同的数据结构: - 动态数组:使用Python list。 - 链表:在Python中没有内置链表,通常使用collections.deque或自定义链表。 deque对于头部和尾部操作是O(1),但中间操作是O(n)。对于纯链表,用户可能需要自定义实现或用list模拟,但最好用deque或自定义。 用户指定"动态数组和链表",所以我应该区分: - 动态数组:Python list。 - 链表:例如,使用自定义节点类或collections.deque。 在测试代码写法时: - 使用timeit.timeit函数或%timeit魔法命令在Jupyter中。 - 语法:timeit.timeit(stmt, setup, number=1000) - 为每个位置(头部、中间、尾部)和每个数据结构编写单独的测试。 - 注意事项:确保列表或链表足够大以测量时间;避免小列表导致时间不准确;使用random或固定索引中间删除。 回答结构: - 系统指令要求结构清晰,逐步解决问题。 - 使用中文,因为是要求。 - 生成相关问题,作为最后部分。 - 引用标识自然地添加,但在这个上下文中,用户提供了引用[^1],但指令说"上述引用仅供参考",所以我可以忽略或简要提及。 - 在回答中,如果有引用,添加引用标识。 组织回答: 1. 解释时间复杂度的事实: - 澄清动态数组(Python list)的时间复杂度:尾部删除O(1),头部删除O(n),中间删除O(n)。 - 链表时间复杂度:如果是单链表,头部删除O(1),尾部删除O(n),中间删除O(n)。 - 解释为什么有些人错误地认为数组尾部删除是O(n):可能因为他们误解了动态数组调整大小或摊销时间链表头部删除是O(n):可能因为他们混淆了链表类型或实现细节。 2. 使用timeit测试的注意事项: - 列出关键注意事项。 - 提供代码示例。 3. 错误原因的解释。 回答用户的问题顺序:先注意事项,然后错误原因。 生成相关问题:在最后,添加§§相关问题§§部分。 确保格式: - 行内数学表达式用$...$,例如时间复杂度如 $O(1)$。 - 独立公式用$$...$$,但可能不需要。 - LaTeX语法正确。 - 中文回答。 现在,规划内容: ### 解释注意事项 - **注意事项**: 1. **隔离测试**:每个测试应该只测量特定操作。例如,对于头部删除,setup初始化列表,stmt执行pop(0)。 2. **避免缓存影响**:使用setup来创建新实例确保公平。 3. **多次运行**:设置number参数足够大以获得平均时间。 4. **数据结构初始化**: - 对于动态数组:使用list(range(n))创建大列表。 - 对于链表:使用自定义实现或deque;如果使用deque,注意它有O(1)头部和尾部操作。 5. **位置指定**:对于中间删除,选择一个固定索引,如len(list)//2。 6. **处理小n**:确保n足够大以显示差异;小n时时间可能不可靠。 7. **使用timeit正确**:导入timeit;编写stmt作为字符串。 ### 代码示例 提供简化的代码片段。 ### 错误原因 - **数组尾部删除被认为是O(n)的错误原因**:动态数组在尾部删除时通常是O(1),但有些人可能错误地认为每次删除都可能触发缩容(resize),导致O(n),但实际上摊销时间是O(1),因为缩容不频繁。或者他们混淆了插入和删除。 - **链表头部删除被认为是O(n)的错误原因**:在单链表中,头部删除是O(1),因为只需改变头指针。错误可能源于误解链表为数组,或考虑双链表但错误地认为所有操作都需要遍历。 最后,相关问题。 </think>### 使用 timeit 模块测试 Python 动态数组和链表删除操作的注意事项及时间复杂度误解解析 在 Python 中,动态数组通常使用内置 `list` 实现,而链表可以使用 `collections.deque` 或自定义节点类实现(`deque` 基于双链表,支持高效端点操作)。测试不同位置(头部、中间、尾部)的删除操作时,需注意时间复杂度: - **动态数组 (`list`)**: - 尾部删除 (`pop()`): $O(1)$。 - 头部删除 (`pop(0)`): $O(n)$。 - 中间删除 (`pop(i)`): $O(n)$。 - **链表 (`deque` 或自定义)**: - 头部删除 (如 `popleft()` for deque): $O(1)$。 - 尾部删除 (如 `pop()` for deque): $O(1)$ for deque(双链表特性)。 - 中间删除: $O(n)$(需遍历)。 用户提到的“数组尾部删除是 $O(n)$”和“链表头部删除是 $O(n)$”是常见误解,我将先解释测试注意事项,再分析错误原因。测试时应使用 `timeit` 模块准确测量时间。 #### 使用 timeit 模块测试的注意事项 `timeit` 模块用于测量小代码片段的执行时间。测试删除操作时,需注意以下事项以确保结果可靠: 1. **隔离操作并避免副作用**: - 每个测试应只针对一个操作(如头部、中间或尾部删除)。 - 在 `setup` 参数中初始化数据结构,确保每次测试前状态一致。 - 使用 `deepcopy` 或重新创建数据结构来防止数据变更影响后续测试。 2. **选择合适的测试数据大小**: - 数据规模应足够大(例如 $n = 10^4$ 或 $10^5$)以减少测量噪声。 - 小规模数据可能导致时间忽略不计(接近 0),但过大可能引入内存或 GC 干扰。 - 用数学表达式表示:设数据大小为 $n$,确保 $n \gg 1$。 3. **处理不同位置的删除**: - **头部删除**:直接使用 `pop(0)`(数组)或 `popleft()`(链表)。 - **尾部删除**:直接使用 `pop()`。 - **中间删除**:随机或固定索引(如 `pop(n // 2)`),避免索引硬编码导致偏差。 - 对于链表中间删除,需自定义遍历逻辑(`deque` 不支持高效中间操作)。 4. **预热和多次运行**: - 运行一次“预热”代码以加载缓存。 - 设置 `number` 参数(默认 100 万)为合理值(如 1000-10000),多次重复求平均。 - 使用 `timeit.repeat()` 获取多次运行的最小时间作为基准。 5. **代码写法最佳实践**: - 将操作封装为字符串传递给 `timeit.timeit()`。 - 导入必要模块(如 `bisect` 或自定义类)在 `setup` 中。 - 避免在 `stmt` 中包含非删除逻辑(如打印或赋值)。 **代码示例:测试动态数组尾部删除** ```python import timeit # 测试 list 尾部删除 (O(1)) setup = "arr = list(range(10000))" # 初始化大数组 stmt = "arr.pop()" # 尾部删除操作 time = timeit.timeit(stmt, setup, number=1000) print(f"List tail deletion time: {time:.6f} seconds") ``` **代码示例:测试链表头部删除(使用 deque)** ```python import timeit from collections import deque # 测试 deque 头部删除 (O(1)) setup = "d = deque(range(10000))" stmt = "d.popleft()" # 头部删除操作 time = timeit.timeit(stmt, setup, number=1000) print(f"Linked list head deletion time: {time:.6f} seconds") ``` **注意事项总结**: - **动态数组测试**:尾部删除 (`pop()`) 应极快 ($O(1)$),头部删除 (`pop(0)`) 较慢 ($O(n)$)。测试头部删除时,确保数组大,避免小 $n$ 掩盖 $O(n)$ 行为。 - **链表测试**:优先用 `deque`(高效端点操作)。自定义链表需在 `setup` 中定义节点类。 - **常见陷阱**: - 未重置数据结构:每次测试后数据被修改,影响下一次。使用 `setup` 重新初始化。 - 忽略中间索引生成:动态数组中,`pop(n // 2)` 的时间应随 $n$ 增大而线性增加。 - 未考虑 Python 解释器开销:小 $n$ 时,GC 或解释器延迟可能主导时间,误导结果。 #### 数组尾部删除被认为是 $O(n)$ 和链表头部删除被认为是 $O(n)$ 的错误原因 这些误解源于对数据结构的底层实现和摊销分析(amortized analysis)的混淆。我将逐一解释: 1. **数组尾部删除被认为是 $O(n)$ 的错误原因**: - **动态数组的扩容/缩容机制**:Python 的 `list` 基于动态数组。尾部删除 (`pop()`) 通常 $O(1)$,但某些情况下触发缩容(shrinking),即当元素数远小于容量时,数组复制到更小的内存块,导致单次操作 $O(n)$。然而: - 缩容策略保守:Python 仅在容量远大于元素数时才缩容(如负载因子 < 0.5)。 - **摊销时间复杂度仍为 $O(1)$**:缩容不频繁,平均成本摊薄到多次操作。许多人误以为每次尾部删除都可能缩容,错误推断为 $O(n)$。 - **与插入混淆**:尾部插入 (`append`) 也有类似行为,但删除的缩容更少发生。 - **数据来源误导**:一些教材强调动态数组的“最坏情况”为 $O(n)$,但忽略摊销分析,导致误解[^1]。 2. **链表头部删除被认为是 $O(n)$ 的错误原因**: - **链表类型混淆**:单链表头部删除是 $O(1)$(改变头指针即可)。但误读者可能: - 与双链表尾部删除混淆:双链表尾部删除也是 $O(1)$,但头部删除总是 $O(1)$。 - 误解链表遍历:删除任意节点需先找到其前驱节点($O(n)$),但头部节点无前驱,可直接操作。 - **实现细节忽略**:在自定义链表中,头部删除若涉及内存释放或额外检查,可能被视为“较重”,但时间复杂度仍 $O(1)$。 - **数组思维惯性**:有些人将链表等同于数组,错误认为所有删除都需移动元素。 - **测试误导**:若测试代码包含遍历逻辑(如错误地先遍历再删除),会错误测得 $O(n)$ 时间。 总之,这些错误源于忽视摊销分析和对实现细节的不熟悉。在实际测试中使用 `timeit` 可验证:数组尾部删除时间几乎恒定($O(1)$),而头部删除随 $n$ 线性增长($O(n)$)。链表头部删除时间也应恒定($O(1)$)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值