资讯专栏INFORMATION COLUMN

JS数据结构与算法_链表

NeverSayNever / 1702人阅读

摘要:上一篇数据结构与算法栈队列下一篇数据结构与算法集合字典写在前面说明数据结构与算法系列文章的代码和示例均可在此找到上一篇博客发布以后,仅几天的时间竟然成为了我写博客以来点赞数最多的一篇博客。

上一篇:JS数据结构与算法_栈&队列
下一篇:JS数据结构与算法_集合&字典

写在前面
说明:JS数据结构与算法 系列文章的代码和示例均可在此找到

上一篇博客发布以后,仅几天的时间竟然成为了我写博客以来点赞数最多的一篇博客。欢喜之余,不由得思考背后的原因,前端er离数据结构与算法太遥远了,论坛里也少有人去专门为数据结构与算法撰文,才使得这看似平平的文章收获如此。不过,这样也更加坚定了我继续学习数据结构与算法的决心(虽然只是入门级的)

一、链表数据结构

相较于之前学习的 栈/队列 只关心 栈顶/首尾 的模式,链表更加像是数组。链表和数组都是用于存储有序元素的集合,但有几点大不相同

链表不同于数组,链表中的元素在内存中并不是连续放置的

链表添加或移除元素不需要移动其他元素

数组可以直接访问任何一个位置的元素,链表必须从表头开始迭代到指定位置访问

下面是单链表的基本结构

长度为3的单链表

每个元素由一个存储元素本身data的节点和一个指向下一个元素的引用next(也称指针或链接)组成

尾节点的引用next指向为null

类比:寻宝游戏,你有一条线索,这条线索是指向寻找下一条线索的地点的指针。你顺着这条链接去下一个地点,得到另一条指向再下一处的线索。得到列表中间的线索的唯一办法,就是从起点(第一条线索)顺着列表寻找

二、链表的实现

链表的实现不像之前介绍的栈和队列一般依赖于数组(至少我们目前是这样实现的),它必须自己构建类并组织逻辑实现。我们先创建一个Node

// 节点基类
class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
  }
}

一般单链表有以下几种方法:

append 在链表尾部添加一个元素

insert 在指定位置插入元素

removeAt 在指定位置删除元素

getNode 获取指定位置的元素

print 打印整个链表

indexOf 查找链表中是否有某个元素,有则返回索引,没有则返回-1

getHead 获取链表头部

getTail 获取链表尾部(有些并未实现尾部)

size 返回链表包含的元素个数

clear 清空链表

// 初始化链表
class LinkedList {
  constructor() {
    this._head = null;
    this._tail = null;
    this._length = 0;
  }
  // 方法...
}

下面我们来实现几个重要的方法

2.1 append方法

在链表尾部添加一个新的元素可分为两种情况:

原链表中无元素,添加元素后,headtail均指向新元素

原链表中有元素,更新tail元素(如下)

代码实现

// 在链表尾端添加元素
append(data) {
  const newNode = new Node(data);
  if (this._length === 0) {
    this._head = newNode;
    this._tail = newNode;
  } else {
    this._tail.next = newNode;
    this._tail = newNode;
  }

  this._length += 1;
  return true;
}
2.2 print方法

为方便验证,我们先实现print方法。方法虽简单,这里却涉及到链表遍历精髓

// 打印链表
print() {
  let ret = [];
  // 遍历需从链表头部开始
  let currNode = this._head;
  // 单链表最终指向null,作为结束标志
  while (currNode) {
    ret.push(currNode.data);
    // 轮询至下一节点
    currNode = currNode.next;
  }
  console.log(ret.join(" --> "));
}

// 验证
const link = new LinkedList();
link.append(1);
link.append(2);
link.append(3);

link.print(); // 1 --> 2 --> 3
2.3 getNode方法

获取指定索引位置的节点,依次遍历链表,直到指定位置返回

// 获取指定位置元素
getNode(index) {
  let currNode = this._head;
  let currIndex = 0;
  while (currIndex < index) {
    currIndex += 1;
    currNode = currNode.next;
  }
  return currNode;
}

// 验证【衔接上面的链表实例】
console.log(link.getNode(0));
// Node { data: 1, next: Node { data: 2, next: Node { data: 3, next: null } } }
console.log(link.getNode(3)); // null
2.4 insert方法

插入元素,需要考虑三种情况

插入尾部,相当于append

插入首部,替代_head并指向原有头部元素

中间,需要断开原有链接并重新组合(如下)

代码实现

// 在链表指定位置插入元素
insert(index, data) {
  // 不满足条件的索引值
  if (index < 0 || index > this._length) return false;
  // 插入尾部
  if (index === this._length) return this.append(data);

  const insertNode = new Node(data);
  if (index === 0) {
    // 插入首部
    insertNode.next = this._head;
    this._head = insertNode;
  } else {
    // 找到原有位置节点
    const prevTargetNode = this.getNode(index - 1);
    const targetNode = prevTargetNode.next;
    // 重塑节点连接
    prevTargetNode.next = insertNode;
    insertNode.next = targetNode;
  }

  this._length += 1;
  return true;
}

