阅读 18

学习JavaScript数据结构与算法之链表(3)

1. 什么是链表

  • 多个元素组成的列表。
  • 元素存储不连续,用next指针连在一起。

数组与链表区别

  • 数组:增删非首尾元素时往往需要移动元素。
  • 链表:增删非首尾元素,不需要移动元素,只需要更改next 的指向即可。
const a = { val: 'a' };
const b = { val: 'b' };
const c = { val: 'c' };
const d = { val: 'd' };
a.next = b;
b.next = c;
c.next = d;

// 遍历链表
let p = a;
while (p) {
    console.log(p.val);
    p = p.next;
}

// 在c和d中间插入e
const e = { val: 'e' };
c.next = e;
e.next = d;

// 删除e
c.next = d;
复制代码

2. 模拟链表操作

  • push(element):向链表尾部添加一个新元素。
  • insert(element, position):向链表的特定位置插入一个新元素。
  • getElementAt(index):返回链表中特定位置的元素。如果链表中不存在这样的元素,则返回 undefined。
  • remove(element):从链表中移除一个元素。
  • indexOf(element):返回元素在链表中的索引。如果链表中没有该元素则返回-1。
  • removeAt(position):从链表的特定位置移除一个元素。
  • isEmpty():如果链表中不包含任何元素,返回 true,如果链表长度大于 0则返回 false。
  • size():返回链表包含的元素个数,与数组的 length 属性类似。
  • toString():返回表示整个链表的字符串。由于列表项使用了 Node 类,就需要重写继

承自 JavaScript 对象默认的 toString 方法,让其只输出元素的值。

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

class LinkedList {
    constructor () {
        this.count = 0
        this.head = null
    }

    // 向链表尾部添加新元素
    push(element) {
        const node = new Node(element)
        let current
        if (this.head === null) {
            this.head = node
        } else {
            current = this.head
            while (current.next !== null) { // 获得最后一项
                current = current.next
            }
            // 将其 next 赋为新元素,建立连接
            current.next = node
        }
        this.count++
    }

    getElementAt(index) {
        if (index >= 0 && index <= this.count) {
            let node = this.head
            for (let i = 0; i < index && node !== null; i++) {
                node = node.next
            }
            return node
        }
        return null
    }

    // 从链表中移除元素
    removeAt (index) {
        if (index >= 0 && index < this.count) {
            let current = this.head
            if (index === 0) {
                this.head = current.next
            } else {
                const previous = this.getElementAt(index - 1)
                current = previous.next
                // 将 previous 与 current 的下一项链接起来:跳过 current,从而移除它
                previous.next = current.next
            }
            this.count--
            return current.element
        }
        return null
    }

    // 插入元素
    insert(element, index) {
        if (index >= 0 && index <= this.count) {
            const node = new Node(element)
            if (index === 0) {
                const current = this.head
                node.next = current
                this.head = node
            } else {
                const previous = this.getElementAt(index - 1)
                node.next = previous.next
                previous.next = node
            }
            this.count++
            return true
        }
        return false
    }

    // 返回某个链表位置
    indexOf(element) {
        let current = this.head
        for (let i = 0; i < this.size() && current !== null; i++) {
            if (element === current.element) {
                return i
            }
            current = current.next
        }
        return -1
    }

    // 从链表中移除元素
    remove(element) {
        const index = this.indexOf(element)
        return this.removeAt(index)
    }

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

    size() {
        return this.count
    }

    getHead() {
        return this.head
    }

    clear() {
        this.head = null
        this.count = 0
    }

    toString() {
        if (this.head === null) {
            return ''
        }
        let objString = `${this.head.element}`
        let current = this.head.next
        for (let i = 1; i < this.size() && current !== null; i++) {
            objString = `${objString},${current.element}`
            current = current.next
        }
        return objString
    }
}

const list = new LinkedList()
list.push(15)
list.push(10)
console.log(list.toString())
复制代码

3. 常见链表问题

1. 删除链表中的节点

解题思路:将被删除节点转移到下个节点

解题步骤:将被删节点的值改为下个节点的值,然后删除下个节点

var deleteNode = function(node) {
    node.val = node.next.val
    node.next = node.next.next
};
复制代码

