javascript:链表(List)及经典问题

207 阅读9分钟

1.链表基本知识

  1. 链表中的每个节点至少包含两个部分,数据域与指针域

  2. 链表中的每个节点,通过指针域的值,形成一个线性结构

  3. 查找节点O(n),插入节点O(1),删除节点O(1)

  4. 不适合快速的定位数据,适合动态的插入和删除数据的场景

2.几种经典的链表实现方式

1. 第一类

// 节点类
class Node {
  constructor(data) {
    this.data = data; // 数据域
    this.next = null; // 指针域
  }
}

function main() {
  //设置链表头
  let head = null;
  // head先指向一个新节点
  head = new Node(1);
  // head指向下一个节点
  head.next = new Node(2);
  // 依次指向
  head.next.next = new Node(3);
  head.next.next.next = new Node(4);
  // 创建了一条4个节点的链表,从头遍历
  let p = head;
  // 当p不为空时,因为链表最后一位是空
  while (p !== null) {
    console.log(p.data, "->");
    // p依次指向链表中的每一个节点
    p = p.next;
  }
}

main();  

2. 第二类

// 拆成两个数组
let data = new Array(10).fill(0); // 数据域
let next = new Array(10).fill(0); // 指针域

// 添加节点
// 在ind节点后面添加一个地址为p的节点,值为val
function add(ind, p, val) {
  // 为了可以中间插入,让p节点指向原先ind节点指向的位置
  next[p] = next[ind];
  // ind节点指向p
  next[ind] = p;
  // 节点p值为val
  data[p] = val;
  return;
}

function main() {
  // 首先定义头节点的地址是3
  let head = 3;
  // 地址为3的节点值为0
  data[3] = 0;
  // 依次添加
  add(3, 5, 1);
  add(5, 2, 2);
  add(2, 7, 3);
  add(7, 9, 100);
  // 遍历
  let p = head;
  // 当p不为0时,因为数组初始填充0
  while (p !== 0) {
    console.log(data[p], "->");
    p = next[p];
  }
}
main();

3.链表的经典应用场景

  • 场景一:操作系统内的动态内存分配
  • 场景二:LRU 缓存淘汰算法

4.经典面试题

141. 环形链表

  • 思路一:我们只需要依次遍历整个链表,并创建一个哈希表来存储遍历过的节点,遍历每一个节点,然后存入哈希表。在存入哈希表之前,先判断哈希表中是否存在该节点。如果不存在,则存入哈希表,一直遍历到某节点的next节点为null,说明链表没有环,遍历结束。当要存入的节点,已经存在于哈希表中,说明链表有环,遍历结束。
  • 思路二:快慢指针;如果链表有环,那么快慢指针一定会相遇,指向同一个节点,当指向同一个节点时,遍历结束
var hasCycle = function(head) {
    // 如果头节点为空,肯定没有环
    if(head === null) return false;
    // 定义两个指针,p是慢指针,q是快指针
    let p = q = head;
    // 如果q.next为空,说明只有一个节点,肯定没有环
    if(q.next === null) return false
    do{
        // p每次走一步,q每次走两步
        p = p.next;
        q = q.next.next;
    }while(p !== q && q && q.next);
    // q和q的next节点都不为null,说明有环
    return q && q.next ;
};

142. 环形链表 II

证明:pq相遇点距入环口的距离与头节点距入环口的距离相等
前提:p每次走一步,q每次走两步
假设入环口距离头节点为a,当慢指针p走到入环口时,q2a处,再假设当前q已在环内,距离入环口为x,即距离p也是x,得出链表总长2a+x。此时q要追上p,要经过x次迭代,所以会在a+x处相遇。此时相遇点距入环口的距离也为a

var detectCycle = function(head) {
    // 先判断有没有环
    if(head === null) return null;
    let p = q = head;
    if(q.next === null) return null
    do{
        p = p.next;
        q = q.next.next;
    }while(p !== q && q && q.next);
    // q或者q的next节点为null,说明没环
    if(q === null || q.next === null) return null;
    // 有环,p重置到头节点,然后p、q同步走,相遇即是开始入环的第一个节点
    p = head;
    while(p !== q) p = p.next, q = q.next;
    return q;
};

202. 快乐数

