单向链表
概念
链表和数组一样,可以用于存储一系列的元素,但是链表和数组的实现机制完全不同,链表类似于火车:有一个火车头,火车头会连接一个节点,节点上有乘客(类似于数据),并且这个节点会连接下一个节点,以此类推
链表和数组的区别
数组:
要存储多个元素,数组(或称为列表)可能是最常用的数据结构
我们之前说过,几乎每一种编程语言都有默认实现数组结构
但是数组也有很多缺点:
数组的创建通常需要申请一段连续的内存空间(一整块的内存),并且大小是固定的(大多数编程语言数组都是固定的),所以当当前数组不能满足容量需求时,需要扩容(一般情况下是申请一个更大的数组,然后将元素组中的元素赋值过去)
而且在数组开头或中间位置插入数据的成本很高,需要进行大量元素的位移
尽管我们学过的js的Array类方法可以帮我们做这些事,但背后的原理依然是这样的
链表的优势
要存储多个元素,另外一个选择就是链表
但不同数组,链表的元素在内存中不必是连续的空间
链表的每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(有些语言成为指针或者连接)组成。
内存空间不是必须连续的,可以充分利用计算机的内存,实现灵活的内存动态管理
链表不必在创建时就确定大小,并且大小可以无限的延伸下去
链表再插入和删除数据时,时间复杂度可以达到O(1),相对数组效率高很多
链表的缺点
链表访问任何一个位置的元素时,都需要从头开始访问(无法跳过第一个元素去访问任何一个元素)
无法通过下标直接访问元素,需要从头一个个访问,直到找到对应的元素
链表的常用方法
append(data):向列表尾部添加一个新的项
insert(position,data):向列表的特定位置插入一个新的项
get(position):获取对应位置的元素
indexOf(data):返回元素在列表中的索引。如果列表中没有该元素则返回-1
update(positon,data):修改某个位置的元素
removeAt(position):从列表的特定位置移除一项
remove(data):从列表中移除一项
isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false
Size():返回链表包含的元素个数。与数组的length属性相似
toString():将链表中的数据转换为字符串
实现链表的常用方法
class CreateData {
constructor(data) {
this.data = data;
this.next = null
}
}
class LinkedList {
constructor() {
this.head = '';
this.length = 0;
}
append(data) {
let newData = new CreateData(data);
if (this.head === '') {
newData.next = this.head;
this.head = newData;
} else {
let current = this.head;
while (current.next) {
current = current.next
}
current.next = newData
}
this.length++;
}
insert(position, data) {
if (position < 0 || position > this.length) return false
let newData = new CreateData(data);
if (position === 0) {
newData.next = this.head;
this.head = newData
} else {
let index = 0;
let current = this.head;
let prev = null;
while (index++ < position) {
prev = current
current = current.next;
}
prev.next = newData;
newData.next = current
}
this.length++
}
get(position) {
if (position < 0 || position >= this.length) return null;
let current = this.head;
let index = 0;
while (index++ < position) {
current = current.next
}
return current.data;
}
indexOf(data) {
let current = this.head;
let index = 0;
while (current.data !== data) {
if (!current.next) return -1
current = current.next;
index += 1;
}
return index
}
update(position, data) {
if (position < 0 || position >= this.length) return false;
let newData = new CreateData(data);
let current = this.head;
let index = 0;
while (index++ < position) {
current = current.next;
}
current.data = data
}
removeAt(position) {
if (position < 0 || position >= this.length) return false;
if (position === 0) {
let current = this.head;
this.head = current.next
} else {
let current = this.head;
let index = 0;
let prev = null;
while (index++ < position) {
prev = current;
current = current.next;
}
prev.next = current.next
}
this.length -= 1;
}
remove(data) {
let i = this.indexOf(data);
this.removeAt(i)
}
isEmpty() {
return this.length > 0 ? false : true;
}
size() {
return this.length
}
toString() {
let current = this.head;
let str = '';
while (current) {
str += current.data + ',';
current = current.next
}
return str.slice(0, str.length - 1)
}
}
测试上面封装链表的方法
let linkedList = new LinkedList();
linkedList.append('小强');
linkedList.append('小红');
linkedList.append('小花');
console.log('链表向最后添加一项', linkedList)
linkedList.insert(1, '小李');
console.log('链表向指定位置插入一项', linkedList)
console.log('根据索引获取某一项的值', linkedList.get(3))
console.log('根据某一项值获取对应索引', linkedList.indexOf('小花'))
linkedList.update(2, '小明')
console.log('根据索引替换传进去的值', linkedList)
linkedList.removeAt(2)
console.log('根据索引删除某一项',linkedList)
linkedList.remove('小强')
console.log('根据某一项值删除数据',linkedList)
console.log('查看是否为空', linkedList.isEmpty())
console.log('查看链表元素的个数', linkedList.size())
console.log('将链表内的元素转化成字符串', linkedList.toString())
双向链表
只能从头遍历到尾或者从尾遍历到头(一般从头到尾)也就是链表相连是单向的,实现的原理是上一个链表中有一个指向下一个的引用。
单向链表的缺点
我们可以轻松的到达下一个节点,但是回到前一个节点是很难的。但是在实际开发中,经常会遇到需要回到上一个节点的情况。
举个例子:
假设一个文本编辑用链表来存储文本。每一行用一个String对象存储
在链表的一个节点中。当编辑器用户向下移动光标时,链表直接操作到下一个节点即可。但是当用户将光标向上移动呢?这个时候为了回到上一个节点,我们可能需要从first开始,依次走到想要的节点上
双向链表原理
既可以从头遍历到尾,又可以从尾遍历到头,也就是链表相连的过程是双向的。一个节点既有向前连接的引用,也有一个向后连接的引用
双向链表可以有效的解决单向链表中提到的问题。
双向链表的缺点
每次在插入或者删除某个节点时,需要处理四个引用,而不是两个,
也就是实现起来要困难一些;并且相当于单向链表,必然占用的内存空间更大一些。但是这些缺点和我们使用起来的方便程度相比,是微不足道的
双向链表的特点:
可以使用一个head和tail分别指向头部和尾部的节点;每个节点都由三部分组成:前一个节点的指针(prev)/保存的元素(item)/后一个节点的指针(next)
双向链表的第一个节点的prev是null
双向链表的最后的节点的next是null
双向链表常见的操作:
append(data):向列表尾部添加一个新的项
insert(position,data):向列表的特定位置插入一个新的项
get(position):获取对应位置的元素
indexOf(data):返回元素在列表中的索引。如果列表中没有该元素则返回-1
update(positon,data):修改某个位置的元素
removeAt(position):从列表的特定位置移除一项
remove(data):从列表中移除一项
isEmpty():如果链表中不包含任何元素,返回true,如果链表长度大于0则返回false
Size():返回链表包含的元素个数。与数组的length属性相似
toString():将链表中的数据转换为字符串
forwardString():返回正向遍历的节点字符串形式
backwordString():返回反向遍历的节点字符串形式
封装双向链表的方法
class CreateNode {
constructor(data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
class DoubleLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.length = 0;
}
append(data) {
let newData = new CreateNode(data);
if (this.length == 0) {
this.head = newData;
this.tail = newData;
} else {
newData.prev = this.tail;
this.tail.next = newData;
this.tail = newData;
}
this.length += 1;
}
toString() {
let current = this.head;
let str = '';
while (current) {
str += current.data + ' ';
current = current.next;
}
return str
}
insert(position, data) {
if (position < 0 || position > this.length) return false;
let newData = new CreateNode(data);
if (this.length === 0) {
this.head = newData;
let tail = newData;
} else if (position === this.length) {
newData.prev = this.tail;
this.tail.next = newData;
this.tail = newData
} else {
if (position === 0) {
newData.next = this.head;
this.head = newData
} else {
let current = this.head;
let index = 0;
while (index++ < position) {
current = current.next;
}
newData.next = current;
newData.prev = current.prev;
console.log(current)
current.prev.next = newData;
current.prev = newData;
}
}
this.length += 1;
}
get(position) {
if (position < 0 || position >= this.length) return null;
let current = this.head;
let index = 0;
while (index++ < position) {
current = current.next;
}
return current.data;
}
indexOf(data) {
let index = 0;
let current = this.head;
while (current.data !== data) {
if (!current.next) return -1
current = current.next;
index += 1;
}
return index
}
update(position, data) {
if (position < 0 || position >= this.length) return false;
let current = this.head;
let index = 0;
while (index++ < position) {
current = current.next;
}
current.data = data
}
removeAt(position) {
if (position < 0 || position >= this.length) return false;
if (position === 0) {
this.head = this.head.next;
} else if (position === this.length - 1) {
this.tail.prev.next = null;
this.tail = this.tail.prev;
} else {
let index = 0;
let current = this.head;
while (index++ < position) {
current = current.next;
};
current.prev.next = current.next;
current.next.prev = current.prev
}
}
remove(data) {
let i = this.indexOf(data);
this.removeAt(i)
}
isEmpty() {
return this.length === 0
}
size() {
return this.length
}
forwardString() {
return this.toString()
}
backwordString() {
let current = this.tail;
let str = '';
while (current) {
str += current.data + ' ';
current = current.prev;
}
return str
}
}
测试双向链表封装方法
let doubleLinkedList = new DoubleLinkedList();
doubleLinkedList.append('小孟');
doubleLinkedList.append('小高');
doubleLinkedList.append('小张');
doubleLinkedList.append('小李');
/* alert默认调用toString方法:根据原型链向上查找 每个实例都有一个__proto__指针 指向类的原型proptotype,在查找的过程中发现我们自己
封装的toString方法 从而使用DoubleLinkedList类中的toString,所以下面代码弹出的结果是字符串形式*/
// 向链表添加数据
alert(doubleLinkedList);
// 双向列表的插入操作
doubleLinkedList.insert(0, '小明');
alert(doubleLinkedList);
// 双向链表根据索引获取元素
alert(doubleLinkedList.get(3))
// 双向链表根据数据返回对应索引
alert(doubleLinkedList.indexOf('小李'))
// 根据索引修改数据
doubleLinkedList.update(2, '小红')
alert(doubleLinkedList)
// 根据索引删除数据
doubleLinkedList.removeAt(1);
alert(doubleLinkedList)
// 根据数据删除元素
doubleLinkedList.remove('小张');
alert(doubleLinkedList)
// 判断链表是否为空
alert(doubleLinkedList.isEmpty())
// 判断链表元素的个数
alert(doubleLinkedList.size())
// 返回正向遍历的节点字符串形式
alert(doubleLinkedList.forwardString())
// 返回反向遍历的节点字符串形式
alert(doubleLinkedList.backwordString())
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。