链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(也称指针或链接)组成。下图展示了一个链表的结构
相对于传统的数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针,因此实现链表时需要额外注意。在数组中,我们可以直接访问任何位置的任何元素,而要想访问链表中间的一个元素,则需要从起点(表头)开始迭代链表直到找到所需的元素。
简而言之,链表相对于数组方便删除、增加 ,但由于结构的特点,导致查询是需要从头开始。
由于需要对节点查找,我们需要每次都对比两个节点是否相同,为了复用,需要将这个方法抽离出来。
// util.js
export function defaultEquals (a, b) {
return a === b
}
对于每一个节点都是都是相互独立的,使用我们需要实现一个Node Class,这个类具有两个属性, Node.element代表元素的本身,Node.next代表下一个元素/链表尾部。
注意:当知道某一个元素,也就同时知道了它的下一个元素。
// lined-list-models.js
export class Node {
constructor (element) {
this.element = element
this.next = undefined
}
}
现在实现链表,以及相应的方法。
// linkList.js
import { defaultEquals } from "./util";
import { Node } from "./linked-list-models";
export default class LinkedList {
constructor (equalsFn = defaultEquals) {
this.head = undefined
this.count = 0
this.equalsFn = equalsFn
}
// 链表尾部插入
push ( element ) { }
// 指定位置插入新元素
insertAt ( element, position ) { }
// 返回指定位置的元素,不存在返回undefined
getElementAt ( index ) { }
// 移除指定元素,
remove (element) { }
// 查找指定元素的位置,不存在返回-1
indexOf ( element ) { }
// 删除指定位置的元素
removeAt ( position ) { }
// 判断链表是否为空,返回 true / false
isEmpty () { }
// 返回链表的长度
size () { }
// 链表字符串化
toSting () { }
}
1.链表尾部的插入
在向尾部插入的时候需要考虑两种清空,一种是链表为空,一种是链表不为空。不为空的时候我们需要遍历到最后一个元素,然后把新元素链继到它的next(lastNode.next = newNode)。而为空的时候,我们需要把链表的head指向该元素。
// 链表尾部插入
push ( element ) {
const node = new Node( element )
let current
if( this.head === undefined ) { // 链表为空
this.head = node
} else {
current = this.head
while (current.next !== undefined) {
current = current.next
}
current.next = node
}
this.count ++
}
2.指定位置删除元素
在指定位置删除时,需要考虑输入的位置是否合法。然后分2种情况,第一张是删除第一个元素,第二是删除其他元素。
删除的具体思路是,找到需要被删除的当前元素(currentNoode)和它的上一个元素(previousNode),在知道当前元素的时候,也代表了知道它的下一个元素(currentNode.next),然后我们把prviousNode.next指向currentNode.next即可。删除头元素只需要把header 指向 current.next。
-
头部删除元素
-
中间删除元素
-
尾部删除元素
// 删除指定位置的元素
removeAt ( position ) {
if ( position >= 0 && position<this.count ) {
let current = this.head
if (position === 0) {
this.head = current.next
} else {
let previous
for (let index = 0; index < position; index++) {
previous = current
current = current.next
}
previous.next = current.next
}
this.count --
return current.element
}
return undefined
}
由于每次查找都需要遍历元素,所以这里我们可以将方法封装
3. 查找指定位置元素
// 返回指定位置的元素,不存在返回undefined
getElementAt ( position ) {
if( position >=0 && position < this.count){
let node = this.head
for (let index = 0; index < position && node !== undefined; index++) {
node = node.next
}
return node
}
return undefined
}
现在可以将 removeAt方法改造
// 删除指定位置的元素
removeAt ( position ) {
if ( position >= 0 && position<this.count ) {
let current = this.head
if (position === 0) {
this.head = current.next
} else {
let previous = this.getElementAt( position-1 )
current = previous.next
// for (let index = 0; index < position; index++) {
// previous = current
// current = current.next
// }
previous.next = current.next
}
this.count --
return current.element
}
return undefined
}
4. 在任意位置插入元素
这里需要考虑两种情况,一种是在第一个,一种是插入在其他位置。当插入的位置为第一个是,head的指向需要改到刚插入的元素。
// 指定位置插入新元素
insertAt ( element, position ) {
if( position >=0 && position < this.count){
let node = new Node(element)
if (position === 0 ){
let current = this.head
node.next = current
node = this.head
} else {
let previous = this.getElementAt ( position - 1)
let current =previous.next
previous.next = node
node.next = current
}
this.count ++
return true
}
return false
}
5.返回元素的位置
这里依然需要遍历链表。
indexOf ( element ) {
let current = this.head
for (let index = 0; index < this.count && this.count !== undefined; index++) {
if ( current === element ){
return index
}
current = current.next
}
return -1
}
6. 删除指定的元素
我们可以先通过 indexOf 来找到元素的位置,再通过 removeAt在指定的位置删除元素。
// 移除指定元素,
remove (element) {
let position = this.indexOf (element)
return this.removeAt (position)
}
7. toSting 方法
// 链表字符串化
toSting () {
if (this.head == null) {
return '';
}
let objString = `${this.head.element}`;
let current = this.head.next;
for (let i = 1; i < this.size() && current != null; i++) {
objString = `${objString},${current.element}`;
current = current.next;
}
return objString;
}