链表--使用什么思路可以快速解题

165 阅读8分钟

链表

其实我们开始做题之前需要对链表的基础部分做一些复习攻略

 1. 链表的结构是怎样的
 2. 链表的遍历方式和我们熟悉的数组有没有区别
 3. 链表常见的操作都有哪些?
 4. 链表的 指针域 和 数据域 都是用来干嘛的?
 5. 你能不能去实现一个简单的链表呢?

如果你对上述的部分都了然于胸,那我们就可以开始我们的刷题之旅了

2. 两数相加

image.png

image.png

我们还是基于双指针的思路,现在目的是去计算两个链表的和, 那么我们可以不可以这么想,对于数组来说,我们去计算它的两个数组和的时候,我们是不是可以按照对应下标去实现两数的相加,对于我们的链表来说, 是不是可以采用类似的思路去尝试一下呢:

现在我们梳理一下思路:

首先呢, 我们需要去定义两个指针, 目的是在遍历链表的过程中,去维护一个和的链表结构

然后呢, 我们需要依次遍历两个链表, 当然会包含没有值情况,需要做一些处理

根据题目的要求: 逆序存储, 可能情况就是需要去记录进位; 结合示例3,我们需要对最后遍历结束后,看进位的结果是不是进行后续的处理

最后,我们去返回链表的头节点, 结束🔚

我对自己之前书写有问题的地方做了编注,希望对你有所帮助

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */
var addTwoNumbers = function(l1, l2) {
    let head = null, tail = null;
    // 进位
    let temp = 0;
    while (l1 || l2) {
        let l1Num = l1 ? l1.val : 0;
        let l2Num = l2 ? l2.val : 0;
        let sum = l1Num + l2Num + temp;

        if (!head && !tail) {
            head = tail = new ListNode(sum % 10)
        } else {
            tail.next = new ListNode(sum % 10)
            tail = tail.next;
        }

        // 计算进位
        temp = Math.floor(sum / 10);
        // if (temp > 1) {
        //     tail.next = tail.val + 1
        // }

        // 问题点
        if (l1) {
            l1 = l1.next;
        }

        // 问题点
        if (l2) {
            l2 = l2.next;
        }
    }

    // 需要注意的是最后的 temp值是不是需要进位  //问题点
    if (temp > 0) {
        tail.next = new ListNode(temp)
    } 
    return head;
};

下面我们来看一道很类似的题目:

445. 两数相加 II

image.png

image.png

解题思路:

其实刚开始我会按照上面题目的思路快速抄完,发现是错的,这是一个令人尴尬的结果;

那么我是错在了哪里,它们之间到底是哪里不同呢?

首先最开始去遍历的时候就不一样,一个正序进位,一个逆序进位;

我们需要一种新的思路的结合去做:

既然我们从头开始是有问题的,那么按照正常的加法运算的逻辑我们是需要从个位开始累加结果的; 我们是不是可以把链表用一种结构存储, 然后每次取最后一个链表节点的值;

你是不是有一种恍然大悟的感觉呢?

这不就是栈嘛,哎呀妈呀,我突然就懂了。

代码实现:

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} l1
 * @param {ListNode} l2
 * @return {ListNode}
 */

var addTwoNumbers = function(l1, l2) {

    let stact_l1 = [], stact_l2 = [];
    while(l1) {
        stact_l1.push(l1.val);
        l1 = l1.next;
    }

    while(l2) {
        stact_l2.push(l2.val)
        l2 = l2.next;
    }

    // 定义一个进位 和 前置节点
    let temp = 0, pre = null;

    while(!stact_l1.length || !stact_l2.length || temp !== 0) {
        let l1_num = stact_l1.length === 0 ? 0 : stact_l1.pop();
        let l2_num = stact_l2.length === 0 ? 0 : stact_l2.pop();

        let sum = l1_num + l2_num + temp;
        temp = Math.floor(sum / 10);

        let newNode = new ListNode(sum % 10);

        newNode.next = pre;
        pre = newNode;
    }
    return pre;
};

回归正题

21. 合并两个有序链表

image.png

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} list1
 * @param {ListNode} list2
 * @return {ListNode}
 */
var mergeTwoLists = function(list1, list2) {
    // 虚拟头节点
    let dummy = new ListNode(0), pre = dummy;
    while(list1 !== null && list2 !== null) {
        if (list1.val > list2.val) {
            pre.next = list2;
            list2 = list2.next;
        } else {
            pre.next = list1;
            list1 = list1.next;
        }
        pre = pre.next;
    }

    if (list1) {
        pre.next = list1;
    }
    if (list2) {
        pre.next = list2;
    }
    return dummy.next;
};

21.合并两个有序链表

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

image.png

让我们来看一下这道题, 如果正着来的话就是删除第k个节点, 遍历到第k个节点执行删除节点的操作p.next = p.next.next;但是如果是逆序的话就有点不一样了, 逆序的第n个节点是不是正序的第 n - k个节点呢?

但是我们需要一个值n, 需要我们去遍历一次链表的长度n;然后再去遍历找到第n-k个节点。

