leetcode-链表重点题目

最近刷 leetcode 链表系列刷的头昏脑胀,感觉要写个博客记录一下做链表题的思路。

两大思路

请添加图片描述

目前链表题做下来,看了这么多题解,可以总结做题有两个方向。

  • 常规思路,迭代,用指针慢慢的找。
  • 递归(递归需要设计的很巧妙)

先列一下 leetcode 需要刷的链表题的合集,大家都可以按照这个合集去刷一遍。

参考文献:

(1):https://fangzhousu.github.io/frontend-knowledge-base/handbook/struct.html#%E3%80%902%E3%80%91%E9%93%BE%E8%A1%A8%E5%9F%BA%E7%A1%80%E6%93%8D%E4%BD%9C%E3%80%90%E5%88%B7%E9%A2%98%E3%80%91

(2):https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html#%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80%E7%89%88%E6%9C%AC

链表题目可以简单的被分为三大类:

  • 链表的处理
    • 合并
    • 删除
  • 链表的反转以及衍生题目
  • 链表成环问题以及衍生题目

刷题

链表的定义

class ListNode {
    
    
  val;
  next = null;
  constructor(value) {
    
    
    this.val = value;
    this.next = null;
  }
}

leetcode 的核心代码模式不需要写链表的定义,但是笔试时是ACM模式,需要自己写链表的定义,这里采用ES6的写法。

移除链表元素

题目地址:https://leetcode-cn.com/problems/remove-linked-list-elements/

链表的迭代操作有两种方式:

  • 直接使用原来的链表来进行删除操作。
  • 设置一个虚拟头结点在进行删除操作。

直接使用原来的链表来进行删除操作

这里的讲解看代码随想录
https://programmercarl.com/0203.%E7%A7%BB%E9%99%A4%E9%93%BE%E8%A1%A8%E5%85%83%E7%B4%A0.html#%E5%85%B6%E4%BB%96%E8%AF%AD%E8%A8%80%E7%89%88%E6%9C%AC

总结:直接使用原来的链表需要考虑删除头节点的情况(跟删除别的节点逻辑不一样,需要单独写一段逻辑),使用虚拟头节点的方式更简单,在返回的时候不要忘了 return dummyNode->next; 。这才是新的头节点。

展示我自己的代码

var removeElements2 = function(head, val) {
    
    
    // 删除头节点
    if(head != null && head.val === val) {
    
    
        head = head.next;
    }

    // 删除非头节点
    let cur = head;
    while(cur != null && cur.next != null) {
    
    
        if(cur.next.val == val) {
    
    
            cur.next = cur.next.next;
        } else {
    
    
            cur = cur.next;
        }
    }

    return head;
};

设置一个虚拟头结点在进行删除操作。

这个方法统一逻辑,代码更简单

代码:

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

设计链表

题目地址:https://leetcode-cn.com/problems/design-linked-list/

这道题目设计链表的五个接口:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点

因为这是一个整体的代码,就不要使用虚拟头节点,因为很多节点操作不需要return header.next;

class ListNode {
    
    
    val;
    next = null;
    constructor(value) {
    
    
        this.val = value;
        this.next = null;
    }
}

var MyLinkedList = function() {
    
    
    this.length = 0;
    this.tail = null;
    this.head = null;
};

/** 
 * @param {number} index
 * @return {number}
 */
