[leetcode]浅谈快慢指针在算法中的应用

天下武功, 无坚不破, 唯快不破 ——— 某大佬

本文为我,leetcode easy player,algorithm(算法)小xuo生在刷题过程中对快慢指针的应用的总结

什么是快慢指针

  1. 快慢说的是移动的速度, 即每次移动的步长的大小
  2. 指针为记录变量内存地址的变量, 用它能间接访问变量的值

为了更直观的了解快慢指针, 请看如下c++demo

在内存中开辟容量为11个整型元素的数组存储空间

初始化整型快慢指针变量都记录数组第一个元素的内存地址

在循环里, 慢指针每次增1, 快指针每次增2

因为c++中指针占4字节即32位的16进制的内存空间, 所以慢指针记录的内存地址每次移动4个字节, 而块指针记录的内存地址每次移动8个字节(
注意因为是16进制, 所以快指针从0x7ffee3c632580x7ffee3c63260也是移动了8个字节)

#include <iostream>
using namespace std;
int main (int argc, char const *argv[]) {
  int arr[11] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
  int *slowPointer = &arr[0];
  cout<<"slowPointer init point address: "<<slowPointer<<endl;
  int *fastPointer = &arr[0];
  cout<<"fastPointer init point address: "<<fastPointer<<endl;
  for (int i = 0; i < 5; ++i) {
    slowPointer++;
    fastPointer += 2;
    cout<<"i = "<<i<<endl;
    cout<<"slowPointer point address: "<<slowPointer<<endl;
    cout<<"slowPointer -> "<<*slowPointer<<endl;
    cout<<"fastPointer point address: "<<fastPointer<<endl;
    cout<<"fastPointer -> "<<*fastPointer<<endl;
  }
  return 0;
}

// slowPointer init point address: 0x7ffee3c63250
// fastPointer init point address: 0x7ffee3c63250
// i = 0
// slowPointer point address: 0x7ffee3c63254
// slowPointer -> 1
// fastPointer point address: 0x7ffee3c63258
// fastPointer -> 2
// i = 1
// slowPointer point address: 0x7ffee3c63258
// slowPointer -> 2
// fastPointer point address: 0x7ffee3c63260
// fastPointer -> 4
// i = 2
// slowPointer point address: 0x7ffee3c6325c
// slowPointer -> 3
// fastPointer point address: 0x7ffee3c63268
// fastPointer -> 6
// i = 3
// slowPointer point address: 0x7ffee3c63260
// slowPointer -> 4
// fastPointer point address: 0x7ffee3c63270
// fastPointer -> 8
// i = 4
// slowPointer point address: 0x7ffee3c63264
// slowPointer -> 5
// fastPointer point address: 0x7ffee3c63278
// fastPointer -> 10

说人话, 龟兔赛跑的故事大家都应该听过, 可以把乌龟🐢比作慢指针, 兔子🐇比作快指针

image

快慢指针的应用场景如下

判断链表是否有环

image
  1. 染色标记, 缺点: 改变了链表的结构, 若链表为只可读的则不可取, 需开辟额外的O(n)空间记录标记信息
var hasCycle = function(head) {
  let res = false
  while (head && head.next) {
    if (head.flag) {
      res = true
      break
    } else {
      head.flag = 1
      head = head.next
    }
  }
  return res
};
  1. 哈希表记录, 缺点: 哈希表需开辟额外的O(n)空间
var hasCycle = function(head) {
  const map = new Map()
  while (head) {
    if (map.get(head)) {
      return true
    } else {
      map.set(head, head)
      head = head.next
    }
  }
  return false
}
  1. 快慢指针, 兔子与乌龟同时在头节点出发, 兔子每次跑两个节点, 乌龟每次跑一个节点, 如果兔子能够追赶到乌龟, 则链表是有环的
image
image
image
image

因为不管有没有环, 以及进环前和进换后耗时都与数据规模成正比, 所以时间复杂度为O(n), 因为只需额外的常数级存储空间记录两个指针, 所以空间复杂度为O(1)

