两种基本数据结构-栈和队列

208 阅读2分钟

栈数据结构

栈数据结构是一种遵从后进先出(LIFO) 原则的有序集合, 新添加或需要被删除的元素都是在栈顶,另一端叫栈底。在生活中的例子有汉诺塔,这是完全符合栈进出原则的例子。在撤回中也是基本符合这个原则的。

src=http___img-blog.csdnimg.cn_20210308195236106.gif&refer=http___img-blog.csdnimg.gif

使用对象来实现栈

这里就不使用JS的数组来实现栈,因为数组是元素的有序的集合,为了保证元素排列有序,它会占用更多的空间,而且数组的时间复杂度大多数都是O(n)。我们直接使用对象来模拟数组。

声明一个Stack

// stack.js
class Stack {
    constructor () {
        this.items = {}
        this.count = 0
    }
    //方法
}

在这个类中,count用来帮助记录栈的长度,增加、删除元素

栈元素的插入

在数组中,需要向尾部插入的话,限制只可以插入一个。

    push (element) {
        this.items[this.count] =element
        this.count ++ // 记录栈的长度、为下一个插入\弹出做准备
    }

现在插入两个元素检查方法是否正确

const stack = new Stack()
stack.push('Jack')
stack.push('Mimi')
console.log(stack) // Stack { items: { '0': 'Jack', '1': 'Mimi' }, count: 2 }

可以看到元素以键值对的格式出现,符合数组的基本格式,并同时返回了栈的长度。

栈元素的弹出

由于count的值是栈的长度和下一个插入元素的键。所以在弹出时,需要count-1

    pop () {
        this.count --
        const result = this.items[this.count]
        delete this.items[this.count]
        return result
    }

但实际这个代码是存在一定问题的,栈是空的时候会报错,所以还需要检查栈是否为空。

栈的大小和是否为空

大小我们可以返回count的值,是否为空直接利用count的值计算

    size () {
        return this.count
    }
    isEmpty  () {
        return this.count === 0
    }

查看栈顶值和栈清空

    peek () {
        if ( this.isEmpty() ){
            return undefined
        }
        return this.count[this.count-1]
    }
    clear () {
        this.items = {}
        this.count = 0
    }

当然清空也可以保证LIFO原则

   while( !this.isEmpty() )
    {
        this.pop()
    }

实现 toString 方法

可以利用字符串拼接和循环来实现

toString() { 
    if (this.isEmpty()) { 
        return ''
    } 
    let objString = `${this.items[0]}`
    for (let i = 1; i < this.count; i++) { 
        objString = `${objString},${this.items[i]}`
    } 
    return objString
   } 

最后,回顾以下我们实现的方法,除了toString方法,其他方法的复杂度均为O(1),表示我们一次就可以直接找到目标元素和对其操作。

队列数据结构

队列数据结构遵循的先进先出(FIFO) 原则的集合。在队尾插入新元素,在队头弹出元素。目前最熟悉的场景莫过于排队做核酸/(ㄒoㄒ)/~~ 。

现在利用对象来模拟队列。

 class Queue{
    constructor () {
        this.count = 0  // 辅助确认队尾
        this.lowestCount = 0 // 确认队头
        this.items = {}
    }
    // 方法
 }   

仍然需要count,但这里面的count跟栈的有一定的区别,单靠count不再能计算队列的长度,但可以指向队尾的下一个元素。还需要loestCount它前指向的队列头元素。所以队列的长度是count - lowerCount

添加元素

跟栈的相同,都是从尾部添加, 所以只有count有变化,lowerCount不需要变化。

    enqueue (item) {
        this.items[this.count] = item
        this.count ++ 
    }

检查队列长度和是否为空

队列长度计算count - lowerCount

    size () {
        return this.count - this.lowestCount
    }
    isEmpty () {
        return this.size() === 0
    }

弹出队列头元素

因为当队头弹出时,lowestCount不再指向弹出的队头,而是弹出队头的下一位,所以需要lowestCount需要+1

    dequeue () {
        if (this.isEmpty()){
            return undefined
        }
        const result = this.items[this.lowestCount]
        delete this.items[this.lowestCount]
        this.lowestCount ++
        return result
    }

实现 toString 方法

    toString () {
        if (this.isEmpty()){
            return undefined
        }
        let word = this.items[this.lowestCount]
        for (let i = this.lowestCount+1 ; i<this.count; i++ ){
            word = `${word},${this.items[i]}`
        }
        return word
    }

从代码来看,队列因为比栈多了lowestCount属性,所以栈可以做到的事情,队列都是做到的。下一篇将讨论双端队列,一种可以允许我们同时从前端和后端添加和移除元素的特殊队列。