前端学算法-链表

140 阅读6分钟
基本概念

链表(Linked List)是一种线性数据结构,由一系列节点(Node)组成,每个节点包含两部分:存储的数据(data)和指向下一个节点的指针(next)。链表在内存中不需要连续的存储空间,而是通过指针动态地连接各个节点。

单链表

单链表是最简单的链表形式,每个节点只包含一个指向下一个节点的指针。

首先定义节点类型:

class Node {
  constructor(data) {
    this.data = data; // 存储数据
    this.next = null; // 指向下一个节点的指针
  }
}

接下来根据上面的节点类型定义一个链表,包含基本的操作方法:

class LinkedList {
  constructor() {
    this.head = null; // 链表的头节点
  }
 
  // 在链表末尾插入节点
  append(data) {
    const newNode = new Node(data);
    if (this.head === null) {
      this.head = newNode;
    } else {
      let current = this.head;
      while (current.next !== null) {
        current = current.next;
      }
      current.next = newNode;
    }
  }
 
  // 在链表头部插入节点
  prepend(data) {
    const newNode = new Node(data);
    newNode.next = this.head;
    this.head = newNode;
  }
 
  // 删除链表中第一个匹配的节点
  delete(data) {
    if (this.head === null) return;
 
    if (this.head.data === data) {
      this.head = this.head.next;
      return;
    }
 
    let current = this.head;
    while (current.next !== null) {
      if (current.next.data === data) {
        current.next = current.next.next;
        return;
      }
      current = current.next;
    }
  }
 
  // 查找链表中是否存在某个数据
  contains(data) {
    let current = this.head;
    while (current !== null) {
      if (current.data === data) {
        return true;
      }
      current = current.next;
    }
    return false;
  }
 
  // 打印链表中的所有数据
  print() {
    let current = this.head;
    let result = '';
    while (current !== null) {
      result += current.data + ' -> ';
      current = current.next;
    }
    console.log(result + 'null');
  }
}
移除链表元素

力扣题目链接

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

var removeElements = function(head, val) {
    // 创建一个虚拟头节点,简化删除头节点的逻辑
    let dummy = new ListNode(0); // 虚拟头节点
    dummy.next = head;
    let current = dummy;
 
    while (current.next !== null) {
        if (current.next.val === val) {
            // 删除当前节点的下一个节点
            current.next = current.next.next;
        } else {
            // 否则,移动到下一个节点
            current = current.next;
        }
    }
 
    return dummy.next; // 返回链表的头节点
};
 
function ListNode(val, next) {
    this.val = (val === undefined ? 0 : val);
    this.next = (next === undefined ? null : next);
}

这个也算比较简单,设置一个虚拟头节点,开始遍历链表,然后就判断当前节点和val相等就移除节点,移除也比较简单就是current.next = current.next.next

翻转链表

力扣题目链接

题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

这个题虽然是简单难度的,但是没写出来,

var reverseList = function(head) {
    if(!head || !head.next) return head;
    let temp = null, pre = null, cur = head;
    while(cur) {
        temp = cur.next;
        cur.next = pre;
        pre = cur;
        cur = temp;
    }
    // temp = cur = null;
    return pre;
};

这里使用的是双指针的解法,也比较好理解。

核心逻辑就是这几步:

temp = cur.next;
cur.next = pre;
pre = cur;
cur = temp;
两两交换链表中的节点

力扣题目链接(opens new window)

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

这个题没任何思路,直接看题解

var swapPairs = function (head) {
  let ret = new ListNode(0, head), temp = ret;
  while (temp.next && temp.next.next) {
    let cur = temp.next.next, pre = temp.next;
    pre.next = cur.next;
    cur.next = pre;
    temp.next = cur;
    temp = pre;
  }
  return ret.next;
};

两两一组,翻转链表

删除链表的倒数第N个节点

力扣题目链接(opens new window)

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

这个题第一想法就是获取链表的长度,然后用长度减去n,遍历时开始计数,当遍历到这个差值是直接返回出去。

var removeNthFromEnd = function(head, n) {
    let len = 0;
    let current = head;
 
    // 计算链表的长度
    while (current) {
        len++;
        current = current.next;
    }
 
    // 计算要删除的节点的位置
    let targetIndex = len - n;
 
    // 创建一个虚拟头节点,方便处理删除头节点的情况
    let dummy = new ListNode(0);
    dummy.next = head;
    current = dummy;
 
    // 遍历到要删除的节点的前一个节点
    for (let i = 0; i < targetIndex; i++) {
        current = current.next;
    }
 
    // 删除节点
    current.next = current.next.next;
 
    return dummy.next;
};

