前言:为什么要学数据结构,它能帮助我们什么?能解决什么问题呢,首页数据结构并不是一门具体的编程语言,它教会我们的是一种思维方式,即如何以更优的方式存储数据和解决的一些问题,通过学习数据结构,可以大大拓宽我们的思维模式。掌握了数据结构与算法,我们看待问题的深度、解决问题的角度会大有不同,对于个人逻辑思维的提升,也是质的飞跃。

下面从以下几个点出现逐一的了解他们实现的原理和场景:

  • 什么是栈
  • 什么是队列
  • 什么是链表
  • 什么是集合
  • 什么是字典
  • 什么 二叉树

什么是栈

在介绍的时候 我们需要了解数组的概念、以及数组常用的方法是什么 才能更好的了解栈的原理是什么。

ArrayJavaScript 数组用于在单一变量中存储多个值,本质上是对象。

如何创建数组:

let arr1 = [1,2,3] // [element0, element1, ..., elementN]
let arr2 = new Array(1,2,3) // new Array(element0, element1[, ...[, elementN]])
let arr3 = new Array(3); // new Array(arrayLength)

数组的常用方法有哪些:

console.log(Object.getOwnPropertyNames(Array.prototype);)
// ["length", "constructor", "concat", "copyWithin", "fill", "find", "findIndex", "lastIndexOf", "pop", "push", "reverse", "shift", "unshift", "slice", "sort", "splice", "includes", "indexOf", "join", "keys", "entries", "values", "forEach", "filter", "flat", "flatMap", "map", "every", "some", "reduce", "reduceRight", "toLocaleString", "toString"]

来几个栗子热热身

const arrs = ['篮球','足球','乒乓球'];

访问数组元素

console.log(arrs[0])    // 篮球
console.log(arrs[arrs.length - 1])    // 乒乓球

添加元素到数组的末尾

const arrs = ['篮球', '足球', '乒乓球'];
arrs.push('羽毛球')
console.log(arrs)    // ["篮球", "足球", "乒乓球", "羽毛球"]

栈的原则:后进先出 (LIPO)

新添加的或者删除的元素都在栈顶,另一端叫栈底,在栈里,新元素都靠近栈顶,旧元素都接近栈顶,可以把试想它就是一摞书、或者是一摞盘子都可以、只要能理解其含义即可。

image.png

那么如何实现一个栈呢?

首先我们要考虑几点:栈都有那些方法或者都有什么作用:

  • push():添加一个或者多个新元素
  • pop():移除栈顶的元素、同时返回被移除的元素
  • peek():返回栈顶的元素、不会对栈做任何的修改
  • isEmpty():判断栈是否为空的状态、返回布尔值
  • clear():清空栈的元素
  • size():返回栈的元素个数

实现一个Stack栈:

    class Stack {
        constructor () {
            this.items = []
        }

        push(data){
            this.items.push(data)
        }

        pop() {
            return this.items.pop()
        }

        peek() {
            return this.items[this.items.length - 1]
        }

        isEmpty(){
            return this.items.length === 0
        }

        size () {
            return this.items.length
        }

        clear() {
            this.items = []
        }
    }

测试:

const foo = new Stack()
foo.push(1)  // [1]
foo.push(2)  // [1,2]
foo.push(3)  // [1,2,3]
foo.push(4)  // [1,2,3,4] 
foo.pop()    // [1,2,3]
foo.clear()  // []

以上是简单的实现方式。

不过在数组的操作如果存在大量的操作数据这可能不是最高效的、大部分的方法的时间复杂度是O(n),意思就是要迭代整个数组知道找到那个元素、n代表数组的长度、如果数组有很多的元素、那么所需的时间会更长,那么有没有更好的方式来实现呢?

答:有的使用对象代替存储、同时声明变量记录当前栈的大小。

class Stack {
    constructor () {
        this.items = {}
        this.count = 0
    }

    push(data){
        this.items[this.count] = data
        this.count ++ 
    }

    pop() {
        if(this.isEmpty()) {
            return undefined
        }

        this.count -- 
        const result = this.items[this.count]
        delete this.items[this.count]
        return result
    }

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

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

    size () {
        return this.count
    }

    clear() {
        this.items = {}
        this.count = 0
    }
}

其实以上的版本还不是很完美、如果我们直接改变数据的结构或对象时就会导致数据结构的混乱吗,比如:

const foo = new Stack()
foo.items = null
foo.push(1) // TypeError

JavaScript 没有私有属性的概念、但是可以通过Symbol属性进行模拟、或者使用WeakMap数据结构进行模拟、请参考以下简单的代码示例:

模拟私有属性Symbol

const _items = Symbol('item') // 声明一个key
class Stack {
    constructor () {
        // 声明约定下划线命名为【私有属性】
        this[_items] = {}
        this._count = 0
    }

    push(data){
        this[_items][this._count] = data
        this._count ++ 
    }
    ...
}

模拟私有属性WeakMap

    const _items = new WeakMap()

    class Stack {
        constructor () {
            // 以Stack为自己的引用为键值,存储items
            _items.set(this,[])
        }

        push(data){
           const list = _items.get(this)
           list.push(data)
        }

        pop(){
            const list = _items.get(this)
            const result = list.pop()
            return result
        }

        ...
}

