据说是腾讯的一个面试题
问题:已知一个长度未知单链表,如何快速找到中间位置的节点?
- 长度未知
- 单向链表
分析问题
目的:找到最中间位置的节点。其实就是一个我们如何去遍历这个单链表的问题。
初步上手
单链表的特性大家都知道,只能单向遍历,而且此处还不知道链表的长度。一种方法是:先遍历找到链表长度n,然后我们遍历(n+1)/2次,让指针指向链表一半的位置出的节点,此时就是链表的中间位置(忽略偶数长度,偶数长度的话其实有两个,最中间的两个,n/2就可代表中间位置的了,他的下一个也是中间位置节点)。这种思路的话,时间复杂度来说是O(n)。实现的话也很容易,这里就不多说。这个大家应该都可以想到,最容易的一种方式。
优化分析
既然上一个复杂度是O(n),那我本能的想想能否降到对数阶O(logN)呢?这或许有些难度,或许可以降到O(n/2),虽然从简单考虑来说,O(n)与O(n/2),可以说都是线性阶,但是效率上总归是有差别的,毕竟可以少一半的执行时间。那如何来实现呢?
另一种方法,可以通过快慢指针的方式,也就是说我们如果一开始有两个指针一个mid,一个last,然后都初始化指向链表头:
mid = list->head;
last = list->head;
然后以一个循环执行,mid指向下一个,last以两倍的方式增长,就是说last指向下一个的下一个,这样对应的last指向链表尾时,mid就正好指向中间位置。
while(last != NULL) //当last指向链表尾的next域即last == NULL结束
{
mid = mid->next;
last = last->next->next;
}
这样的话当last停下来,mid就是中间位置的指针,而循环只跑了n/2次,也就是时间复杂度为O(n/2)。
写个函数具体实现一下
前置条件:一个非空链表指针;
后置条件:返回中间节点指针;
假设我们已经定义了Node节点类型,以及链表类型List(一个结构,仅含头指针)
Node * FindMid(List * ls)
{
//传入空链表时或者仅含一个节点,提前结束
if(ls->head == NULL || ls->head->next == NULL)
return NULL;
Node * mid = ls->head;
Node * last = ls->head;
//当这个while循环停下来时,last指向尾节点的next,即NULL,而mid恰好指向我们要的中间节点
//循环条件检查是否当前的下一个是否为空,即是否last指向了链表尾
while(last->next != NULL)
if(last->next->next != NULL)
{
mid = mid->next;
last = last->next->next;
}else
//最后一次,当前last指向下一个,即前进至链表尾节点,避免last越界
last = last->next;
return mid;
}
总结
题目不难,关键是如何去找到更优的算法,从而在查找效率上取得进步。这样的算法就时间复杂度考虑其实也还是线性阶,不过应该是基于已知条件下的最优算法了,毕竟总体上较之第一个算法时间节省了一半。
启发
快慢指针法,一快一慢,两个指针,快指针类似于一个渐增的标尺,慢指针即标识标尺的中间位置。每轮循环两指针都渐增,当标尺增长至单链表尾,慢指针其实就标识着链表的中间节点。