JavaScript 数据结构与算法之美 - 线性表(数组、栈、队列、链表)

前言

  1. 基础知识就像是一座大楼的地基,它决定了咱们的技术高度。
  2. 咱们应该多掌握一些可移值的技术或者再过十几年应该都不会过期的技术,数据结构与算法就是其中之一。

栈、队列、链表、堆 是数据结构与算法中的基础知识,是程序员的地基。前端

笔者写的 JavaScript 数据结构与算法之美 系列用的语言是 JavaScript ,旨在入门数据结构与算法和方便之后复习。node

1. 线性表与非线性表

线性表(Linear List):就是数据排成像一条线同样的结构。每一个线性表上的数据最多只有前和后两个方向。数组、链表、队列、栈 等就是线性表结构。git

线性表

非线性表:数据之间并非简单的先后关系。二叉树、堆、图 就是非线性表。程序员

非线性表

本文主要讲线性表,非线性表会在后面章节讲。github

2. 数组

数组

定义

  • 数组 (Array) 是一个有序的数据集合,咱们能够经过数组名称 (name) 和索引 (index) 进行访问。
  • 数组的索引是从 0 开始的。

特色

  • 数组是用一组连续的内存空间来存储的。 因此数组支持 随机访问,根据下标随机访问的时间复杂度为 O(1)。面试

  • 低效的插入和删除。 数组为了保持内存数据的连续性,会致使插入、删除这两个操做比较低效,由于底层一般是要进行大量的数据搬移来保持数据的连续性。 插入与删除的时间复杂度以下: 插入:从最好 O(1) ,最坏 O(n) ,平均 O(n) 删除:从最好 O(1) ,最坏 O(n) ,平均 O(n)算法

注意

可是由于 JavaScript 是弱类型的语言,弱类型则容许隐式类型转换。编程

隐式:是指源码中没有明显的类型转换代码。也就是说,一个变量,能够赋值字符串,也能够赋值数值。segmentfault

let str = "string"
str = 123 
console.log(str)  //   123
复制代码

你还能够直接让字符串类型的变量和数值类型的变量相加,虽然得出的最终结果未必是你想象的那样,但必定不会报错。数组

let a = 123
let b = "456"
let c = a + b
// 数值加字符串,结果是字符串
console.log(c)  //   "123456"
复制代码

数组的每一项能够是不一样的类型,好比:

// 数组的类型有 数值、字符串,还能够随意变动类型
const arr = [ 12, 34, "abc" ]
arr[2] = { "key": "value" }  // 把数组的第二项变成对象
console.log(arr) //  [ 12, 34,  { "key": "value"} ]
复制代码

定义的数组的大小是可变的,不像强类型语言,定义某个数组变量的时候就要定义该变量的大小。

const arr = [ 12, 34, "abc"] 
arr.push({ "key": "value" }) // 添加一项 对象
consolelog(arr) //  [ 12, 34, "abc", { "key": "value" } ]
复制代码

实现

JavaScript 原生支持数组,并且提供了不少操做方法,这里不展开讲。

3. 栈

栈

定义

  1. 后进者先出,先进者后出,简称 后进先出(LIFO),这就是典型的结构。
  2. 新添加的或待删除的元素都保存在栈的末尾,称做栈顶,另外一端就叫栈底
  3. 在栈里,新元素都靠近栈顶,旧元素都接近栈底。
  4. 从栈的操做特性来看,是一种 操做受限的线性表,只容许在一端插入和删除数据。
  5. 不包含任何元素的栈称为空栈

栈也被用在编程语言的编译器和内存中保存变量、方法调用等,好比函数的调用栈。

实现

栈的方法:

  • push(element):添加一个(或几个)新元素到栈顶。
  • pop():移除栈顶的元素,同时返回被移除的元素。
  • peek():返回栈顶的元素,不对栈作任何修改。
  • isEmpty():若是栈里没有任何元素就返回 true,不然返回 false。
  • clear():移除栈里的全部元素。
  • size():返回栈里的元素个数。
