记录一下对js数据结构基础的学习,包含栈/队列/链表及其衍生

61 阅读7分钟

一.数组(Array)

最常用的数据结构之一,相信大家都很熟悉了,这里就不多讲

二.栈(Stack)

它的结构类似于数组,但和数组不同,他只能在结尾处添加和删除元素,即遵循先进后出原则(FILO),它的结尾被称之为栈顶,添加元素称之为压栈,删除称之为弹出

常见的应用,比如递归时的函数调用就是栈结构

数据结构实现:

      class Stack {
        // 声明私有变量使用#加变量名
        #items = [];

        // 压栈
        push(...element) {
          this.#items.push(...element);
        }

        // 弹出
        pop() {
          return this.#items.pop();
        }

        // 查询栈顶
        peek() {
          return this.#items[this.#items.length - 1];
        }

        // 查询是否为空
        isEmpty() {
          return this.#items.length === 0;
        }

        // 清空栈
        clear() {
          this.#items = [];
        }

        // 查询长度
        size() {
          return this.#items.length;
        }

        // 查询对象在栈中的位置
        search(item) {
          return this.#items.indexOf(item);
        }
        
        // 转为字符串
        toString() {
          return this.#items.toString()
        }
      }

三.队列(Queue)

和栈结构不同,队列遵循先进先出(FIFO)原则,即只能在数组的末尾添加元素,在数组的开头删除元素,在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。

我们知道,数组的删除第一个元素,会让后面的所有元素往前移动,在队列中因为只会删除队头,即数组第一个元素,会导致性能的浪费,因此我们封装队列的时候不去用数组,而是使用对象来封装。

常见的应用,比如js的事件循环,发布订阅的消息列表,弹窗显示等

1.普通队列
    class Queue {
        constructor() {}

        #items = {};
        #count = 0;
        #lowestCount = 0;

        enqueue(...element) {
            element.forEach(item => {
              this.#items[this.#count] = item;
              this.#count++
            })
        }

        dequeue() {
          if (this.isEmpty()) return
          const head = this.#items[this.#lowestCount]
          delete this.#items[this.#lowestCount]
          // 注意,因为当前队头已经被删除了,所以需要将队头的指针++
          this.#lowestCount++
          return head;
        }

        front() {
          return this.#items[this.#lowestCount]
        }

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

        clear() {
          this.#items = {};
        }

        size() {
          return this.#count - this.#lowestCount;
        }
    }
2.优先队列

与普通队列不同,优先队列中的元素带有优先级,入队的时候,需要根据优先级插入到对应位置

