链表知识点与经典题型

338 阅读9分钟

image.png

1.知识点

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

链表的类型

单链表:每个节点只有一个指针域,指向下一个节点。

双链表:每个节点有两个指针域,一个指向上一个节点,一个指向下一个节点。

循环链表:链表收尾相连。

链表的存储方式

数组在内存中是连续分布的。

链表是通过指针域的指针链接内存中的各个节点,所以链表中的节点在内存中不是连续分布的。

链表的操作

删除节点

删除D节点,如图所示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/35f34ab900524c828995aba55bfcb6bb~tplv-k3u1fbpfcp-zoom-1.image

只要将C节点的next指针 指向E节点就可以了。

添加节点

如图所示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1eb924fb7f1b494c8f3fbb2821708112~tplv-k3u1fbpfcp-zoom-1.image

可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。

但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点,通过next指针进行删除操作,查找的时间复杂度是O(n).

性能分析

再把链表的特性和数组的特性进行一个对比,如图所示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6643b4b264eb464fbef065697a6fddac~tplv-k3u1fbpfcp-zoom-1.image

数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。

链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。

经典题型

1.移除链表元素

题意:删除链表中等于给定值 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
输出:[]

LeetCode 第203题

思路:

找到需要删除节点的上一个节点,然后把上一个节点的next指向需要删除的节点的下一个节点。如果是头结点呢,则有两种方式,一种是把头结点指向下一个节点,一种是设置一个虚拟头结点,把它的next节点指向头结点,这样就跟删除其他节点一样操作。

// 在原来链表操作
var removeElements = function (head, val) {
    //如果头结点需要删除,则把头结点指向下一个节点
    while (head && head.val === val) {
        head = head.next
    }
    let cur = head
    while (cur && cur.next) {
        if (cur.next.val === val) {
            cur.next = cur.next.next
        } else {
            cur = cur.next
        }
    }
    return head
}
// 设置一个虚拟头结点
var removeElements = function (head, val) {
    if (!head) return null
    let cur = dummyHead = new ListNode(0, head)
    while (cur && cur.next) {
        if (cur.next.val === val) {
            cur.next = cur.next.next
        } else {
            cur = cur.next
        }
    }
    return dummyHead.next
};

LeetCode 第707题

2.设计链表

题意:

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

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
// @lc code=start
var ListNode = function (val, next) {
    this.val = val
    this.next = next
}
/**
 * Initialize your data structure here.
 */
var MyLinkedList = function () {
    this.head = null
    this.tail = null
    this.size = 0
};

/**
 * Get the value of the index-th node in the linked list. If the index is invalid, return -1.
 * @param {number} index
 * @return {number}
 */
MyLinkedList.prototype.getNode = function (index) {
    // 判断边界条件
    if (index < 0 || index >= this.size) return null
    let node = this.head
    while (index-- > 0) {
        node = node.next
    }
    return node
};

MyLinkedList.prototype.get = function (index) {
    if (index < 0 || index >= this.size) return -1
    return this.getNode(index).val
}

/**
 * Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtHead = function (val) {
    const node = new ListNode(val, this.head)
    this.head = node
    if (!this.tail) {
        this.tail = this.head
    }
    this.size++
};

/**
 * Append a node of value val to the last element of the linked list.
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtTail = function (val) {
    const node = new ListNode(val)
    if (!this.tail) {
        this.head = this.tail = node
    } else {
        this.tail.next = node
        this.tail = node
    }
    this.size++
};

/**
 * Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
 * @param {number} index
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtIndex = function (index, val) {
    if (index <= 0) return this.addAtHead(val)
    if (index === this.size) return this.addAtTail(val)
    if (index > this.size) return
    const node = this.getNode(index - 1)
    node.next = new ListNode(val, node.next)
    this.size++
};

/**
 * Delete the index-th node in the linked list, if the index is valid.
 * @param {number} index
 * @return {void}
 */
MyLinkedList.prototype.deleteAtIndex = function (index) {
    if (index < 0 || index >= this.size) return
    if (index === 0) {
        this.head = this.head.next
        this.size--
        return
    }
    const node = this.getNode(index - 1)
    node.next = node.next.next
    // 处理尾节点
    if (index === this.size - 1) {
        this.tail = node
    }
    this.size--
};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * var obj = new MyLinkedList()
 * var param_1 = obj.get(index)
 * obj.addAtHead(val)
 * obj.addAtTail(val)
 * obj.addAtIndex(index,val)
 * obj.deleteAtIndex(index)
 */