// Stack类
function Stack() {
  this.items = [];

  // 添加新元素到栈顶
  this.push = function(element) {
    this.items.push(element);
  };
  // 移除栈顶元素,同时返回被移除的元素
  this.pop = function() {
    return this.items.pop();
  };
  // 查看栈顶元素
  this.peek = function() {
    return this.items[this.items.length - 1];
  };
  // 判断是否为空栈
  this.isEmpty = function() {
    return this.items.length === 0;
  };
  // 清空栈
  this.clear = function() {
    this.items = [];
  };
  // 查询栈的长度
  this.size = function() {
    return this.items.length;
  };
  // 打印栈里的元素
  this.print = function() {
    console.log(this.items.toString());
  };
}
复制代码

测试:

// 建立Stack实例
var stack = new Stack();
console.log(stack.isEmpty()); // true
stack.push(5); // undefined
stack.push(8); // undefined
console.log(stack.peek()); // 8
stack.push(11); // undefined
console.log(stack.size()); // 3
console.log(stack.isEmpty()); // false
stack.push(15); // undefined
stack.pop(); // 15
console.log(stack.size()); // 3
stack.print(); // 5,8,11
stack.clear(); // undefined
console.log(stack.size()); // 0
复制代码

栈的应用实例:JavaScript 数据结构与算法之美 - 实现一个前端路由,如何实现浏览器的前进与后退 ?

4. 队列

队列

普通队列

定义

  • 队列是遵循 FIFO(First In First Out,先进先出)原则的一组有序的项。
  • 队列在尾部添加新元素,并从顶部移除元素。
  • 最新添加的元素必须排在队列的末尾。
  • 队列只有 入队 push() 和出队 pop()。

实现

队列里面有一些声明的辅助方法:

  • enqueue(element):向队列尾部添加新项。
  • dequeue():移除队列的第一项,并返回被移除的元素。
  • front():返回队列中第一个元素,队列不作任何变更。
  • isEmpty():若是队列中不包含任何元素,返回 true,不然返回 false。
  • size():返回队列包含的元素个数,与数组的 length 属性相似。
  • print():打印队列中的元素。
  • clear():清空整个队列。

代码:

// Queue类
function Queue() {
	this.items = [];

	// 向队列尾部添加元素
	this.enqueue = function(element) {
		this.items.push(element);
	};

	// 移除队列的第一个元素,并返回被移除的元素
	this.dequeue = function() {
		return this.items.shift();
	};

	// 返回队列的第一个元素
	this.front = function() {
		return this.items[0];
	};

	// 判断是否为空队列
	this.isEmpty = function() {
		return this.items.length === 0;
	};

	// 获取队列的长度
	this.size = function() {
		return this.items.length;
	};

	// 清空队列
	this.clear = function() {
		this.items = [];
	};

	// 打印队列里的元素
	this.print = function() {
		console.log(this.items.toString());
	};
}
复制代码

测试:

// 建立Queue实例
var queue = new Queue();
console.log(queue.isEmpty()); // true
queue.enqueue('John'); // undefined
queue.enqueue('Jack'); // undefined
queue.enqueue('Camila'); // undefined
queue.print(); // "John,Jack,Camila"
console.log(queue.size()); // 3
console.log(queue.isEmpty()); // false
queue.dequeue(); // "John"
queue.dequeue(); // "Jack"
queue.print(); // "Camila"
queue.clear(); // undefined
console.log(queue.size()); // 0
复制代码

优先队列

定义

优先队列中元素的添加和移除是依赖优先级的。

应用

  • 一个现实的例子就是机场登机的顺序。头等舱和商务舱乘客的优先级要高于经济舱乘客。
  • 再好比:火车,老年人、孕妇和带小孩的乘客是享有优先检票权的。

优先队列分为两类

  • 最小优先队列
  • 最大优先队列

最小优先队列是把优先级的值最小的元素被放置到队列的最前面(表明最高的优先级)。 好比:有四个元素:"John", "Jack", "Camila", "Tom",他们的优先级值分别为 4,3,2,1。 那么最小优先队列排序应该为:"Tom","Camila","Jack","John"。

最大优先队列正好相反,把优先级值最大的元素放置在队列的最前面。 以上面的为例,最大优先队列排序应该为:"John", "Jack", "Camila", "Tom"。

实现

实现一个优先队列,有两种选项:

    1. 设置优先级,根据优先级正确添加元素,而后和普通队列同样正常移除
    1. 设置优先级,和普通队列同样正常按顺序添加,而后根据优先级移除

这里最小优先队列和最大优先队列我都采用第一种方式实现,你们能够尝试一下第二种。

