链表:
要存储多个元素,数组可能是最常用的数据结构。
数组结构的优点是通过下标值存取元素效率非常高。
数组结构的缺点:
- 数组的创建通常需要申请一段连续的内存空间(一整块的内存)。
- 数组的大小是固定的,如果当前数组不能满足容量需求,那么久需要进行扩容(虽然 JS 中数组的大小不是固定的,但是大多数编程语言都是固定的)。
扩容:一般情况是申请一个更大的数组,然后将当前数组中的元素复制过去。- 在一个数组中进行插入和删除元素的成本很高,需要进行大量元素的位移。(比如:一个数组中有 10000 个元素,想要在第 100 位插入一个元素,那么原先在第 100 位及之后的元素都需要向后挪动一位)。
要存储多个元素,另一个选择就是链表。
链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的指针组成。
链表很类似于火车:有一个火车头,火车头会连接第一节车厢;每节车厢就是一个元素,车厢中有乘客(类似于元素本身的数据),还会连接到下一个车厢(指向下一个元素)。
链表的优点和缺点:
链表的优点:
- 内存空间不必是连续的,因此可以充分利用计算机的内存,实现灵活的内存动态管理。
- 链表不必在创建时就确定大小,并且大小可以无限地延伸下去。
- 链表在插入和删除元素时,时间复杂度可以达到O(1),相比数组来说效率高很多。
链表的缺点:链表无法通过下标值直接访问元素,访问任何一个位置的元素,都需要从头开始一个个访问,直到找到对应的元素。
链表的实现:
// 封装节点类
function Node(element, next) {
// 链表中的节点需要包含两个内容:一个是元素本身的数据,另一个是指向下一个节点的指针
this.element = element
this.next = null
}
// 封装链表
function LinkedList() {
// head 指针指向第一个元素
this.head = null
// 记录链表的长度
this.length = 0
}
// 链表中的操作
// 向链表的尾部添加一个新的项
LinkedList.prototype.append = function(element) {
// 1. 创建节点
var node = new Node(element)
// 2. 如果添加的是第一个节点,让 head 指针指向它即可
if (this.length === 0) {
this.head = node
} else {
// 3. 如果添加的不是第一个节点,从头开始找
// 首先,获取到 head 指向的第一个节点
var current = this.head
// 然后,判断当前节点的 next 的指向:如果 next 的指向为空,则说明是最后一个节点;否则就不是最后一个节点,获取到下一个节点接着判断
while (current.next) {
current = current.next
}
// 一直判断到节点的 next 指向为空,找到了最后一个节点,让它的 next 指向要添加的新的项
current.next = node
}
// 4. 将链表的长度加 1
this.length++
}
// 向链表的特定位置插入一个新的项
LinkedList.prototype.insert = function(element, position) {
// 1. 对 position 进行越界判断
if (position < 0 || position > this.length) return false
// 2. 创建节点
var node = new Node(element)
// 3. 如果插入的位置是第一个,那么需要 head 指向新插入的节点,新插入节点的 next 指向原来的第一个节点
if (position === 0) {
node.next = this.head
this.head = node
} else {
// 4. 如果插入的位置不是第一个
// 遍历循环一直到 position 位置:保存前一个节点为 previous;当前节点为 current
var previous = null
var current = null
for (var i = 0; i <= position; i++) {
if (i === 0) {
previous = null
current = this.head
} else {
// 此处变量值中的 current 是上一次循环所得的结果,也就是上一个节点
previous = current
current = current.next
}
}
// 将前一个节点的 next 指向新插入的节点;新插入节点的 next 指向原先在 position 位置的节点
previous.next = node
node.next = current
}
// 5. 将链表的长度加 1
this.length++
return true
}
// 获取链表对应位置的节点的数据
LinkedList.prototype.get = function(position) {
// 1. 对 position 进行越界判断
if (position < 0 || position >= this.length) return null
// 2. 遍历循环一直到 position 位置,获取 position 位置的节点
var current = null
for (var i = 0; i <= position; i++) {
if (i === 0) {
current = this.head
} else {
// 此处 变量值 current.next 中的 current 是上一次循环所得的结果,也就是上一个节点
current = current.next
}
}
return current.element
}
// 返回元素在链表中的索引,如果链表中没有该元素则返回-1
LinkedList.prototype.indexOf = function(element) {
var index = 0
var current = this.head
// 开始查找:如果 current 不为空,则进行值的比较
while(current) {
if (current.element === element) {
return index
}
current = current.next
index++
}
// 如果 current 为空,则返回 -1
return -1
}
// 修改某个位置的元素
LinkedList.prototype.update = function(element, position) {
// 1. 对 position 进行越界判断
if (position < 0 || position >= this.length) return false
// 2. 遍历循环一直到 position 位置,获取 position 位置的节点
var current = null
for (var i = 0; i <= position; i++) {
if (i === 0) {
current = this.head
} else {
// 此处 变量值 current.next 中的 current 是上一次循环所得的结果,也就是上一个节点
current = current.next
}
}
// 3. 将 position 位置的节点的 element 修改为新的 element
current.element = element
}
// 从链表的特定位置移除一项,并返回被移除的项
LinkedList.prototype.removeAt = function(position) {
// 1. 对 position 进行越界判断
if (position < 0 || position >= this.length) return null
// 2. 如果删除的是第一个节点,则直接让 head 指向第一个节点的下一个节点
var current = null
if (position === 0) {
current = this.head
this.head = this.head.next
} else {
// 3. 如果删除的不是第一个:遍历循环一直到 position 位置:保存前一个节点为 previous;当前节点为 current
var previous = null
for (var i = 0; i <= position; i++) {
if (i === 0) {
previous = null
current = this.head
} else {
// 此处变量值中的 current 是上一次循环所得的结果,也就是上一个节点
previous = current
current = current.next
}
}
// 让前一个节点的 next 指向原先在 position 位置节点的下一个节点
previous.next = current.next
}
// 5. 将链表的长度减 1
this.length--
// 6. 返回被删除的项
return current.element
}
// 从链表中移除一项
LinkedList.prototype.remove = function(element) {
// 1. 获取 element 在链表中的位置
var position = this.indexOf(element)
// 2. 根据位置删除节点
return this.removeAt(position)
}
// 如果链表中没有任何元素就返回 true,否则返回 false
LinkedList.prototype.isEmpty = function() {
return this.length === 0
}
// 返回链表中的元素个数
LinkedList.prototype.size = function() {
return this.length
}
// 将链表中元素的内容以字符串形式返回
LinkedList.prototype.toString = function() {
// 1. 获取第一个节点
var current = this.head
var resultStr = ''
// 2. 循环获取每一个节点:如果 current 不为空,则将当前节点的内容拼接到 resultStr 中,让 current 指向下一个节点
while (current) {
resultStr += current.element + ' '
current = current.next
}
return resultStr
}
// 调用链表
var linkedList = new LinkedList()
linkedList.append('Mary')
linkedList.append('Lily')
linkedList.append('Tom')
linkedList.append('Bob')
linkedList.insert('Lee', 2)
console.log(linkedList.toString()) // Mary Lily Lee Tom Bob
console.log(linkedList.get(2)) // Lee
console.log(linkedList.indexOf('Lee')) // 2
linkedList.update('Peny', 2) // Mary Lily Peny Tom Bob
console.log(linkedList.get(2)) // Peny
console.log(linkedList.removeAt(2)) // Peny
双向链表:
单向链表:链表相连的过程是单向的,只能是从头遍历到尾或者从尾遍历到头(一般是从头遍历到尾),实现的原理的是上一个链表节点中有一个指向下一个节点的指针。
单向链表有一个明显的缺点:单向链表可以轻松地到达下一个节点,但是想要回到上一个节点却很难。
双向链表:链表相连的过程是双向的,既可以从头遍历到尾,也可以从尾遍历到头,实现的原理是一个节点既有向前链接的指针,也有向后连接的指针。
双向链表的优缺点:
双向链表的优点:可以有效地解决单向链表难以回到上一个节点的问题。
双向链表的缺点:
- 相对于单向链表,占用内存空间更大一些。
- 每次在插入或删除某个节点时,需要处理 4 个引用,而不是单向链表的 2 个引用,实现起来要相对困难。
双向链表的实现:
- 可以使用一个 head 和一个 tail 分别指向头部和尾部的节点;
- 每个节点都由三部分组成:指向前一个节点的指针(pre)、元素的数据(item)、指向后一个节点的指针(next);
- 双向链表的第一个节点的 pre 是 null;
- 双向链表的最后一个节点的 next 是 null;
// 封装节点类
function Node(element, prev, next) {
// 双向链表中的节点需要包含两个内容:一个是元素本身的数据,一个是指向上一个节点的指针,另一个是指向下一个节点的指针
this.element = element
this.prev = null
this.next = null
}
// 封装双向链表
function DoubleLinkedList() {
// head 指针指向第一个元素
this.head = null
// tail 指向最后一个节点
this.tail = null
// 记录双向链表的长度
this.length = 0
}
// 双向链表中的操作
// 向双向链表的尾部添加一个新的项
DoubleLinkedList.prototype.append = function(element) {
// 1. 创建节点
var node = new Node(element)
// 2. 如果添加的是第一个节点,让 head 指针和 tail 指针都指向它即可
if (this.length === 0) {
this.head = node
this.tail = node
} else {
// 3. 如果添加的不是第一个节点
// this.tail 指向的是最后一个节点,让最后一个节点的 next 指向要添加的新的项,新添加的项的 prev 指向最后一个节点,this.tail 指向新添加的项
this.tail.next = node
node.prev = this.tail
this.tail = node
}
// 4. 将链表的长度加 1
this.length++
}
// 向双向链表的特定位置插入一个新的项
DoubleLinkedList.prototype.insert = function(element, position) {
// 1. 对 position 进行越界判断
if (position < 0 || position > this.length) return false
// 2. 创建节点
var node = new Node(element)
// 3. 如果双向链表为空,且插入的位置是第一个
if (this.length === 0) {
this.head = node
this.tail = node
} else {
// 4. 如果双向链表不为空
// 5. 插入的位置是第一个:让原先第一个节点的 prev 指向新插入的节点;新插入节点的 next 指向原先第一个节点;head 指向新插入的节点
if (position === 0) {
this.head.prev = node
node.next = this.head
this.head = node
} else if (position === this.length) {
// 6. 插入的位置是最后一个
node.prev = this.tail
this.tail.next = node
this.tail = node
} else {
// 7. 插入的位置是中间
// 遍历循环一直到 position 位置:保存当前节点为 current
var current = null
for (var i = 0; i <= position; i++) {
if (i === 0) {
current = this.head
} else {
// 此处变量值中的 current 是上一次循环所得的结果,也就是上一个节点
current = current.next
}
}
// 将新插入节点的 prev 指向原先在 position 位置前的节点; 将新插入节点的 next 指向原先在 position 位置的节点;将原先在 position 位置前的节点的 next 指向新插入的节点;将原先在 position 位置节点的 prev 指向新插入的节点
node.prev = current.prev
node.next = current
current.prev.next = node
current.prev = node
}
}
// 5. 将链表的长度加 1
this.length++
return true
}
// 获取双向链表对应位置的节点的数据
DoubleLinkedList.prototype.get = function(position) {
// 1. 对 position 进行越界判断
if (position < 0 || position >= this.length) return null
// 2. 遍历循环一直到 position 位置,获取 position 位置的节点
var current = null
for (var i = 0; i <= position; i++) {
if (i === 0) {
current = this.head
} else {
// 此处 变量值 current.next 中的 current 是上一次循环所得的结果,也就是上一个节点
current = current.next
}
}
return current.element
}
// 返回元素在双向链表中的索引,如果双向链表中没有该元素则返回-1
DoubleLinkedList.prototype.indexOf = function(element) {
var index = 0
var current = this.head
// 开始查找:如果 current 不为空,则进行值的比较
while(current) {
if (current.element === element) {
return index
}
current = current.next
index++
}
// 如果 current 为空,则返回 -1
return -1
}
// 修改某个位置的元素
DoubleLinkedList.prototype.update = function(element, position) {
// 1. 对 position 进行越界判断
if (position < 0 || position >= this.length) return false
// 2. 遍历循环一直到 position 位置,获取 position 位置的节点
var current = null
for (var i = 0; i <= position; i++) {
if (i === 0) {
current = this.head
} else {
// 此处 变量值 current.next 中的 current 是上一次循环所得的结果,也就是上一个节点
current = current.next
}
}
// 3. 将 position 位置的节点的 element 修改为新的 element
current.element = element
}
// 从双向链表的特定位置移除一项,并返回被移除的项
DoubleLinkedList.prototype.removeAt = function(position) {
// 1. 对 position 进行越界判断
if (position < 0 || position >= this.length) return null
var current = null
// 2. 如果双向链表只有一个节点
if (this.length === 1) {
current = this.head
this.head = null
this.tail = null
} else {
// 3. 如果双向链表不是只有一个节点
// 4. 要删除的是第一个节点:将第二个节点的 prev 指向 null;head 指向第二个节点
if (position === 0) {
current = this.head
this.head.next.prev = null
this.head = this.head.next
} else if (position === this.length - 1) {
// 5. 要删除的是最后一个节点:将倒数第二个节点的 next 指向 null;tail 指向倒数第二个节点
current = this.tail
this.tail.prev.next = null
this.tail = this.tail.prev
} else {
// 6. 要删除的是中间的节点:遍历循环一直到 position 位置,保存当前节点为 current
for (var i = 0; i <= position; i++) {
if (i === 0) {
current = this.head
} else {
// 此处变量值中的 current 是上一次循环所得的结果,也就是上一个节点
current = current.next
}
}
// 让原先在 position 位置节点的前一个节点的 next 指向原先在 position 位置节点的下一个节点;原先在 position 位置节点的下一个节点的 prev 指向原先在 position 位置节点的前一个节点
current.prev.next = current.next
current.next.prev = current.prev
}
}
// 5. 将链表的长度减 1
this.length--
// 6. 返回被删除的项
return current.element
}
// 从双向链表中移除一项
DoubleLinkedList.prototype.remove = function(element) {
// 1. 获取 element 在链表中的位置
var position = this.indexOf(element)
// 2. 根据位置删除节点
return this.removeAt(position)
}
// 如果双向链表中没有任何元素就返回 true,否则返回 false
DoubleLinkedList.prototype.isEmpty = function() {
return this.length === 0
}
// 返回双向链表中的元素个数
DoubleLinkedList.prototype.size = function() {
return this.length
}
// 将双向链表中元素的内容以字符串形式返回
DoubleLinkedList.prototype.toString = function() {
return this.forwardString()
}
// 返回正向遍历的元素内容的字符串形式
DoubleLinkedList.prototype.forwardString = function() {
// 1. 获取第一个节点
var current = this.head
var resultStr = ''
// 2. 循环获取每一个节点:如果 current 不为空,则将当前节点的内容拼接到 resultStr 中,让 current 指向下一个节点
while (current) {
resultStr += current.element + ' '
current = current.next
}
return resultStr
}
// 返回反向遍历的元素内容的字符串形式
DoubleLinkedList.prototype.backworddString = function() {
// 1. 获取最后一个节点
var current = this.tail
var resultStr = ''
// 2. 循环获取每一个节点:如果 current 不为空,则将当前节点的内容拼接到 resultStr 中,让 current 指向上一个节点
while (current) {
resultStr += current.element + ' '
current = current.prev
}
return resultStr
}
// 调用双向链表
var doubleLinkedList = new DoubleLinkedList()
doubleLinkedList.append('Mary')
doubleLinkedList.append('Lily')
doubleLinkedList.append('Tom')
doubleLinkedList.append('Bob')
doubleLinkedList.insert('Lee', 2)
console.log(doubleLinkedList.toString()) // Mary Lily Lee Tom Bob
console.log(doubleLinkedList.get(2)) // Lee
console.log(doubleLinkedList.indexOf('Lee')) // 2
doubleLinkedList.update('Peny', 2) // Mary Lily Peny Tom Bob
console.log(doubleLinkedList.get(2)) // Peny
console.log(doubleLinkedList.removeAt(2)) // Peny