数组
数组是可以再内存中连续存储多个元素的结构,在内存中的分配也是连续的,数组中的元素通过数组下标进行访问,数组下标从0开始。
-
优点:
- 按照索引查询元素速度快
- 按照索引遍历数组方便
-
缺点:
添加,删除的操作慢,因为要移动其他的元素。
-
适用场景:
频繁查询,对存储空间要求不大,很少增加和删除的情况。
栈
栈是一种特殊的线性表,仅能在线性表的一端操作,栈顶允许操作,栈底不允许操作。 栈的特点是:先进后出,或者说s是后进先出,从栈顶放入元素的操作叫入栈,取出元素叫出栈。
class Stack {
datas = [];
push = (data) => {
this.datas.push(data);
};
pop = () => {
if (this.size() > 0) {
return this.datas.pop();
}
return null;
};
peek = () => {
if (this.size() > 0) {
return this.datas[this.size() - 1];
}
return null;
};
size = () => {
return this.datas.length;
};
clear = () => {
this.datas.length = 0;
};
isEmpty = () => {
return this.size() === 0;
};
}
- 适用场景:匹配规则(括号的匹配)、回溯相关问题(操作撤销、回文数等)、深度优先算法
// 1024 的 2进制 '10000000000'
// 进制转化
function hexadecimalConversion(number, scale) {
const stack = new Stack();
let num = number;
let res = "";
while (num > 0) {
stack.push(num % scale);
num = Math.floor(num / scale);
}
while (!stack.isEmpty()) {
res += stack.pop();
}
return res;
}
hexadecimalConversion(1024, 2); // '10000000000'
队列
只允许在一端插入数据操作,在另一端进行删除数据操作的特殊线性表;进行插入操作的一端称为队尾(入队列),进行删除操作的一端称为队头(出队列);队列具有先进先出(FIFO)的特性 .
class Queue {
constructor() {
this.datas = [];
}
enqueue(data) {
this.datas.push(data);
}
dequeue() {
if (this.size()) {
return this.datas.shift();
}
return null;
}
peek() {
if (this.size()) {
return this.datas[0];
}
return null;
}
size() {
return this.datas.length;
}
isEmpty() {
return this.size() === 0;
}
}
- 适用场景:event队列、promise回调队列、消息队列、广度优先算法
const treeNode = {
value: "1",
children: [
{
value: "1-1",
children: [
{
value: "1-1-1",
children: [
{
value: "1-1-1-1",
},
{
value: "1-1-1-2",
},
],
},
{
value: "1-1-2",
},
],
},
{
value: "1-2",
children: [
{
value: "1-2-1",
},
{
value: "1-2-2",
children: [
{
value: "1-2-2-1",
},
{
value: "1-2-2-2",
},
],
},
],
},
],
};
/*
[
{ value: "1" },
{ value: "1-1" },
{ value: "1-1-1" },
{ value: "1-1-1-1" },
{ value: "1-1-1-2" },
{ value: "1-1-2" },
{ value: "1-2" },
{ value: "1-2-1" },
{ value: "1-2-2" },
{ value: "1-2-2-1" },
{ value: "1-2-2-2" },
];
*/
function deepTraversal(node) {
let nodes = [];
if (node != null) {
let stack = [];
stack.push(node);
while (stack.length != 0) {
const item = stack.pop();
nodes.push(item);
let childrens = item.children;
if (childrens) {
for (let i = childrens.length - 1; i >= 0; i--) {
stack.push(childrens[i]);
}
delete item.children;
}
}
}
return nodes;
}
deepTraversal(treeNode);
链表
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,这些节点不必在内存中相连。每个节点由数据部分Data和链部分Next,Next指向下一个节点,这样当添加或者删除时,只需要改变相关节点的Next的指向,效率很高。
class Node {
constructor(data) {
this.data = data; // 节点的数据域
this.next = null; // 节点的指针域
}
}
class SingList {
constructor(equalsFn) {
this.size = 0; // 单链表的长度
this.head = new Node("head"); // 表头节点
this.currentNode = null; // 当前节点的指向
this.equalsFn = equalsFn;
}
// 向单链表中插入元素
insert(item, index) {
if (index < 1) {
item.next = this.head.next;
this.head.next = item;
} else if (index > this.size) {
this.currentNode.next = item;
} else {
let i = 0;
let node = this.head;
while (i < index) {
node = node.next;
}
item.next = node.next;
node.next = item;
}
this.size++;
}
// 在单链表中删除一个节点
remove(item) {
let node = this.head;
while (node) {
if (equalsFn(node.next, item)) {
node.next = node.next.next;
this.size--;
return true;
}
}
return false;
}
// 在单链表的尾部添加元素
append(element) {
if (this.size) {
this.currentNode.next = element;
} else {
this.head.next = element;
}
this.currentNode = element;
this.size++;
}
// 在单链表中寻找item元素
indexOf(item) {
let i = 1;
let node = this.head.next;
while (node) {
if (equalsFn(node, item)) {
return i;
}
i++;
node = node.next;
}
return -1;
}
getElementAt(index) {
if (index < 1) {
return this.head;
}
let i = 1;
let node = this.head.next;
while (i < index) {
node = node.next;
i++;
}
return node;
}
// 获取单链表的最后一个节点
getLast() {
return this.currentNode;
}
getHead() {
return this.head;
}
// 判断单链表是否为空
isEmpty() {
return this.size === 0;
}
// 获取单链表的长度
getLength() {
return this.size;
}
// 单链表的遍历显示
toString() {
}
// 清空单链表
clear() {
this.size = 0;
this.head.next = null;
this.currentNode = null;
}
}
-
应用场景:数据量较小,需要频繁增加,删除操作的场景 。如react fiber 、数据库取值
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BuOSbfZf-1637222000355)(/Users/kangle483/Desktop/来自与 康乐 的会话_1637056737543.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yGDKCmNE-1637222000358)(/Users/kangle483/Desktop/截屏2021-11-15 下午11.45.41.png)]
-
优点:
链表是很常用的一种数据结构,不需要初始化容量,可以任意加减元素;
添加或者删除元素时只需要改变前后两个元素结点的指针域指向地址即可,所以添加,删除很快; -
缺点:
因为含有大量的指针域,占用空间较大;
查找元素需要遍历链表来查找,非常耗时。 -
拓展:
-
双向链表
双向链表和普通链表的区别在于,在链表中,一个节点只有下一个节点的链接,而双向链表中链接是双向的,有一个节点的链接上一个元素和一个节点的链接下一个元素
-
循环链表
循环链表的节点可以向单链表或者双向链表一样,循环链表和链表之前的区别在于最后一个元素的下一个元素的指针指向的是第一个元素。
-
有序链表
有序链表是指是一个有序的链表结构。
-
集合
集合是一组无序且唯一的项组成的
class Set {
constructor() {
this.items = {};
}
// 添加项
add(item) {
if (!this.has(item)) {
this.items[item] = item;
return true;
}
return false;
}
//删除项
delete(item) {
if (this.has(item)) {
delete this.items[item];
return true;
}
return false;
}
//判断集合中是否有某项
has(item) {
return Object.prototype.hasOwnProperty.call(this.items, item);
}
// 移除所有项
clear() {
this.items = {};
}
// 获取集合中的数据的个数
size() {
return Object.keys(this.item).length;
}
// 返回集合中所有的数据
values() {
return Object.values(this.items);
}
// 并集
union(otherSet) {
const unionSet = new Set();
this.values().forEach((item) => unionSet.add(item));
otherSet.values().forEach((item) => unionSet.add(item));
return unionSet;
}
// 交集
intersection(otherSet) {
const intersectionSet = new Set();
this.values().forEach(
(item) => otherSet.has(item) && intersectionSet.add(item)
);
return intersectionSet;
}
// 差集
difference(otherSet) {
const differenceSet = new Set();
this.values().forEach(
(item) => !otherSet.has(item) && intersectionSet.add(item)
);
return differenceSet;
}
// 子集
ifSub(otherSet) {
if (otherSet.size() < this.size()) {
return false;
}
return this.values().every((item) => otherSet.has(item));
}
}
- 应用场景:ES6的Set 、 数据去重、唯一标识
字典
集合表示一组互不相同的元素(不重复的元素)。在字典中,存储的是[键,值]对,其中键名是用来查询特定元素的。字典和集合很相似,集合以[值,值]的形式存储元素,字典则是以[键,值]的形式来存储元素。字典也称作映射
// 字典的value数据结构
class DictionaryValue {
constructor(key, value) {
this.key = key;
this.value = value;
}
toString() {
return `[${this.key} : ${this.value}]`;
}
}
class Dictionary {
constructor() {
this.table = {};
}
set(key, value) {
if (key !== null && value !== null) {
this.table[key] = new DictionaryValue(key, value);
return true;
}
return false;
}
get(key) {
if (this.hasKey(key)) {
return this.table[key];
}
return undefined;
}
hasKey(key) {
return Object.prototype.hasOwnProperty.call(this.table, key);
}
clear() {
this.table = {};
}
size() {
return Object.keys(this.table).length;
}
isEmpty() {
return this.size() === 0;
}
keyValues() {
return Object.values(this.table);
}
keys() {
return this.keyValues().map((item) => item.key);
}
values() {
return this.keyValues().map((item) => item.value);
}
forEach(callback) {
this.keyValues().forEach((item) => callback(item));
}
toString() {
// 依据自己的方式来写
}
}
- 应用场景:大量数据下检索查找、 ES6中的Map、后端的redis
散列表
散列算法的作用是尽可能快地在数据结构中找到一个值。如果要在数据结构中获得一个值(使用get方法),需要遍历整个数据结构来找到它。如果使用散列函数,就知道值的具体位置,因此能够快速检索到该值。散列函数的作用是给定一个键值,然后返回值在表中的地址。
class HashTable {
constructor(toString) {
this.table = {};
this.toString = toString || this.toString;
}
toString(key) {
if (key === null) {
return "null";
}
if (key === undefined) {
return "undefined";
}
return key.toString();
}
// 散列方法 获取hash值
loselosehashCode(key) {
if (typeof number === "number") {
return key;
}
const tableKey = this.toString(key);
let hash = 0;
for (let i = 0; i < tableKey.length; i++) {
hash = 37 * hash + tableKey.charCodeAt(i);
}
return hash % 100;
}
put(key, value) {
if (key !== null || value !== undefined) {
const hash = this.loselosehashCode(toString(key));
if (!this.table[hash]) {
this.table[hash] = new SingList();
}
this.table[hash].push(new DictionaryValue(key, value));
return true;
}
return false;
}
get(key) {
if (key !== null || value !== undefined) {
const hash = this.loselosehashCode(toString(key));
if (this.table[hash]) {
const current = this.table[hash].getHead().next;
while (current) {
if (current.key === key) {
return current.value;
}
current = current.next;
}
}
}
return undefined;
}
remove(key) {
if (key !== null || value !== undefined) {
const hash = this.loselosehashCode(toString(key));
if (this.table[hash]) {
const current = this.table[hash].getHead();
while (current.next) {
if (current.next.data.key === key) {
current.next = current.next.next;
return true;
}
current = current.next;
}
}
}
return false;
}
}
-
应用场景:数据查找,
-
Q : 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
示例:
给定 nums = [2, 7, 11, 15] , target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
树
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。把它叫做 “树” 是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:
- 每个节点有零个或多个子节点;
- 没有父节点的节点称为根节点;
- 每一个非根节点有且只有一个父节点;
- 除了根节点外,每个子节点可以分为多个不相交的子树;
二叉树基本概念
- 定义
二叉树是每个节点最多有两棵子树的树结构。通常子树被称作“左子树”和“右子树”。二叉树常被用于实现二叉查找树和二叉堆。
class TreeNode {
constructor(key) {
this.left = null;
this.right = null;
this.key = key;
}
}
class Tree {
constructor() {
this.root = null;
}
// 插入树节点
insert(key) {
if (this.root === null) {
this.root = new TreeNode(key);
} else {
this.insertNode(this.root, key);
}
}
insertNode(node, key) {
if (node.Key > key) {
if (node.left === null) {
node.left = new TreeNode(key);
} else {
this.insertNode(node.left, key);
}
} else {
if (node.right === null) {
node.right = new TreeNode(key);
} else {
this.insertNode(node.right, key);
}
}
}
// 前序遍历
preOrderTraverse(callback) {
this.preOrderTraverseNode(this.root, callback);
}
preOrderTraverseNode(node, callback) {
if (node !== null) {
callback(node);
this.preOrderTraverseNode(node.left, callback);
this.preOrderTraverseNode(node.right, callback);
}
}
// 中序遍历
inOrderTraverse(callback) {
this.inOrderTraverseNode(this.root, callback);
}
inOrderTraverseNode(node, callback) {
if (node !== null) {
this.inOrderTraverseNode(node.left, callback);
callback(node);
this.inOrderTraverseNode(node.right, callback);
}
}
// 后序遍历
postOrderTraverse(callback) {
this.postOrderTraverseNode(this.root, callback);
}
postOrderTraverseNode(node, callback) {
if (node !== null) {
this.postOrderTraverseNode(node.left, callback);
this.postOrderTraverseNode(node.right, callback);
callback(node);
}
}
}
-
相关性质
二叉树的每个结点至多只有2棵子树(不存在度大于2的结点),二叉树的子树有左右之分,次序不能颠倒。
二叉树的第i层至多有2(i-1)个结点;深度为k的二叉树至多有2k-1个结点。
一棵深度为k,且有2^k-1个节点的二叉树称之为 满二叉树 ;
深度为k,有n个节点的二叉树,当且仅当其每一个节点都与深度为k的满二叉树中,序号为1至n的节点对应时,称之为 完全二叉树 。
-
三种遍历方法
在二叉树的一些应用中,常常要求在树中查找具有某种特征的节点,或者对树中全部节点进行某种处理,这就涉及到二叉树的遍历。二叉树主要是由3个基本单元组成,根节点、左子树和右子树。如果限定先左后右,那么根据这三个部分遍历的顺序不同,可以分为先序遍历、中序遍历和后续遍历三种。
(1) 先序遍历 若二叉树为空,则空操作,否则先访问根节点,再先序遍历左子树,最后先序遍历右子树。
(2) 中序遍历 若二叉树为空,则空操作,否则先中序遍历左子树,再访问根节点,最后中序遍历右子树。
(3) 后序遍历 若二叉树为空,则空操作,否则先后序遍历左子树访问根节点,再后序遍历右子树,最后访问根节 点。
-
拓展:
-
二叉搜索树(BinarySearchTree)
(又:二叉查找树,二叉排序树)它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。二叉搜索树作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
-
- 节点的颜色只能是红色或者黑色;
- 根节点是黑色的;(根性质)
- NIL 节点的颜色是黑色;
- 如果节点的颜色是红色,则其子节点均为黑色;(红性质)
- 从任一节点到其后代任一叶子节点的路径上的黑色节点的数量相同;(黑性质)
-
堆
堆是一种比较特殊的数据结构,是二叉树的数组表现形式,具有以下的性质:
堆中某个节点的值总是不大于(大顶堆)或不小于(小顶堆)其父节点的值;
堆总是一棵完全二叉树。
将根节点最大的堆叫做大顶堆,根节点最小的堆叫做小顶堆。
图
图是网络结构的抽象模型。图是一组由边连接的节点(或顶点)。图是重要的,因为任何二元关系都可以用图来表示
一个图G = (V, E)由以下元素组成
V:一组顶点
E:一组边,连接V中的顶点
-
图有多种表现形式:
-
邻接矩阵
邻接矩阵每个节点都和一个整数相关联,该整数将作为数组的索引。我 们用一个二维数组来表示顶点之间的连接。如果索引为i的节点和索引为j的节点相邻,则array[i][j] === 1
,否则array[i][j] === 0
-
邻接表
也可以使用一种叫作邻接表的动态数据结构来表示图。邻接表由图中每个顶点的相邻顶点列表所组成。存在好几种方式来表示这种数据结构。我们可以用列表(数组)、链表,甚至是散列表或是字典来表示相邻顶点列表。-
关联矩阵
还可以用关联矩阵来表示图。在关联矩阵中,矩阵的行表示顶点,列表示边。使用二维数组来表示两者之间的连通性,如果顶点v是边e的入射点,则
array[v][e] === 1
; 否则,array[v][e] === 0
-
class Graph {
constructor(isDirected = false) {
this.isDirected = isDirected; // 是否有向
this.vertices = [];
this.adjList = new Dictionary();
}
// 添加点
addVertex(v) {
if (!this.vertices.includes(v)) {
this.vertices.push(v);
this.adjList.set(v, []);
}
}
// 添加边
addEdge(v, w) {
//如果没有这个点 创建一个点
if (!this.adjList.get(v)) {
this.addVertex(v);
}
//如果没有这个点 创建一个点
if (!this.adjList.get(w)) {
this.addVertex(w);
}
this.adjList.get(v).push(w);
// 如果不是有向的 那么另外一个点也要加上能够访问
if (!this.isDirected) {
this.adjList.get(w).push(v);
}
}
getVertices() {
return this.vertices;
}
getadjList() {
return this.adjList;
}
// 广度优先
bfs(v, callback) {
const color = [];
const queue = new Queue();
for (let i = 0; i < this.vertices.length; i++) {
color[this.vertices[i]] = "white"; //{1}
}
queue.enqueue(v);
while (!queue.isEmpty()) {
const u = queue.dequeue();
const neighbors = this.adjList.get(u);
color[u] = "grey";
for (let i = 0; i < neighbors.length; i++) {
const w = neighbors[i];
if (color[w] === "white") {
color[w] = "grey";
queue.enqueue(w);
}
}
color[u] = "black";
if (callback) {
callback(u);
}
}
}
// 深度优先
dfs(v, callback) {
const color = [];
const stack = new Stack();
for (let i = 0; i < this.vertices.length; i++) {
color[this.vertices[i]] = "white";
}
stack.push(v);
while (!stack.isEmpty()) {
const u = stack.pop();
const neighbors = this.adjList.get(u);
color[u] = "grey";
for (let i = 0; i < neighbors.length; i++) {
const w = neighbors[i];
if (color[w] === "white") {
color[w] = "grey";
stack.push(w);
}
}
color[u] = "black";
if (callback) {
callback(u);
}
}
}
// 打印出一个邻接表
toString() {
let s = "";
for (let i = 0; i < this.vertices.length; i++) {
s += this.vertices[i] + " -> ";
const neighbors = this.adjList.get(this.vertices[i]);
for (var j = 0; j < neighbors.length; j++) {
s += neighbors[j] + " ";
}
s += "\n";
}
return s;
}
}
const graph = new Graph();
const myVertices = ["A", "B", "C", "D", "E", "F", "G", "H", "I"];
for (let i = 0; i < myVertices.length; i++) {
graph.addVertex(myVertices[i]);
}
graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("A", "D");
graph.addEdge("C", "D");
graph.addEdge("C", "G");
graph.addEdge("D", "G");
graph.addEdge("D", "H");
graph.addEdge("B", "E");
graph.addEdge("B", "F");
graph.addEdge("E", "I");
console.log(graph.toString());
// A -> B C D
// B -> A E F
// C -> A D G
// D -> A C G H
// E -> B I
// F -> B
// G -> C D
// H -> D
// I -> E
graph.bfs(myVertices[0],console.log);
// A;
// B;
// C;
// D;
// E;
// F;
// G;
// H;
// I;
- 图的遍历
- 广度优先算法
- 深度优先算法