下面只重写 enqueue() 方法和 print() 方法,其余方法和上面的普通队列彻底相同。

实现最小优先队列

// 定义最小优先队列
function MinPriorityQueue () {
  this.items = [];

  this.enqueue = enqueue;
  this.dequeue = dequeue;
  this.front = front;
  this.isEmpty = isEmpty;
  this.size = size;
  this.clear = clear;
  this.print = print;
}
复制代码

实现最小优先队列 enqueue() 方法和 print() 方法:

// 优先队列添加元素,要根据优先级判断在队列中的插入顺序
function enqueue (element, priority) {
  var queueElement = {
    element: element,
    priority: priority
  };

  if (this.isEmpty()) {
    this.items.push(queueElement);
  } else {
    var added = false;
    for (var i = 0; i < this.size(); i++) {
      if (queueElement.priority < this.items[i].priority) {
        this.items.splice(i, 0, queueElement);
        added = true;
        break ;
      }
    }

    if (!added) {
      this.items.push(queueElement);
    }
  }
}

// 打印队列里的元素
function print () {
  var strArr = [];

  strArr = this.items.map(function (item) {
    return `${item.element}->${item.priority}`;
  });

  console.log(strArr.toString());
}
复制代码

最小优先队列测试:

// 建立最小优先队列minPriorityQueue实例
var minPriorityQueue = new MinPriorityQueue();

console.log(minPriorityQueue.isEmpty());     // true
minPriorityQueue.enqueue("John", 1);         // undefined
minPriorityQueue.enqueue("Jack", 3);         // undefined
minPriorityQueue.enqueue("Camila", 2);       // undefined
minPriorityQueue.enqueue("Tom", 3);          // undefined
minPriorityQueue.print();                    // "John->1,Camila->2,Jack->3,Tom->3"
console.log(minPriorityQueue.size());        // 4
console.log(minPriorityQueue.isEmpty());     // false
minPriorityQueue.dequeue();                  // {element: "John", priority: 1}
minPriorityQueue.dequeue();                  // {element: "Camila", priority: 2}
minPriorityQueue.print();                    // "Jack->3,Tom->3"
minPriorityQueue.clear();                    // undefined
console.log(minPriorityQueue.size());        // 0
复制代码

实现最大优先队列

// 最大优先队列 MaxPriorityQueue 类
function MaxPriorityQueue () {
  this.items = [];

  this.enqueue = enqueue;
  this.dequeue = dequeue;
  this.front = front;
  this.isEmpty = isEmpty;
  this.size = size;
  this.clear = clear;
  this.print = print;
}

// 优先队列添加元素,要根据优先级判断在队列中的插入顺序
function enqueue (element, priority) {
  var queueElement = {
    element: element,
    priority: priority
  };

  if (this.isEmpty()) {
    this.items.push(queueElement);
  } else {
    var added = false;

    for (var i = 0; i < this.items.length; i++) {
      // 注意,只须要将这里改成大于号就能够了
      if (queueElement.priority > this.items[i].priority) {
        this.items.splice(i, 0, queueElement);
        added = true;
        break ;
      }
    }

    if (!added) {
      this.items.push(queueElement);
    }
  }
}
复制代码

最大优先队列测试:

// 建立最大优先队列maxPriorityQueue实例
var maxPriorityQueue = new MaxPriorityQueue();

console.log(maxPriorityQueue.isEmpty());     // true
maxPriorityQueue.enqueue("John", 1);         // undefined
maxPriorityQueue.enqueue("Jack", 3);         // undefined
maxPriorityQueue.enqueue("Camila", 2);       // undefined
maxPriorityQueue.enqueue("Tom", 3);          // undefined
maxPriorityQueue.print();                    // "Jack->3,Tom->3,Camila->2,John->1"
console.log(maxPriorityQueue.size());        // 4
console.log(maxPriorityQueue.isEmpty());     // false
maxPriorityQueue.dequeue();                  // {element: "Jack", priority: 3}
maxPriorityQueue.dequeue();                  // {element: "Tom", priority: 3}
maxPriorityQueue.print();                    // "Camila->2,John->1"
maxPriorityQueue.clear();                    // undefined
console.log(maxPriorityQueue.size());        // 0
复制代码

循环队列

定义

