------------恢复内容开始------------

背景

数组并不总是组织数据的最佳数据结构,原因如下。在很多编程语言中,数组的长度是固定的,所以当数组已被数据填满时,再要加入新的元素就会非常困难。

在数组中,添加和删除元素也很麻烦,因为需要将数组中的其他元素向前或向后平移,以反映数组刚刚进行了添加或删除操作。

然而,JavaScript 的数组并不存在上述问题,因为使用 split() 方法不需要再访问数组中的其他元素了。

JavaScript 中数组的主要问题是,它们被实现成了对象,与其他语言(比如 C++ 和 Java)的数组相比,效率很低。

如果你发现数组在实际使用时很慢,就可以考虑使用链表来替代它。除了对数据的随机访问,链表几乎可以用在任何可以使用一维数组的情况中。如果需要随机访问,数组仍然是更好的选择。

定义

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的集合。如下图所示:

数组元素靠它们的位置进行引用,链表元素则是靠相互之间的关系进行引用。然而要标识出链表的起始节点却有点麻烦,许多链表的实现都在链表最前面有一个特殊节点,叫做头节点。如下图所示:

链表中插入一个节点的效率很高。向链表中插入一个节点,需要修改它前面的节点(前驱),使其指向新加入的节点,而新加入的节点则指向原来前驱指向的节点。下图演示了如何在 Tue 节点后加入 Fri 节点。

从链表中删除一个元素也很简单。将待删除元素的前驱节点指向待删除元素的后继节点,同时将待删除元素指向 null,元素就删除成功了。下图演示了从链表中删除“Fri”节点的过程。

链表节点(Node)类实现

完整代码地址,Node类包含两个属性:

  • el 用来保存节点上的数据
  • next 用来保存指向下一个节点的链接

1. 构造函数

function Node (el) {
this.el = el;
this.next = null;
}

链表(Link)类实现

完整代码地址,Link类提供了以下的方法:

  • insert 插入新节点
  • remove 删除节点
  • display 显示链表元素的方法
  • 其他一些辅助方法

1. 构造函数

function Link () {
this.head = new Node('head');
}

2. find:按节点的值查找节点

Link.prototype.find = function (el) {
var currNode = this.head;
while (currNode && currNode.el != el) {
currNode = currNode.next;
}
return currNode;
}
find 方法展示了如何在链表上进行移动。首先,创建一个新节点,并将链表的头节点赋给这个新创建的节点。
然后在链表上进行循环,如果当前节点的 el 属性和我们要找的信息不符,就从当前节点移动到下一个节点。如果查找成功,该方法返回包含该数据的节点;否则,返回 null
 
3. insert:插入一个节点
Link.prototype.insert = function (newEl, oldEl) {
var newNode = new Node(newEl);
var findNode = this.find(oldEl);
if (findNode) {
newNode.next = findNode.next;
findNode.next = newNode;
} else {
throw new Error('找不到给定插入的节点');
}
}

find 方法一旦找到给定的节点,就可以将新节点插入链表了。

4. display:展示链表节点元素

// 展示链表中的元素
Link.prototype.display = function () {
var currNode = this.head.next;
while (currNode) {
console.log(currNode.el);
currNode = currNode.next;
}
}

5. findPrev:寻找给定节点的前一个节点

Link.prototype.findPrev = function (el) {
var currNode = this.head;
while (currNode.next && currNode.next.el !== el) {
currNode = currNode.next;
}
return currNode;
}

6. remove:删除给定的节点

Link.prototype.remove = function (el) {
var prevNode = this.findPrev (el);
if (prevNode.next != null) {
prevNode.next = prevNode.next.next;
} else {
throw new Error('找不到要删除的节点');
}
}

链表(Link)类测试

var link = new Link();
link.append(1);
link.append(3);
link.display();
console.log('------------');
link.insert(2, 1);
link.display();
console.log('------------');
link.remove(1);
link.display();

运行结果:

1
3
------------
1
2
3
------------
2
3

