Skip to content

链表

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

指针的类型

  1. 单向链表:只有数据域和指向下一个节点的指针域。
  2. 双向链表:每一个节点具有两个指针域,一个指向下一个节点,一个指向上一个节点。
  3. 环形链表:链表首位相连。

链表的存储方式

链表在内存中不是连续分布的,通过指针域中的指针指向链接在内存中的各个节点。所以链表的内存分布是散乱的分布在内存的某地址上,分配机制取决于操作系统的内存管理。

链表节点的定义

如下是一个链表节点的类:

typescript
/**
 * val:节点值
 * next:指向下一节点的引用
 */
class ListNode {
  constructor(public val?: number, public next?: ListNode | null) {}
}

链遍常用操作:

  1. 初始化链表:

    typescript
    const n0 = new ListNode(1);
    const n1 = new ListNode(3);
    const n2 = new ListNode(2);
    const n3 = new ListNode(5);
    const n4 = new ListNode(4);
  2. 插入节点

    typescript
    /* 在链表的节点 n0 之后插入节点 P */
    function insert(n0: ListNode, P: ListNode): void {
        const n1 = n0.next;
        P.next = n1;
        n0.next = P;
    }
  3. 删除节点

    typescript
    /* 删除链表的节点 n0 之后的首个节点 */
    function remove(n0: ListNode): void {
        if (!n0.next) {
            return;
        }
        // n0 -> P -> n1
        const P = n0.next;
        const n1 = P.next;
        n0.next = n1;
    }
  4. 访问节点

    typescript
    /* 访问链表中索引为 index 的节点 */
    function access(head: ListNode | null, index: number): ListNode | null {
        for (let i = 0; i < index; i++) {
            if (!head) {
                return null;
            }
            head = head.next;
        }
        return head;
    }

练习题

INFO

题意:删除链表中等于给定值 val 的所有节点。

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

示例 2: 输入:head = [], val = 1 输出:[]

示例 3: 输入:head = [7,7,7,7], val = 7 输出:[]

代码示例:

typescript
/**
 * val:节点值
 * next:指向下一节点的引用
 */
class ListNode {
  constructor(public val?: number, public next?: ListNode | null) {}
}

function removeElements(head: ListNode | null, val: number): ListNode | null {
  //使用虚拟头节点的方式
  const dumyHead = new ListNode()
  dumyHead.next = head
  //定义一个临时指针
  let cur = dumyHead
  while (cur.next != null) {
    if (cur.next.val == val) {
      cur.next = cur.next.next
    } else cur = cur.next
  }
  return dumyHead.next
}

INFO

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

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

代码示例:

typescript
/**
 * val:节点值
 * next:指向下一节点的引用
 */
class ListNode {
  constructor(
    public val?: number,
    public next?: ListNode | null,
  ) {}
}

class MyLinkedList {
  // 记录链表长度
  constructor(
    private size: number = 0,
    private head: ListNode | null = null,
    private tail: ListNode | null = null,
  ) {}

  // 获取链表中第 index 个节点的值
  get(index: number): number {
    // 索引无效的情况
    if (index < 0 || index >= this.size) {
      return -1
    }
    let curNode = this.getNode(index)
    // 这里在前置条件下,理论上不会出现 null的情况
    return curNode.val!
  }

  // 在链表的第一个元素之前添加一个值为 val的节点。插入后,新节点将成为链表的第一个节点。
  addAtHead(val: number): void {
    let node: ListNode = new ListNode(val, this.head)
    this.head = node
    if (!this.tail) {
      this.tail = node
    }
    this.size++
  }

  // 将值为 val 的节点追加到链表的最后一个元素。
  addAtTail(val: number): void {
    let node: ListNode = new ListNode(val, null)
    if (this.tail) {
      this.tail.next = node
    } else {
      // 还没有尾节点,说明一个节点都还没有
      this.head = node
    }
    this.tail = node
    this.size++
  }

  // 在链表中的第 index个节点之前添加值为 val的节点。
  // 如果 index等于链表的长度,则该节点将附加到链表的末尾。如果 index大于链表长度,则不会插入节点。如果 index小于0,则在头部插入节点。
  addAtIndex(index: number, val: number): void {
    if (index === this.size) {
      this.addAtTail(val)
      return
    }
    if (index > this.size) {
      return
    }
    // <= 0 的情况都是在头部插入
    if (index <= 0) {
      this.addAtHead(val)
      return
    }
    // 正常情况
    // 获取插入位置的前一个 node
    let curNode = this.getNode(index - 1)
    let node: ListNode = new ListNode(val, curNode.next)
    curNode.next = node
    this.size++
  }

  // 如果索引 index有效,则删除链表中的第 index个节点。
  deleteAtIndex(index: number): void {
    if (index < 0 || index >= this.size) {
      return
    }
    // 处理头节点
    if (index === 0) {
      this.head = this.head!.next!
      // 如果链表中只有一个元素,删除头节点后,需要处理尾节点
      if (index === this.size - 1) {
        this.tail = null
      }
      this.size--
      return
    }
    // 索引有效
    let curNode: ListNode = this.getNode(index - 1)
    curNode.next = curNode.next!.next
    // 处理尾节点
    if (index === this.size - 1) {
      this.tail = curNode
    }
    this.size--
  }