当输入值为19时
19 -> 82 -> 68 -> 100 -> 1
链表思维->唯一指向思维
题目就可以转化为,把每个数看做节点,转化规则看做指针,1看成空地址,本质上就是判断一个链表是否有环。 如果遍历到重复的节点值,说明有环,就不是快乐数,即141的解法

var getNext = function(x){
    let z = 0;
    while(x){
        // 取个位的平方相加
        z += (x % 10) * (x % 10);
        // 去掉个位
        x = Math.floor(x / 10);
    }
    return z;
}
var isHappy = function(n) {
    let p = q = n;
    do{
        p = getNext(p);
        q = getNext(getNext(q));
    }while(p !== q && q !== 1);
    // q为1说明是快乐数
    return q === 1;
};

206. 反转链表

  1. 第一种解法 定义指针——prepre指向空(反转头)
    定义指针——curcur指向我们的头节点(未反转头)
    定义指针——pp指向cur所指向节点的下一个节点(未反转头的下一位),这样我们的指针就初始化完毕了
    首先,我们将cur指针所指向的节点指向pre指针所指向的节点
    然后移动指针pre到指针cur所在的位置,移动curp所在的位置,此时,我们已经反转了第一个节点
    将我们的p指针指向cur指针所指向节点的下一个节点
    然后重复上述操作,当cur指针指向 null 的时候,就完成了整个链表的反转
var reverseList = function(head) {
    if(head === null) return null;
    let pre = null, cur = head, p = head.next;
    while(cur){
        cur.next = pre;
        pre = cur;
        (cur = p) && (p = p.next);
    }
    return pre;
};
  1. 第二种解法
var reverseList = function(head) {
    // 给递归加结束条件,返回原链表最后一个节点,即新链表第一个节点
    if(head === null || head.next === null) return head;  
    // 拿到新链表第一个节点,并在最后返回
    let p = reverseList(head.next);
    // 保存当前节点的下一个节点
    let tail = head.next;
    // 当前节点指向下一个节点的指向
    head.next = tail.next;
    // 下一个节点指向当前节点
    tail.next = head;
    return p;
};

92. 反转链表 II

编程技巧:虚拟头节点
思路:找到待反转区域的前一位,然后把反转区域看成一个链表,调用反转头n个节点的函数,反转n-m+1个节点后,再把反转区域的前一位指向反转后的新链表的头节点

// 反转前n个节点,参照92题
var reverseN = function(head, n) {
    if(n === 1) return head;
    let p = reverseN(head.next, n - 1);
    let tail = head.next;
    head.next = tail.next;
    tail.next = head;
    return p;
};
var reverseBetween = function(head, left, right) {
    // 先设置一个虚拟头节点,p指向虚拟头,用作迭代
    let ret = new ListNode(0, head), p = ret;
    // 反转的位数,后续改变了left的值,所以先存起来
    let cnt = right - left + 1;
    // 先--,因为要走left-1步,找到待反转区域的前一位
    while(--left) p = p.next;
    // 反转p后的节点
    p.next = reverseN(p.next, cnt);
    return ret.next;
};

25. K 个一组翻转链表

// 反转前n个节点,参照92题
var __reverseN = function(head, n) {
    if(n === 1) return head;
    let p = __reverseN(head.next, n - 1);
    let tail = head.next;
    head.next = tail.next;
    tail.next = head;
    return p;
};
// 判断剩余链表中够不够n个节点
var reverseN = function(head, n){
    let p = head;
    // 后续改变了n的值,所以先存起来
    let cnt = n;
    while(--n && p) p = p.next;
    // p为空退出循环,说明不够n个节点返回原节点
    if(p === null) return head;
    return __reverseN(head, cnt);
}

var reverseKGroup = function(head, k) {
    // 设置虚拟头,p指向待反转区域的前一位,q指向待反转区域的头一位,也是反转后的尾节点
    let ret= new ListNode(0, head), p = ret, q = p.next;
    // 反转后的头节点是原头节点时,证明没有反转,剩的节点不够k个,停止循环
    while((p.next = reverseN(q, k)) !== q){
        // 此时q是待反转区域的前一位
        p = q;
        q = p.next;
    }
    return ret.next;
};

61. 旋转链表

举例,我们命名一个指针指向链表的Head,K=2是让Head往右移动两位。第一步,通过遍历,得到链表的长度length和链表的尾节点,然后让尾节点指向链表的Head,这样就成了环,再让尾节点走length - k步,得到计算新链表head的前一位,断开和head的指向,形成新链表

