JavaScript 版数据结构与算法(三)链表
今天,我们要讲的是数据结构与算法中的链表。
链表简介
链表是什么?链表是一种动态的数据结构,这意味着我们可以任意增删元素,它会按需扩容。为何要使用链表?下面列举一些链表的用途:
- 因为数组的存储有缺陷:增删元素时往往需要移动元素。而链表在内存中的放置并不是连续的,元素通过 next 属性指向下个元素,所以链表增删元素,不需要移动元素,只需要更改 next 的指向即可。
- 在生活中,最形象的链表莫过于火车了,车头是 head,每一节车厢都有一个 next 用于连接后面的车厢,想增删车厢,只需要更改 next 即可。
- 在使用分离链接法解决散列表冲突时,我们也会用链表存储位置冲突的元素。
- 在 JavaScript 这门语言中有两个非常重要的链:作用域链和原型链。学习链表对于理解 JavaScript 的这两个特性也非常有帮助。
使用 JavaScript 编写链表类
与前面两节课相同,编写链表类,我们仍然使用构造器函数的方法。
function LinkedList() {
...
}
module.exports = LinkedList;
私有变量
与栈和队列不同,链表类的私有变量不是一个数组,而是一个指针 head。这个指针其实就是指向某个对象的普通变量而已。除此之外,我们还要定义私有变量 length 来记录链表的长度和一个私有的构造器函数 Node 来构建包含 next 属性的链表元素。
function LinkedList() {
var Node = function (element) {
this.element = element;
this.next = null;
};
var length = 0;
var head = null;
}
那么链表元素究竟在代码中长什么样呢?假设一个链表先后有 15,10 两个元素,那么这个链表其实就长这样:
{
element: 15,
next: {
element : 10,
next: null
}
}
私有变量 head 就指向 element 为 15 的那个对象,length 就是 2,构造器函数 Node 仅仅用来创建链表元素。
实现 append 和 toString 方法
了解了私有变量,我们来实现各种类方法。我们期望链表类拥有 append 和 toString 方法,即追加元素和转为字符串,可以跑通下面的测试:
var linkedList = new LinkedList();
// 添加15
linkedList.append(15);
// 添加10
linkedList.append(10);
// 转化为字符串
expect(linkedList.toString()).toBe('15,10');
如果仅仅是为了跑通上述测试,那么非常简单,只需要用数组即可,但是注意我们是给链表类实现方法,所用的数据结构必须为链表才行,所以当 append(15) 时,head 应该为:
{
element: 15,
next: null
}
当 append(10) 时,head 应该为:
{
element: 15,
next: {
element : 10,
next: null
}
}
所以,我们编写的代码如下:
this.append = function (element) {
var node = new Node(element),
current;
// 链表为空直接将 head 指向新元素
if (head === null) {
head = node;
} else {
// 链表不为空需要将 current 移动到最后一个元素
current = head;
while (current.next) {
current = current.next;
}
// 然后将最后一个元素的 next 属性指向新元素
current.next = node;
}
length++;
};
...
this.toString = function () {
var current = head,
string = '';
while (current) {
string += current.element + (current.next ? ',' : '');
current = current.next;
}
return string;
};
上述两个方法都遍历了链表:
while (current) {
// 此处编写循环中的逻辑
...
current = current.next;
}
看到这里,很多不熟悉 JavaScript 的同学可能会问:current = cuuren.next 是什么?让我慢慢解释一下。在 JavaScript 中,变量分为基本类型和引用类型,其中对象类型是引用类型的,也就是说创建一个对象时,在内存开辟了一块地方,后续无论你将这个变量传给多少个其他变量,这些变量都指向同一块内存:
var a = { name: 'lewis' };
b = a;
b.name = 'susan';
console.log(a); // { name: 'susan' }
所以在链表中,我们可以使用 head、current 等变量来指向某个存在内存中的变量:
{
element: 15, // head 指向 element 为 15 的对象
next: {
element : 10, // current 是个临时变量,可以更改它的指向来遍历链表
next: null
}
}
所以 current = current.next 就相当于 current 原来指向 element 为 15 的对象,后来指向了 element 为 10 的对象,因为后者挂在前者的 next 属性上,就像上述代码中的那样。现在你应该明白了吧!更详细的引用类型的知识可以自行谷歌。
实现 removeAt 方法
实现 removeAt 方法,即删除指定位置的元素,可以跑通如下测试:
var linkedList = new LinkedList();
linkedList.append(15);
linkedList.append(10);
// 删除位置小于0的元素时返回 null
expect(linkedList.removeAt(-1)).toBe(null); // 断言一
// 删除位置大于链表长度的元素时返回 null
expect(linkedList.removeAt(3)).toBe(null); // 断言二
// 删除位置为1的元素并返回
expect(linkedList.removeAt(1)).toBe(10); // 断言三
// 删除位置为0的元素并返回
expect(linkedList.removeAt(0)).toBe(15); // 断言四
// 链表现在没有元素了
expect(linkedList.toString()).toBe('');
断言一、二都是异常情况,应该使用条件语句来判断并跳过,断言三、四是正常情况,应该删除元素并返回。
不了解断言和单元测试的同学,可以先看《Jest 单元测试入门》这篇博客。
实现代码如下:
this.removeAt = function (position) {
// 用于跳过异常情况
if (position > -1 && position < length) {
var current = head,
previous,
index = 0;
// 删除头部元素
if (position === 0) {
head = current.next;
} else {
// 找出指定位置元素,并让它的前一个元素连接它的后一个元素
while (index < position) {
previous = current;
current = current.next;
index++;
}
previous.next = current.next;
}
length--;
return current.element;
}
return null;
};
这个方法的技巧是找出指定位置元素,但本质还是遍历链表,只是终止条件有差别而已:
while (index < position) {
// 代码逻辑
...
current = current.next;
index++;
}
实现 insert 方法
实现 insert 方法,即向指定位置插入指定元素,跑通如下测试:
var linkedList = new LinkedList();
expect(linkedList.insert(0, 15)); // 断言一
expect(linkedList.insert(1, 12)); // 断言二
expect(linkedList.insert(0, 10)); // 断言三
expect(linkedList.insert(-1, 8)); // 断言四
expect(linkedList.insert(4, 8)); // 断言五
expect(linkedList.toString()).toBe('10,15,12');
断言一、三是往头部插入,断言二是往非头部插入,断言四、五都是异常非法输入。实现代码如下:
this.insert = function (position, element) {
// 用于跳过非法输入,对应第四个和第五个断言
if (position > -1 && position <= length) {
var node = new Node(element),
current = head,
previous,
index = 0;
// 往头部插入,对应第一个和第三个断言
if (position === 0) {
node.next = current;
head = node;
} else {
// 往非头部插入,对应第二个断言
while (index < position) {
previous = current;
current = current.next;
index++;
}
node.next = current;
previous.next = node;
}
length++;
return true;
}
return false;
};
这个方法的技巧也是在链表中查找指定元素,其他都是无聊的边界判断。
实现 indexOf 方法
实现 indexOf 方法,即返回指定位置的元素,跑通如下测试。
var linkedList = new LinkedList();
linkedList.append(15);
linkedList.append(10);
expect(linkedList.indexOf(12)).toBe(2); // 断言一
expect(linkedList.indexOf(8)).toBe(-1); // 断言二
断言一是正常情况,返回 position,断言二没有该元素返回 -1 。技巧还是在链表中遍历查找元素。
this.indexOf = function (element) {
var current = head,
index = 0;
while (current) {
if (element === current.element) {
return index;
}
index++;
current = current.next;
}
return -1;
};
其他方法比较简单不再赘述。
总结
玩转链表,有以下技巧:
- 确定私有变量和元素结构,主要包括一个 head 指针,一个构造器函数 Node,用于生成包含 next 属性的对象。
- 掌握遍历链表的方法,即使用 while 循环,通过
current = curren.next来遍历。 - 学习在遍历链表时使用
previous来记录当前节点的上一个节点。 - 考虑各种边界情况:空链表、在查找范围外等情况。
除了掌握上述技巧,动手写代码也是很重要的!今天,就到此为止。
教程示例代码及目录
示例代码:https://github.com/lewis617/javascript-datastructures-algorithms
目录:http://www.liuyiqi.cn/tags/数据结构与算法/
JavaScript 版数据结构与算法(三)链表的更多相关文章
- JavaScript 版数据结构与算法(二)队列
今天,我们要讲的是数据结构与算法中的队列. 队列简介 队列是什么?队列是一种先进先出(FIFO)的数据结构.队列有什么用呢?队列通常用来描述算法或生活中的一些先进先出的场景,比如: 在图的广度优先遍历 ...
- JavaScript 版数据结构与算法(四)集合
今天,我们要讲的是数据结构与算法中的集合. 集合简介 什么是集合?与栈.队列.链表这些顺序数据结构不同,集合是一种无序且唯一的数据结构.集合有什么用?在 Python 中,我经常使用集合来给数组去重: ...
- JavaScript 版数据结构与算法(一)栈
今天,我们要讲的是数据结构与算法中的栈. 栈的简介 栈是什么?栈是一个后进先出(LIFO)的数据结构.栈有啥作用?栈可以模拟算法或生活中的一些后进先出的场景,比如: 十进制转二进制,你需要将余数倒序输 ...
- Android版数据结构与算法(三):基于链表的实现LinkedList源码彻底分析
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. LinkedList 是一个双向链表.它可以被当作堆栈.队列或双端队列进行操作.LinkedList相对于ArrayList来说,添加,删除元素效 ...
- javascript实现数据结构与算法系列:栈 -- 顺序存储表示和链式表示及示例
栈(Stack)是限定仅在表尾进行插入或删除操作的线性表.表尾为栈顶(top),表头为栈底(bottom),不含元素的空表为空栈. 栈又称为后进先出(last in first out)的线性表. 堆 ...
- JavaScript版EAN码校验算法
<script type="text/javascript"> $(document).ready(function () { $("#btnCalc&q ...
- 第一章:javascript: 数据结构与算法
在前端工程师中,常常有一种声音,我们为什么要学数据结构与算法,没有数据结构与算法,我们一样很好的完成工作.实际上,算法是一个宽泛的概念,我们写的任何程序都可以称为算法,甚至往冰箱里放大象,也要通过开门 ...
- Android版数据结构与算法(一):基础简介
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 一.前言 项目进入收尾阶段,忙忙碌碌将近一个多月吧,还好,不算太难,就是麻烦点. 数据结构与算法这个系列早就想写了,一是梳理总结,顺便逼迫自己把一 ...
- Android版数据结构与算法(七):赫夫曼树
版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 近期忙着新版本的开发,此外正在回顾C语言,大部分时间没放在数据结构与算法的整理上,所以更新有点慢了,不过既然写了就肯定尽力将这部分完全整理好分享出 ...
随机推荐
- 关于用VMware克隆linux系统后,无法联网找不到eth0网卡的问题
当使用克隆后的虚拟机时发现系统中的网卡eth0没有了,使用ifconfig -a会发现只有eth1.因为系统是克隆过来的,原有的eth0以及ip地址都是原先网卡的,VMware发现已经被占用,就会创建 ...
- poj 3683 2-sat建图+拓扑排序输出结果
发现建图的方法各有不同,前面一题连边和这一题连边建图的点就不同,感觉这题的建图方案更好. 题意:给出每个婚礼的2个主持时间,每个婚礼的可能能会冲突,输出方案. 思路:n个婚礼,2*n个点,每组点是对称 ...
- Cetnos搭建vsftp服务器
1.首先yum安装vsftp server 以3.0.2为例 命令:yum -y install vsftpd 2.配置文件 vsftp.conf 具体配置内容如下: anonymous_ena ...
- WeTest+微信:小程序云端测试系统上线
日前,微信新增小程序测试系统,可便于开发者检测小程序缺陷,评估小程序产品质量.在小程序发布之前,开发者可将小程序代码提交到测试系统,在不同型号的手机真机上运行,执行完毕后自动生成测试报告.小程序云端测 ...
- c# 网页打印全流程
说明:我要实现的就是将数据库中Group表的数据查找出来,替换打印模版中的内容,再将模版文件打印出来 1.准备好要打印的模版group_O_train.html <div class=" ...
- js数组、内置对象、自定义对象
[js中的数组] 1.数组的基本概念? 数组是在内存空间中连续存储的一组有序数据的集合 元素在数组中的顺序,称为下标.可以使用下表访问数字的每个元素. 2.如何声明一个数组? ① 使用字面量声明: 在 ...
- JAVA中String = null 与 String = "" 的区别
JAVA中String = null 与 String = ""的区别 笔者今天在Debug的时候发现的NPE(NullPointerException),辛辛苦苦地调试了半天,终 ...
- OpenCppCoverage 的使用
OpenCppCoverage 的使用 OpenCppCoverage 是一款好用方便的 C++ 代码覆盖率检测工具,可以独立在命令行运行也可以作为 Visual Studio 13/15/17 的插 ...
- 结对编程1-四则运算GUI实现(58、59)
题目描述 我们在个人作业1中,用各种语言实现了一个命令行的四则运算小程序.进一步,本次要求把这个程序做成GUI(可以是Windows PC 上的,也可以是Mac.Linux,web,手机上的),成为一 ...
- 扫雷游戏制作过程(C#描述):第二节、界面设计
前言 这里给出教程原文地址. 该项目已经放在github上托管. 扫雷界面设计 界面的设计,首先需要创建一个菜单栏.具体方法在左边找到工具箱窗口,展开其中的菜单和工具栏,找到MenuStrip选项,双 ...