leetCode链表相关

本文详细介绍了链表的基本概念、类型及其与数组的区别。讨论了链表的插入、删除、查询操作,并通过LeetCode题目展示了具体实现,如删除指定元素、设计链表、反转链表、两两交换节点、删除倒数第N个节点以及查找链表相交节点。此外,还探讨了环形链表的检测和处理。通过实例解析,加深了对链表操作的理解。
摘要由CSDN通过智能技术生成

链表

什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点是又两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。

链表分为单链表,双链表,循环链表

class Node {
    constructor(data) {
        this.data = data;  // 节点的数据域
       // 节点的指针域 单链表中用不上prev前指针
        this.prev = null;  
        this.next = null;  // 节点的指针域
    }
}

链表和数组的区别:

  • 数组在内存中是连续分布的,但是链表在内存中不是连续分布的
  • 链表是通过指针域中指针链接在内存中的各个节点
  • 所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
类型插入/删除时间复杂度查询适用场景
数组O(n)O(1)数据量固定,频繁查询,较少增删
链表O(1)O(n)数据量步固定,频繁增删,较少查询

链表的操作

  1. 删除节点,比如p->q->s,如果我们想要删除q节点,我们只用将p节点的next指针指向s节点即可。(此时q节点依然存在于内存中,所以需要内存释放。)
  2. 增加节点,比如p->q->s,如果我们想要增加节点t在p和q中间,只需要将p的next指针指向t,t的next指针指向q,就完成了添加操作
  • 可以看到,链接的增加节点和删除节点操作时间复杂度都是O(1)。
  1. 查询节点,链接只支持从头到尾进行查找,不像数组那样,找到第五个数直接array[4]。因此查询时间复杂度为O(n)

1、移除链表元素

  1. leetCode203移除链表元素

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

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

迭代的方法删除链表中所有节点值等于特定值的节点。

用temp 表示当前节点。

  1. 如果temp 的下一个节点不为空且下一个节点的节点值等于给定的val,则需要删除下一个节点。删除下一个节点可以通过以下做法实现:

Temp.next=temp.next.next

  1. 如果temp 的下一个节点的节点值不等于给定的 \textit{val}val,则保留下一个节点,将temp 移动到下一个节点即可。

  2. 当temp 的下一个节点为空时,链表遍历结束,此时所有节点值等于 val 的节点都被删除。

具体实现方面,由于链表的头节点head 有可能需要被删除,因此创建哑节点 dummyHead,令dummyHead.next=head,初始化temp=dummyHead,然后遍历链表进行删除操作。最终返回dummyHead.next 即为删除操作后的头节点。

var removeElements = function(head, val) {
    const dummyHead = new ListNode(0);
    dummyHead.next = head;
    let temp = dummyHead;
    while (temp.next !== null) {
        if (temp.next.val == val) {
            temp.next = temp.next.next;
        } else {
            temp = temp.next;
        }
    }
    return dummyHead.next;
};

2. 设计链表

  1. leetCode707 设计链表

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3
class MyLinkedList {
  constructor () {
    this.data = null
  }
  // 获取链表中第 index 个节点的值。如果索引无效,则返回-1
  get (index) {
    let curr = this.data
    for (let i = 0; i < index; i++) {
      if (!curr) break
      curr = curr.next
    }
    return curr ? curr.val : -1
  }
  // 在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点
  addAtHead (val) {
    this.data = { val, next: this.data }
  }
  // 将值为 val 的节点追加到链表的最后一个元素
  addAtTail (val) {
    let curr = this.data
    if (!curr) return this.data = { val, next: null }
    while (curr.next) {
      curr = curr.next
    }
    curr.next = { val, next: null }
  }
  // 在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点
  addAtIndex (index, val) {
    if (index <= 0) return this.addAtHead(val)
    let curr = this.data
    index--
    while (index && curr.next) {
      index--
      curr = curr.next
    }
    if (index > 0) return
    let tmp = curr.next
    curr.next = { val, next: tmp }
  }
  // 如果索引 index 有效,则删除链表中的第 index 个节点
  deleteAtIndex (index) {
    let curr = this.data, prev = null
    if (!curr) return
    if (index === 0) return this.data = curr.next
    while (index && curr.next) {
      index--
      prev = curr
      curr = curr.next
    }
    if (index > 0) return
    prev.next = curr.next
  }
}

3. 翻转链表

  1. leetCode206 反转链表

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

var reverseList = function(head) {
  let prev = null,
      curr = head;
  while (curr !== null) {
    [curr.next, prev, curr] = [prev, curr, curr.next];
  }
  return prev;
};

