leetcode 鏈表相關題目解析

image

前言

本文基於leetcode的探索鏈表卡片所編寫。遺憾的是, 個人卡片的進度爲80%, 依然沒有所有完成。我在探索卡片的時候, 免不了谷歌搜索。而且有一些題目, 個人解答可能不是最優解。敬請見諒。算法

關於鏈表

鏈表

鏈表屬於比較簡單的數據結構, 在這裏我在過多的贅述。值的注意的是, 本文都是基於單鏈表的, 雙鏈表的設計我尚未實現。數據結構

常見的套路

關於鏈表的算法題目, 我本身總結了如下幾種套路, 或者說常見的手段this

  1. 同時保有當前節點的指針, 以及當前節點的前一個節點的指針。
  2. 快慢指針, fast指針的移動速度是slow指針的兩倍, 若是鏈表成環那麼fast和slow必然會相遇。
  3. 虛假的鏈表頭, 經過 new ListNode(0), 建立一個虛假的頭部。獲取真正鏈表只需返回head.next(這在須要生成一個新鏈表的時候頗有用)。
  4. 同時保有當前鏈表的尾部的指針, 以及頭部的節點指針。
  5. 善用while循環。
  6. 鏈表的頭部和尾部是鏈表比較特殊的節點, 須要注意區別對待

設計單鏈表

原題的地址, 我在原題的基礎使用了TypeScript模擬實現了鏈表。

鏈表須要擁有如下幾種方法:spa

  • get, 根據鏈表的索引獲取鏈表節點的value
  • addAtTail, 添加一個節點到鏈表的尾部

addAtTail

  • addAtHead, 添加一個節點到鏈表的頭部

addAtHead

  • addAtIndex, 添加一個節點到鏈表的任意位置

addAtIndex

  • deleteAtIndex, 刪除任意位置的節點

deleteAtIndex

// 定義鏈表節點類以及鏈表類的接口
interface LinkedListNodeInterface {
  val: number;
  next: LinkedListNode;
}

interface LinkedListInterface {
  head: LinkedListNode;
  length: number;
  get(index: number): number;
  addAtHead(val: number): void;
  addAtTail(val: number): void;
  addAtIndex(index: number, val: number): void;
  deleteAtIndex(index: number): void
}

class LinkedListNode implements LinkedListNodeInterface {
  constructor (
    public val: number = null,
    public next: LinkedListNode = null
  ) {}
}

class LinkedList implements LinkedListInterface {
  constructor (
    public head: LinkedListNode = null,
    public length: number = 0
  ) {}

  /**
   * 經過while循環鏈表, 同時在循環的過程當中使用計數器計數, 既能夠實現
   */
  public get(index: number): number {
    if (index >= 0 && index < this.length) {
      let num: number = 0
      let currentNode: LinkedListNode = this.head
      while (index !== num) {
        num += 1
        currentNode = currentNode.next
      }
      return currentNode.val
    }
    return -1
  }

  /**
   * 將新節點的next屬性指向當前的head, 將head指針指向新節點
   */
  public addAtHead (val: number): void {
    let newNode: LinkedListNode = new LinkedListNode(val, this.head)
    this.head = newNode
    this.length += 1
  }

  /**
   * 將鏈表尾部的節點的next屬性指向新生成的節點, 獲取鏈表尾部的節點須要遍歷鏈表
   */
  public addAtTail(val: number): void {
    let newNode: LinkedListNode = new LinkedListNode(val, null)
    let currentNode: LinkedListNode = this.head
    if (!this.head) {
      this.head = newNode
    } else {
      while (currentNode && currentNode.next) {
        currentNode = currentNode.next
      }
      currentNode.next = newNode
    }
    this.length += 1
  }

  /**
   * 這裏須要須要運用技巧, 遍歷鏈表的同時, 同時保留當前的節點和當前節點的前一個節點的指針
   */
  public addAtIndex(index: number, val: number): void {
    if (index >= 0 && index <= this.length) {
      let newNode: LinkedListNode = null
      if (index === 0) {
        // 若是index爲0, 插入頭部須要與其餘位置區別對待
        this.addAtHead(val)
      } else {
        let pointer: number = 1
        let prevNode: LinkedListNode = this.head
        let currentNode: LinkedListNode = this.head.next
        while (pointer !== index && currentNode) {
          prevNode = currentNode
          currentNode = currentNode.next
          pointer += 1
        }
        // 中間插入
        newNode = new LinkedListNode(val, currentNode)
        prevNode.next = newNode
        this.length += 1
      }
    }
  }

