數據結構與算法的重溫之旅(五)——如何運用鏈表

上一篇文章可能講了太多偏向理論的東西,如今就利用理論來實踐一下。本文精選了八道經典的鏈表實際運用的問題。若是如今都還不能手寫一個鏈表的話建議去上一章按照示例代碼重複幾遍。面試

1、單鏈表實現迴文字符串問題

什麼是迴文字符串呢?即正着讀反着讀都同樣,好比abbba這個字符串就是一個迴文字符串。那若是在面試中別人問了這個問題該如何經過鏈表來解決了?請你們思考一段時間,想好以後再看下面答案:算法

class linkedList {
    constructor() {
        this.head = null
    }
    isPalindromeString () {
        let prev = null;
        let slow = this.head;
        let fast = this.head;
        while (fast && fast.next) {
            fast = fast.next.next;
            let next = slow.next;
            slow.next = prev;
            prev = slow;
            slow = next;
        }

        if (fast) {
            slow = slow.next;
        }

        while (slow) {
            if (slow.element !== prev.element) {
                return false;
            }
            slow = slow.next;
            prev = prev.next;
        }

        return true;
    }
}

let test = new linkedList()複製代碼

在這裏首先運用兩個指針,一個快指針和一個慢指針。快指針跑兩步而慢指針跑一步,每一步慢指針都將原來結點的指向而指向前一個地方。當快指針到了邊界後就中止遍歷,這時須要判斷整個鏈表是奇數仍是偶數,奇數的話則慢指針走一步,以後經過慢指針記錄的當前位置再從新遍歷鏈表,看是否相等。下面有個測試用例,只需把head值改掉就能夠了:緩存

this.head = {
            element: 'a',
            next: {
                element: 'b',
                next: {
                    element: 'c',
                    next: {
                        element: 'c',
                        next: {
                            element: 'b',
                            next: {
                                element: 'a',
                                next: null
                            }
                        }
                    }
                }
            }
        }複製代碼

2、單鏈表反轉

顧名思義,就是將整個鏈表反轉過來。若是用雙向鏈表來作則十分的簡單,可是本題是單鏈表來反轉,看答案以前請思考一下:bash

reserveLinked () {
    let prev = null
    let currNode = this.head
    while (currNode.next) {
        let state = true // 判斷邊界調節
        let next = currNode.next //存儲下一個結點
        // 鏈表指針反轉
        currNode.next = prev 
        prev = currNode
        // 邊界判斷
        if (next.next == null) {
            next.next = currNode
            state = false
        }
        currNode = next
        if (!state) break
    }
}複製代碼

這題的解決思路是遍歷鏈表的是時候將每一個結點的指向都取反,這樣就能夠不用新建一個鏈表就能實現。數據結構

3、鏈表中環的檢測

鏈表中的環的檢測顧名思義就是檢測一條鏈表內是否有環。在看答案以前前思考如何作:post

hasCircle () {
    let fastIndex = this.head
    let slowIndex = this.head
    let currIndex = this.head
    while (currIndex.next) {
        if (fastIndex.next && fastIndex.next.next) {
            fastIndex = fastIndex.next.next
        }
        else {
            return false
        }
        slowIndex = slowIndex.next
        if (slowIndex === fastIndex) {
            return true
        }
    }
}複製代碼

本題的作法主要是經過兩個指針來實現的:一個快指針一個慢指針。快指針走兩步慢指針走一步。恰好到環的入口的時候這兩個指針會恰好指向環的入口。下面有一個環的測試數據,能夠拿這個來測試:測試

this.circleLink = {
    element: 'a',
    next: {
        element: 'b',
        next: {
            element: 'c',
            next: {
                element: 'd',
                next: null
            }
        }
    }
}
let temp = this.circleLink
while (temp.next) {
    temp = temp.next
    if (!temp.next) {
        temp.next = this.circleLink
        break
    }
}
this.head = {
    element: 'head',
    next: this.circleLink
}複製代碼

4、兩個有序的鏈表合併

鏈表裏的另外一個比較常見的操做是鏈表合併,簡單點就是兩個有序的鏈表合併成一個有序鏈表。其實用雙向鏈表來作的話可能更加簡單。在看作法以前先思考一下:ui