var reverseList = function (head) {
  let prev = null       // 尾随cur的prev指针,开始时指向null
  let cur = head        // 推进指针,开始时指向头结点
  while (cur) {         // cur指针推进到null节点,则退出循环
    let next = cur.next // 暂存cur的下一节点
    cur.next = prev     // 将cur的next指针指向prev
    prev = cur          // 将prev更新为cur节点
    cur = next          // 将cur指针推进一个节点
  }                     // 退出while时,cur指向null,prev指向原链的尾节点
  return prev
};

4. 交换链表

  1. leetCode24 两两交换链表中的节点

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

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

输入:head = [1,2,3,4]
输出:[2,1,4,3]
const swapPairs = (head) => {
  const dummy = new ListNode(0);
  dummy.next = head;
  let prev = dummy;

  while (head && head.next) {
    const next = head.next; // 临时保存head.next,因为head.next待会要改变
    head.next = next.next;
    next.next = head;
    prev.next = next;  

    prev = head;      // 指针更新
    head = head.next; // 指针更新
  }
  return dummy.next;
};

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

  1. leetCode19 删除链表的倒数第 N 个结点

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

**进阶:**你能尝试使用一趟扫描实现吗?

思路

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

const removeNthFromEnd = (head, n) => {
    // 定义虚拟节点
    const dummy = new ListNode(0, head)
        // 定义左右指针,都指向虚拟节点
    let left = dummy,
        right = dummy
        // 右指针先走n+1步
    while (1 + n--) {
        right = right.next
    }
    // 如果此时右指针到null了,说明删除的是第一个节点
    // 直接返回dummy.next.next;
    if (!right) return dummy.next.next
        // 右指针没到头
    while (right) {
        // 左右指针一起走
        right = right.next
        left = left.next
    }
    // 右指针走到null之后,删除左指针的下一个节点即可
    left.next = left.next.next
        // 返回虚拟节点的next
    return dummy.next
}

6. 链表相交

  1. leetCode的面试题02.07 链表相交

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

思路

解法1:

两个指针最多走过headA链表长度 + headB链表长度的距离

  1. 如果相交,会提前相遇在相交节点。此时返回相交节点。
  2. 如果不相交,则各自走过headA链表长度 + headB链表长度的距离,指向null。此时返回null.
var getIntersectionNode = function(headA, headB) {
    var p1 = headA, p2 = headB;
    while (p1 != p2) {
        p1 = p1 ? p1.next : headB;
        p2 = p2 ? p2.next : headA;
    }
    return p1;
};

解法2:

  1. 先获取两个链表的长度,
  2. 然后判定谁是长的链表谁是短的链表,获取长链表和短链表的长度差
  3. 相差多少则长链表就先走多少步,然后遍历长链表或者遍历短链表都可以
  4. 因为此时的长链表和短链表长度都一样了,所以遍历谁都可以
  5. 如果相等则返回链接,
  6. 如果遍历完毕都还没有找到相等的链接,则直接返回null,说明此时并没有找到相交链接
function getListNodeLength(head) {
    if (head == null) return 0
    let len = 0
    let curr = head
    while (curr != null) {
        curr = curr.next
        len++
    }
    return len
}
var getIntersectionNode = function(headA, headB) {
    let lenA = getListNodeLength(headA)
    let lenB = getListNodeLength(headB)
    let diff = 0
    let longer = headA
    let shorter = headB
    if (lenA > lenB) {
        diff = lenA - lenB
    } else {
        diff = lenB - lenA
        longer = headB
        shorter = headA
    }
    // 相差多少个长度,就让长的链表先多走步
    for (let i = 0; i < diff; i++) {
        longer = longer.next
    }
    while (longer != null) {
        if (longer == shorter) {
            return longer // 返回shorter 或者longer都可以
        }
        longer = longer.next
        shorter = shorter.next
    }
    return null
}

环形链表

  1. leetCode142 环形链表 II

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

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

思路

首先需要判断是否有环

  1. 可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

    为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

    首先第一点: fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

  2. 如果有环,就判断环的入口,那么就根据代码随想录即可明白啦。y和z是相等的

  3. 这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点。也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

var detectCycle = function (head) {
  let slow = head;
  let fast = head;
  while (fast) {
    if (fast.next == null) { // fast.next走出链表了,说明无环
      return null;
    }
    slow = slow.next;        // 慢指针走一步
    fast = fast.next.next;   // 慢指针走一步
    if (slow == fast) {      // 首次相遇
      fast = head;           // 让快指针回到头节点
      while (true) {         // 开启循环,让快慢指针相遇
        if (slow == fast) {  // 相遇,在入环处
          return slow;
        }
        slow = slow.next;
        fast = fast.next;    // 快慢指针都走一步
      }
    }
  }
  return null;
};

总结

关于链表的相关几道leetCode题,参考各种大佬的解法,了解了一些相关基础。觉得自己写下来印象会更深刻些

参考

  1. 代码随想录
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值