链表以及数组的缺点
-
链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同.
-
这一章中,我们就来学习一下另外种非常常见的用于存储数据的线性结构: 链表
-
数组:
- 要存储多个元素,数组(或称为列表)可能是最常用的数据结构。
- 我们之前说过,几乎每一种编程语言都有默认实现数组结构。
- 但是数组也有很多缺点:
- 数组的创建通常需要申请一段连续的内存空间(一整块的内存), 并且大小是固定的(大多数编程语言数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容(-般情况下是申请一个更大的数组,比如2倍.然后将原
数组中的元素复制过去) - 而且在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移.
- 尽管我们已经学过的JavaScript的Array类方法可以帮我们做这些事,但背后的原理依然是这样。
链表的优势
- 要存储多个元素,另外一个选择就是 链表.
- 但不同于数组,链表中的元素在内存中 不必是连续的空间 .
- 链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针或者连接)组成
- 相对于数组,链表有一些优点:
- 内存空间不是必须连续的.可以充分利用计算机的内存,实现灵活的内存动态管理.
- 链表不必在创建时就确定大小,并且大小可以无限的延伸下去.
- 链表在插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多.
- 相对于数组,链表有一些缺点:
- 链表访问任何个位置的元素时,都需要从头开始访问.(无法跳过第一个元素访问任何一 个元素).
- 无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素.
链表到底是什么?
- 什么是链表呢?
- 其实上面我们已经简单的提过了链表的结构,我们这里更加详细的分析一下.
- 链表类似于火车:有一个火车头,火车头会连接一个节点节点上有乘客(类似于数据),并且这个节点会连接下一
个节点以此类推.
链表的火车结构:
链表结构的封装
我们来先创建一个链表类
// 封装链表类
function LinkedList() {
// 内部的类:节点类
function Node(data) {
this.data = data
this.next = null
}
// 属性
this.head = null
this.length = 0
// 链表中的方法
}
代码解析:
- 封装LinkedList的类,用于表示我们的链表结构. (和Java中的链表同名,不同Java中的这个类是一个双向链表,下一篇内容讲到双向链表)
- 在LinkedList类中有一个Node类 用于封装每一个节点上的信息.(和优先级队列的封装样)
- 链表中我们保存两个属性一个是链表的长度一个是链表中第一个节点
- 当然还有很多链表的操作方法.我们在下文中一起学习
链表的常见操作
我们先来认识一下, 链表中应该有哪些常见的操作
- append(element) : 向列表尾部添加一个新的项
- insert(position, element) : 向列表的特定位置插入一个新的项。
- get(position) :获取对应位置的元素
- indexOf(element) :返回元素在列表中的索引。如果列表中没有该元素则返回-1。
- update(position) :修改某个位置的元素
- removeAt(position) :从列表的特定位置移除项。
- remove(element) :从列表中移除项。
- isEmpty() :如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false.
- size() :返回链表包含的元素个数。与数组的length属性类似。
- toString() :由于列表项使用了Node类,就需要重写继承自JavaScript对象默认的toString方法,让其只输出元素的
值。
整体你会发现操作方法和数组非常类似,因为链表本身就是一种可以代替数组的结构.
1.append方法
- 向链表尾部追加数据可能有两种情况:
- 链表本身为空,新添加的数据时唯一的节点.
- 链表不为空,需要向其他节点后面追加节点
- 链表本身为空,新添加的数据时唯一的节点.
// 1.追加方法
LinkedList.prototype.append = function (data) {
// 1.创建新节点
let newNode = new Node(data);
// 2.判断是否添加的是第一个节点
if (this.length == 0){ // 2.1是第一个节点
this.head = newNode
}else { // 2.2.不是第一个节点
// 找到最后一个节点
let current = this.head // 指针
while (current.next) {
current = current.next
}
// 最后节点的next指向新的节点
current.next = newNode
}
// 3.length+1
this.length += 1
}
2.toString方法
我们先来实现一下链表的toString方法 这样会方便测试上面的添加代码
- 该方法比较简单主要是获取每个元素
- 还是从head开头,因为获取链表的任何元素都必须从第一个节点开头
- 循环遍历每一个节点,并且取出其中的element,拼接成字符串.
- 将最终字符串返回.
// 2.toString方法
LinkedList.prototype.toString = function () {
// 定义变量
let current = this.head
let listString = ''
while (current) {
listString += current.data + ' '
current = current.next
}
return listString
}
3.inser方法
接下来实现另外一个添加数据的方法:在任意位置插入数据
- 添加到第一个位置:
- 添加到第一个位置表示新添加的节点是头,就需要将原来的头节点作为新节点的next
- 另外这个时候的head应该指向新节点
- 添加到其他位置:
- 如果是添加到其他位置,就需要先找到这个节点位置了.
- 我们通过while循环,一点点向 下找并且在这个过程中保存上一个节点和下一个节点
- 找到正确的位置后,将新节点的next指向下一个节点将上一个节点的next指向新的节点
3.其他操作
其他方法方式差不多,所以就不多讲了:
// 封装链表类
function LinkedList() {
// 内部的类:节点类
function Node(data) {
this.data = data
this.next = null
}
// 属性
this.head = null
this.length = 0
// 1.追加方法
LinkedList.prototype.append = function (data) {
// 1.创建新节点
let newNode = new Node(data);
// 2.判断是否添加的是第一个节点
if (this.length == 0){ // 2.1是第一个节点
this.head = newNode
}else { // 2.2.不是第一个节点
// 找到最后一个节点
let current = this.head
while (current.next) {
current = current.next
}
// 最后节点的next指向新的节点
current.next = newNode
}
// 3.length+1
this.length += 1
}
// 2.toString方法
LinkedList.prototype.toString = function () {
// 定义变量
let current = this.head
let listString = ''
while (current) {
listString += current.data + ' '
current = current.next
}
return listString
}
// 3.insert方法
LinkedList.prototype.insert = function (position, data) {
// 1.对position进行越界判断
if (position < 0 || position > this.length) return false
// 2.根据data创建newNode
let newNode = new Node(data)
// 3.判断插入的位置是否是第一个
if (position == 0) {
newNode.next = this.head
this.head = newNode
}else {
let index = 0
let current = this.head
let previous = null
while (index++ < position) {
previous = current
current = current.next
}
newNode.next = current
previous.next = newNode
}
// 4.length+1
this.length += 1
return true
}
// 4.get方法
LinkedList.prototype.get = function(position) {
// 1.越界判断
if (position < 0 || position >= this.length) return null
// 2.获取对应的data
let current = this.head
let index = 0
while (index++ < position) {
current = current.next
}
return current.data
}
// 5.indexOf方法
LinkedList.prototype.indexOf = function (data) {
// 1.定义变量
let current = this.head
let index = 0
// 2.开始查找
while (current) {
if (current.data == data) {
return index
}
index++
current = current.next
}
// 3.找到最后没找到,返回-1
return -1
}
// 6.update方法
LinkedList.prototype.update = function (position, newDate) {
// 1、越界判断
if (position < 0 || position >= this.length) return false
// 2.查找正确的节点
let current = this.head
let index = 0
while (index++ < position) {
current = current.next
}
// 3.更新数据
current.data = newDate
return true
}
// 7.removeAt方法
LinkedList.prototype.removeAt = function (position) {
// 越界判断
if (position < 0 || position >= this.length) return false
// 2.判断删除的是不是第一个节点
if (position == 0) {
this.head = this.head.next
} else {
let index = 0
let current = this.head
let previous = null
while (index++ < position) {
previous = current
current = current.next
}
// 前一个next指向current的next即可
previous.next = current.next
}
// 3.length-1
this.length -= 1
return true
}
// 8.remove方法
LinkedList.prototype.remove = function (data) {
// 获取列表的对应位置
let position = this.indexOf(data)
// 根据位置删除元素
return this.removeAt(position)
}
// 9.isEmpty
LinkedList.prototype.isEmpty = function () {
return this.length == 0
}
// 10.size()
LinkedList.prototype.seze = function () {
return this.length
}
}
// 测试代码
// 创建linkList
let list = new LinkedList()
list.append('aaa')
list.append('bbb')
list.append('ccc')
// alert(list)
// 测试append方法
// list.insert(0, 'abc')
// list.insert(3, 'cba')
// list.insert(5, 'jjj')
alert(list)
// 测试get方法
// alert(list.get(3))
// alert(list.get(0))
// alert(list.get(5))
// 测试indexOf方法
// alert(list.indexOf('aaa'))
// 测试update方法
// list.update(0, 'nnn')
// list.update(2, 'mmm')
// alert(list)
//
// list.removeAt(0)
// alert(list)
// list.removeAt(3)
// alert(list)
//
// list.remove('aaa')
// alert(list)
// list.remove('mmm')
// alert(list)