// 验证
link.insert(0, 0);
link.insert(4, 4);
link.insert(5, 5);
link.print(); // 0 --> 1 --> 2 --> 3 --> 4 --> 5
2.5 removeAt方法

在指定位置删除元素同添加元素类似

首部:重新定义_head

其他:找到目标位置的前后元素,重塑连接,如果目标位置为尾部,则重新定义_tail

代码实现

// 在链表指定位置移除元素
removeAt(index) {
  if (index < 0 || index >= this._length) return false;

  if (index === 0) {
    this._head = this._head.next;
  } else {
    const prevNode = this.getNode(index - 1);
    const delNode = prevNode.next;
    const nextNode = delNode.next;
    // 若移除为最后一个元素
    if (!nextNode) this._tail = prevNode;
    prevNode.next = nextNode;
  }

  this._length -= 1;
  return true;
}

// 验证
link.removeAt(3);
link.print(); // 0 --> 1 --> 2 --> 4 --> 5
2.6 其它方法

完整的链表代码,可点此获取

// 判断数据是否存在于链表内,存在返回index,否则返回-1
indexOf(data) {
  let currNode = this._head;
  let index = 0;
  while (currNode) {
    if (currNode.data === data) return index;
    index += 1;
    currNode = currNode.next;
  }

  return -1;
}

getHead() {
  return this._head;
}

getTail() {
  return this._tail;
}

size() {
  return this._length;
}

isEmpty() {
  return !this._length;
}

clear() {
  this._head = null;
  this._tail = null;
  this._length = 0;
}
三、链表的应用 3.1 基于链表实现的StackQueue

基于链表实现栈

class Stack {
  constructor() {
    this._link = new LinkedList();
  }
  push(item) {
    this._link.append(item);
  }
  pop() {
    const tailIndex = this._link - 1;
    return this._link.removeAt(tailIndex);
  }
  peek() {
    if (this._link.size() === 0) return undefined;
    return this._link.getTail().data;
  }
  size() {
    return this._link.size();
  }
  isEmpty() {
    return this._link.isEmpty();
  }
  clear() {
    this._link.clear()
  }
}

基于链表实现队列

class Queue {
  constructor() {
    this._link = new LinkedList();
  }
  enqueue(item) {
    this._link.append(item);
  }
  dequeue() {
    return this._link.removeAt(0);
  }
  head() {
    if (this._link.size() === 0) return undefined;
    return this._link.getHead().data;
  }
  tail() {
    if (this._link.size() === 0) return undefined;
    return this._link.getTail().data;
  }
  size() {
    return this._link.size();
  }
  isEmpty() {
    return this._link.isEmpty();
  }
  clear() {
    this._link.clear()
  }
}
3.2 链表翻转【面试常考】

(1)迭代法

迭代法的核心就是currNode.next = prevNode,然后从头部一次向后轮询

代码实现

reverse() {
  if (!this._head) return false;

  let prevNode = null;
  let currNode = this._head;
  while (currNode) {
    // 记录下一节点并重塑连接
    const nextNode = currNode.next;
    currNode.next = prevNode;
    // 轮询至下一节点
    prevNode = currNode;
    currNode = nextNode;
  }

  // 交换首尾
  let temp = this._tail;
  this._tail = this._head;
  this._head = temp;

  return true;
}

(2)递归法

递归的本质就是执行到当前位置时,自己并不去解决,而是等下一阶段执行。直到递归终止条件,然后再依次向上执行

function _reverseByRecusive(node) {
  if (!node) return null;
  if (!node.next) return node; // 递归终止条件

  var reversedHead = _reverseByRecusive(node.next);
  node.next.next = node;
  node.next = null;
  
  return reversedHead;
};

_reverseByRecusive(this._head);
3.3 链表逆向输出

利用递归,反向输出

function _reversePrint(node){
  if(!node) return;// 递归终止条件
  _reversePrint(node.next);
  console.log(node.data);
};
四、双向链表和循环链表 4.1 双向链表

双向链表和普通链表的区别在于,在链表中,一个节点只有链向下一个节点的链接,而在双向链表中,链接是双向的:一个链向下一个元素,另一个链向前一个元素,如下图

正是因为这种变化,使得链表相邻节点之间不仅只有单向关系,可以通过prev来访问当前节点的上一节点。相应的,双向循环链表的基类也有变化

class Node {
  constructor(data) {
    this.data = data;
    this.next = null;
    this.prev = null;
  }
}

继承单向链表后,最终的双向循环链表DoublyLinkedList如下【prev对应的更改为NEW

class DoublyLinkedList extends LinkedList {
  constructor() {
    super();
  }