class Node {
    constructor (element) {
        this.element = element
        this.next = null
    }
}
class linkedList {
    constructor() {
        this.firstLink = {
            element: 1,
            next: {
                element: 3,
                next: {
                    element: 5,
                    next: {
                        element: 7,
                        next: {
                            element: 9,
                            next: null
                        }
                    }
                }
            }
        }
        this.secondLink = {
            element: 2,
            next: {
                element: 4,
                next: {
                    element: 6,
                    next: {
                        element: 8,
                        next: null
                    }
                }
            }
        }
    }
    // 添加結點
    insertNode (newElement, item) {
        let newNode = new Node(newElement)
        if (this.secondLink == null) {
            this.secondLink = newNode
        }
        else {
            let currentNode = this.findPrevious(item)
            if (currentNode) {
                newNode.next = currentNode.next
                currentNode.next = newNode
            }
            else {
                newNode.next = this.secondLink
                this.secondLink = newNode
            }
        }
    }
    // 尋找上一個結點
    findPrevious (item) {
        let currNode = this.secondLink
        while (currNode.next != null && currNode.next.element !== item) {
            currNode = currNode.next
            if (currNode.next == null && currNode.element !== item) {
                currNode = null
                break
            }
        }
        return currNode
    }
    // 鏈表合併
    linkedAdd () {
        let firstIndex = this.firstLink
        let secondIndex = this.secondLink
        while (firstIndex) {
            if (firstIndex.element < secondIndex.element) {
                this.insertNode(firstIndex.element, secondIndex.element)
                firstIndex = firstIndex.next
            }
            else if (secondIndex.next == null) {
                secondIndex.next = {
                    element: firstIndex.element,
                    next: null
                }
                firstIndex = firstIndex.next
                secondIndex = secondIndex.next
            }
            else {
                secondIndex = secondIndex.next
            }
        }
        return this.secondLink
    }
}複製代碼

這裏面假設firstLink鏈表爲最深的鏈表,咱們以他來作循環判斷條件。設定兩個變量firstIndex和secondIndex來記錄兩個鏈表的初始結點,當第一個鏈表的結點值小於第二個鏈表的結點值的時候就將第一個鏈表的結點值插入到第二個鏈表當前結點值的後面,而後firstIndex就等於下一個結點值。若是secondIndex這個結點值遍歷完後,則將firstLink鏈表剩下的結點所有插入到secondLink鏈表裏。最終secondLink是合併後的鏈表。this

5、刪除鏈表倒數第n個結點

若是被刪除的鏈表是個雙向鏈表的話則十分的簡單,本題的思路主要在於用雙指針實現,方法以下:spa

