数据结构,直白的理解,就是研究数据的存储方式。算法
数据结构是一门学科,它教会咱们「如何存储具备复杂关系的数据更有助于后期对数据的再利用」。编程
数据结构大体包含如下几种存储结构:数组
线性表数据结构
树结构编程语言
下面对各类数据结构作详细讲解。测试
将具备 「一对一」 关系的数据 「线性」 地存储到物理空间中,这种存储结构就称为线性存储结构(简称线性表)。this
使用线性表存储数据的方式能够这样理解,即「把全部数据用一根线儿串起来,再存储到物理空间中」。
使用线性表存储的数据,要求数据类型必须一致。
线性表并非一种具体的存储结构,它包含顺序存储结构和链式存储结构,是顺序表和链表的统称。spa
上图中咱们能够看出,线性表存储数据可细分为如下 2
种:设计
3a)
所示,将数据依次存储在连续的整块物理空间中,这种存储结构称为顺序存储结构(简称顺序表);3b)
所示,数据分散的存储在物理空间中,经过一根线保存着它们之间的逻辑关系,这种存储结构称为链式存储结构(简称链表);数据结构中,一组数据中的每一个个体被称为数据元素,简称元素。指针
另外,对于具备 「一对一」 逻辑关系的数据,咱们一直在用「某一元素的左侧(前边)或右侧(后边)」这样不专业的词,其实线性表中有更准确的术语:
以下图,元素 3
它的直接前驱是 2
,此元素的前驱元素有 2
个,分别是 1
和 2
;同理,此元素的直接后继是 4
,后继元素也有 2
个,分别是 4
和 5
。
顺序表,全名顺序存储结构,是线性表的一种。
顺序表存储数据时,会提早申请一整块足够大小的物理空间,而后将数据依次存储起来,存储时作到数据元素之间不留一丝缝隙。
顺序表,简单的理解就是经常使用的数组,例如使用顺序表存储 [1, 2, 3, 4, 5]
,如图:
因为顺序表结构的底层实现借助的就是数组,所以对于初学者来讲,能够把顺序表彻底等价为数组,但实则不是这样。数据结构是研究数据存储方式的一门学科,它囊括的都是各类存储结构,而数组只是各类编程语言中的基本数据类型,并不属于数据结构的范畴。
使用顺序表存储数据以前,除了要申请足够大小的物理空间以外,为了方便后期使用表中的数据,顺序表还须要实时记录如下 2
项数据:
因为顺序表能够简单的理解为数组,因此在这里就定义一个数组,将数组看成一个顺序表来处理。
和大多数其余语言不一样,JavaScript
数组的length
是没有上界的。 因此存储容量也是动态变化的。
// 定义一个顺序表 function List(list) { this.data = list || []; this.getLength = getLength; this.clear = clear; // 清空顺序表 this.insertEl = insertEl; // 插入元素 this.removeEl = removeEl; // 删除元素 this.changeEl = changeEl; // 更改元素 this.findEl = findEl; // 查找元素 }
向已有顺序表中插入数据元素,根据插入位置的不一样,可分为如下 3
种状况:
虽然数据元素插入顺序表中的位置有所不一样,可是都使用的是同一种方式去解决,即:经过遍历,找到数据元素要插入的位置,而后作以下两步工做:
例如,在 [1, 2, 3, 4, 5]
的第 3 个位置上插入元素 6
,实现过程以下:
3
个数据元素的位置。3
以及后续元素 4
和 5
总体向后移动一个位置。6
放入腾出的位置。function getLength() { return this.data.length; } /** * @param {any} el 要插入的元素 * @param {Number} index 元素下标 */ function insertEl(el, index) { if (index > this.getLength() || index < 0) { throw new Error('插入位置有错'); } else { // 插入操做,须要将从插入位置开始的后续元素,逐个后移 for (var len = this.getLength(); len >= index; len--) { this.data[len] = this.data[len - 1]; } // 后移完成后,直接将所需插入元素,添加到顺序表的相应位置 this.data[index] = el; return this.data; } }
为数组添加元素,能够经过push()
、unshift()
、splice(index, 0, element)
方法实现。
// 向数组头部添加元素: arr.unshift(); // 向数组末尾添加元素: arr.push(); // 向数组下标 index 处插入元素: arr.splice(index, 0, el)
从顺序表中删除指定元素,只需找到目标元素,并将其后续全部元素总体前移 1
个位置便可。
后续元素总体前移一个位置,会直接将目标元素删除,可间接实现删除元素的目的。
例如,从 [1, 2, 3, 4, 5]
中删除元素 3
的过程,以下图所示:
js 代码实现:
/** * @param {Number} index 被删除元素的下标 */ function removeEl(index) { if (index > this.getLength() || index < 0) { throw new Error('删除位置有误'); } else { // 删除操做 for (var i = index; i < this.getLength() - 1; i++) { this.data[i] = this.data[i + 1]; } this.data.length--; return this.data; } }
删除数组的元素,能够经过pop()
、shift()
、splice(index, 1)
方法实现。
// 从数组头部删除元素: arr.shift(); // 从数组末尾删除元素: arr.pop(); // 从数组下标 index 处删除一个元素: arr.splice(index, 1);
顺序表中查找目标元素,可使用多种查找算法实现,好比说二分查找算法、插值查找算法等。
这里,咱们选择顺序查找算法,具体实现代码为:
/** * @param {any} el 须要查找的元素 */ function findEl(el) { for (let i = 0; i < this.getLength(); i++) { if (this.data[i] == el) { return i; // 第一次匹配到的元素的下标 } } return -1; //若是查找失败,返回 -1 }
查找数组元素的下标,可使用indexOf()
、lastIndexOf()
方法实现。
顺序表更改元素的实现过程是:
/** * @param {any} el 须要更改的元素 * @param {any} newEl 为新的数据元素 */ function changeEl( el, newEl) { const index = this.findEl(el); this.data[index] = newEl; return this.data; }
以上是顺序表使用过程当中最经常使用的基本操做,这里给出完整的实现代码:
class List { constructor(list) { this.data = list || []; } clear() { delete this.data; this.data = []; return this.data; } getLength() { return this.data.length; } insertEl(index, el) { if (index > this.getLength() || index < 0) { throw new Error('插入位置有错'); } else { // 插入操做,须要将从插入位置开始的后续元素,逐个后移 for (var len = this.getLength(); len >= index; len--) { this.data[len] = this.data[len - 1]; } // 后移完成后,直接将所需插入元素,添加到顺序表的相应位置 this.data[index] = el; return this.data; } } // 经过下标删除元素 removeEl(index) { if (index > this.getLength() || index < 0) { throw new Error('删除位置有误'); } else { // 删除操做 for (var i = index; i < this.getLength() - 1; i++) { this.data[i] = this.data[i + 1]; } this.data.length--; return this.data; } } changeEl(el, newEl) { const index = this.findEl(el); this.data[index] = newEl; return this.data; } findEl(el) { for (let i = 0; i < this.getLength(); i++) { if (this.data[i] == el) { return i; // 第一次匹配到的元素的下标 } } return -1; //若是查找失败,返回 -1 } } const list = new List([1, 2, 3, 4, 5]) console.log('初始化顺序表:') console.log(list.data.join(',')); console.log('删除下标为 0 的元素:') console.log(list.removeEl(0).join(',')); console.log('在下标为 3 的位置插入元素 6:') console.log(list.insertEl(3, 6).join(',')); console.log('查找元素 3 的下标:') console.log(list.findEl(3)); console.log('将元素 3 改成 6:') console.log(list.changeEl(3, 6).join(','))
程序运行结果:
初始化顺序表: 1,2,3,4,5 删除下标为 0 的元素: 2,3,4,5 在下标为 3 的位置插入元素 6: 2,3,4,6,5 查找元素 3 的下标: 1 将元素 3 改成 6: 2,6,4,6,5
数组不老是组织数据的最佳数据结构,缘由以下。在不少编程语言中,数组的长度是固定的,因此当数组已被数据填满时,再要加入新的元素就会很是困难。在数组中,添加和删除元素也很麻烦,由于须要将数组中的其余元素向前或向后平移,以反映数组刚刚进行了添加或删除操做。
然而,JavaScript
的数组并不存在上述问题,由于使用 split()
方法不须要再访问数组中的其余元素了。
JavaScript
中数组的主要问题是,它们被实现成了对象,与其余语言(好比 C++ 和 Java) 的数组相比,效率很低(请参考 Crockford 那本书的第 6 章)。
若是你发现数组在实际使用时很慢,就能够考虑使用链表
来替代它。除了对数据的随机访问,链表
几乎能够用在任何可使用一维数组的状况中。若是须要随机访问,数组仍然是更好的选择。
链表,别名链式存储结构
或单链表
,用于存储逻辑关系为 「一对一」 的数据。
咱们知道,使用顺序表(底层实现靠数组)时,须要提早申请必定大小的存储空间,这块存储空间的物理地址是连续的,以下图所示。
链表则彻底不一样,使用链表存储数据时,是随用随申请,所以数据的存储位置是相互分离的,换句话说,数据的存储位置是随机的。
咱们看到,上图根本没法体现出各数据之间的逻辑关系。对此,链表的解决方案是,每一个数据元素在存储时都配备一个指针,用于指向本身的直接后继元素,以下图所示。
数据元素随机存储,并经过指针表示数据之间逻辑关系的存储结构就是链式存储结构。
链表中每一个数据的存储都由如下两部分组成:
链表中存储各数据元素的结构以下图所示:
上图所示的结构在链表中称为节点。也就是说,链表实际存储的是一个一个的节点,真正的数据元素包含在这些节点中
:
其实,一个完整的链表须要由如下几部分构成:
头指针
:一个普通的指针,它的特色是永远指向链表第一个节点的位置。很明显,头指针用于指明链表的位置,便于后期找到链表并使用表中的数据;节点
:链表中的节点又细分为头节点
、首元节点
和其余节点
:
头节点
:其实就是一个不存任何数据的空节点,一般做为链表的第一个节点。对于链表来讲,头节点不是必须的,它的做用只是为了方便解决某些实际问题;首元节点
:因为头节点(也就是空节点)的缘故,链表中称第一个存有数据的节点为首元节点。首元节点只是对链表中第一个存有数据节点的一个称谓,没有实际意义;其余节点
:链表中其余的节点;一个存储 [1, 2, 3]
的完整链表结构如图所示:
链表中有头节点时,头指针指向头节点;反之,若链表中没有头节点,则头指针指向首元节点。
咱们设计的链表包含两个类:
Node
类用来表示节点;LinkedList
类提供了插入的节点、删除节点、显示列表元素的方法,以及其余辅助方法。Node
类包含两个属性,element
和 next
。
// Node 类 class Node { constructor(element) { this.element = element; // element 用来保存节点上的数据 this.next = null; // next 用来保存指向下一节点的指针 } }
LinkedList
类提供了对链表进行操做的方法。链表只有一个属性,那就是使用一个 Node
对象来保存该链表的头节点。
function LinkedList() { // head 节点的 next 属性被初始化为 null,当有新元素插入时,next 会指向新的元素 this.head = new Node('head'); this.find = find; this.insert = insert; this.findPrevious = findPrevious; this.remove = remove; this.display = display; }
同顺序表同样,向链表中增添元素,根据添加位置不一样,可分为如下 3
种状况:
虽然新元素的插入位置不固定,可是链表插入元素的思想是固定的,只需作如下两步操做,便可将新元素插入到指定的位置:
next
指针指向插入位置后的节点;next
指针指向插入节点;例如,咱们在链表 [1, 2, 3, 4]
的基础上分别实如今头部、中间部位、尾部插入新元素 5
,其实现过程以下图所示:
注意
:链表插入元素的操做必须是先步骤1
,再步骤2
;反之,若先执行步骤2
,除非再添加一个指针,做为插入位置后续链表的头指针,不然会致使插入位置后的这部分链表丢失,没法再实现步骤1
。
建立一个辅助方法 find()
,该方法遍历链表,查找给定数据。
find()
方法实现:
function find(item) { var currNode = this.head; // 建立一个新节点 while (currNode.element != item) { // 若是当前节点数据不符合咱们要找的节点 currNode = currNode.next; // 当前节点移动到一下节点 } return currNode; }
一旦找到 给定
的节点,就能够将新节点插入链表了。首先,将新节点的 next
指针指向 给定
节点 next
指向的节点。而后设置 给定
节点的 next
属性指向新节点。
insert()
方法的定义以下:
function insert(newElement, item) { var newNode = new Node(newElement); var current = this.find(item); // 向链表插入节点 newNode newNode.next = current.next; // 将新节点的 next 指针指向插入位置后的节点 (步骤 1) current.next = newNode; // 设置插入位置前的 next 指针指向新节点(步骤2) }
如今已经能够开始测试咱们的链表实现了。然而在测试以前,先来定义一个 display()
方法,该方法用来显示链表中的元素:
function display() { var currNode = this.head; while(!(currNode.next == null)) { // 当当前节点的 next 指针为 null 时 循环结束 //为了避免显示头节点,程序只访问当前节点的下一个节点中保存的数据 console.log(chrrNode.next.element); currNode = currNode.next; } }
测试程序:
function Node(element) { this.element = element; this.next = null; } function LinkedList() { this.head = new Node('head'); this.find = find; this.insert = insert; this.findPrevious = findPrevious; this.remove = remove; this.display = display; } function find(item) { var currNode= this.head; while(currNode.element != item) { currNode = currNode.next; } return currNode; } function insert(newElement, item) { var newNode = new Node(newElement); var current = this.find(item); newNode.next = current.next; current.next = newNode; } function findPrevious() {} function remove() {} function display() { var currNode = this.head; while(!(currNode.next == null)) { console.log(currNode.next.element) currNode = currNode.next; } } var list = new LinkedList(); list.insert(1, 'head') list.insert(2, 1) list.insert(3, 2) list.insert(4, 3) list.display()
程序运行结果:
1 2 3 4
从链表中删除指定数据元素时,须要进行如下 2
步操做:
直接前驱节点
;next
指针,使其再也不指向待删除节点;从链表中删除节点时,须要先找到待删除节点的直接前驱节点
。找到这个节点后,修改它的 next
指针,咱们能够定义一个方法 findPrevious()
,来作这件事。
findPrevious()
方法遍历链表中的元素,检查每个节点的 直接后继节点
中是否存储着待删除数据。若是找到,返回该节点(待删除节点的直接前驱节点
)。
function findPrevious(item) { var currNode = this.head; while(!(currNode.next == null) && currNode.next.element != item) { currNode = currNode.next; } return currNode; }
删除节点的原理很简单,找到该节点的直接前驱节点 prevNode
后,修改它的 next
指针:
prevNode.next = prevNode.next.next;
remove()
方法的 js
实现:
function remove(item) { var prevNode = this.findPrevious(item); if(!(prevNode.next == null)) { prevNode.next = prevNode.next.next; } }
测试程序:
... var list = new LinkedList(); list.insert(1, 'head') list.insert(2, 1) list.insert(3, 2) list.insert(4, 3) list.display() list.remove(2) list.display()
程序运行结果:
// 没调用 list.remove(2) 以前 1 2 3 4 // 调用list.remove(2) 1 3 4
更新链表中的元素,只需经过遍历找到存储此元素的节点,对节点中的数据域作更改操做便可。
function change(index, newElement) { var currNode = this.head; for(var i = 0; i <= index; i++) { currNode = currNode.next; if (currNode == null) { throw new Error('更新位置无效'); return; } } currNode.element = newElement; return currNode; }
最后给出 js
实现的拥有基本操做 增删改查
的链表完整代码:
class Node { constructor(element) { this.element = element; this.next = null; } } class LinkedList { constructor() { this.head = new Node('head'); } // 链表查找元素 find(item) { var currNode= this.head; while(currNode.element != item) { currNode = currNode.next; } return currNode; } // 链表添加元素 insert(newElement, item) { var newNode = new Node(newElement); var current = this.find(item); newNode.next = current.next; current.next = newNode; } // 链表查找直接前驱节点(辅助方法) findPrevious(item) { var currNode = this.head; while(!(currNode.next == null) && currNode.next.element != item) { currNode = currNode.next; } return currNode; } // 链表删除节点 remove(item) { var prevNode = this.findPrevious(item); if(!(prevNode.next == null)) { prevNode.next = prevNode.next.next; } } // 链表更改节点 change(index, newElement) { var currNode = this.head; for(var i = 0; i <= index; i++) { currNode = currNode.next; if (currNode == null) { throw new Error('更新位置无效'); return; } } currNode.element = newElement; return currNode; } display() { var currNode = this.head; while(!(currNode.next == null)) { console.log(currNode.next.element) currNode = currNode.next; } } } var list = new LinkedList(); list.insert(1, 'head') list.insert(2, 1) list.insert(3, 2) list.insert(4, 3) console.log('初始化链表:') list.display() // 1, 2, 3, 4 list.remove(2) console.log('删除数据为 2 的节点: ') list.display() // 1, 3, 4 list.change(2, 6) console.log('将下标为 2 的 节点数据更改成 6:') list.display() // 1, 3, 6
总体程序运行结果:
初始化链表: 1 2 3 4 删除数据为 2 的节点: 1 3 4 将下标为 2 的 节点数据更改成 6: 1 3 6