前言
在javascript中,有几种数据结构,本章要向你讲解的是 栈 和 队列 及 链表
栈
定义
-
后进者先出,先进者后出,简称 后进先出(LIFO),这就是典型的
栈
结构。 -
新添加的或待删除的元素都保存在栈的末尾,称作
栈顶
,另一端就叫栈底
。 -
在栈里,新元素都靠近栈顶,旧元素都接近栈底。
-
从栈的操作特性来看,是一种
操作受限
的线性表,只允许在一端插入和删除数据。 -
不包含任何元素的栈称为
空栈
。
现实场景
如现实中的一摞书和餐厅里叠放的盘子,如下
使用场景
被用在编程语言的编译器和内存中保存变量,方便调用等,还被用于浏览器历史记录
实现
栈的方法:
-
push(element):添加一个(或几个)新元素到栈顶。
-
pop():移除栈顶的元素,同时返回被移除的元素。
-
peek():返回栈顶的元素,不对栈做任何修改。
-
isEmpty():如果栈里没有任何元素就返回 true,否则返回 false。
-
clear():移除栈里的所有元素。
-
size():返回栈里的元素个数。
// Stack类
class Stack {
constructor(){
this.items = []
}
// 添加新元素到栈顶
push (element) {
this.items.push(element);
}
// 移除栈顶元素,同时返回被移除的元素
pop() {
return this.items.pop();
}
// 查看栈顶元素
peek () {
return this.items[this.items.length - 1];
}
// 判断是否为空栈
isEmpty () {
return this.items.length === 0;
}
// 清空栈
clear () {
this.items = [];
}
// 查询栈的长度
size () {
return this.items.length;
}
// 打印栈里的元素
print () {
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
扩展
把十进制转换成二进制 (要把十进制转换成二进制,可以将改十进制数除以2,二进制是满二进一,并对商取整,知道结果是0为止)
function decimalToBinary(decNumber){
const remStack = new Stack();
let number = decNumber;
let rem;
let binaryString = '';
while (number>0) {
rem = Math.floor(number % 2);
remStack.push(rem)
number = Math.floor(number / 2)
}
while (!remStack.isEmpty()) {
binaryString += remStack.pop().toString()
}
return binaryString
}
console.log(decimalToBinary(233)) //11101001
队列
普通队列
定义
- 队列是遵循 FIFO(First In First Out,先进先出)原则的一组有序的项。
- 队列在尾部添加新元素,并从顶部移除元素。
- 最新添加的元素必须排在队列的末尾。
- 队列只有 入队 push() 和出队 pop()。
现实场景
商场排队
实现
-
enqueue(element):向队列尾部添加新项。
-
dequeue():移除队列的第一项,并返回被移除的元素。
-
front():返回队列中第一个元素,队列不做任何变动。
-
isEmpty():如果队列中不包含任何元素,返回 true,否则返回 false。
-
size():返回队列包含的元素个数,与数组的 length 属性类似。
-
print():打印队列中的元素。
-
clear():清空整个队列。
class Queue {
constructor(){
this.items = []
}
// 向队列尾部添加元素
enqueue(element){
this.items.push(element);
}
// 移除队列的第一个元素,并返回被移除的元素
dequeue (){
return this.items.shift();
}
// 返回队列的第一个元素
front(){
return items[0];
}
// 判断是否为空队列
isEmpty (){
return items.length == 0;
}
// 清空队列
clear (){
this.items = [];
}
// 获取队列的长度
size (){
return this.items.length;
}
// 打印队列里的元素
print (){
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
循环队列
定义
循环队列,顾名思义,它长得像一个环。把它想像成一个圆的钟就对了。
关键是:确定好队空和队满的判定条件。
现实场景
鼓传花游戏(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
链表
定义
- 链表存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的,它是通过 指针 将 零散的内存块 串连起来的。
- 每个元素由一个存储元素本身的 节点 和一个指向下一个元素的 引用(也称指针或链接)组成。
特点
-
链表是通过指针将零散的内存块串连起来的。 所以链表不支持 随机访问,如果要找特定的项,只能从头开始遍历,直到找到某个项。 所以访问的时间复杂度为 O(n)。
-
高效的插入和删除。 链表中插入或者删除一个数据,我们并不需要为了保持内存的连续性而搬移结点,因为链表的存储空间本身就不是连续的,只需要考虑相邻结点的指针改变。 所以,在链表中插入和删除一个数据是非常快速的,时间复杂度为 O(1)。
单链表
定义
由于链表的起始点的确定比较麻烦,因此很多链表的实现都会在链表的最前面添加一个特殊的节点,称为 头节点,表示链表的头部。
针对链表的插入和删除操作,我们只需要考虑相邻结点的指针改变,所以插入与删除的时间复杂度为 O(1)。
实现
- 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():打印链表的所有元素。
class Node {
constructor(element) {
this.element = element; // 当前节点的元素
this.next = null; // 下一个节点指针
}
}
// 单链表
class SinglyLinkedList {
constructor() {
this.length = 0; // 链表的长度
this.head = null; // 链表的头部节点
}
// 向链表尾部添加一个新的节点
append(element) {
var node = new Node(element);
var currentNode = this.head;
// 判断是否为空链表
if (this.head === null) {
// 是空链表,就把当前节点作为头部节点
this.head = node;
} else {
// 从 head 开始一直找到最后一个 node
while (currentNode.next) {
// 后面还有 node
currentNode = currentNode.next;
}
// 把当前节点的 next 指针 指向 新的节点
currentNode.next = node;
}
// 链表的长度加 1
this.length++;
}
// 向链表特定位置插入一个新节点
insert(position, element) {
if (position < 0 && position > this.length) {
// 越界
return false;
} else {
var node = new Node(element);
var index = 0;
var currentNode = this.head;
var previousNode;
// 在最前插入节点
if (position === 0) {
node.next = currentNode;
this.head = node;
} else {
// 循环找到位置
while (index < position) {
index++;
previousNode = currentNode;
currentNode = currentNode.next;
}
// 把前一个节点的指针指向新节点,新节点的指针指向当前节点,保持连接性
previousNode.next = node;
node.next = currentNode;
}
this.length++;
return true;
}
};
// 从链表的特定位置移除一项
removeAt(position) {
if ((position < 0 && position >= this.length) || this.length === 0) {
// 越界
return false;
} else {
var currentNode = this.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;
}
this.length--;
return true;
}
};
// 从链表中移除指定项
remove(element) {
var index = this.indexOf(element);
return this.removeAt(index);
};
// 返回元素在链表的索引,如果链表中没有该元素则返回 -1
indexOf(element) {
var currentNode = this.head;
var index = 0;
while (currentNode) {
if (currentNode.element === element) {
return index;
}
index++;
currentNode = currentNode.next;
}
return -1;
};
// 如果链表中不包含任何元素,返回 true,如果链表长度大于 0,返回 false
isEmpty() {
return this.length === 0;
};
// 返回链表包含的元素个数,与数组的 length 属性类似
size() {
return this.length;
};
// 获取链表头部元素
getHead = function () {
return this.head.element;
};
// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
toString() {
var currentNode = this.head;
var string = '';
while (currentNode) {
string += ',' + currentNode.element;
currentNode = currentNode.next;
}
return string.slice(1);
};
// 打印链表数据
print() {
console.log(this.toString());
};
// 获取整个链表
list() {
console.log('head', this.head)
return this.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(); // 具体控制台
双向链表
单向链表只有一个方向,结点只有一个后继指针 next 指向后面的结点。 而双向链表,它支持两个方向,每个结点不止有一个后继指针 next 指向后面的结点,还有一个前驱指针 prev 指向前面的结点。
单向链表与双向列表比较
- 双向链表需要额外的两个空间来存储后继结点和前驱结点的地址。 所以,如果存储同样多的数据,双向链表要比单链表占用更多的内存空间。 虽然两个指针比较浪费存储空间,但可以支持双向遍历,这样也带来了双向链表操作的灵活性。
- 双向链表提供了两种迭代列表的方法:从头到尾,或者从尾到头。 我们可以访问一个特定节点的下一个或前一个元素。
- 在单向链表中,如果迭代链表时错过了要找的元素,就需要回到链表起点,重新开始迭代。
- 在双向链表中,可以从任一节点,向前或向后迭代,这是双向链表的一个优点。
- 所以,双向链表可以支持 O(1) 时间复杂度的情况下找到前驱结点,正是这样的特点,也使双向链表在某些情况下的插入、删除等操作都要比单链表简单、高效。
实现
class Node {
constructor(element){
this.element = element; //当前节点的元素
this.next = null; //下一个节点指针
this.previous = null; //上一个节点指针
}
}
// 创建双向链表 DoublyLinkedList 类
class DoublyLinkedList {
constructor(){
this.length = 0; // 链表长度
this.head = null; // 链表头部
this.tail = null; // 链表尾部
}
// 向链表尾部添加一个新的项
append (element) {
var node = new Node(element);
var currentNode = this.tail;
// 判断是否为空链表
if (currentNode === null) {
// 空链表
this.head = node;
this.tail = node;
} else {
currentNode.next = node;
node.prev = currentNode;
this.tail = node;
}
this.length++;
}
// 向链表特定位置插入一个新的项
insert (position, element) {
if (position < 0 && position > this.length) {
// 越界
return false;
} else {
var node = new Node(element);
var index = 0;
var currentNode = this.head;
var previousNode;
if (position === 0) {
if (!this.head) {
this.head = node;
this.tail = node;
} else {
node.next = currentNode;
currentNode.prev = node;
this.head = node;
}
} else if (position === this.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;
}
this.length++;
return true;
}
}
// 从链表的特定位置移除一项
removeAt (position) {
if ((position < 0 && position >= this.length) || this.length === 0) {
// 越界
return false;
} else {
var currentNode = this.head;
var index = 0;
var previousNode;
if (position === 0) {
// 移除第一项
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
this.head = currentNode.next;
this.head.prev = null;
}
} else if (position === this.length - 1) {
// 移除最后一项
if (this.length === 1) {
this.head = null;
this.tail = null;
} else {
currentNode = this.tail;
this.tail = currentNode.prev;
this.tail.next = null;
}
} else {
while (index < position) {
index++;
previousNode = currentNode;
currentNode = currentNode.next;
}
previousNode.next = currentNode.next;
previousNode = currentNode.next.prev;
}
this.length--;
return true;
}
}
// 从链表中移除指定项
remove(element) {
var index = this.indexOf(element);
return this.removeAt(index);
}
// 返回元素在链表的索引,如果链表中没有该元素则返回 -1
indexOf (element) {
var currentNode = this.head;
var index = 0;
while (currentNode) {
if (currentNode.element === element) {
return index;
}
index++;
currentNode = currentNode.next;
}
return -1;
}
// 如果链表中不包含任何元素,返回 true ,如果链表长度大于 0 ,返回 false
isEmpty () {
return this.length == 0;
}
// 返回链表包含的元素个数,与数组的 this.length 属性类似
size () {
return this.length;
}
// 获取链表头部元素
gethead () {
return this.head.element;
}
// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
toString() {
var currentNode = this.head;
var string = '';
while (currentNode) {
string += ',' + currentNode.element;
currentNode = currentNode.next;
}
return string.slice(1);
}
print () {
console.log(this.toString());
}
// 获取整个链表
list() {
console.log('this.head: ', this.head);
return this.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(); // 请看控制台输出
循环链表
定义
循环链表是一种特殊的单链表。 循环链表和单链表相似,节点类型都是一样。 唯一的区别是,在创建循环链表的时候,让其头节点的 next 属性指向它本身。
这种行为会导致链表中每个节点的next属性都指向链表的头节点,换句话说,也就是链表的尾节点指向了头节点,形成了一个循环链表 环形链表从任意一个节点开始,都可以遍历整个链表。
实现
class Node {
constructor(element){
this.element = element; // 当前节点的元素
this.next = null; // 下一个节点指针
}
}
// 循环链表
class CircularLinkedList{
constructor(){
this.length = 0;
this.head = null;
}
append (element) {
var node = new Node(element),
current;
if (!this.head) {
this.head = node;
// 头的指针指向自己
node.next = this.head;
} else {
current = this.head;
while (current.next !== this.head) {
current = current.next;
}
current.next = node;
// 最后一个节点指向头节点
node.next = this.head;
}
this.length++;
return true;
}
insert (position, element) {
if (position > -1 && position < this.length) {
var node = new Node(element),
index = 0,
current = this.head,
previous;
if (position === 0) {
// 头节点指向自己
node.next = this.head;
this.head = node;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = node;
node.next = current;
}
this.length++;
return true;
} else {
return false;
}
}
removeAt (position) {
if (position > -1 && position < this.length) {
var current = this.head,
previous,
index = 0;
if (position === 0) {
this.head = current.next;
} else {
while (index++ < position) {
previous = current;
current = current.next;
}
previous.next = current.next;
}
this.length--;
return current.element;
} else {
return false;
}
}
remove (element) {
var current = this.head,
previous,
indexCheck = 0;
while (current && indexCheck < this.length) {
if (current.element === element) {
if (indexCheck == 0) {
this.head = current.next;
this.length--;
return true;
} else {
previous.next = current.next;
this.length--;
return true;
}
} else {
previous = current;
current = current.next;
indexCheck++;
}
}
return false;
}
indexOf (element) {
var current = this.head,
index = 0;
while (current && index < this.length) {
if (current.element === element) {
return index;
} else {
index++;
current = current.next;
}
}
return -1;
}
isEmpty () {
return this.length === 0;
}
size () {
return this.length;
}
// 由于链表使用了 Node 类,就需要重写继承自 JavaScript 对象默认的 toString() 方法,让其只输出元素的值
toString () {
var current = this.head,
string = '',
indexCheck = 0;
while (current && indexCheck < this.length) {
string += ',' + current.element;
current = current.next;
indexCheck++;
}
return string.slice(1);
}
// 获取链表头部元素
gethead () {
return this.head.element;
}
// 打印链表数 据
print () {
console.log(this.toString());
}
// 获取整个链表
list () {
console.log('this.head: ', this.head);
return this.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"
circularLinked.remove('Tom');
circularLinked.removeAt(2);
circularLinked.print(); // "Susan,Jack,Paul"
circularLinked.list(); // 具体控制台
最后
数据结构的思想也在日常的编程中也颇为重要,要做到活学活用, 望各位看官 不吝点赞(前端界的一枚小学生)