链表

基本概念

链表又称单链表、链式存储结构,用于存储逻辑关系为“一对一”的数据。

使用链表存储数据,不强制要求数据在内存中集中存储,各个元素可以分散存储在内存中。

链表存储数据间逻辑关系的实现方案是:为每一个元素配置一个指针,每个元素的指针都指向自己的直接后继元素,如下图所示:

1

链表组成结构:节点(结点)

链表由节点构成,每个节点由两部分组成:数据域和指针域。

数据域用来存储元素的值,指针域用来存放指针。

2

一个完整的链表应该由以下几部分构成:

  1. 头指针:一个和结点类型相同的指针,它的特点是:永远指向链表中的第一个结点。上文提到过,我们需要记录链表中第一个元素的存储位置,就是用头指针实现。
  2. 结点:链表中的节点又细分为头结点、首元结点和其它结点:
    • 头结点:某些场景中,为了方便解决问题,会故意在链表的开头放置一个空结点,这样的结点就称为头结点。也就是说,头结点是位于链表开头、数据域为空(不利用)的结点。
    • 首元结点:指的是链表开头第一个存有数据的结点。
    • 其他节点:链表中其他的节点。

也就是说,一个完整的链表是由头指针和诸多个结点构成的。每个链表都必须有头指针,但头结点不是必须的。

例如,创建一个包含头结点的链表存储 {1,2,3},如下图所示

3

再次强调,头指针永远指向链表中的第一个结点。换句话说,如果链表中包含头结点,那么头指针指向的是头结点,反之头指针指向首元结点。

JS实现链表

需要注意链表的不同插入方法:尾部插入、头部插入、指定元素后面插入、指定元素前面插入

class Node {
    constructor(val) {
        this.val = val
        this.next = null
    }
}

class LinkedList {
    constructor() {
        // 头结点为空节点
        this.head = new Node(null)
        this.length = 0
    }
    // 从尾部插入节点
    append(val) {
        let newNode = new Node(val)
        if (this.head.next === null) { // 空链表
            this.head.next = newNode
        } else { // 找到尾节点
            let lastNode = this.head.next
            while (lastNode.next !== null) {
                lastNode = lastNode.next
            }
            lastNode.next = newNode
        }
    }
    // 从头部插入
    appendFromHead(val) {
        let newNode = new Node(val)
        if (this.head.next === null) {
            this.head.next = newNode
        } else {
            let tmp = this.head.next
            this.head.next = newNode
            newNode.next = tmp
        }
    }
    // 从指定元素后面插入
    appendAfterElement(val, element) {
        let node = new Node(val)
        if (this.head.next === null) { // 链表为空
            this.head.next = node
        } else {
            // 遍历链表找到element元素
            let target = null
            let tmp = this.head.next
            while (tmp) {
                if (tmp.val === element) {
                    target = tmp
                    break
                }
                tmp = tmp.next
            }
            if (!target) {
                console.log('未找到目标节点');
                return
            }
            let next = target.next
            target.next = node
            node.next = next
        }
    }
    // 从指定元素前面插入
    appendBeforeElement(val, element) {
        let node = new Node(val)
        if (this.head.next === null) { // 链表为空
            this.head.next = node
        } else {
            // 遍历链表找到element元素
            let target = null
            let tmp = this.head.next
            let pre = this.head
            while (tmp) {
                if (tmp.val === element) {
                    target = tmp
                    break
                }
                pre = tmp
                tmp = tmp.next
            }
            if (!target) {
                console.log('未找到目标节点');
                return
            }
            pre.next = node
            node.next = target
        }
    }
    // 删除节点
    remove(element) {
        let curr = this.head.next
        let pre = this.head
        while (curr !== null) {
            if (curr.val === element) {
                pre.next = curr.next
                return
            }
            pre = curr
            curr = curr.next
        }
        console.log('节点不存在');
    }
    // 打印链表
    print() {
        let arr = []
        let node = this.head
        do {
            arr.push(node)
            node = node.next
        } while (node)
        console.log(arr);
    }
}

let linkedList = new LinkedList()
linkedList.append(1)
linkedList.append(2)
linkedList.appendFromHead(0)
linkedList.appendAfterElement(1.5, 1)
linkedList.appendBeforeElement(0.5, 1)

linkedList.remove(1)

linkedList.print()

双向链表

单链表的每个结点再增加一个指向其前趋的指针域 pre,这样形成的链表有两条不同方向的链,称之为双向链表。

特点:

  1. 双链表一般也由头指针head唯一确定。
  2. 每一结点均有:
    • 数据域(value)
    • 左链域(pre)指向前趋结点.
    • 右链域(next)指向后继。
    • 是一种对称结构(既有前趋势,又有后继)
  3. 双链表的前插和后插难度是一样的。

缺点:指针数的增加会导致存储空间需求增加;二是添加和删除数据时需要改变更多指针的指向。

4

节点结构

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.pre = null;
  }
}