  public deleteAtIndex(index: number): void {
    if (index >= 0 && index < this.length && this.length > 0) {
      if (index === 0) {
        this.head = this.head.next
      } else {
        let pointer: number = 1
        let prevNode: LinkedListNode = this.head
        let currentNode: LinkedListNode = this.head.next
        // 值得注意的是這裏的判斷條件使用的是currentNode.next
        // 這意味着currentNode最遠到達當前鏈表的尾部的節點,而非null
        // 這是由於prevNode.next = prevNode.next.next, 咱們不能取null的next屬性
        while (pointer !== index && currentNode.next) {
          prevNode = currentNode
          currentNode = currentNode.next
          pointer += 1
        }
        prevNode.next = prevNode.next.next
      }
      this.length -= 1
    }
  }
}

環形鏈表

原題地址, 將環形鏈表想象成一個跑道, 運動員的速度是肥宅的兩倍, 那麼通過一段時間後, 運動員必然會超過肥宅一圈。這個時候, 運動員和肥宅必然會相遇。快慢指針的思想就是源於此。
/**
 * 判斷鏈表是否成環
 */
function hasCycle (head: LinkedListNode): boolean {
  // 快指針
  let flst = head
  // 慢指針
  let slow = head

  while (flst && flst.next && flst.next.next) {
    flst = flst.next.next
    slow = flst.next
    if (flst === slow) {
      return true
    }
  }
  return false
}

環形鏈表II

原題地址, 在環形鏈表的基礎上, 咱們須要獲取環形鏈表的入口。一樣使用快慢指針實現。可是值的注意的是。鏈表可能只有 部分成環, 這意味着。快慢指針相遇的點, 可能並非環的入口。

image

慢節點的運動距離爲, a + b - c設計

快節點的運動距離爲, 2b + a - c指針

快節點的運動距離是慢節點的兩倍。能夠得出這個公式 2(a + b - c) = 2b + a - c, 化簡 2a - 2c = a - c, 能夠得出 a = c。code

相遇的點距離入口的距離, 等於起點距離入口距離遞歸

function hasCycleEntrance (head: LinkedListNode): LinkedListNode | Boolean {
  // 快指針
  let flst = head
  // 慢指針
  let slow = head
  while (flst && flst.next && flst.next.next) {
    flst = flst.next.next
    slow = flst.next
    // 快指針移動到入口,而且速度變爲1
    if (flst === slow) {
      // 變道起點
      flst = head
      // a 和 c距離是一致的
      // 每一次移動一個next,必然會在入口出相遇
      while (flst !== slow) {
        flst = flst.next
        slow = slow.next
      }
      return flst
    }
  }
  return false
}

相交鏈表

原題地址, 相交鏈表的解題思路依然是使用快慢指針。思路見下圖, 將a鏈的tail連接到b鏈head, 若是a與b鏈相交, 那麼就會成環。套用上一題的思路就能夠獲取到a與b的交點。

image

function hasCross (headA: LinkedListNode, headB: LinkedListNode): LinkedListNode {
  if (headA && headB) {
        
    // 自身相等的狀況下
    if (headA === headB) {
        return headA
    }
    
    // a鏈的tail連上b鏈的head
    let lastA: LinkedListNode = headA
    let lastB: LinkedListNode = headB
    
    while (lastA && lastA.next) {
        lastA = lastA.next
    }
    
    lastA.next = headB
    
    let fast: LinkedListNode = headA
    let slow: LinkedListNode = headA
    
    while (fast && fast.next && fast.next.next) {
        slow = slow.next
        fast = fast.next.next
        
        if (slow === fast) {
            fast = headA
            
            while (slow !== fast) {
                slow = slow.next
                fast = fast.next
                
                if (slow === fast) {
                    lastA.next = null
                    return slow
                }
            }
            lastA.next = null
            return fast
        } 
    }
    lastA.next = null  
    return null  
  } 
}

刪除鏈表的倒數第N個節點

原題地址, 這裏我使用的是比較笨的辦法, 先計算鏈表的長度, 獲取正序的時n的大小。而後按照刪除鏈表中某一個節點的方法進行刪除便可。須要區分刪除的是不是第一個。

