List,即线性表,也叫做列表。有以下存储结构:
- 顺序存储方式:数组
- 链式存储方式:单向链表、双向链表、单向循环链表、双向循环链表
在JavaScript数据结构中,一直缺少List这种常用的数据结构。这里通过对比其他语言的数据结构,比如java的ArrayList、LinkedList等源码,通过顺序存储方式、链式存储方式实现List这一功能。
一、数组方式
1.1、数据结构优缺点
定义:数组(Array)是有序的元素序列
优点:读取效率极高
缺点:插入、删除效率极低(需要移动该节点后续所有数据)
1.2、功能实现
参照java sdk的ArrayList源码,简单实现JavaScript版的ArrayList数据结构。
通过对ArrayList源码查看,我们发现其本质也是数组的存储结构
对于add方法的实现,则是在数组末尾追加
按照jdk的思路,我们初步定义ArrayList数据结构如下
class ArrayList {
constructor() {
this.list = [];
}
// 大小
size() {
return this.list.length;
}
// 新增元素
add(object) {
this.list[this.list.length] = object;
}
// 移除指定元素
remove(object) {
let removeIndex = this.list.indexOf(object);
if (removeIndex !== -1) {
this.list.splice(removeIndex, 1);
}
}
// 按下标移除元素
removeAtIndex(index) {
if (this.list.length > index) {
this.list.splice(index, 1);
}
}
// 清空
clear() {
this.list = [];
}
// 查找
get(index) {
if (this.list.length > index) {
return this.list[index];
} else {
return null;
}
}
}
1.3、验证
简单测试,符合逾期无bug
二、单向链表方式
2.1、数据结构优缺点
定义:a1是a2的前驱,ai+1 是ai的后继,a1没有前驱,an没有后继
n为线性表的长度 ,若n==0时,线性表为空表
优点:增加、删除效率极高
缺点:读取效率低,特别是逆向读取效率极低
2.2、功能实现
在js、java中我们以对象代替指针,首先我们需要定义节点数据模型
class Node {
constructor(item, next) {
this.item = item;
this.next = null;
}
}
然后提供OneWayList为单向链表的数据结构,提供增删查改等功能
// 单向链表
class OneWayList {
constructor() {
this.headNode = new Node(null, null);//头指针
this.size = 0;//链表长度
}
// 在链表末尾追加
add(object) {
let node = this.headNode;
for (let i = 0; i < this.size; i++) {
node = node.next;
}
let newNode = new Node(object, null);
node.next = newNode;
this.size = this.size + 1;
}
// 修改
set(index, object) {
let node = this.headNode;
node = node.next;
if (index < this.size && index > -1) {
for (let i = 0; i < index; i++) {
node = node.next;
}
node.item = object;
}
}
// 移除指定index的数据、或移除最后一个数据
remove(index = this.size - 1) {
let node = this.headNode;
if (index < this.size && index > -1) {
for (let i = 0; i < index; i++) {
node = node.next;
}
//断开中间数据
node.next = node.next?.next;
this.size = this.size - 1;
}
}
// 链表长度
size() {
return this.size;
}
// 清空单向链表
clear() {
this.headNode = new Node(null, null);
this.size = 0;
}
// 列表转字符串
toArray() {
let arrayList = [];
let node = this.headNode;
for (let i = 0; i < this.size; i++) {
node = node.next;
arrayList[arrayList.length] = node.item;
}
return arrayList;
}
}
2.3、验证
对上述数据结构做简单的测试,符合预期
const temp = new OneWayList();
temp.add('第一个');
temp.add('第二个');
temp.add('第三个');
temp.add('第四个');
temp.set(1, '修改第二个');
temp.remove(2)
console.log(temp.toArray())
//[ '第一个', '修改第二个', '第四个' ]
三、双向链表方式
3.1、数据结构优缺点
定义:双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。不过一般我们都构造双向循环链表
优点:增加、删除效率较高
缺点:读取效率较低
3.2、功能实现
这里需要注意的是对链表的处理,需要同时处理前驱指针与后继指针,稍微麻烦点
由于是双向非循环链表,
1,当size为0时,头指针的前驱与后继皆为空;
2,当size非0时,头指针的前驱为空,后继非空。最后一个指针的前驱非空,后继为空
class Node {
constructor(item, next, prev) {
this.item = item;
this.next = next;
this.prev = prev;
}
}
// 单向链表
class LinkedList {
constructor() {
this.headNode = new Node(null, null, null);//头指针
this.size = 0;//链表长度
}
// 在链表末尾追加
add(object) {
let node = this.headNode;
for (let i = 0; i < this.size; i++) {
node = node.next;
}
// 添加前驱指针
let newNode = new Node(object, null, node);
// 添加后继指针
node.next = newNode;
this.size = this.size + 1;
}
// 修改
set(index, object) {
let node = this.headNode;
node = node.next;
if (index < this.size && index > -1) {
for (let i = 0; i < index; i++) {
node = node.next;
}
node.item = object;
}
}
// 移除指定index的数据、或移除最后一个数据
remove(index = this.size - 1) {
let node = this.headNode;
if (index < this.size && index > -1) {
for (let i = 0; i < index + 1; i++) {
node = node.next;
}
//断开前驱指针
if (node.prev != null) {
node.prev.next = node.next;
}
//断开后继指针
if (node.next != null) {
node.next.prev = node.prev;
}
this.size = this.size - 1;
}
}
// 链表长度
size() {
return this.size;
}
// 清空单向链表
clear() {
this.headNode = new Node(null, null, null);
this.size = 0;
}
// 列表转字符串
toArray() {
let arrayList = [];
let node = this.headNode;
for (let i = 0; i < this.size; i++) {
node = node.next;
arrayList[arrayList.length] = node.item;
}
return arrayList;
}
}
3.3、验证
简单的验证
const temp = new LinkedList();
temp.add('第一个');
temp.add('第二个');
temp.add('第三个');
temp.add('第四个');
temp.set(1, '修改第二个');
temp.remove(3)
console.log(temp.toArray())
// [ '第一个', '修改第二个', '第三个' ]
四、双向循环链表方式
4.1、数据结构优缺点
双向循环链表是单向循环链表的每个结点中,再设置一个指向其前驱结点的指针域
对于空的双向循环链表,则是收尾相连
优点:增加、删除效率较高
缺点:读取效率低
4.2、功能实现
由于是双向循环链表,
1,当size为0时,头指针的前驱与后继皆为本身;
2,当size非0时,头指针的前驱为最后一个元素,最后一个元素后继为头指针
3,另外在循环查找元素时候,可根据查找路径灵活使用prev与next指针
class Node {
constructor(item, next, prev) {
this.item = item;
this.next = next;
this.prev = prev;
}
}
// 双向循环链表
class LinkedList {
constructor() {
this.headNode = new Node(null, null, null);//头指针
this.headNode.next=this.headNode;
this.headNode.prev=this.headNode;
this.size = 0;//链表长度
}
// 在链表末尾追加
add(object) {
let node = this.headNode;
for (let i = 0; i < this.size; i++) {
node = node.next;
}
// 添加新节点的前驱指针、后继指针为头指针
let newNode = new Node(object, this.headNode, node);
// 添加后继指针
node.next = newNode;
// 头指针的前驱为新节点
this.headNode.prev=newNode;
this.size = this.size + 1;
}
// 修改
set(index, object) {
let node = this.headNode;
node = node.next;
if (index < this.size && index > -1) {
for (let i = 0; i < index; i++) {
node = node.next;
}
node.item = object;
}
}
// 移除指定index的数据、或移除最后一个数据
remove(index = this.size - 1) {
let node = this.headNode;
if (index < this.size && index > -1) {
for (let i = 0; i < index + 1; i++) {
node = node.next;
}
//断开前驱指针
if (node.prev != null) {
node.prev.next = node.next;
}
//断开后继指针
if (node.next != null) {
node.next.prev = node.prev;
}
this.size = this.size - 1;
}
}
// 链表长度
size() {
return this.size;
}
// 清空单向链表
clear() {
this.headNode = new Node(null, this, this);//头指针
this.size = 0;
}
// 列表转字符串
toArray() {
let arrayList = [];
let node = this.headNode;
for (let i = 0; i < this.size; i++) {
node = node.next;
arrayList[arrayList.length] = node.item;
}
return arrayList;
}
}
4.3、验证
简单的验证
const temp = new LinkedList();
temp.add('第一个');
temp.add('第二个');
temp.add('第三个');
temp.add('第四个');
temp.set(1, '修改第二个');
temp.remove(3)
console.log(temp.toArray())
//[ '第一个', '修改第二个', '第三个' ]
有偏薄之处欢迎指正