面试必看算法题|链表问题总结

前言

链表的相关问题,在面试中出现频率较高,这些问题往往也是解决其他复杂问题的基础;
在这篇文章里,我将梳理链表问题的问题 & 解法。如果能帮上忙,请务必点赞加关注,这真的对我非常重要。


目录


1. 概述

1.1 链表的定义

链表是一种常见的基础数据结构,是一种线性表。与顺序表不同的是,链表中的每个节点不是顺序存储的,而是通过节点的指针域指向到下一个节点。

1.2 链表的优缺点

1.3 链表的类型

单链表、双链表、循环链表、静态链表


2. 删除链表节点

删除链表节点时,考虑到可能删除的是链表的第一个节点(没有前驱节点),为了编码方便,可以考虑增加一个 哨兵节点。其中,在删除链表的倒数第 N 个节点问题里,使用快慢指针在一趟扫描里找出倒数第 N 个节点是比较重要的编程技巧。

237. Delete Node in a Linked List 删除链表中的节点 【题解】
203. Remove Linked List Elements 移除链表元素 【题解】
不移除野指针
class Solution {
    fun removeElements(head: ListNode?, `val`: Int): ListNode? {
        // 哨兵节点
        val sentinel = ListNode(-1)
        sentinel.next = head

        var pre = sentinel
        var cur: ListNode? = sentinel
        while (null != cur) {
            if (`val` == cur.`val`) {
                // 移除
                pre.next = cur.next
            } else {
                pre = cur
            }
            cur = cur.next
        }
        return sentinel.next
    }
}

移除野指针
class Solution {
    fun removeElements(head: ListNode?, `val`: Int): ListNode? {
        // 哨兵节点
        val sentinel = ListNode(-1)
        sentinel.next = head

        var pre = sentinel
        var cur: ListNode? = sentinel
        while (null != cur) {
            val removeNode = if (`val` == cur.`val`) {
                // 移除
                pre.next = cur.next
                cur
            } else {
                pre = cur
                null
            }
            cur = cur.next
            if (null != removeNode) {
                removeNode.next = null
            }
        }
        return sentinel.next
    }
}
19. Remove Nth Node From End of List 删除链表的倒数第N个节点 【题解】

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

class Solution {
    fun removeNthFromEnd(head: ListNode, n: Int): ListNode? {
        // 哨兵节点
        val sentinel = ListNode(-1)
        sentinel.next = head

        var fast: ListNode? = sentinel
        var slow: ListNode? = sentinel

        for (index in 0 until n) {
            fast = fast!!.next
        }

        // 找到倒数第 k 个节点的前驱
        while (null != fast!!.next) {
            fast = fast.next
            slow = slow!!.next
        }
        slow!!.next = slow.next!!.next
        return sentinel.next
    }
}

复杂度分析:

类似地,876. Middle of the Linked List 链表的中间结点 【题解】 也是通过快慢指针来找到中间节点的:

class Solution {
    fun middleNode(head: ListNode?): ListNode? {
        if (null == head || null == head.next) {
            return head
        }
        var fast = head
        var slow = head

        while (null != fast && null != fast.next) {
            fast = fast.next!!.next
            slow = slow!!.next
        }

        return slow
    }
}
86. Partition List 分隔链表 【题解】

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

思路:分隔链表无非是先将大于等于 val 的节点从原链表中移除到第二个链表中,最后再拼接两个链表。

class Solution {
    fun partition(head: ListNode?, x: Int): ListNode? {
        if (null == head) {
            return null
        }

        // 哨兵节点
        val sentinel = ListNode(-1)
        sentinel.next = head
        var pre = sentinel
        // 第二链表
        var bigHead : ListNode? = null
        var bigRear = bigHead

        var cur = head
        while (null != cur) {
            if (cur.`val` >= x) {
                // 大于等于:移除
                pre.next = cur.next
                if(null == bigHead){
                    bigHead = cur
                    bigRear = cur
                }else{
                    bigRear!!.next = cur
                    bigRear = cur
                }
            } else {
                pre = cur
            }
            if (null == cur.next) {
                // 拼接
                pre.next = bigHead
                bigRear?.next = null
                break
            }
            cur = cur.next
        }
        return sentinel.next
    }
}

复杂度分析:

328. Odd Even Linked List 奇偶链表 【题解】

思路:奇偶链表无非是先将奇节点放在一个链表里,偶节点放在另一个链表里,最后把偶节点接在奇链表的尾部

