2
数组

说起链表结构的话,不得不提数组结构

先看一下数组的本质是什么

  • 通过一段连续分配的内存空间 元素在内存中呈线性连续排列
  • 每个元素都可以存储字节相同的数据(或地址)
  • 对象的话 存的是内存地址 此时地址指向的数据被称为卫星数据

数组这样规定有它的好处:

1 - 访问索引比较方便
内存地址 0xA000 0xA008 0xA010 0xA018 0xA020 0xA028 0xA030 0xA038
序数 0 1 2 3 4 5 6 ...
其他数据 数据 1 数据 2 数据 3 数据 4 数据 5 数据 6 ...预留区域 其他数据
  • arr = 0xA000
  • a[3] = 0xA000 + 3 * 8(字节) = 0xA018

可以看到 因为数组是连续排序的元素,所以可以很方便的通过下标/索引去访问到目标元素,时间复杂度仅为O(1)

2 - 追加元素也比较方便

因为数组在申请内存空间时, 会多预留一部分空闲区域,以便之后的各种操作。 所以在预留区域追加元素十分简单 时间复杂度也是O(1)
有个问题就是, 如果预留区域小于追加元素的个数怎么办? 其实也不难,根据简单的分配算法可以大致理解为
数组的元素为n, 预留区域是N, 其中 N = 2n。 如果要追加n+1个元素,对于前n个元素,每个追加的也是常数时间,仅在预留区域内正常追加即可。那么最后一个超过预留区域的元素的追加怎么办?为了避免溢出内存,会将整个2n个元素拷贝到新的内存空间,再对最后一个元素在新的内存预留空间进行追加,那么时间复杂度也就是:
2n + (n+1) / n 也就是 3n+1 / n 大致等于 3
所以它也是常数级的操作 比较简单

但对于数组的其他操作就较复杂

因为其他操作 都会影响到数组中的所有元素

JS数组操作 时间复杂度
push O(1)
pop(删除最后一个元素并返回删除的元素) O(1)
shift(删除第一个元素并返回删除的元素) O(n)
unshift (在数组的首位添加一个元素) O(n)
slice(截取) O(n)
splice(从数组中添加/删除项目,然后返回被删除的项目) O(n)
concat O(n)
find O(n)
filter O(n)
every O(n)

所以在这个时候 就体现了链表的优势。

链表
  • 通过一段离散的内存空间 元素在内存中通过指针的形式进行连接 也呈线性排列
  • 因为有指针 在内存中可以离散的分配
  • 头指针 指向链表的第一个节点

链表和数组的一个最大不同就是链表在内存空间中的表现是离散的(当然也可以连续) 所以 链表的某些操作就不会影响到链表中的其他元素 从而可以减少操作的复杂度

单向链表

除了上述的链表特点外 单向链表还有自身的特点:

  • 因为是单向 所有只有一个方向
  • 链表节点包括 keynext 指针

    key 可以是数据或卫星数据的地址
    next 指针指向下一个链表节点
  • 尾指针 指向null

实现单向链表:

// 链表节点
class ListNode {
  constructor(key) {
    this.key = key;
    this.next = null;
  }
}

// 链表
class LinkedList {
  constructor() {
    this.head = null;
    this._length = 0;
  }

  static createNode(key) {
    return new ListNode(key);
  }

  // 首部插入 O(n)
  insert(node) {
    if (this.head) {
      node.next = this.head;
    }
    this.head = node;
    this._length++;
  }

  // 查找 O(n)
  find(key) {
    let p = this.head; // 指针
    while (p && p.key !== key) {
      // 终止条件是 p===null || p.key=key
      //          没找到 || 找到了
      p = p.next;
    }
    return p;
  }

  // 删除 del O(n)
  delete(node) {
    // a. 删除根节点
    if (node === this.head) {
      this.head = node.next;
      this._length--;
      return;
    }

    // 取node的上个节点
    let prevNode = this.head;
    while (prevNode && prevNode.next !== node) {
      prevNode = prevNode.next;
    }

    // b. 删除尾节点
    if (!node.next) {
      prevNode.next = null;
    }

    // c. 删除中间节点
    if (node.next) {
      prevNode.next = node.next;
    }

    this._length--;
  }
}

let node = LinkedList.createNode(1);

let m = new LinkedList();

m.insert(LinkedList.createNode(1));
m.insert(LinkedList.createNode(2));
m.insert(LinkedList.createNode(3));
m.insert(LinkedList.createNode(4));
m.insert(LinkedList.createNode(5));
m.insert(LinkedList.createNode(6));
// console.log(m);
// console.log(m.find(2))

代码多读读就懂了 很简单

知道了单向链表之后 双向链表也就自然懂了

双向链表

由上面单向链表的代码可以发现,单向链表只有一个next指针,因而在获取上一个链表元素的时候略麻烦一些 双向链表因为是双向的 所以没有此问题

除了上述的链表特点外 双向链表还有自身的特点:

  • 因为是双向 所以有两个方向
  • 链表节点包括 keyprev 指针next 指针

    key 可以是数据或卫星数据的地址
    prev 指针指向上一个链表节点
    next 指针指向下一个链表节点
  • 头指针 的prev指向null
  • 尾指针 的next指向null

代码和单向链表挺相似:

class ListNode {
  constructor() {
    this.key = key;
    this.prev = null;
    this.next = null;
  }
}

class LinkedNode {
  constructor() {
    this.head = null;
    this._length = 0;
  }

  static createNode(key) {
    return new ListNode(key);
  }

  insert(node) {
    if (this.head) {
      this.head.prev = node;
      node.next = this.head;
    }
    this.head = node;
    this._length++;
  }

  find(key) {
    // 查找和单向链表相同
    let p = this.head;
    while (p && p.key !== key) {
      p = p.next;
    }
    return p;
  }

  delete(node) {
    // 根节点
    if (node === this.head) {
      node.next.prev = null;
      this.head = node.next;
    }

    // 非第一个
    if (node.prev) {
      node.prev.next = node.next;
    }

    // 非最后一个
    if (node.next) {
      node.next.prev = node.prev;
    }
  }
}

到此为止, 分别简单实现了单向和双向链表。 所以问题来了,链表这么香吗? 这么香为什么不用链表而非要用数组呢? 其实 链表在时间复杂度上也有损耗, 有的甚至比数组还复杂:

链表操作 时间复杂度
追加(append/push) O(1) <数组是 o(1),无变化>
索引(arr[idx]) O(n) <数组是 o(1),链表的索引变慢了>
插入(insert) O(1) <数组是 o(n),链表的插入变快了>
删除(delete/remove) O(1) <数组是 o(n),链表的删除变快了>
合并(concat/merge) O(1) <数组是 o(m+n),链表的合并变快了>

可以看到 链表的 索引访问 反而变慢了, 因为链表去访问元素的时, 需要从head开始,不停的循环, 直到找到目标元素。而数组则只需在内存中根据索引内存地址很容易的找到目标元素。 但是链表的其他操作,比如插入 删除 合并等 确实比数组快多了, 因为在数组中这些操作都影响到整体数组,复杂度是O(n)。 而链表有指针这个东西,所以只需关注目标元素即可,所以复杂度是 O(1)


Funky_Tiger
443 声望33 粉丝

刷题,交流,offer,内推,涨薪,相亲,前端资源共享...