剑指offer-链表专题-2022.04

140 阅读4分钟

1.链表专题

1.1从尾到头打印链表

  • 输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。
  • 方法:使用数组unshift倒叙存储链表节点的值,返回数组
function printListFromTailToHead(head){
    let arr=[];
    while(head){
        arr.unshift(head.val);
        head=head.next;
    }
    return arr;
}

1.2反转链表

  • 给定一个单链表的头结点pHead(该头节点是有值的),长度为n,反转该链表后,返回新链表的表头。要求:空间复杂度 O(1) ,时间复杂度 O(n)。
  • 方法: (1)用数组unshift存储节点,构造单链表。(注意此方法:空间复杂度O(n))
function ListNode(x){
    this.val = x;
    this.next = null;
}
function ReverseList(pHead){
    if(!pHead) return null;
    const arr=[];
    //存储节点
    while(pHead){
        arr.unshift(pHead);
        pHead=pHead.next;
    }
    //构造链表
    let head=new ListNode(0);//哨兵节点
    let cur=head;
    for(let i=0;i<arr.length;i++){
        cur.next=arr[i];
        cur=cur.next;
    }
    //注意:千万不要忘记构造链表时,最后一个节点的next值为null
    //因为连接的节点不是新创建的,是原来的节点,next储存有原来的地址)
    cur.next=null;
    return head.next;
}

(2)指针法(第一二个指针分别指向原链表和反转链表,第三个指针存储原链表地址)(空间复杂度 O(1))

function ReverseList(pHead){
    if(!pHead) return null;
    let pre=null;  //指向反转链表的最新反转节点
    let cur=pHead;  //指向原链表的第一个待反转节点
    let next=null;  //指向原链表的下一个待反转节点
    while(cur){
        next=cur.next;  //存储原链表的下一个待反转节点
        cur.next=pre;  //将原链表的头节点添加到反转链表
        pre=cur;  //更新反转链表
        cur=next;  //更新原链表
    }
    return pre;  //返回反转链表
}

1.3合并两个排序的链表

  • 输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。要求:空间复杂度 O(1),时间复杂度 O(n)
  • 方法:双指针分别指向两个链表的第一个节点,比较两节点值的大小,值较小的节点给新链表,当其中一个链表为空时,将另一个链表给新链表。
function ListNode(x){
    this.val = x;
    this.next = null;
}
function Merge(pHead1, pHead2){
    let one=pHead1;
    let two=pHead2;
    let head=new ListNode(0);  //设置哨兵节点
    let cur=head;
    while(one&&two){
        if(one.val<two.val){  //值较小的节点给新链表
            cur.next=one;
            one=one.next;
        }else{
            cur.next=two;
            two=two.next;
        }
        cur=cur.next;
    }
    cur.next=one?one:two;  //当其中一个链表为空时,将另一个链表给新链表
    return head.next;
}

1.4两个链表的第一个公共节点

  • 输入两个无环的单向链表,找出它们的第一个公共结点,如果没有公共节点则返回空。要求:空间复杂度 O(1),时间复杂度 O(n)
  • 方法:使用两个指针,一个指针将链表1从头遍历到尾,遍历完接着将链表2从头遍历到尾;另一个指针将链表2从头遍历到尾,遍历完接着将链表1从头遍历到尾。两个指针,以相同的速度,同时遍历相同的长度(链表1+链表2),能够同时到达终点。
function FindFirstCommonNode(pHead1, pHead2){
    let one=pHead1;
    let two=pHead2;
    while(one!==two){
        one=one?one.next:pHead2;
        two=two?two.next:pHead1;
    }
    return one;
}

1.5链表中环的入口节点

  • 给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。要求:空间复杂度O(1),时间复杂度O(n)
  • 方法: (1)哈希法:遍历链表,若当前节点不在set中,添加到set;若在set中,返回节点。
function EntryNodeOfLoop(pHead){
    const set=new Set();
    while(pHead){
        if(set.has(pHead)){
            return pHead;
        }else{
            set.add(pHead);
        }
        pHead=pHead.next;
    }
}

