数组不总是组织数据的最佳数据结构,原因如下。在很多编程语言中,数组的长度是固定的,所以当数组已被数据填满时,再要加入新的元素就会非常困难。在数组中,添加和删除元素也很麻烦,因为需要将其他元素向前或向后平移。然后JavaScript的数组并不存在上述问题,因为使用splice()方法不需要再访问数组中其他元素了。
JavaScript中的数组的主要问题是,它们被实现成了对象,与其他语言(比如C++和Java)的数组相比,效率很低。链表中存储的元素所占用的内存单元是分散的,不像传统语言的数组是一块这连续的内存空间。种数据结构 主要是删除和数据和添加数据很方便,数组主要是查找数据很方便。
链表是由一组节点组成的集合。每个节点都使用一个对象引用指向它的后继节点。
在链表的两个元素a和b之间插入一个节点q ,p为指向节点a的指针。为了插入数据元素x,首先要生成一个数据域为x的新节点,q为指向新增节点的指针,其过程如下:
1. 让p节点(a元素)的next指向新增的节点
2. 让新增的节点q的next指向b元素
接下来设计一个基于对象的链表。
这里使用2个类:Node类用来表示节点,LinkedList类提供了插入节点、删除节点、显示列表元素的方法,以及其他一些辅助方法。
Node类包含两个属性:element用来保存节点上的数据,next用来保存指向下一个节点的连接。
function Node (element) {
this.element = element;
this.next = null;
}
LinkedList类提供了对链表操作的方法。
function LinkedList () {
this.head = new Node('head');
this.find = find;
this.insert = insert;
this.display = display;
}
这里是给链表初始化了一个头节点head节点。
由链表的内存中存储结构不是连续的区域,因此每个节点都是要保存下一个节点或者上一个节点的引用,通过它可以来寻址其他节点,链表的节点查询,通常就是要遍历节点来判断节点存储的内容是否是要查找的值,一般循环的代码都是while循环,不断的改变currNode等于下一个节点,直到currNode.next === null。链表的插入也是改变某节点的对其他节点的引用值,链表就像项链一样用next把这些节点串联在一起:
删除操作和插入操作基本一致, 主要是改变节点的next属性,释放引用。
完整代码如下:
function Node (element) {
this.element = element;
this.next = null;
}
function LinkedList () {
this.head = new Node('head');
this.find = find;
this.insert = insert;
this.display = display;
this.findPrevious = findPrevious;
this.remove = remove;
}
function remove (item) {
var prevNode = this.findPrevious(item);
if (!(prevNode.next === null)) {
prevNode.next = prevNode.next.next;
}
}
function findPrevious (item) {
var currNode = this.head;
while (currNode.next !== null && currNode.next.element !== item) {
currNode = currNode.next;
}
return currNode;
}
function display () {
var currNode = this.head;
while (currNode.next !== null) {
console.log(currNode.next.element);
currNode = currNode.next;
}
}
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 currNode = this.find(item);
newNode.next = currNode.next;
currNode.next = newNode;
}
运行测试:
2.双向链表
链表从头节点遍历到尾节点很简单,但反过来,从后向前遍历没那么简单。如果我们在每个节点上面再加一个 指向前一个节点的指针,那么我们就可以通过一个节点很方便的找到它的前一个节点和后一个节点。
因此,Node类可以增加一个previous属性:
function Node (element) {
this.element = element;
this.next = null;
this.previous = null;
}
双向链表的Insert()方法和单向链表的类似,但是需要设置新节点的previous属性,使其指向该节点的前一个节点。
function insert (newElement, item) {
var newNode = new Node(newElement);
var currNode = this.find(item);
newNode.next = current.next;
newNode.previous = currNode;
currNode.next = newNode;
}
双向链表的remove()方法比单向链表的效率更高,因为不需要查找前置节点了。
function remove (item) {
var currNode = this.find(item);
if (currNode !== null) {
currNode.previous.next = currNode.next;
currNode.next.previous = currNode.previous;
currNode.next = null;
currNode.previous = null;
}
}
function findLast () {
var currNode = this.head;
while (currNode.next !== null) {
currNode = currNode.next;
}
return currNode;
}
如果你无法容忍这个缺点,那么可以使用双向循环链表。
3.循环链表
循环链表分为单向循环链表和双向循环链表。在创建循环链表的时候,让其头结点的next属性指向它本身,即: head.next = head
单向循环链表:
双向循环链表:
下面是单向循环链表的代码:
function LinkedList () {
this.head = new Node('head');
this.head.next = this.head;
this.find = find;
this.insert = insert;
this.display = this.display;
this.findPrevious = findPrevious;
this.remove = remove;
}
由于循环链表是环形的链表,没有 “尾节点” ,在编列的时候要注意,别陷入死循环,需要检查 头节点:
function display () {
var currNode = this.head;
while (currNode.next !== null) && currNode.next.element !== 'head') {
console.log(currNode.next.element);
currNode = currNode.next;
}
}
循环链表好处就是可以从任意节点都可以遍历完所有节点。在有些环境下非常适合做为存储数据的底层结构。