这里写一个查询位置的方法好了,默认元素是一个数字类型, 实际中可能是对象,但代表优先级的属性八成也是数字,改改就行XD

      class PriorityQueue {
        #items = [];
        search(item) {
          // 值不为数字,报错
          if (typeof item !== "number") {
            throw new Error("PriorityQueue.search() only accepts numbers");
          }
          // 当队列为空时,直接返回0
          if (this.#items.length === 0) {
            console.log(0);
            return 0;
          }
          // 当item大于队列中最大值时,返回队列长度
          if (item >= this.#items[this.#items.length - 1]) {
            return this.#items.length;
          // 当item小于队列中最小值时,返回0
          } else if (item <= this.#items[0]) {
            return 0;
          }
          // 设置查找区域头尾指针
          let i = this.#items.length;
          let j = 0;
          // 最终结果,一定是i<j
          while (i >= j) {
            let mid = Math.floor((i + j) / 2);
            if (this.#items[mid] === item) return mid;
            // 插入值小于中间值,则item小于中间值, 将中间值-1设为最大值
            // 插入值大于中间值,则item大于中间值, 将中间值+1设为最小值
            else if (item < this.#items[mid]) {
              i =  mid - 1
            } else {
              j =  mid + 1;
            }
          }
          return j;
        }

        enqueue(...arr) {
          arr.forEach((item) => {
            this.#items.splice(this.search(item), 0, item);
          });
        }

        toString() {
          return this.#items.toString();
        }
      }

偷个懒,这里使用的是数组(因为插入之后还是要循环并让所有的之后元素角标++),而且其他方法不加了

3.循环队列

循环队列即头尾相接的队列,当指针前进之后大于队列长度时,指针将回到队列头,实现则是改变出队的方法,将出队的元素再次入队即可

        loopDequeue() {
          const el = this.dequeue()
          this.enqueue(el)
          return el
        }
4.双端队列

双端队列既可以在头部删除,在尾部添加,也可以在头部添加,在尾部删除,他和普通队列的不同也在添加删除的方法上

        addFront(element) {
            if (this.isEmpty()) {
                this.#items[this.#count] = element;
                this.#count++
                return
            }
            this.#lowestCount--
            this.#items[this.#lowestCount] = element;
        }

        removeFront() {
            if (this.isEmpty()) return
            const result = this.#items[this.#lowestCount]
            delete this.#items[this.#lowestCount]
            this.#lowestCount++
            return result
        }
        
        peekFront() {
            if (this.isEmpty()) return
            return this.#items[this.#lowestCount]
        }

        addBack(element) {
            this.#items[this.#count] = element;
            this.#count++
        }

        removeBack() {
            if (this.isEmpty()) return
            const result = this.#items[this.#count - 1]
            delete this.#items[this.#count - 1]
            this.#count--
            return result
        }
        
        peekBack() {
            if (this.isEmpty()) return
            return this.#items[this.#count - 1]
        }

三.链表(linkedList)

与js不同,很多的语言中,数组的长度是无法改变的,插入和删除数组中的元素,往往需要创建一个新的数组,而链表则在插入和删除时非常灵活,可以在任意位置去做这两个操作,js中,使用unshift方法也是会让原数组所有元素向后移动,造成大量的性能损耗,例如上面的队列,在这种时候,我们就可以选择链表

1.单向链表

顾名思义,单向链表只有一个头和一个next指针,只能顺着这个头向下遍历

image.png

            class linkList {
                constructor() {
                    this.header = null;
                    this.count = 0;
                }

                push(element) {
                    const node = new Node(element);
                    if (this.header === null) {
                        this.header = node;
                        // 当链表中已经有节点的时候,需要拿到最后一个节点,此时我们可以循环,并遍历链表获取最后一个节点,将其next设置为新增节点即可
                    } else {
                        let currentNode = this.header;
                        while (currentNode.next) {
                            // 不断将下一个节点赋值给currentNode变量,当currentNode变量的next属性为null时,说明是最后一个节点,跳出循环并添加节点
                            currentNode = currentNode.next;
                        }
                        currentNode.next = node;
                    }
                }

                removeNode(index) {
                    if (index < 0) return;
                    else if (index === 0) {
                        const d = this.header;
                        this.header = this.header.next;
                        return d;
                    } else {
                        let i = 0;
                        let currentNode = this.header;
                        while (currentNode.next) {
                            if (i === index - 1) {
                                const d = currentNode.next;
                                currentNode.next = currentNode.next.next;
                                return d;
                            } else {
                                i++;
                                currentNode = currentNode.next;
                            }
                        }
                    }
                }

                removeData(index) {
                    if (index < 0) return;
                    else if (index === 0) {
                        const d = this.header.data;
                        this.header.data = null;
                        return d;
                    } else {
                        let i = 0;
                        let currentNode = this.header;
                        while (currentNode.next) {
                            if (i === index) {
                                const d = currentNode.data;
                                currentNode.data = null;
                                return d;
                            } else {
                                i++;
                                currentNode = currentNode.next;
                            }
                        }
                    }
                }
            }
2.双向链表

双向链表的头尾都可以开始遍历,他的节点也会多出prev指针,和next不同,分别指向上一个和下一个节点

image.png

            // 除了next指针,新加prev指针
            class DNode extends Node {
                constructor(element) {
                    super(element)
                    this.prev = null
                }
            }
            // 新加#footer私有属性,用于记录链表的尾部
            class DLinkList {
                #footer
                #header
                #count

                constructor() {
                    this.#footer = null
                    this.#header = null
                    this.#count = 0
                }

                search(index) {
                    const flag = this.size() / 2 >= index
                    if(!index) {
                        return this.#header
                    } else if (index > 0) {
                        let currentNode = flag ? this.#footer : this.#header
                        // 从前往后的角标,前后往前应该是最后一个角标-当前角标即 size - 1 - index
                        let num = flag ? (this.size() - 1 - index) : index
                        if(index === (this.size() - 1)) return this.#footer
                        for (let i = 0; i < num; i++) {
                            currentNode = flag ? currentNode.prev : currentNode.next
                        }
                        return currentNode
                    }
                }

                push(data) {
                    const node = new DNode(data)
                    if(this.isEmpty()) {
                        this.#header = node
                    } else {
                        const currentNode = this.#footer
                        currentNode.next = node
                        node.prev = currentNode
                    }
                    this.#footer = node
                    this.#count++
                }

                insert(data, index) {
                    const getEmptyNode = () => new DNode(null)
                    if(index < 0) throw new Error('index必须大于0,而你传入的index为' + index)
                    if(this.isEmpty()) {
                        this.push(null)
                    }
                    // 当需要插入的index<size&&index>0时,意味着此时的插入将不会导致footer指针改变,
                    // 而index>=size时,footer将改变,插入元素必定是队尾
                    if(index < this.size()) {
                        if(index === 0) {
                            this.unshift(data)
                        } else {
                            const node = new DNode(data)
                            let prevNode = this.search(index - 1)
                            console.log('prevNode', prevNode);
                            let currentNode = prevNode.next
                            prevNode.next = node
                            node.prev = prevNode
                            node.next = currentNode
                            currentNode.prev = node
                            this.#count++
                        }
                    } else {
                        for (let i = (this.size() - 1); i < index; i++) {
                            i === index - 1 ? this.push(data) : this.push(null)
                        }
                    }
                }

                unshift(data) {
                    if(this.isEmpty()) return
                    let currentNode = this.#header
                    if(this.size() === 1) {
                        this.#header = null
                        this.#footer = null
                    } else {
                        currentNode.next.prev = null
                        this.#header = currentNode.next
                    }
                    this.#count--
                    return currentNode
                }

                pop() {
                    if(this.isEmpty()) return
                    let currentNode = this.#footer
                    if(this.size() === 1) {
                        this.#header = null
                        this.#footer = null
                    } else {
                        currentNode.prev.next = null
                        this.#footer = currentNode.prev
                    }
                    this.#count--
                    return currentNode
                }

                shift() {
                    if(this.isEmpty()) return
                    let currentNode = this.#header
                    if(this.size() === 1) {
                        this.#header = null
                        this.#footer = null
                    } else {
                        currentNode.next.prev = null
                        this.#header = currentNode.next
                    }
                    this.#count--
                    return currentNode
                }

                removeAt(index) {
                    if(this.isEmpty()) return
                    if(index >= this.size() || index < 0) return
                    else if (index === (this.size() - 1)) return this.pop()
                    else if (index === 0) return this.shift()
                    else {
                        const targetNode = this.search(index)
                        targetNode.prev.next = targetNode.next
                        targetNode.next.prev = targetNode.prev
                        this.#count--
                        return targetNode
                    }
                }

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

                size() {
                    return this.#count
                }
            }
3.单向循环链表

单向循环链表的特点是最后一个节点next指向第一个节点,双向则还有第一个节点的prev指向最后一个节点,这里封装一个单向的,偷下懒,只写主要功能XD

            class CLinkList {
                constructor() {}

                #header = null
                #count = 0

                search(index) {
                    if(!this.#header || index < 0) return
                    let currentNode = this.#header
                    index = index % this.#count
                    if(index === 0) return currentNode
                    else if(index < this.#count) {
                        for(let i = 0; i < index; i++) {
                            currentNode = currentNode.next
                        }
                    }
                    return currentNode
                }

                push(data) {
                    const node = new Node(data)
                    if(!this.#header) this.#header = node
                    else {
                        const currentNode = this.search(this.#count-1)
                        currentNode.next = node
                    }
                    node.next = this.#header
                    this.#count++
                }

                insert(data,index) {
                    if(index < 0) return
                    if(!this.#header) return this.push(data)
                    else {
                        const node = new Node(data)
                        if(index % this.#count === 0) {
                            const footerNode = this.search(this.#count-1)
                            footerNode.next = node
                            node.next = this.#header
                            this.#header = node
                        } else {
                            const prevNode = this.search(index-1)
                            const currentNode = prevNode.next
                            prevNode.next = node
                            node.next = currentNode
                        }
                        this.#count++
                    }
                }

                removeAt(index) {
                    if(!this.#header || index < 0) return
                    let currentNode
                    if(index % this.#count === 0) {
                        currentNode = this.#header
                        if(this.#count === 1) {
                            this.#header = null
                        } else {
                            const prevNode = this.search(this.#count-1)
                            prevNode.next = this.#header.next
                            this.#header = this.#header.next
                        }
                    } else {
                        const prevNode = this.search(index-1)
                        currentNode = prevNode.next
                        prevNode.next = currentNode.next
                    }
                    this.#count--
                    return currentNode
                }
            }