数据结构与算法之链表(三)

1,011 阅读4分钟
  • 多个元素组成的列表
  • 元素存储不连续,用 next 指针连在一起
  • 单向链表 { value, next }
  • 双向链表 { value, prev, next }

数组 vs 链表

  • 数组:增删非首尾元素时往往需要移动元素。
  • 链表:增删非首尾元素,不需要移动元素,只需要更改 next 的指向即可

获取、修改元素时,数组效率高

添加、删除元素时,链表效率高

  • js 中类似于链表的典型就是原型链
  • 但是 js 中没有链表这种数据结构,我们可以通过一个 object 来模拟链表

链表操作:

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

使用数组生成链表并反转:

function createLinkList(arr) {
  const length = arr.length;
  if (length === 0) throw new Error("arr is empty");

  let curNode = {
    value: arr[length - 1]
  };

  if (length === 1) return curNode;

  for (let i = length - 2; i >= 0; i--) {
    curNode = {
      value: arr[i],
      next: curNode
    };
  }

  return curNode;
}

const arr = [100, 200, 300, 400];
const listNode = createLinkList(arr);

function reverseLinkList(listNode) {
  let preNode = null;
  let curNode = null;
  let nextNode = listNode;

  while (nextNode) {
    // 第一个元素删除其next,防止循环引用
    if (curNode && !preNode) {
      delete curNode.next;
    }

    // 反转指针
    if (curNode && preNode) {
      curNode.next = preNode;
    }

    preNode = curNode;
    curNode = nextNode;
    nextNode = nextNode.next;
  }
  // 设置最后一个next
  curNode.next = preNode;

  return curNode;
}

console.log(reverseLinkList(listNode));

1、LeetCode: 237.删除链表中的节点

const deleteNode = function(node) {
    // 当前节点和下个节点赋值相同后,再将下个节点删除
    node.val = node.next.val;
    node.next = node.next.next;
};

2、LeetCode: 206.反转链表

解题思路

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

解题步骤

  • 双指针一前一后遍历链表
  • 反转双指针
// 1 -> 2 -> 3 -> 4 -> 5 -> null
// 5 -> 4 -> 3 -> 2 -> 1 -> null

// 时间复杂度 O(n) n为链表的长度
// 空间复杂度 O(1)
const reverseList = function(head) {
  // 创建一个指针
  let p1 = head;
  // 创建一个新指针
  let p2 = null;
  while(p1) {
      const temp = p1.next;
      p1.next = p2;
      p2 = p1;
      p1 = temp;
  }
  return p2;
}

3、LeetCode: 2.两数相加

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */

// 时间复杂度 O(n) n 为 l1 l2 链表长度的较大值
// 空间复杂度 O(n) n 为 l1 l2 链表长度的较大值
const 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、LeetCode: 83.删除排序链表中的重复元素

解题思路

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

解题步骤

  • 遍历链表,如果发现当前元素和下个元素值相同,就删除下个元素值
  • 遍历结束后,返回原链表的头部
// 时间复杂度 O(n) n为链表的长度
// 空间复杂度 O(1)

const 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、LeetCode:141.环形链表

解题思路

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

解题步骤

  • 用一快一慢两个指针遍历链表,如果指针能够相逢,就返回 true
  • 遍历结束后,还没有相逢就返回 false
const 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、手写instanceOf

首先我们先了解下原型链有关知识点:

  • 原型链的本质是链表
  • 原型链上的节点是各种原型对象,比如 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
  • 如果在 A 对象上没有找到 x 属性,那么会沿着原型链找 x 属性

手写instanceOf解法:

  • 遍历 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;
}

console.log(instanceOf([], Array)); // true
console.log(instanceOf({}, Object)); // true
console.log(instanceOf(1, Number)); // true

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

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

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

let p = json;
path.forEach(key => {
    p = p[key];
})

console.log(p); // 7