[路飞]链表基础及经典问题(leetcode)

648 阅读11分钟

一、链表基础知识

  • 单向链表的访问特点:只能从前向后依次访问
  • 与访问数组不同的是数组可以直接访问中间位置的任意元素,而链表必须从头乖乖的一步一步走到访问节点。(数组给出下标我们可以直接定位到元素,而链表需要从头一步步查找)
  • 链表结构只是通过指针域控制的,我们可以通过改变指针域的指向来实现插入/删除的目的;
  • 单向链表再加一个指针可以构造成双向链表,二叉树也是在单向链表的基础上多一个指针的;看似相同,但深究其中相差还是很大的;
  • 链表思维就是唯一指向的思维
  1. 链表中的每个节点至少包含两个成分:数据域和指针域
  2. 链表中的每个节点,通过指针域的形成一个线性结构;
  3. 查找节点O(n),插入节点O(1),删除节点O(1);
  4. 不适合快速定位数据,适合动态的插入和删除数据的应用场景

二、链表的典型应用场景

1、操作系统内的动态内存分配

2、LRU缓存淘汰算法

LRU (Least Recently Used) 意思就是近期最少使用算法,它的核心思想就是会优先淘汰那些近期最少使用的缓存对象。

缓存是高速设备之于低速设备的称呼

链表与数组结合在一起使用的叫块状链表,适用于那种动态扩容的线性表

三、经典面试题

1、链表的访问

leetcode 141.环形链表

  1. 题目简述
  • 实现目标:判断如果链表中存在环,则返回 true 。 否则,返回 false 。

示例:

circularlinkedlist.png

输入: head = [3,2,0,-4], pos = 1
输出: true
解释: 链表中有一个环,其尾部连接到第二个节点。
  1. javascript实现 两种方法实现,1、通过给遍历过的节点添加flag标志位。 2、通过快慢指针方法
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function(head) {
    // 对遍历过的节点,添加flag标志
    // if(!head || !head.next) return false
    // let p = head;
    // while(p && p.next) {
    //     if(p.flag){
    //         return true
    //     }else {
    //         p.flag = '1'
    //         p = p.next
    //     }
    // }
    // return false

    // 快慢指针
    if(!head || !head.next) return false
    let p = q = head;
    while(q && q.next) {
        p = p.next;
        q = q.next.next;
        if(p == q) return true
    }
    return false

};
  1. 视频讲解:链接

leetcode 142.环形链表II

  1. 题目简述
  • 实现目标:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null 。 示例:

circularlinkedlist.png

输入: head = [3,2,0,-4], pos = 1
输出: 返回索引为 1 的链表节点
解释: 链表中有一个环,其尾部连接到第二个节点。
  1. javascript实现 解题思路:快慢指针法
  • 1⃣️ 首先确定是否有环
  • 2⃣️ 快慢指针焦点到环起始点 == 头节点到环起始点 ?

111639415505_.pic.jpg 我们假设链表头到环起始点的距离为a,从环起始点到相遇点的距离为b,从相遇点到环起始点的 距离为c。

慢指针走过a+b的距离,快指针走过a+n(b+c)+b的距离。由于快指针是慢指针的二倍,所 以:2(a+b)= a+n(b+c)+b,而我们实际上并不用关心n是多少,有可能是10,也有可能是1,因此 上述公式可以简化为:a=c;

a+b = n(b+c),n = 1; a = c;

另外一种理解是:

image.png

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

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function(head) {
    // 添加flag标志位,一旦发现节点有flag,说明是再次到达,这时是环的起始点;
    // if(head == null)  return null;
    // let p = head;
    // while (p.next) {
    //     if(p.flag) {
    //         return p;
    //     }else {
    //         p.flag = true;
    //         p = p.next;
    //     }
    // }
    // return null
    
    
    // 快慢指针
    if(!head || !head.next) return null
    let p = q = head;
    while(q && q.next) {
        p = p.next;
        q = q.next.next;
        if(p == q) {
            // 是有环的
            // 在pq的交点到环起始点 == 头到环起始点
            // 1、定义一个变量记录从头开始向后走
            // 2、慢指针p继续往后走
            
            let start = head;
            while(start != p) {
                start = start.next;
                p = p.next;
            }
            return start

        }
    }
    return null
};

leetcode 202.快乐数

  1. 题目简述
  • 编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果 可以变为 1,那么这个数就是快乐数。

