1. 线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2. 顺序表
2.1 概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
注意: 数组 和 顺序表之间的区别和联系
顺序表是数据结构中的概念, 更广义. 各种编程语言中都可以实现顺序表.
数组是编程语言中的概念, 有的语言有数组, 有的语言没有, 有的语言中数组功能强大, 有的语言数组功能羸弱.
数组可以理解成顺序表的一种具体实现形式.
2.2 实现
我们来实现一个动态顺序表. 以下是需要支持的接口.
此处我们不借助 JS 数组内置的方法, 就使用最简单的数组操作(下标访问) 来实现, 这样有助于我们更好的理解顺序表的原理.
function ArrayList() { this.data = []; // 打印顺序表元素 this.print = function (e) { var log = ''; for (var i = 0; i < this.data.length; i++) { log += this.data[i]; if (i < this.data.length - 1) { log += ', '; } } console.log(log); } // 尾插 this.pushBack = function (e) { // 插入完毕, length 会自动自增 this.data[this.data.length] = e; } // 尾删 this.popBack = function () { this.data.length -= 1; } // 头插 this.pushFront = function (e) { // 头插需要从后往前向后搬运一格 for (var i = this.data.length; i > 0; i--) { this.data[i] = this.data[i - 1]; } // 插入新元素 this.data[i] = e; } // 头删 this.popFront = function () { // 头删需要从前往后向前搬运一格 for (var i = 0; i < this.data.length - 1; i++) { this.data[i] = this.data[i + 1]; } // 再删除最后一个元素 this.data.length -= 1; } // 根据下标获取元素 this.get = function (i) { return this.data[i]; } // 根据下标设置元素 this.set = function (i, e) { this.data[i] = e; } // 中间指定位置插入, pos 表示插入到哪个下标上 this.insert = function (pos, e) { // 从后往前向后搬运一格元素. 搬到 pos 为止 for (var i = this.data.length; i > pos; i--) { this.data[i] = this.data[i - 1]; } // 把新元素放到 pos 位置上 this.data[pos] = e; } // 查找元素所在的位置 this.indexOf = function (e) { for (var i = 0; i < this.data.length; i++) { if (this.data[i] === e) { return i; } } return -1; } // 按照位置删除 this.erase = function (pos) { // 从 pos 位置开始, 从前往后遍历, 向前搬运一格 for (var i = pos; i < this.data.length - 1; i++) { this.data[i] = this.data[i + 1]; } // 删除最后一个位置的元素 this.data.length -= 1; } // 按照值删除元素 this.remove = function (e) { var pos = this.indexOf(e); if (pos == -1) { return; } this.erase(pos); } // 获取元素个数 this.size = function () { return this.data.length; } // 清空 this.clear = function () { this.data.length = 0; }}
2.3 顺序表要点
元素存放在连续的空间上.
随机访问效率比较高 O(1)
在尾部插入/删除元素效率较高. O(1)
在元素中间位置插入/删除元素效率比较低 O(N), 需要搬运元素.
注意: JavaScript 中的数组, 并不是严格意义上的顺序表.
在 V8 引擎中, 数组有两种实现方式:
快数组: 连续内存空间上的顺序表
慢数组: 本质上是 hash 表
初始情况下数组是快数组的形态. 如果当前数组中存在很多的 "空洞元素", 此时就会自动转换成慢数组, 降低空间的开销. 例如如下代码
var a = [1, 2]a[1030] = 1;
具体可以参考 https://zhuanlan.zhihu.com/p/96959371
PS: 研究 JS 的底层实现, 最好的办法就是阅读 JS 引擎源码. 其中比较知名的就是 V8 引擎(Chrome / Node.js 中采用的 JS 引擎). 阅读引擎代码需要一定的 C++ 基础.
V8 源码地址: https://github.com/v8/v8
3. 链表
3.1 链表的概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
单向、双向
带头、不带头
循环、非循环
虽然有这么多的链表的结构,但是我们重点掌握两种:
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
无头双向链表
3.2 链表的实现
主要实现两种:
1) 不带头结点(傀儡节点), 单向, 不带环的链表
// 构造一个链表对象function LinkedList() { // 构造一个节点 function ListNode(val) { this.val = val; this.next = null; } // 表示链表的头结点 this.head = null; // 打印链表 this.print = function () { var log = ''; for (var cur = this.head; cur != null; cur = cur.next) { log += cur.val; if (cur.next != null) { log += ', '; } } console.log(log); } // 头插 this.pushFront = function (e) { var node = new ListNode(e); if (this.head == null) { // 当前链表为空的情况 this.head = node; return; } // 当前链表非空的情况 node.next = this.head; this.head = node; } // 头删 this.popFront = function () { if (this.head == null) { // 空链表, 不需要删除 return; } this.head = this.head.next; } // 尾插 this.pushBack = function (e) { var node = new ListNode(e); if (this.head == null) { // 空链表情况 this.head = node; return; } // 非空情况, 需要找到链表尾部 var cur = this.head; while (cur.next != null) { cur = cur.next; } // 此时 cur 就指向链表最后一个节点了. cur.next = node; } // 尾删 this.popBack = function () { if (this.head == null) { // 空链表不需要删除 return; } if (this.head.next == null) { // 只有一个节点, 直接删除 this.head = null; return; } // 非空情况, 且存在多个节点, 需要找到尾节点的前一个链表 var prev = this.head; var cur = this.head.next; while (cur.next != null) { prev = prev.next; cur = cur.next; } // 此时, cur 指向最后一个元素, prev 指向 cur 之前. prev.next = null; } // 根据下标获取元素 this.get = function (index) { var cur = this.head; for (var i = 0; i < index; i++) { cur = cur.next; } return cur.val; } // 根据下标获取节点 this.getNode = function (index) { var cur = this.head; for (var i = 0; i < index; i++) { cur = cur.next; } return cur; } // 根据下标设置元素 this.set = function (index, e) { var cur = this.head; for (var i = 0; i < pos; i++) { cur = cur.next; } cur.val = e; } // 获取链表长度 this.size = function () { var size = 0; for (var cur = this.head; cur != null; cur = cur.next) { size++; } return size; } // 中间位置插入(插入到指定位置之后) // pos 为节点引用 this.insertAfter = function (pos, e) { var node = new ListNode(e); node.next = pos.next; pos.next = node; } // 中间位置插入(插入到指定位置之前) // pos 为节点引用 this.insertBefore = function (pos, e) { if (pos == this.head) { // 如果是插入在头结点之前, 则直接调用头插就好 this.pushFront(e); return; } // 先找到 pos 前一个节点位置 var cur = this.head; while (cur != null) { if (cur.next == pos) { break; } cur = cur.next; } if (cur == null) { // 没找到 pos 元素, 说明 pos 是非法元素 return; } var node = new ListNode(e); node.next = cur.next; cur.next = node; } // 查找元素 e 所在的节点 this.find = function (e) { for (var cur = this.head; cur != null; cur = cur.next) { if (cur.val === e) { return cur; } } return null; } // 指定位置删除元素 this.erase = function (pos) { // 如果链表为空, 不需要删除 if (this.head == null) { return; } // 如果 pos 为头结点, 直接调用头删 if (pos == this.head) { this.popFront(); return; } // 先找到 pos 的前一个位置 var cur = this.head; while (cur != null) { if (cur.next == pos) { break; } cur = cur.next; } // 删除 pos 元素 cur.next = pos.next; } // 指定位置删除元素(要求时间复杂度 O(1)) this.eraseFast = function (pos) { // 如果链表为空, 不需要删除 if (this.head == null) { return; } // 如果 pos 为头结点, 直接调用头删 if (pos == this.head) { this.popFront(); return; } // 如果 pos 是最后一个节点, 直接调用尾删 if (pos.next == null) { this.popBack(); return; } var nextNode = pos.next; pos.val = nextNode.val; pos.next = nextNode.next; } // 指定值删除元素 this.remove = function (e) { var pos = this.find(e); this.eraseFast(pos); }}
2) 带头结点(傀儡节点), 双向, 带环链表
function DLinkedList() { function DListNode(val) { this.val = val; this.prev = this; this.next = this; } // 创建一个傀儡节点 this.head = new DListNode(-1); // 打印整个链表 this.print = function () { var log = ''; for (var cur = this.head.next; cur != this.head; cur = cur.next) { log += cur.val; if (cur.next != this.head) { log += ', '; } } console.log(log); } // 尾插, 本质上是插入到 head 之前 this.pushBack = function (e) { var prev = this.head.prev; var next = this.head; var cur = new DListNode(e); prev.next = cur; cur.prev = prev; cur.next = next; next.prev = cur; } // 尾删, 本质上是删除 head 之前的元素 this.popBack = function () { var toDel = this.head.prev; var prev = toDel.prev; var next = toDel.next; prev.next = next; next.prev = prev; } // 头插,本质上是插入到 head 后面 this.pushFront = function (e) { var prev = this.head; var next = this.head.next; var cur = new DListNode(e); prev.next = cur; cur.prev = prev; cur.next = next; next.prev = cur; } // 头删,本质上是删除 head 后面的元素 this.popFront = function () { var toDel = this.head.next; var prev = toDel.prev; var next = toDel.next; prev.next = next; next.prev = prev; } // 根据下标获取节点 this.getNode = function (pos) { if (pos >= this.size()) { return null; } var cur = this.head.next; for (var i = 0; i < pos; i++) { cur = cur.next; } return cur; } // 根据下标获取元素 this.get = function (pos) { var cur = this.getNode(pos); return cur != null ? cur.val : null; } this.set = function (pos, e) { var cur = this.getNode(pos); if (cur == null) { return; } cur.val = e; } this.size = function () { var size = 0; for (var cur = this.head.next; cur != this.head; cur = cur.next) { size++; } return size; } // 插入到 pos 节点之后 this.insertAfter = function (pos, e) { var prev = pos; var next = pos.next; var cur = new DListNode(e); prev.next = cur; cur.prev = prev; cur.next = next; next.prev = cur; } // 插入到 pos 节点之前 this.insertBefore = function (pos, e) { var prev = pos.prev; var next = pos; var cur = new DListNode(e); prev.next = cur; cur.prev = prev; cur.next = next; next.prev = cur; } // 查找元素 e 所在节点 this.find = function (e) { for (var cur = this.head.next; cur != this.head; cur = cur.next) { if (cur.val === e) { return cur; } } return null; } // 删除指定位置的元素 this.erase = function (pos) { var prev = pos.prev; var next = pos.next; prev.next = next; next.prev = prev; } // 指定值删除元素 this.remove = function (e) { var toDel = this.find(e); if (toDel == null) { return; } this.erase(toDel); }}
3. 顺序表和链表的区别和联系 [面试必考]
顺序表元素存储在连续内存空间上,链表元素不在连续空间上。
顺序表随机访问效率高 O(1), 但是在中间位置插入删除元素效率低O(N)
链表随机访问效率低O(N), 但是在中间位置插入删除元素效率高O(1)
如果是双向链表, 在给定位置插入删除元素, 效率都是 O(1)
如果是单向链表, 在给定位置之后插入元素效率高 O(1), 在制定位置前插入元素效率低O(N)
如果是单向链表, 在给定位置之后删除元素效率高 O(1), 在制定位置删除元素效率低 O(N), 当然这里有特殊的优化做法, 可以勉强做到 O(1)