关于快慢指针的若干应用详解

一.问题来源

  昨晚看微博,发现于梁斌penny,他在说现在的面试制度考不出来真功夫,也就是基本功,面试题千篇一律的算法,看过会,不看就不会。期间提到了快慢指针求中位数。

  查资料时我发现,这其实是计算机系统原理里的知识点。

二.快慢指针概念

  快慢指针中的快慢指的是移动的步长,即每次向前移动速度的快慢。例如可以让快指针每次沿链表向前移动2,慢指针每次向前移动1次。

三.快慢指针的应用

3.1 判断单链表是否为循环链表

  对于初学者来说,要解决这个问题,最可能采取的方法就是使用两个循环。当外层循环步进一个节点时,内层循环就遍历外层循环节点之后的所有节点,然后比较内外循环的两个节点。若有节点地址相等,则表明该单链表有循环,反之则不存在循环。这种方法无疑效率比较低。

  今天给大家介绍一个经典的方法,通过快慢指针来检查单链表是否存在循环。其思路很简单,大家可以想一下上体育课长跑的情景。当同学们绕着操场跑步的时候,速度快的同学会遥遥领先,最后甚至会超越其它同学一圈乃至n圈——这是绕圈跑。那么如果不是绕圈跑呢?速度快的同学则会一直领先直到终点,不会再次碰到后面的速度慢同学——不考虑地球是圆的这种情况。

  快慢指针的设计思想也是这样。快指针每次步进多个节点——这个视情况而定,慢指针每次只步进一个节点。那么如果该链表存在循环的话,快指针一定会再次碰到慢指针,反之则不存在循环。

让快慢指针从链表头开始遍历,快指针向前移动两个位置,慢指针向前移动一个位置;如果快指针到达NULL,说明链表以NULL为结尾,不是循环链表。如果 快指针追上慢指针,则表示出现了循环。

int isExitsLoop(LinkList L) {
    LinkList fast, slow;
    fast = slow = L;
	while (fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast)
        {
            break;
        }
    }
    return ((fast == NULL) || (fast->next == NULL));
}

  注:不一定直接是一个环,可能说先共同走一段路,在尾部形成环。如果是第一种情况(长度为5,从1开始),看分解如下表。

1 3 5 2 4 1
1 2 3 4 5 1

 

3.2 在有序链表中寻找中位数

  该方法在不借助计数器变量实现寻找中位数的功能。原理是:快指针的移动速度是慢指针移动速度的2倍,因此当快指针到达链表尾时,慢指针到达中点。程序还要考虑链表结点个数的奇偶数因素,当快指针移动x次后到达表尾(1+2x),说明链表有奇数个结点,直接返回慢指针指向的数据即可。如果快指针是倒数第二个结点,说明链表结点个数是偶数,这时可以根据“规则”返回上中位数或下中位数或(上中位数+下中位数)的一半。

while (fast&&slow)
{
  if (fast->next==NULL)
      return slow ->data;
  else if (fast->next!= NULL && fast->next->next== NULL)
      return (slow ->data + slow ->next->data)/2;
  else
  {
      fast= fast->next;
      fast= fast->next;
      slow = slow ->next;
  }
 }

3.3 如果链表为存在环,如果找到环的入口点?

  有一个单链表,其中可能有一个环,也就是某个节点的next指向的是链表中在它之前的节点,这样在链表的尾部形成一环。
  那么问题来了,如何判断一个链表是不是这类链表?如果链表为存在环,如果找到环的入口点?  当fast若与slow相遇时,slow肯定没有走遍历完链表(不是一整个环,有开头部分,如上图)或者恰好遍历一圈(未做验证,看我的表格例子,在1处相遇)。于是我们从链表头、与相遇点分别设一个指针,每次各走一步,两个指针必定相遇,且相遇第一点为环入口点(慢指针走了n步,第一次相遇在c点,对慢指针来说n=s+p,也就是说如果慢指针从c点再走n步,又会到c点,那么顺时针的CB距离是n-p=s,但是我们不知道s是几,那么当快指针此时在A点一步一步走,当快慢指针相遇时,相遇点恰好是圆环七点B(AB=CB=s))。

node* findLoopPort(node *head) {
    node *fast, *slow;
    fast = slow = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            break;
        }
    }
    if ((fast == NULL) || (fast->next == NULL)) {
        return NULL;
    }
    slow = head;
    while (slow != fast) {
        slow = slow->next;
        fast = fast->next;
    }
    return slow;
}

3.4 扩展问题

  判断两个单链表是否相交,如果相交,给出相交的第一个点(两个链表都不存在环)。

  比较好的方法有两个:

  1.将其中一个链表首尾相连,检测另外一个链表是否存在环,如果存在,则两个链表相交,而检测出来的依赖环入口即为相交的第一个点。
  2.如果两个链表相交,那个两个链表从相交点到链表结束都是相同的节点,我们可以先遍历一个链表,直到尾部,再遍历另外一个链表,如果也可以走到同样的结尾点,则两个链表相交。
这时我们记下两个链表length,再遍历一次,长链表节点先出发前进(lengthMax-lengthMin)步,之后两个链表同时前进,每次一步,相遇的第一点即为两个链表相交的第一个点。

四.参考文献及结束语

4.1 问题1

  相差多少?才能保证相遇?如果从物理学的角度理解(s-t曲线),肯定相遇,那么问题是如何尽快相遇(也就是快慢指针相差几倍才能使A点尽可能靠近源点,也就是说相遇尽可能早,这样复杂度就低了)?看下图,自己研究吧,笔者未做详细探索。

 

