单链表的时间复杂度

让我们来研究一下单链表的时间复杂度

相比于数组,单链表在插入删除节点的时候,不需要移动大量的元素,只需要改变指针的指向,所以我们往往看到好多文章说它的时间复杂度是O(1)。但是,这种说法是不对的,应该根据情况而定。

O(1)的情况

一个已知头结点的链表,删除某结点,且告诉你该元素的地址node

由于这是单链表,我们无法获取node前一个节点的地址,看上去貌似不能删除这个结点。但是,是否删除这个节点只是看这个节点的data值是否还存在于链表中,因此,我们可以让链表看起来删除了node,实则删除了结点node.next.

newNode=node.next;  
node.data=newNode.data;//移交元素  
node.next=newNode.next;//移交指针  
free(newNode);//释放目标删除结点后一个节点的内存  
newNode=NULL;//置空指针 

这样,看起来删除了node结点,实际上node.next成了真正的牺牲品。上述操作在O(1)内完成。

一个已知头结点的链表,在某结点后面插入新节点,大小为newdata,且告诉你该结点的地址node
newNode=NULL;  
newNode.data=newdata;  
newNode.next=node.next;  
node.next=newNode;

O(n)的情况

一个已知头结点的链表,删除第index个元素

首先需要从头开始向后遍历,直到找到第index-1个结点,这需要O(n)时间;找到以后,改变指针的指向,这需要O(1)的时间。所以这种情况下,时间复杂度为O(n)。

let i=0; 
let p = head; 
while(head&&i<=index-2)//找到第index-1个结点退出  
{  
    p=p.next;  
    i++;  
}  
let q=p.next;//q是第index个节点,即要删除的节点  
p.next=q.next;//转移指针  
free(q);//释放内存
newNode=NULL;  
newNode.data=newdata;  
newNode.next=node.next;  
node.next=newNode;
一个已知头结点的链表,在第index个元素前插入一个元素

首先需要从头开始向后遍历,直到找到第index-1个结点,这需要O(n)时间;找到以后,创建新节点,改变指针的指向,这需要O(1)的时间。所以这种情况下,时间复杂度为O(n)。

let p=head;  
int i=0;  
while(p&&i<=index-2)  
{  
   p=p.next;  
    i++;  
}  
let newNode=NULL;  
newNode.data=newdata;  
newNode.next=p.next;  
p.next=newNode;
### 单链表操作的时间复杂度分析 #### 查找操作 对于单链表而言,由于其存储结构的特点决定了无法随机访问元素,因此查找操作只能通过顺序扫描完成。这意味着从头节点开始逐一遍历整个列表,直到找到目标节点或到达链表末尾为止。这种特性使得查找操作的时间复杂度为 \( O(n) \),其中 \( n \) 表示链表中的节点数量[^1]。 #### 插入操作 - **已知前驱节点的情况下** 如果在执行插入之前已经明确了待插入位置的前驱节点,则仅需调整指针即可完成插入动作,此过程无需额外遍历其他节点,所以时间复杂度为 \( O(1) \)[^1]。 - **未知前驱节点时** 当未提供具体前驱节点而只是给出索引或其他条件来定位插入点时,必须先从头节点出发逐步寻找合适的插入位置,这一阶段所需时间为 \( O(n) \)。随后再进行实际的插入逻辑处理 (\( O(1) \)) ,综合来看整体时间复杂度仍为 \( O(n) \)[^4]。 #### 删除操作 同样地,在删除某个特定节点时也需要考虑是否提前获知该节点的前驱信息: - 若事先知晓前驱节点的位置,则可以直接修改相应指针关系从而移除指定节点,这部分耗时固定为常数级别即 \( O(1) \)[^1]; - 而如果只知道被删对象本身而不清楚它的上一节点是谁的话,则不得不像上述情况那样从前至后逐一排查直至发现目标及其前置关联项,然后再实施真正的删除步骤 (同样是 \( O(1) \)) 。最终得出结论——当缺乏明确指引时,此类场景下的总开销亦呈现线性增长趋势达到 \( O(n) \)[^1]。 另外值得注意的是特殊情形下的一些表现形式比如向链表首端追加数据(\( addFirst() \)) 或者是在末端附加新成员(\( addLast() \)): - 对于前者来说每次新增都只需更新一次链接地址便可生效因而具备最优效率表现为恒定值\( O(1)\); - 至于后者则因需要抵达最后一个现有项目处才能继续扩展长度的缘故而导致最差状况下单次作业可能触及全部存量记录进而呈现出渐近上限等于输入规模大小的结果也就是典型的\( O(n)\)行为模式[^3]. 最后提及一下关于两个已有升序排列好的独立序列如何高效融合成一个新的降序组织起来的新实例问题解答如下所示: ```c++ // 假设 Node 结构定义如下: struct Node { int data; struct Node* next; }; void mergeAndReverse(Node*& A, Node*& B){ Node dummyHead(-1); // 创建虚拟头结点用于构建新的链表 Node *tail = &dummyHead; while(A && B){ if(A->data >= B->data){ tail ->next=A; A=A->next; } else{ tail ->next=B; B=B->next; } // 头插法实现逆置效果 Node *temp=tail->next; tail->next=temp->next; temp->next=dummyHead.next; dummyHead.next=temp; tail=&dummyHead;// 更新尾巴始终指向最新加入的那个节点前面那个地方 } // 将剩余部分接到新链表前端 while(A || B){ Node *current=(A)?A:B; if(A) A=A->next; else B=B->next; tail->next=current; current->next=dummyHead.next; dummyHead.next=current; } } ``` 以上代码片段展示了如何利用比较后再采用头插方式快速生成所需的递减有序单链表,并且保持了期望中的时间复杂度水平【O(m+n)】,这里m,n分别代表原两链表长度[^2]。 相关问题
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值