循环队列,顾名思义,它长得像一个环。把它想像成一个圆的钟就对了。

关键是:肯定好队空和队满的断定条件。

循环队列的一个例子就是击鼓传花游戏(Hot Potato)。在这个游戏中,孩子们围城一个圆圈,击鼓的时候把花尽快的传递给旁边的人。某一时刻击鼓中止,这时花在谁的手里,谁就退出圆圈直到游戏结束。重复这个过程,直到只剩一个孩子(胜者)。

下面咱们在普通队列的基础上,实现一个模拟的击鼓传花游戏,下面只写击鼓传花的代码片断:

// 实现击鼓传花
function hotPotato (nameList, num) {
  var queue = new Queue();

  for (var i = 0; i < nameList.length; i++) {
    queue.enqueue(nameList[i]);
  }

  var eliminated = '';

  while (queue.size() > 1) {
    // 循环 num 次,队首出来去到队尾
    for (var i = 0; i < num; i++) {
      queue.enqueue(queue.dequeue());
    }
    // 循环 num 次事后,移除当前队首的元素
    eliminated = queue.dequeue();
    console.log(`${eliminated} 在击鼓传花中被淘汰!`);
  }

  // 最后只剩一个元素
  return queue.dequeue();
}

// 测试
var nameList = ["John", "Jack", "Camila", "Ingrid", "Carl"];
var winner = hotPotato(nameList, 10);
console.log(`最后的胜利者是:${winner}`);
复制代码

执行结果为:

// John 在击鼓传花中被淘汰!
// Ingrid 在击鼓传花中被淘汰! 
// Jack 在击鼓传花中被淘汰!
// Camila 在击鼓传花中被淘汰!
// 最后的胜利者是:Carl
复制代码

队列小结

一些具备某些额外特性的队列,好比:循环队列、阻塞队列、并发队列。它们在不少偏底层系统、框架、中间件的开发中,起着关键性的做用。

以上队列的代码要感谢 leocoder351

5. 链表

定义

  • 链表存储有序的元素集合,但不一样于数组,链表中的元素在内存中并非连续放置的,它是经过 指针零散的内存块 串连起来的。
  • 每一个元素由一个存储元素自己的 节点 和一个指向下一个元素的 引用(也称指针或连接)组成。

简单的连接结构图:

单链表结构图

其中,data 中保存着数据,next 保存着下一个链表的引用。 上图中,咱们说 data2 跟在 data1 后面,而不是说 data2 是链表中的第二个元素。值得注意的是,咱们将链表的尾元素指向了 null 节点,表示连接结束的位置。

特色

  • 链表是经过指针将零散的内存块串连起来的。 因此链表不支持 随机访问,若是要找特定的项,只能从头开始遍历,直到找到某个项。 因此访问的时间复杂度为 O(n)。

  • 高效的插入和删除。 链表中插入或者删除一个数据,咱们并不须要为了保持内存的连续性而搬移结点,由于链表的存储空间自己就不是连续的,只须要考虑相邻结点的指针改变。 因此,在链表中插入和删除一个数据是很是快速的,时间复杂度为 O(1)。

三种最多见的链表结构,它们分别是:

  • 单链表
  • 双向链表
  • 循环链表

单链表

定义

单链表结构图

因为链表的起始点的肯定比较麻烦,所以不少链表的实现都会在链表的最前面添加一个特殊的节点,称为 头节点,表示链表的头部。

通过改造,链表就成了以下的样子:

有头节点的链表

针对链表的插入和删除操做,咱们只须要考虑相邻结点的指针改变,因此插入与删除的时间复杂度为 O(1)。

在 d2 节点后面插入 d4 节点:

插入节点

删除 d4 节点:

删除节点

实现

  • Node 类用来表示节点。
  • LinkedList 类提供插入节点、删除节点等一些操做。

单向链表的八种经常使用操做:

  • append(element):尾部添加元素。
  • insert(position, element):特定位置插入一个新的项。
  • removeAt(position):特定位置移除一项。
  • remove(element):移除一项。
  • indexOf(element):返回元素在链表中的索引。若是链表中没有该元素则返回 -1。
  • isEmpty():若是链表中不包含任何元素,返回 true,若是链表长度大于 0,返回 false。
  • size():返回链表包含的元素个数,与数组的 length 属性相似。
  • getHead():返回链表的第一个元素。
  • toString():因为链表使用了 Node 类,就须要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值。
  • print():打印链表的全部元素。