(2)快慢指针法:快指针的速度是慢指针的二倍,二者在环中相遇,设起点A到入环节点B的距离为x,B到相遇点C距离为y,则2(x+y)=x+y+C,C=x+y。

function EntryNodeOfLoop(pHead){
    let fast=pHead;
    let slow=pHead;
    while(fast&&fast.next){
        fast=fast.next.next;
        slow=slow.next;
        if(fast==slow) break;
    }
    //若两指针因都为null而跳出循环,返回null
    if(!fast||!fast.next) return null;
    fast=pHead;  //两指针相遇后,fast指针从头开始走
    while(fast!=slow){  //再次相遇的点就是入环节点
        fast=fast.next;
        slow=slow.next;
    }
    return fast;
}

1.6链表中倒数最后k个结点

  • 输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。如果该链表长度小于k,请返回一个长度为 0 的链表。要求:空间复杂度 O(n),时间复杂度 O(n);进阶:空间复杂度 O(1),时间复杂度 O(n)
  • 方法: (1)用数组存储链表,查找倒数第k个节点
function (pHead){
  const arr=[];
  while(pHead){
    arr.push(pHead);  //使用数组存储链表
    pHead=pHead.next;
  }
  if(arr.length<k) return null;
  return arr[arr.length-k];
}

(2)快慢指针:fast指针先走k步,然后fast接着走的同时,slow从头开始走,当fast指针走完链表时(走了n-k步),slow指针到达倒数第k个节点

function (pHead){
  let fast=pHead;
  let slow=pHead;
  while(fast&&k){  //fast指针先走k步
    fast=fast.next;
    k--;
  }
  if(!fast&&k) return null;  //链表长度小于k,返回null
  while(fast){  
    fast=fast.next;  //fast接着走完链表,走(n-k)步,
    slow=slow.next;  //slow从头开始走,到达倒数第k个节点
  }
  return slow;
}

1.7删除链表的节点

  • 给定单链表的头指针和一个要删除的节点的值,定义一个函数删除该节点,返回删除后的链表的头节点
  • 方法:双指针法
function deleteNode(head,val){
    let header=new ListNode(0); //创建哨兵节点
    header.next=head;
    let pre=header;
    let cur=head;
    while(cur){
      if(cur.val==val){
        cur=cur.next; //当前指针后移
        pre.next=cur; //pre指针连接当前节点
      }
      pre=pre.next;
      cur=cur.next;
    }
    return header.next;
}

1.8删除链表中重复的节点(一)

  • 在一个排序链表中,存在重复的结点,请删除该链表中重复的结点,使链表中的所有元素都只出现一次,返回链表头指针。例如,链表 1->2->3->3->4->4->5 处理后为 1->2->3->4->5,进阶:空间复杂度 O(1) ,时间复杂度 O(n)
  • 方法:双指针直接删除法:比较相邻两个节点,若两节点重复,将当前指针cur指向后一个节点;重复比较,使得当前指针cur指向重复节点的最后一个节点;重复节点保留,pre指针存储当前节点。若两节点不重复,两指针后移。
function (pHead){
    let head=new ListNode(0); //哨兵节点
    head.next=pHead;
    let pre=head;
    let cur=pHead;
    while(cur){
      if(cur.next&&cur.val==cur.next.val){
        cur=cur.next; //cur指针指向相邻两个重复节点的后一个节点
        while(cur.next&&cur.val==cur.next.val){
          cur=cur.next; //cur指针指向相邻两个重复节点的后一个节点
        }
        pre.next=cur; //pre存储当前节点
      }else{ //节点后移
        pre=pre.next;
        cur=cur.next;
      }
    }
    return head.next;
}

1.9删除链表中重复的节点(二)

-在一个排序链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5,进阶:空间复杂度 O(1) ,时间复杂度 O(n)

  • 方法: (1)使用set,暴力解法:遍历链表,使用set存储重复节点的值,再次遍历链表,使用双指针将重复的节点删除
