第四章 栈
4.1栈数据结构
一种遵从后进先出 ( LIFO )原则的有序集合。栈拥有栈顶和栈底,新元素都靠近栈顶,旧元素都接近栈底
用处:被用在编程语言的编译器和内存中保存变量,方法调用。也被用于浏览器历史记录存储
4.1.1 创建一个基于数组的栈
class Stack{
constructor(){
this.items=[] //需要一种数据结构来保存栈里的元素
}
}
为栈添加几种方法:
1.push: 添加元素到栈顶
2.pop: 移除栈顶的元素 , 同时返回被移除的元素
3.peek: 返回栈顶的元素, 不对栈进行任何修改
4.isEmpty: 如果栈没有任何元素 返回true , 否则返回false
5.clear: 移除栈里的所有元素
6.size: 返回栈里的元素个数。类似于数组的length属性
1.向栈中添加元素
push(ele){
this.items.push(ele)
}
2.向栈中移除元素
pop(){
return this.items.pop()
}
3.查看栈顶元素
peek(){
return this.items[this.items.length-1]
}
4.检查栈是否为空
isEmpty(){
return this.items.length===0 //对于集合,最好用size代替length
}
5.移除栈里的元素
clear(){
this.items=[]
}
4.1.2 使用Stack类
1.初始化Stack类,验证栈是否为空
const stack=new Stack()
stack.isEmpty()
stack.push(5)
stack.peek() //查看栈顶元素 ,输出5
stack.size() // 1
4.2 基于JavaScript对象的Stack类
在使用数组时,大部分方法的时间复杂度是O(n)
O(n) :我们需要迭代整个数组直到找到要找的那个元素,在最坏的情况下需要迭代数组的所有位置,其中的 n 代表数组的长度。
如果 数组有更多的元素的话,所需的时间会更长。
( 1 ) 首先声明一个Stack类
class Stack{
constructor(){
this.count=0; //定义count属性来帮我们记录栈的大小
this.items={};
}
//1.向栈中插入元素
push(ele){
this.item[this.count]=ele;
this.count++
}
//2.栈的大小
size(){
return this.count
}
//3.是否为空
isEmpty(){
return this.count===0
}
}
const stack=new Stack();
stack.push(1)
//在Stack内部 items包含的值和count属性
items={0:1};count:1
- 从栈中弹出元素 ( 由于栈后进先出的原则,需要将最后一位移除 )
pop(){
if(this.isEmpty()){
return undefined
}
this.count--
const res = this.items[this.count] //获取最后一位,即栈顶的值
delete this.items[this.count] //从对象中删除最后一位
return res
}
- 查看栈顶的值并将栈清空
peek(){
if(this.isEmpty()){
return undefined
}
return this.items[this.count-1]
}
clear(){
this.items={}
this.count=0
}
//也可以遵循LIFO原则
while(!this.isEmpty()){
this.pop();
}
4.3 创建toString方法
toString(){
if(this.isEmpty()){
return ''
}
let objString=this.items[0]
for(var i=1;i<this.items.length;i++){
objString=`${objString}${this.items[i]}`
}
return objString
}
除了String方法,其他方法的复杂度均为O(1),代表我们可以直接找到目标元素对其进行操作
4.4 保护数据结构内部元素
由于声明的stack类items 和 count属性并没有得到保护
const stack = new Stack()
consoloe.log(Objcet.kesy(stack)) //打印出 items 和 count属性
consoloe.log(stack.items) //可以直接访问到
//尽管我们以基于原型的类能节省内存空间并在扩展方面优于基于函数的类,但这种方式并不能声明 私有属性(变量) 或 方法
4.5 下划线命名约定
在javaScript 使用下划线来约定一个属性为 私有属性 (只是一种约定,并不能保护数据)
class stack{
constructor(){
this._items={}
this._count=0
}
}
4.6 使用weakMap实现类( 键:对象 , 值:任意类型 )
weakMap可以确保属性是私有的,其可以存储键值对,其中键是对象,值可以是任意类型
如果用weakMap来存储items属性: Stack类是这样的
const items=new WeakMap() //声明一个WeakMap类型的变量items
class Stack{
constructor(){
items.set(this,[]) //以this(Stack类)为键 , 把代表栈的数组存入items
}
push(ele){
const s=items.get(this) //从weakMap中取值 , 即以Stack为键从items中取值
s.push(ele);
}
pop(){
const s=items.get(this)
const r=s.pop()
return s
}
}
4.7 用栈解决问题
将 10进制 转换为 2进制
function fn(number){
const stack=new Stack()
let num=number
let rem; //进制数 0/1
let str=''
while(num>0){
rem=Math.floor(num%2); //获取二进制数
stack.push(rem); //将二进制数放进数组
num=Math.floor(num/2)
}
while(!stack.isEmpty()){
str += stack.pop().toString()
}
return str
}
第五章 队列和双端队列
5.1 队列数据结构
队列:先进先出 ,例: 排队
5.1.1创建队列
创建一个类来表示队列
class Queue{
this.count=0; //控制队列的大小
this.lowestCount=0 //追踪第一个元素
this.items={} //存储数据
}
队列几种方法:
1.enqueue: 向队列尾部添加一个(或多个)新的项
2.dequeue: 移除队列的第一项,并返回被移除的元素
3.peek: 返回队列中第一个元素---最先被添加,最先被移除的元素。队列不做任何变动。
4.isEmpty: 如果队列中不包含任何元素,返回true
6.size: 返回队列包含的元素个数。类似于数组的length属性
1.向队列添加元素
enqueue(res){
this.items[this.count]=res
this.count++
}
2.从队列中移除元素
dequeue(){
let res=this.items[this.lowestCount]
delete this.items[this.lowestCount]
this.lowestCount++
return res
}
例如:
items={
0:8,
1:5,
}
count=2;lowestCount=0
//当执行了dequeue后
items={
1:5
}
count=1;lowestCount=1
3.查看队列头元素 (由于队列先进先出,则队列元素为索引为0的元素)
peek(){
if(this.isEmpty()){
return undefined
}
let res=this.items[this.lowestCount]
return res
}
4.检查队列是否为空并获取它的长度
isEmpty(){
return this.count-this.lowestCount===0
}
size(){
return this.count-this.lowetCount
}
- 清空队列
clear(){
this.items={}
this.count=0
this.lowestCount=0
}
6.toString方法
toString(){
if(this.isEmpty()){
return ''
}
let objStr=this.items[this.lowestCount]
for(var i=1;i<this.items.length;i++){
objStr=`${objStr}${this.items[i]}`
}
return objStr
}
5.2 双端队列数据结构
一种允许同时从前端和后端添加和删除元素的特殊队列( 遵守了栈的后进先出 队列先进先出的)
常见应用 : 存储一系列的撤销操作
5.2.1 创建Deque类
class Deque{
constructor{
this.count=0
this.lowestCount=0
this.items={}
}
}
几种方法:
1.addFront : 在双端队列前端添加新的元素
2.addBack : 在双端队列后端添加新的元素
3.removeFront : 在双端队列前端移除元素
4.removeBack : 在双端队列后端移除元素
5.peekFront : 返回前端第一个元素
6.peekBack : 返回后端第一个元素
1.向前端添加元素
addFront(ele){
if(this.isEmpty()){ //如果双端队列为空,可以执行addBack方法将元素添加到后端,也是队列前端
this.addBack(ele)
}
else if(this.lowestCount>0){ //当有元素已经被队列移除,也就是说lowestCount >=1 ,若要将元素加入前端 则需要lowestCount-1
this.lowestCount--
this.items[this.lowestCount]=ele
}
else{
for(var i=this.count;i>0;i--){
this.items[i]=this.items[i-1]
}
this.count++
this.lowestCount=0 //因为lowestCount跟踪的是以前的第一位,当增加了新元素后,lowestCount的值会变为1,由于其需要追踪第一个元 素,所以将它等于0
this.items[0]=ele
}
}
5.3使用队列和双端队列来解决问题
5.3.1 击鼓传花游戏(循环队列)
击鼓传花,将花传递给旁边人,传花停止,花在谁手上谁出局,直到剩最后一名。
class Queue{
constructor(){
this.items={}
this.count=0
this.lowestCount=0
}
//添加元素
enqueue(ele){
this.items[this.count]=ele
this.count++
}
//删除元素
dequeue(){
if(this.isEmpty()){
return undefined
}
let res=this.items[this.lowestCount]
delete this.items[this.lowestCount]
this.lowestCount++
return res
}
//判空
isEmpty(){
return this.count===0
}
//查看队列头元素
peek(){
if(this.isEmpty()){
return undefined
}
return this.items[this.lowestCount]
}
//查看队列长度
size(){
return this.count-this.lowestCount
}
}
var arr=['玩家1','玩家2','玩家3','玩家4','玩家5']
var base=6
var winner;
function hotPotao(arr,base){
var queue=new Queue()
var lowerList=[]
for(let i=0;i<arr.length;i++){
queue.enqueue(arr[i])
}
while(queue.size()>1){
for(let i=0;i<base;i++){
queue.enqueue(queue.dequeue()) //将前端元素放在后端
}
queue.dequeue() //把最后排在队列首位(即花在其位置的元素)删除
}
winner = queue.peek()
console.log(winner);
return winner
}
hotPotao(arr,base)
5.3.2 回文检查器(双端队列)
回文即是正反都能读的单次,例 : madam ,racecar
class Deque{ //定义一个双端队列的类
constructor(){
this.items={}
this.count=0
this.lowestCount=0
}
//判空
isEmpty(){
return this.count===0
}
//查看队列长度
size(){
return this.count-this.lowestCount
}
//向前端添加元素
addFront(ele){
if(this.isEmpty()){
this.items[this.count]=ele
this.count++
}
else if(this.lowestCount>0){ //队列删除过元素时
this.lowestCount--
this.items[this.lowestCount]=ele
}
else{
for(var i=this.count;i>0;i--){
this.items[i]=this.items[i-1]
}
this.count++
this.lowestCount=0;
this.items[0]=ele
}
}
//向后端添加元素
addBack(ele){
this.items[this.count]=ele
this.count++
}
//从前端移除元素
removeFront(){
let res=this.items[this.lowestCount]
delete this.items[this.lowestCount]
this.lowestCount++
return res
}
//从后端移除元素
removeBack(){
let res=this.items[this.count-1]
this.count--
return res
}
}
var str="aabaa";
var palindromeCheck = function(str){
var deque=new Deque()
var arr=[]
for(var i=0;i<str.split('').length;i++){
deque.addBack(str.split('')[i])
}
let isEqual=true //判断是否为回文
let firstStr , lastStr
while (deque.size()>1 && isEqual) {
firstStr = deque.removeFront()
lastStr =deque.removeBack()
if(firstStr!==lastStr){
isEqual=false
}
}
return isEqual
}
第六章 链表
6.1 链表数据结构
数组: 虽然作为最常用的数据存储结构,但有其缺点:1.数组的大小是固定的 2.在数组插入移除元素成本高,需要移动元素
链表: 一种动态的数据结构 , 它存储有序的元素集合,但元素在链表中的位置并不是连续放置的。每个元素有一个存储元素本身的节点 和 指向下一个元素的引用(称为指针或链接)组成
使用链表 :
- 添加或移除元素的时候不需要移动元素
- 不同与数组能直接访问任何位置元素 ,若要访问链表中的一个元素,需要从起点 (表头) 开始迭代链表直到找到所需元素
链表例子:
1.类似康加舞队,每个人就是一个元素,手就是链向下一个人的指针。
6.1.1 创建一个链表
import {defaultEquals} from '../util'
class LinkedList{
constructor( equalsFn=defaultEquals){
this.count=0 //定义链表中存储元素的数量
this.head=undefined //数据结构是动态的,将第一个元素的引用保存下来
this.equalsFn=equalsFn //比较链表中的元素是否相等,需要传入一个内部调用函数
}
}
//为表示链表中的元素,还需要一个助手类 Node, 它表示我们想要添加到链表中的项
class Node{
constructor(element){
this.element=element
this.next=undefined //node实例被创建时,它的next指针总是undefined
}
}
//链表的方法:
1.push(ele) : 向链表尾部添加一个新元素
2.insert(ele,position) : 向链表的指定位置插入一个新元素
3.getElementAt(index) : 返回链表中特定位置的元素。如果链表中不存在这样的元素,返回Undefined
4.remove(ele) : 从链表中移除一个元素
5.indexOf(ele) : 返回元素在链表中的索引,如果链表中没有该元素返回-1
6.removeAt(position) : 从链表特定位置移除一个元素
7.isEmpty() : 判空
8.size() : 返回元素个数
9.toString() : 返回表示整个链表的字符串
// defaultEquals函数的定义如下:
export function defaultEquals(a,b){
return a===b
}
1.向链表尾部添加元素
class Node{
constructor(element){
this.element=element
this.next=undefined
}
}
push(ele){
const node =new Node(ele) //先将传入的数据,创建Node项
let current //用来存储链表中元素项的变量
if(this.head==null){ //判断链表是否为空,若为空head为传入元素的引用
this.head=node
}else{
current=this.head //获取到第一个节点
while(current.next!=null){ //如果下一个节点不为空,则将下一个节点赋值给current,当next为null时,说明找到最后一项
current=current.next
}
current.next=node //将新元素赋给最后一项元素的next,建立连接
}
this.count++
}
2 .从链表中移除元素
class Node{
constructor(ele){
this.ele=ele
this.next=undefined
}
}
removeAt(index){ //需要找到对应位置的元素current,需要将上一级的next指向current.next,再执行current=current.next
if(index>=0&&index<this.count){ //进行边界判断
let current = this.head //this.head就是实例化的一个node类
if(index==0){ //移除第一项
this.head=current.next
}else{
let previous
for(var i=0;i<index;i++){
previouse=current //获取查找元素的上一项
current=current.next //遍历查找到对应index的元素
}
previous.next=current.next
}
this.count--
return current.element
}
return undefined
}
//方法重构
removeAt(index){
if(index>=0&&index<this.count){
let current = this.head
if(index==0){
this.head=current.next
}else{
let pervious = getElementAt(index-1) //获取到查找元素的上一项
let current = getElementAt(index)
previous.next=current.next
}
this.count--
}
return undefined
}
3.迭代链表获取元素
getElementAt(index){
if(index>=0&&index<this.count){
let current = this.head
for(var i=0;i<index;i++){
current=current.next
}
return current
}
return undefined
}
4.在任意位置插入元素
insert(ele,index){
if(index>=0&&index<this.count){
let current = this.head
var node=new Node(ele)
if(index===0){
this.head=node
node.next=current
}
else{
let previous=this.getElementAt(index-1)
let current=previous.next
previous.next=node
node.next=current
}
this.count++
return true
}
return false
}
5.indexof方法
indexof(ele){
let current = this.head
for(let i=0;i<this.count&& current !=null;i++){
if(this.equalsFn(element,curent.element)){
return i
}
current=current.next
}
return -1
}
6.从链表中移除元素
remove(ele){
let id = this.indexof(ele)
return this.removeAt(id)
}
7.isEmpty , size , getHead方法
isEmpty(){
return this.count===0
}
size(){
return this.count
}
getHead(){
return this.head
}
8.toString方法
toString(){
if(this.head==null){
return ''
}
let objStr=`${this.head.element}`
let current=this.head
for(var i=1;i<this.count-1;i++){
objStr+=`${current.element}`
current=current.nexr
}
return objStr
}
6.2 双向链表
双向链表 与 单向链表 的区别:
链表中每个元素只有指向下一个元素的指针,双向链表中,链接时双向的一个指向上一个元素,一个指向下一个元素
单向链表 缺点:如果迭代时错过了要找的元素,就要回到起点,重新开始迭代
6.2.1 创建一个双向链表
//双向链表的类
class DoubleLinkedList extends LinkedList{
constructor(equalsFn=defaultEquals){
super(equalsFn) //因为要调用LinkedList的构造函数,需要初始化
}
this.tail=undefined; //对链表中最后一个元素的引用
}
//节点类
class DoublyNode extends Node{
constructor(element,next,prev){
super(element,next)
this.prev=prev //表示指向element的上一个节点
}
}
6.2.2 在任意位置插入元素
在双向链表中插入元素,与单向链表只需要控制一个next指针不同,双向链表需要同时控制next和 prev 这两个指针
insert(ele,index){
if(index>=0&&index<=this.count){
const node = new DoublyNode(ele)
let current = this.head
if(index===0){ //向索引为0的位置插入元素
if(this.head==null){ //链表为空
this.head=node
this.tail=node //最后一个元素就是第一个元素
}else{
node.next=this.head //设置添加元素指向下一元素的指针
this.head.prev=node //设置头部元素指向上一元素的指针
this.head=node
}
}
else if(index==this.count){ //在最后一项插入元素
this.tail.next=node
node.prev=this.tail
this.tail=node
}
else{
const prevEl= this.getElementAt[index-1] //获取插入位置的前一项
const current=prevEL.next
prevEl.next=node
node.prev=prevEl
node.next=current
}
this.count++
return true
}
return false
}
6.2.3 从任意位置移除元素
removeAt(index){
if(index>=0&&index<this.count){
const node = getElementAt(index)
let current=this.head
if(index==0){ //删除第一个元素
this.head=current.next //将头部赋值为第二项
if(this.count===1){ //如果只有一项
this.tail=undefined
}else{
this.head.prev=undefined //第一项的prev都为undefined
}
}
else if(index==this.count-1){
current=this.tail
this.tail=current.prev
this.tail.next=undefined
}else{
current=this.getElementAt(index)
const previous=current.prev
previous.next=current.next
current.next.prve=previous
}
this.count--
retuen current.element
}
return undefined
}
6.3 循环链表
最后一个元素指向下一个元素的指针(tail.next)不是引用undefined,而是指向第一个元素(head)
双向循环链表:有指向head元素的tail.next,有指向tail元素的head.prev
6.3.1 创建循环链表
class CirculaLinkedList extends LindList{
counstructor(equalsFn=defaultEquals){
super(equalsFn)
}
}
6.3.2 在任意位置插入新元素
insert(ele,index){
if(index>=0&&index<this.count-1){
let node =new Node(ele)
let current=this.head
if(index==0){ //在头部插入新元素
if(this.count==null){
this.head=node
node.next=this.head //循环链表最后的元素就是我们创建的指向自己的节点
}else{
node.next=current //插入节点指向下一个的指针为第一个元素
current =this.getElementAt(this.count)
this.head=node
current.next=node
}
}else{ //在任意位置插入元素
previous=this.getElementAt(index-1)
node.next=previous.next
previous.next=node
}
this.count++
return true
}
return false
}
6.3.3 在任意位置移除元素
removeAt(index){
if(index>=0&&index<this.count){
let current=this.head
if(index==0){
if(this.size()==1){ //链表中只有1个元素
this.head=undefined
}
else{
const removed=this.head
this.head=this.head.next
let current=this.getElementAt(this.size()-1)
current.next=this.head
current=removed
}
else{
const previous=this.getElementAt(index-1)
current=previous.next
previous.next=current.next
}
this.count--
return current
}
}
return undefined
}
第七章 集合(Set类)
集合 : 是由一组无序且唯一的项组成。
7.1创建集合类
class Set{
constructor(){
this.items={}; //既可以使用对象,也可以使用数组表示
}
}
add(ele) :向集合中添加元素
delete(ele): 从集合中移除元素
has(ele): 元素在集合中,返回true,否则返回false
clear() :移除集合中所有元素
size(): 返回集合中元素的数量
values(): 返回集合中所有值的数组
1.判断是否有元素
has(ele){
return Object.prototype.hasOwnProperty.call(this.items,ele) //改变this指向为this.items来判断是否存在
}
//疑问: 为什么不用this.items.hasOwnProperty(ele)
// 因为不是所有对象都继承了Object.prototype甚至Object.prototype对象上的方法也有可能覆盖
2.添加元素
add(ele){
if(!this.has(ele)){ //集合中的数据是唯一的
this.items[ele]=ele
return true
}
return false
}
3.删除元素和清空集合
delete(ele){
if(this.has(ele)){
delete this.items[ele]
return true
}
return false
}
clear(){
this.items={}
}
4.获取长度
size(){
return Object.keys(this.items).length //返回对象多有属性的一个数组
}
//方法二
size(){
let count=0
for(let key in this.items){
if(this.items.hasOwnProperty(key)){ //不能简单使用for in 来迭代items对象,因为这样会获取到items对象原型上的属性。需要验证对象中是否有该key值
count++
}
}
return count
}
5.获取对象元素的数组
values(){
return Object.values(this.items)
}
values(){
let values=[]
for(let key in this.items){
if(this.items.hasOwnProperty(key)){
values.push(this.items[key])
}
}
return values
}
7.2 使用集合计算
7.2.1 并集
union(otherSet){
const unionSet = new Set() //创建集合,代表两个集合的并集
this.values().forEach(item=>unionSet.add(item))
otherSet.values().forEach(item=>unionSet.add(item))
return unionSet
}
7.2.2 交集
intersection(otherSet){
const intersection=new Set()
const values=this.values()
for(let i=0;i<values.length;i++){
if(otherSet.has(values[i])){
intersection.push(values[i])
}
}
return intersection
}
//优化
// 通过迭代元素少的数组来优化代码
intersection(otherSet){
const intersection=new Set()
let biggerValues,smallerValues
if(otherSet.size()<this.size()){
smallerValues=otherSet.values
biggerValues=this.values
}else{
smallerValues=this.values
biggerValues=otherSet.values
}
for(let i=0;i<smallerValues.length;i++){
if(biggerValues.has(smallerValues[i])){
intersection.push(smallerValues[i])
}
}
return intersection
}
7.2.3 差集
difference(otherSet){
const difference=new Set()
const values=this.values
for(var i=0;i<values.length;i++){
if(!otherSet.has(values[i])){
difference.add(values[i])
}
}
return difference
}
7.2.4 子集
isSubsetOf(otherSet){
if(this.size()>otherSet.size()){
return false
}
let res = this.values.every(item => return otherSet.has(item)==true)
return res
}
//every() 方法使用指定函数检测数组中的所有元素:
//如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
//如果所有元素都满足条件,则返回 true。
第8章 字典和散列表
在字典(映射)中,我们用[键,值]对的形式来存储数据
集合:表示一种互不相同的元素(不重复的元素),以[值,值]的形式来存储
字典:以[键,值]的形式来存储
8.1创建字典类
//以下是Dictionary的骨架
import {defaultToString} from '../util'
export default class Dictionary{
constructor(toStrFn=defaultToString){
this.toStrFn=toStrFn
this.table={}
}
}
//可以通过table[key] 来获取对象属性,这也是称它为关联数组的原因
在map类中,理想的情况是用字符串作为键名
export function defaultToString(item){
if(item==null){
return 'null'
}
if(item==undefined){
return 'undefined'
}
else if(typeof item=='string' || item instance of String){
return `${item}`
}
return item.toString()
}
8.2 使用一些字典方法
1.检测一个键是否存在与字典中
hasKey(key){
return this.table[this.toStrFn(key)] != null
}
2.设置键和值
set(key,value){
if(key!=null&&value!==null){
const tableKey=this.toStrFn(key)
this.table[tableKey]=new ValuePair(key,value)
return true
}
return false
}
//实例化ValuePair 类。
class ValuePaie{
constructor(key,value){
this.key=key;
this.value=value;
}
toString(){
return `[#${this.key}:${this.value}]`;
}
}
3.从字典中移除一个值
remove(key){
if(this.hasKey(key)){
delete this.table[this.toStrFn(key)]
return true
}
return false
}
4.从字典中查找一个值
get(key){
const valuePair=this.table[this.toStrFn(key)]
return valuePair==null? 'undefined':valuePair.value
}
8.3 散列表
HashMap类,是Dictionary类的一种散列表
散列算法的作用是尽快可能在数据结构中找到一个值。如果要来数据结构中获取一个值,需要迭代整个数据结构来找。如果使用散列表,就知道值的具体位置,因此能够快速检索到该值。
散列函数的作用是给定一个键值,返回值在表中的地址
第9章 递归
9.1 理解递归
递归通常涉及函数调用自身
function fn(someParam){
fn(someParam)
}
function a(){
b()
}
function b(){
a()
} //会一直执行下去
重点 :因此每个递归函数都必须有一个基线条件,即一个不在递归调用的条件(停止点)
9.2 递归算法
计算一个数的阶乘,数n的阶乘为n!
9.2.1 迭代阶乘
function fn(number){
if(number < 0 ) return undefined
let total=1
for(let i=numver;i>1;i--){
total=total*n;
}
return total
}
9.2.2 递归阶乘
function fn(n){
if(n===1|n===0){
return 1
}
return n*fn(n-1)
}
1.调用栈(Call Stack)
执行fn(3),当n的值为1时,可以看到Call Stack里有3个fn函数的调用
如果继续执行fn(1),会看到当fn(1)被返回后,Call Stack开始弹出fn的调用
第10章 树
10.1 树相关术语
1.每个节点都有一个父节点,根节点除外
2.节点分为:内部节点和外部节点,有至少一个子节点的为内部节点,没有子节点的元素为外部节点(叶节点)
3.子数:由节点和它的后代组成
4.深度: 节点的一个属性,节点的深度取决于它祖先节点的数量
10.2 二叉树和二叉搜索树
二叉树: 二叉树中的节点最多只能有两个节点:一个是左侧子节点,一个是右侧子节点。
二叉搜索树:二叉搜索树的一种,只允许你在左侧存入比父节点小的值,在右侧存入比父节点大的值
10.2.1 创建二叉搜索树类(BinarySearchTree类)
//node类 来表示二叉搜索树中的每个节点
export class Node{
constructor(key){
this.key=key //节点值
this.left=null //左侧子节点引用
this.right=null //右侧节点引用
}
}
注意:和链表一样,我们通过指针来表示节点之间的关系(树相关的术语称为边),在树中,我们对节点的称呼为键。
声明BinarySearchTree类:
export defalut class BinarySearchTree{
constructor(compareFn=defaultCompare){
this.compareFn=conpareFn //用来比较节点指
this.root=null //Node类型的根节点
}
}
//需要实现的一些方法:
1.insert(key) 向树中插入一个新的值
2.search(key) 向树中查找一个键。如果节点存在返回true,否则返回false
3.inOrderTraverse() 通过中序遍历方式遍历所有的节点
4.preOrderTraverse() 通过先序遍历方式遍历所有节点
5.postOrderTraverse() 通过后续遍历方式所有节点
6.min() 返回树中最小的值/键
7.max() 返回树中最大的值/键
8.remove() 从树中移除某个键
10.2.2 向二叉搜索树插入一个键
1.insert(key){
if(this.root==null){
this.root=new Node(key); //如果根节点为空,则给根节点赋值
}else{
this.insertNode(this.root,key) //否则,向根节点中添加key值
}
}
2.insertNode(node,key){
if(this.compareFn(key,node.key===compare.LESS_THAN)){
if(node.left===null){
node.left=new Node(key)
}else{
insertNode(node.left,key)
}
}else{
if(node.right==null){
node.right=new Node(key)
}else{
insertNode(node.right,key)
}
}
}
10.3 树的遍历
遍历是对树的一种基本运算,所谓遍历二叉树,就是按一定规则和顺序走遍二叉树的所有节点,使每一个节点都被访问一次,且只访问一次。由于二叉树使非线性结构,因此,树的遍历实质上是将二叉树各个节点转换成一个线性序列。
10.3.1 中序遍历
实质 :中序遍历首先找到左子树的最后一个节点,其次找到根节点,最后找到右子树。
class BianrySearchTree{
constructor(){
this.root=null
}
}
class Node{
constructor(key){
this.key=key
this.left=null
this.right=null
}
}
const binarySearchTree=new BinarySearchTree()
let mediumOrderArray = []
function mediumOrder(root) {
console.log(root);
if (root == null) return
mediumOrder(root.left)
console.log(root.key);
mediumOrderArray.push(root.key)
mediumOrder(root.right)
}
mediumOrder(binarySearchTree.root)
10.3.2 先序遍历
实质 :先序遍历首先访问根节点,在访问左子树,最后访问右子树。
let preorderArr=[]
function preorder(root){
if(root==null) return
preorderArr.push(root.key)
preorder(root.left)
preorder(root.right)
}
preorder(binarySearchTree.root)
10.3.3 后序遍历
实质 :后序遍历首先访问左子树的最后一个节点,在访问右子树,最后访问根节点。
let followUpArr=[]
function followUp(root){
if(root==null) return
followUp(root.left)
followUp(root.right)
followUpArr.push(root.key)
}
followUp(binarySearchTree.root)
console.log(followUpArr);
10.4 搜索树中的值
10.4.1 搜索最小值
min(){
return this.minNode(this.root)
}
minNode(node){
let current=node
while(current!=null&¤t.left!=null){
current=current.left
}
return current
}
10.4.2 搜索最大值
max(){
return this.maxNode(this.root)
}
maxNode(node){
let current=node
while(current!=null&¤t.right!=null){
current=current.right
}
return current.right
}
10.4.3 搜索一个特定的值
我们要做第一件事:声明一个search()方法
search(key){
return this.searchNode(this.root,key)
}
searchNode(node,key){
if(node==null){ //检验传入的参数node是否合法
return false
}
if(this.compareFn(key,node.key===Compare.LESS_THAN)){ //要找的键比当前的节点小
return this.searchNode(node.left,key) //那么继续在左侧的子树上搜索
}
if(this.compareFn(key,node.key===Compare.BIGGER_THAN)){
return this.searchNode(node.right,key)
}
else{ //证明要找的键和当前节点键相等
return true
}
}
10.4.3 移除一个节点 ( 不懂 )
remove(key){ //接收要移除的键,并且调用了removeNode方法
this,root=this.removeNode(this.root,key) //root被赋值为removeNode方法的返回值
}
removeNode(node,key){
if(node==null){ //如果检测的节点为null,说明键不存在与树中,返回null
return null
}
if(this.compareFn(key,node.key)===Compare.LESS_THAN){
node.left=this.removeNode(node.left,key) //在以左侧节点为根节点寻找
return node;
}
else if(this.compareFn(key,node.key)===Compare.BIGGER_THAN){
node.right=this.removeNode(node.right,key)
return node
}
// 情况一:移除一个叶节点
else{
if(node.left==null&&node.right==null){ //该节点是一个没有左侧和没有右侧子节点的叶节点
node = null //给这个节点赋值为null来移除它
return node
}
}
}
10.4.4 二叉树练习
//二叉树
class BinarySearchTree {
constructor() {
this.root = null
}
//插入键
insert(key) {
if (this.root == null) {
this.root = new Node(key)
} else {
this.insertNode(this.root, key)
}
}
insertNode(node, key) {
if (node.key > key) {
if (node.left == null) {
node.left = new Node(key)
} else {
this.insertNode(node.left, key)
}
} else {
if (node.right == null) {
node.right = new Node(key)
} else {
this.insertNode(node.right, key)
}
}
}
//搜索键
search(key) {
return this.searchNode(this.root, key)
}
searchNode(node, key) {
if (node == null) {
return false
}
if (key > node.key) {
return this.searchNode(node.right, key)
}
if (key < node.key) {
return this.searchNode(node.left, key)
}
if (node.key == key) {
return true
}
}
//返回树中最小的键
min() {
return this.minNode(this.root)
}
minNode(node) {
let current = node
while (current != null && current.left != null) {
current = current.left
}
return current
}
//返回树种最大的键
max() {
return this.maxNode(this.root)
}
maxNode(node) {
let current = node
while (current != null && current.right != null) {
current = current.right
}
return current
}
// 移除一个节点
remove(key){
this.root=this.removeNode(this.root,key)
}
removeNode(node,key){
if(node==null){
return null
}
if(key<node.key){
console.log(node);
node.left=this.removeNode(node.left,key)
console.log(node);
return node
}
if(key>node.key){
node.right=this.removeNode(node.right,key)
console.log(node);
return node
}
}
}
class Node {
constructor(key) {
this.key = key
this.left = null
this.right = null
}
}
const binarySearchTree = new BinarySearchTree()
binarySearchTree.insert(10)
binarySearchTree.insert(9)
binarySearchTree.insert(8)
binarySearchTree.insert(11)
binarySearchTree.insert(12)
console.log(binarySearchTree);
//中序遍历
let mediumOrderArray = []
function mediumOrder(root) {
if (root == null) return
mediumOrder(root.left)
mediumOrderArray.push(root.key)
mediumOrder(root.right)
}
mediumOrder(binarySearchTree.root)
//先序遍历
let preorderArr=[]
function preorder(root){
if(root==null) return
preorderArr.push(root.key)
preorder(root.left)
preorder(root.right)
}
preorder(binarySearchTree.root)
//后续遍历
let followUpArr=[]
function followUp(root){
if(root==null) return
followUp(root.left)
followUp(root.right)
followUpArr.push(root.key)
}
followUp(binarySearchTree.root)
binarySearchTree.remove(9)
console.log(binarySearchTree);
10.5 自平衡树
树存在一个问题: 树的一条分支会有很多层,而其他分支却有几层。
问题导致的后果: 这会在需要某条边上添加、移除和搜索某个节点时引起一些性能问题
解决这个问题: AVL树 (一种自平衡二叉搜索树) ,意思是任意一个节点左右两侧子树的高度之差最多为1
10.5.1 创建AVLTree类,计算节点高度以及平衡因子
class AVLTree extends BinarySearchTree{
constructor(compareFn=defaultCompare){
super(compareFn)
this.compareFN=compareFn
this.root=null
}
}
在AVL树中插入与移除节点与BST树完全相同,只不过AVL树的不同之处在于需要检验它的平衡因子
计算一个节点的高度如下:
getNodeHeight(node){
if(node==null){ //如果节点为空,返回高度-1
return -1
}
return Math.max(this.getNodeHeight(node.left),this.getNodeHeight(node.right))+1 //该节点的高度为左侧子节点或右侧子节点的最大高度+1
}
10.5.2 平衡操作---AVL旋转
左-左(LL) :向右的单旋转
右-右(RR): 想左的单旋转
左-右(LR): 向右的双旋转(先LL旋转,再RR旋转)
右-左(RL): 向左的双旋转(先RR旋转,再LL旋转)
· 左-左(LL) 向右的单旋转
出现的情况 : 节点的左侧子节点高度大于右侧子节点的高度,并且左侧子节点也是平衡或左侧较重的
rotationLL(node){
const temp=node.left //存储根节点左测的值(改变后的根节点)
node.left=temp.right //将根节点左侧的指针指向temp右侧的指针
temp.right=node //将temp的右侧子节点置为根节点
return node
}
· 右-右(RR) 向左的单旋转
出现的情况 : 右侧子节点高度大于左侧子节点高度,并且右侧子节点高度左侧较重。
解决方法 : 1.对左侧子节点进行LL(向右的单旋转),使节点形成 右侧高度大于左侧高度,且右侧子节点左侧是较重的 ,即可在操作RR向左旋转使节点平衡
2.在对不平衡的节点进行RR
即: 先做一次LL旋转 , 再做一次RR旋转
rotationRL(node){
node.right=this.rotationLL(node.rihgt)
return this.rotationRR(node)
}