从链表中删除数据的时间复杂度真的是O(1)吗?

本文经授权转载自微信公众号:小争哥(xiaozhengge0822),作者:小争哥

数组和链表作为最基础的数据结构,在面试的时候,经常会被问到。最常被问到的一个问题,那就是,对比一下数组和链表。如果你是Java工程师,会被问到可能就是对比一下ArrayList和LinkedList。

从我面试的经历来看,最常听到的答案就是:数组查找的时间复杂度是O(1),但是删除、插入数据的时间复杂度是O(n);而链表查找的时间复杂度是O(n),但是删除、插入数据的时间复杂度是O(1)。

当然,这样的答案并不全错,但是过于笼统,过于教科书。为什么我会这么说呢?

这里有几点回答不准确的地方,我一一纠正一下。

1. "数组查找的时间复杂度是O(1)"--描述不准确

实际上,在数组中查找一个数据,并不快速。打个比方,如果我们数组中存储的是10个整数,2、3、1、19、...、12,如果我们要在里面查找数据12,我们还是要一个一个的遍历整个数组元素,所以时间复杂度仍然是O(n)。即便数组中存储的数据是有序的,我们利用二分查找算法来查找,那时间复杂度也只能做到O(logn)。

也就是说,在数组中,按照元素的值来查找,时间复杂度并不是O(1)。所以,“数组查找的时间复杂度是O(1)”表述是不对的。那正确是表述应该是怎样的呢?

正确的表述应该是:“数组中按照下标随机访问的时间复杂度是O(1)”。这也是数组这种数据结构最大的特点之一了。如果在面试中,你能如此回答,面试官肯定会对你刮目相看。

2. “链表删除数据的时间复杂度是O(1)”--描述不准确

实际上,这句话也不是全错,主要还是描述不够准确。链表的删除操作可以分为好几种情况,我们现在列出三种,将其定义为三个删除函数。

void remove(Node* list, int dataValueToBeDeleted);	
void remove(Node* list, Node *pNodeToBeDeleted);	
void remove(Node* list, Node* pPreviousNodeToBeDeleted);

我们逐一来看下,每个不同的删除方式,时间复杂度都是多少。

1)第一种删除方式:void remove(Node* list, int dataValueToBeDeleted);

其中,list表示链表,dataValueToBeDeleted参数表示要删除的元素的值。比如,链表中存储了5个数据,分别是:4、7、9、1、3,当dataValueToBeDeleted=9的时候,表示删除链表中值等于9的那个结点。

对于这种删除方式来说,我们总是要从链表头开始遍历整个链表数据,才能找到存储了数据9的那个结点,所以,遍历链表操作的时间复杂度是O(n)。

当通过遍历找到这个要删除的结点的时候,我们删除它就很快速了,因为在遍历的过程中,我们可以事先记录要删除结点的前驱结点。有了前驱结点,我们只需要简单的指针操作,就可以将要删除的结点删除。这一步操作的时间复杂度是O(1)。

所以,根据大O时间复杂度表示法中的加法法则,这种删除操作的总的时间复杂度是O(n)。

2)第二种删除方式:void remove(Node* list, Node *pNodeToBeDeleted);

这种删除方式中,list仍然表示链表,而我们pNodeToBeDeleted表示要删除的结点的指针,反过来说,也就是,我们要删除pNodeToBeDeleted这个指针指向的结点。

对于这种删除方式,我们也要分两种情况来考虑。

如果list链表是单链表,那这种删除方式的时间复杂度是O(n)。因为,我们要删除pNodeToBeDeleted结点,就要先找到它的前驱结点。而在单链表中,查找一个结点的前驱结点的时间复杂度是O(n)。所以,对于单链表来说,这种删除操作的时间复杂度是O(n)。

如果list链表是双向链表,那这种删除方式的时间复杂度是O(1)。因为,在双向链表中,我们可以快速地得到pNodeToBeDeleted结点的前驱结点,时间复杂度是O(1)。

3)第三种删除方式:void remove(Node* list, Node* pPreviousNodeToBeDeleted);

这种删除方式表示,我们删除pPreviousNodeToBeDeleted指针指向的下一个结点。这里注意,并不是要删除pPreviousNodeToBeDeleted指针指向的结点。

因为我们已经拿到了要删除结点的前驱结点,所以,不管是单链表还是双向链表,对于这种删除方式,时间复杂度都是O(1)。

3. “链表插入数据的时间复杂度是O(1)”--描述不准确

对于插入的情况,跟删除情况类似,也是描述不够准确。在链表中插入数据,有很多种情况。比如,我们定义的下面这几个函数表示的情况。

void insertBefore(Node* p, int dataValue);	
void insertAfter(Node* p, int dataValue);

第一个函数表示,在p节点前面插入一个结点,结点值为dataValue。对于单链表来说,这种插入操作的时间复杂度是O(n)。对于双向链表来说,这种插入操作的时间复杂度是O(1)。这里我只给出结论,具体留给你自己来分析吧。

第二个函数表示,在p节点后面插入一个结点,结点值为dataValue。不管是单链表还是双向链表,这种插入操作的时间复杂度都是O(1)的。

当然,还有跟多的插入方式,比如下面这两种,时间复杂度又是不一样的。

void insertAtHead(Node* listHead, int dataValue);	
void insertToTail(Node* listHead, int dataValue);

总结一下,也就是说,在分析链表和数组上操作的区别的时候,一定要描述准确各个操作是如何定义的,这样才好具体分析时间复杂度。同样的操作,不同的定义方式,时间复杂度是不一样的

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

640?wx_fmt=png

好文章,我在看❤️

猜你喜欢

转载自blog.csdn.net/hollis_chuang/article/details/102480657
今日推荐