面试必学数据结构 - 链表

210 阅读9分钟

链表

链表,这一经典的数据结构,自计算机科学诞生以来便扮演着举足轻重的角色。它摒弃了数组必须依赖连续内存空间的限制,转而通过节点间的指针或链接,将离散的数据元素串联起来,形成了一条灵活多变的数据链。链表不仅具备出色的动态扩展能力,能够在运行时高效地插入、删除和遍历节点,还以其独特的结构特性,为算法设计与实现提供了丰富的灵感与可能。在当今的软件开发中,链表依然是处理动态数据集合的重要工具,其价值与意义不容小觑。

链表规则

在js中没有专门打造一个链表结构,但是我们可以将对象要求成链表的形式来使用,链表和数组一样都是线性的,但是数组是连续的,而链表是离散的,在数组中我们称里面的内容为元素,而链表我们称里面的内容为节点,下面是数组和链表得区别图

1.png 链表的访问顺序也是从1开始访问到5的

head = {
    val:1,
    next:{
        val: 2,
        next:{
            val:3,
            next: ...
        }
    }
}

这个就是一个链表结构,这一整段代码是一个对象,而对象是存储在堆当中的,那么它就会有一个引用地址,而里面的next又是一个对象,这个对象也有一个引用地址。我们称最外层的是第一个节点,下一个next是第二个节点,接着写也是一直按照这个规则。

而我们想要访问到其中一个节点的内容,必须从第一个节点开始访问,一直到我们想要访问的内容,如果其中有一个无法访问,那么我们也就没有办法访问到我们想要访问的内容

就好像我们平时只能联系我们的老师,老师联系年级主任,主任联系校长,我们是无法直接联系到校长的。如果出现了一个副校长,那么年级主任的上级就指向了副校长,副校长的上一级为校长。如果移除这个副校长,那么直接将年级主任的上一级变为校长就好了

1gif.gif

链表的优缺点

优点

假如我们要往数组中里面添加一个元素,要不要时间呢,肯定是要的,因为这个元素后面的元素都要往后挪一个,时间复杂度是n,而在链表中,我们只需要改变上一级的指向就可以了,时间复杂度为1,可以高效的增删节点

缺点

在链表中,如果我们想要访问一个节点,那么我们就必须从第一个节点开始,一级一级的向下访问,而且要保证其中的每一级都可以访问,所有链表的访问效率是很低效的

leetcode链表题

合并两个有序链表(21)

题目:我们将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有节点组成的


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


  @param {ListNode} list1
  @param {ListNode} list2
  @return {ListNode}
 
var mergeTwoLists = function(list1, list2) {
   const res = new ListNode(); //创建一个对象,用对象中的next属性来记录链表中对象属性val较小的对象
   const head = res; //记录最初始的我们创建的对象地址值
   //循环到其中一个链表的指向为null
   while(list1&&list2){
    //如果list2.val更小,那么用res.next属性指向这个对象
   if(list1.val>list2.val){
    res.next = list2
    //list2的链表从下一个对象开始比较
    list2 = list2.next;
   }
   else{
    res.next = list1
    //list1的链表从下一个对象开始比较
    list1 = list1.next
   }
   //用res的next属性得到的对象的next去记录下一个链表比较出来的val属性更小的对象
   res = res.next;
}  
//判断list1链表是否为空,如果不为空则最后将list1放在新链表中,如果为空,则将list2放在新链表中
res.next= list1!==null? list1:list2;
//我们将初始记录的地址值的的第一个next属性记录的对象返回即可完成题目要求
   return head.next;
};
};

上面是题目的提示,让我们知道list中有两个参数,一个是它本身的内容,还有一个是它的指向,我这里使用的是双指针法,具体怎么实现的代码上有注释。

删除排序链表中的重复元素(83)

题目: 给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 ,返回 已排序的链表 。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    //判断头节点的指向是否为null,如果是,直接返回头节点
    if(!head) return head
    //先记录初始化的头节点
    let res = head;
    //判断对象的指向是否为null
    while(res.next){
    //判断对象的内容是否等于它所指向的对象内容,相等则使它指向的对象变为原来对象的对象
    if(res.val==res.next.val){
        res.next = res.next.next;
    }
    //不相等则它指向的对象不变,使它指向的对象变为res,让下一轮判断指向对象的指向对象是否相等
    else{
        res.next==res.next
        res=res.next
        }
}
//最后返回我们初始记录的头节点
return head;
};