  append(data) {
    const newNode = new DoublyNode(data);

    if (this._length === 0) {
      this._head = newNode;
      this._tail = newNode;
    } else {
      newNode.prev = this._tail; // NEW
      this._tail.next = newNode;
      this._tail = newNode;
    }

    this._length += 1;
    return true;
  }

  insert(index, data) {
    if (index < 0 || index > this._length) return false;
    if (index === this._length) return this.append(data);

    const insertNode = new DoublyNode(data);
    if (index === 0) {
      insertNode.prev = null; // NEW
      this._head.prev = insertNode; // NEW
      insertNode.next = this._head;
      this._head = insertNode;
    } else {
      // 找到原有位置节点
      const prevTargetNode = this.getNode(index - 1);
      const targetNode = prevTargetNode.next;
      // 重塑节点连接
      prevTargetNode.next = insertNode;
      insertNode.next = targetNode;
      insertNode.prev = prevTargetNode; // NEW
      targetNode.prev = insertNode; // NEW
    }

    this._length += 1;
    return true;
  }

  removeAt(index) {
    if (index < 0 || index > this._length) return false;

    let delNode;

    if (index === 0) {
      delNode = this._head;
      this._head = this._head.next;
      this._head.prev = null; // NEW
    } else {
      const prevNode = this.getNode(index - 1);
      delNode = prevNode.next;
      const nextNode = delNode.next;
      // 若移除为最后一个元素
      if (!nextNode) {
        this._tail = prevNode;
        this._tail.next = null; // NEW
      } else {
        prevNode.next = nextNode; // NEW
        nextNode.prev = prevNode; // NEW
      }
    }

    this._length -= 1;
    return delNode.data;
  }
}
4.2 循环链表
循环链表可以像链表一样只有单向引用,也可以像双向链表一样有双向引用。循环链表和链 表之间唯一的区别在于,单向循环链表最后一个节点指向下一个节点的指针tail.next不是引用null, 而是指向第一个节点head,双向循环链表的第一个节点指向上一节点的指针head.prev不是引用null,而是指向最后一个节点tail

总结

链表的实现较于栈和队列的实现复杂许多,同样的,链表的功能更加强大

我们可以通过链表实现栈和队列,同样也可以通过链表来实现栈和队列的问题

链表更像是数组一样的基础数据结构,同时也避免了数组操作中删除或插入元素对其他元素的影响

上一篇:JS数据结构与算法_栈&队列
下一篇:JS数据结构与算法_集合&字典

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101290.html

相关文章

  • 唠叨一下js对象哈希表那些事

    摘要:的扩展知识对于哈希表来说,最重要的莫过于生成哈希串的哈希算法和处理冲突的策略了。由于链表的查找需要遍历,如果我们将链表换成树或者哈希表结构,那么就能大幅提高冲突元素的查找效率。 最近在整理数据结构和算法相关的知识,小茄专门在github上开了个repo https://github.com/qieguo2016...,后续内容也会更新到这里,欢迎围观加星星! js对象 js中的对象是基...

    Nosee 评论0 收藏0
  • 探索vue源码之缓存篇

    摘要:中采用算法来实现缓存的高效管理。通过这两个属性,将缓存中的变量连接起来。以上图举例缓存这个对象中保存了三个变量。如果缓存数组为空,则返回将为传入参数的缓存对象标识为最常使用的,即,并调整双向链表,返回改变后的。 vue.js入坑也有了小半年的时间了,圈子里一直流传着其源码优雅、简洁的传说。最近的一次技术分享会,同事分享vue.js源码的缓存部分,鄙人将其整理出来,与大家一起学习 一、从...

    Forest10 评论0 收藏0
  • JS数据结构算法_栈&队列

    摘要:下一篇数据结构与算法链表写在前面说明数据结构与算法系列文章的代码和示例均可在此找到原计划是把你不知道的三部全部看完的,偶然间朋友推荐了数据结构与算法的一套入门视频,学之。手头上恰好有学习数据结构与算法的书籍,便转而先把数据结构与算法学习。 下一篇:JS数据结构与算法_链表 写在前面 说明:JS数据结构与算法 系列文章的代码和示例均可在此找到 原计划是把《你不知道的Javascript》...

    AndroidTraveler 评论0 收藏0
  • web技术分享| LRU 缓存淘汰算法

    摘要:双向链表用于管理缓存数据结点的顺序,新增数据和缓存命中最近被访问的数据被放置在结点,尾部的结点根据内存大小进行淘汰。 了解 LRU 之前,我们应该了解一下缓存,大家都知道计算机具有缓存内存,可以临时存储最常用的数据,当缓存数据超过一定大小时,系统会进行回收,以便释放出空间来缓存新的数据,但从系统中检索数据的成本...

    graf 评论0 收藏0

发表评论

0条评论

NeverSayNever

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<