嘿,掘友们!今天我们来了解并实现数据结构 —— 链表。
要存储多个元素,数组可能是最常用的数据结构。但是这种数据结构有一个缺点,数组的大小是固定的,从数组的起点或中间插入或移除的成本很高,因为需要移动元素。
链表存储有序的元素集合,不同于数组的是,链表中的元素在内存中并不是连续放置的。每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针或链接)组成。下图是一个链表的结构。
相比数组,链表的一个好处在于,添加或移除元素的时候不需要移动其他元素。然而,链表需要使用指针。在数组中,我们可以直接访问任何位置的任何元素,而想要访问链表中间的一个元素,则需要从起点开始迭代链表知道找到所需要的元素。
在实现链表之前,我们先声明和定义属性和方法。
count
存储链表中的元素数量head
头指针equalsFn
比较元素是否相等push(element)
向链表尾部添加一个新元素insert(element, index)
向链表的特定位置插入一个新元素getElementAt(index)
返回链表中的特定位置的元素,如果不存在返回undefinedremove(element)
从链表中移除一个元素removeAt(index)
从链表的特定位置移除一个元素indexOf(index)
返回元素在链表中的索引,如果链表中没有该元素,返回-1isEmpty()
如果链表中不包含任何元素,返回true,否则返回falsesize()
返回链表包含的元素个数print()
返回表示整个链表的字符串定义defaultEquals
函数,作为默认的相等性比较函数。
function defaultEquals(a, b) {
return a === b
}
要表示链表中的元素,我们需要一个助手类,叫做 Node
。Node类表示我们想要添加到链表中的项。
class Node {
constructor(element) {
this.element = element // 元素的值
this.next = undefined // 下一个元素的指针
}
}
创建 LinkedList
类的“骨架”
class LinkedList {
constructor() {
this.count = 0
this.head = undefined
}
}
在尾部添加元素可能有两种场景:链表为空,添加的是第一个元素;链表不为空,向其尾部添加元素
class LinkedList {
constructor() { ... }
push(element) {
const node = new Node(element) // 创建Node项
if(this.head == null) {
this.head = node
} else {
let current = this.head // 指向链表的current变量
while (current.next != null) {
current = current.next
}
current.next = node
}
this.count++
}
}
首先,把 element
作为值传入,创建Node项
。
先实现第一个场景,向空链表添加一个元素,当创建一个 LinkedList
对象时,head
会指向 undefined
。
如果 head
元素为 undefined
或 null
,就意味着在向链表添加第一个元素。因此要做的就是让 head
指向 node
元素。
再来看第二个场景,向一个不为空的链表尾部添加元素。
要在链表的尾部添加元素,就要找到最后一个元素。但我们只有第一个元素的引用,需要循环访问链表,直到最后一项。当 current.next
元素为 undefined
或 null
时,我们就知道到达链表尾部了。然后让当前元素的 next
指向 node
元素。
this.head == null 相当于 this.head === undefined || this.head === null
current.next !=null 相当于 current.next !== undefined || current.next !== null
循环到目标 index
的代码片段在 LinkedList
类的方法中很常见。将这部分逻辑独立为单独的方法,这样就能在不同的地方复用它。
class LinkedList {
constructor() { ... }
push(element) { ... }
getElementAt(index) {
if(index >= 0 && index < this.count) {
let node = this.head
for(let i = 0; i < index && node != null; i++) {
node = node.next
}
return node
}
return undefined
}
}
为了确保我们能迭代链表知道找到一个合法的位置,需要对传入的 index 参数进行合法性验证。如果传入的位置不合法,返回 undefined
,因为这个位置在链表中不存在。
然后,初始化 node
变量,该变量会从链表的第一个元素 head 开始,迭代整个链表知道目标 index
,结束循环时,node
元素将是 index 位置元素的引用。
我们要实现两种移除元素的方法。第一种是从特定位置移除一个元素(removeAt),第二种是根据元素值移除元素(remove)。
我们先实现第一种移除元素的方法,要移除元素存在两种场景:第一种,移除第一个元素,第二种,移除第一个元素以外的元素。
removeAt(index) {
if(index >= 0 && index < this.count) {
if(index === 0) {
this.head = this.head.next
} else {
const previous = this.getElement(index - 1)
const current = previous.next
previous.next = current.next
}
this.count--
return current.element
}
return undefined
}
先看第一种场景:我们从链表中移除第一个元素。想移除第一个元素,让 head
指向链表的第二个元素就实现了。
再看第二种场景:移除除第一个元素以外的元素。我们获取要删除元素的前一个元素。current
引用要删除的元素。将前一个元素的 next
指向要删除元素的 next
,就可以实现了。
移除最后一个元素也通用,previous
引用最后元素的前一个元素,最后一个元素的 next 指向 undefined
,那么将 previous.next = undefined
,就完成了最后一个元素的移除。
我们再来实现移除元素的第二种方法:根据元素值移除元素(remove)。
remove(element) {
const index = this.getElement(element)
return this.removeAt(index)
}
我们复用前面的两种方法 getElement
和 removeAt
,就可以实现。
先获取要删除元素的索引