如果 n 是快乐数就返回 true ;不是,则返回 false 。

示例:

输入: n = 19
输出: true
解释: 12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
  1. javascript实现

示例分析: 19 -> 82 -> 68 -> 100 -> 1

  1. 链表思维就是唯一指向的思维;
  2. 把19,82,68,100看成链表中的节点;
  3. 把他们之间的转换规则看成链表的指针;
  4. 把 1 看成链表的空地址null;
  5. 链表中如果有环,链表就不会有空地址null;
//链表判环-快慢指针法
function getNext(num) {
    let sum = 0;
    while(num) {
        // 求平方和
        sum += (num%10) * (num%10)
        num = parseInt(num/10)
    }
    return sum;
}
var isHappy = function(n) {
    if(n==1 || getNext(n)==1) return true
    let p = q = n;
    while(q && getNext(q) && q != 1)  {
        p = getNext(p)
        q = getNext(getNext(q))
        if(p == q) {
            return false
        }
    }
    return true
};


2、链表的反转

如: 一个链表 1 -> 2 -> 3 -> 4 -> null; 反转之后就是 4 -> 3 -> 2 -> 1 -> null

  • 如何实现链表反转
  1. 定义一个空指针 pre;
  2. 再设置一个curn指针,cur->1, n -> 2;
  3. 让cur.next -> pre, pre -> cur; cur = n, n = n.next;
  4. cur 是未反转链表的头节点,
  5. pre 是反转后的头节点,
  6. n 是未反转的头节点(cur)的下一位;

leetcode206.反转链表

  1. 题目简述
  • 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例: image.png
输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]
  1. javascript实现
/**
 * 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) {
    // 法1
    // if(head == null) return head;
    // // pre: 反转后的头节点
    // // cur: 未反转的头节点
    // // n: 未反转的头节点的下一个节点
    // let pre = null, cur = head, n = head.next;
    // while(cur) {
    //     cur.next = pre;
    //     pre = cur;
    //     // 避免当n 为空时错误
    //     (cur = n) && (n = n.next)
    // }
    // //单链表可以用头指针的名字来命名
    // return pre

    // 法2
    // 基于递归的回溯过程来反转单链表
    // 解题思路。递归的回溯过程就是逆序的遍历链表
    // 例: 链表 1 -> 2 -> 3 -> 4 -> 5 -> null
    // 为方便理解这里节点直接用数字代替
    // 1、递归到链表的最后一个节点 5 后,5.next == null,直接返回 5;
    // 2、然后执行节点4,期望5.next = 4, 就是相当于  4.next.next = 4 5->4反转完成;让4节点指向null
    // 往后一次类推,最后 5 -> 4 -> 3 -> 2 -> 1 -> null

    if(head == null || head.next == null) return head;
    let pre = head, cur = reverseList(head.next);
    pre.next.next = head;
    head.next = null
    return cur

};

leetcode92. 反转链表II

  1. 题目简述
  • 给你单链表的头指针 head 和两个整数left 和 right ,其中left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 示例:

image.png

输入: head = [1,2,3,4,5], left = 2, right = 4
输出: [1,4,3,2,5]
  1. 解题思路
  • 先找到待反转区域的前一位 (记作 pre)

这时我们需要创建一个虚拟头节点(是自己创建的一个真实的链表节点,它不是指针也不是引用)。

  • 为什么要虚拟头节点呢?

    因为如果待反转区域是从链表的头节点开始时,这时的待反转区域就没有前一位了,为了方便操作,我们可以在原链表头节点之前创建一个虚拟头节点;

  • 通常什么时候需要虚拟头节点?

    链表头地址有可能改变时,借助虚拟头节点可以方便我们操作链表。

  • 然后把 pre 之后的所有节点看成一个单独的链表, 并反转这个链表的前n个节点(n = right - left + 1);
  • 最后再把 pre 指向 反转链表的头节点;
  1. javascript 实现
/**
 * 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} left
 * @param {number} right
 * @return {ListNode}
 */

function reverseN(head, num) {
    if(num == 1) return head;
    // p暂存下一节点
    let p = head.next, c = reverseN(head.next, num-1);
    // 让当前节点与后一节点的指针对调
    head.next = p.next;
    p.next = head
    return c
}
var reverseBetween = function(head, left, right) {
    let ret = new ListNode(0, head);
    let pre = ret; //初始pre从虚拟头节点开始
    let count = right - left + 1
    while(--left) pre = pre.next; // 找到待反转区域的前一位
    pre.next = reverseN(pre.next, count); // 让pre 指向反转完成的头节点
    return ret.next
};