具体代码:

// 单链表
function SinglyLinkedList() {
	// 节点
	function Node(element) {
		this.element = element; // 当前节点的元素
		this.next = null; // 下一个节点指针
	}

	var length = 0; // 链表的长度
	var head = null; // 链表的头部节点

	// 向链表尾部添加一个新的节点
	this.append = function(element) {
		var node = new Node(element);
		var currentNode = head;

		// 判断是否为空链表
		if (head === null) {
			// 是空链表,就把当前节点做为头部节点
			head = node;
		} else {
			// 从 head 开始一直找到最后一个 node
			while (currentNode.next) {
				// 后面还有 node
				currentNode = currentNode.next;
			}
			// 把当前节点的 next 指针 指向 新的节点
			currentNode.next = node;
		}
		// 链表的长度加 1
		length++;
	};

	// 向链表特定位置插入一个新节点
	this.insert = function(position, element) {
		if (position < 0 && position > length) {
			// 越界
			return false;
		} else {
			var node = new Node(element);
			var index = 0;
			var currentNode = head;
			var previousNode;

			// 在最前插入节点
			if (position === 0) {
				node.next = currentNode;
				head = node;
			} else {
				// 循环找到位置
				while (index < position) {
					index++;
					previousNode = currentNode;
					currentNode = currentNode.next;
				}
				// 把前一个节点的指针指向新节点,新节点的指针指向当前节点,保持链接性
				previousNode.next = node;
				node.next = currentNode;
			}

			length++;

			return true;
		}
	};

	// 从链表的特定位置移除一项
	this.removeAt = function(position) {
		if ((position < 0 && position >= length) || length === 0) {
			// 越界
			return false;
		} else {
			var currentNode = head;
			var index = 0;
			var previousNode;

			if (position === 0) {
				head = currentNode.next;
			} else {
				// 循环找到位置
				while (index < position) {
					index++;
					previousNode = currentNode;
					currentNode = currentNode.next;
				}
				// 把当前节点的 next 指针 指向 当前节点的 next 指针,便是 删除了当前节点
				previousNode.next = currentNode.next;
			}

			length--;

			return true;
		}
	};

	// 从链表中移除指定项
	this.remove = function(element) {
		var index = this.indexOf(element);
		return this.removeAt(index);
	};

	// 返回元素在链表的索引,若是链表中没有该元素则返回 -1
	this.indexOf = function(element) {
		var currentNode = head;
		var index = 0;

		while (currentNode) {
			if (currentNode.element === element) {
				return index;
			}

			index++;
			currentNode = currentNode.next;
		}

		return -1;
	};

	// 若是链表中不包含任何元素,返回 true,若是链表长度大于 0,返回 false
	this.isEmpty = function() {
		return length === 0;
	};

	// 返回链表包含的元素个数,与数组的 length 属性相似
	this.size = function() {
		return length;
	};

	// 获取链表头部元素
	this.getHead = function() {
		return head.element;
	};

	// 因为链表使用了 Node 类,就须要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
	this.toString = function() {
		var currentNode = head;
		var string = '';

		while (currentNode) {
			string += ',' + currentNode.element;
			currentNode = currentNode.next;
		}

		return string.slice(1);
	};

	// 打印链表数据
	this.print = function() {
		console.log(this.toString());
	};

	// 获取整个链表
	this.list = function() {
		console.log('head: ', head);
		return head;
	};
}
复制代码

测试:

// 建立单向链表实例
var singlyLinked = new SinglyLinkedList();
console.log(singlyLinked.removeAt(0)); // false
console.log(singlyLinked.isEmpty()); // true
singlyLinked.append('Tom');
singlyLinked.append('Peter');
singlyLinked.append('Paul');
singlyLinked.print(); // "Tom,Peter,Paul"
singlyLinked.insert(0, 'Susan');
singlyLinked.print(); // "Susan,Tom,Peter,Paul"
singlyLinked.insert(1, 'Jack');
singlyLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(singlyLinked.getHead()); // "Susan"
console.log(singlyLinked.isEmpty()); // false
console.log(singlyLinked.indexOf('Peter')); // 3
console.log(singlyLinked.indexOf('Cris')); // -1
singlyLinked.remove('Tom');
singlyLinked.removeAt(2);
singlyLinked.print(); // "Susan,Jack,Paul"
singlyLinked.list(); // 具体控制台
复制代码