上面介绍的链表我们称作:单向链表(单链表),其特点是链表的链接方向是单向的,对链表的访问要通过顺序读取从头部开始。下面我们介绍另一种链表:双向链表(双链表)

双向链表

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。

所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。如下图所示:

双向链表节点(DNode)类实现

完整代码地址,相比于单向链表节点(Node)类,我们只需新增一个 prev 属性,指向之前一个链表节点的引用即可。

function DNode (el) {
this.el = el;
this.prev = null;
this.next = null;
}

双向链表(DLink)类实现

完整代码地址,相比于单链表,双链表的操作会复杂一点。

1. 构造函数

function DLink () {
this.head = new DNode('head');
}

2. append:向链表结尾添加一个节点

CLink.prototype.append = function (el) {
var currNode = this.head;
while (currNode.next != null) {
currNode = currNode.next;
}
var newNode = new Node(el);
newNode.next = currNode.next;
currNode.next = newNode;
}

3. find:查找给定的节点

DLink.prototype.find = function (el) {
var currNode = this.head;
while (currNode && currNode.el != el) {
currNode = currNode.next;
}
return currNode;
}

4. insert:插入一个节点

DLink.prototype.insert = function (newEl, oldEl) {
var newNode = new DNode(newEl);
var currNode = this.find(oldEl);
if (currNode) {
newNode.next = currNode.next;
newNode.prev = currNode;
currNode.next = newNode;
} else {
throw new Error('未找到指定要插入节点位置对应的值!')
}
}

5. display:顺序展示链表节点

DLink.prototype.display = function () {
var currNode = this.head.next;
while (currNode) {
console.log(currNode.el);
currNode = currNode.next;
}
}

6. findLast:查找最后一个节点

DLink.prototype.findLast = function () {
var currNode = this.head;
while (currNode.next != null) {
currNode = currNode.next;
}
return currNode;
}

7. dispReverse:逆序展示链表元素

DLink.prototype.dispReverse = function () {
var currNode = this.head;
currNode = this.findLast();
while (currNode.prev != null) {
console(currNode.el);
currNode = currNode.prev;
}
}

8. remove:删除节点

DLink.prototype.remove = function (el) {
var currNode = this.find(el);
if (currNode && currNode.next != null) {
currNode.prev.next = currNode.next;
currNode.next.prev = currNode.prev;
currNode.next = null;
currNode.previous = null;
} else {
throw new Error('找不到要删除对应的节点');
}
}

双向链表(DLink)类测试

// 实例化
var doubleLink = new DLink();
doubleLink.append(1);
doubleLink.append(2);
doubleLink.append(4);
doubleLink.display();
console.log('-------------------------');
doubleLink.dispReverse();
console.log('-------------------------');
doubleLink.insert(3, 2);
doubleLink.display();
doubleLink.remove(1);
console.log('-------------------------');
doubleLink.display();

运行结果:

1
2
4
-------------------------
4
2
1
-------------------------
1
2
3
4
-------------------------
2
3
4

循环链表

循环链表和单向链表相似,节点类型都是一样的。

唯一的区别是,在创建循环链表时,让其头节点的 next 属性指向它本身,即:this.head.next = this.head,并保证链表中最后一个节点的 next 属性,始终指向 head,如下图所示:

循环链表(CLink)实现

1. 构造函数

完整代码地址,这里,我们沿用单链表中的节点(Node)类,做为循环链表的节点类。不同的是,我们在 CLink 构造函数阶段,就要把 this.head 赋值给 this.head.next

function CLink () {
this.head = new Node('head');
this.head.next = this.head;
}

2. append: 向链表节点增加一个元素

DLink.prototype.append = function (el) {
var currNode = this.head;
while (currNode.next != null && currNode.next != this.head) {
currNode = currNode.next;
}
var newNode = new Node(el);
newNode.next = currNode.next;
newNode.prev = currNode;
currNode.next = newNode;
}

3. find:根据节点的值查找链表节点

CLink.prototype.find = function (el) {
var currNode = this.head;
while (currNode && currNode.el != el) {
currNode = currNode.next;
}
return currNode;
}

