数据结构与算法的重温之旅(五)——如何运用链表

上一篇文章可能讲了太多偏向理论的东西,如今就利用理论来实践一下。本文精选了八道经典的链表实际运用的问题。若是如今都还不能手写一个链表的话建议去上一章按照示例代码重复几遍。面试

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

非链表的具体解法能够参照一下这篇文章:非链表解法

上一篇文章:数据结构与算法的重温之旅(四)——链表​​​​​​​

下一篇文章:数据结构与算法的重温之旅(六)——栈​​​​​​​ 

相关文章
相关标签/搜索