3.反转链表

题意:反转一个单链表。

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

来源: LeetCode 第 206 题

思路:

改变链表的next指针的指向,直接将链表反转。

迭代法

var reverseList = function (head) {
  let pre = null, cur = head
  while (cur) {
    let next = cur.next
    // 把next指针指向前面节点
    cur.next = pre
    pre = cur
    cur = next
  }
  return pre
}

递归法

var reverseList = function (head) {
  const reverse = (pre, cur) => {
    if (!cur) return pre
    let next = cur.next
    cur.next = pre
    return reverse(cur, next)
  }
  return reverse(null, head)
}

var reverseList = function (head) {
  const reverse = (node) => {
    if (!node || !node.next) return node
    // 找到尾节点
    let tail = reverse(node.next)
    // 修改节点的next指向
    node.next.next = node
    node.next = null
    return tail
  }
  return reverse(head)
}

4.两两交换链表中的节点

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

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

示例 1:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efa7d87855b14e1da0a007bb977247b8~tplv-k3u1fbpfcp-zoom-1.image

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

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

来源: LeetCode 第 24 题

思路:

使用虚拟头结点,然后就是交换相邻两个元素,这时一定要画图,不画图,操作多个指针很容易乱。

初始时,cur指向虚拟头结点,然后进行如下三步:

https://code-thinking.cdn.bcebos.com/pics/24.两两交换链表中的节点1.png

迭代法

var swapPairs = function (head) {
    let cur = dummyHead = new ListNode(null, head)
    let node1, node2
    while (cur.next && cur.next.next) {
        node1 = cur.next
        node2 = cur.next.next
        node1.next = node2.next
        node2.next = node1
        cur.next = node2
        cur = node1
    }
    return dummyHead.next
}

递归法

递归的写法相对简洁点

var swapPairs = function (head) {
    if (!head || !head.next) return head
    let next = head.next
    let nextHead = swapPairs(head.next.next)
    next.next = head
    head.next = nextHead
    return next
}

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

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

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

示例 1:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3c2c4b9772fd456a9991fdc60ed54db4~tplv-k3u1fbpfcp-zoom-1.image

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

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

来源: LeetCode 第 19 题

思路:

由于要删除倒数第n个节点,那我们就得找到倒数第n+1个节点,我们知道单链表只能往后找,那怎么找倒数第n+1个呢?刚开始的想法是先遍历一遍,算出链表的长度,然后再减去n+1个,算出要遍历的次数,就能找到我们要删除的节点的前一个节点。但是这样的话需要遍历两遍,不符合题意。看了别人的解法才知道,原来还可以用双指针,让快指针先走n+1步,然后慢指针和快指针同时走,当快指针走到结尾,慢指针也就是我们要找的节点。

var removeNthFromEnd = function (head, n) {
    let dummyHead = new ListNode(0, head)
    let slow = dummyHead, fast = dummyHead
    while (n--) {
        fast = fast.next
    }
    while (fast && fast.next) {
        fast = fast.next
        slow = slow.next
    }
    slow.next = slow.next.next
    return dummyHead.next
};

6.链表相交

给定两个(单向)链表,判定它们是否相交并返回交点。请注意相交的定义基于节点的引用,而不是基于节点的值。换句话说,如果一个链表的第k个节点与另一个链表的第j个节点是同一节点(引用完全相同),则这两个链表相交。

示例 1:

输入:listA = [4,1,8,4,5], listB = [5,0,1,8,4,5]

输出:Reference of the node with value = 8

输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

来源: LeetCode 02.07. 链表相交

7.环形链表

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

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

**说明:**不允许修改给定的链表。

进阶:

  • 你是否可以使用 O(1) 空间解决此题?

来源: LeetCode 第 142 题

思路:

利用快慢指针,当快慢指针相遇的时候,再弄一个新的指针从head开始走,慢指针也开始走,当新指针和慢指针相遇的时候就是环的起点。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/af5d62a3ae3544f0b9d03a7b65d8fc1a~tplv-k3u1fbpfcp-zoom-1.image

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

var detectCycle = function (head) {
  if (!head || !head.next) return null;
  let slow = head.next, fast = head.next.next;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
    if (fast == slow) {
      slow = head;
      while (fast !== slow) {
        slow = slow.next;
        fast = fast.next;
      }
      return slow;
    }
  }
  return null;
}

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