function (pHead){
    if(!pHead) return;
    const set=new Set(); //建立set存储重复节点的值
    let head=new ListNode(0);
    head.next=pHead;
    let pre=pHead;
    let cur=pHead.next;
    while(cur){
      if(pre.val==cur.val){ 
          set.add(pre.val); //遍历链表,使用set存储重复节点的值
      }
      pre=pre.next;
      cur=cur.next;
    }
    pre=head;
    cur=pHead;
    while(cur){
        if(set.has(cur.val)){ //再次遍历链表,使用双指针将重复的节点删除
            cur=cur.next;
            pre.next=cur;
        }else{
            pre=pre.next;
            cur=cur.next;
        }
    }
    return head.next;
}

(2)双指针直接删除法:比较相邻两个节点,若两节点重复,将当前指针cur指向后一个节点;重复比较,使得当前指针cur指向重复节点的最后一个节点;重复节点不保留(当前节点后移),pre指针存储当前节点。若两节点不重复,两指针后移。

function (pHead){
    let head=new ListNode(0);
    head.next=pHead;
    let pre=head;
    let cur=pHead;
    while(cur){
      if(cur.next&&cur.val==cur.next.val){
        cur=cur.next; //cur指针指向相邻两个重复节点的后一个节点
        while(cur.next&&cur.val==cur.next.val){
          cur=cur.next; //cur指针指向相邻两个重复节点的后一个节点
        }
        cur=cur.next; //重复节点不保留
        pre.next=cur; //pre存储当前节点
      }else{ //节点后移
        pre=pre.next;
        cur=cur.next;
      }
    }
    return head.next;
}

1.10复杂链表的复制

  • 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针random指向一个随机节点),请对此链表进行深拷贝,并返回拷贝后的头结点。
  • 思路: (1)哈希法:首先创建一个哨兵节点,遍历链表,先将除随机指针外的部分创建并连接,同时用哈希表记录指针之间的映射;遍历链表,添加随机指针指向。
function RandomListNode(x){
    this.label = x;
    this.next = null;
    this.random = null;
}
function Clone(pHead){
    if(!pHead) return null;
    const map=new Map(); //设置map存储映射关系
    let head=new RandomListNode(0); //哨兵节点
    let pre=head; //pre指向复制链表
    let cur=pHead; //cur指向原链表
    while(cur){ 
      let clone=new RandomListNode(cur.label); //拷贝节点
      pre.next=clone; //连接节点
      map.set(cur,clone); //记录映射
      pre=pre.next;
      cur=cur.next;
    }
    cur=pHead;
    while(cur){
      map.get(cur).random=map.get(cur.random); //添加拷贝节点的随机指针
      cur=cur.next;
    }
    return head.next;
}

(2)拷贝连接拆分法:将每个复制的节点连接到源节点之后,添加复制节点的随机指针指向,最后拆分链表,返回拷贝链表。

function Clone(pHead){
    if(!pHead) return null;
    //将拷贝节点连接到对应原节点后
    let cur=pHead;
    while(cur){
        let clone=new RandomListNode(cur.label);//拷贝节点
        clone.next=cur.next;
        cur.next=clone;
        cur=clone.next;
    }
    //添加随机指针指向
    let old=pHead;
    let clone=pHead.next;
    while(old){
        clone.random=old.random?old.random.next:null; //添加拷贝节点的随机指针指向
        if(old.next) old=old.next.next; //注意判空
        if(clone.next) clone=clone.next.next;
    }
   //拆分链表
    old=pHead;
    clone=pHead.next;
    let res=pHead.next; //res指针指向第一个拷贝节点
    while(old){
        if(old.next) old.next=old.next.next; //拆分原链表(必须先拆分原链表)
        if(clone.next) clone.next=clone.next.next; //拆分拷贝链表
        old=old.next;
        clone=clone.next;
    }
    return res;
}