2. 反转链表

解题思路

  • 反转两个节点: 将n+1的next指向n
  • 反转多个节点:双指针遍历链表,重复上述操作

解题步骤

  • 双指针一前一后遍历链表
  • 反转双指针
var reverseList = function(head) {
    let p1 = head;
    let p2 = null;
    while(p1) {
        const tmp = p1.next
        p1.next = p2
        p2 = p1
        p1 = tmp
    }
    return p2
};
复制代码

3. 两数相加

解题步骤:

    1. 新建一个空链表
    1. 遍历被相加的两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留到下一位去相加
var addTwoNumbers = function(l1, l2) {
    const l3 = new ListNode(0)
    let p1 = l1;
    let p2 = l2;
    let p3 = l3;
    let carry = 0;
    while(p1 || p2) {
        const v1 = p1 ? p1.val : 0;
        const v2 = p2 ? p2.val : 0;
        const val = v1 + v2 + carry;
        carry = Math.floor(val / 10);
        p3.next = new ListNode(val % 10);
        if(p1) p1 = p1.next
        if(p2) p2 = p2.next
        p3 = p3.next;
    }
    if(carry) {
        p3.next = new ListNode(carry);
    }
    return l3.next;
};
复制代码

4. 删除排序链表中的重复元素

解题思路:

    1. 因为链表是有序的,所以重复元素一定相邻。
    1. 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值

解题步骤:

    1. 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值
    1. 遍历结束后,返回原链表的头部

时间复杂度O(n), 空间复杂度O(1)

var deleteDuplicates = function(head) {
    let p = head;
    while(p && p.next) {
        if(p.val === p.next.val) {
            p.next = p.next.next;
        } else {
            p = p.next;
        }
    }
    return head;
};
复制代码

5. 环形链表

解题思路:

    1. 两个人在圆形操场上的起点同时起跑,速度快的人一定会超过速度慢的人一圈。
    1. 用一块一慢两个指针遍历链表,如果指针能够相逢,那么链表就有圈。

解题步骤:

    1. 用一块一慢两个指针遍历链表,如果指针能够相逢,就返回true
    1. 遍历结束后,还没有相逢就返回false

时间复杂度O(n),空间复杂度O(1)

var hasCycle = function(head) {
    let p1 = head;
    let p2 = head;
    while(p1 && p2 && p2.next) {
        p1 = p1.next
        p2 = p2.next.next
        if(p1 === p2) {
            return true;
        }
    }
    return false;
};
复制代码

6. 原型链

  • 原型链的本质是链表。

  • 原型链上的节点是各种原型对象,比如Function.prototype、Object.prototype.....

  • 原型链通过 __proto__ 属性连接各种原型对象。

  • obj -> Object.prototype -> null

  • func -> Function.prototype -> Object.prototype -> null

  • arr -> Array.prototype -> Object.prototype -> null

原型链知识点:如果A沿着原型链能找到B.prototype, 那么 A instanceof B 为 true.

const obj = {}
obj instanceof Object // true
复制代码

如果在A对象上没有找到x属性,那么会沿着原型链找x属性

instanceof原理,并用代码实现

  • 知识点:如果A沿着原型链能找到B.prototype,那么A instanceof B为true。
  • 解法: 遍历A的原型链,如果找到B.prototype,返回true,否则返回false
const instanceOf = (A, B) => {
    let p = A;
    while(p) {
        if(p === B.prototype) {
            return true;
        }
        p = p.__proto__
    }
    return false;
}
复制代码
var foo = [],
    F = function() {};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';

console.log(foo.a);
console.log(foo.b);

console.log(F.a);
console.log(F.b);
复制代码

使用链表指针获取JSON的节点值

const json = {
    a: { b: { c: 1 } },
    d: { e: 2 },
};

const path = ['a', 'b', 'c'];

let p = json;
path.forEach((k) => {
    p=p[k];
});
复制代码
  • 链表里的元素存储不是连续的,之间通过 next 连接
  • JavaScript中没有链表,但可以用Object模拟链表。
  • 链表常用操作:修改next、遍历链表。
  • JS 中的原型链也是一一个链表。
  • 使用链表指针可以获取JSON的节点值。