1.栈
描述:
栈先进后出,相当于数组的倒序输出,栈顶就是最后一个元素,栈尾就是第一个元素
1.1 栈的封装
// 栈类
function Stack() {
// 栈中的属性
this.items = []
// 栈相关的方法
// 压栈操作
Stack.prototype.push = function (element) {
this.items.push(element)
}
// 出栈操作
Stack.prototype.pop = function () {
return this.items.pop()
}
// peek 查看栈顶的元素
Stack.prototype.peek = function () {
return this.items[this.items.length - 1]
}
// 判断栈中的元素是否为空
Stack.prototype.isEmpty = function () {
return this.items.length == 0
}
// 获取栈中元素的个数
Stack.prototype.size = function () {
return this.items.length
}
// toString方法
Stack.prototype.toString = function () {
var resultString = ''
for (var i = 0; i < this.items.length; i++) {
resultString += this.items[i] + ' '
}
return resultString
}
}
// 栈的使用
var s = new Stack()
s.push(20)
s.push(30)
s.push(10)
console.log(s);//items: [ 20, 30, 10 ]
console.log(s.peek());//10
console.log(s.isEmpty());//false
s.pop()
s.pop()
console.log(s);//items: [ 20 ]
1.2 栈的使用,将十进制转换成2进制
如100,转换成2进制为1100100,就是是100 一直除以2取余,直到结束,把所有余数倒序排列出来就是2进制数
// 函数:将十进制转换成2进制
function zhuanhuan(item){
// 1。定义一个栈对象
var stack = new Stack()
// 循环除法
while (item > 0) {
stack.push(item % 2)
item = Math.floor(item / 2)
}
// 将数据取出
var binayriStrng = ""
while (!stack.isEmpty()) {
binayriStrng += stack.pop()
}
return binayriStrng
}
console.log(zhuanhuan(100));//1100100
2. 队列
描述:
先进先出,与栈唯一不同的就是队列依次弹出数组第一个元素,栈是依次弹出最后一个元素
2.1 队列封装如下:
// 自定义队列
function Queue() {
this.items = []
// 队列操作的方法
// enqueue方法 将元素添加到队列
Queue.prototype.enqueue = function (element) {
this.items.push(element)
}
// delete 删除队列前端元素
Queue.prototype.dequeue = function () {
return this.items.shift()
}
// 查看前端的元素
Queue.prototype.front = function () {
return this.items[0]
}
// 查看队列是否为空
Queue.prototype.isEmpty = function () {
return this.items.length == 0
}
// 查看队列中元素的个数
Queue.prototype.size = function () {
return this.items.length
}
// toString方法
Queue.prototype.toString = function () {
var resultString = ''
for (var i = 0; i < this.items.length; i++) {
resultString += this.items[i] + ' '
}
return resultString
}
}
// 使用队列
var queue = new Queue()
queue.enqueue('a')
queue.enqueue('b')
queue.enqueue('c')
console.log(queue);// items: [ 'a', 'b', 'c' ]
// 删除
queue.dequeue()
console.log(queue);//items: [ 'b', 'c' ]
console.log(queue.front());//b
console.log(queue.toString());//bc
2.2 优先级队列
描述
:在放入队列中元素时,都包含一个优先级属性,用来判断该元素应该放在队列中的哪个位置
实现思路
:for循环,判断传入的优先级与队列中已有属性进行对比,如果比队列中优先级小那么就插入到其对应索引处([ ].splice(i, 0, queueElement)),如果比原队列优先级都大则直接push到后面即可。
// 封装优先级队列
function PriorityQueue() {
this.items = []
// 封装一个新的构造函数, 用于保存元素和元素的优先级
function QueueElement(element, priority) {
this.element = element
this.priority = priority
}
// 添加元素的方法
PriorityQueue.prototype.enqueue = function (element, priority) {
// 1.根据传入的元素, 创建新的QueueElement
var queueElement = new QueueElement(element, priority)
// 2.获取传入元素应该在正确的位置
if (this.items.length == 0) {
this.items.push(queueElement)
} else {
var added = false
for (var i = 0; i < this.items.length; i++) {
// 注意: 我们这里是数字越小, 优先级越高
if (queueElement.priority < this.items[i].priority) {//这里比较是插入到中间,如果没有进去判断added
this.items.splice(i, 0, queueElement)
added = true
break
}
}
// 遍历完所有的元素, 优先级都大于新插入的元素时, 就插入到最后
if (!added) {//到这里说明元素没有找到合适的位置插入,直接push到数组的后方
this.items.push(queueElement)
}
}
}
// 删除元素的方法
PriorityQueue.prototype.dequeue = function () {
return this.items.shift()
}
// 获取前端的元素
PriorityQueue.prototype.front = function () {
return this.items[0]
}
// 查看元素是否为空
PriorityQueue.prototype.isEmpty = function () {
return this.items.length == 0
}
// 获取元素的个数
PriorityQueue.prototype.size = function () {
return this.items.length
}
}
// 创建优先级队列对象
var pQueue = new PriorityQueue()
// 添加元素
pQueue.enqueue("abc", 10)
pQueue.enqueue("cba", 5)
pQueue.enqueue("nba", 12)
pQueue.enqueue("mba", 3)
console.log(pQueue);
/* items: [
QueueElement { element: 'mba', priority: 3 },
QueueElement { element: 'cba', priority: 5 },
QueueElement { element: 'abc', priority: 10 },
QueueElement { element: 'nba', priority: 12 }
] */
// 遍历所有的元素
var size = pQueue.size()
for (var i = 0; i < size; i++) {
console.log(item.element + "-" + item.priority)
}
//mba-3 cba-5 abc-10 nba-12
2.3面试题:击鼓传花
击鼓传花的规则:
- 几个朋友一起玩一个游戏, 围成一圈, 开始数数, 数到某个数字的人自动淘汰.
- 最后剩下的这个人会获得胜利, 请问最后剩下的是原来在哪一个位置上的人?
实现思路:
利用队列,封装一个函数传入所有人名字和数字这两个参数,然后把数字之前的人名依次删除,并且添加到队列的后方,而数字本身将它删除,直到队列中大小变为1则结束,返回出这个名字对应原索引值和名字即可
// 实现击鼓传花的函数
function passGame(nameList, num) {
// 1.创建一个队列, 并且将所有的人放在队列中
// 1.1.创建队列
var queue = new Queue()
// 1.2.通过for循环, 将nameList中的人放在队列中
for (var i = 0; i < nameList.length; i++) {
queue.enqueue(nameList[i])
}
// 2.寻找最后剩下的人
while (queue.size() > 1) {
// 将前num-1中的人, 都从队列的前端取出放在队列的后端
for (var i = 0; i < num; i++) {
queue.enqueue(queue.dequeue())
}
// 将第num个人, 从队列中移除
queue.dequeue()
}
// 3.获取剩下的一个人
console.log(queue.size());
var endName = queue.dequeue()
console.log("最终留下来的人:" + endName) //最终留下来的人:小hong
// 4.获取该人在队列中的位置
return nameList.indexOf(endName)
}
// 验证结果
var names = ['小li','小hong','小bai','小丽'];
var index = passGame(names, 3) // 数到3的人淘汰
console.log("最终位置:" + index) //最终位置:1
3. 链表
3.1 单向链表结构
- 只能从头遍历到尾或者从尾遍历到头
- 也就是链表相连的过程是单向的
- 实现原理是上一个链表中有一个指向下一个的引用
缺点:
– 可以轻松的到达下一个节点,但是回到前一个节点是很难的
3.1.1 封装单向链表
// 封装链表的构造函数
function LinkList() {
// 封装一个Node类, 用于保存每个节点信息
function Node(data) {
this.data = data //保存数据
this.next = null //下一个指针属性
}
// 属性
this.head = null
this.length = 0
}
}
3.1.2 append方法(插入到最后)
// 1.添加参数到最后append
LinkList.prototype.append = function (data) {
// 1.创建一个新节点
var newNode = new Node(data)
// 2.判断是否添加的是第一个节点
if (this.length == 0) {//如果链表长度为0,则把新节点 直接放到头部 /是第一个节点
this.head = newNode
} else { //不是第一个节点
var current = this.head //保存头节点
while (current.next) { //循环,如果头节点的next存在,那么就一直继续,直到找到链表最后一项值
current = current.next
}
current.next = newNode //然后把最后节点的next指向新节点
}
this.length += 1
}
3.1.3 toString方法(打印)
因为是链表结构,所以我们需要重新封装一个函数来打印
// 2.toString方法
LinkList.prototype.toString = function () {
// 1.定义变量
var current = this.head
var listString = ""
// 2.循环获取一个个的节点
while (current) {
listString += current.data + " "
current = current.next
}
return listString
}
3.1.4 insert方法(选择位置插入)
第一种情况,插入位置为0,也就是插入第一个节点
if(position == 0){
newNode.next = this.head //新节点的next指向head指向节点
this.head = newNode //head指向新节点
}
第二种情况,插入位置不为0
var index = 0 //定义索引值
var current = this.head //current负责保存head节点指向的下一个节点
var previous = null //previous保存前一个节点,默认是head也就是null
while(index++ < position){//如果索引小于position时循环,等于说明找到对应节点位置
previous = current //此时current就是前一个节点
current = current.next //next指向下一个节点
}
newNode.next = current
previous.next = newNode
完整代码:
// 3.insert方法,选择位置插入
LinkList.prototype.insert = function (position, data) {
// 1.对positrion进行越界判断
// -100
if (position < 0 || position > this.length) return false
// 2.根据data创建newNode
var newNode = new Node(data)
// 3.判断插入的位置是否是第一个
if(position == 0){
newNode.next = this.head //新节点的next指向head指向节点
this.head = newNode //head指向新节点
}else{
var index = 0 //定义索引值
var current = this.head //current负责保存head节点指向的下一个节点
var previous = null //previous保存前一个节点,默认是head也就是null
while(index++ < position){//如果索引小于position时循环,等于说明找到对应节点位置
previous = current //此时current就是前一个节点
current = current.next //next指向下一个节点
}
newNode.next = current
previous.next = newNode
}
// 4.length+1
this.length += 1
return true
}
3.1.5 get方法(根据位置返回对应值)
// 4.get方法,根据位置返回对应值
LinkList.prototype.get = function (position){
// 1.越界判断
if(position < 0 || position >= this.length) return null
// 2.获取对应的data
var current = this.head //拿到第一个节点
var index = 0 //定义索引
while(index++ < position){//循环直到不满足条件
current = current.next
}
return current.data
}
3.1.6 indexOf方法(查找元素对应索引)
// 5.indexOf方法,根据元素找见索引
LinkList.prototype.indexOf = function(data){
var current = this.head
var index = 0
while(current){//一直查找到结束
if(current.data == data){//如果相同则返回对应索引
return index
}
current = current.next
index += 1
}
// 3.找到最后没有找到,返回-1
return -1
}
3.1.7 updata方法(更新对应下标的值)
// 6.updata方法,修改对应下标值
LinkList.prototype.updata = function(position,newData){
// 1.越界判断
if(position < 0 || position >= this.length) return false
// 2.查找正确节点
var current = this.head
var index = 0
while(index++ < position){
current = current.next
}
// 3.将position位置的node的data修改成newData
current.data = newData
return true
}
3.1.8 removeAt方法(从链表对应位置删除节点)
// 7.removeAt方法 删除对应位置
LinkList.prototype.removeAt = function(position){
// 1.越界判断
if(position < 0 || position >= this.length) return false
// 2.判断是否删除的是第一个节点
var current =this.head
if(position ==0){
this.head = this.head.next
}else{
var index = 0
var previous = null //定义前一个节点
while(index++ <position){
previous = current
current = current.next
}
// 前一个节点的next指向,current的next即可
previous.next = current.next
}
// 3.length-1
this.length -= 1
return current.data
}
3.1.9 remove方法(删除对应元素节点)
// 8.remove方法,直接调用已有方法
LinkList.prototype.remove = function(data){
// 1.获取data在列表中的位置
var position = this.indexOf(data)
// 2.根据位置信息,删除节点
return this.removeAt(position)
}
3.1.10 isEmpty方法(判断是否为空)
// 9.isEmpty方法
LinkList.prototype.isEmpty = function(){
return this.length == 0
}
3.1.11 size方法(返回长度)
// 10.size方法
LinkList.prototype.size = function(){
return this.length
}
3.1.12 完整代码
// 封装链表的构造函数
function LinkList() {
// 封装一个Node类, 用于保存每个节点信息
function Node(data) {
this.data = data //保存数据
this.next = null //下一个指针属性
}
// 属性
this.head = null
this.length = 0
// 1.添加参数到最后append
LinkList.prototype.append = function (data) {
// 1.创建一个新节点
var newNode = new Node(data)
// 2.判断是否添加的是第一个节点
if (this.length == 0) {//如果链表长度为0,则把新节点 直接放到头部 /是第一个节点
this.head = newNode
} else { //不是第一个节点
var current = this.head //保存头节点
while (current.next) { //循环,如果头节点的next存在,那么就一直继续,直到找到链表最后一项值
current = current.next
}
current.next = newNode //然后把最后节点的next指向新节点
}
this.length += 1
}
// 2.toString方法
LinkList.prototype.toString = function () {
// 1.定义变量
var current = this.head
var listString = ""
// 2.循环获取一个个的节点
while (current) {
listString += current.data + " "
current = current.next
}
return listString
}
// 3.insert方法,选择位置插入
LinkList.prototype.insert = function (position, data) {
// 1.对positrion进行越界判断
// -100
if (position < 0 || position > this.length) return false
// 2.根据data创建newNode
var newNode = new Node(data)
// 3.判断插入的位置是否是第一个
if(position == 0){
newNode.next = this.head //新节点的next指向head指向节点
this.head = newNode //head指向新节点
}else{
var index = 0 //定义索引值
var current = this.head //current负责保存head节点指向的下一个节点
var previous = null //previous保存前一个节点,默认是head也就是null
while(index++ < position){//如果索引小于position时循环,等于说明找到对应节点位置
previous = current //此时current就是前一个节点
current = current.next //next指向下一个节点
}
newNode.next = current
previous.next = newNode
}
// 4.length+1
this.length += 1
return true
}
// 4.get方法,根据位置返回对应值
LinkList.prototype.get = function (position){
// 1.越界判断
if(position < 0 || position >= this.length) return null
// 2.获取对应的data
var current = this.head //拿到第一个节点
var index = 0 //定义索引
while(index++ < position){//循环直到不满足条件
current = current.next
}
return current.data
}
// 5.indexOf方法,根据元素找见索引
LinkList.prototype.indexOf = function(data){
var current = this.head
var index = 0
while(current){
if(current.data == data){
return index
}
current = current.next
index += 1
}
// 3.找到最后没有找到,返回-1
return -1
}
// 6.updata方法,修改对应下标值
LinkList.prototype.updata = function(position,newData){
// 1.越界判断
if(position < 0 || position >= this.length) return false
// 2.查找正确节点
var current = this.head
var index = 0
while(index++ < position){
current = current.next
}
// 3.将position位置的node的data修改成newData
current.data = newData
return true
}
// 7.removeAt方法 删除对应位置
LinkList.prototype.removeAt = function(position){
// 1.越界判断
if(position < 0 || position >= this.length) return false
// 2.判断是否删除的是第一个节点
var current =this.head
if(position ==0){
this.head = this.head.next
}else{
var index = 0
var previous = null
while(index++ <position){
previous = current
current = current.next
}
// 前一个节点的next指向,current的next即可
previous.next = current.next
}
// 3.length-1
this.length -= 1
return current.data
}
// 8.remove方法
LinkList.prototype.remove = function(data){
// 1.获取data在列表中的位置
var position = this.indexOf(data)
// 2.根据位置信息,删除节点
return this.removeAt(position)
}
// 9.isEmpty方法
LinkList.prototype.isEmpty = function(){
return this.length == 0
}
// 10.size方法
LinkList.prototype.size = function(){
return this.length
}
}
// 测试代码
// 1.创建LinkedList
var list = new LinkList()
// 2.测试append方法
list.append('a')
list.append('b')
list.append('c')
console.log(list.toString());//a b c
// 3.测试insert方法
list.insert(0,'hhh')
list.insert(3,'3hh')
console.log(list.toString());//hhh a b 3hh c
// 4.get方法
console.log(list.get(2)); //b
// 5.测试indexOf方法
console.log(list.indexOf('a'));// 1
// 6.测试updata方法
list.updata(0,'hh')
console.log(list.toString());//hh a b 3hh c
// 7.removeAt测试
list.removeAt(3)
console.log(list.toString());//hh a b c
// 8.remove方法的测试
list.remove('a')
console.log(list.toString());//hh b c
// 9.测试isEmpty和size方法
console.log(list.isEmpty());//false
console.log(list.size());// 3
4. 双向链表结构
介绍:
- 既可以从头遍历到尾, 又可以从尾遍历到头
- 也就是链表相连的过程是双向的. 那么它的实现原理, 你能猜到吗?
- 一个节点既有向前连接的引用, 也有一个向后连接的引用.
- 双向链表可以有效的解决单向链表中提到的问题.
- 双向链表有什么缺点呢?
- 每次在插入或删除某个节点时, 需要处理四个节点的引用, 而不是两个. 也就是实现起来要困难一些
- 并且相当于单向链表, 必然占用内存空间更大一些.
- 但是这些缺点和我们使用起来的方便程度相比, 是微不足道的.
双向连接的图解:
4.1 封装双向链表
// 封装双向链表
function DoublyLinkedList() {
// 内部类:节点类
function Node(data){
this.data = data
this.prev = null //前节点
this.next = null //后节点
}
// 属性
this.head = null //初始状态头指向空
this.tail = null //尾指向空
this.length = 0
}
4.1.2 append方法(在尾部追加数据)
情况一: 链表原来为空
链表中原来如果没有数据, 那么直接让head和tail指向这个新的节点即可.
情况二: 链表中已经存在数据
因为我们是要将数据默认追加到尾部, 所以这个变得也很简单.
首先tail中的next之前指向的是null. 现在应该指向新的节点newNode: this.tail.next = newNode
因为是双向链表, 新节点的next/tail目前都是null. 但是作为最后一个节点, 需要有一个指向前一个节点的引用. 所以这里我们需要newNode.prev = this.tail
因为目前newNod已经变成了最后的节点, 所以this.tail属性的引用应该指向最后: this.tail = newNode即可
代码3部分不用多做解析, length需要+1
// append方法,在尾部追加数据
DoublyLinkedList.prototype.append = function (data) {
// 1.根据元素创建节点
var newNode = new Node(data)
// 2.判断列表是否为空列表
if (this.head == null) {//因为链表为空只有一个节点,所以head和tail都指向newNode
this.head = newNode
this.tail = newNode
} else {
this.tail.next = newNode //尾节点的下一个节点为新节点
newNode.prev = this.tail //新节点的prev等于tail
this.tail = newNode //把新节点赋值给尾节点
}
// 3.length+1
this.length++
}
4.1.3 遍历的方法
// 正向遍历的方法
DoublyLinkedList.prototype.forwardString = function () {
var current = this.head
var forwardStr = ""
while (current) {
forwardStr += "," + current.data
current = current.next
}
return forwardStr.slice(1)
}
// 反向遍历的方法
DoublyLinkedList.prototype.reverseString = function () {
var current = this.tail
var reverseStr = ""
while (current) {
reverseStr += "," + current.data
current = current.prev
}
// slice方法是因为有个逗号在第一个位置要去掉
return reverseStr.slice(1)
}
// 实现toString方法
DoublyLinkedList.prototype.toString = function () {
return this.forwardString()
}
4.1.4 insert (在任意位置插入数据)
思路:首先判断新节点插入的位置,如果是第一个位置
则分成链表为空和不为空这两种情况,如果插入到最后位置
,那么直接添加,如果是中间
,那么需要四个指针的指向。因为插入不同位置指针的操作是不同的
第一个位置插入(不为空的情况)
中閒位置插入
// 在任意位置插入数据
DoublyLinkedList.prototype.insert = function (position, data) {
// 1.判断越界的问题
if (position < 0 || position > this.length) return false
// 2.创建新的节点
var newNode = new Node(data)
// 3.判断插入的位置
if (position === 0) { // 在第一个位置插入数据
// 判断链表是否为空
if (this.head == null) {
this.head = newNode
this.tail = newNode
} else {//如果不为空,那么插入到一个位置
this.head.prev = newNode
newNode.next = this.head
this.head = newNode
}
} else if (position === this.length) { // 插入到最后的情况
this.tail.next = newNode
newNode.prev = this.tail
this.tail = newNode
} else { // 在中间位置插入数据
// 定义属性
var index = 0
var current = this.head
var previous = null
// 查找正确的位置
while (index++ < position) {
previous = current
current = current.next
}
// 交换节点的指向顺序
newNode.next = current
newNode.prev = previous
current.prev = newNode
previous.next = newNode
}
// 4.length+1
this.length++
return true
}
4.1.5 get方法(查找对应位置数据)
判断传入位置是在this.lengs的前面还是后面,前面则从前往后查找,后面则从后往前查找
// get方法
DoublyLinkedList.prototype.get = function(position){
// 1.越界判断
if(position <0 || position >= this.length) return null
// 进行判断,看position位置和链表一半进行对比,提高效率
if(this.length / 2 > position){ //小于一半则从前往后查找,大于一半则从后往前查找
var current = this.head
var index = 0
while(index++ < position){
current = current.next
}
return current.data
}else{
var current = this.tail
var index = this.length -1
while(index-- < position){
current = current.next
}
return current.data
}
}
4.1.6 indexOf方法(根据元素查找索引)
DoublyLinkedList.prototype.indexOf = function(data){
// 1.定义变量
var current = this.head
var index = 0
// 2.查找和data相同的点
while(current){
if(current.data == data){
return index
}
current = current.index
index += 1
}
return -1
}
4.1.7 updata方法(更新链表中对应位置元素)
// updata方法
DoublyLinkedList.prototype.updata = function(position,newData){
// 1.越界判断
if(position <0 || position >= this.length) return null
// 2。寻找正确节点
var current = this.head
var index = 0
while(index++ < position){
current = current.next
}
current.data = newData
return true
}
4.1.8 removeAt方法(删除对应位置节点)
思路:分成几种情况来更改指针next和prev的指向
- 只有一个节点情况,删除这一个
有2个即以上节点情况:
- 删除最后一个节点
- 删除第一个节点
- 删除中间的节点
// removeAt方法
DoublyLinkedList.prototype.removeAt = function(position){
// 1.越界判断
if(position < 0 || position >= this.length) return null
// 2.判断是否只有一个节点
var current = this.head
if(this.length == 1){
this.head = null
this.tail = null
}else{
if(position == 0){//如果删除的是第一个位置
this.head.next.prev = null //第二个节点的prev指向null
this.head = this.head.next//head指向第二个节点
}else if(position == this.length -1){//如果删除的是最后一个节点
current = this.tail
this.tail.prev.next = null //倒数第二个节点的next指向null
this.tail = this.tail.prev//tail指向倒数第二个节点
}else{//如果是删除中间的节点
var index = 0
while(index++ < position){//循环找见对应位置节点
current = current.next
}
current.prev.next = current.next //前一个节点的next指向后一个节点
current.next.prev = current.prev
}
}
// 3.length-1
this.length -= 1
return current.data
}
4.1.9 remove方法(删除对应元素)
// remove方法
DoublyLinkedList.prototype.remove = function(data){
// 1.根据data获取索引
var index = this.indexOf(data)
return this.removeAt(index)
}
4.1.10 其他方法封装(isEmpty,size,getHead ,getTail )
// isEmpty方法
DoublyLinkedList.prototype.isEmpty = function(){
return this.length == 0
}
// size方法
DoublyLinkedList.prototype.size = function(){
return this.length
}
// 获取链表第一个元素
DoublyLinkedList.prototype.getHead = function(){
return this.head.data
}
// 获取链表最后一个元素
DoublyLinkedList.prototype.getTail = function(){
return this.tail.data
}
4.1.11 完整代码展示
// 封装双向链表
function DoublyLinkedList() {
// 内部类:节点类
function Node(data) {
this.data = data
this.prev = null
this.next = null
}
// 属性
this.head = null //初始状态头指向空
this.tail = null //尾指向空
this.length = 0
// append方法,在尾部追加数据
DoublyLinkedList.prototype.append = function (data) {
// 1.根据元素创建节点
var newNode = new Node(data)
// 2.判断列表是否为空列表
if (this.head == null) {//因为链表为空只有一个节点,所以head和tail都指向newNode
this.head = newNode
this.tail = newNode
} else {
this.tail.next = newNode //尾节点的下一个节点为新节点
newNode.prev = this.tail //新节点的prev等于tail
this.tail = newNode //把新节点赋值给尾节点
}
// 3.length+1
this.length++
}
// 正向遍历的方法
DoublyLinkedList.prototype.forwardString = function () {
var current = this.head
var forwardStr = ""
while (current) {
forwardStr += "," + current.data
current = current.next
}
// slice方法是因为有个逗号在第一个位置要去掉
return forwardStr.slice(1)
}
// 反向遍历的方法
DoublyLinkedList.prototype.reverseString = function () {
var current = this.tail
var reverseStr = ""
while (current) {
reverseStr += "," + current.data
current = current.prev
}
return reverseStr.slice(1)
}
// 实现toString方法
DoublyLinkedList.prototype.toString = function () {
return this.forwardString()
}
// 在任意位置插入数据
DoublyLinkedList.prototype.insert = function (position, data) {
// 1.判断越界的问题
if (position < 0 || position > this.length) return false
// 2.创建新的节点
var newNode = new Node(data)
// 3.判断插入的位置
if (position === 0) { // 在第一个位置插入数据
// 判断链表是否为空
if (this.head == null) {
this.head = newNode
this.tail = newNode
} else {//如果不为空,那么插入到一个位置
this.head.prev = newNode
newNode.next = this.head
this.head = newNode
}
} else if (position === this.length) { // 插入到最后的情况
this.tail.next = newNode
newNode.prev = this.tail
this.tail = newNode
} else { // 在中间位置插入数据
// 定义属性
var index = 0
var current = this.head
var previous = null
// 查找正确的位置
while (index++ < position) {
previous = current
current = current.next
}
// 交换节点的指向顺序
newNode.next = current
newNode.prev = previous
current.prev = newNode
previous.next = newNode
}
// 4.length+1
this.length++
return true
}
// get方法
DoublyLinkedList.prototype.get = function(position){
// 1.越界判断
if(position <0 || position >= this.length) return null
// 进行判断,看position位置和链表一半进行对比,提高效率
if(this.length / 2 > position){ //小于一半则从前往后查找,大于一半则从后往前查找
var current = this.head
var index = 0
while(index++ < position){
current = current.next
}
return current.data
}else{
var current = this.tail
var index = this.length -1
while(index-- < position){
current = current.next
}
return current.data
}
}
// indexOf方法
DoublyLinkedList.prototype.indexOf = function(data){
// 1.定义变量
var current = this.head
var index = 0
// 2.查找和data相同的点
while(current){
if(current.data == data){
return index
}
current = current.index
index += 1
}
return -1
}
// updata方法
DoublyLinkedList.prototype.updata = function(position,newData){
// 1.越界判断
if(position <0 || position >= this.length) return null
// 2。寻找正确节点
var current = this.head
var index = 0
while(index++ < position){
current = current.next
}
current.data = newData
return true
}
// removeAt方法
DoublyLinkedList.prototype.removeAt = function(position){
// 1.越界判断
if(position < 0 || position >= this.length) return null
// 2.判断是否只有一个节点
var current = this.head
if(this.length == 1){
this.head = null
this.tail = null
}else{
if(position == 0){//如果删除的是第一个位置
this.head.next.prev = null //第二个节点的prev指向null
this.head = this.head.next//head指向第二个节点
}else if(position == this.length -1){//如果删除的是最后一个节点
current = this.tail
this.tail.prev.next = null //倒数第二个节点的next指向null
this.tail = this.tail.prev//tail指向倒数第二个节点
}else{//如果是删除中间的节点
var index = 0
while(index++ < position){//循环找见对应位置节点
current = current.next
}
current.prev.next = current.next //前一个节点的next指向后一个节点
current.next.prev = current.prev
}
}
// 3.length-1
this.length -= 1
return current.data
}
// remove方法
DoublyLinkedList.prototype.remove = function(data){
// 1.根据data获取索引
var index = this.indexOf(data)
return this.removeAt(index)
}
// isEmpty方法
DoublyLinkedList.prototype.isEmpty = function(){
return this.length == 0
}
// size方法
DoublyLinkedList.prototype.size = function(){
return this.length
}
// 获取链表第一个元素
DoublyLinkedList.prototype.getHead = function(){
return this.head.data
}
// 获取链表最后一个元素
DoublyLinkedList.prototype.getTail = function(){
return this.tail.data
}
}
// 1.创建双向链表对象
var list = new DoublyLinkedList()
// 2.追加元素
list.append("abc")
list.append("cba")
list.append("nba")
list.append("mba")
// 3.获取所有的遍历结果
console.log(list.forwardString()) // abc,cba,nba,mba
console.log(list.reverseString()) // mba,nba,cba,abc
// 4.测试任意位置插入
list.insert(0,'hhh')
console.log(list.toString()) // abc,cba,nba,mba
// 5.get测试
console.log( list.get(0)); //hhh
// 5.测试indexOf
console.log(list.indexOf('hhh')); //0
// 6.测试updata
list.updata('0','hh')
console.log(list.toString()) // hh,abc,cba,nba,mba
// 7.测试removeAt
console.log(list.removeAt(1));//abc
console.log(list.toString()) //hh,cba,nba,mba
// 8.测试remove方法
console.log(list.remove('hh')); //'hh'
console.log(list.toString()) //cba,nba,mba
// 9.测试其他方法
console.log(list.size()); //3
console.log(list.getHead()); //cba
5. 集合
特殊之处:
集合里面的元素不能通过下标值访问
,也不能重复
。
没有顺序意味着不能通过下标值进行访问,不能重复意味着相同的对象在集合中指挥存在一份
在ES6中以及有封装好的set类型
,这里自己实现一下,了解set内部机制
5.1封装集合类
因为set也就是集合中没有重复的数字且无序,所以这里我们使用对象{}
的方式去保存数据
// 封装集合类
function Set(){
// 属性
this.items = {}
// 方法
// add方法
Set.prototype.add = function(value){
// 判断当前集合中是否已经包含了该元素
if(this.has(value)){
return false
}
// 将元素添加到集合中
this.items[value] = value
return true
}
// has方法
Set.prototype.has = function(value){
// Object.prototype.hasOwnProperty方法接受一个字符串作为参数,返回一个布尔值,表示该实例对象自身是否具有该属性。
return this.items.hasOwnProperty(value)
}
// remove方法
Set.prototype.remove = function(value){
// 1.判断该集合中是否包含该元素
if(!this.has(value)){
return false
}
// 2.将元素从属性中删除
delete this.items[value]
return true
}
// clear方法
Set.prototype.clear = function (){
this.items = {}
}
// size方法
Set.prototype.size = function (){
// Object.keys方法的参数是一个对象,返回一个数组。
return Object.keys(this.items).length
}
// 获取集合中所有的值
Set.prototype.values = function (){
return Object.keys(this.items)
}
}
// 测试Set类
var set = new Set()
// 2.添加元素
set.add('a')
set.add('c')
set.add('b')
console.log(set.add('a')); // false
console.log(set.values());// [ 'a', 'c', 'b' ]
// 3.删除元素
console.log(set.remove('c'));// true
console.log(set.values());// [ 'a', 'b' ]
// 4.clear
console.log(set.size());// 2
set.clear()
console.log(set.size());// 0
5.2 集合之间的操作
并集:返回两个集合中所有存在的元素
交集:返回两个集合中同时存在的元素
差集:返回所有存在第一个集合但不存在在第二个集合中的元素
子集:验证一个给定集合是否是另一个集合的子集,就是包含在其中的集合元素
5.2.1 并集的实现
// 集合间的操作
// 并集
Set.prototype.union = function (otherSet){
// this:集合对象A
// otherSet:集合对象B
// 1.创建一个新的集合
var unionSet = new Set()
// 2.将a集合中的所有元素都添加到新结合中
var values = this.values()
for(var i = 0;i < values.length ; i++){
unionSet.add(values[i])
}
// 3.将b集合中的所有元素都添加到新结合中
var values = otherSet.values()
for(var i = 0;i < values.length ; i++){
unionSet.add(values[i])
}
return unionSet
}
测试
// 测试union
var one = new Set()
one.add('a')
one.add('b')
var two = new Set()
two.add('c')
two.add('d')
two.add('a')
console.log(one.union(two)); // items: { a: 'a', b: 'b', c: 'c', d: 'd' }
5.2.2交集的实现
// 交集
Set.prototype.intersection = function (otherSet){
// this:集合对象A
// otherSet:集合对象B
// 1.创建一个新的集合
var intersection = new Set()
var values = this.values()
for(var i = 0;i < values.length ; i++){
var item = values[i]
// 如果b集合中都存在则放到新集合中
if(otherSet.has(item)){
intersection.add(item)
}
}
return intersection
}
测试
// 测试union
var one = new Set()
one.add('a')
one.add('b')
var two = new Set()
two.add('c')
two.add('d')
two.add('a')
// 测试交集
console.log(one.intersection(two)) //{ a: 'a' }
5.2.3 差集的实现
差集就是把A集合中的元素遍历,B集合中没有的放入新集合当作
// 差集
Set.prototype.difference = function (otherSet){
// this:集合对象A
// otherSet:集合对象B
// 1.创建一个新的集合
var difference = new Set()
var values = this.values()
for(var i = 0;i < values.length ; i++){
var item = values[i]
// 如果B集合中不存在则放到新集合中
if(!otherSet.has(item)){
difference.add(item)
}
}
return difference
}
测试
// 测试差集
var one = new Set()
one.add('a')
one.add('b')
var two = new Set()
two.add('c')
two.add('d')
two.add('a')
console.log(one.difference(two)) // { b: 'b' }
5.2.4 子集的实现
// 子集
Set.prototype.subset = function (otherSet){
// this:集合对象A
// otherSet:集合对象B
// 1.创建一个新的集合
var subset = new Set()
var values = this.values()
for(var i = 0;i < values.length ; i++){
var item = values[i]
// 如果有一个元素不存在B集合中,那么直接返回false
if(!otherSet.has(item)){
return false
}
}
// 如果全部存在则返回true
return true
}
6. 字典
字典的主要特点是一一对应的关系
.
比如保存一个人的信息, 在合适的情况下取出这些信息.
使用数组的方式: [18, “Coderwhy”, 1.88]. 可以通过下标值取出信息.
使用字典的方式: {“age” : 18, “name” : “Coderwhy”, “height”: 1.88}. 可以通过key取出value
7. 哈希表
哈希表通常是基于数组进行实现的, 但是相对于数组, 它也很多的优势:
- 它可以提供非常快速的插入-删除-查找操作
- 无论多少数据, 插入和删除值需要接近常量的时间: 即O(1)的时间级. 实际上, 只需要几个机器指令即可
- 哈希表的速度比树还要快, 基本可以瞬间查找到想要的元素
- 哈希表相对于树来说编码要容易很多.
哈希表相对于数组的一些不足:
- 哈希表中的数据是没有顺序的, 所以不能以一种固定的方式(比如从小到大)来遍历其中的元素.
- 通常情况下, 哈希表中的key是不允许重复的, 不能放置相同的key, 用于保存不同的元素.
所以哈希表到底是什么?
它的结构就是数组, 但是它神奇的地方在于对下标值的一种变换, 这种变换我们可以称之为哈希函数, 通过哈希函数可以获取到HashCode.
哈希表
幂的连乘
- 现在, 我们想通过一种算法, 让cats转成数字后不那么普通. 数字相加的方案就有些过于普通了.
有一种方案就是使用幂的连乘, 什么是幂的连乘呢? - 其实我们平时使用的大于10的数字, 可以用一种幂的连乘来表示它的唯一性:比如: 7654 = 710³+610²+5*10+4
- 我们的单词也可以使用这种方案来表示: 比如cats = 327³+127²+20*27+17= 60337
- 这样得到的数字可以几乎保证它的唯一性, 不会和别的单词重复.
- 问题: 如果一个单词是zzzzzzzzzz(一般英文单词不会超过10个字符). 那么得到的数字超过7000000000000. 数组可以表示这么大的下标值吗?
而且就算能创建这么大的数组, 事实上有很多是无效的单词. 创建这么大的数组是没有意义的.
7.1认识哈希化
现在需要一种压缩方法, 把幂的连乘方案系统中得到的巨大整数范围压缩到可接受的数组范围中.
对于英文词典, 多大的数组才合适呢?
- 如果只有50000个单词, 可能会定义一个长度为50000的数组.
但是实际情况中, 往往需要更大的空间来存储这些单词. 因为我们不能保存单词会映射到每一个位置. (比如两倍的大小: 100000).
如何压缩呢?
- 现在, 就找一种方法, 把0到超过7000000000000的范围, 压缩为从0到100000.
- 有一种简单的方法就是使用取余操作符, 它的作用是得到一个数被另外一个数整除后的余数…
取余操作的实现: - 为了看到这个方法如何工作, 我们先来看一个小点的数字范围压缩到一个小点的空间中.
- 假设把从0~199的数字, 比如使用largeNumber代表, 压缩为从0到9的数字, 比如使用smallRange代表.
- 下标值的结果: index = largeNumber % smallRange;
- 当一个数被10整除时, 余数一定在0~9之间;比如13%10=3, 157%10=7.
当然, 这中间还是会有重复, 不过重复的数量明显变小了. 因为我们的数组是100000, 而只有50000个单词. - 就好比, 你在0~199中间选取5个数字, 放在这个长度为10的数组中, 也会重复, 但是重复的概率非常小. (后面我们会讲到真的发生重复了应该怎么解决)
我们来看看几个概念:
哈希化:
将大数字转化成数组范围内下标的过程, 我们就称之为哈希化.
哈希函数:
通常我们会将单词转成大数字, 大数字在进行哈希化的代码实现放在一个函数中, 这个函数我们成为哈希函数.
哈希表:
最终将数据插入到的这个数组, 我们就称之为是一个哈希表
7.2哈希函数实现
// 设计哈希函数
// 1.将字符串转化成比较大的数字:hashCode
// 2.将打的数字hashCode压缩到数组范围(大小)内
function hashFunc(str,size){//size数组的大小,str字符串
// 1.初始化hashCode的值
var hashCode = 0
// 2.霍纳算法, 来计算hashCode的数值
for (var i = 0; i < str.length; i++) {
//37是比较常用的一个质数,str.charCodeAt是获得对应的uncode编码
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3.取模运算
hashCode = hashCode % size
return hashCode
}
console.log(hashFunc("abc", 7)) // 4
console.log(hashFunc("cba", 7)) // 3
console.log(hashFunc("nba", 7)) // 5
console.log(hashFunc("mba", 7)) // 1
7.3 哈希表实现
7.3.1创建哈希表
// 封装哈希表类
function HashTable() {
// 属性
this.storage = []
this.count = 0 //存放数组中已经有了几个元素
this.limit = 7 //数组的容量
// 方法
// 设计哈希函数
// 1.将字符串转化成比较大的数字:hashCode
// 2.将打的数字hashCode压缩到数组范围(大小)内
HashTable.prototype.hashFunc = function (str, size) {
//size数组的大小,str字符串
// 1.初始化hashCode的值
var hashCode = 0
// 2.霍纳算法, 来计算hashCode的数值
for (var i = 0; i < str.length; i++) {
hashCode = 37 * hashCode + str.charCodeAt(i)
}
// 3.取模运算
hashCode = hashCode % size
return hashCode
}
}
7.3.2 插入/修改数据
// 插入/修改操作
HashTable.prototype.put = function (key,value){
// 1.根据传入的key获取对应的hashCode, 也就是数组的index
var index = this.hashFunc(key, this.limit)
// 2.从哈希表的index位置中取出桶(另外一个数组)
var bucket = this.storage[index]
// 3.查看上一步的bucket是否为nul
if (bucket === null) {
// 3.1创建桶
bucket = []
this.storage[index] = bucket
}
alert(bucket)
// 4.判断是新增还是修改原来的值.
//查看是否之前已经放置过key对应的value
var override = false//变量override来记录是否是修改操作
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
//如果放置过, 那么就是依次替换操作, 而不是插入新的数据.
if (tuple[0] === key) {
tuple[1] = value
override = true
}
}
// 5.如果不是修改操作, 那么插入新的数据
if (!override) {
bucket.push([key, value])
//数据增加了一项.
this.count++
}
}
7.3.3 获取操作
// 获取存放的数据
HashTable.prototype.get = function (key) {
// 1.根据key获取hashCode(也就是index)
var index = this.hashFunc(key, this.limit)
// 2.获取对应的bucket
var bucket = this.storage[index]
// 3.如果bucket为null, 那么说明这个位置没有数据
if (bucket == null) {
return null
}
// 4.有bucket, 判断是否有对应的key
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] === key) {
return tuple[1]
}
}
// 5.没有找到, return null
return null
}
7.3.4 删除数据
// 删除数据
HashTable.prototype.remove = function (key) {
// 1.获取key对应的index
var index = this.hashFunc(key, this.limit)
// 2.获取对应的bucket
var bucket = this.storage[index]
// 3.判断同是否为null, 为null则说明没有对应的数据
if (bucket == null) {
return null
}
// 4.遍历bucket, 寻找对应的数据
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] === key) {
bucket.splice(i, 1)
this.count--
return tuple[1]
}
}
// 5.来到该位置, 说明没有对应的数据, 那么返回null
return null
}
7.3.5 isEmpty(判断哈希表是否为空)
// isEmpty方法
HashTable.prototype.isEmpty = function () {
return this.count == 0
}
7.3.6 获取哈希表中数据的个数
// size方法
HashTable.prototype.size = function () {
return this.count
}
7.3.7 哈希表扩容
// 哈希表扩容
HashTable.prototype.resize = function (newLimit) {
// 1.保存旧的数组内容
var oldStorage = this.storage
// 2.重置属性
this.limit = newLimit
this.count = 0
this.storage = []
// 3.遍历旧数组中的所有数据项, 并且重新插入到哈希表中
oldStorage.forEach(function (bucket) {
// 1.bucket为null, 说明这里面没有数据
if (bucket == null) {
return
}
// 2.bucket中有数据, 那么将里面的数据重新哈希化插入
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
this.put(tuple[0], tuple[1])
}
}.bind(this))
}
那我们在什么时候调用扩容方法呢?在每次添加完新的数据时,都进行判断
修改一下put方法
// 5.如果是新增, 前一步没有覆盖
if (!override) {
bucket.push([key, value])
this.count++
// 数组扩容
if (this.count > this.limit * 0.75) {
this.resize(this.limit * 2)
}
如果我们不断的删除数组,那么数组最小也将数量先知道一半
修改remove方法
// 4.遍历bucket, 寻找对应的数据
for (var i = 0; i < bucket.length; i++) {
var tuple = bucket[i]
if (tuple[0] === key) {
bucket.splice(i, 1)
this.count--
// 缩小数组的容量
if (this.limit > 8 && this.count < this.limit * 0.25) {
this.resize(Math.floor(this.limit / 2))
}
}
return tuple[1]
}
7.3.8 容量质数
虽然在链地址法中将容量设置为质数, 没有在开放地址法中重要, 但是其实链地址法中质数作为容量也更利于数据的均匀分布.
判断质数
质数的特点:
- 质数也称为素数.
- 质数表示大于1的自然数中, 只能被1和自己整除的数.
普通方法:
function isPrime(num) {
for (var i = 2; i < num; i++) {
if (num % i == 0) {
return false
}
}
return true
}
// 测试
alert(isPrime(3)) // true
alert(isPrime(32)) // false
alert(isPrime(37)) // true
高效方法:不全部进行遍历
function isPrime(num) {
// 1.获取平方根
var temp = parseInt(Math.sqrt(num))
// 2.循环判断
for (var i = 2; i <= temp; i++) {
if (num % i == 0) {
return false
}
}
return true
}
封装获取新的容量代码
这里是为了让我们 的容量始终是一个质数,让其可以均匀分布
实现:
// 判断是否是质数
HashTable.prototype.isPrime = function (num) {
var temp = parseInt(Math.sqrt(num))
// 2.循环判断
for (var i = 2; i <= temp; i++) {
if (num % i == 0) {
return false
}
}
return true
}
// 获取质数
HashTable.prototype.getPrime = function (num) {
while (!isPrime(num)) {
num++
}
return num
}
修改插入数据代码:
// 扩容数组的数量
if (this.count > this.limit * 0.75) {
var primeNum = this.getPrime(this.limit * 2)
this.resize(primeNum)
}
修改删除数据代码
// 缩小数组的容量
if (this.limit > 7 && this.count < this.limit * 0.25) {
var primeNum = this.getPrime(Math.floor(this.limit / 2))
this.resize(primeNum)
}
8. 二叉树
8.1 二叉搜索树
概念:二叉搜索树是一颗二叉树, 可以为空;如果不为空,满足以下性质:
- 非空左子树的所有键值小于其根结点的键值。
- 非空右子树的所有键值大于其根结点的键值。
- 左、右子树本身也都是二叉搜索树。
8.1.1 创建二叉搜索树
// 创建BinarySearchTree
function BinarySerachTree() {
//封装一个用于保存每一个结点的类
function Node(key) {
this.key = key //结点对应的key
this.left = null //指向的左子树
this.right = null //指向的右子树
}
// 保存根的属性
this.root = null
// 二叉搜索树相关的操作方法
}
8.1.2 插入数据
// 向树中插入数据
BinarySerachTree.prototype.insert = function (key) {
// 1.根据key创建对应的node
var newNode = new Node(key)
// 2.判断根结点是否有值
if (this.root === null) {//第一次插入, 直接修改根结点即可.
this.root = newNode
} else {//其他次插入, 需要进行相关的比较决定插入的位置.
this.insertNode(this.root, newNode)
}
}
//insertNode用来进行比较插入的位置
BinarySerachTree.prototype.insertNode = function (node, newNode) {
if (newNode.key < node.key) { // 1.准备向左子树插入数据
if (node.left === null) { // 1.1.node的左子树上没有内容
node.left = newNode
} else { // 1.2.node的左子树上已经有了内容,递归调用直到找到对应位置插入即可
this.insertNode(node.left, newNode)
}
} else { // 2.准备向右子树插入数据
if (node.right === null) { // 2.1.node的右子树上没有内容
node.right = newNode
} else { // 2.2.node的右子树上有内容
this.insertNode(node.right, newNode)
}
}
}
8.1.3 遍历二叉搜索树
有先序遍历/中序遍历/后序遍历
先序遍历(根左右)
BinarySerachTree.prototype.preOrderTraversal = function (handler) {
//handler是用户自己传入的一个打印函数,是为了方便一次性输出所有数据
this.preOrderTranversalNode(this.root, handler)
}
BinarySerachTree.prototype.preOrderTranversalNode = function (node, handler) {
if (node !== null) {//如果根不为空
// 1.打印当前经过的节点
handler(node.key)
// 2.遍历所有的左子树,递归的方式一直调用,然后一层层返回数据结果
this.preOrderTranversalNode(node.left, handler)
// 3.遍历所有的右子树
this.preOrderTranversalNode(node.right, handler)
}
}
测试代码:
// 测试前序遍历结果
var resultString = ""
bst.preOrderTraversal(function (key) {//打印函数
resultString += key + " "
})
console.log(resultString) // 11 7 5 3 6 9 8 10 15 13 12 14 20 18 25
中序遍历(左根右)
// 中序遍历
BinarySerachTree.prototype.inOrderTraversal = function (handler) {
this.inOrderTraversalNode(this.root, handler)
}
BinarySerachTree.prototype.inOrderTraversalNode = function (node, handler) {
if (node !== null) {
//先直接找到最左边的节点,然后将其打印,然后继续向上返回
this.inOrderTraversalNode(node.left, handler)
//打印此节点
handler(node.key)
//左节点找到后,找到对应的右节点,然后递归调用,将其打印出来
this.inOrderTraversalNode(node.right, handler)
}
}
测试:
// 测试中序遍历结果
resultString = ""
bst.inOrderTraversal(function (key) {
resultString += key + " "
})
console.log(resultString) // 3 5 6 7 8 9 10 11 12 13 14 15 18 20 25
后序遍历(左右根)
// 后续遍历
BinarySerachTree.prototype.postOrderTraversal = function (handler) {
}
BinarySerachTree.prototype.postOrderTraversalNode = function (node, handler) {
if (node !== null) {//先打印左节点,然后找见对应右节点打印,最后打印根节点
this.postOrderTraversalNode(node.left, handler)
this.postOrderTraversalNode(node.right, handler)
handler(node.key)
}
}
测试:
// 测试后续遍历结果
resultString = ""
bst.postOrderTraversal(function (key) {
resultString += key + " "
})
console.log(resultString) // 3 6 5 8 10 9 7 12 14 13 18 25 20 15 11
8.1.4 最大值和最小值
根据二叉搜索树的特性:左节点永远小于右节点,所有最小值就是最左边的节点,而最大值就是最右边的节点
// 获取最大值和最小值
BinarySerachTree.prototype.min = function () {
var node = this.root
while (node.left !== null) {//循环,直到没有左节点,也就是找到最左边节点
node = node.left
}
return node.key
}
BinarySerachTree.prototype.max = function () {
var node = this.root
while (node.right !== null) {//循环,直到没有右节点,也就是找到最右边节点
node = node.right
}
return node.key
}
8.1.5 搜索值是否存在二叉搜索树中
根据用户传入的key值,不断地去和每个根节点进行比较,比根节点大则继续递归向右下查找,返之比根节点小则递归向左查找,直到找到对应值,然后返回。
// 搜索特定的值
BinarySerachTree.prototype.search = function (key) {
return this.searchNode(this.root, key)
}
BinarySerachTree.prototype.searchNode = function (node, key) {
// 1.如果传入的node为null那么, 那么就退出递归
if (node === null) {
//node === null, 也就是后面不再有节点的时候.说明没有找到直接返回false即可
return false
}
// 2.判断node节点的值和传入的key大小
if (node.key > key) { //node.key > key, 那么说明传入的值更小, 需要向左查找.
return this.searchNode(node.left, key)
} else if (node.key < key) { // 如果node.key < key, 那么说明传入的值更大, 需要向右查找
return this.searchNode(node.right, key)
} else { // 2.3.相同, 说明找到了key
return true
}
}
8.1.6 删除节点
第一种方法(简单)
在Node类中添加一个boolean的字段, 比如名称为isDeleted.
- 要删除一个节点时, 就将此字段设置为true.
- 其他操作, 比如find()在查找之前先判断这个节点是不是标记为删除.
- 这样相对比较简单, 每次删除节点不会改变原有的树结构.
- 但是在二叉树的存储中, 还保留着那些本该已经被删除掉的节点.
第二种方法
首先我们需要找到对应删除的节点,然后考虑已下三种情况:
- 该节点是叶子结点
- 该节点有一个子节点
- 该节点有两个子节点.
第一步:先查找要删除的节点
// 删除结点
BinarySerachTree.prototype.remove = function (key) {
// 1.定义临时保存的变量
var current = this.root //保存要删除的节点,默认从根节点开始
var parent = null
var isLeftChild = true //记录current是在父节点的左侧还是右侧,便于等会设置left和right进行删除操作
// 2.开始查找节点
while (current.key !== key) {
parent = current
if (key < current.key) { //那么就查找左侧
isLeftChild = true //说明是左节点
current = current.left
} else { //那么就查找右侧
isLeftChild = false
current = current.right
}
// 如果发现current已经指向null, 那么说明没有找到要删除的数据
if (current === null) return false
}
return true
}
第二步:现在已经找见了需要删除的节点,根据不同情况删除
- 删除的节点是叶子节点(没有子节点)
// 3.删除的结点是叶结点
if (current.left === null && current.right === null) {//如果是叶子节点
if (current == this.root) {//如果查找值等于根节点,设置为空
this.root == null
} else if (isLeftChild) {//在左边就将父节点的左节点设为空
parent.left = null
} else {//在右边就将父节点的右节点设为空
parent.right = null
}
}
- 删除的节点有一个子节点
// 4.删除有一个子节点的节点
//只有一个左节点情况
else if (current.right === null) {
if (current == this.root) {//如果current等于根节点
this.root = current.left //将根节点设置为left节点
} else if (isLeftChild) { //如果是删除左节点
//将parent的left设置为current的left
parent.left = current.left
} else {//否则说明删除的是右节点
parent.right = current.left
}
//只有一个右节点情况
} else if (current.left === null) {
if (current == this.root) {
this.root = current.right
} else if (isLeftChild) {
parent.left = current.right
} else {
parent.right = current.right
}
}
- 删除的节点有两个子节点
删除这个节点,其实就是找下面的节点进行替换上去就可,具体应该这样找 - 比current小一点点的节点, 一定是current左子树的最大值.
- 比current大一点点的节点, 一定是current右子树的最小值.
前驱&后继
而在二叉搜索树中, 这两个特别的节点, 有两个特比的名字. - 比current小一点点的节点, 称为current节点的前驱.
- 比current大一点点的节点, 称为current节点的后继.
也就是为了能够删除有两个子节点的current, 要么找到它的前驱, 要么找到它的后继.
寻找后继:
// 找后继的方法
BinarySerachTree.prototype.getSuccessor = function (delNode) {
// 1.使用变量保存临时的节点
var successorParent = delNode
var successor = delNode
var current = delNode.right // 要从右子树开始找
// 2.寻找节点
while (current != null) {
successorParent = successor
successor = current
current = current.left
}
// 3.如果是删除图中15的情况, 还需要如下代码
if (successor != delNode.right) {
successorParent.left = successor.right
successor.right = delNode.right
}
return successor
}
找到后继之后处理:
// 5.删除有两个节点的节点
else {
// 1.获取后继节点
var successor = this.getSuccessor(current)
// 2.判断是否是根节点
if (current == this.root) {
this.root = successor
} else if (isLeftChild) {
parent.left = successor
} else {
parent.right = successor
}
// 3.将删除节点的左子树赋值给successor
successor.left = current.left
}
9. 红黑树
首先说一下为什么要使用红黑树:
二叉搜索树有个问题:就是如果插入的数据是有序的情况下,就会在某一方向一直衍生下去,深度大,导致查询效率慢,这种情况也叫非平衡二叉树,插入的寻找的效率由平衡二叉树O(logN)变成)O(N),相当于编写了一条链表.
如下:
所以为了保证查询的效率,要尽量保证左边节点数接近右边节点数,这时候有了红黑树,用来进行分布,让其均匀的分布,重而提高查找效率
上图是二叉搜索树,现在我们看一下红黑树实现有序数据插入的效果
那么下面就让我们来看一下红黑树
的实现:
9.1 红黑树的规则:
红黑树的相对平衡:
9.2 红黑树的变换
9.2.1 变色
9.2.2 旋转
9.3 插入操作的五种情况
插入操作分成5种情况来实现:
插入的新节点N一般都为红色节点,这样是为了方便减少冲突。
首先为了方便下面的书写我们规定三个变量:
父节点:P 祖节点:G 叔节点(也就是父亲节点的兄弟节点):U
如下图所示:
- 情况一:新节点N位于树的根,将红色变黑色(因为根据性质2,根节点为黑色,而我们新插入的节点默认全为红色)
- 情况二:新节点的父节点是黑色,那么直接插入即可,不需要操作
- 情况三:父红叔红祖黑 -> 父黑叔黑祖红
- - 情况四:父红叔黑祖黑(且N是左节点) -> 父黑祖红右旋转
- - 情况五: 父红叔黑祖黑(且N是右节点) -> 以p为根,左旋转,将P作为新插入的红色节点考虑,自己N变黑色,祖变红色,以祖为根,进行右选择
9.4 案例:依次插入10 9 8 7 6 5 4 3 2 1
结合上方的情况和规则进行对应插入变换着色
这样就完成了红黑树,主要是记住规则和五种情况变换,达到尽可能的时间复杂度O(logN)而不是非平衡二叉树的O(N)
end~
10. 图
图的特点:
- 一组顶点:通常用 V (Vertex) 表示顶点的集合
- 一组边:通常用 E (Edge) 表示边的集合
- 边可以是有向的, 也可以是无向的.(比如A — B, 通常表示无向. A --> B, 通常表示有向)
- 相邻顶点
- 由一条边连接在一起的顶点称为相邻顶点.
- 比如0 - 1是相邻的, 0 - 3是相邻的. 0 - 2是不相邻的
- 度:一个顶点的度是相邻顶点的数量.
现实中的使用:对交通路线的建模
10.1 图的表示
图的顶点可以抽象成一个数组保存A,B,C,D
10.1.1 邻接矩阵
用一个二维数组来表示顶点之间的连接情况,0代表没有连接,1代表有连接,如果是带权路径的话可以把1换成权值即可
但是它有一个缺点:
如果图是一个稀疏图的话,矩阵中将存在大量的0,浪费空间
10.1.2邻接表
邻接表由图中每个顶点以及和顶点相邻的顶点列表组成.
我们下面使用邻接表的方式
上图表示,与A有关的节点全部现实,就是下标对应顶点,然后每个下标对应的是一个数组,存放连接的节点信息
10.2 图的封装
这里利用的是字典类型,也就是键值对的方式去存储数据,一个下标key对应一个【】,上方我们有关于字典的代码直接引用即可。
function Graph() {
// 属性
this.vertexes = [] // 存储顶点,用数组形式
this.adjList = new Dictionay() // 存储边,Dictionay是我们封装的字典类型
// 方法
}
10.2.1 添加方法
添加顶点的实现:
// 添加方法
Graph.prototype.addVertex = function (v) {
//将添加的节点放入数组
this.vertexes.push(v)
//给该顶点对应的空数组,因为默认刚插入的顶点有空数组存放边
this.adjList.set(v, [])
}
添加边:添加边需要传入两个顶点, 因为边是两个顶点之间的边, 边不可能单独存在.
//传入的两个顶点,也就是边因为我们这里是无向图,所以两个顶点对应都要添加
Graph.prototype.addEdge = function (v, w) {
//查找对应顶点v,将w放到它的数组中
this.adjList.get(v).push(w)
//查找对应顶点w,将v放到它的数组中
this.adjList.get(w).push(v)
}
测试:
// 测试代码
var graph = new Graph()
// 添加顶点
var myVertexes = ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
//循环加入顶点,这样可以创建每一个数组对应
for (var i = 0; i < myVertexes.length; i++) {
graph.addVertex(myVertexes[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');
效果如下:
10.2.2 toString
遍历显示图的结果
Graph.prototype.toString = function () {
//定义一个resultStr 用于返回
var resultStr = ""
//for循环,拿到每一个key值
for (var i = 0; i < this.vertexes.length; i++) {
//将第一次对应key放入
resultStr += this.vertexes[i] + "->"
//根据本次key找到对应数组
var adj = this.adjList.get(this.vertexes[i])
//遍历数组
for (var j = 0; j < adj.length; j++) {
//添加进字符串
resultStr += adj[j] + " "
}
//遍历完一个key加个换行
resultStr += "\n"
}
return resultStr
}
结果如下:
10.3 图的遍历
思想:
在于必须访问每个第一次访问的节点, 并且追踪有哪些顶点还没有被访问到.
两种方法:
- 广度优先搜索(BFS): 基于队列, 入队列的顶点先被探索.
- 深度优先搜索(DFS):基于栈, 通过将顶点存入栈中, 顶点是沿着路径被探索的, 存在新的相邻顶点就去访问
为了记录顶点是否被访问过, 我们使用三种颜色来反应它们的状态:(或者两种颜色也可以)
- 白色: 表示该顶点还没有被访问.
- 灰色: 表示该顶点被访问过, 但并未被探索过.
- 黑色: 表示该顶点被访问过且被完全探索过.
初始化颜色代码:
// 初始化颜色代码
Graph.prototype.initializeColor = function () {
var colors = [] //定义一个数组接收颜色
//遍历所有顶点
for (var i = 0; i < this.vertexes.length; i++) {
//默认情况每个顶点对应颜色为white
colors[this.vertexes[i]] = "white"
}
return colors
}
10.3.1 广度优先搜索
图解思路:由上到下访问每层,一层一层的访问下去遍历
Graph.prototype.bfs = function (v, handler) {
// 1.初始化颜色
var color = this.initializeColor()
// 2.创建队列
var queue = new Queue()
// 3.将传入的顶点放入队列中
queue.enqueue(v)
// 4.从队列中依次取出和放入数据
while (!queue.isEmpty()) {
// 4.1.从队列中取出数据
var qv = queue.dequeue()
// 4.2.获取qv相邻的所有顶点
var qAdj = this.adjList.get(qv)
// 4.3.将qv的颜色设置成灰色
color[qv] = "gray"
// 4.4.将qAdj的所有顶点依次压入队列中
for (var i = 0; i < qAdj.length; i++) {
var a = qAdj[i]
if (color[a] === "white") {
color[a] = "gray"
queue.enqueue(a)
}
}
// 4.5.因为qv已经探测完毕, 将qv设置成黑色
color[qv] = "black"
// 4.6.处理qv
if (handler) {
handler(qv)
}
}
}
测试代码:
// 调用广度优先算法
var result = ""
graph.bfs(graph.vertexes[0], function (v) {
result += v + " "
})
alert(result) // A B C D E F G H I
10.3.2 深度优先搜索
思路
深度优先搜索算法将会从第一个指定的顶点开始遍历图, 沿着路径知道这条路径最后被访问了.
接着原路回退并探索吓一条路径.
// 深度优先搜索
Graph.prototype.dfs = function (handler) {
// 1.初始化颜色
var color = this.initializeColor()
// 2.遍历所有的顶点, 开始访问
for (var i = 0; i < this.vertexes.length; i++) {
if (color[this.vertexes[i]] === "white") {
this.dfsVisit(this.vertexes[i], color, handler)
}
}
}
// dfs的递归调用方法
Graph.prototype.dfsVisit = function (u, color, handler) {
// 1.将u的颜色设置为灰色
color[u] = "gray"
// 2.处理u顶点
if (handler) {
handler(u)
}
// 3.u的所有邻接顶点的访问
var uAdj = this.adjList.get(u)
for (var i = 0; i < uAdj.length; i++) {
var w = uAdj[i]
if (color[w] === "white") {
this.dfsVisit(w, color, handler)
}
}
// 4.将u设置为黑色
color[u] = "black"
}
11. 常见的排序算法
详细注释在代码中体现
11.1 冒泡排序
思路:
1.比较相邻的两个元素,如果前一个比后一个大,则交换位置。双层for循环
2.第一轮的时候最后一个元素应该是最大的一个。
3.按照步骤一的方法进行相邻两个元素的比较,这个时候由于最后一个元素已经是最大的了,所以最后一个元素不用比较。
function maopao(arr){
for(var i = 0; i<arr.length; i++){
for(var j = 0; j<arr.length; j++){
if(arr[j] > arr[j+1]){ //找到较大的值进行交换到后面
var temp = arr[j]
arr[j] = arr[j+1]
arr[j+1] = temp
}
}
}
}
var arr = [3,4,2,1,5,6,8,6]
maopao(arr)
console.log(arr);
11.2 选择排序
选择排序---从第一个开始在后面比较,找出最小值进行交换
function xuanZe(arr){
for(let i = 0; i < arr.length; i++){
let minIndex = i //定义一个最小索引
//从第二个开始比较
for(let j = i+1; j < arr.length; j++){
//如果后一个比第一个数大,就把后面的索引赋值给最小索引
if(arr[minIndex] > arr[j]){
minIndex = j
}
}
let temp = arr[i] //交换索引对应位置的值
arr[i] = arr[minIndex]
arr[minIndex] = temp
// [arr[i],arr[minIndex]] = [arr[minIndex],arr[i]]
}
}
var arr = [4,3,6,7,1,2,8,5]
xuanZe(arr)
console.log(arr);
11.3 快速排序
快速排序是对冒泡排序的一种改进,第一趟排序时将数据分成两部分,一部分比另一部分的所有数据都要小。
然后递归调用,在两边都实行快速排序。
左右各一列,左边放小的,右面放大的;不停的划分的过程
var arr = [2,4,3,6,5]
function quickSort(arr){
if(arr.length <= 1){
return arr
}
// 将数组进行分两边,左边数组小,右边大,然后有一个中间值,一直拆分,递归返回最终结果
let middle = Math.floor(arr.length / 2)
console.log(middle);
let middleData = arr.splice(middle,1)[0] //得到中间值
console.log(middleData);
let left = []
let right = []
// 循环比大小
for(let i = 0; i < arr.length; i++){
if(arr[i] < middleData){
left.push(arr[i])
}else{
right.push(arr[i])
}
}
return quickSort(left).concat([middleData],quickSort(right))
}
console.log(quickSort(arr));
11.4 插入排序
前后比较,如果前面值比后面大,那么把后面值取出来,依次让前面值向后移,移到后面值位置,
然后再把后面值放到前面的空位上
function insertSort(arr) {
// 默认第一个排好序了,从第一个数据开始获取数据,向前面的有序插入
for (var i = 1; i < arr.length; i++) {
// 内层循环:获取i位置的元素,和前面的数据依次比较
var temp = arr[i] //后一个值赋值给temp
var j = i
while (j > 0 && arr[j - 1] > temp) {//前一个值一旦大于后一个值,如果前一个值小于后一个值则结束循环
arr[j] = arr[j - 1] //则后一个值得位置就被前一个值所代替
j-- //再次向前查找
}
// 把j位置这个空出来的位置放上temp后面这个值,这就叫插入排序
arr[j] = temp
}
}
var Arr = [3, 5, 74, 64, 64, 3, 1, 8, 3, 49, 16, 161, 9, 4]
console.log(Arr, "before");
insertSort(Arr)
console.log(Arr, "after");
11.5 希尔排序
希尔排序---升级的插入排序,以间隔来对比数据,先从一半的间隔开始对比然后进行插入排序,
直到为1再进行一次插入排序即可
function shellSort(arr) {
// gap 即为增量
//首先定义一个间隔gap,每次循环间隔变成前面的一半
for (let gap = Math.floor(arr.length / 2); gap > 0; gap = Math.floor(gap / 2)) {
for (let i = gap; i < arr.length; i++) { //定义i为间隔数,也就是中间值的索引
let cur = arr[i] //定义cur为这个间隔中间值
let j = i //保存一下这个i值,方便下面进行插入
//j-gap>=0保证不会发生前面越界的情况,如果后一个值小于前一个值
while (j - gap >= 0 && cur < arr[j - gap]) {
arr[j] = arr[j - gap] //把前面的值移动到后面这个值的位置
j -= gap // 得到前一个值
}
arr[j] = cur //把中间值给了前一个值
}
}
return arr
}
var arr = [3,2,1,4]
console.log(shellSort(arr));
11.6 归并排序
function mergeSort(arr) {
if (arr.length < 2) return arr
let middle = Math.floor(arr.length / 2);
let left = arr.slice(0, middle);
let right = arr.slice(middle)
return merge(mergeSort(left), mergeSort(right))
}
function merge(left, right) {
let result = []
while (left.length > 0 && right.length > 0) {
if (left[0] >= right[0]) {
result.push(right.shift())
} else {
result.push(left.shift())
}
}
while (left.length) {
result.push(left.shift())
}
while (right.length) {
result.push(right.shift())
}
return result
}
var arr = [3,2,7,5]
console.log(mergeSort(arr));