但是我们想一想有没有优化的思路呢? 我们能不能只遍历一次链表就能得出倒数第k个节点

梳理一下思路:

首先, 我们定义一个 p1节点, 然后呢当我们遍历到k个节点的时候此时到链表结尾的空指针的距离为 n - k;

那么 如果此时再走 n - k 步是不是到链表的结尾了呢
假设 我们定义两个指针, fast = head, slow = head;
fast 先跑 k 步, 然后 slow 从头开始跑 当 fast.next === null
slow 就跑到了倒数第k个节点

代码实现

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function(head, n) {
    // 结合虚拟头节点 避免出现空指针的问题
    let dummy = new ListNode(-1);
    dummy.next = head;
    // 找到 倒数第k个节点
    let x = findFromEnd(dummy, n); // error
    // 删除第 k 个节点
    x.next = x.next.next;

    return dummy.next;
};

function findFromEnd(head, n) {
    if (!head) return null;
    let fast = head;
    while (n--) fast = fast.next;
    // 遍历到第k个节点
    let slow = head;
    // slow ,fast 同时走 n - k 步
    while(fast.next !== null) {
        fast = fast.next;
        slow = slow.next;
    }
    // slow 此时正指向 n-k 个节点
    return slow;
}

876. 链表的中间结点

image.png

其实延续上面题目的思路,结合快慢指针的套路可以很快的解题。

首先,我们定义了两个指针 fast, slow;

然后, 按照fast每次走两步, slow 每次走一步的策略;

最后, fast走到链表尾端的时候, slow刚好走到链表的中间

 * 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 middleNode = function(head) {
    let slow = head, fast = head;
    // 需要注意一下这个条件
    while(fast !== null && fast.next !== null) {
        fast = fast.next.next;
        slow = slow.next;
    }
    return slow;
};

141.环形链表

image.png

解题思路:

现在我们到了判断链表有没有环的题目, 假设链表没有环, 那么链表就会跑到链表的尾部; 如果 链表指针一直没有等于null,那么此时确定是有环的.

让我们先定义快慢指针 fast,slow,每次fast走两步, slow走一步,如果fast===slow说明两者相遇。

代码实现:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
 
var hasCycle = function(head) {
    // 现在我们到了判断链表有没有环的题目
    // 假设 链表没有环, 那么链表就会跑到链表的尾部
    // 
    // 那么此时确定是有环的
    // 先定义两个快慢指针
    let fast = head, slow = head;
    while(fast !== null && fast.next !== null) {
        fast = fast.next.next;
        slow = slow.next;
        // 快慢节点相遇
        if (slow === fast) {
            return true;
        }
    }
    return false;
};

142. 环形链表II

image.png

解题思路:

寻找入环的第一个节点,那么是不是就在判断有环的基础上, 根据此时的状态,fast 指针的位置和 从head开始到环的入口的距离是一致的详细解析.

代码实现:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function(head) {
    let slow = head, fast = head;
    while(fast !== null && fast.next !== null) {

        slow = slow.next;
        fast = fast.next.next;

        if (fast === slow) {
            // 相遇了
            let temp = head;
            // 当 temp === fast 在入口点相遇了
            while (temp !== fast) {
                temp = temp.next;
                fast = fast.next;
            }
            return temp;
        }
    }
    return null;
};

160. 相交链表

image.png

解题思路:

其实刚开始有一个比较稳妥的思路,就是说需要我们提供一个额外的存储空间;

借助于set结构,我们先存一条链表,然后看另一条链表中是不是有重复出现的元素,就判断两者就是相交的状态

代码实现:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    // 存一条 取一条
    let set = new Set();
    let temp = headA;

    while(temp !== null) { // error
        set.add(temp)
        temp = temp.next;
    }

    temp = headB;
    while(temp !== null) { // error
        if (set.has(temp)) {
            return temp;
        }
        temp = temp.next;
    }

    return null;
};

如果不使用额外的空间,使用两个指针能做吗?

这种思路的难点在于: 当两条链表的长度不一致的时候, 如何保证两个链表的节点可以对应上

相交链表-2.jpg

如果, p1p2两个节点同时前进, 怎么能保证同时到达相交的节点呢?

这个问题确实是一个棘手的问题, 我们想想如何去解决呢?

image.png

如上图所示, 我们可以让p1先遍历完A链表,然后在遍历B链表; 让p2先遍历完B链表,然后在遍历A链表;这样在逻辑上实现了两条链表连接到一起, 然后可以找到相交的节点

你可能还会问一句, 如果没有相交节点呢, 也就是说最后会返回null, 其实我们已经包括这种判断。

代码实现:

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    // 如果不使用额外的空间
    // 使用两个指针能做吗?
    let p1 = headA, p2 = headB;
    // p1!== p2 还没相交
    while(p1 !== p2) {
        if (p1 === null)  p1 = headB;
        else {
            p1 = p1.next;
        }

        if (p2 === null) p2 = headA;
        else {
            p2 = p2.next;
        }
    }
    return p1;
};