从头到尾,探索链表的无限可能性

何为链表

在计算机科学的世界里,有一种神奇的数据结构,它以一种独特的方式将数据节点连接在一起,创造出无限的可能性。它就是链表。从头到尾,这篇文章将带您踏上一场探索链表的奇妙之旅,揭开它隐藏的秘密,探寻它所带来的无限潜力。无论您是一名编程新手还是经验丰富的软件工程师,本文将为您详细解说链表的工作原理、优势与应用场景。准备好迎接数据的律动和魅力了吗?让我们一起跳进这个充满惊喜的链表世界吧!

链表是一种常见的线性数据结构,它由一系列节点组成,每个节点包含两部分:一个是存储数据的字段(通常称为"数据域"),另一个是指向下一个节点的引用(通常称为"指针"或"引用")。这样的设计使得链表中的节点在内存中并不需要连续的存储空间,它们可以分布在内存的任意位置,通过指针相互连接起来。

ini 复制代码
function ListNode(val) {
  this.val = val;
  this.next = null
}

let node1 = new ListNode(1)
node1.next = new ListNode(2)
node1.next.next = new ListNode(3)

这是一个简单的链表。该链表的头节点是node1,它的值为1,下一个节点是2,再下一个节点是3。而

链表的增删查改

在JS中,我们可以使用对象的方式来模拟链表。比如下面这个ListNode类:

kotlin 复制代码
class ListNode {
  constructor(val) {
    this.val = val;
    this.next = null;
  }
}

增:头部插入,尾部插入,指定位置插入。

  • 头部插入
javascript 复制代码
function addAtHead(head, val) {
  const newHead = new ListNode(val); // 创建一个新的节点,值为val
  newHead.next = head; // 将新节点的next指向原来的头节点
  return newHead; // 返回新的头节点
}

在这个函数内部,我们首先创建了一个新的节点newHead,它的值就是我们要插入的val。然后,将新节点的next指向原来的头节点,这样就把原来的链表整体往后移动了一位,而新节点成为了新的头节点。

最后,返回新的头节点即可。

  • 尾部插入
ini 复制代码
function addAtTail(head, val) {
  const newTail = new ListNode(val); // 创建一个新的节点,值为val
  if (!head) { // 如果原链表为空,则直接返回新节点作为头节点
    return newTail;
  }
  let cur = head;
  while (cur.next) { // 找到链表的尾节点
    cur = cur.next;
  }
  cur.next = newTail; // 将新节点连接到尾节点的next
  return head; // 返回头节点
}

在这个函数内部,我们首先创建了一个新的节点newTail,它的值就是我们要插入的val。然后,我们判断原链表是否为空。如果为空,说明原链表只有一个节点或者没有节点,此时直接将新节点作为头节点返回即可。

如果原链表不为空,我们需要找到原链表的尾节点。我们使用一个指针cur来遍历链表,直到找到尾节点,即cur.nextnull。然后,将新节点newTail连接到尾节点的next,即cur.next = newTail

最后,返回头节点即可。

  • 指定位置插入
ini 复制代码
function addAtIndex(head, index, val) {
  if (index < 0) { // 如果index小于0,直接插入到头部
    return addAtHead(head, val);
  }
  const newNode = new ListNode(val); // 创建新节点
  if (index === 0) { // 如果index等于0,直接插入到头部
    newNode.next = head;
    return newNode;
  }
  let cur = head;
  for (let i = 0; i < index - 1; i++) { // 找到要插入位置的前一个节点
    if (!cur) { // 如果cur为null,说明已经到达链表末尾,无法插入
      return head;
    }
    cur = cur.next;
  }
  if (!cur) { // 如果cur为null,说明已经到达链表末尾,无法插入
    return head;
  }
  newNode.next = cur.next; // 将新节点的next指向cur的next
  cur.next = newNode; // 将cur的next指向新节点
  return head;
}

在这个函数内部,首先判断了插入位置index的情况。如果index小于0,就直接调用addAtHead方法将新节点插入到头部;如果index等于0,也直接将新节点插入到头部;否则,需要找到要插入位置的前一个节点,然后进行插入操作。

具体的插入过程如下:

  1. 首先创建一个新的节点newNode,它的值为val
  2. 如果index为0,直接将newNodenext指向head,然后返回newNode作为新的头节点。
  3. 如果index不为0,使用一个指针cur来遍历链表,找到第index-1个节点,即要插入位置的前一个节点。
  4. 然后将newNodenext指向cur.next,将curnext指向newNode

删:头部删除,尾部删除,指定位置删除。

  • 头部删除
javascript 复制代码
function deleteAtHead(head) {
  if (!head) { // 如果链表为空,直接返回null
    return null;
  }
  const newHead = head.next; // 将头节点的下一个节点作为新的头节点
  head.next = null; // 将原头节点断开连接
  return newHead; // 返回新的头节点
}

在这个函数内部,首先判断链表是否为空。如果链表为空,直接返回null

如果链表不为空,我们需要进行删除操作。首先将头节点的下一个节点作为新的头节点,即将head.next赋值给newHead

