[leetcode]淺談快慢指針在算法中的應用

天下武功, 無堅不破, 唯快不破 ——— 某大佬

本文為我,leetcode easy player,algorithm(算法)小xuo生在刷題過程中對快慢指針的應用的總結

什么是快慢指針

  1. 快慢說的是移動的速度, 即每次移動的步長的大小
  2. 指針為記錄變量內存地址的變量, 用它能間接訪問變量的值

為了更直觀的了解快慢指針, 請看如下c++demo

在內存中開辟容量為11個整型元素的數組存儲空間

初始化整型快慢指針變量都記錄數組第一個元素的內存地址

在循環(huán)里, 慢指針每次增1, 快指針每次增2

因為c++中指針占4字節(jié)即32位的16進制的內存空間, 所以慢指針記錄的內存地址每次移動4個字節(jié), 而塊指針記錄的內存地址每次移動8個字節(jié)(
注意因為是16進制, 所以快指針從0x7ffee3c632580x7ffee3c63260也是移動了8個字節(jié))

#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

快慢指針的應用場景如下

判斷鏈表是否有環(huán)

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. 快慢指針, 兔子與烏龜同時在頭節(jié)點出發(fā), 兔子每次跑兩個節(jié)點, 烏龜每次跑一個節(jié)點, 如果兔子能夠追趕到烏龜, 則鏈表是有環(huán)的
image
image
image
image

因為不管有沒有環(huán), 以及進環(huán)前和進換后耗時都與數據規(guī)模成正比, 所以時間復雜度為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
}

尋找鏈表的入環(huán)節(jié)點

image

此題也可用標記法和哈希表法解決, 用快慢指針法解決如下

  • 假設入環(huán)之前的長度為L, 入環(huán)之后快慢指針第一相遇時快指針比慢指針??多跑了N圈, 每一圈的長度為C, 此時快指針??在環(huán)內離入環(huán)節(jié)點的距離為C'
  • 此時慢指針??走過的距離為: L + C'
  • 此時快指針??走過的距離為: L + C' + N * C
  • 因為快指針??的速度是慢指針??的兩倍, 所以有: 2 * (L + C') = L + C' + N * C
  • 整理后得到: (N - 1) * C + (C - C') = L
  • 由此可知, 若此時有兩個慢指針??同時分別從鏈表頭結點和快慢指針第一次相遇的節(jié)點出發(fā), 兩者必然會在入環(huán)節(jié)點相遇
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

這一題可以利用尋找鏈表的入環(huán)節(jié)點的思想, 把數組當成對鏈表的一種描述, 數組里的每一個元素的值表示鏈表的下一個節(jié)點的索引

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

  • 把數組索引為0的元素當成鏈表的頭節(jié)點
  • 索引為0的元素的值為1, 表示頭節(jié)點的下一個節(jié)點的索引為1, 即數組中的3
  • 再下一個節(jié)點的索引為3, 即為第一個2
  • 再下一個節(jié)點的索引為2, 即為4
  • 再下一個節(jié)點的索引為4, 即為第二個2
  • 再下一個節(jié)點的索引為2, 即為4
  • 此時形成了一個環(huán)
  • 而形成環(huán)的原因是下一節(jié)點的索引一致, 即為重復的數字
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指向它的下下一個節(jié)點

image

此題可利用兩次正向遍歷鏈表, 或者一次正向遍歷的同時記錄前節(jié)點, 然后再反向遍歷

題目的進階要求只使用一趟掃描, 利用快慢指針可實現

創(chuàng)建虛擬頭節(jié)點, 讓快指針??向前移動N + 1個節(jié)點, 然后兩個指針以同樣的速度直至快指針??移動至尾結點, 此時慢指針??移動到的位置即為被刪除的指針前面的一個指針

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. 快慢指針
  • 慢指針??依次訪問下一個節(jié)點, 并翻轉鏈表
  • 快指針??依次訪問下下一個節(jié)點, 直到快指針??沒有下一個節(jié)點(奇數鏈表)或者下下一個節(jié)點(偶數鏈表), 此時已完成了前半截鏈表的翻轉
  • 依次比較前半截鏈表和后半截鏈表節(jié)點的值
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
    // 找到中間節(jié)點, 并翻轉前半截鏈表,
    // 讓_head指向翻轉后的前半截鏈表的頭部,
    // 讓slow指向后半截鏈表的頭節(jié)點
    while (fastPointer && fastPointer.next) {
      _head = slowPointer
      slowPointer = slowPointer.next
      fastPointer = fastPointer.next.next
      _head.next = temp
      temp = _head
    }
    // 奇數鏈表跳過最中間的節(jié)點
    if (fastPointer) {
      slowPointer = slowPointer.next
    }
    while (_head) {
      if (_head.val !== slowPointer.val) {
        return false
      }
      _head = _head.next
      slowPointer = slowPointer.next
    }
    return true
  }
};

快樂數

image
  1. 循環(huán)并緩存每次獲取位的平方和, 如果已緩存過, 就退出循環(huán), 判斷退出循環(huán)后是否為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這個數, 對其反復的求每位的平方和會進入循環(huán), 而且進入循環(huán)時其值不為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
}

總結

利用快慢指針創(chuàng)造的差值, 可節(jié)省內存空間, 減少計算次數, 常用于鏈表數據結構和判斷循環(huán)



快慢指針, 一對快樂的指針, just be happy!

image
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市嫡秕,隨后出現的幾起案子渴语,更是在濱河造成了極大的恐慌,老刑警劉巖昆咽,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驾凶,死亡現場離奇詭異牙甫,居然都是意外死亡,警方通過查閱死者的電腦和手機调违,發(fā)現死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門窟哺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人技肩,你說我怎么就攤上這事脏答。” “怎么了亩鬼?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵殖告,是天一觀的道長。 經常有香客問我雳锋,道長黄绩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任玷过,我火速辦了婚禮爽丹,結果婚禮上,老公的妹妹穿的比我還像新娘辛蚊。我一直安慰自己粤蝎,他們只是感情好,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布袋马。 她就那樣靜靜地躺著初澎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪虑凛。 梳的紋絲不亂的頭發(fā)上碑宴,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天,我揣著相機與錄音桑谍,去河邊找鬼延柠。 笑死,一個胖子當著我的面吹牛锣披,可吹牛的內容都是我干的贞间。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼雹仿,長吁一口氣:“原來是場噩夢啊……” “哼增热!你這毒婦竟也來了?” 一聲冷哼從身側響起盅粪,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤钓葫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后票顾,有當地人在樹林里發(fā)現了一具尸體础浮,經...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡帆调,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了豆同。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片番刊。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖影锈,靈堂內的尸體忽然破棺而出芹务,到底是詐尸還是另有隱情,我是刑警寧澤鸭廷,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布枣抱,位于F島的核電站,受9級特大地震影響辆床,放射性物質發(fā)生泄漏佳晶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一讼载、第九天 我趴在偏房一處隱蔽的房頂上張望轿秧。 院中可真熱鬧,春花似錦咨堤、人聲如沸菇篡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽驱还。三九已至,卻和暖如春津滞,著一層夾襖步出監(jiān)牢的瞬間铝侵,已是汗流浹背灼伤。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工触徐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狐赡。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓撞鹉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親颖侄。 傳聞我的和親對象是個殘疾皇子鸟雏,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內容