  // 获取指定 Node节点
  private getNode(index: number): ListNode {
    // 这里不存在没办法获取到节点的情况,都已经在前置方法做过判断
    // 创建虚拟头节点
    let curNode: ListNode = new ListNode(0, this.head)
    for (let i = 0; i <= index; i++) {
      // 理论上不会出现 null
      curNode = curNode.next!
    }
    return curNode
  }
}

INFO

  • 题意:反转一个单链表。

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

双指针写法:

typescript
function reverseList(head: ListNode | null): ListNode | null {
  //初始化,pre是链表反转之后链表尾部节点指向null。并添加指针cur指向头部节点
  let pre = null,cur = head
  //遍历链表,结束条件为:原链表的当前节点为null的时候结束遍历
  while (cur) {
    //临时指针存储当前节点的,防止节点丢失
    let temp = cur.next
    //破坏节点的方向,将当前节点的后继节点变成它的前驱节点,实现节点之间方向的反转
    cur.next = pre
    //移动pre的指针让它向前移动一个节点
    pre = cur
    //cur指针也向前推动一个节点
    cur = temp as ListNode | null
  }
  //当cur指针指向null的时候,循环结束,此时pre为反转链表的头部节点
  return pre
}

递归写法:

typescript
function reverse(cur: ListNode | null, pre: ListNode | null): ListNode | null {
  //递归终止的条件,就是当前节点的为null,此时 pre 代表反转之后的链表的头部节点
  if (cur == null) return pre
  //临时存储当前节点的后继节点的引用
  let temp = cur.next as ListNode | null
  //破坏节点的方向,将当前节点的后继节点指向 pre 节点的引用,就是指向当前节点的前一个节点
  cur.next = pre
  //递归调用,实现cur,prev指针向前移动一位
  return reverse(temp, cur)
}

function reverseList(head: ListNode | null): ListNode | null {
  //初始化,第二个参数传递null是反转链表之后的新链表的尾部节点的nest域指向null
  return reverse(head, null)
}

INFO

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

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

代码示例:

typescript
/**
 * val:节点值
 * next:指向下一节点的引用
 */
class ListNode {
  constructor(public val?: number, public next?: ListNode | null) {}
}

function swapPairs(head: ListNode | null): ListNode | null {
  //生成虚拟头节点
  const dumyHead = new ListNode()
  //虚拟头节点的next域指向原链表头节点
  dumyHead.next = head
  //要操作原链表的第一个和第二个节点,需要将cur指针放在虚拟头节点的位置(大体的思路是:如果要操作索引为1,2的元素,则需要将cur指针指向索引为0的节点)
  let cur = dumyHead
  while (cur?.next != null && cur?.next!.next != null) {
    //缓存要操作的第一个节点
    const temp = cur.next
    //缓存第二个节点的 next 域的的引用,如果不缓存的话,在改变第二个节点的 next 域指向第一个节点的时候(为了实现交换),原来指向第三个节点的引用会丢失
    const temp1 = cur.next.next?.next
    //将指向第一个节点的引用指向第二个节点(实现将第二个节点元素交换到第一个节点元素的位置)
    cur.next = cur.next.next
    //将第二个节点的引用指向第一个节点,实现将第一个元素换到第二个节点的位置
    cur.next!.next = temp
    //将新链表的第二个节点的引用指向原链表的第二个节点的next域的引用
    temp.next = temp1
    //推进cur指针指向下个要被操作的两个元素之前的节点
    cur = cur.next!.next
  }
  //返回新链表头部节点
  return dumyHead.next
}

INFO

删除列表中倒数第N个节点

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

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

代码示例:

typescript
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  const dumyHead = new ListNode();
  dumyHead.next = head;
  //防止当n大于链表长度,指针提前为null。
  n++;
  //初始化快慢指针
  let fast = dumyHead,slow = dumyHead;
  //先让快指针移动 n + 1步
  while (n-- && fast != null) {
    fast = fast.next!;
  }
  //然后快慢指针同时移动,此时如果快指针迭代到null的时候,慢指针刚好位于被删除元素的前一个元素
  while (fast != null) {
    fast = fast.next!;
    slow = slow.next!;
  }
  //改变慢指针的指向的节点的next域实现删除节点
  slow.next = slow.next?.next;
  return dumyHead.next;
}

INFO

环形链表

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

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

代码示例:

typescript
function detectCycle(head: ListNode | null): ListNode | null {
  let slowNode: ListNode | null = head,
    fastNode: ListNode | null = head;
  while (fastNode !== null && fastNode.next !== null) {
    slowNode = slowNode!.next!;
    fastNode = fastNode.next!.next!;
    if (slowNode === fastNode) {
      slowNode = head;
      while (slowNode !== fastNode) {
        slowNode = slowNode!.next!;
        fastNode = fastNode!.next!;
      }
      return slowNode;
    }
  }
  return null;
}