var hasCycle = function(head) {
  let slowPointer = head
  let fastPointer = head
  while (fastPointer && fastPointer.next) {
    slowPointer = slowPointer.next
    fastPointer = fastPointer.next.next
    if (fastPointer === slowPointer) {
      return true
    }
  }
  return false
}

寻找链表的入环节点

image

此题也可用标记法和哈希表法解决, 用快慢指针法解决如下

  • 假设入环之前的长度为L, 入环之后快慢指针第一相遇时快指针比慢指针🐢多跑了N圈, 每一圈的长度为C, 此时快指针🐰在环内离入环节点的距离为C'
  • 此时慢指针🐢走过的距离为: L + C'
  • 此时快指针🐰走过的距离为: L + C' + N * C
  • 因为快指针🐰的速度是慢指针🐢的两倍, 所以有: 2 * (L + C') = L + C' + N * C
  • 整理后得到: (N - 1) * C + (C - C') = L
  • 由此可知, 若此时有两个慢指针🐢同时分别从链表头结点和快慢指针第一次相遇的节点出发, 两者必然会在入环节点相遇
image
image
image
image
var detectCycle = function(head) {
  let slowPointer = head
  let fastPointer = head
  while (fastPointer && fastPointer.next) {
    slowPointer = slowPointer.next
    fastPointer = fastPointer.next.next
    if (slowPointer === fastPointer) {
      slowPointer = head
      while (slowPointer !== fastPointer) {
        slowPointer = slowPointer.next
        fastPointer = fastPointer.next.next
      }
      return slowPointer
    }
  }
  return null
};

寻找重复数

image

此题暴力解法为先排序再寻找重复的数字, 注意不同JavaScript引擎对sort的实现原理不一样, V8 引擎 sort 函数对数组长度小于等于 10 的用插入排序(时间复杂度O(n^2), 空间复杂度O(1)),其它的用快速排序(时间复杂度O(nlogn), 递归栈空间复杂度O(logn)), 参考https://github.com/v8/v8/blob/ad82a40509c5b5b4680d4299c8f08d6c6d31af3c/src/js/array.js

这一题可以利用寻找链表的入环节点的思想, 把数组当成对链表的一种描述, 数组里的每一个元素的值表示链表的下一个节点的索引

如示例1中的[1, 3, 4, 2, 2]

  • 把数组索引为0的元素当成链表的头节点
  • 索引为0的元素的值为1, 表示头节点的下一个节点的索引为1, 即数组中的3
  • 再下一个节点的索引为3, 即为第一个2
  • 再下一个节点的索引为2, 即为4
  • 再下一个节点的索引为4, 即为第二个2
  • 再下一个节点的索引为2, 即为4
  • 此时形成了一个环
  • 而形成环的原因是下一节点的索引一致, 即为重复的数字
image
image
image
image
var findDuplicate = function(nums) {
  let slowPointer = 0
  let fastPointer = 0
  while (true) {
    slowPointer = nums[slowPointer]
    fastPointer = nums[nums[fastPointer]]
    if (slowPointer == fastPointer) {
      let _slowPointer = 0
      while (nums[_slowPointer] !== nums[slowPointer]) {
        slowPointer = nums[slowPointer]
        _slowPointer = nums[_slowPointer]
      }
      return nums[_slowPointer]
    }
  }
};

删除链表的倒数第N个元素

image

要删除链表的倒数第N个元素, 需要找到其倒数第N + 1个元素, 让这个元素的next指向它的下下一个节点

image

此题可利用两次正向遍历链表, 或者一次正向遍历的同时记录前节点, 然后再反向遍历

题目的进阶要求只使用一趟扫描, 利用快慢指针可实现

创建虚拟头节点, 让快指针🐰向前移动N + 1个节点, 然后两个指针以同样的速度直至快指针🐰移动至尾结点, 此时慢指针🐢移动到的位置即为被删除的指针前面的一个指针

image
image
image
image
image
image
var removeNthFromEnd = function(head, n) {
  const dummy = new ListNode(null)
  dummy.next = head
  let slowPointer = dummy
  let fastPointer = dummy
  while (n-- > -1) {
    fastPointer = fastPointer.next
  }
  while (fastPointer !== null) {
    slowPointer = slowPointer.next
    fastPointer = fastPointer.next
  }
  slowPointer.next = slowPointer.next.next
  return slowPointer === dummy ? slowPointer.next : head
};

回文链表

image
  1. 把链表变成双向链表, 并从两端向中间比较

时间复杂度为O(n), 因为要存储prev指针, 所以空间复杂度为O(n)

var isPalindrome = function(head) {
  if (head === null) {
    return true
  } else {
    let headPointer = head
    let tailPointer = head
    while (tailPointer.next) {
      tailPointer.next.prev = tailPointer
      tailPointer = tailPointer.next
    }
    while(headPointer !== tailPointer) {
      if (headPointer.next !== tailPointer) {
        if (headPointer.val === tailPointer.val) {
          headPointer = headPointer.next
          tailPointer = tailPointer.prev
        } else {
          return false
        }
      // 考虑偶数链表
      } else {
        return headPointer.val === tailPointer.val
      }
    }
    return true
  }
};
  1. 快慢指针
  • 慢指针🐢依次访问下一个节点, 并翻转链表
  • 快指针🐰依次访问下下一个节点, 直到快指针🐰没有下一个节点(奇数链表)或者下下一个节点(偶数链表), 此时已完成了前半截链表的翻转
  • 依次比较前半截链表和后半截链表节点的值
image
image
image
image
image

时间复杂度O(n), 空间复杂度O(1)

var isPalindrome = function(head) {
  if (head === null) {
    return true
  } else if (head.next === null) {
    return true
  } else {
    let slowPointer = head
    let fastPointer = head
    let _head = null
    let temp = null
    // 找到中间节点, 并翻转前半截链表,
    // 让_head指向翻转后的前半截链表的头部,
    // 让slow指向后半截链表的头节点
    while (fastPointer && fastPointer.next) {
      _head = slowPointer
      slowPointer = slowPointer.next
      fastPointer = fastPointer.next.next
      _head.next = temp
      temp = _head
    }
    // 奇数链表跳过最中间的节点
    if (fastPointer) {
      slowPointer = slowPointer.next
    }
    while (_head) {
      if (_head.val !== slowPointer.val) {
        return false
      }
      _head = _head.next
      slowPointer = slowPointer.next
    }
    return true
  }
};

快乐数

image
  1. 循环并缓存每次获取位的平方和, 如果已缓存过, 就退出循环, 判断退出循环后是否为1, 缺点: 需开辟O(n)的存储空间
var isHappy = function(n) {
  const memory = {}
  while (n !== 1) {
    function getBitSquareSum (n) {
      let sum = 0
      while (n !== 0) {
        const bit = n % 10
        sum += bit * bit
        n = parseInt(n / 10)
      }
      return sum
    }
    n = getBitSquareSum(n)
    if (memory[n] === undefined) {
      memory[n] = 1
    } else {
      break
    }
  }
  return n === 1
};
  1. 慢指针🐢获取一次每位的平方和, 快指针🐰获取两次每位的平方和, 当两个指针值一样时判断其是否为1

如37这个数, 对其反复的求每位的平方和会进入循环, 而且进入循环时其值不为1

image
image
image
image
image
image
image
image
image
var isHappy = function (n) {
  let slowPointer = n
  let fastPointer = n
  function getBitSquareSum (n) {
    let sum = 0
    while (n !== 0) {
      const bit = n % 10
      sum += bit * bit
      n = parseInt(n / 10)
    }
    return sum
  }
  do {
    slowPointer = getBitSquareSum(slowPointer)
    fastPointer = getBitSquareSum(getBitSquareSum(fastPointer))
  } while (slowPointer !== fastPointer)
  return slowPointer === 1
}

总结

利用快慢指针创造的差值, 可节省内存空间, 减少计算次数, 常用于链表数据结构和判断循环



快慢指针, 一对快乐的指针, just be happy!

image
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容