4.2 问题2

  s=t,s=2t和s=3t,s=6t的效果一样吗?

4.3 感想

  指针可以相差倍数,那也可以相差固定位数啦?比如求链表的倒数第n位.

4.4 参考文献

  http://anyhu.blog.sohu.com/184515249.html
  http://www.nowamagic.net/librarys/veda/detail/1842

时间: 2024-10-02 03:34:27

关于快慢指针的若干应用详解的相关文章

C 语言指针变量的运算详解_C 语言

指针变量保存的是地址,本质上是一个整数,可以进行部分运算,例如加法.减法.比较等,请看下面的代码: #include <stdio.h> int main(){ int a = 10, *pa = &a, *paa = &a; double b = 99.9, *pb = &b; char c = '@', *pc = &c; //最初的值 printf("&a=%#X, pa=%#X, pb=%#X, pc=%#X\n", &

结构体指针之 段错误 详解(精典!!!)

一个网友问了我一个问题,一个C程序运行出现了段错误,这个问题非常好,很多初学者都容易犯这个错误,具体代码如下: 这个编译没有问题,但是运行是段错误    Segmentation fault 因为你定义了一个结构体指针p,用来指向此类结构体,但是你却没有给他赋值,此时p的值为NULL,你并没有在内存中为p分配任何空间,所以p->a=1这句就会出段错误. 修改方法1:可以给p分配一段内存空间,并使其指向此空间: p=(struct abc *)malloc(sizeof(struct abc));

函数指针的一些概念详解_C 语言

函数指针 最近看android camera 的source ,发现大量的call back ,多线程,有必要对其中的基础 :函数指针复习一下,觉得函数指针主要还是用在call back 函数,以及多线程多进程编程中.函数在被编译器编译后就是一段二进制码,而这段二进制码有一个入口地址,而这个入口地址就是函数指针的值了. 首先看函数指针的语法,举一个最简单的例子,要创建一个函数指针,则它与它指向的函数,在参数个数类型以及返回值上都保持一致,跟重载的要求应该是一样的. Int a(int a ) {

关于C语言指针赋值的问题详解_C 语言

一个代码: 复制代码 代码如下: #include<stdio.h>#include<stdlib.h>#define uchar unsigned char#define uint unsigned int void display(uchar *p); char h[4] = {'A','B','C','\0'};char e[4] = {'E','F','L','\0'};char l[4] = {'M','N','O','\0'};char o[4] = {'X','Y',

智能指针与弱引用详解_Android

在android 中可以广泛看到的template<typename T> class Sp 句柄类实际上是android 为实现垃圾回收机制的智能指针.智能指针是c++ 中的一个概念,因为c++ 本身不具备垃圾回收机制,而且指针也不具备构造函数和析构函数,所以为了实现内存( 动态存储区) 的安全回收,必须对指针进行一层封装,而这个封装就是智能指针,其实说白了,智能指针就是具备指针功能同时提供安全内存回收的一个类.当然,智能指针的功能还不只这些,想了解更多大家可以去研究下- 智能指针有很多实现

C++函数的传入参数是指针的指针(**)的详解

要修改变量的值,需要使用变量类型的指针作为参数或者变量的引用.如果变量是一般类型的变量,例如int,则需要使用int 类型的指针类型int *作为参数或者int的引用类型int&.但是如果变量类型是指针类型,例如char*,那么需要使用该类型的指针,即指向指针的指针类型 char* *,或者该类型的引用类型char*&.   首先要清楚  不管是指针还是值传入函数后都会创建一个副本,函数结束后值内容不能传出来是因为值的副本,而传入的值并没被修改,指针能传出来是因为我们修改的是指针指向的内容

智能指针与弱引用详解

在android 中可以广泛看到的template<typename T> class Sp 句柄类实际上是android 为实现垃圾回收机制的智能指针.智能指针是c++ 中的一个概念,因为c++ 本身不具备垃圾回收机制,而且指针也不具备构造函数和析构函数,所以为了实现内存( 动态存储区) 的安全回收,必须对指针进行一层封装,而这个封装就是智能指针,其实说白了,智能指针就是具备指针功能同时提供安全内存回收的一个类.当然,智能指针的功能还不只这些,想了解更多大家可以去研究下- 智能指针有很多实现

详解C++中的this指针与常对象_C 语言

C++ this指针详解 this 是C++中的一个关键字,也是一个常量指针,指向当前对象(具体说是当前对象的首地址).通过 this,可以访问当前对象的成员变量和成员函数. 所谓当前对象,就是正在使用的对象,例如对于stu.say();,stu 就是当前对象,系统正在访问 stu 的成员函数 say(). 假设 this 指向 stu 对象,那么下面的语句中,this 就和 pStu 的值相同: Student stu; //通过Student类来创建对象 Student *pStu = &s

详解Java中的指针、引用及对象的clone

对象|详解 Java语言的一个优点就是取消了指针的概念,但也导致了许多程序员在编程中常常忽略了对象与引用的区别,本文会试图澄清这一概念.并且由于Java不能通过简单的赋值来解决对象复制的问题,在开发过程中,也常常要要应用clone()方法来复制对象.本文会让你了解什么是影子clone与深度clone,认识它们的区别.优点及缺点.看到这个标题,是不是有点困惑:Java语言明确说明取消了指针,因为指针往往是在带来方便的同时也是导致代码不安全的根源,同时也会使程序的变得非常复杂难以理解,滥用指针写成的