反轉鏈表

原題地址, 常見的反轉鏈表的方式就是使用遞歸或者迭代的方式。反轉鏈表的過程, 若是拆解開來, 能夠分爲下面幾步。從拆解的過程能夠看出, 反轉鏈表的過程就是依次將head的後面的節點, 放到鏈表的頭部。

1 -> 2 -> 3 -> 4 -> null索引

2 -> 1 -> 3 -> 4 -> null接口

3 -> 2 -> 1 -> 4 -> null

4 -> 3 -> 2 -> 1 -> null

const reverseList = function(head: LinkedListNode): LinkedListNode {
    
    let newHead: LinkedListNode = head
    let current: LinkedListNode = head
    
    // current的指針將會向後移動
    function reverse () {
        let a = current.next
        let b = current.next.next
        a.next = head
        current.next = b
        head = a
    }
    
    while (current && current.next) {
       reverse() 
    }
    return head
};

刪除鏈表中的節點

原題地址。我使用的也是笨辦法。因爲鏈表頭部特殊性,刪除頭部時須要進行遞歸(由於在第一次刪除頭部的節點後, 新的頭部也有多是知足刪除條件的節點)。對於其餘位置的節點使用常規辦法便可。
function removeElements (head: LinkedListNode, val: number): LinkedListNode {

  /**
   * 刪除鏈表的頭部
   */
  function deleteHead () {
    head = head.next
    if (head && head.val === val) {
        deleteHead()
    }
  } 

  if (head) {
      if (head.val === val) {
          // 遞歸刪除頭部的節點
          deleteHead()
      }

      if (head && head.next) {
        let prevNode = head
        let currentNode = head.next

        while (currentNode) {
          // 刪除鏈表中間符合條件的節點
            if (currentNode.val === val) {
                prevNode.next = currentNode.next
                currentNode = currentNode.next
            } else {
                prevNode = prevNode.next
                currentNode = currentNode.next
            }
        }
      }  
  }
  return head
}

奇偶鏈表

原題地址, 對於這道題目咱們就須要運用上以前提到的兩種套路(同時保留頭部的指針以及當前的節點的指針和虛假的頭部)
function oddEvenList (head: LinkedListNode): LinkedListNode {
  let oddHead: LinkedListNode = new LinkedListNode(0)
  let evenHead: LinkedListNode = new LinkedListNode(0)
  let oddTail: LinkedListNode = oddHead
  let evenTail: LinkedListNode = evenHead
  let index: number = 1
  
  while (head) {
      // 連接不一樣奇偶兩條鏈
      // 這裏默認開頭是1,因此index從1開始
      if (index % 2 !== 0) {
          oddTail.next = head
          oddTail = oddTail.next
      } else {
          evenTail.next = head
          evenTail = evenTail.next
      }
      head = head.next
      index += 1
  }
  
  // 偶數鏈的結尾是null,由於是尾部
  evenTail.next = null
  // evenHead.next忽略到假頭
  oddTail.next = evenHead.next
  
  // oddHead.next忽略到假頭
  return oddHead.next
}

迴文鏈表

原題地址, 何爲所謂的迴文鏈表, 1 -> 2 -> 1 或者 1 -> 1 亦或則 1 -> 2 -> 2 -> 1 能夠被稱爲迴文鏈表。迴文鏈表若是長度爲奇數, 那麼除去中間點, 兩頭的鏈表應當是在反轉後應當是相同的。若是是偶數個, 鏈表的一半通過反轉應該等於前半部分。固然有兩種狀況須要特殊考慮, 好比鏈表爲 1 或者 1 -> 1 的狀況下。在排除了這兩種特點狀況後, 能夠經過快慢指針獲取鏈表的中點(fast的速度是slow的兩倍)。反轉中點以後的鏈表後, 而後從頭部開始和中點開始對比每個節點的val。
function isPalindrome (head: LinkedListNode): boolean {
  if (!head) {
    return true
  }

  // 經過快慢指針獲取中點
  let fast: LinkedListNode = head
  let slow: LinkedListNode = head

  // 鏈表中點
  let middle = null

  // 循環結束後慢節點就是鏈表的中點
  while (fast && fast.next && fast.next.next) {
      fast = fast.next.next
      slow = slow.next
  }

  // 一個和兩個的狀況
  if (fast === slow) {
      if (!fast.next) {
          return true
      } else if ( fast.val === fast.next.val ) {
          return true
      } else {
          return false
      }
  }

  // middle保持對中點的引用
  // slow日後移動
  middle = slow

  // 反轉中點之後的鏈表
  function reverse () {
      let a = slow.next
      let b = slow.next.next
      a.next = middle
      slow.next = b
      middle = a
  }

  while (slow && slow.next) {
      reverse()
  }

  // 從頭部和中點開始對比
  while (head && middle) {
      
      if (head.val !== middle.val) {
          return false
      }
      
      head = head.next
      middle = middle.next
      
  }

  return true    
}