代码上半部分是告诉我们每个对象中有val属性和next属性。

这里使用了迭代法(双指针法的简化版)来遍历链表并删除重复节点。

删除排序链表中的重复元素Ⅱ(82)

题目:给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 ,返回 已排序的链表 。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var deleteDuplicates = function(head) {
    //题目要求是删除原始链表中所有重复数字的节点
    //所有第一个节点可能也会被删除
    //所有我们创建一个我们能控制不被删除的哑节点
    let dummy = new ListNode()
    //在哑节点上添加头节点
    dummy.next = head
    //记录初始地址值
    let res = dummy
    //由于我们要用到res对象的指向对象和指向对象
    //所有我们使用它们为循环条件,且顺序不能反
    while(res.next&&res.next.next){
        //判断res对象的指向的对象内容是否等于res指向对象的指向对象的内容
    if(res.next.val==res.next.next.val){
        //相等的话定义一个变量记录最开始的res指向对象的内容
        let value = res.next.val;
        //我们更改res的指向,直到res指向的对象与我们使用value记录的对象内容不同
        while(value == res.next.val){
            res.next = res.next.next;
        }
    }
    //不相同则开始下一节点的判断
    else{
        res=res.next
    }
}
//最后返回我们记录的初始地址值的next属性记录的对象
return dummy.next
};

这种方法是一种基于迭代和哑节点的链表处理技术,它有效地解决了删除链表中所有重复节点的问题,包括可能删除头节点的情况。这种方法的时间复杂度是O(n),其中n是链表的长度,因为每个节点最多被访问一次。空间复杂度是O(1),因为除了几个必要的指针和变量外,没有使用额外的数据结构。

反转链表(262)

题目:给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    //先判断head是否为空,如果为空则直接返回head
    if(!head) return head
    //定义一个指针记录我们操作对象要指向的上一个对象
    let per = null
    //定义一个指针为我们要改变它指向的对象
    let cur = head
    //定义一个指针记录将断开和上一级联系的对象
    let next = cur.next
    //循环条件为当我们要改变指向的值存在
while(cur){
    //使该对象断开和之前后一级的联系,指向前一级
    cur.next=per
    //使前一级对象变为我们记录好的要改变指向的对象
    per = cur
    //使我们要改变的对象为下一级
    cur = next
    //由于可能cur为链表的最后一项
    //最后一项的next属性为null,找null就会报错
    //所有我们要先判断next熟悉不为null,才能使next = next.next。
    if(next){
    next=next.next;
    }
}
//循环结束,cur为null,per为指针该变的最后一项,也就是新链表的前一项
return per
};

这种方法是一种简单而有效的单链表反转方法,它不需要额外的数据结构,只需要几个指针的迭代更新即可实现链表的反转。时间复杂度为O(n),其中n是链表的长度,因为每个节点都被访问一次。空间复杂度为O(1),因为只使用了几个指针变量。

总结

链表作为一种经典的数据结构,在计算机科学中占据着举足轻重的地位。它打破了数组必须依赖连续内存空间的限制,通过节点间的指针链接,将离散的数据元素串联起来,形成了灵活多变的数据链。链表在处理动态数据集合时展现出极高的效率,特别是在需要频繁插入和删除操作的场景中。然而,链表也有其局限性,如访问效率相对较低,需要从头节点开始逐级访问。

在JavaScript中,虽然没有内置的链表结构,但我们可以通过对象模拟链表的形式。链表中的每个节点都包含数据部分(val)和指向下一个节点的指针(next)。这种结构使得链表在处理动态数据时具有出色的性能。

在解决LeetCode等算法题时,链表题目常常涉及合并、删除重复元素和反转等操作。这些操作都可以通过迭代或递归的方式实现。例如,合并两个有序链表时,可以使用双指针法高效地比较和拼接节点;删除重复元素时,需要遍历链表并判断相邻节点的值是否相同;反转链表时,则可以通过迭代更新指针的指向来实现。

总的来说,链表是一种功能强大且灵活多变的数据结构,它广泛应用于各种算法和系统中。掌握链表的基本概念和操作方法,对于提高编程能力和解决复杂问题具有重要意义。