整个链表数据在 JavaScript 里是怎样的呢 ?

为了看这个数据,特地写了个 list 函数:

// 获取整个链表
	this.list = function() {
		console.log('head: ', head);
		return head;
	};
复制代码

重点上上面的最后一行代码: singlyLinked.list() ,打印的数据以下:

因此,在 JavaScript 中,单链表的真实数据有点相似于对象,其实是 Node 类生成的实例。

双向链表

单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。 而双向链表,它支持两个方向,每一个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。

双向链表

插入

删除

单向链表与又向链表比较

  • 双向链表须要额外的两个空间来存储后继结点和前驱结点的地址。 因此,若是存储一样多的数据,双向链表要比单链表占用更多的内存空间。 虽然两个指针比较浪费存储空间,但能够支持双向遍历,这样也带来了双向链表操做的灵活性。
  • 双向链表提供了两种迭代列表的方法:从头至尾,或者从尾到头。 咱们能够访问一个特定节点的下一个或前一个元素。
  • 在单向链表中,若是迭代链表时错过了要找的元素,就须要回到链表起点,从新开始迭代。
  • 在双向链表中,能够从任一节点,向前或向后迭代,这是双向链表的一个优势。
  • 因此,双向链表能够支持 O(1) 时间复杂度的状况下找到前驱结点,正是这样的特色,也使双向链表在某些状况下的插入、删除等操做都要比单链表简单、高效。

实现

具体代码:

// 建立双向链表 DoublyLinkedList 类
function DoublyLinkedList() {
  function Node(element) {
    this.element = element; //当前节点的元素
    this.next = null; //下一个节点指针
    this.previous = null; //上一个节点指针
  }

  var length = 0; // 链表长度
  var head = null; // 链表头部
  var tail = null; // 链表尾部

  // 向链表尾部添加一个新的项
  this.append = function(element) {
    var node = new Node(element);
    var currentNode = tail;

    // 判断是否为空链表
    if (currentNode === null) {
      // 空链表
      head = node;
      tail = node;
    } else {
      currentNode.next = node;
      node.prev = currentNode;
      tail = node;
    }

    length++;
  };

  // 向链表特定位置插入一个新的项
  this.insert = function(position, element) {
    if (position < 0 && position > length) {
      // 越界
      return false;
    } else {
      var node = new Node(element);
      var index = 0;
      var currentNode = head;
      var previousNode;

      if (position === 0) {
        if (!head) {
          head = node;
          tail = node;
        } else {
          node.next = currentNode;
          currentNode.prev = node;
          head = node;
        }
      } else if (position === length) {
        this.append(element);
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }

        previousNode.next = node;
        node.next = currentNode;

        node.prev = previousNode;
        currentNode.prev = node;
      }

      length++;

      return true;
    }
  };

  // 从链表的特定位置移除一项
  this.removeAt = function(position) {
    if ((position < 0 && position >= length) || length === 0) {
      // 越界
      return false;
    } else {
      var currentNode = head;
      var index = 0;
      var previousNode;

      if (position === 0) {
        // 移除第一项
        if (length === 1) {
          head = null;
          tail = null;
        } else {
          head = currentNode.next;
          head.prev = null;
        }
      } else if (position === length - 1) {
        // 移除最后一项
        if (length === 1) {
          head = null;
          tail = null;
        } else {
          currentNode = tail;
          tail = currentNode.prev;
          tail.next = null;
        }
      } else {
        while (index < position) {
          index++;
          previousNode = currentNode;
          currentNode = currentNode.next;
        }
        previousNode.next = currentNode.next;
        previousNode = currentNode.next.prev;
      }

      length--;

      return true;
    }
  };

  // 从链表中移除指定项
  this.remove = function(element) {
    var index = this.indexOf(element);
    return this.removeAt(index);
  };

  // 返回元素在链表的索引,若是链表中没有该元素则返回 -1
  this.indexOf = function(element) {
    var currentNode = head;
    var index = 0;

    while (currentNode) {
      if (currentNode.element === element) {
        return index;
      }

      index++;
      currentNode = currentNode.next;
    }

    return -1;
  };

  // 若是链表中不包含任何元素,返回 true ,若是链表长度大于 0 ,返回 false
  this.isEmpty = function() {
    return length == 0;
  };

  // 返回链表包含的元素个数,与数组的 length 属性相似
  this.size = function() {
    return length;
  };

  // 获取链表头部元素
  this.getHead = function() {
    return head.element;
  };

  // 因为链表使用了 Node 类,就须要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
  this.toString = function() {
    var currentNode = head;
    var string = '';

    while (currentNode) {
      string += ',' + currentNode.element;
      currentNode = currentNode.next;
    }

    return string.slice(1);
  };

  this.print = function() {
    console.log(this.toString());
  };

  // 获取整个链表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };
}
复制代码