合併兩個有序鏈表

原題地址, 對於建立一個新的鏈表使用的思路就是建立一個虛假的頭部, 這道題目的解答也是如此。以及同時保留頭部的指針以及尾部的指針, 不管是添加節點仍是返回鏈表都會很是方便。
function mergeTwoLists (l1: LinkedListNode, l2: LinkedListNode): LinkedListNode {
  // 頭部的引用
  let newHead: LinkedListNode = new LinkedListNode(0)
  // 尾部的引用
  let newTail: LinkedListNode = newHead
  
  while (l1 && l2) {
    if (l1.val < l2.val) {
        newTail.next = l1
        l1 = l1.next
    } else {
        newTail.next = l2
        l2 = l2.next
    }
    // 始終指向尾部
    newTail = newTail.next
  }
  
  if (!l1) {
    newTail.next = l2
  }
  
  if (!l2) {
    newTail.next = l1
  }
  
  // 忽略虛假的頭部
  return newHead.next
}

鏈表相加

原題地址。生成虛假的頭部後, 兩個鏈表兩兩相加, 注意 進位以及 保留位便可。若是val不存在, 取0。

(2 -> 4 -> 3) + (5 -> 6 -> 7 -> 8)

0343

  • 8765

__
9108

function addTwoNumbers (l1: LinkedListNode, l2: LinkedListNode): LinkedListNode {
    let newHead: LinkedListNode = new LinkedListNode(0)
    let newTail: LinkedListNode = newHead
    // carry是進位,8 + 8 = 16 ,進位爲1
    let carry: number = 0
    while (l1 || l2) {
        let a: number = l1 ? l1.val : 0
        let b: number = l2 ? l2.val : 0
        // val是保留的位
        let val: number = (a + b + carry) % 10
        carry = Math.floor((a + b + carry) / 10)
        let newNode = new LinkedListNode(val)
        newTail.next = newNode
        newTail = newTail.next
        if (l1) {
            l1 = l1.next
        }
        if (l2) {
            l2 = l2.next
        }
    }
    if (carry !== 0) {
        // 注意最後一位的進位
        let newNode: LinkedListNode = new LinkedListNode(carry)
        newTail.next = newNode
        newTail = newTail.next
    }
    
    return newHead.next
}

旋轉鏈表

原題地址, 經過觀察可知, 所謂的旋轉就是依次將鏈表尾部的節點移動到鏈表的頭部, 同時能夠發現若是旋轉的次數等於鏈表的長度。鏈表是沒有發生改變的。因此經過提早計算出鏈表的長度, 能夠減小旋轉的次數。

輸入: 0-> 1-> 2 -> NULL

向右旋轉 1 步: 2 -> 0 -> 1 -> NULL
向右旋轉 2 步: 1 -> 2 -> 0 -> NULL
向右旋轉 3 步: 0 -> 1 -> 2 -> NULL
向右旋轉 4 步: 2 -> 0 -> 1 -> NULL

var rotateRight = function(head, k) {
    if (!head || !head.next) {
        return head
    }
    
    let length = 0
    let c = head
    // 計算出鏈表的長度
    while (c) {
        length += 1
        c = c.next
    }
    
    // 將鏈表的尾部移動到鏈表的頭部
    // 鏈表尾部的前一個next指向null
    function rotate () {
        let a = head
        let b = head.next
        while (b && b.next) {
            a = b
            b = b.next
        }
        b.next = head
        head = b
        a.next = null
    }
    
    // 避免沒有必要的選裝
    let newK = k % length
    let index = 1
    
    while (index <= newK) {
        rotate()
        index += 1
    }
    
    return head
};
相關文章
相關標籤/搜索