class Solution {
    fun oddEvenList(head: ListNode?): ListNode? {
        if (null == head) {
            return null
        }

        var odd: ListNode = head
        var even = head.next
        val evenHead = even

        while (null != even && null != even.next) {
            // 偶节点
            odd.next = even.next
            odd = odd.next!!
            // 奇节点
            even.next = odd.next
            even = even.next
        }
        odd.next = evenHead
        // 头节点不动
        return head
    }
}
83. Remove Duplicates from Sorted List 删除排序链表中的重复元素
82. Remove Duplicates from Sorted List II 删除排序链表中的重复元素 II

3. 反转链表

反转链表问题在面试中出现频率 非常非常高,相信有过几次面试经验的同学都会同意这个观点。在这里,我找出了 4 道反转链表的问题,从简单延伸到困难,快来试试吧。

206. 反转链表 Reverse Linked List 【题解】

反转一个单链表。

解法1:递归

class Solution {
    fun reverseList(head: ListNode?): ListNode? {
        if(null == head || null == head.next){
            return head
        }
        val prefix = reverseList(head.next)
        head.next.next = head
        head.next = null
        return prefix
    }
}
复制代码

复杂度分析:

解法2:迭代

class Solution {
    fun reverseList(head: ListNode?): ListNode? {
        var cur: ListNode? = head
        var headP: ListNode? = null

        while (null != cur) {
            val tmp = cur.next
            cur.next = headP
            headP = cur
            cur = tmp
        }
        return headP
    }
}

复杂度分析:

92. 反转链表 II Reverse Linked List II 【题解】

给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

class Solution {
    fun reverseBetween(head: ListNode?, m: Int, n: Int): ListNode? {
        if (null == head || null == head.next) {
            return head
        }

        // 哨兵节点
        val sentinel = ListNode(-1)
        sentinel.next = head
        var rear = sentinel

        // 1\. 找到反转开始位置前驱节点
        var cur = sentinel
        for (index in 0 until m - 1) {
            cur = cur.next!!
            rear = cur
        }

        // 2\. 反转指定区域
        rear.next = reverseList(rear.next!!, n - m + 1)
        return sentinel.next
    }

    /**
     * 反转指定区域
     * @param size 长度
     */
    fun reverseList(head: ListNode, size: Int): ListNode? {
        var cur: ListNode? = head
        var headP: ListNode? = null
        // 反转的起始点需要连接到第 n 个节点
        val headTemp = head

        var count = 0
        while (null != cur && count < size) {
            val tmp = cur.next
            cur.next = headP
            headP = cur
            cur = tmp

            count++
        }

        // 连接到第 n 个节点
        headTemp.next = cur
        return headP
    }
}

复杂度分析:

234. Palindrome Linked List 回文链表 【题解】

请判断一个链表是否为回文链表。

思路:使用快慢指针找到中间节点,反转后半段链表(基于反转链表 II),比较前后两段链表是否相同,最后再反转回复到原链表。

class Solution {
    fun isPalindrome(head: ListNode?): Boolean {
        if (null == head || null == head.next) {
            return true
        }

        // 1\. 找到右边中节点(右中节点)
        var fast = head
        var slow = head

        while (null != fast && null != fast.next) {
            slow = slow!!.next
            fast = fast.next!!.next
        }

        // 2\. 反转后半段
        val reverseP = reverseList(slow!!)

        // 3\. 比较前后两段是否相同
        var p = head
        var q: ListNode? = reverseP
        var isPalindrome = true

        while (null != p && null != q) {
            if (p.`val` == q.`val`) {
                p = p.next
                q = q.next
            } else {
                isPalindrome = false
                break
            }
        }

        // 4\. 恢复链表
        reverseList(reverseP)
        return isPalindrome
    }

    /**
     * 反转链表
     */
    private fun reverseList(head: ListNode): ListNode {
        // 略,见上一节...
    }
}

复杂度分析:

25. K 个一组翻转链表 Reverse Nodes in k-Group

给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。


4. 合并有序链表

合并有序链表问题在面试中出现频率 较高,其中,合并两个有序链表 是比较简单的,而它的进阶版 合并K个升序链表 要考虑的因素更全面,难度也有所增强,快来试试吧。

21. Merge Two Sorted Lists 合并两个有序链表 【题解】

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

