【数据结构与算法】用JS普通对象手撸一个双端队列

972 阅读4分钟

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

介绍

双端队列是一种将栈的原则和队列的原则混合在一起的数据结构,即头部和尾部都可以添加或删除。本期我们将用js普通对象不借助数组,去实现一个双端队列类,以便加深对此数据结构的认识。

概念

双端队列(deque,或称double-ended queue)是一种允许我们同时从前端和后端添加和移除元素的特殊队列。它同时遵守了先进先出和后进先出原则,也可以说它是把队列和栈相结合的数据结构。

正文

1.基础结构

我们先看一下,要实现的大体结构与其方法:

const items = new WeakMap();
const count = new WeakMap();
const delCount = new WeakMap();
class Deque {
    constructor() {
        items.set(this, {})
        count.set(this, 0)
        delCount.set(this, 0)
        return this;
    }
    addBefore(item) {}
    addAfter(item) {}
    peekBefore() {}
    peekAfter() {}
    removeBefore() {}
    removeAfter() {}
    clear() {
        items.set(this, {});
        count.set(this, 0);
        delCount.set(this, 0);
    }
    get() {
        return {
            ...items.get(this),
            length: this.size()
        }
    }
    size() {
        return count.get(this) - delCount.get(this);
    }
}

export default Deque;

我们使用WeakMap这个ES6新数据结构来完成队列的私有变量了,保护其内部的参数不被外界破坏。我们在这里创建三个私有变量分别为items元素存储器与count计数器还有delCount删除收集计数器,至于后面的几个方法为:

  • size:返回当前队列的元素个数。
  • get:返回一个当期队列所有元素和长度值。
  • clear:清除队列所有的元素。
  • addBefore&addAfter:分别在前后注入一个元素,并返回当前队列的长度。
  • peekBefore&peekAfter:分别在最前和最后获取元素,并返回这个元素的值。
  • removeBefore&removeAfter:分别在最前和最后抹除元素,并返回这个元素的值。

其中size,clear,get方法与上一期的手撸队列完全一致这里不做赘述,接下来我们先看一下具体实现吧。

2.addBefore&addAfter

addBefore(item) {
    let _delCount = delCount.get(this);
    let _items = items.get(this)
    let _count = count.get(this);
    if (this.size() === 0) {
        this.addAfter(item);
    }
    else if (_delCount > 0) {
        _delCount -= 1;
        _items[_delCount] = item;
        delCount.set(this, _delCount)
    }
    else{
        for (let i = _count; i > 0; i--) {  
            _items[i] = _items[i-1] 
        }
        _items[0] = item;
        items.set(this, _items)
        count.set(this, _count+1)
    }
    return this.size()
}
addAfter(item) {
    let _items = items.get(this)
    let _count = count.get(this);
    _items[_count] = item;;
    items.set(this, _items)
    count.set(this, _count+1)
    return this.size()
}

我们先说addAfter吧,很简单,我们就在count数量上加一,并将传入的元素,赋值到其位置上,就完了操作。而addBefore方法,会出现三种状况,第一种,就是本身队列是空的,那么我们直接让他走addAfter方法,比较如果count出现负数是不好看的。第二种则是之前删除过元素的情况(即delCount>0),相当于在之前删除的位置给其赋值,再delCount自然让其减一来完成。最后一种则为,该队列不是空的之前也没删除过值的记录(即delCount=0),我们会用for循环往下一层推当前层的值,最后给第0项赋值,并保存来完成的。

接下来,我们使用一下它:

import Deque from "./js/Deque"
const deque= new Deque();
console.log('addBefore->',deque.addBefore(1));
console.log('get->',deque.get());
console.log('addAfter->',deque.addAfter(2));
console.log('get->',deque.get());
console.log('addBefore->',deque.addBefore(0));
console.log('get->',deque.get());
console.log('addAfter->',deque.addAfter(3));
console.log('get->',deque.get());

我们分别在队列的前后各增加了两条数据,总共是4条,可以看到下面的结果是成功的。

微信截图_20211111205329.png

3.peekBefore&peekAfter

peekBefore() {
    if(this.size()==0) return undefined
    return items.get(this)[delCount.get(this)]
}
peekAfter() {
    if(this.size()==0) return undefined
    return items.get(this)[count.get(this)-1]
}

peek只读取首和尾的值,很简单,peekBefore就读delCount的值,如果你未删除那么delCount就是0了,而peekAfter是读从count下一个值然后减去一即为最后一项了,这样就可以取出当前首和尾了。

接下来,我们使用一下它:

console.log('get->',deque.get());
console.log('peekBefore->',deque.peekBefore())
console.log('peekAfter->',deque.peekAfter())

这里我们看到队列的数据是值分别为0,1,2,3四个值,所以我们会分别取出0和3的,在下面可以看到是符合要求的。

微信截图_20211111212514.png

4.removeBefore&removeAfter

removeBefore() {
    if(this.size()==0) return undefined
    let _items = items.get(this);
    let _delCount = delCount.get(this);
    let item = _items[_delCount];
    delete _items[_delCount]
    items.set(this,_items);
    delCount.set(this,_delCount+1);
    return item;
}
removeAfter() {
    if(this.size()==0) return undefined
    let _items = items.get(this);
    let _count = count.get(this);
    _count-=1;
    let item = _items[_count];
    delete _items[_count]
    items.set(this,_items);
    count.set(this,_count)
    return item
}

与peek类似,只是remove会移除掉当前值,并且分别在会在delCount与count基础上做出对应变化,来给其计数。当然别忘了还要更新下刚才处理的元素。

接下来,我们使用一下它:

console.log('get->',deque.get());
console.log('removeBefore->',deque.removeBefore())
console.log('get->',deque.get());
console.log('removeAfter->',deque.removeAfter())
console.log('get->',deque.get());

我们会移除首位两个元素的值即0和3,期望是最后所剩下只有1和2,长度是2,下面可以看到也是符合期望的。

微信截图_20211111212611.png

结语

本次我们学习实现了队列的一个变种——双端队列。他就像你在排队,售票员在队伍头有一个,而且在队伍尾也有一个。在程序中,可以做那种文件撤销回退那种功能,当然在前端写js完全可以用数组代替,但是,本次着重还是以思想为前提,去完成案例,而非单纯的实现。