测试:

// 建立双向链表
var doublyLinked = new DoublyLinkedList();
console.log(doublyLinked.isEmpty()); // true
doublyLinked.append('Tom');
doublyLinked.append('Peter');
doublyLinked.append('Paul');
doublyLinked.print(); // "Tom,Peter,Paul"
doublyLinked.insert(0, 'Susan');
doublyLinked.print(); // "Susan,Tom,Peter,Paul"
doublyLinked.insert(1, 'Jack');
doublyLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(doublyLinked.getHead()); // "Susan"
console.log(doublyLinked.isEmpty()); // false
console.log(doublyLinked.indexOf('Peter')); // 3
console.log(doublyLinked.indexOf('Cris')); // -1
doublyLinked.remove('Tom');
doublyLinked.removeAt(2);
doublyLinked.print(); // "Susan,Jack,Paul"
doublyLinked.list(); // 请看控制台输出
复制代码

整个链表数据在 JavaScript 里是怎样的呢 ?

// 获取整个链表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };
复制代码

调用 doublyLinked.list(); .

控制台输出以下:

链表代码实现的关键是弄清楚:前节点与后节点与边界。

循环链表

循环链表是一种特殊的单链表。 循环链表和单链表类似,节点类型都是同样。 惟一的区别是,在建立循环链表的时候,让其头节点的 next 属性指向它自己。 即:

head.next = head;
复制代码

这种行为会致使链表中每一个节点的 next 属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,造成了一个循环链表。以下图所示:

循环链表

循环链表:在单链表的基础上,将尾节点的指针指向头结点,就构成了一个循环链表。环形链表从任意一个节点开始,均可以遍历整个链表。

代码:

// 循环链表
function CircularLinkedList() {
	// 节点
	function Node(element) {
		this.element = element; // 当前节点的元素
		this.next = null; // 下一个节点指针
	}

	var length = 0,
		head = null;

	this.append = function(element) {
		var node = new Node(element),
			current;

		if (!head) {
			head = node;
			// 头的指针指向本身
			node.next = head;
		} else {
			current = head;

			while (current.next !== head) {
				current = current.next;
			}

			current.next = node;
			// 最后一个节点指向头节点
			node.next = head;
		}

		length++;
		return true;
	};

	this.insert = function(position, element) {
		if (position > -1 && position < length) {
			var node = new Node(element),
				index = 0,
				current = head,
				previous;

			if (position === 0) {
				// 头节点指向本身
				node.next = head;
				head = node;
			} else {
				while (index++ < position) {
					previous = current;
					current = current.next;
				}
				previous.next = node;
				node.next = current;
			}
			length++;
			return true;
		} else {
			return false;
		}
	};
	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;
				}
				previous.next = current.next;
			}
			length--;
			return current.element;
		} else {
			return false;
		}
	};
	this.remove = function(element) {
		var current = head,
			previous,
			indexCheck = 0;
		while (current && indexCheck < length) {
			if (current.element === element) {
				if (indexCheck == 0) {
					head = current.next;
					length--;
					return true;
				} else {
					previous.next = current.next;
					length--;
					return true;
				}
			} else {
				previous = current;
				current = current.next;
				indexCheck++;
			}
		}
		return false;
	};
	this.remove = function() {
		if (length === 0) {
			return false;
		}
		var current = head,
			previous,
			indexCheck = 0;
		if (length === 1) {
			head = null;
			length--;
			return current.element;
		}
		while (indexCheck++ < length) {
			previous = current;
			current = current.next;
		}
		previous.next = head;
		length--;
		return current.element;
	};
	this.indexOf = function(element) {
		var current = head,
			index = 0;
		while (current && index < length) {
			if (current.element === element) {
				return index;
			} else {
				index++;
				current = current.next;
			}
		}
		return -1;
	};
	this.isEmpty = function() {
		return length === 0;
	};
	this.size = function() {
		return length;
	};

	// 因为链表使用了 Node 类,就须要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
	this.toString = function() {
		var current = head,
			string = '',
			indexCheck = 0;
		while (current && indexCheck < length) {
			string += ',' + current.element;
			current = current.next;
			indexCheck++;
		}
		return string.slice(1);
	};

	// 获取链表头部元素
	this.getHead = function() {
		return head.element;
	};

	// 打印链表数据
	this.print = function() {
		console.log(this.toString());
	};

	// 获取整个链表
	this.list = function() {
		console.log('head: ', head);
		return head;
	};
}
复制代码

