目录
2.3 insert(position, element)任意位置插入
2.5 indexOf(element):返回元素在列表中的索引
2.7 removeAt(position)从列表的特定位置移除一项
链表和数组一样, 可以用于存储一系列的元素, 但是链表和数组的实现机制完全不同.
一. 认识链表
1.数组
要存储多个元素,数组(或列表)可能是最常用的数据结构。
1.1 数组的优点
- 几乎每一种编程语言都有默认实现数组结构, 这种数据结构非常方便,提供了一个便利的
[]
语法来访问它的元素。 - 数组通过下标值修改和获取元素性能比较高。
1.2 数组的缺点
- 数组的创建通常需要申请一段连续的内存空间(一整块的内存), 并且大小是固定的(大多数编程语言数组都是固定的), 所以当当前数组不能满足容量需求时, 需要扩容. (一般情况下是申请一个更大的数组, 比如2倍. 然后将原数组中的元素复制过去)
- 而且在数组开头或中间位置插入数据的成本很高, 需要进行大量元素的位移.(尽管我们已经学过的JavaScript的
Array
类方法可以帮我们做这些事,但背后的原理依然是这样)。
2.链表
要存储多个元素, 另外一个选择就是使用链表.
链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言称为指针或者链接)组成.
2.1 相对于数组, 链表有一些优点
- 内存空间不必是连续的. 可以充分利用计算机的内存. 实现灵活的内存动态管理.
- 链表不必在创建时就确定大小, 并且大小可以无限的延伸下去.
- 链表在插入和删除数据时, 时间复杂度可以达到O(1). 相对数组效率高很多.
2.2 相对于数组, 链表有一些缺点
- 链表访问任何一个位置的元素时, 无法通过下标直接访问元素, 都需要从头开始访问.(无法跳过第一个元素访问任何一个元素).
3.什么是链表
链表类似于火车: 有一个火车头, 火车头会连接一个节点, 节点上有乘客, 并且这个节点会连接下一个节点, 以此类推.
3.1 链表的火车结构
3.2 链表的数据结构
3.3 给火车加上数据后的结构
二. 链表封装
1.创建链表类
1.1 创建一个链表类
// 封装链表类
function LinkedList() {
// 内部的类:节点类
function Node(data) { // data:数据; next:指向的下一个节点
this.data = data;
this.next = null;
}
// 属性
this.head = null; // 默认情况下head是null
this.length = 0; // 记录链表的长度
// 链表中的方法
}
1.2 代码解析
- 封装LinkedList的类, 用于表示我们的单向链表结构. (和Java中的链表同名, 不同Java中的这个类是一个双向链表, 后面我们会讲解双向链表)
- 在LinkedList类中有一个Node类, 用于封装每一个节点上的信息.(和优先级队列的封装一样)
- 链表中我们保存两个属性, 一个是链表的长度, 一个是链表中第一个节点.
2.链表常见操作
- 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方法,让其只输出元素的值。
整体会发现操作方法和数组非常类似, 因为链表本身就是一种可以代替数组的结构.但是某些方法实现起来有些麻烦, 所以我们一个个来慢慢实现它们.
2.1 append(element)尾部追加数据
向链表尾部追加数据可能有两种情况:
- 链表本身为空, 新添加的数据时唯一的节点.
- 链表不为空, 需要向其他节点后面追加节点.
封装代码:
// 封装链表类
function LinkedList() {
// 内部的类:节点类
function Node(data) { // data:数据; next:指向的下一个节点
this.data = data;
this.next = null;
}
// 属性
this.head = null; // 默认情况下head是null
this.length = 0; // 记录链表的长度
// 1.append(element):向列表尾部添加一个新的项
LinkedList.prototype.append = function (data) {
var newNode = new Node(data)
// 判断是否添加的是第一个节点
if (this.length == 0) { // 是第一个节点:直接把新元素加在后面
this.head = newNode;
}else { // 不是第一个节点:找到最后一个节点,让最后一个节点的next指向新的节点
var current = this.head // current:第一个节点
while (current.next) {
current = current.next;
}
current.next = newNode;
}
// 添加新节点后长度加1
this.length += 1
}
}
代码解读:
-
首先需要做的是将data传入方法, 并根据data创建一个Node节点.
-
场景一: 链表本身是空的, 比如这种情况下我们插入了一个15作为元素.
-
场景二: 链表中已经有元素了, 需要向最后的节点的next中添加节点.
- 这个时候要向链表的尾部添加一个元素, 首先我们需要找到这个尾部元素.
- 记住: 我们只有第一个元素的引用, 因此需要循环访问链表, 直接找到最后一个项.
- 找到最后一项后, 最后一项的next为null, 这个时候不让其为null, 而是指向新创建的节点即可.
-
最后, 一定不要忘记将链表的length+1.
2.2 toString()转成字符串
我们先来实现一下链表的toString方法, 这样会方便测试上面的添加代码:
// 2.toString():转换成字符串
LinkedList.prototype.toString = function () {
// 定义变量
var current = this.head
var listString = ""
// 循环获取一个个的节点
while (current) {
listString += current.data + " ";
current = current.next;
}
return listString;
}
代码解读:
- 该方法比较简单, 主要是获取每一个元素
- 还是从head开头, 因为获取链表的任何元素都必须从第一个节点开头.
- 循环遍历每一个节点, 并且取出其中的element, 拼接成字符串.
- 将最终字符串返回.
测试方法:
// 测试代码
// 创建LinkedList链表
var list = new LinkedList();
// 测试append方法
list.append('abc')
list.append('cba')
list.append('nba')
// 打印链表的结果
alert(list)
2.3 insert(position, element)任意位置插入
封装代码:
// 3.insert(position, element):任意位置插入
LinkedList.prototype.insert = function(position,data) {
// 3.1 对position进行越界判断:不能是负数;长度不能超过现有元素个数+1(即length=元素个数)
if(position < 0 || position > this.length) {
return false;
}
// 3.2 根据data创建newNode
var newNode = new Node(data)
// 3.3 情况1:插入的位置是第一个
if (position == 0) {
newNode.next = this.head;
this.head = newNode;
}else { //情况2:插入的位置不是第一个
var index = 0; // index指向的节点位置就是要插入的位置 index++:先判断再加
var current = this.head; // 指向第一个节点
var previous = null; // 指向第一个节点的前一个
while(index++ < position) {
previous = current;
current = current.next; // 指向要插入的位置的节点
}
newNode.next = current;
previous.next = newNode;
}
// 3.4 length+1
this.length += 1;
return true;
}
代码解读:
-
代码3.1位置,处理了越界问题, 基本传入位置信息时, 都需要进行越界的判断. 如果越界, 返回false, 表示数据添加失败. (因为位置信息是错误的, 所以数据肯定是添加失败的)
-
代码3.2位置,定义了一些变量, 后续需要使用它们来保存信息.
-
代码3.3位置进行了判断, 这是因为添加到第一个位置和其他位置是不同的.
-
情况1:添加到第一个位置
- 添加到第一个位置, 表示新添加的节点是头, 就需要将原来的头节点, 作为新节点的next
- 另外这个时候的head应该指向新节点.
-
情况二:添加到其他位置
- 如果是添加到其他位置, 就需要先找到这个节点位置了.
- 我们通过while循环, 一点点向下找. 并且在这个过程中保存上一个节点和下一个节点.
- 找到正确的位置后, 将新节点的next指向下一个节点, 将上一个节点的next指向新的节点.
-
最后, 不要忘记length+1
-
返回true, 表示元素插入成功了.
测试方法:
// 测试insrt方法
list.insert(3,'aaa')
// 打印链表的结果
alert(list)
2.4 get(position):获取对应位置的元素
封装代码:
// 4.get(position):获取对应位置的元素
LinkedList.prototype.get = function (position) {
// 4.1 越界判断
if (position < 0 || position >= this.length) {
return null;
}
// 获取对应数据
var current = this.head;
var index = 0;
while (index++ < position) {
current = current.next
}
return current.data;
}
测试方法:
// 测试get方法
alert(list.get(2))
// 打印链表的结果
alert(list)
2.5 indexOf(element):返回元素在列表中的索引
封装代码:
// 5.indexOf(element):返回元素在列表中的索引
LinkedList.prototype.indexOf = function(data) {
// 5.1 定义变量
var current = this.head;
var index = 0;
// 5.2 开始查找
while(current) {
if(current.data == data) {
return index;
}
current = current.next;
index += 1;
}
return -1
}
代码解析:
- 代码1的位置还是定义需要的变量.
- 代码2的位置, 通过while循环获取节点
- 通过节点获取元素和element进行对比, 如果和传入element相同, 表示找到, 直接返回index即可.
- 如果没有找到, index++, 并且指向下一个节点.
- 到最后都没有找到, 说明链表中没有对应的元素, 那么返回-1即可.
测试方法:
// 测试indexOf方法
alert(list.indexOf('aaa'))
2.6 update(position)修改某个位置的元素
封装代码:
// 6.update(position):修改某个位置的元素
LinkedList.prototype.update = function (position,newData) {
// 6.1 越界判断
if (position < 0 || position >= this.length) {
return false;
}
// 6.2 查找正确的节点
var current = this.head;
var index = 0;
while(index++ < position) {
current = current.next;
}
//6.3 将position位置的node的data修改为newData
current.data = newData
return true;
}
测试方法:
// 测试update方法
list.update(0,'mmm')
// 打印链表的结果
alert(list)
2.7 removeAt(position)从列表的特定位置移除一项
封装代码:
// 7.removeAt(position):从列表的特定位置移除一项\
LinkedList.prototype.removeAt = function(position) {
// 7.1 越界判断
if (position < 0 || position >= this.length) {
return null;
}
// 7.2 判断是否删除的是第一个节点
var current = this.head;
if(position == 0) { // 如果删除的是第一个就直接让head指向第二个
this.head = this.head.next;
}else { // 删除的不是第一个:移除掉删除的,让删的前一个指向删的下一个
var index =0;
var previous = null;
while(index++ < position) {
previous = current;
current = current.next;
}
// 7.3 前一个节点的next指向current的next
previous.next = current.next;
}
// 7.4 长度-1
this.length -= 1;
return current.data;
}
代码解析:
-
代码7.1部分, 还是越界的判断. (注意: 这里越界判断中的等于length也是越界的, 因为下标值是从0开始的)
-
代码7.2部分还是定义了一些变量, 用于保存临时信息
-
代码7.3部分进行判断, 因为移除第一项和其他项的方式是不同的
-
移除第一项的信息:
- 移除第一项时, 直接让head指向第二项信息就可以啦.
- 那么第一项信息没有引用指向, 就在链表中不再有效, 后面会被回收掉.
- 移除其他项的信息:
- 移除其他项的信息操作方式是相同的.
- 首先, 我们需要通过while循环, 找到正确的位置.
- 找到正确位置后, 就可以直接将上一项的next指向current项的next, 这样中间的项就没有引用指向它, 也就不再存在于链表后, 会面会被回收掉.
测试方法:
// 测试removeAt方法
list.removeAt(0)
// 打印链表的结果
alert(list)
2.8 remove(element)从列表中移除一项
有了上面的indexOf和removeAt方法, 我们可以非常方便实现根据元素来删除信息:
// 7.remove(element):从列表中移除一项
LinkedList.prototype.remove = function(data) {
// 7.1获取data在列表中的位置
var position = this.indexOf(data);
// 7.2根据位置信息删除节点
return this.removeAt(position);
}
代码解析:
- 获取元素所在位置(已经封装好), 根据位置移除元素(已经封装好)
测试方法:
// 测试remove方法
list.remove('cba')
// 打印链表的结果
alert(list)
2.9 isEmpty()判空
封装代码:
// 9.isEmpty()判空
LinkedList.prototype.isEmpty = function() {
return this.length == 0
}
测试方法:
// 测试isEmpty
alert(list.isEmpty())
// 打印链表的结果
alert(list)
2.10 size()节点个数
封装代码:
// 10.sixe()节点个数
LinkedList.prototype.size = function() {
return this.length;
}
测试方法:
// 测试size
alert(list.size())
// 打印链表的结果
alert(list)
四.完整代码
完成的LinkedList代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
// 封装链表类
function LinkedList() {
// 内部的类:节点类
function Node(data) { // data:数据; next:指向的下一个节点
this.data = data;
this.next = null;
}
// 属性
this.head = null; // 默认情况下head是null
this.length = 0; // 记录链表的长度
// 1.append(element):向列表尾部添加一个新的项
LinkedList.prototype.append = function (data) {
var newNode = new Node(data)
// 判断是否添加的是第一个节点
if (this.length == 0) { // 是第一个节点:直接把新元素加在后面
this.head = newNode;
} else { // 不是第一个节点:找到最后一个节点,让最后一个节点的next指向新的节点
var current = this.head // current:第一个节点
while (current.next) {
current = current.next;
}
current.next = newNode;
}
// 添加新节点后长度加1
this.length += 1
}
// 2.toString():转换成字符串
LinkedList.prototype.toString = function () {
// 定义变量
var current = this.head
var listString = ""
// 循环获取一个个的节点
while (current) {
listString += current.data + " ";
current = current.next;
}
return listString;
}
// 3.insert(position, element):任意位置插入
LinkedList.prototype.insert = function (position, data) {
// 3.1 对position进行越界判断:不能是负数;长度不能超过现有元素个数+1(即length=元素个数)
if (position < 0 || position > this.length) {
return false;
}
// 3.2 根据data创建newNode
var newNode = new Node(data)
// 3.3 情况1:插入的位置是第一个
if (position == 0) {
newNode.next = this.head;
this.head = newNode;
} else { //情况2:插入的位置不是第一个
var index = 0; // index指向的节点位置就是要插入的位置 index++:先判断再加
var current = this.head; // 指向第一个节点
var previous = null; // 指向第一个节点的前一个
while (index++ < position) {
previous = current;
current = current.next; // 指向要插入的位置的节点
}
newNode.next = current;
previous.next = newNode;
}
// 3.4 length+1
this.length += 1;
return true;
}
// 4.get(position):获取对应位置的元素
LinkedList.prototype.get = function (position) {
// 4.1 越界判断
if (position < 0 || position >= this.length) {
return null;
}
// 获取对应数据
var current = this.head;
var index = 0;
while (index++ < position) {
current = current.next
}
return current.data;
}
// 5.indexOf(element):返回元素在列表中的索引
LinkedList.prototype.indexOf = function(data) {
// 5.1 定义变量
var current = this.head;
var index = 0;
// 5.2 开始查找
while(current) {
if(current.data == data) {
return index;
}
current = current.next;
index += 1;
}
return -1
}
// 6.update(position):修改某个位置的元素
LinkedList.prototype.update = function (position,newData) {
// 6.1 越界判断
if (position < 0 || position >= this.length) {
return false;
}
// 6.2 查找正确的节点
var current = this.head;
var index = 0;
while(index++ < position) {
current = current.next;
}
//6.3 将position位置的node的data修改为newData
current.data = newData
return true;
}
// 7.removeAt(position):从列表的特定位置移除一项\
LinkedList.prototype.removeAt = function(position) {
// 7.1 越界判断
if (position < 0 || position >= this.length) {
return null;
}
// 7.2 判断是否删除的是第一个节点
var current = this.head;
if(position == 0) { // 如果删除的是第一个就直接让head指向第二个
this.head = this.head.next;
}else { // 删除的不是第一个:移除掉删除的,让删的前一个指向删的下一个
var index =0;
var previous = null;
while(index++ < position) {
previous = current;
current = current.next;
}
// 7.3 前一个节点的next指向current的next
previous.next = current.next;
}
// 7.4 长度-1
this.length -= 1;
return current.data;
}
// 7.remove(element):从列表中移除一项
LinkedList.prototype.remove = function(data) {
// 7.1获取data在列表中的位置
var position = this.indexOf(data);
// 7.2根据位置信息删除节点
return this.removeAt(position);
}
// 8.isEmpty()判空
LinkedList.prototype.isEmpty = function() {
return this.length == 0
}
// 9.sixe()节点个数
LinkedList.prototype.size = function() {
return this.length;
}
}
// 测试代码
// 创建LinkedList链表
var list = new LinkedList();
// 测试append方法
list.append('abc')
list.append('cba')
list.append('nba')
// 测试insrt方法
list.insert(3, 'aaa')
// 测试get方法
alert(list.get(2))
// 测试indexOf方法
alert(list.indexOf('aaa'))
// 测试update方法
list.update(0,'mmm')
// 测试removeAt方法
list.removeAt(0)
// 测试remove方法
list.remove('cba')
// 测试isEmpty
alert(list.isEmpty())
// 测试size
alert(list.size())
// 打印链表的结果
alert(list)
</script>
</body>
</html>