以上其实都不是很完美,但是鱼和熊掌不可兼得,还是根据自己的应用场景做一些取舍。

用栈能解决什么问题?

参考力扣的原题为例,试着用栈的思维来解决

来自力扣20题有效的括号

image.png

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function(s) {
    let stack = []

    let left = ['(','[',"{"]

    for (let i = 0; i< s.length; i++) {

    let ch = s[i]

    if(left.includes(ch)) {

        stack.push(ch)
    }

    if(stack.length === 0) return false

    if (ch == ')' && stack.pop() != '(') return false
    if (ch == ']' && stack.pop() != '[') return false
    if (ch == '}' && stack.pop() != '{') return false
    }

    return  stack.length === 0
};

什么是队列

队列遵循先进先出(FIFO)原则的一组有序的项,队列在尾部添加新元素、并从顶部移除元素、最新添加的元素必须在队列末尾。常见的理解队列:排队买票

队列常用的方法:

  • enqueue():添加一个或者多个新元素。
  • dequeue():移除队列的第一项(即排在队列最前面的项)并返回被移除的元素。
  • peek():返回队列中第一个元素——最先被添加,也将是最先被移除的元素。
  • isEmpty():判断队列是否为空的状态、返回布尔值。
  • clear():清空队列的元素。
  • size():返回队列的元素个数。

实现一个Queue队列:

class Queue {
    constructor(){
        this.count = 0; // 记录对象的size
        this.lowestCount = 0;   // 跟踪第一个元素
        this.items = {} // 初始化对象
    }
    enqueue(data) {
        this.items[ this.count ] = data
        this.count ++ 
    }

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

    peek(){
        if(this.isEmpty()) {
            return undefined
        }  

        return this.items[this.lowestCount]
    }

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

    size () {
        return this.count - this.lowestCount
    }

    clear(){
        this.count = 0; 
        this.lowestCount = 0;  
        this.items = {}
    }
}

队列的应用场景:可以试想一个javascript的事件循环、其实底层就是遵从先进先出的实现原理。

进阶版:双端队列

允许我们同时在前端和后端添加和移除的元素的特殊队列,细心的童鞋可以实现的方式其实是融合了队列的方法。请看以下代码:

class Deque {
    constructor(){
        this.count = 0; // 记录对象的size
        this.lowestCount = 0;   // 跟踪第一个元素
        this.items = {} // 初始化对象
    }
    // 在队列前端添加元素
    addFront(data) {
        if(this.isEmpty()) {
            this.addBack(data)
        }else if( this.lowestCount > 0) {
            this.lowestCount -- 
            this.items[ this.lowestCount] = data
        }else {
            for (let i = this.count; i > 0 ; i--) {
                this.items[i] = this.items[ i - 1]
            }
            this.count ++;
            this.lowestCount = 0;
            this.items[0] = data
        }
        
    }
    // 在队列后端添加元素
    addBack(data) {
        this.items[ this.count ] = data
        this.count ++ 
    }

    // 在队列后端删除元素
    removeBack() {
        if(this.isEmpty()) {
            return undefined
        }

        this.count -- 
        const result = this.items[this.count]
        delete this.items[this.count]
        return result
    }

    // 在队列前端删除元素
    removeFront(){
        if(this.isEmpty()) {
            return undefined
        }

        const result = this.items[ this.lowestCount]
        delete this.items[this.lowestCount] 
        this.lowestCount ++ 
        return result
    }

    // 返回在队列前端第一个元素
    peekBack() {
        if(this.isEmpty()) {
            return undefined
        }
        return this.items[ this.count - 1]
    }

    // 返回在队列后端第一个元素
    peekFront(){
        if(this.isEmpty()) {
            return undefined
        }  

        return this.items[this.lowestCount]
    }

    // 是否回空
    isEmpty(){
        return this.size() === 0
    }

    // 队列大小
    size () {
        return this.count - this.lowestCount
    }

    // 清除队列
    clear(){
        this.count = 0; 
        this.lowestCount = 0;  
        this.items = {}
    }
}

通过一个例子在深入了解一下它的底层原理实现:

验证简单的回文数

let strings = `madam`

function isPalindrome(aString) {
    
    const deque = new Deque()
    const lowerString = aString.toLocaleLowerCase().split(' ').join('')

    let isEqual = true;
    let firstChar, lastChar;

    for (let i = 0; i < aString.length; i++) {
        deque.addBack(lowerString.charAt(i))
    }

    while (deque.size() > 1 && isEqual) {

        firstChar = deque.removeFront()
        lastChar = deque.removeBack()

        if(firstChar !== lastChar) {
            isEqual = false
        }
    }

    return isEqual
}

以上就是内容介绍了JavaScript的最简单的数据结构,其实万变不离其宗只要从底层掌握了数据结构的各种演变的形态,利用它们的特性和实现的原理,那么遇到一些难解的问题的时候就有很多的发散性想法,当然也是我们在处理数据结构的必须掌握的方法论。

最后

本文系列参照《学习JavaScript数据结构与算法第3版》进行的整理归纳、希望能帮助大家。


THIS
765 声望9 粉丝

多读书、多看报、少吃零食、多睡觉