测试:

// 建立单向链表实例
var circularLinked = new CircularLinkedList();
console.log(circularLinked.removeAt(0)); // false
console.log(circularLinked.isEmpty()); // true
circularLinked.append('Tom');
circularLinked.append('Peter');
circularLinked.append('Paul');
circularLinked.print(); // "Tom,Peter,Paul"
circularLinked.insert(0, 'Susan');
circularLinked.print(); // "Susan,Tom,Peter,Paul"
circularLinked.insert(1, 'Jack');
circularLinked.print(); // "Susan,Jack,Tom,Peter,Paul"
console.log(circularLinked.getHead()); // "Susan"
console.log(circularLinked.isEmpty()); // false
console.log(circularLinked.indexOf('Peter')); // 3
console.log(circularLinked.indexOf('Cris')); // -1
circularLinked.remove('Tom');
circularLinked.removeAt(2);
circularLinked.print(); // "Susan,Jack,Paul"
circularLinked.list(); // 具体控制台
复制代码

整个链表数据在 JavaScript 里是怎样的呢 ?

// 获取整个链表
  this.list = function() {
    console.log('head: ', head);
    return head;
  };
复制代码

调用 circularLinked.list() 。

控制台输出以下:

你知道你们发现没有,为何从 1 - 4 - 1 了,还有 next 节点,并且是还能够一直点 next ,重复的展开下去,这正是 循环 的缘由。

链表总结

  • 写链表代码是最考验逻辑思惟能力的,要熟练链表,只有 多写多练,没有捷径
  • 由于,链表代码处处都是指针的操做、边界条件的处理,稍有不慎就容易产生 Bug。
  • 链表代码写得好坏,能够看出一我的写代码是否够细心,考虑问题是否全面,思惟是否缜密。
  • 因此,这也是不少面试官喜欢让人手写链表代码的缘由。
  • 必定要本身写代码实现一下,才有效果。

6. 文章输出计划

JavaScript 数据结构与算法之美 的系列文章,坚持 3 - 7 天左右更新一篇,暂定计划以下表。

标题 连接
时间和空间复杂度 github.com/biaochenxuy…
线性表(数组、链表、栈、队列) github.com/biaochenxuy…
实现一个前端路由,如何实现浏览器的前进与后退 ? github.com/biaochenxuy…
栈内存与堆内存 、浅拷贝与深拷贝 精彩待续
非线性表(树、堆) 精彩待续
递归 精彩待续
冒泡排序 精彩待续
插入排序 精彩待续
选择排序 精彩待续
归并排序 精彩待续
快速排序 精彩待续
计数排序 精彩待续
基数排序 精彩待续
桶排序 精彩待续
希尔排序 精彩待续
堆排序 精彩待续
十大经典排序汇总 精彩待续

若是有错误或者不严谨的地方,请务必给予指正,十分感谢。

7. 最后

文章中的代码已经所有放在了个人 github 上,若是喜欢或者有所启发,欢迎 star,对做者也是一种鼓励。

关注个人公众号,第一时间接收最新的精彩博文。

文章能够转载,但须注明做者及出处,须要转载到公众号的,喊我加下白名单就好了。

参考文章:

数组:为何不少编程语言中数组都从 0 开始编号?

JS中的算法与数据结构——链表(Linked-list)

JavaScript数据结构 03 - 队列

链表(上):如何实现 LRU 缓存淘汰算法?

JavaScript数据结构——队列

笔芯
相关文章
相关标签/搜索