js 数据结构之线性结构

193 阅读5分钟

数组、链表、栈、队列四种数据结构……


一 数组

数组是最简单、最为人熟悉的数据结构,是一种线性表的顺序存储结构,它的特点是用一组地址连续的存储单元依次存储数据元素。

在 js 中,数组里的元素可以是不同类型的数据。

array.png

数组的特性

  • 查询快:数组是线性查存储,通过元素的下标,就可以方便的更新元素和查找元素,其时间复杂度为O(1)
  • 增删慢:数组的长度是固定的,我们想要增加/删除一个元素,需要移动该元素后的其他元素,其时间复杂度为O(n)。(js 语言中数组可以扩容,长度不是固定的)

二 链表(LinkedList)

链表虽是一种线性结构,但其存储方式是不连续的,而是将零散的内存块串联在一起。

链表相关的名词

名词说明
首元节点链表中存储第一个数据元素的节点
头节点首元节点之前的一个节点,其 next 引用指向首元节点
头指针指向链表中的第一个节点的指针

链表存储方式为,以 head 为头节点,头节点是链表的开始,可以不存放数据,代表链表的第一个节点,每个节点都有一个 next 的向下引用,指向下一个节点,直到最后一个节点。

链表在内存中存在的特性是,每个内存块成为一个节点,除了存储数据,还会存储指向下一个节点的指针,也即每个节点由 data(数据)和 next(指向下个节点的引用)组成。

链表的特性

  • 增删快:链表增删只需要改变指针指向,复杂度为O(1)
  • 查询慢:随机访问需要每次从头开始遍历,复杂度为O(n)
  • 相对数组而言,要保存指针使占用的内存空间较大。

1. 单向链表

单向链表有两个特殊的节点,即首节点和尾节点,首节点表示链表起始点,尾节点表示链表结束。

单向链表.png

2. 单向循环链表

单向循环链表.png

3. 双向链表

每个节点有两个指针,指向前一个节点地址的前驱指针prev和后一个节点的后继指针next。首节点的前驱指针和尾节点的后继指针均指向空地址。

双向链表的特点

  • 相较单向链表,要存两个指针使消耗的内存空间更多。
  • 增删操作比单向链表更快。
  • 对于一个有序链表,双向链表的按值查询效率比单链表要高。

双向链表.png

4. 双向循环链表

双向循环链表.png

5. js 代码实现链表

// 创建节点的类
class Node {
    constructor(element) {
        this.element = element;
        this.next = null;
    }
}

// 链表类
class LinkList {
    constructor() {
        this.length = 0;
        this.head = null;
    }
    // 追加 若链表为空,则设置head, 若不为空,则将尾节点的next指向element,length加1
    append(element) {
        let node = new Node(element);
        let current;
        if (!this.head) {
            this.head = node;
        } else {
            current = this.head;
            while(current.next) {
                current = current.next
            }
            current.next = node;
        }
        ++this.length;
    }
    /*
    * 若 position 为0,将 element 的指针指向 head
    * 若不为0,设置 previous 和 next 的指向
    * length 加 1
    */
    insert(position, element) {
        let current;
        let previous;
        let index = 0;
        let node = new Node(element);
        if (position >= 0 && position <= this.length) {
            if (position === 0) {
                node.next = this.head;
                this.head = node;
            } else {
                current = this.head;
                while(index < position) {
                    previous = current;
                    current = current.next;
                    index++;
                }
                node.next = current;
                previous.next = node;
            }
            this.length++;
        }
    }
    /*
    * 根据索引删除
    * 改变 position - 1 处的指针指向
    */
    removeAt(position) {
        let current;
        let previous;
        let index = 0;
        if (position > -1 && position < this.length) {
            current = this.head;
            if (position === 0) {
                this.head = current.next;
            } else {
                while(index < position) {
                    previous = current;
                    current = current.next;
                    index++;
                }
                previsou.next = current.next
            }
            --this.length;
            return true;
        }
        return false;
    }
    /*
    * 删除元素
    */
    remove(element) {
        let index = this.indexOf(element);
        this.removeAt(index);
    }
    /*
    * 查找索引
    */
    indexOf(element) {
        let index = 0;
        let current = this.head;
        while(current) {
            if(current.element === element) {
                return index
            }
            current = current.next;
            index++;
        }
        return -1;
    }
    /*
    * 链表是否为空
    */
    isEmpty() {
        return this.length === 0;
    }
    /*
    * 查看链表长度
    */
    size() {
        return this.length;
    }
}

三 栈(stack)

只允许在有序的线性数据集合的一端(栈顶)进行增删操作,按照后进先出的原理运作。

栈.png

栈的复杂度

  • 访问: O(n)
  • 增删:O(1)

栈溢出

栈溢出是指定义的数据所需的内存超过了执行栈的最大存储范围,此时系统会抛出错误。比如死循环,数据量较大的递归。

可使用尾递归来优化递归导致的栈溢出。

数组实现的栈

// 栈类
class Stack() {
    constructor() {
        this.data = [];
    }

    // 入栈方法
    push(element) {
        this.data.push(element);
        return this.data.length;
    }

    // 出栈方法
    pop() {
        if(this.data.length) {
            this.data.pop();
            return this.data.length;
        }
        return false;
    }

    // 查询栈顶方法
    peek() {
        return this.data[this.data.length - 1];
    }

    // 是否为空
    isEmpty() {
        return this.data.length === 0;
    }

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

四 队列(Queue)

队列按照先进先出的原理运作,只允许在尾端进行添加操作,在头部进行删除操作。

队列.png

队列的复杂度

  • 访问: O(n)
  • 增删:O(1)

队列溢出

  • 真溢出:由于存储空间不够而产生的溢出。可使用扩容的方式解决。
  • 假溢出:队列中尚余足够的空间,但元素不能入队。一般是由队列的存储结构或操作方式不当所致。如下图所示:

队列溢出.png

由于只能从队尾添加元素,所以不能入队,解决方法是,删除一个元素后,所有元素应向前移一位。

数组实现的队列

// 队列
class Queue {
    constructor() {
        this.data = [];
    }
    // 入队
    push(element) {
        this.data.push(element);
        return this.data.length;
    }
    // 出队
    shift() {
        if(this.data.length) {
            this.data.shift();
            return this.data.length;
        }
    }
    // 队首元素
    peek() {
        return this.data[0];
    }
    // 清空队列
    clear() {
        this.data = [];
    }
    // 队列是否为空
    isEmpty() {
        return this.data.length === 0;
    }
}

参考:yancy__fo_ 、 《数据结构与算法JavaScript描述》