leetcode25.k个一组翻转链表

  1. 题目简述 给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。

如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

进阶:

你可以设计一个只使用常数额外空间的算法来解决此问题吗? 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

示例:

image.png

输入: head = [1,2,3,4,5], k = 2
输出: [2,1,4,3,5]
  1. javascript实现
/**
 * 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} k
 * @return {ListNode}
 */
//  每k个节点一组进行反转; 首先看链表是否有k个节点
//  如果够k个节点,每次反转前k个节点
// 设 p = 待反转区域的前一个节点,那么p.next 就是下一组待反转区域的前一个节点

function __reverseN(head, n) {
    if(n == 1) return head;
    let p = head.next, q = __reverseN(head.next, n-1);
    head.next = p.next
    p.next = head
    return q
}
function reverseN(head, k) {
    let p = head;
    let n = k
    while(--k && p) p = p.next;
    if(p == null) return head;
    return __reverseN(head, n)
}

var reverseKGroup = function(head, k) {
    // 设 p = 待反转区域的前一个节点,那么p.next 就是下一组待反转区域的前一个节点q
    let ret = new ListNode(-1, head), p = ret, q = p.next;
    // 如果反转后的头节点 == q 的话就意味着没有发生反转;
    while((p.next = reverseN(q,k)) != q) {
        p = q;
        q = p.next;
    }
    return ret.next

};

leetcode61.旋转链表

  1. 题目简述 给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

示例:

image.png

输入: head = [1,2,3,4,5], k = 2
输出: [4,5,1,2,3]
  1. javascript实现
/**
 * 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} k
 * @return {ListNode}
 */
var rotateRight = function(head, k) {
    // 记录链表的长度,找到链表最后一个节点让它指向头节点,这就是一个圈啊
    if(head == null) return head;
    let n = 1, p = head;
    while(p.next) p = p.next, n += 1;
    p.next = head;
    // 当k == n时,相当于转一圈又回到原位了;
    k %= n; // 忽略整圈数
    // 关键:找尾节点!
    // 向左移动k==》把前k个节点移动到后面
    // 向右移动k==》把后k个节点移动到前面,相当于把前 n-k 个节点移到后面
    k = n - k;
    while(k--) p = p.next;
    head = p.next;
    p.next = null
    return head


};

3、链表的节点删除

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

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

示例:

image.png

输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]
  1. javascript实现
/**
 * 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}
 */
//  1 -> 2 -> 3 -> 4 -> 5 -> null
//  找到待删除节点的前一个节点p
//  从p 到最后null,间隔n 个节点
//  设置虚拟节点,p;
//  头节点 q,
//  先让q 向后走n 步;
// 这时 p与q 间隔n个节点,
// 然后让p,q 同步向后走,当q为null时,p就是待删除节点的前一个节点
var removeNthFromEnd = function(head, n) {
    let ret = new ListNode(0, head),p = ret, q = head;
    while(n--) q= q.next;
    while(q) {
        p = p.next;
        q= q.next;
    }
    
    p.next = p.next.next;
    return ret.next
};

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

  1. 存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。返回同样按升序排列的结果链表。

示例:

image.png

输入: head = [1,1,2,3,3]
输出: [1,2,3]
  1. javascript实现
/**
 * 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 deleteDuplicates = function(head) {
    if(head ==null) return head;
    let p = head;
    while(p.next) {
        if(p.val == p.next.val) {
            p.next = p.next.next
        }else {
            p = p.next
        }
    }
    return head;
};

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

  1. 存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。返回同样按升序排列的结果链表。

示例:

image.png

输入: head = [1,2,3,3,4,4,5]
输出: [1,2,5]
  1. javascript实现
/**
 * 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 deleteDuplicates = function(head) {
    // 头节点可能重复,需要借助虚拟节点
    let ret = new ListNode(0, head), p = ret, q;
    while(p.next) {
        // 当p.next与它下一节点值重复
        if(p.next.next && p.next.val == p.next.next.val) {
            //先找到不等于p.next的节点
            q = p.next.next;
            while(q && q.val == p.next.val) q = q.next;
            p.next = q;
        }else {
            p = p.next
        }
    }
    return ret.next
};