一、复杂度概述
数据结构概述:在计算机中存储和组织数据的方式。
算法概述:解决方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制。
算法复杂度(时间/空间复杂度 ),大O表示法:
-
O(1) 常数阶
-
O(log(n)) 对数阶
-
O(n) 线性阶
-
O(nlog(n)) 线性和对数乘积
-
O(n2) 平方阶
-
O(2n) 指数阶
二、数组Array
线性结构(数组、链表、栈、队列和哈希表),非线性结构(树、图),抽象数据结构(集合、字典可看这一篇文章)。这些数据结构均有许多变种,本文主要介绍基础数据结构。
数组(Array)在js中有专门的api调用,常用方法可参考JavaScript 数组参考手册 (w3school.com.cn),此处不多赘述。
三、链表LinkedList
每个元素由存储元素本身的节点与一个指向下一个元素的引用组成
可以灵活实现动态内存管理
插入和删除操作时,时间复杂度O(1)
查找时间复杂度O(n)
单链表的示意图:
单链表的封装:
function LinkedList(){
//用来生成每个节点的实例
function Node(data){
this.data = data
this.next = null
}
//创建链表头
this.head = null
//创建链表长度
this.length = 0
}
单链表常见方法:
//增
LinkedList.prototype.insert = function(position, data){
//边界检查,越界返回false
if(position < 0 || position > this.length) return false
let newNode = new Node(data)
//如果插入第一个则直接赋值
if(position === 0){
this.head = newNode
}else{
let now = 0
let previous = null
let current = this.head
//遍历链表直到找到position指向的位置然后插入
while(now++ < position){
previous = current
current = current.next
newNode.next = current
previous.next = newNode
}
}
this.length += 1
return true
}
//删
LinkedList.prototype.removeAt = function(position){
//边界检查,越界返回false
if(position < 0 || position > this.length-1){
return false
}
//遍历链表,直到找到position指向的节点
let now = 0
let previous = null
let current = head
if(position === 0){
this.head = current.next
}else{
while(now++ < position){
previous = current
current = current.next
}
//删除当前节点
let output = current
previous.next = current.next
}
//减小链表长度
this.length-=1
return output.data
}
//查
LinkedList.prototype.indexOf = function(ele){
let current = this.head
let now = 0
//遍历链表直到找到data匹配的position
while(current){
if(current.data === ele) return now
current = current.next
now++
}
return null
}
//改
LinkedList.prototype.update = function(position, data){
//边界检查,越界返回false
if(position < 0 || position > this.length-1){
return false
}
//遍历链表,直到找到position指向的节点
let now = 0
let current = this.head
while(now++ < position){
current = current.next
}
//修改匹配到的节点为指定data
current.data = data
}
//返回链表的字符串形式
LinkedList.prototype.toString = function(){
let arrlist = []
let current = this.head
while(current){
arrlist.push(current.data)
current = current.next
}
return arrlist.toString()
}
四、栈Stack:先进后出线性表
只允许在表的一端进行插入与删除操作,这一端被称为栈顶,另一端被称为栈底
向栈顶插入新元素被称为入栈或压栈,它是把新元素放到栈顶元素上使其称为栈顶元素
从栈顶删除元素被称为出栈或弹栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素
栈可以用数组或链表的形式实现
栈的示意图:
栈的常用方法:
function Stack(){
//初始化栈
this.stack = []
//进栈
Stack.prototype.push = function(i){
this.stack.push(i)
}
//出栈
Stack.prototype.pop = function(){
return this.stack.pop()
}
//取栈顶
Stack.prototype.peek = function(){
return this.stack[stack.length-1]
}
//判断栈空
Stack.prototype.isEmpty = function(){
return this.stack.length===0
}
//求栈的大小
Stack.prototype.size = function(){
return this.stack.length
}
//返回栈的字符串形式
Stack.prototype.toString = function(){
let str = this.stack.reduce((origin, current)=>{
return origin+=current + ', '
}, '[')
return str.indexOf(',')>0 ? str.replace(/..$/, ']') : str.replace('[','[]')
}
}
栈的应用示例
// 使用栈结构完成十进制到二进制的转换
function de2bi(decnumber){
let stack = new Stack()
while(decnumber>0){
stack.push(decnumber%2)
decnumber = Math.floor(decnumber/2)
}
let bin = ''
while(!stack.isEmpty()){
bin += stack.pop()
}
return bin
}
// 验证有效的括号
var isValid = function(s) {
let map = {
"{":"}",
"[":"]",
"(":")",
}
let leftArr = [];
for(let ch of s){
if(ch in map){
leftArr.push(ch)
}else{
if(ch!=map[leftArr.pop()]){
return false
}
}
}
return !leftArr.length
};
五、队列queue:先进先出线性表
只能在表的前端进行删除操作
只能在表的后端进行插入操作
与栈类似,都有数组与链表结构
队列的示意图:
队列的常用方法:
function Queue(){
//初始化队列
this.queue = []
//进对
Queue.prototype.enqueue = function(e){
this.queue.push(e)
}
//出队
Queue.prototype.dequeue = function(){
return this.queue.shift()
}
//取队头
Queue.prototype.front = function(){
return this.queue[0]
}
//判断队空
Queue.prototype.isEmpty = function(){
return this.queue.length === 0
}
//求对列长
Queue.prototype.size = function(){
return this.queue.length
}
//返回字符串
Queue.prototype.toString = function(){
let str = ''
str = this.queue.reduce((origin, current)=>origin+=current+', ', '[')
return str.indexOf(',')>0 ? str.replace(/..$/, ']') : str.replace('[','[]')
}
}
队列的应用示例
//指定一定数量的参与者围成一圈从第一位开始从1开始依次报数,报到给定数字时出局,后一位从1开始重新报数,求最后一位出局者
function passgame(arrList, number){
//将参与者数组封装成一个队列
const queue = new Queue()
arrList.forEach(item=>queue.enqueue(item))
//给定循环的终止条件
while(queue.size()>1){
//开始数数
for(let i = 1; i < number; i++){
queue.enqueue(queue.dequeue())
}
queue.dequeue()
}
return queue.front()
}
console.log(passgame([1, 2, 3, 4, 5, 6], 3)) //1
六、哈希表Hash table
根据关键码值(Key value)而直接进行访问的数据结构。通过把关键码值映射到表中某个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
哈希表的示意图:
哈希表冲突:当通过哈希函数取得一个哈希值时,很有可能这个值的下标所处的位置上已经存放过数据,便出现冲突。
解决冲突:
-
链地址法
-
开放地址法
哈希表的常用方法(此处使用数组实现):
-
function HashTable(){ //初始化哈希表 //哈希表总长(可依据需求设置) this.limit = 10 //表中数据量 this.total = 0 //哈希表数组 this.item = [] //哈希函数(可依据需求设置) HashTable.prototype.hashFunc = function(key, limit){ //使用unicode编码的幂乘(可用霍纳法则简化)相加计算原始下标 let keyvalue = 0 for(let i=0; i<key.length; i++){ keyvalue=keyvalue * 12 + key.charCodeAt(i) } //使用取余运算计算hashCode return keyvalue%limit } //增 HashTable.prototype.put = function(key, value){ //根据hashCode找到对应下标的basket修改或者添加数据 let index = this.hashFunc(key, this.limit) let arr = this.item[index] if(arr){ //如果basket有内容则按照链地址法 let completed = false //遍历数组,如果发现重名数据则直接修改 arr.forEach(item=>{ if(key===item[0]){ completed = true return item[1] = value } }) //如果没有重名数据则向数组末尾添加新数据 if(completed){ arr.push([key, value]) this.total++ }else{ //如果basket为null则重新创建数组 this.item[index] = [[key, value]] this.total++ } } //查 HashTable.prototype.get = function(key){ let index = this.hashFunc(key, this.limit) let basket = this.item[index] //如果basket为null则没有对应数据 if(basket===undefined){ return null }else{ //如果有basket, 则遍历basket,遍历完没有对应key则不存在对应数据 for(let i = 0; i < basket.length; i++){ if(key===basket[i][0]) return basket[i][1] } return null } } //删 HashTable.prototype.remove = function(key){ let index = this.hashFunc(key, this.limit) let basket = this.item[index] //如果basket为null则没有对应数据 if(!basket){ return null }else{ //如果有basket, 则遍历basket,遍历完没有对应key则不存在对应数据 for(let i = 0; i < basket.length; i++){ if(key===basket[i][0]){ this.total-- return basket.splice(i, 1) } } return null } } //求表长 HashTable.prototype.size = function(){ return this.total } //判断表空 HashTable.prototype.isEmpty = function(){ return this.total===0 } }
七、树
树Tree:天然具有递归性质的数据机构,是一种特殊的图(无向无环图)。由n(n≥1)个有限节点组成一个具有层次关系的结构。每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树。
树的一般表示法:
树的孩子兄弟表示法:
二叉树(重点):
-
树中的每个节点最多只能有两个子节点,这样的树就成了二叉树
-
二叉树可以为空,也就是没有任何节点
-
若不为空,则它是由根节点和称为其左子树TL与右子树TR的两个不相交的二叉树组成
-
树除了以上链式表示外,还可以用数组,如下图
二叉搜索树(BST binary search tree)
-
二叉树的特殊形式,满足以下性质:
-
非空左子树的键值要小于根节点的键值
-
非空右子树的键值要大于根节点的键值
-
左右子树本身也是二叉搜索树
-
-
这种树的查找效率很高,蕴含了二分查找的思想
-
中序遍历二叉搜索树结果为有序表
二叉搜索树的封装
function BinaryTree(){
//使用根变量来指向根节点
this.root = null
//节点定义
function TreeNode(key){
this.key = key
this.left = null
this.right = null
}
}
// 构建树
let root = new TreeNode(1);
root.left = new TreeNode(2);
root.right = new TreeNode(3);
root.left.left = new TreeNode(4);
root.left.right = new TreeNode(5);
二叉搜索树的常用方法:
function BinarySearchTree(){
//初始化
//使用根变量来指向根节点
this.root = null
//封装一个节点,里面有key,左子树指针,右子树指针
function Node(key){
this.key = key
this.left = null
this.right = null
}
//插值
BinarySearchTree.prototype.insert = function(key){
let newNode = new Node(key)
//判断根节点是否为空,空则直接插入
if(this.root === null){
this.root = newNode
}else{
//不为空则执行递归函数来将新节点插入相应位置
this.insertNode(this.root, newNode)
}
}
//插节点
BinarySearchTree.prototype.insertNode = function(root, newNode){
//当root存在时继续递归
if(root !== null){
//当新节点的值大于当前树根节点时
if(newNode.key > root.key){
//如果树根节点有右子树时,将右子树当做新树根节点传入
if(root.right !== null){
this.insertNode(root.right, newNode)
}else{
//如果树根节点的右子树不存在时,将新节点存放于右节点上
root.right = newNode
}
}else{
//当新节点的值小于当前树根节点时
if(root.left !== null){
//如果树根节点有左子树时,将左子树当做新树根节点传入
this.insertNode(root.left, newNode)
}else{
//如果树根节点的左子树不存在时,将新节点存放于左节点上
root.left = newNode
}
}
}
}
//查找
BinarySearchTree.prototype.search = function(key){
//如果根节点不存在直接返回null
if(this.root === null){
return false
}else{
//如果存在就遍历整棵树
let current = this.root
//current不存在则跳出循环
while(current !== null){
//遇到键值匹配则存在此数据
if(current.key === key) return true
//当查找的键值小于当前节点的键值则向节点的左边查找否则向右查找
current = current[key < current.key ? 'left': 'right']
}
//如果循环到了树的最深层都没有相应键值则证明不存在这个值
return false
}
}
}
//中序遍历
BinarySearchTree.prototype.inOrderTraversal = function(){
let arr = []
//如果根节点不存在则直接返回
if(this.root === null) return arr
//如果根节点存在则递归遍历树
this.traversalNode('in', this.root, item=>{
arr.push(item)
})
return arr
}
//前序遍历
BinarySearchTree.prototype.preOrderTraversal = function(){
let arr = []
//如果根节点不存在则直接返回
if(this.root === null) return arr
//如果根节点存在则递归遍历树
this.traversalNode('pre', this.root, item=>{
arr.push(item)
})
return arr
}
//后序遍历
BinarySearchTree.prototype.postOrderTraversal = function(){
let arr = []
//如果根节点不存在则直接返回
if(this.root === null) return arr
//如果根节点存在则递归遍历树
this.traversalNode('post', this.root, item=>{
arr.push(item)
})
return arr
}
//中序、前序、后序遍历均为深度遍历(不必非得BST)
BinarySearchTree.prototype.traversalNode = function(order, root, callback){
//root存在才继续执行递归
if(root !== null){
//中序遍历
if(order === 'in'){
//从当前节点的左子树开始继续遍历
this.traversalNode(order, root.left, callback)
//已经从左子树处跳回的那个节点对节点的key进行处理
callback(root.key)
//从当前节点的右子树开始遍历
this.traversalNode(order, root.right, callback)
//前序遍历
}else if(order === 'pre'){
//先对当前节点进行操作
callback(root.key)
//从当前节点的左子树开始继续递归遍历
this.traversalNode(order, root.left, callback)
//从当前节点的右子树开始继续递归遍历
this.traversalNode(order, root.right, callback)
//后序遍历
}else if(order === 'post'){
//从当前节点的左子树开始继续递归遍历
this.traversalNode(order, root.left, callback)
//从当前节点的右子树开始继续递归遍历
this.traversalNode(order, root.right, callback)
//对当前节点进行操作
callback(root.key)
}
}
}
//找最小值
BinarySearchTree.prototype.min = function(){
//二叉搜索树特点中整个树最左边的节点是最小值
let current = this.root
while(current.left !== null){
current = current.left
}
return current.key
}
//找最大值
BinarySearchTree.prototype.max = function(){
//整个树最右边的节点是最大值
let current = this.root
while(current.right !== null){
current = current.right
}
return current.key
}
//删
//删除二叉搜索树中的任意节点,此方法难度较高,将几种移除的可能性提前列出
// 1.移除的节点是叶子节点,即度为0时
// 2.移除的节点只有一个子节点,即度为1时
// 3.溢出的节点有两个子节点,即度为2时
//每种情况下也有多种情形,详细步骤在代码中讨论
BinarySearchTree.prototype.remove = function(key){
//初始化需要使用到的变量
let parrent = null
let current = this.root
let isLeftChild = true
//遍历二叉树直到找到目标节点
while(current !== null){
if(key = current.key) break
//当目标键值小于当前节点键值时向左子树继续查找
if(key < current.key){
parrent = current
current = current.left
isLeftChild = true
}else{
parrent = current
current = current.right
isLeftChild = false
}
}
//当current不是空时则找到了目标节点
if(current !== null){
//此时目标节点会分成3种情况,进行分别讨论
//情况一,度为0
if(current.left === null && current.right === null){
//当根节点的键值满足时直接删除
if(current.key === this.root.key){
this.root = null
}else{
//不是根节点时就是叶子节点,直接删除
//根据目标节点是父节点的左子树还是右子树来决定清空父节点的哪个指针
parrent[isLeftChild ? 'left' : 'right'] = null
}
//当目标节点度为2的情况,此处使用前驱来代替目标节点
}else if(current.left !== null && current.right !== null){
//当前节点为根节点时,直接将前驱当做根节点
if(current.key === this.root.key){
const preNode = this.findPre(current)
preNode.left = current.left
preNode.right = current.right
this.root = preNode
}else{
//当前节点不为根节点时
//根据目标节点是父节点的左子树还是右子树来决定父节点的哪个指针指向前驱节点
const preNode = this.findPre(current)
preNode.left = current.left
preNode.right = current.right
parrent[isLeftChild ? 'left' : 'right'] = preNode
}
}else{
//当目标节点度为1的情况
//当current为根节点时直接将根节点指针指向current的子节点
if(current.key === this.root.key){
this.root = current.left || current.right
}else{
//current不是根节点时将current的子节点代替current的位置
const childNode = current.left || current.right
parrent[isLeftChild ? 'left' : 'right'] = childNode
}
}
return current.key
}else{
//当current为空时则说明没有对应目标节点,返回false
return false
}
}
//返回一个传入参数的前驱节点
BinarySearchTree.prototype.findPre = function(root){
let current = root.left
let preParrent = root
//循环直到搜索到根节点的前驱节点
while(current.right !== null){
preParrent = current
current = current.right
}
//前驱节点还有左子树时
if(current.left !== null){
//如果是根节点的左子树则将前驱节点的左子树赋值给根节点的左子树
//如果不是则用前驱节点的左子树赋值给前驱节点的父节点的右子树
preParrent[current === root.left ? 'left' : 'right'] = current.left
}else{
//没有左子树则直接置空前驱节点的原位置
//如果是根节点的左子树则清空左子树
//如果不是则清空父节点的右子树
preParrent[current === root.left ? 'left' : 'right'] = null
}
//返回前驱节点
return current
}
图Graph:由顶点的有穷非空集合V(G)和顶点之间边的集合E(G)组成,通常表示为: G=(V,E),其中,G表示个图,V是图G中顶点的集合,E是图G中边的集合。
图有两种表示法:
1.邻接矩阵:
-
可以通过一个二维数组来表示图的结构
-
当图为稀疏图时这个二维数组许多的空间都会被赋值为0,浪费计算机存储空间
邻接矩阵示意图:
2.邻接表:
-
邻接表由一个顶点以及跟这个顶点相邻的其他顶点列表组成
-
列表有许多方式可以存储: 数组,链表,哈希表
图的封装:
function Graph(){
//用来保存顶点的数组结构
this.vertexes = []
//用来保存边的字典结构,js中直接用对象代替
this.edge = {}
}
图的常用方法:
function Graph(){
//初始化图
//用来保存顶点的数组结构
this.vertexes = []
//用来保存边的字典结构,js中直接用对象代替
this.edge = {}
//添加顶点
Graph.prototype.setVertex = function(v){
//保存顶点,并在邻接表中生成一个数组
this.vertexes.push(v)
this.edge[v] = []
}
//添加边
Graph.prototype.setEdge = function(v1, v2){
//顶点判断,此处实现无向图,因此没有对应的两个顶点则返回false
if(this.edge.hasOwnProperty(v1) && this.edge.hasOwnProperty(v1)){
//在邻接表中v1对应的位置压入要互相形成边的v2
this.edge[v1].push(v2)
//在邻接表中v2对应的位置压入要互相形成边的v1
this.edge[v2].push(v1)
return true
}
return false
}
//返回字符串
Graph.prototype.toString = function(){
//遍历邻接表中的顶点
let result = ''
for(key in this.edge){
result += `${key} -> `
this.edge[key].forEach(item=>{
result += `${item} `
})
result += '\n'
}
return result
}
//初始化节点
Graph.prototype.initializeColor = function(){
//创建一个字典保存每个顶点的颜色用于遍历,每次遍历前初始化颜色为白色表示没有访问也没有探索过
//颜色使用数字表示,白-0,灰-1,黑-2
this.color = {}
this.vertexes.forEach(item=>{
this.color[item] = 0
})
}
}
图的遍历:
-
图的遍历意味着要将图的每个顶点访问一次,并且不能有重复的访问
-
遍历方法:
-
广度优先遍历(Breadth-first-search BFS)
-
深度优先搜索(Depth-first-search DFS)
-
-
为了记录顶点是否被访问过,使用三种状态来反应他们的状态
-
白色:表示该顶点还没有被访问过
-
灰色:表示该顶点被访问过但没有被探索过
-
黑色:表示该顶点被访问过且被探索过
-
1.广度优先遍历(层次遍历)示意图:
实现方法:
Graph.prototype.BFS = function(v, handler){
//初始化颜色
this.initializeColor()
//创建队列来保存访问的顶点
let queue = new Queue()
//将第一个顶点压入队列
queue.enqueue(v)
//将第一个顶点置为灰色表示访问但未探索过
this.color[v] = 1
while(queue.size() > 0){
let current = queue.dequeue()
//判断当前取出的顶点有没有被探索过,探索过则取队列中下一个顶点
if(this.color[current] === 2){
continue
}
//将从队列取出的顶点置为黑色表示访问且探索过
this.color[current] = 2
//执行回调函数并传入顶点名作为参数
handler(current)
//将与取出的顶点相连的顶点都压入队列并将没有访问过的顶点置为灰色
this.edge[current].forEach(item=>{
queue.enqueue(item)
if(this.color[item] === 0){
this.color[item] = 1
}
})
}
}
2.深度优先遍历示意图:
实现方法:
Graph.prototype.DFS = function(v, handler){
//初始化顶点颜色
this.initializeColor()
//执行递归
this.DFSPerVertex(v, handler)
}
Graph.prototype.DFSPerVertex = function(v, handler){
//已经探索过的节点退出递归
if(this.color[v] === 2){
return
}
//探索当前节点并将当前节点置为黑色
this.color[v] = 2
//执行回调
handler(v)
//访问与当前探索的顶点相连的其他顶点
this.edge[v].forEach(item=>{
//没有访问过的顶点置为灰色,访问过的顶点不再访问
if(this.color[item] === 0){
this.color[item] = 1
DFSPerVertex(item, handler)
}
})
}