上一篇文章可能讲了太多偏向理论的东西,现在就利用理论来实践一下。本文精选了八道经典的链表实际运用的问题。如果现在都还不能手写一个链表的话建议去上一章按照示例代码重复几遍。
一、单链表实现回文字符串问题
什么是回文字符串呢?即正着读反着读都一样,比如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
}
}
}
}
}
}
二、单链表反转
顾名思义,就是将整个链表反转过来。如果用双向链表来做则十分的简单,但是本题是单链表来反转,看答案之前请思考一下:
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
}
}
这题的解决思路是遍历链表的是时候将每个结点的指向都取反,这样就可以不用新建一个链表就能实现。
三、链表中环的检测
链表中的环的检测顾名思义就是检测一条链表内是否有环。在看答案之前前思考如何做:
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
}
四、两个有序的链表合并
链表里的另一个比较常见的操作是链表合并,简单点就是两个有序的链表合并成一个有序链表。其实用双向链表来做的话可能更加简单。在看做法之前先思考一下:
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是合并后的链表。
五、删除链表倒数第n个结点
如果被删除的链表是个双向链表的话则十分的简单,本题的思路主要在于用双指针实现,方法如下:
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
}
}
先快指针找出当前链表的长度,然后总长度减去要删除的倒数下标就是当前要删除的顺数下标。
六、求链表的中间结点
这道题就是第一道题里的关键解法,就是用两个指针,一个快指针和慢指针。快指针走两步慢指针走一步,当快指针走完的时候慢指针刚好走到中间:
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
}
}
七、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
}
}
}
八、约瑟夫问题
最后这道题是属于进阶类型的。约瑟夫问题的意思是这样的,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
非链表的具体解法可以参照一下这篇文章:非链表解法