数据结构
数据结构:相互之间存在的一种多种的特定关系的数据元素的集合
逻辑结构 物理结构
逻辑结构是面向问题的,物理结构是面向计算机的,物理结构的基本目标就是将数据结构及其逻辑关系存储到计算机的内存中
高德纳
程序设计的实质是对确定问题,选择一种好的结构,加上设计一种好的算法. 程序设计=数据结构+算法
对象是暴露行为,数据结构暴露是数据
什么是数据
数据是描述客观事物的符号
数据=符号
- 可以输入到计算机中。
- 能够被计算机识别和处理
数据的目的是存储,存储的目的是:后期再利用
简单数据类型,我们可以用变量或者数组对数据进行储存
数据结构的作用主要是:阐述关系
结构:简单的理解就是关系,不同的数据元素之间不是独立的,而是存在特定关系的
逻辑结构:
逻辑结构:数据对象中数据元素之间的相互关系
- 集合结构
- 线性结构
- 树形结构
- 图形结构
集合结构:
集合结构:
数据元素同属于一个集合,他们之间没有其他关系,
他们的共同属性是:'用属于一个集合'
线性结构 :
最典型的数据关系是一对一.线性结构是一种有序的数据集合.
线性结构:除了第一个和最后一个数据元素之外,其他数据元素都是首尾相接的
1、必存在一个第一个元素
2、必存在最后的一个元素
3、除最后一个元素外,其他的数据元素均有一个唯一的“后续”
4、除第一个元素之外,其他数据元素均有一个唯一的前驱
数组就是线性结构,盏,队列
树形结构
树形结构
数据元素一对多的关系
图形结构
图形结构
数据元素是多对多的关系
物理结构:数据元素存储到计算中的存储器。内存而言数据的存储结构应该正确的反应数据元素之间的逻辑关系
顺序存储,链式存储
常见的数据结构
数据元素
数据的基本单位也称为结点或则记录
数据对象
相同特性的数据元素的集合,是数据的一个子集
数据对象
独立含义的数据的最小单位
js的数组不是真正意义上的数组
数组:在内存中用一串连续的区域来存放一些值。数组是相同类型数据元素的有序集合
数组是由相同类型的元素的集合组成的数据结构
连续内存空间
JS的数组元素是不是可以是任意类型,JS中的内存地址是不连续的
JS数组的优缺点:
优点
1、按照索引查询元素的时候速度很快
2.储存大量数据
3.按照数据去遍历数组
4.定义方便,访问灵活
缺点
1.根据内容查找会很慢
2.数组的大小一经确定不能改变,不适合动态储存
3.数组只能存储相同类型的数据
4.增加、删除元素效率很低
栈
内储存中的堆栈和数据结构中的堆栈不是一个概念,内储存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构
后进先出
数据结构中的栈:是一种受限制的线性表,它遵循后进先出(LIFO)
其限制是仅允许在表的一端进行插入和删除运算,这一端被称为栈顶,相对地,把另—端称为栈底
向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素
从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素
受限制最直白的理解是:新增数据,删除数据,查找等操作时,不能随心所欲,必须遵循一定的限制(规则)
栈的应用
class Stack {
constructor() {
this.items = []
}
// 添加元素到栈
push(ele) {
this.items.push(ele)
}
// 出栈
pop() {
return this.items.pop()
}
// 返回栈顶元素
peek() {
return this.items[this.items.length - 1]
}
// 判断栈中元素是否为空
istempy() {
return this.items.length === 0
}
// 获取栈中元素的个数
size() {
return this.items.length
}
// 清空栈
clear() {
this.items.length = 0
}
}
const num=(a)=>{
let stac=new Stack()
let render=0
let top=''
while(a>0){
render=a%2
stac.push(render)
a=Math.floor(a/2)
}
while(!stac.istempy()){
top+=stac.pop()
}
return top
}
console.log(num(100));
js执行上下文
执行上下文:就是当前js代码被解析和执行所在环境的抽象概念
js中的任何代码都是在执行下文中运行的(执行环境)
全局执行上下文 (默认,最基础的执行上下文)
1.创建全局对象
2.将this指向这个全局对象
函数执行上下文
每次调用函数的时候,会为这个函数创建一个新的执行上下文。
Ecal执行上下文
eval('alert('asda')')
能够将符合标准的字符串当作s代码执行
执行栈,调用栈
后进先出,用于存储在代码执行期间创建的所有的执行上下文
const one =()=>{
two()
console.log('我是one');
};
const two=() =>{
console.log('我是two ' );
};
one();
1.JS引擎创建一个新的全局执行上下文并且将这个执行上下文推入到当前的执行栈中
执行栈用于:存储在代码执行期间创建的所有的执行上下文
每当发生函数调用的时候,JS引擎都会为该函数创建一个新的执行上下文并且PUSH到当前执行栈的栈顶
当调用one函数的时候,js引擎为这个函数创建一个新的执行上下文并将其推到当前执行栈的栈顶
class Stack {
constructor() {
this.items = []
}
// 添加元素到栈
push(ele) {
this.items.push(ele)
}
// 出栈
pop() {
return this.items.pop()
}
// 返回栈顶元素
peek() {
return this.items[this.items.length - 1]
}
// 判断栈中元素是否为空
istempy() {
return this.items.length === 0
}
// 获取栈中元素的个数
size() {
return this.items.length
}
// 清空栈
clear() {
this.items.length = 0
}
}
const num=(a)=>{
let stac=new Stack()
let render=0
let top=''
while(a>0){
render=a%2
stac.push(render)
a=Math.floor(a/2)
}
while(!stac.istempy()){
top+=stac.pop()
}
return top
}
console.log(num(100));
栈溢出
调用栈超出了栈的范围造成了溢出
let bar=()=>{
bar()
}
bar()
斐波那契数列
function num(a){
if(a==1||a==0){
return 1
}
return num(a-1)+num(a-2)
}
console.log(num(2));
尾递归
function num(a,total=1,total2=1){
if(a<=1){
return total2
}
return num(a-1,total2,total+total2)
}
console.log(num(50));
递归需要同时保存成百上千个调用帧。很容易就会发生栈溢出,但是对于尾递归来说。由于只存在一个调用栈,所以永远不会发生栈溢出错误
队列(queue)
队列,(queue),它是一种一种运算受限的线性表,FIFO(先进先出)
栈:后进先出LIFO
队列(Queue),它是一种运算受限的线性表,队列遵循先进先出原则(FIFO t lnFirst Out)
队列是一种受限的线性结构
受限之处在于它只允许在表的前端〈front)进行删除操作,而在表的后端(rear)进行插入操作
面对无法同时处理多个请求的场景,我们通常就会使用队列,先进先出,一个一个的解决问题,保证有序
constructor() {
this.items = []
}
// 入队
push(ele) {
this.items.push(ele)
}
// 出队
pop() {
return this.items.shift()
}
// 返回队列顶元素
peek() {
return this.items[this.items.length - 1]
}
// 判断队列中元素是否为空
istempy() {
return this.items.length === 0
}
// 获取队列中元素的个数
size() {
return this.items.length
}
// 清空队列
clear() {
this.items.length = 0
}
}
js的异步队列
javascript:单线程,同一时间只能做一件事
1.new Promise的时候,excutor函数是立即执行的。(promise本身是同步的)
2.基于then或者catch存放的方法是异步的
线程是最小的执行单元,进程是最小的资源管理单元
打开一个软件的同时,就打开了一个进程
线程是从属于进程,在软件运行的过程里面(在这个进程)
JS:要设计成单线程
JS的单线程,与它的用途有关
JS的主要作用:完成与用户交互,以及DOM操作。
一个线程在一个DOM上添加了内容,另外一个线程又删除了这个DO节点
避免复杂性,从一开始,JS就是单线程,再未来也不会改变
H5,web worker标准,它是允许JS创建多个线程,但是,子线程是完全受主线程控制的,而且子线程是不可以操作DOM的。
IO的时候,(输入输出的时候),主线程不去管IO,挂起处于等待中的任务,先运行排在后面的任务,等待IO设备返回了结果,再回过头,把挂起等待的任务继续执行下去。于是所有的任务:同步任务,异步任务
同步任务指的是:在主线程上排队执行的任务,只有前一个任务执行完毕以后,才能够去执行下一个任务。
异步任务:不进入主线程,而是进入“"任务队列",只有“任务队列"通知主线程,某个异步任务可以执行了,这个任务才会进入主线程执行。
同步会阻塞后面的代码
1.最先执行的是:同步代码,执行完毕以后,立即出栈,让出主线程
2.同步代码执行完毕,立即出栈,此时主线程是出于:空闲状态
主线程去读取任务队列,队列遵循的原则是先进先出,但是,有个条件,触发条件相等,会遵循先进先出,如果触发条件不相同,则优先执行到达触发条件的代码,等待0秒不是,主线程一有空就立即执行
主线程里边维护着一个任务队列,这个任务保存的是:异步的代码
JS是单线程语言,浏览器只会分配一个主线程给JS,用来执行任务(函数)
但是一次只能执行一个任务。
事件轮询
1.所有的同步任务都是在主线程上执行,形成一个执行栈
2.主线程之外,还存在一个任务队列,只要存在异步任务,就会在任务队列里放置一个事件
3.一旦执行栈里边同步任务代码执行完毕,主线程就会去读取“任务队列",看任务队列有哪些对应的异步任务,结束等待状态,进入执行栈,开始执行
主线程不断重复上面3个步骤
主线程执行完毕以后,从事件队列中去读取任务队列的过程,我们称之为事件循环(Event Loop)
物理的存储结构:
顺序存储
用一段连续的存储单元依次存储线性表的数据元素。小朋友排队
分果果
链式存储
内存地址可以是连续的,也可以是不连续的。把数据元素存放在
任意的存储单元里,指针来存放数据元素的地址
数组
大小是固定的,一经声明就要占用整块的连续的内存空间,如果
说,声明的数组过大,系统可能没有足够的连续的内存空间用于分
配,(out of memory)内存不足。如果声明的数组国小,当不够
用时,又需要去重新一块更大的内存数据拷贝
链表
链表中的元素在内存中不必是连续的空间
链表的每个元素由一个存储元素本身的节点和一个指向下一个元
素的引用(有些语言称为指针或者链接)组成
1.插入删除:链表的性能好
链表没有大小限制,支持动态扩容,因为链表的每个结点都需要存储前驱/后驱结点的指针,内存消耗会翻倍。
2.查询修改:数组性能好
单链表
// 节点类
class Node {
constructor(element) {
this.element = element
this.next = null
}
}
// 链表
class likeList {
constructor() {
// 链表头
this.head = null
// 链表长度
this.length = 0
}
// 链表尾部追加元素
append(element) {
// 创建节点
let node = new Node(element)
// console.log(node);
if (this.length == 0) {
this.head = node
} else {
//通过head找到后面的节点
let curreet = this.head
// console.log(curreet,'curreet');
// 遍历,是否是最后一个节点,next为空是最后一个节点
while (curreet.next) {
curreet = curreet.next
// console.log(curreet, '43');
}
curreet.next = node
}
this.length += 1
}
//获取链表头
getHEad() {
return this.head
}
//toString
toString() {
let curreet = this.head
let linkString = ''
while (curreet) {
linkString += ',' + curreet.element
curreet = curreet.next
}
return linkString.slice(1)
}
insert(element, position) {
// 位置不能为负数
if (position < 0 || position > this.length) {
return false
}
let index = 0
let curreet = this.head
// 上一个节点
let privious = null
let node = new Node(element)
// 判断插入是否为第一个
if (position == 0) {
node.next = this.head
this.head = node
} else {
while (index < position) {
privious = curreet
curreet = curreet.next
index++
}
node.next = curreet
privious.next = node
}
this.length += 1
return true
}
// 获取对应位置元素
get(position) {
// 越界判断
if (position < 0 || position > this.length) {
return null
}
// 获取对应节点
let curreet = this.head
let index = 0
while (index < position) {
curreet = curreet.next
index++
}
return curreet.element
}
remove(position) {
// 越界判断
if (position < 0 || position >= this.length) {
return null
}
// 获取对应节点
let curreet = this.head
let index = 0
let privious = null
if (position == 0) {
this.head = this.head.next
}
else {
while (index < position) {
privious = curreet
curreet = curreet.next
index++
}
// console.log(1);
privious.next = curreet.next
}
this.length--
console.log(privious);
return curreet.element
}
}
const link = new likeList()
js原型链
JS是基于对象设计和开发出来的语言
面向对象有三大特点:(封装、继承于多态)
“基于对象"是使用对象,但是我们无法利用现有的对象模板去产生新的对象类型,继而去产生一个新的对象,也就是说'基于对象'是没有继承的特点。但是面向对象对象实现了继承和多态,基于现象是没有实现这些的
oop面向对象的支持两种继承方式:接口继承,实现继承
ECMAscript是无法去实现去接口继承的,JS只支持实现继承。实现继承主要依靠原型链去实现
prototype和__proto__([[proto]])
1.所有的引用类型(数组、函数、对象)可以自由的扩展属性(null除外)
2.所有的引用类型都有一个__proto__([[proto]])属性(隐式原型,它其实就是一个普通的对象)
3.所有函数都有一个prototype属性(显示原型,他也是一个普通的对象)
4.所有的引用类型,它的__proto__([[proto]])属性指向它的构造函数的prototype属性
5.当视图得到一个对象的属性时,如果这个对象的本身不存在这个属性,那么就会去它的__proto__([[proto]])属性中去寻找(去它的构造函数的prototype属性)中去寻找
function Teacher( name,hobby) {
this.name = name;
this.hobby = hobby;
}
Object.prototype.toString = function() {
console.log(“我的名字是${this.name} ,我的爱好是${this.hobby}`);
};
var tiantian = new Teacher('甜甜',‘吃饭睡觉打豆豆');
tiantian.toString();
1.tiantian的构造函数是:Teacher,所有的引用类型,它的__proto__属性指向它的构造函数的prototype属性2.Teacher.prototype是一个普通的对象,这个对象的构造函数是object
function Teacher() {
console.log(Teacher.prototype.__proto__ =-= 0bject.prototype); true
当调用这个对象本身并不存在的属性或者是方法时,它会一层层地往上找,一直找到null为止,null表示空的对象{}
function Teacher( name,hobby) {this.name = name;
this.hobby = hobby;
this.say = function ( ) {
console.log('你看看你,一天天就知道吃吃睡睡");};
}
//构造方法是空的,
function Tiantian() {}
//原型链的机制:利用原型让一个引用类型继承另外一个引用类型的属性和方法
T·iantian.prototype = new Teacher('芳芳',‘吃饭睡觉打豆豆');
var tiantian = new Tiantian();
tiantian.say();
hash(哈希表)
hash一般翻译为散列,是吧任意长度的输入通过散列算法变换成固定长度的输入,该输入的值就是散列值.这种转换是一种压缩映射.映射表达是一一对应的关系,也就是说,散列值的空间通常会小于输入空间
哈希算法不能从结果推算输入,哈希算法是不可逆的
哈希特性:
1.不可逆,哈希算法可以当做一种加密算法的存在
md5就是不可逆的,对比的方式进行校验,MD5是不可逆的
web安全:彩虹表(大的数据库),撞库
2、计算极快
20G高清电影,5K文本文件
哈希的用途
1.密码
2.文件完整的校验(文件进行了改变哈希值就不一样了)
const md5=require('md5-node')
const pass=123456
const name='你好'
console.log(md5(pass)); e10adc3949ba59abbe56e057f20f883e
console.log(md5(name));
7eca689f0d3389d9dea66ae112e5cfd7
哈希表
通常是基于数组实现的,但是相对于数组,它有很多的优势:
1、它可以提供非常快速的插入-删除-查找操作
哈希表的结构:就是数组,但是它和数组的不一样的地方是:哈希表对于索引的一种转换。这种转换我
们称之为哈希函数。|
字符串name转换成我们数组的索引
1、单词如何转换数字
编码?
ASCII码,编码的方式就可以将字符转换成数字
a =>97A=>651=>49
money: 109+111+110+101+121= 552 ,我们就把552作为money的索引存在数组中552这个结果很有可能会重
复。552可以是其他的数相加得来
重点:有一个哈希函数,通过哈希函数就会把要存储的值能够映射到一个位置,这个位置就是它的下标或者是索
引。传一个字符串进来,就会把它映射成为数字
哈希表是以键值对的形式存储的数据结构,不同点是哈希表的键:是经过哈希函数计算得出来的,关键码。每一
个关键码对应一个值,我们把这种以关键码->值的形式存储数据的数组我们称为哈希表(散列表)
最简单的哈希函数:就是把一个字符它的ASCII码加在一起,然后再去模一个数字,最后模出来的是多少就是多
少(取余数)
hash还是有可能重复
链地址法,开放地址法
开放地址法:寻找空白的位置来放置冲突的数据项
线性探测法
经过哈希化得到的是index = 5,但是在插入的时候,发现这个位置已经有了10,怎么办
index+1的位置,—点点的查找合适的位置(空位置)来放置
线性查找法:查询index=5不是对应的会index+1向下继续查找直到找到或为空的
平方探测法 (index+1)^2
二次hash法
10 11 12 13 14 15
0 1 2 3 4 5
聚集会影响到hash表的性能,无论是插入/删除/查询。
链地址法
简单的哈希表
class hashTable {
constructor() {
this.table = []
}
//hash函数
toseloaeHashCode(key) {
let hash = 0
for (let i = 0; i < key.length; i++) {
hash = key[i].charCodeAt() //计算key 的unicode码
}
//取模
return hash % 37 //取37的质数很大程度上去避免碰撞
}
//新增元素
put(key, value) {
const position = this.toseloaeHashCode(key)
this.table[position] = value
}
// 移除元素
remove(key){
this.table[this.toseloaeHashCode(key)]=undefined
}
//获取元素
get(key){
return this.table[this.toseloaeHashCode(key)]
}
}
const hashtable=new hashTable()
hashtable.put('zhang','3139803131@qq.com')
hashtable.put('bo','313131@qq.com')
hashtable.put('1','3@qq.com')
console.log(hashtable);
console.log(hashtable.get('zhang'));
树
前端中的树: dom tree+css rule = render tree ,渲染和重绘
计算机中的树:在 windows中,一切的东西都是存储在硬盘上的,windows 是通过某个硬盘-某个硬盘的分区-分
区上特定的系统文件
linux系统中一切都是存在唯一的一个虚拟文件系统,这个虚拟的文件系统是树状的一个结构,一切都以根/目录开始
数组的优缺点:数组的主要优点,查询(通过下标查询会很快),插入和删除数据的时候,效率会很低,需要有
大量的位移。
链表:插入和删除效率很高,查找效率很低,需要从头开始依次访问链表中每个数据项,直到找到
哈希表:插入/删除/查询效率都是很高的,但是空间利用率不高,底层使用的是数组,某些是单元没有被利用
的。哈希表中的元素是无序的,不能够按照特定的顺序来便来哈希表中的元素,不能够快速的找出哈希表中的
最大值或者是最小值。
每种数据都有自己特定的应用场景
linux树
树
树(Tree) :n (n≥0)个结点构成的有限集合当n=0时,称为空树
对于任一棵非空树(n>0),它具备以下性质:树中有一个称为“根(Root)”的特殊结点,用r表示;
其余节点可分为m(m>0)个互不相交的有限集T1,T2,... ,Tm,其中每个集合本身又是一棵树,
称为原来树的“子树(SubTree)
注意
子树之间不可以相交
除了根节点外,每个结点有且仅有一个父节点;一棵N个节点的树有N-1条边。
1.树(tree)是n(n>=0)个节点的有限集合,n=0称为空树
2.从逻辑上看,树具有如下特点:
(1)任何非空树种有且仅有一个结点没有前驱结点,这个结点就是根结点
(2)除根结点之外,其余的结点有且仅有一个直接的前驱结点
(3)包括根结点在内,每个结点可以有多个直接后维结点
(4)树形结构是一种具有递归特性的数据结构(任何一棵子树又满足树的概念)
(5)树形结构种的数据元素之间存在的关系的是:一对多,或者是多对一的关系
3.树的术语
a.节点的度,节点所拥有子树的个数
b.树的度,树中节点度的最大值(下图为3 B的EFG)
C.叶子(终端节点):度为0的节点(下图为3 EFGHIJ)
d.分支结点(非终端结点)︰度不为0的结点。除根结点之外的分支结点统称为:内部结点。
根结点我们又称为开始结点
e.子节点:节点的直接后驱(节点的子树的根)
f.父节点:节点的直接前驱
g.兄弟节点:同一个父节点
h.子孙节点
i.祖先节点
j.路径:这个节点自上而下的通过每条路径的每条边
k.节点的层:根节点的层是一,其余节点的层数等于父节点的层数加1
L.树的深度(高度):书中所有层数的最大值
树的存储结构
1.计算机只能是顺序或者是链式的存储,所以树这样的结构是不
能够直接存储的,要将其转换为顺序或者是链
式存储
双亲表示法:双亲表示法采用数组存储普通的树,其核心思想:顺
序存储每个结点的同时,给各个结点附加一个
记录其父结点位置的变量,存储的父结点的下标。实际操作的时
候,就是从上往下,顺序去遍历一棵树,并为
相应的域赋值。优点:可以快速的获取任意结点的父结点位置。缺
点:如果要获取某个结点的子结点,遍历了。
⒉.孩子表示法:孩子表示法,是建立多个指针域,指向它的子结
点的地址。也就是说,任何一个结点,都掌握它所有子结点的信
息。数组+链表的来实现。顺序表=>数组,从树的根结点开始,使
用数组依次存储树的各个结点,需要注意的是:孩子表示法会给各
个结点配备一个链表,用于存储各结点的孩子结点位于数组中的
位置,如果说,结点没有子结点(叶子结点),则该结点的链表为
空链表
3.孩子兄弟表示法
把普通的树,转成二叉树:从树的根结点开始,依次用链表存储各个结点的孩子结点和兄弟结点
二叉树:其实所有的树的本质都是可以使用二叉树进行模拟出来的,所以二叉树非常重要
孩子兄弟表示法
二叉树
二叉树存储:数组和链表,最适合存储方式:链表
1.如果说树中每个节点最多只能有两个子节点,这样的树我们称为二叉树.只有两个分叉的树
2.二叉树是n个节点(n>0)的有限集合.如果这个集合是空集.空二叉树
二叉树特点
1.每个节点最多有两个子树.=>二叉树不存在度大于2的节点
2.左子树和右子树是有序的,次序是不能任意颠倒的
3.即使树中某个节点只有一颗子树也要区分左子树还是右子树
特殊的二叉树
满二叉树
在一棵二叉树中,如果所有的分支结点都存在左子树和右子树,并且所有的叶子都在同一
层上,这样的二叉树就是满二叉树
满二叉树叶子只能出现在最下一层,出现在其他层,不可能达成平衡。非叶子的节点的度一定是2
完全二叉树
二叉搜索(查找,排序)树(BST)
二叉搜索树其实就是普通的二叉树上加了一些限制
二叉树对于节点是没有任何限制,但是二叉搜索树中在插入节点的有一页特殊要求
1.非空左子树的所有的键值都小于其根节点的键值
2.非空右子树的所有的键值都大于其根节点的键值
3.左右子树本身都是二叉搜索树
二叉搜索树的特点:相对较小的值总是保存左子节点上,相对较大的值总是1保存在右子节点上
二叉搜索树的遍历
不重复的访问二叉树中所有的节点
先序遍历 中序遍历 后序遍历
先序遍历
1.访问根节点
2.先序遍历左节点
3.先序遍历其右节点
中序遍历
先递归遍历其左子树,
从最后一个左子树开始存入数组,然后回溯遍历双亲结点,
在是右子树。递归循环。
后序遍历
1、后序遍历其左子树
2、后序遍历其右子树
3、访问根结点
二叉搜索树删除
1、没有子树
2、仅有一棵子树
3、有两棵子树
二叉搜索树的优点:
二叉搜索树的优点:作为数据存储的结构有重要的意义,可以快速的找到给定的关键字的数
据项,并且可以快速的插入和删除数据
二叉搜索树的缺点:具有局限性。同样的数据,可以对应不同的二叉搜索树
比较好的二叉搜索树的结构:左右分布均匀的,但是我们插入连续的数据的时候,会导致数
据分布不均匀
我们就把这个分布不均匀的树->非平衡树
// 节点
class Node {
constructor(value) {
this.value = value
this.left = null
this.right = null
}
}
// 相对小的值存左边相对较大的值存右边
// 二叉搜索树
class BinarySearchTree {
constructor() {
this.root = null
}
// 插入值比较
insrtNode(node, newNode) {
// 如果是右边
if (newNode.value > node.value) {
// 原来的有节点有没有节点
if (node.right == null) {
node.right = newNode
} else {
this.insrtNode(node.right, newNode)
}
} else if (newNode.value < node.value) {
// 左边
if (node.left == null) {
node.left = newNode
} else {
this.insrtNode(node.left, newNode)
}
}
}
// 插入值
insert(value) {
let newNode = new Node(value)
// 空树
if (this.root == null) {
this.root = newNode
} else {
this.insrtNode(this.root, newNode)
}
}
// 先序遍历
preOrderTraversal(cd) {
this.preOrder(this.root, cd)
}
preOrder(node, cd) {
if (node == null) {
return
}
cd(node.value)
this.preOrder(node.left, cd)
this.preOrder(node.right, cd)
}
//中序遍历
zhongxubianli(cd) {
this.zhongpreOrder(this.root, cd)
}
zhongpreOrder(root, cd) {
if (root == null) return
this.zhongpreOrder(root.left, cd)
cd(root.value)
this.zhongpreOrder(root.right, cd)
}
// 后序遍历
houxubianli(cd) {
this.houpreOrder(this.root, cd)
}
houpreOrder(node, cd) {
if (node == null) return
this.houpreOrder(node.left, cd)
this.houpreOrder(node.right, cd)
cd(node.value)
}
// 最大值
max() {
let node = this.root
while (node.right !== null) {
node = node.right
}
return node.value
}
// 最小值
min() {
let node = this.root
while (node.left !== null) {
node = node.left
}
return node.value
}
// 寻找特定的值
search(val) {
let node = this.root
while (node !== null) {
if (node.value > val) {
node = node.left
} else if (node.value < val) {
node = node.right
}else {
return true
}
}
return '没有找到'
}
}
const node = new BinarySearchTree()
node.insert(11)
node.insert(22)
node.insert(3)
node.insert(55)
node.insert(5)
node.insert(100)
node.insert(10)
node.insert(1)
console.dir(node);
const arr = []
const cd = (val) => {
arr.push(val)
}
//先序遍历
node.preOrderTraversal(cd)
//中序遍历
node.zhongxubianli(cd)
//后序遍历
node.houxubianli(cd)
console.log(arr);
console.log(node.max());
console.log(node.min());
console.log(node.search(101));
二叉平衡树: AVL 红黑树(R-B tree)
AVL树相对于红黑树,它的插入/部除操作效率都不高。
R-B tree红黑树是一种自平衡的二叉搜索树,以前叫做平衡二叉B树
出了规定左节点小于根节点,右节点大于根节点以外,还规定左子树和右子树的高度相差的
高度不得超过1
我们需要建立一颗尽可能矮的树
平衡因子
平衡因子:左子树的高度减去其右子树的高度
所以,平衡二叉树中,各个结点的平衡因子的绝对值小于等于1。【-1,0,1]。可以满足我们的二叉平衡树的需求,平衡二叉树是一颗二叉搜索树,只不过平衡树比较爱而已
控制平衡因子:如果平衡因子的绝对值超过了1,那我们称之为失衡,节点需要随时添加,随时删除
红黑树增加的一些特性(平衡二叉树)
1.节点是红色或则是黑色(节点上有一个color属性)
2.根节点是黑色
3.叶子节点都是黑色,且为null(null节点)
4.链接红色节点的两个子节点都是黑色,红色节点的父节点都是黑色,红色节点的子节点
都是黑色
5.从任意结点出发,到其每个叶子结点的路径中包含相同数据的黑色结点
从根节点到叶子结点的最长路径不大于最短路径的2倍
红黑树插入数据的时候.会先去遍历数据应该插入到那个位置,插入的数据一定是红色
平衡树左旋
left( node)
let tmp = node.right;
node.right = tmp.left;
tmp.left = node;
tmp.color = node.color;
node.colder = RED;
}
图(一中数据结构)(Graph)
集合只有同属于一个集合;
线性结构存在一对一的关系,
树形结构一对多的关系,
图形结构,多对多的关系。
导航的最优路径:耗时最少,路程最短的路径,推荐一个方便最快捷的路线。其实就把经过的地方看作图上的一个一个的点,从起点出发,与另外的一个点或者其他的点产生了关联。
图的概念
1.集合:同属于一个集合,
线性结构存在一对一的关系;
树形结构:一对多;
图形结构:多对多
图:是一种比树更为复杂的数据结构。树结点之间是一对多的关系,并且存在着一个父与子
的层级划分。多对多的关系,并且的,所有的顶点都是平等的,无所谓谁是父亲,谁是儿
子。在图中,最基本的单元是顶点(vertex) ;顶点相当于书中的结点,顶点之间的关系:
被称为边(edge)|
图的分类
按照连接的两个顶点的边的不同,可以把图分为以下几种:
1.无向图:边没有方向的图称为无向图,边的作用仅仅是连接两个顶点,没有其他含义
2.有向图:边不仅连接两个顶点,并且具有方向性,可能是单向也可能是双向的
3.带权图:边可以带权重
图的术语
1.相邻顶点:当两个顶点通过一条边相连时,我们称这两个顶点是相连的,并且是依附于这两
个顶点的
2.度:某个顶点的度:是依附于这个顶点的边的个数
树结点的度:节点所拥有的的子树的个数
树的度:树中结点度最大值
3.子图:一幅图中,所有边的子集组成的图,包含这些边的依附的顶点
4.路径:是由边顺序链接的一系列的顶点组成
5.环:是至少含有一条边且终点和起点相同的路径
6.连通图:如果图中,任意一个顶点都存在一条路径到达另外一个顶点,那么这幅图我们就称
之为连通图
7.连通子图:一个非连通图由若干个连通的部分组成,每一个连通的部分就可以称为:该图的
连通子图
无向图有向图
带权图
联通子图
连通图,非连通图
自环,平行边
子图
图的存储结构
顺序储存,链式存储
1. 线性表:它仅有的关系就是线性关系
2. 树形结构:有清晰的层次的结构
3. 图形结构:集合中的每一个元素都有可能有关系,我们要弄清楚图的存储结构,弄清楚以后我们用代码去实现图的时候,就没有那么困难了。
图的存储
图是由顶点和边构成。
所以在图里边:要存储的图形结构的信息,
无非就是存储图的顶点和图的边。
顶点可以直接用数组去存储
1,2,3,4=>[1,2,3,4]
A,B,C,D =>[A,B,C,D]
边存储起来就比较麻烦了
存储结构:
1.邻接矩阵
矩阵是一个按照长方阵列排列的负数或者实数集合。
N*M数据的集合(九宫格)
去除表格线的九宫格就是矩阵的样式。矩阵是由行和列组成的一组数表。
邻接矩阵让每一个结点和证书相关联
用1表示顶点于顶点有直接的关系,用O表示没有连接
优点:表示非常明确
缺点:浪费大量内存,存储太多0
2.邻接表
邻接表:由图中的每个顶点以及和顶点相邻的顶点列表组成。数组/链表/字典
邻接矩阵表示
邻接表表示
解析:A与B、C、D连接,B与A、E、F连接。
图的遍历
1.遍历:从某个结点出发,按照一定的搜索路线,依次访问数据结构中全部结点,而且每个结点访问一次。
2.二叉树中:树的遍历,从根结点出发,按照一定的访问规则,依次访问树的每个结点信息
1.先序遍历
2.中序遍历
3.后序遍历
4.层级遍历
3.图的遍历
a.广度优先遍历(BFS)
优先横向遍历图,广度优先的思想,从图中的某个顶点v出发,在访问了v以后,依
次去访问v的各个未曾访问过的邻结点,然后分别从这些邻接点出发,依次访问它们
的邻结点。图中所有的顶点都要被访问到
b.深度优先(DFS)有一个递归的概念
先遍历图的纵向
4.图遍历的思路
1.每一个顶点都有3种状态
a.未发现(没有发现这个顶点)
b.已经发现(发现其他的顶点连接到这里,但是没有去查找这个顶点的全部连接的顶点)
c.已经探索(已经发现这个顶点连接的全部顶点)
2.记录顶点是否被访问过,使用三种颜色来反映他们的状态
1.白色(未发现)
2.灰色(已经发现)
3.黑色(已经探索)
3.广度优先的遍历过程
1.发现未发现顶点后,存放队列中,等待查找,并且将这些顶点标记未已发现
2.在队列中拿出已经发现的顶点,开始探索全部顶点,并且要跳过已经探索的顶点
3.遍历完这个顶点以后,将这个顶点标志为已经探索
4.循环在队列中探索下一个顶点
4.深度优先
广度优先算法我们使用的是队列,深度优先的原理:使用递归
1.从某一顶点开始查找,并且将这个定边标记为已经发现(灰色)
2.从这个顶点开始探索其他的全部的顶点,并且跳过已经发现的顶点
3.遍历完这个顶点以后,将这个顶点标记为已探索(黑色)
4.递归返回,继续探索下一个路径的最深顶点
广度优先遍历
图没有横向和纵向的概念
深度优先
广度优先
class Stack {
constructor() {
this.items = []
}
// 添加元素到栈
push(ele) {
this.items.push(ele)
}
// 出栈
dequeue() {
return this.items.shift()
}
// 返回顶端元素
front() {
return this.items[0]
}
// 返回w端元素
front() {
return this.items[this.items.length - 1]
}
// 判断栈中元素是否为空
istempy() {
return this.items.length === 0
}
// 获取栈中元素的个数
size() {
return this.items.length
}
// 清空栈
clear() {
this.items.length = 0
}
}
// 存储顶点和边
class Graph {
constructor() {
this.vertices = []
this.edgeList = []
}
addVerTices(v) {
//添加顶点
this.vertices.push(v)
//添加边
this.edgeList[v] = []
}
addEdge(a, b) {
this.edgeList[a].push(b)
this.edgeList[b].push(a)
}
toString() {
let res = ''
for (let i = 0; i < this.vertices.length; i++) {
let verText = this.vertices[i]
res += `${verText}=>`
let edge = this.edgeList[verText]
for (let j = 0; j < edge.length; j++) {
res += edge[j]
}
res += '\n'
}
return res
}
// 初始化颜色
initcolors() {
let colors = {}
for (let i = 0; i < this.vertices.length; i++) {
colors[this.vertices[i]] = 'white'
}
return colors
}
//广度优先
bfs(v, calback) {
// 将全部顶点设置成白色
let color = this.initcolors()
/*
color={
A:'white'
}
*/
let que = new Stack()
que.push(v)
while (!que.istempy()) {
//从A开始遍历
const qVertext = que.dequeue()
const edge = this.edgeList[qVertext]
for (let i = 0; i < edge.length; i++) {
//当前顶点
const e = edge[i]
if (color[e] == 'white') {
// 未发现顶点全部入列
color[e] = 'gray'
que.push(e)
}
}
color[qVertext] = 'black'
if (calback) {
calback(qVertext)
}
}
}
}
const graph = new Graph()
graph.addVerTices('A')
graph.addVerTices('B')
graph.addVerTices('C')
graph.addVerTices('D')
graph.addVerTices('E')
graph.addVerTices('F')
graph.addEdge('A', 'B')
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('B', 'E')
graph.addEdge('B', 'F')
graph.addEdge('D', 'C')
console.log('广度优先');
graph.bfs('A', e => {
console.log(e);
})
深度优先
class Stack {
constructor() {
this.items = []
}
// 添加元素到栈
push(ele) {
this.items.push(ele)
}
// 出栈
dequeue() {
return this.items.shift()
}
// 返回顶端元素
front() {
return this.items[0]
}
// 返回w端元素
front() {
return this.items[this.items.length - 1]
}
// 判断栈中元素是否为空
istempy() {
return this.items.length === 0
}
// 获取栈中元素的个数
size() {
return this.items.length
}
// 清空栈
clear() {
this.items.length = 0
}
}
// 存储顶点和边
class Graph {
constructor() {
this.vertices = []
this.edgeList = []
}
addVerTices(v) {
//添加顶点
this.vertices.push(v)
//添加边
this.edgeList[v] = []
}
addEdge(a, b) {
this.edgeList[a].push(b)
this.edgeList[b].push(a)
}
toString() {
let res = ''
for (let i = 0; i < this.vertices.length; i++) {
let verText = this.vertices[i]
res += `${verText}=>`
let edge = this.edgeList[verText]
for (let j = 0; j < edge.length; j++) {
res += edge[j]
}
res += '\n'
}
return res
}
// 初始化颜色
initcolors() {
let colors = {}
for (let i = 0; i < this.vertices.length; i++) {
colors[this.vertices[i]] = 'white'
}
return colors
}
//广度优先
bfs(v, calback) {
// 将全部顶点设置成白色
let color = this.initcolors()
/*
color={
A:'white'
}
*/
let que = new Stack()
que.push(v)
while (!que.istempy()) {
//从A开始遍历
const qVertext = que.dequeue()
const edge = this.edgeList[qVertext]
for (let i = 0; i < edge.length; i++) {
//当前顶点
const e = edge[i]
if (color[e] == 'white') {
// 未发现顶点全部入列
color[e] = 'gray'
que.push(e)
}
}
color[qVertext] = 'black'
if (calback) {
calback(qVertext)
}
}
}
//深度优先
dfs(v, callback) {
let color = this.initcolors()
this.dfsVislit(v,callback,color)
}
//递归实现深度优先
dfsVislit(v, callback, color) {
color[v] = 'gray'
if (callback) {
callback(v)
}
let edge=this.edgeList[v]
// 遍历
for(let i=0;i<edge.length;i++){
let e=edge[i]
if(color[e]=='white'){
this.dfsVislit(e,callback,color)
}
}
color[v]='black'
}
}
const graph = new Graph()
graph.addVerTices('A')
graph.addVerTices('B')
graph.addVerTices('C')
graph.addVerTices('D')
graph.addVerTices('E')
graph.addVerTices('F')
graph.addEdge('A', 'B')
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('B', 'E')
graph.addEdge('B', 'F')
graph.addEdge('D', 'C')
graph.dfs('A', e => {
console.log(e);
})
最短路径
1.路径:由边顺序连接的顶点组成的
寻找最短路径,所谓路径指的是:如果从图中的某一个顶点(起点,圆点)到达另外一个顶点(终点),路径不可能只有一条,如何找到一条路径使得沿这个路径边上的权值总和(路径长度)
2.回溯点
是离上一个顶点最近的点,A的回溯点是null,B的回溯点是:A,E的回溯朔点是B
回溯路径(所有的回溯点组成,回溯路径)
3.两种比较常见的求最短路径的算法
1.迪杰斯特拉算法,是贪心算法的一个应用,用来解决单源点到其余顶点的最短路径的问题
2.Floyd算法,经典的动态规划算法。
class Stack {
constructor() {
this.items = []
}
// 添加元素到栈
push(ele) {
this.items.push(ele)
}
// 出栈
dequeue() {
return this.items.shift()
}
// 返回顶端元素
front() {
return this.items[0]
}
// 返回w端元素
front() {
return this.items[this.items.length - 1]
}
// 判断栈中元素是否为空
istempy() {
return this.items.length === 0
}
// 获取栈中元素的个数
size() {
return this.items.length
}
// 清空栈
clear() {
this.items.length = 0
}
}
// 存储顶点和边
class Graph {
constructor() {
this.vertices = []
this.edgeList = []
}
addVerTices(v) {
//添加顶点
this.vertices.push(v)
//添加边
this.edgeList[v] = []
}
addEdge(a, b) {
this.edgeList[a].push(b)
this.edgeList[b].push(a)
}
toString() {
let res = ''
for (let i = 0; i < this.vertices.length; i++) {
let verText = this.vertices[i]
res += `${verText}=>`
let edge = this.edgeList[verText]
for (let j = 0; j < edge.length; j++) {
res += edge[j]
}
res += '\n'
}
return res
}
// 初始化颜色
initcolors() {
let colors = {}
for (let i = 0; i < this.vertices.length; i++) {
colors[this.vertices[i]] = 'white'
}
return colors
}
//广度优先
bfs(v, calback) {
// 将全部顶点设置成白色
let color = this.initcolors()
/*
color={
A:'white'
}
*/
let que = new Stack()
que.push(v)
//将所有回溯点设为null
let prev = {}
for (let i = 0; i < this.vertices.length; i++) {
prev[this.vertices[i]] = null
}
while (!que.istempy()) {
//从A开始遍历
const qVertext = que.dequeue()
const edge = this.edgeList[qVertext]
for (let i = 0; i < edge.length; i++) {
//当前顶点
const e = edge[i]
if (color[e] == 'white') {
// 未发现顶点全部入列
color[e] = 'gray'
prev[e]=qVertext
que.push(e)
}
}
color[qVertext] = 'black'
if (calback) {
calback(qVertext)
}
}
return prev
}
//深度优先
dfs(v, callback) {
let color = this.initcolors()
this.dfsVislit(v, callback, color)
}
//递归实现深度优先
dfsVislit(v, callback, color) {
color[v] = 'gray'
if (callback) {
callback(v)
}
let edge = this.edgeList[v]
// 遍历
for (let i = 0; i < edge.length; i++) {
let e = edge[i]
if (color[e] == 'white') {
this.dfsVislit(e, callback, color)
}
}
color[v] = 'black'
}
}
const graph = new Graph()
graph.addVerTices('A')
graph.addVerTices('B')
graph.addVerTices('C')
graph.addVerTices('D')
graph.addVerTices('E')
graph.addVerTices('F')
graph.addEdge('A', 'B')
graph.addEdge('A', 'C')
graph.addEdge('A', 'D')
graph.addEdge('B', 'E')
graph.addEdge('B', 'F')
graph.addEdge('D', 'C')
const prev=graph.bfs('A')
graph.addEdge('D', 'F')
// 测试最短路径
const shortPath = (from, to) => {
let vertext = to //当前顶点的回溯点
let stack=new Stack()
while(vertext!==from){
stack.push(vertext)
vertext=prev[vertext]
}
stack.push(vertext)
let path=''
while(!stack.istempy()){
path+=`${stack.dequeue()}=>`
}
path=path.slice(0,path.length-2)
return path
}
const path=shortPath('A','D')
console.log(path);