看了题解,有个更巧妙的做法,使用快慢指针,首先将快指针移动至n,然后双指针同步移动,当快指针到最后一个时,慢指针就移动到将要删除的节点了,然后删除节点即可slow.next = slow.next.next

var removeNthFromEnd = function (head, n) {
  // 创建哨兵节点,简化解题逻辑
  let dummyHead = new ListNode(0, head);
  let fast = dummyHead;
  let slow = dummyHead;
  while (n--) fast = fast.next;
  while (fast.next !== null) {
    slow = slow.next;
    fast = fast.next;
  }
  slow.next = slow.next.next;
  return dummyHead.next;
};
链表相交

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

图示两个链表在节点 c1 开始相交:

这个题竟然是简单级别的,自己真菜。

首先想了下,没思路,看了下力扣上的提示,说可以先判断链表是否相交,判断相交到是很简单,首先遍历第一个链表,拿到最后一个节点,然后再遍历第二个节点,判断前一个最后的节点是不是在第二个的的链表上就可以了,顺着这个思路,来个暴力破解,两层for循环,判断前一个节点是不是在第二个节点上,如果在那就是相交

var getIntersectionNode = function(headA, headB) {
    
    let listA = headA
    let listB = headB
​
    while(listA){
        listB = headB;
        while(listB){
            if(listA == listB){
                return listA
            }
            listB = listB.next
        }
        listA = listA.next
    }
​
    return null
};

也能通过测试。

这里有个关键点,在遍历listB之前,需要重置下listB = headB,不然会listA遍历一个节点,listB直接退出了。

看了题解,发现一个更好的解法,首先计算两个链表的长度,找到差值,然后判断长链表减去差值 和 短链表是否一样,如果不一样就开始同步遍历,然后判断是否相等,这个方法确实巧妙,时间复杂度只有o(n)

var getIntersectionNode = function(headA, headB) {
    if (!headA || !headB) return null;
 
    // 计算链表 A 的长度
    let lenA = 0, lenB = 0;
    let currentA = headA, currentB = headB;
 
    while (currentA) {
        lenA++;
        currentA = currentA.next;
    }
 
    while (currentB) {
        lenB++;
        currentB = currentB.next;
    }
 
    // 重置指针
    currentA = headA;
    currentB = headB;
 
    // 让较长的链表先移动差值步
    if (lenA > lenB) {
        for (let i = 0; i < lenA - lenB; i++) {
            currentA = currentA.next;
        }
    } else {
        for (let i = 0; i < lenB - lenA; i++) {
            currentB = currentB.next;
        }
    }
 
    // 同时移动两个指针,直到找到交点
    while (currentA && currentB) {
        if (currentA === currentB) {
            return currentA;
        }
        currentA = currentA.next;
        currentB = currentB.next;
    }
 
    return null;
};
环形链表

力扣题目链接(opens new window)

题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

这个题第一想法就是创建一个hash表,然后开始遍历链表,挨个向hash表中塞值,如果是环形链表,一定会在hash表中有重复的

var detectCycle = function(head) {
    if (!head) return null;
    
    const map = new Map();
    let current = head;
    let index = 0;
    
    while (current) {
        if (map.has(current)) {
            return current; // 找到环的起点
        }
        map.set(current, index); // 存储节点和索引
        current = current.next;
        index++;
    }
    
    return null; // 无环
};

更优的解法,快慢指针

var detectCycle = function(head) {
    if (!head || !head.next) return null;
    
    let slow = head;
    let fast = head;
    
    // 第一步:检测是否有环
    while (fast && fast.next) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow === fast) break; // 相遇点
    }
    // 无环
    if (slow !== fast) return null;
    
    // 第二步:找到环的起点
    slow = head;
    while (slow !== fast) {
        slow = slow.next;
        fast = fast.next;
    }
    
    return slow;
};

看下这个图片就知道了,画的很形象

首先定义一个快指针,每次遍历两个,定义一个慢指针,每次遍历一个,第一次相遇时循环体中,然后一个指针在相遇点开始遍历,一个指针从头开始遍历,下次相遇就是环的起点了。