4. insert:插入一个节点

CLink.prototype.insert = function (newEl, oldEl) {
var newNode = new Node(newEl);
var currNode = this.find(oldEl);
if (currNode) {
newNode.next = currNode.next;
currNode.next = newNode;
} else {
throw new Error('未找到指定要插入节点位置对应的值!');
}
}

5. display:展示链表元素节点

CLink.prototype.display = function () {
var currNode = this.head.next;
while (currNode && currNode != this.head) {
console.log(currNode.el);
currNode = currNode.next;
}
}

6. 根据给定值寻找前一个节点

CLink.prototype.findPrev = function (el) {
var currNode = this.head;
while (currNode.next && currNode.next.el !== el) {
currNode = currNode.next;
}
return currNode;
}

7. 删除给定值对应的节点

CLink.prototype.remove = function (el) {
var prevNode = this.findPrev(el);
if (prevNode.next != null) {
prevNode.next = prevNode.next.next;
prevNode.next.next = null;
} else {
throw new Error('找不到要删除的节点');
}
}

循环链表(CLink)类测试

var circleLink = new CLink ();
circleLink.append(1);
circleLink.append(2);
circleLink.append(4);
circleLink.display();
console.log('---------------------------');
circleLink.insert(3, 2);
circleLink.display();
console.log('---------------------------');
circleLink.remove(4);
circleLink.display();

运行结果:

1
2
4
---------------------------
1
2
3
4
---------------------------
1
2
3

链表实战

题目:

传说在公元 1 世纪的犹太战争中,犹太历史学家弗拉维奥·约瑟夫斯和他的 40 个同胞被罗马士兵包围。犹太士兵决定宁可自杀也不做俘虏,于是商量出了一个自杀方案。他们围成一个圈,从一个人开始,数到第三个人时将第三个人杀死,然后再数,直到杀光所有人。约瑟夫和另外一个人决定不参加这个疯狂的游戏,他们快速地计算出了两个位置,站在那里得以幸存。写一段程序将 n 个人围成一圈,并且第 m 个人会被杀掉,计算一圈人中哪两个人最后会存活。使用循环链表解决该问题。

题解:

function survival (n, m) {
if (n <= 2) {
return;
}
var clink = new CLink();
for (var i = 1; i <= n; i++) {
clink.append(i);
}
var p = clink.head,
count = 0;
while (n > 2) {
p = p.next;
if (p === clink.head) {
continue;
}
count++;
if (count === m) {
clink.remove(p.el);
count = 0;
n--;
}
}
console.log('幸存者:');
clink.display();
}

测试:

survival(10, 3);

运行结果:

幸存者:
4
10

解析:

首先,我们把 n 个人按1-n的序号排好,然后按照每第 m 个人死亡来循环遍历循环链表,直到剩下两个人为止。

1 2 3 4 5 6 7 8 9 10
x x x
x x
x x
x

JavaScript 链表的更多相关文章

  1. 【数据结构与算法】--JavaScript 链表

    一.介绍 JavaScript 原生提供了数组类型,但是却没有链表,虽然平常的业务开发中,数组是可以满足基本需求,但是链表在大数据集操作等特定的场景下明显具有优势,那为何 JavaScript 不提供 ...

  2. JavaScript链表

        //实现列表类     function list() {         this.listSize = 0;//元素个数 属性         this.pos = 0;//当前位置 属性 ...

  3. JavaScript中常见数据结构

    数据结构 栈:一种遵从先进后出 (LIFO) 原则的有序集合:新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底.在栈里,新元素都靠近栈顶,旧元素都接近栈底. 队列:与上相反,一种遵循先进 ...

  4. 学习javascript数据结构(二)——链表

    前言 人生总是直向前行走,从不留下什么. 原文地址:学习javascript数据结构(二)--链表 博主博客地址:Damonare的个人博客 正文 链表简介 上一篇博客-学习javascript数据结 ...

  5. 用JavaScript来实现链表LinkedList

    本文版权归博客园和作者本人共同所有,转载和爬虫请注明原文地址. 写在前面 好多做web开发的朋友,在学习数据结构和算法时可能比较讨厌C和C++,上学的时候写过的也忘得差不多了,更别提没写过的了.但幸运 ...

  6. 数据结构与算法之链表-javascript实现

    链表的定义: 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的.链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成.每个结点 ...

  7. 数据结构与算法JavaScript (三) 链表

    我们可以看到在javascript概念中的队列与栈都是一种特殊的线性表的结构,也是一种比较简单的基于数组的顺序存储结构.由于javascript的解释器针对数组都做了直接的优化,不会存在在很多编程语言 ...

  8. javascript数据结构与算法--链表

    链表与数组的区别?  1. 定义: 数组又叫做顺序表,顺序表是在内存中开辟一段连续的空间来存储数据,数组可以处理一组数据类型相同的数据,但不允许动态定义数组的大小,即在使用数组之前必须确定数组的大小. ...

  9. javascript中的链表结构—从链表中删除元素

    1.概念 上一个博文我们讲到链表,其中有一个方法remove()是暂时注释的,这个方法有点复杂,需要添加一个Previous()方法找到要删除的元素的前一个节点,这一个博文我们来分析一下这个remov ...