var rotateRight = function(head, k) {
    if(head === null) return null;
    let n = 1, p = head;
    // 走到最后一位,并算出链表长度
    while(p.next) p = p.next, n++;
    // 首尾相连
    p.next = head;
    // 取余,抛掉整圈数
    k %= n;
    // 走n-k步
    k = n - k;
    while(k--) p = p.next;
    head = p.next;
    // 断开首尾连接
    p.next = null;
    return head;
};

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

需要找到待删除元素的前一个节点,该节点此时距离末尾空节点也为N
方法:设p指向虚拟头节点、q节点指向头节点,并往后走N步,再pq一起走,当q指向空节点时,q指向待删除元素的前一个节点

var removeNthFromEnd = function(head, n) {
    let ret = new ListNode(0, head), p = ret, q = head;
    // q先往后走n步
    while(n--) q = q.next;
    // p、q一起走
    while(q) p = p.next, q = q.next;
    // 此时p指向待删除元素的前一位,绕过下一个节点,完成删除
    p.next = p.next.next;
    return ret.next;
};

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

var deleteDuplicates = function(head) {
    if(head === null) return null;
    let p = head;
    while(p.next){
        // 判断当前节点和下一个节点值是否相同,相同则删除下一个节点,否则继续向后遍历
        if(p.val === p.next.val){
            p.next = p.next.next;
        }else{
            p = p.next;
        }
    }
    return head;
};

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

var deleteDuplicates = function(head) {
    if(head === null) return null;
    let ret = new ListNode(0, head), p = ret, q;
    while(p.next){
        // 判断当前节点的下一个节点值是否产生重复,重复则找到下一个不重复的节点的地址,否则继续向后遍历
        if(p.next.next && p.next.val === p.next.next.val){
            q = p.next.next;
            while(q && q.val === p.next.val) q = q.next;
            p.next = q;
        }else{
            p = p.next;
        }   
    }
    return ret.next
};

86. 分隔链表

我们可以设置两个头指针,一个连接所有小于特定值x,另一个连接大于x,再把两条链表相连

var partition = function(head, x) {
    // r1:放所有小于x的虚拟头节点
    // r2:放所有大于x的虚拟头节点
    // p1:r1链表的尾节点
    // p2:r2链表的尾节点
    // p:迭代原链表
    // q:存p的下一个节点
    let r1 = new ListNode(), r2 = new ListNode(), p1 = r1, p2 = r2, p = head, q;
    while(p){
        // 先将p的下一个节点存到q中
        q = p.next;
        // 小于x时往r1接入,否则往r2接入
        if(p.val < x){
            // p.next先存原先p.next中的值,p1接p,再重新让p1指向此时最后一个节点p
            p.next = p1.next;
            p1.next = p;
            p1 = p;
        }else{
            // 同上
            p.next = p2.next;
            p2.next = p;
            p2 = p;
        }
        p = q;
    }
    // 两链表相连
    p1.next = r2.next;
    return r1.next;
};

138. 复制带随机指针的链表

1->2->3->4

  1. 首先,在每个节点后面新插入一个当前节点的复制节点1->1'->2->2'->3->3'->4->4'
  2. 再让每个复制出来的节点的random指针向后指一位,比如假设1random指针指向3,那么复制出的1'random也指向3,正确的应该时让1'random指向3'
  3. 最后将新链表两两分开,拆成两条独立的链表,得到1'->2'->3'->4'
var copyRandomList = function(head) {
    // 判空
    if(head === null) return null;
    // 第一步
    let p = head, q, new_head;
    while(p){
        // 复制一个新节点,并接在当前q节点后
        q = new Node(p.val);
        q.random = p.random;
        q.next = p.next;
        p.next = q;
        p = q.next;
    }
    // 第二步
    p = head.next;
    while(p){
        if(p.random) p.random = p.random.next;
        // 判空
        (p = p.next) && (p = p.next)
    }
    // 第三步
    new_head = head.next;
    p = head;
    while(p){
        // p是旧链表节点,p是新链表节点
        q = p.next;
        p.next = q.next;
        if(p.next) q.next = p.next.next;
        p = p.next;
    }
    return new_head;
};