class linkedList {
    constructor() {
        this.head = {
            element: 'a',
            next: {
                element: 'b',
                next: {
                    element: 'c',
                    next: {
                        element: 'd',
                        next: {
                            element: 'e',
                            next: {
                                element: 'f',
                                next: {
                                    element: 'g',
                                    next: null
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    deletePoint (val) {
        let deep = 0
        let index = 0
        let fastIndex = this.head
        let slowIndex = this.head
        while (fastIndex) {
            ++deep
            fastIndex = fastIndex.next
        }
        if (deep < val) {
            throw '當前鏈表長度沒有這麼長'
        }
        else if (val === 0) {
            throw '必須傳大於0的數'
        }
        while (index < deep - val) {
            slowIndex = slowIndex.next
            ++index
        }
        this.removeNode(slowIndex.element)
    }
    removeNode (item) {
        let currNode = this.findPrevious(item)
        if (currNode.next != null) {
            currNode.next = currNode.next.next
        }
        else if (currNode && !currNode.next) {
            this.head = this.head.next
        }
    }
    findPrevious (item) {
        let currNode = this.head
        while (currNode.next != null && currNode.next.element !== item) {
            currNode = currNode.next
        }
        return currNode
    }
}複製代碼

先快指針找出當前鏈表的長度,而後總長度減去要刪除的倒數下標就是當前要刪除的順數下標。

6、求鏈表的中間結點

這道題就是第一道題裏的關鍵解法,就是用兩個指針,一個快指針和慢指針。快指針走兩步慢指針走一步,當快指針走完的時候慢指針恰好走到中間:

centerPoint () {
    let fastIndex = this.head
    let slowIndex = this.head
    while (fastIndex) {
        if (fastIndex.next == null || fastIndex.next.next == null) {
            return slowIndex.element
        }
        fastIndex = fastIndex.next.next
        slowIndex = slowIndex.next
    }
}複製代碼

7、LRU緩存淘汰算法

LRU全稱實際上是最近最少使用,計算機中一般用這種方法裏提升緩存的使用率。思想是這樣的,若想讀取一個3,發現緩存沒有則存入緩存,此時鏈表爲(3)。讀取一個4,發現緩存沒有則存入緩存,此時鏈表爲(4,3)。讀取一個5,發現緩存沒有則存入緩存,此時鏈表爲(5,4,3)。讀取一個3,發現緩存存在則直接讀緩存,3在鏈表中被提早,得(3,5,4)。讀取一個2,發現緩存中沒有,可是緩存已滿,去掉最後一個結點以後插入鏈表,得(2,3,5)。具體實現以下:

class Node {
    constructor (element) {
        this.element = element
        this.next = null
    }
}

// 鏈表類
class linkedList {
    constructor () {
        this.countLength = 0 // 鏈表長度
        this.head = null // 頭節點
    }
    find (item) {
        let temp = this.head
        while (temp) {
            if (temp.next && temp.next.element === item) {
                return temp
            }
            temp = temp.next
        }
        return null
    }
    LRU (item) {
        let newPoint = new Node(item)
        let hasPoint = this.find(item)
        if (hasPoint == null) {
            newPoint.next = this.head
            this.head = newPoint
            if (this.countLength <= 5) {
                this.countLength++
            }
            else {
                let temp = this.head
                while (temp.next) {
                    if (temp.next.next == null) {
                        temp.next = null
                    }
                    temp = temp.next
                }
            }
        }
        else {
            let temp = hasPoint.next
            if (hasPoint.next.next) {
                hasPoint.next = hasPoint.next.next
            }
            else {
                hasPoint.next = null
            }
            temp.next = this.head
            this.head = temp
        }
    }
}複製代碼

8、約瑟夫問題

最後這道題是屬於進階類型的。約瑟夫問題的意思是這樣的,N我的圍成一圈,從第一我的開始報數,報到m的人出圈,剩下的人繼續從1開始報數,報到m的人出圈;如此往復,直到全部人出圈,求最後一我的的編號。約瑟夫問題是循環鏈表裏的經典問題,在看答案以前請思考一下:


class Node {
    constructor(element) {
        this.element = element
        this.next = null
        this.state = false
    }
}
class linkedList {
    constructor() {
        this.head = null
        this.circleLength = 0
    }
    newCircle (length) {
        let temp = length
        this.circleLength = temp
        let curr = this.head
        while (length >= 0) {
            let newNode = new Node(temp - length + 1)
            if (curr == null) {
                this.head = newNode
                curr = this.head
            }
            else if (length > 0) {
                curr.next = newNode
                curr = curr.next
            }
            else {
                curr.next = this.head
            }
            --length
        }
    }
    josephQuestion (n, m) {
        let index = 1
        let step = 1
        this.newCircle(n)
        let curr = this.head
        while (index <= this.circleLength) {
            if (step !== m) {
                if (!curr.state) {
                    ++step
                }
                curr = curr.next
            }
            else {
                if (curr.state) {
                    curr = curr.next
                    continue
                }
                else {
                    if (this.circleLength === index && !curr.state) {
                        return curr.element
                    }
                    step = 1
                    ++index
                    curr.state = true
                    curr = curr.next
                }
            }
        }
    }
}

let test = new linkedList()
test.josephQuestion(41, 3)複製代碼

本算法的思路是先新建一個循環鏈表,每n個數就標記爲true,到最後index等於鏈表長度的時候則表示已經標記了index-1個數了,這個時候就只需把最後剩下的返回就能夠了。其實能夠直接刪除掉結點,只不過我嫌麻煩。這道題不用鏈表的話它是由一個算法的,能夠利用遞歸來寫,遞歸公式以下:

f(1) = 0

f(n) = (f(n-1)+q)%n

非鏈表的具體解法能夠參照一下這篇文章:非鏈表解法

上一篇文章:數據結構與算法的重溫之旅(四)——鏈表​​​​​​​

下一篇文章:數據結構與算法的重溫之旅(六)——棧​​​​​​​ 

相關文章
相關標籤/搜索