class Solution {
    fun mergeTwoLists(l1: ListNode?, l2: ListNode?): ListNode? {
        if (null == l1) return l2
        if (null == l2) return l1

        // 哨兵节点
        val sentinel = ListNode(-1)
        var rear = sentinel

        var p = l1
        var q = l2

        while (null != p && null != q) {
            if (p.`val` < q.`val`) {
                rear.next = p
                rear = p
                p = p.next
            } else {
                rear.next = q
                rear = q
                q = q.next
            }
        }
        rear.next = if (null != p) p else q

        return sentinel.next
    }
}

复杂度分析:

23. Merge k Sorted Lists 合并K个升序链表 【题解】

给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。

解法1:暴力法

思路1:与合并两个有序链表类似,每轮从 k 个链表中取出最小的节点,并插入结果链表中。其中,从 k 个数中取出最小节点的时间复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>O</mi><mo stretchy="false">(</mo><mi>k</mi><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">O(k)</annotation></semantics></math>O(k)。

思路2:这个思路与上个思路类似,时间复杂度和空间复杂度页相同,即:依次将 k 个链表与结果链表合并。

复杂度分析:

解法2:排序法

思路:用一个数组保存所有节点之后,进行快速排序,随后将数组输出单链表。

class Solution {
    fun mergeKLists(lists: Array<ListNode?>): ListNode? {
        if (lists.isNullOrEmpty()) {
            return null
        }
        // 1\. 用一个数组保存所有节点
        val array = ArrayList<ListNode>()
        for (list in lists) {
            var cur = list
            while (null != cur) {
                array.add(cur)
                cur = cur.next
            }
        }
        // 2\. 快速排序
        array.sortWith(Comparator { node1, node2 -> node1.`val` - node2.`val` })
        // 3\. 输出为链表
        val newHead = ListNode(-1)
        var rear = newHead
        for (node in array) {
            rear.next = node
            rear = node
        }
        return newHead.next
    }
}

复杂度分析:

解法3:归并法

思路:将 k 组链表分为两部分,然后递归地处理两组链表,最后再合并起来。

class Solution {

    // 合并 k 个有序链表
    fun mergeKLists(lists: Array<ListNode?>): ListNode? {
        if (lists.isNullOrEmpty()) {
            return null
        }
        return mergeKLists(lists, 0, lists.size - 1)
    }

    fun mergeKLists(lists: Array<ListNode?>, left: Int, right: Int): ListNode? {
        if (left == right) {
            return lists[left]
        }
        // 归并
        val mid = (left + right) ushr 1
        return mergeTwoLists(
            mergeKLists(lists, left, mid),
            mergeKLists(lists, mid + 1, right)
        )
    }

    // 合并两个有序链表
    fun mergeTwoLists(l1: ListNode?, l2: ListNode?): ListNode? {
        // 略,见上一节...
    }
}

复杂度分析:

解法4:小顶堆法

class Solution {

    // 合并 k 个有序链表
    fun mergeKLists(lists: Array<ListNode?>): ListNode? {
        if (lists.isNullOrEmpty()) {
            return null
        }

        // 最小堆
        val queue = PriorityQueue<ListNode>(lists.size) { node1, node2 -> node1.`val` - node2.`val` }

        // 1\. 建堆
        for (list in lists) {
            if (null != list) {
                queue.offer(list)
            }
        }

        val sentinel = ListNode(-1)
        var rear = sentinel

        // 2\. 出队
        while (queue.isNotEmpty()) {
            val node = queue.poll()!!
            // 输出到结果链表
            rear.next = node
            rear = node
            // 存在后继节点,加入堆中
            if (null != node.next) {
                queue.offer(node.next)
            }
        }
        return sentinel.next
    }
}

复杂度分析:


5. 排序链表

147. Insertion Sort List 对链表进行插入排序 |【题解】
148. Sort List 排序链表 【题解】

6. 环形链表

链表相交 & 成环问题可以归为一类问题,在面试中出现频率较高;在之前的一篇文章里,我们单独讨论过:《算法面试题 | 链表相交 & 成环问题》

最后

国内互联网面试的流程逐渐在向国外靠拢,像字节跳动、BAT 等大厂,手撕算法题已经成为了必选动作。这也是公司面试筛选人的低成本办法,如果你写出了算法并且通过了,要么你聪明要么你勤奋(刷题了)。

希望大家引起重视,好好准备。

另外,我把自己这段时间整理的Android最重要最热门的学习方向资料放在了我的GitHub,里面还有不同方向的自学编程路线、面试题集合/面经、及系列技术文章等。

资源持续更新中,欢迎大家一起学习和探讨。

原文地址:https://juejin.cn/post/6882370280946302983

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章