随机推荐

  1. 使用vsftpd 搭建ftp服务

    ftp 基础服务器基础知识 ftp有三种登录方式.匿名登录(所有用户).本地用户.虚拟用户(guest). FTP工作模式 主动模式:服务端从20端口主动向客户端发起链接. 控制端口21:数据传输端口 ...

  2. SSH 信任关系建立

    需求hostA通过ssh登陆到hostB,实现免密登陆,以及SCP的免密传送文件 由于hostA要登陆到hostB 首先需要在hostA上生成密钥,使用以下命令 ssh-keygen -t rsa 按 ...

  3. etcd原理详解代码剖析

    1 架构 从etcd的架构图中我们可以看到,etcd主要分为四个部分. HTTP Server: 用于处理用户发送的API请求以及其它etcd节点的同步与心跳信息请求. Store:用于处理etcd支 ...

  4. k8s入坑之路(5)kube-apiserver详解

    API Server kube-apiserver 是 Kubernetes 最重要的核心组件之一,主要提供以下的功能 提供集群管理的 REST API 接口,包括认证授权.数据校验以及集群状态变更等 ...

  5. 1. 处理静态资源 2. controller如何接受请求得参数 3. 如何把controller得数据保存到view. 4. 在controller如何完成重定向到指定路径 5. controller返回json数据

    1. 1. 处理静态资源2. controller如何接受请求得参数3. 如何把controller得数据保存到view.4. 在controller如何完成重定向到指定路径5. controller ...

  6. [cf1326F]Wise Men

    对答案序列求一个高维后缀和,再通过差分将其解出,后者复杂度为$o(n2^{n})$ 对于求后缀和后的结果,即01序列仅要求1处有边(不要求0处没有边),那么也即要求将原图划分为若干条长度给定且没有公共 ...

  7. [loj3256]火灾

    将问题差分,即求$\sum_{i=1}^{r}S_{i}(t)-\sum_{i=1}^{l-1}S_{i}(t)$,由于两者类似,不妨考虑前者 构造矩阵$A_{i,j}=S_{j}(i)-S_{j}( ...

  8. [atARC062F]Painting Graphs with AtCoDeer

    求出点双后缩点,对于点双之间,显然不存在简单环,即每一个简单环一定在一个点双内部,换言之即每一个点双可以独立的考虑,然后将结果相乘 (对于点双之间的边任意染色,即若有$s$条边,还会有$k^{s}$的 ...

  9. 对象池模式(Object Pool Pattern)

    本文节选自<设计模式就该这样学> 1 对象池模式的定义 对象池模式(Object Pool Pattern),是创建型设计模式的一种,将对象预先创建并初始化后放入对象池中,对象提供者就能利 ...

  10. java 代理模式实现代码

    目录 1.静态代理 2.动态代理 1.静态代理 接口类AdminService.java接口 public interface AdminService { void update(); Object ...