MyLinkedList.prototype.get = function(index) {
    
    
    // 先判断是否超出范围
    // 取得的是索引
    if(index < 0 || index >= this.length) return ;
    let cur = this.head;
    for(let i = 0; i < index; i++) {
    
    
        cur = cur.next;
    }
    return cur.val;
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtHead = function(val) {
    
    
    let node = new ListNode(val);
    this.length++;
    node.next = this.head;
    this.head = node;
    // 判断是否第一个加进来的节点,让尾指针也指向这个节点。
    if(!this.tail) {
    
    
        this.tail = node;
    }
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtTail = function(val) {
    
    
    let node = new ListNode(val);
    this.length++;
    this.tail.next = node;
    this.tail = node;
    // 判断是否第一个加进来的节点,让头指针也指向这个节点。
    if(!this.head) {
    
    
        this.head = node;
    }
};

/** 
 * @param {number} index 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtIndex = function(index, val) {
    
    
    // 先判断 index 是否超出范围
    if(index < 0 || index >= this.length) return ;
    // 新建节点
    let node = new ListNode(val);
    // 判断index 是否为头节点
    if(index == 0) {
    
    
        node.next = this.head;
        this.head = node;
    } else {
    
    
        let cur = this.head;
        for(let i = 0; i < index - 1; i++) {
    
    
            cur = cur.next;
        }
        node.next = cur.next;
        cur.next = node;
    }
    this.length++;
};

/** 
 * @param {number} index
 * @return {void}
 */
MyLinkedList.prototype.deleteAtIndex = function(index) {
    
    
        // 是删除第 index 个节点
        // 不是删除 索引为 index 的节点
        // 判断 index 是否超出范围
        if (index <= 0 || index > this.length) return;
        let cur = this.head;
        // 删除的是头节点
        if(index == 1) {
    
    
            this.head = this.head.next;
        } else {
    
    
            for(let i = 0; i < index - 2; i++) {
    
    
                cur =cur.next;
            }
            cur.next = cur.next.next;
        }
};

/**
 * 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)
 */

反转链表(简单)

地址:https://leetcode-cn.com/problems/reverse-linked-list/

两个思路:

  • 定义一个新链表,实现链表的反转,但是浪费空间
  • 双指针法,具体思路看图(参考代码随想录)

img

代码:

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    
    
    let pre = null;
    let cur = head;
    // 保存 cur 的下一个节点
    let temp = null;
    while(cur != null) {
    
    
        temp = cur.next;
        cur.next = pre;
        // 更新 pre 和 cur 指针
        pre = cur;
        cur = temp;
    }
    return pre;
};

反转链表2

地址:https://leetcode-cn.com/problems/reverse-linked-list-ii/

这题一开始不怎么会写,看了官方题解,讲的很好。

方法:穿针引线

image.png

使用上一题的解法,反转 left 到 right 部分以后,再拼接起来。我们还需要记录 left 的前一个节点,和 right 的后一个节点。如图所示:

image.png

算法步骤:

第 1 步:先将待反转的区域反转;
第 2 步:把 pre 的 next 指针指向反转以后的链表头节点,把反转以后的链表的尾节点的 next 指针指向 succ。

image.png

代码:(仔细反复看)

var reverseBetween = function(head, left, right) {
    
    
    // 因为头节点可能会变化,就使用虚拟头节点避免复杂的讨论
    let dummyNode = new ListNode(-1);
    dummyNode.next = head;

    let pre = dummyNode;
    // 第一步:从虚拟节点走 left - 1 步,来到 left 节点的前一个节点
    for(let i = 0; i < left - 1; i++) {
    
    
        pre = pre.next;
    }

    // 第二步:从pre 再走 right - left + 1步,来到right 节点
    let rightNode = pre;
    for(let i = 0; i < right - left + 1; i++) {
    
    
        rightNode = rightNode.next;
    }

    // 第三步:切断出一个子链表
    let leftNode = pre.next;
    let curr = rightNode.next;

    // 第四步:切断连接
    pre.next = null;
    rightNode.next = null;

    // 第五步:用之前的代码反转这个子链表
    let reverse = reverseList(leftNode);

    // 第六步:接回原来的链表中
    pre.next = reverse;
    leftNode.next = curr;
    return dummyNode.next
};

var reverseList = function(head) {
    
    
    let pre = null;
    let cur = head;
    // 保存 cur 的下一个节点
    let temp = null;
    while(cur != null) {
    
    
        temp = cur.next;
        cur.next = pre;
        // 更新 pre 和 cur 指针
        pre = cur;
        cur = temp;
    }
    return pre;
};

合并两个有序链表

地址:https://leetcode-cn.com/problems/merge-two-sorted-lists/

这题自己也用普通方法写出来了,和官方题解一比就太垃圾了,所以参考官方题解修改答案

递归比较麻烦,使用迭代比较简单,能A就行。。

这题不难,但是理解的话看官方的图更好

地址:https://leetcode-cn.com/problems/merge-two-sorted-lists/solution/he-bing-liang-ge-you-xu-lian-biao-by-leetcode-solu/

代码:

var mergeTwoLists = function(list1, list2) {
    
    
    // 创建一个虚拟头节点
    let prehead = new ListNode(-1);
    // 这个prev 指针就是控制控制合并的这条链子
    let prev = prehead;
    // l1,l2 分别指向两个链表
    let l1 = list1;
    let l2 = list2;
    // 循环到一个比较短的链表走完
    while(l1 != null && l2 != null) {
    
    
        // 开始比较两个链表,假如两个链表比较的某个节点值相等,就假设l1的值更小
        if(l1.val <= l2.val) {
    
    
            prev.next = l1;
            prev = l1;
            l1 = l1.next;
        } else {
    
    
            prev.next = l2;
            prev = l2;
            l2 = l2.next;
        }
    }
    // 合并后 l1 和 l2 最多只有以恶还未被合并完,直接将链表末尾指向未合并完的链表即可
    prev.next = l1 === null ? l2 : l1;
    return prehead.next 
};

删除排序链表中的重复元素

地址:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/

思路:由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。

具体地,我们从指针 cur 指向链表的头节点,随后开始对链表进行遍历。如果当前 cur 与 cur.next 对应的元素相同,那么我们就将 cur.next 从链表中移除;否则说明链表中已经不存在其它与 cur 对应的元素相同的节点,因此可以将cur 指向 cur.next。

当遍历完整个链表之后,我们返回链表的头节点即可。

注意:

当我们遍历到链表的最后一个节点时,\textit{cur.next}cur.next 为空节点,如果不加以判断,访问 cur.next 对应的元素会产生运行错误。因此我们只需要遍历到链表的最后一个节点

代码:

var deleteDuplicates = function(head) {
    
    
    // 先要判断一下节点是否为空
    if (head === null) {
    
    
        return null;
    }
    let cur = head;
    // 因为要判断 cur.next.val 所以要判断 cur.next != null
    while (cur.next != null) {
    
    
        if (cur.val === cur.next.val) {
    
    
            cur.next = cur.next.next;
        } else {
    
    
            cur = cur.next;
        }
    }
    return head;
};

删除排序链表中的重复元素2

地址:https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/

思路:

还是比较简单的。

由于给定的链表是排好序的,因此重复的元素在链表中出现的位置是连续的,因此我们只需要对链表进行一次遍历,就可以删除重复的元素。由于链表的头节点可能会被删除,因此我们需要额外使用一个哑节点(dummy node)指向链表的头节点。

具体地,我们从指针 cur 指向链表的哑节点,随后开始对链表进行遍历。如果当前 cur.next 与 cur.next.next 对应的元素相同,那么我们就需要将 cur.next 以及所有后面拥有相同元素值的链表节点全部删除。我们记下这个元素值 x,随后不断将 cur.next 从链表中移除,直到 cur.next 为空节点或者其元素值不等于 x 为止。此时,我们将链表中所有元素值为 x 的节点全部删除。

注意:

如果当前 cur.next.next 对应的元素不相同,那么说明链表中只有一个元素值为 cur.next 的节点,那么我们就可以将 cur 指向 cur.next。

当遍历完整个链表之后,我们返回链表的的哑节点的下一个节点 dummy.next 即可。

需要注意 cur.next 以及 cur.next.next 可能为空节点,如果不加以判断,可能会产生运行错误。

代码:

var deleteDuplicates = function(head) {
    
    
    // 判断 head 不为空
    if(!head) {
    
    
        return head;
    }

    let dummy = new ListNode(-1, head);
    let cur = dummy;
    while(cur.next != null && cur.next.next != null) {
    
    
        if(cur.next.val === cur.next.next.val) {
    
    
            // 用 x 把这个重复的值记录下来
            let x = cur.next.val;
            // 循环将所有和 x 值相同的值删除
            // 因为这里是循环,所以要判断 cur.next 不为空,不然会报空指针异常
            // 如果这里是 if 判断就不用 判 cur.next 是否为空了。
            while(cur.next != null && cur.next.val === x) {
    
    
                cur.next = cur.next.next;
            }
        } else {
    
    
            cur = cur.next;
        }
    }
    return dummy.next;
};

删除链表中的节点

地址:https://leetcode-cn.com/problems/delete-node-in-a-linked-list/

这题有点脑筋急转弯的意思。。。

笔试绝对不会有这种题的感觉。。。

放个官方思路链接:

https://leetcode-cn.com/problems/delete-node-in-a-linked-list/solution/shan-chu-lian-biao-zhong-de-jie-dian-by-x656s/

代码:

var deleteNode = function(node) {
    
    
    node.val = node.next.val;
    node.next = node.next.next;
};

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

地址:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/

思路:

这题就是经典前后指针的应用。

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

代码:

var removeNthFromEnd = function(head, n) {
    
    
    // 碰到倒数第几个的题目就是经典前后指针
    // 哑节点
    let dummy = new ListNode(-1, head);
    let first = dummy;
    let second = dummy;
    // first 先移动 n 个位置
    for(let i = 0; i < n; i++) {
    
    
        first = first.next;
    }
    // first 走到 链表的最后一个节点
    while(first.next != null) {
    
    
        first = first.next;
        second = second.next;
    }
    // 删除这个节点
    second.next = second.next.next;
    return dummy.next;
};

两两交换链表中的节点

地址:https://leetcode-cn.com/problems/swap-nodes-in-pairs/

思路:

这题链表交换比较复杂,需要画图理解。

可以看官方题解的图(很详细)

https://leetcode-cn.com/problems/swap-nodes-in-pairs/solution/liang-liang-jiao-huan-lian-biao-zhong-de-jie-di-91/

核心逻辑:

第一步:设置一个哑节点。设立temp指针指向哑节点,temp指针后面设置node1和node2指针。

temp = dummyHead;
node1 = temp.next;
node2 = temp.next.next;

在这里插入图片描述

第二步:原来是 dummy ----> 1 -----> 2。现在要改成 dummy ----> 2 ----> 1,所以 temp的 next 要先指向 node2, node2 的 next 要指向 node1, node1 的 next 要 指向 3。

temp.next = node2;
node1.next = node2.next;
node2.next = node1;

在这里插入图片描述

第三步:这一轮结束后,让 temp 指向 node1, node1 为 temp.next,node2 为 temp.next.next。

temp = node1;
node1 = temp.next;
node2 = temp.next;

接下来就重复第二步就行了。

结束条件:

如果 temp 的后面没有节点或者只有一个节点,则没有更多的节点需要交换,因此结束交换。

完整代码:

var swapPairs = function(head) {
    
    
    let dummy = new ListNode(-1, head);
    let temp = dummy;
    while(temp.next != null && temp.next.next != null) {
    
    
        // 第一步
        let node1 = temp.next;
        let node2 = temp.next.next;
        // 第二步
        temp.next = node2;
        node1.next = node2.next;
        node2.next =node1;

        // 第三步
        temp = node1;
    }
    return dummy.next;
};

链表相交

地址:https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci/

思路:

题目没怎么看懂,就按照代码随想录的来写了。。。(感觉面试笔试应该也不会考的吧。。。)

看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点:

面试题02.07.链表相交_1

我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图:

面试题02.07.链表相交_2

此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。

否则循环退出返回空指针。

// 获取链表长度的函数
var getListLen = function(head) {
    
    
    let len = 0;
    let cur = head;
    while(cur != null) {
    
    
        len++;
        cur = cur.next;
    }
    return len;
}
var getIntersectionNode = function(headA, headB) {
    
    
    let curA = headA;
    let curB = headB;
    // 获取 headA 和 headB 的长度
    let lenA = getListLen(headA);
    let lenB = getListLen(headB);
    // 让 lenA 为最长的那个,方便后面的 lenA - lenB
    if(lenA < lenB) {
    
    
        // 进行交换
        let tempLen = lenB;
        lenB = lenA;
        lenA = tempLen;
        let tempNode = curB;
        curB = curA;
        curA = tempNode;
    }
    // 两者长度差
    let len = lenA - lenB;
    for(let i = 0; i < len; i++) {
    
    
        curA = curA.next;
    }
    while(curA != null && curA !== curB) {
    
    
        curA = curA.next;
        curB = curB.next;
    }
    return curA;
};

环形链表

地址:https://leetcode-cn.com/problems/linked-list-cycle/

思路:

这题比较简单:

两个方法解决

  • 哈希表:最容易想到的方法是遍历所有节点,每次遍历到一个节点时,判断该节点此前是否被访问过。
  • 快慢指针:因为存在环,所以快指针一定会追上慢指针

哈希表解决:

// 哈希表
var hasCycle = function(head) {
    
    
    let set = new Set();
    while(head) {
    
    
        if(set.has(head)) {
    
    
            return true;
        }
        set.add(head);
        head = head.next;
    }
    return false;
};

快慢指针解决:

// 快慢指针法
var hasCycle = function(head) {
    
    
    // 说明不成环
    if(head == null || head.next == null) return false;
    let slow = head;
    let fast = head.next;
    while(slow != fast) {
    
    
        // 说明不成环
        if(fast == null || fast.next == null) {
    
    
            return false;
        }
        // 慢指针移动一步
        slow = slow.next;
        // 快指针移动两步
        fast = fast.next.next;
    }
    return true;
};

环形链表2

地址:https://leetcode-cn.com/problems/linked-list-cycle-ii/

这题跟上一题一样,两个方法,只是这题要记录下如环的第一个节点

哈希表解决:

// 哈希表解决
var detectCycle = function(head) {
    
    
    let set = new Set();
    while(head) {
    
    
        if(set.has(head)) {
    
    
            return head;
        }
        set.add(head);
        head = head.next;
    }
    return null;
};

快慢指针思路:

公式推理看:https://programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html#%E6%80%9D%E8%B7%AF

结论:当快节点和慢节点在环内相遇时,设置一个节点prev 指向头,然后和慢节点一起移动,最后这两个节点相遇的地方就是环的入口。

代码:

// 哈希表解决
var detectCycle = function(head) {
    
    
    // 说明不成环
    if(head == null || head.next == null) return null;
    let slow = head;
    let fast = head;
    while(fast != null) {
    
    
        slow = slow.next;
        if(fast.next != null) {
    
    
            fast = fast.next.next;
        } else {
    
    
            // 说明不成环
            return null;
        }
        // 快节点和慢节点相遇
        if(fast === slow) {
    
    
            let prev = head;
            while(prev != slow) {
    
    
                prev = prev.next;
                slow = slow.next;
            }
            // prev 和 慢节点相遇
            return prev;
        }
    }
    // 说明不成环
    return null;
};

猜你喜欢

转载自blog.csdn.net/hangao233/article/details/124544776
今日推荐