然后将原头节点的next指针置为null,断开与后续节点的连接,以确保被删除的头节点不再与链表关联。

最后,返回新的头节点newHead

  • 尾部删除
javascript 复制代码
function deleteAtTail(head) {
  if (!head) { // 如果链表为空,直接返回null
    return null;
  }
  if (!head.next) { // 如果链表只有一个节点,删除后返回null
    return null;
  }
  let cur = head;
  while (cur.next.next) { // 找到倒数第二个节点
    cur = cur.next;
  }
  cur.next = null; // 将倒数第二个节点的next指向null,断开与尾节点的连接
  return head; // 返回头节点
}

在这个函数内部,首先判断链表是否为空。如果链表为空,直接返回null

然后判断链表是否只有一个节点。如果链表只有一个节点,删除后返回null

如果链表不为空且节点数大于1,我们需要进行删除操作。首先使用一个指针cur来遍历链表,找到倒数第二个节点,即要删除的节点的前一个节点。

然后将倒数第二个节点的next指针置为null,断开与尾节点的连接,以确保被删除的尾节点不再与链表关联。

最后,返回头节点head

  • 指定位置删除
ini 复制代码
function deleteAtPosition(head, position) {
  if (!head) { // 如果链表为空,直接返回null
    return null;
  }
  if (position === 1) { // 如果要删除的是头节点,直接返回头节点的下一个节点作为新的头节点
    return head.next;
  }
  let cur = head;
  let count = 1;
  while (cur && count < position - 1) { // 找到要删除节点的前一个节点
    cur = cur.next;
    count++;
  }
  if (!cur || !cur.next) { // 如果找不到要删除的位置或者要删除的节点不存在,直接返回头节点
    return head;
  }
  cur.next = cur.next.next; // 将前一个节点的next指向要删除节点的下一个节点,实现删除操作
  return head; // 返回头节点
}

在这个函数内部,首先判断链表是否为空。如果链表为空,直接返回null

然后判断要删除的节点是否是头节点。如果要删除的是头节点,直接返回头节点的下一个节点作为新的头节点。

如果要删除的节点不是头节点,我们需要进行删除操作。首先使用一个指针cur来遍历链表,找到要删除节点的前一个节点。

然后根据指定的位置position,通过循环移动cur指针,直到它指向要删除节点的前一个节点。

如果无法找到要删除的位置或者要删除的节点不存在(curcur.next为空),直接返回头节点。

最后,将前一个节点的next指针指向要删除节点的下一个节点,实现删除操作。

查:查找节点

ini 复制代码
function findNode(head, value) {
  let cur = head;
  while (cur) {
    if (cur.value === value) { // 如果当前节点的值等于目标值,返回当前节点
      return cur;
    }
    cur = cur.next; // 否则继续遍历下一个节点
  }
  return null; // 遍历完整个链表都没有找到目标值,返回null
}

在这个函数内部,我们使用一个指针cur来遍历链表。

在每次循环中,我们首先判断当前节点cur的值是否等于目标值value。如果相等,则表示找到了目标节点,直接返回当前节点cur

如果当前节点的值不等于目标值,我们将指针cur移动到下一个节点,继续遍历。

如果遍历完整个链表都没有找到目标值,即指针cur变为null,那么表示目标节点不存在于链表中,返回null

改:更新节点

ini 复制代码
function updateNode(head, position, value) {
  let cur = head;
  let count = 1;
  while (cur && count < position) { // 找到要更新的节点
    cur = cur.next;
    count++;
  }
  if (!cur) { // 如果要更新的位置超出链表长度,直接返回头节点
    return head;
  }
  cur.value = value; // 更新节点的值
  return head; // 返回头节点
}

在这个函数内部,我们使用一个指针cur来遍历链表,找到要更新的节点。

首先,我们判断链表是否为空。如果链表为空,直接返回头节点。

然后,我们通过循环移动指针cur,直到它指向要更新的节点的位置。我们使用计数器count来记录当前节点的位置。

如果要更新的位置超出了链表的长度(即cur变为null),则直接返回头节点。

接下来,我们将要更新节点的值修改为给定的新值。 最后,返回头节点。

总结

本文详细介绍了链表的增删改查,相信很多看到这里的不少小伙伴已经有疑惑了,链表和数组的差距怎么这么大,像数组的增删改查方便轻松的多。像链表这样麻烦的数据结构有什么用呢?直接用数组不好吗?

其实不管什么数据结构都有其优点与缺点。二者最大的区别便在于:由于链表中的元素在内存中不必连续存储,因此插入和删除一个元素的时间复杂度为O(1),而数组则需要将后面的元素全部向后移动,时间复杂度为O(n)。

感谢各位读者坚持看完本文,如果文章对你有所帮助,还望点个赞支持一下,感谢。

相关推荐
mldong33 分钟前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排1 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60612 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角3 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰3 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi5 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen5 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马6 小时前
AI源码分析:拆解模型底层实现逻辑
前端
BillKu6 小时前
全局样式变量:CSS 自定义属性(--border-color)和SCSS 变量($border-color)的说明
javascript·css·scss