【前端算法系列】链表

933. 最近的请求次数

  • 有新请求就入队,3000ms前发出的请求出队
  • 队列长度就是最近请求次数
/*
    时间复杂度:有while循环体,时间复杂度是O(n),n就是被踢出去的请求个数
    空间复杂度 O(n):设置了数组this.q,队列的长度是请求个数,n
*/
var RecentCounter = function(){
    this.q = []
}

RecentCounter.prototype.ping=function(t){
    this.q.push(t) // 每次发起请求,把新请求入队
    while(this.q[0]<t-3000){ // 小于表示不在范围内,大于才或等于才正常
        this.q.shift() // 把队头不在[t-3000, t]这个时间范围内的老请求就踢出去
    }
    return this.q.length
}
const a = new RecentCounter()
a.ping([])
a.ping([1])
a.ping([100])
a.ping([3001])
a.ping([3002])

2.两数相加

思路:遍历两个链表,模拟相加操作,将个位数追加到新链表上,将十位数留到下一位去相加

/** 时间复杂度:有while循环体,所以是O(n),循环次数n是l1、l2链表长度的较大值
 *  空间复杂度:有新造的链表l3,这个链表的长度也可能是两个链表l1、l2中较长的链表长度,还有进一的例子(较长链表再加一位),O(n),n为两链表较长的长度
 */ 
/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
var addTwoNumbers = function(l1, l2){
    const l3 = new ListNode(0) // 新链表
    let p1 = l1
    let p2 = l2
    let p3 = l3 // 往新链表追加元素时,需要一个指针不停指向最后一个节点,才能在最后一个节点进行追加
    let carry = 0 // 十位上的数,留到下一轮相加
    while(p1 || p2){
        const v1 = p1? p1.val:0; // l1、l2链表有长有短,为空就为0
        const v2 = p2? p2.val:0; 
        const val = v1+v2+carry // 加上上一轮带过来的carry
        carry = Math.floor(val/10) // 获取十位上的数
        p3.next = new ListNode(val%10) // 获取个位上的数
        // 判断有没有值,没值就不能调用.next,有值就把指针移到下一位 
        if(p1) p1 = p1.next
        if(p2) p2 = p2.next
        p3 = p3.next   
    }
    // 循环体结束后,判断最后一个是否进一了,有的话就追加到新链表上
    if(carry) p3.next = new ListNode(carry)
    return l3.next // 空结点(0)的后面,所以next
}

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

  • 因为链表是有序的,所以重复元素一定相邻
  • 遍历链表,如果发现当前元素和下一个元素值相同,就删除下一个元素值
  • 遍历结束后,返回原链表的头部
/** 时间复杂度:O(n) while循环体,n为链表长度
 *  空间复杂度:没有额外的存储,所以为O(1)
 */
var deleteDuplicates = function(head) {
    let p = head
    while(p && p.next){
        if(p.val ===p.next.val){
            p.next = p.next.next
        }else{
            p = p.next // 不停遍历下去
        }
    }
    return head
};

★82.删除排序链表中的重复元素 II(不留独苗,哨兵结点)

跟上面的区别是不留独苗,即把前驱和后继一起删除
链表的第一个结点,因为没有前驱结点,所以需要哨兵结点(处理头结点为空的边界问题)
遇到值相同的相邻节点时,不断往前遍历,直到找到值不相等的相邻节点,将初始位置的上一个节点的next指针指向现在遍历到的结点位置

const deleteDuplicates = function(head) {
    // 极端情况:0个或1个结点,则不会重复,直接返回
    if(!head || !head.next) {
        return head
    }
    // dummy 登场
    let dummy = new ListNode() 
    // dummy 永远指向头结点
    dummy.next = head   
    // cur 从 dummy 开始遍历
    let cur = dummy 
    // 当 cur 的后面有至少两个结点时
    while(cur.next && cur.next.next) {
        // 对 cur 后面的两个结点进行比较
        if(cur.next.val === cur.next.next.val) {
            // 若值重复,则记下这个值
            let val = cur.next.val
            // 反复地排查后面的元素是否存在多次重复该值的情况
            while(cur.next && cur.next.val===val) {
                // 若有,则删除
                cur.next = cur.next.next 
            }
        } else {
            // 若不重复,则正常遍历
            cur = cur.next
        }
    }
    // 返回链表的起始结点
    return dummy.next;
}

https://github.com/Alex660/Algorithms-and-data-structures

剑指Offer 18.删除链表的节点(删除指定节点)

var deleteNode = function(head, val) {
    if (head == null) return null;
    if (head.val == val) return head.next;
    let p = head;
    while (p.next != null && p.next.val != val) {
        p = p.next;
    }
    if (p.next != null) {
        p.next = p.next.next;
    }
    return head;
};

237.删除链表中的节点(同上)

  • 无法直接获取被删出节点的上一个节点
  • 将被删除节点转移到下一个节点

1)将被删除节点的值改为下个节点
2)删除下一个节点
比如删除9,先把9赋值到节点1上,把最后一个9删掉,就变相删除节点1

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

★判断链表是否有环

  • 方法1:设置flag
    • 暴力求解,遍历链表,用哈希表Map或Set记录访问过的结点,后面看在新访问的元素是否之前存在Map/Set里面,表示又走回到原来的老结点去
    • 直接在head上定义属性flag
var hasCycle = function(head) {
  let map = new Map()
  while(head!=null){
    if(map.has(head)){
      return true
    }else{
      map.set(head, head)
    }
    head = head.next
  }
  return false
}

// flag
// 入参是头结点 
const hasCycle = function(head) {
    // 只要结点存在,那么就继续遍历
    while(head){
        // 如果 flag 已经立过了,那么说明环存在
        if(head.flag){
            return true
        }else{
            // 如果 flag 没立过,就立一个 flag 再往
            下走
            head.flag = true
            head = head.next
        }
    }
    return false
}
  • 方法2:快慢指针
    思路:两个人在操场上起点同时起跑,速度快的人一定会和速度慢的人相遇
    用一快一慢两个指针(即快指针走两步,慢指针走一步)遍历链表,如果指针能够相遇,那么链表就表示有环

其他方法:每走一个节点就存到一个数组里,下次走的话再去数组查找有没有当前节点(走n次O(n))

/** 时间复杂度:O(n),有while循环体,循环次数是n次
 *  空间复杂度:o(1),没有额外的存储
*/ 
var hasCycle = function(head) {
    let slow = head, fast = head
    // 判断当前节点的next是否为null,一直判断
    while(slow&&fast&&fast.next){
        fast = fast.next.next
        slow = slow.next
        if(fast === slow){
            return true
        }
    }
    return false
}

面试题02.08.环路检测

  • 设置标记判断
const detectCycle = function(head) {
    while(head){
        if(head.flag){
            return head;
        }else{
            head.flag = true;
            head = head.next;
        }
    }
    return null;
}
  • 快慢指针:快慢两个指针,找相遇的点。相遇后一个指针重置,改成两个慢指针。再相遇即为入口
var detectCycle = function(head) {
    let slow = head, fast = head
    // 判断当前节点的next是否为null,一直判断
    while(slow&&fast&&fast.next){
        fast = fast.next.next
        slow = slow.next
        if(fast === slow){
            // 相遇后重置指针,再相遇即为入口
            slow=head
            while(slow!=fast){
                slow=slow.next
                fast=fast.next
            }
            return slow;
        }
    }
    return null
}

链表:

  • 查找:需要遍历,遍历n次就是O(n)
  • 插入:找到要插入的位置,新结点的next指向要插入位置的后面结点,再把前面结点的next指向新结点 O(1)时间复杂度
  • 删除:把前面结点的next指向后面结点的,相当于跨过要删除的结点,再把要删除的结点从内存中删掉 O(1)时间复杂度

双链表:既有前驱,又有后继,可以往前走,也可往后走

如何创建链表、排序、检验是否闭环

链表的快速排序比链表的堆排序使用更大

链表只会暴露头指针,所有元素是不能直接访问到的,必须通过头指针不断访问next对象才能拿到具体元素。这里的快排序和数组的快排序不一样,数组通过下标可以访问到元素,可以从左和右两边快排序,链表的不能这样做,因为拿不到最后的值

// 构造链表
class ListNode {
    constructor(val) {
        this.val = val
        this.next = undefined
    }
}
class NodeList {
    constructor(arr) {
        this.head = new ListNode(arr.shift())
        let next = this.head
        arr.forEach(item => {
            next.next = new ListNode(item)
            next = next.next
        })
    }
}

★206.反转链表

1->2->3->4 反转后 4->3->2->1
把每个结点的next指向前驱(上一个)结点,可以用递归或迭代(用三个指针)

  • 方法一:reverse()
var reverseList = function(head) {
    const res = []
    let curr = head
    while(curr !== null) {
        res.unshift(curr.val)
        curr = curr.next
    }
    return res
};
  • 方法二:迭代反转
    思路:定义三指针pre->cur->next,只要让cur.next = pre就可以反转
    通过三指针, 将单链表中的每个节点的后继指针指向它的前驱节点即可
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9oq49nju-1610543019568)(…/images/linkList/linkList_img_02.png)]
/** 时间复杂度:有while循环体,O(n)
 *  空间复杂度:临时变量是单个值,没有数组,也没矩阵,所以空间复杂度是 O(1)
 */
const reverseList = function(head) {
    if(!head||!head.next) return head
    // 初始化前驱结点为 null
    let pre = null;
    // 初始化目标结点为头结点
    let cur = head;
    // 只要目标结点不为 null,遍历就得继续
    while (cur !== null) {
        // 记录一下next结点 (用next来保存后面的节点)
        let next = cur.next;
        // 反转指针
        cur.next = pre;
        // pre 往前走一步
        pre = cur;
        // cur往前走一步
        cur = next;
    }
    // 反转结束后,pre 就会变成新链表的头结点
    return pre
}
  • 方法三:递归
    1)如果当前节点是最后一个元素(curr.next == null),就让头节点指向当前节点
    2)如果当前节点不是最后一个元素,即有curr.next,就递归这个next,让next变为当前节点的前驱节点,就可以让前驱节点的next指向当前节点(进行反转),当前节点的next断开设置为null
/* 递归:如果当前结点还有next,就递归反转它的next结点,让这个next结点变成当前结点的前驱节点
(前驱节点的next指向当前节点,当前节点的next断开,即设置为null)
*/
var reverseList = function(head) {
  // 已经到了最后一个元素,头结点指向最后一个元素
    if(head == null || head.next == null){
        return head
    }
    // 反转上一个节点
    // 如果curr还有下一个结点,递归调用reverseList(curr.next)对下一个结点反转
    const curr = reverseList(head.next);
    //例如,1,2,3,4,5, null 
    // 让下一个结点的next指向curr
    head.next.next = head; 
    // 注意把head.next设置为null,切断4链接5的指针
    // 当前结点的下一个结点设为null
    head.next = null
    //每层递归返回当前的节点,也就是最后一个节点。(因为head.next.next改变了,所以下一层curr变4,head变3)
    return curr;
};

92.反转链表 II(局部反转)

1)定义p为游标,一直遍历到m-1的位置,赋值给节点leftHead作为缓存节点,leftHead.next就是m开始反转的位置
2)在i=m;i<n;i++中遍历进行反转
3)处理区间节点:把缓存节点的后继节点等于反转后的第一个节点leftHead.next=pre,反转后的最后一个节点next指向cur

// 入参是头结点、m、n
const reverseBetween = function(head, m, n) {
    // 定义pre、cur,用leftHead来承接整个区间的前驱结点
    let pre,cur,leftHead
    // 别忘了用 dummy 嗷
    const dummy = new ListNode()  
    // dummy后继结点是头结点
    dummy.next = head
    // p是一个游标,用于遍历,最初指向 dummy
    let p = dummy  
    // p往前走 m-1 步,走到整个区间的前驱结点处
    for(let i=0;i<m-1;i++){
        p = p.next
    }
    // 缓存这个前驱结点到 leftHead 里
    leftHead = p
    // start 是反转区间的第一个结点
    let start = leftHead.next  
    // pre 指向start
    pre = start
    // cur 指向 start 的下一个结点
    cur = pre.next
    // 开始重复反转动作
    for(let i=m;i<n;i++){
        let next = cur.next
        cur.next = pre
        pre = cur
        cur = next
    }
    //  leftHead 的后继结点此时为反转后的区间的第一个结点
    leftHead.next = pre
    // 将区间内反转后的最后一个结点 next 指向 cur
    start.next=cur
    // dummy.next 永远指向链表头结点
    return dummy.next
}

反转相邻的链表,让两两相邻的元素进行反转

1->2->3->4 反转后 2->1->4->3
如果有第5个结点,那么第5个就不用动

排序链表

在 O(n log n) 时间复杂度和常数级空间复杂度下,对链表进行排序

示例 1:

输入: 4->2->1->3

输出: 1->2->3->4

思路:选一个基准元素,把所有小于基准元素放左边,大于放右边;
再对基本元素左边的第一个元素作为左边的基本元素,进行上面的步骤

p指针左侧小于基准元素,p指针和q之间时大于基准元素,所以以p指针为中间线,p左侧小于,右侧大于

7大于基准元素6,p不动,q继续遍历
3小于基准元素6,3和7换位置,更新p指针
4和9也换位置,更新p指针
5和7也换位置,更新p指针
遍历完后,因为基准元素要在中间,所以p指针跟索引0的基准元素6换位置
然后把左边的进行递归,把右边的进行递归

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-20zl3LRj-1610543019572)(../images/linkList/linkList_img_01.png)]

思路:
1)找到数组的基准元素part

  • 设置两个指针q、p,q不断遍历,当遍历到比基准元素小的,就跟p的下一个结点做交换swap(p.next, q),且移动p的指针
  • q遍历完成后,把p位置元素和基准元素做交换swap(p, begin),让基准元素在中间
    2)拿到基准元素后,递归排序基准元素左边的sort(begin, part),递归排序基准元素右边的sort(part.next, end)
// 声明链表的节点
class Node {
    constructor(value) {
        this.val = value
        this.next = undefined
    }
}
// 声明链表的数据结构
class NodeList {
    constructor(arr) {
        // 声明链表的头部节点
        this.head = new Node(arr.shift())
        // 指针
        let next = this.head
        arr.forEach(item => {
            // next的next指向下一个结点
            next.next = new Node(item)
            // 把新结点作为下一个结点的父结点
            next = next.next
        })
    }
    // 交换两个节点的值
    static swap = (p, q) => {
        let val = p.val
        p.val = q.val
        q.val = val
    }
    // 寻找基准元素的节点
    static partion = (begin, end) => {
        // 基准元素
        let val = begin.val
        let p = begin
        let q = begin.next
        // 没有截止就继续遍历
        while (q !== end) {
            // 和基准元素做比较,比它小,就放左边
            if (q.val < val) {
                // 跟p的下一个结点交换
                // p = p.next
                // NodeList.swap(p, q)
                // 或者这样写
                NodeList.swap(p.next, q)
                // 移动p的指针
                p = p.next
            }
            // q不断移动
            q = q.next
        }
        // 让基准元素跑到中间去
        NodeList.swap(p, begin)
        return p
    }
    sort(){
        NodeList.sort(this.head) 
        let res = []
        let next = this.head
        while(next){
            res.push(next.val)
            next = next.next
        }
        return res
    }
    // 排序
    static sort(begin, end) {
        if (begin !== end) {
            // 获取基准元素
            let part = NodeList.partion(begin, end)
            // 递归基准元素的左边和右边
            this.sort(begin, part)
            this.sort(part.next, end)
        }
    }
}
const nodeList = new NodeList([4, 1, 3, 2, 7, 9, 10, 12, 6])
console.log(nodeList.sort())

环形链表

// 声明链表的节点

class Node {
  constructor (value) {
    this.val = value
    this.next = undefined
  }
}

// 声明链表的数据结构

class NodeList {
  constructor (arr) {
    // 声明链表的头部节点
    let head = new Node(arr.shift())
    let next = head
    arr.forEach(item => {
      next.next = new Node(item)
      next = next.next
    })
    return head
  }
}

export default function isCircle (head) {
  // 慢指针
  let slow = head
  // 快指针
  let fast = head.next
  while (1) {
    if (!fast || !fast.next) {
      return false
    } else if (fast === slow || fast.next === slow) {
      return true
    } else {
      slow = slow.next
      fast = fast.next.next
    }
  }
}

★剑指Offer 25.合并两个排序的链表

  1. 迭代
    比较一下哪个小就把哪个链表的头拿出来放到新的链表中(且对应的指针向后移j=j.next),一直这样循环,直到有一个链表为空,再把另一个不为空的链表挂到新的链表中
var mergeTwoLists = function(l1, l2) {
    if(!l1) return l2
    if(!l2) return l1
    // 定义头阶段
    let head = new ListNode()
    let cur = head
    // 指针再l1和l2直接穿梭
    while(l1 && l2){
        // 如果l1的节点值比较小
        if(l1.val <l2.val){
            // 先串起l1的节点
            cur.next = l1
            l1 = l1.next // l1指针先前走一步,即指向下一个l1
        }else{
            // l2比较小,串起l2节点
            cur.next = l2
            // l2向前走一步
            l2 = l2.next
        }
        cur = cur.next // 指针向后走
    }
    // 处理链表不登长情况
    cur.next = l1? l1:l2 // 判断还有没有l1,没有就l2
    return head.next // 返回起始节点
}

const l1=new NodeList([1,2,4])
const l2=new NodeList([1,3,4])
console.log(l1)
console.log(mergeTwoLists(l1.head, l2.head))

  1. 递归
var mergeTwoLists = function(l1, l2) {
    if (l1 === null) return l2;
    if (l2 === null) return l1;
    if (l1.val < l2.val) {
      l1.next = mergeTwoLists(l1.next, l2);
      return l1;
    } else {
      l2.next = mergeTwoLists(l1, l2.next);
      return l2;
    }
};

剑指Offer22.链表中倒数第k个节点

  1. 双指针求解:
    第一个指针走k步,第二个指针走1步,然后同时移动,当第一个指针到达链表末尾时,返回第二个指针即可
var getKthFromEnd = function(head, k) {
    var p = head, q=head
   while(p){
       if(k>0){  // 第一个指针先走k步
           p = p.next
           k--
       }else{  // 再同时移动,q返回的就是结果
            p = p.next
            q = q.next
       }
   }
    return q
}

const l1=new NodeList([1, 2, 3, 4, 5])
console.log(getKthFromEnd(l1.head, 2))
  1. 用栈解决
    先push到栈里,再迭代出栈
var getKthFromEnd = function(head, k) {
  let stack=[], res=[]
  while(head){
      stack.push(head)
      head = head.next
  }
  while(k>0){
      res = stack.pop()
      k--
  }
  return res 
}

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

快慢指针,快指针先走2步,快慢指针再一起走

const removeNthFromEnd = function(head, n) {
    // 初始化 dummy 结点
    const dummy = new ListNode()
    // dummy指向头结点
    dummy.next = head
    // 初始化快慢指针,均指向dummy
    let fast = dummy
    let slow = dummy

    // 快指针闷头走 n 步
    while(n!==0){
        fast = fast.next
        n--
    }
    
    // 快慢指针一起走
    while(fast.next){
        fast = fast.next
        slow = slow.next
    }
    
    // 慢指针删除自己的后继结点
    slow.next = slow.next.next
    // 返回头结点
    return dummy.next
}

复杂链表的复制

  1. 用哈希做映射
    新的链表节点用哈希存起来,再通过引用地址找到random指针指向的节点。空间复杂度是O(N)
var copyRandomList = function(head) {
  if(!head) return null;
  let cur = head,preHead = new Node(),temp = preHead,map = new Map();
  while(cur) {
      temp.val = cur.val;
      temp.next = cur.next ? new Node() : null;
      map.set(cur,temp);// 把temp的在值存起来
      temp = temp.next;
      cur = cur.next;
  }
  // 初始化,进行第二次遍历  
  temp = preHead;
  // 创建节点之间的关系   先创建链表,再从哈希中取值
  while(head) {
      // 通过引用地址找到对应的链表节点
      temp.random = head.random ? map.get(head.random): null;
      head = head.next;
      temp = temp.next;
  }
  return preHead;  
};
  1. 原地复用
    通过在原链表中创建新节点,优化了第一种方法的O(N)的空间复杂度
    1)创建新节点以及实现新节点和元链表节点的连接
    2)根据原链表的rangdom指向去生成新的节点的random的指向
    3)链表的分割
var copyRandomList = function(head) {
  if (head == null) {
    return head;
  }
  //将拷贝节点放到原节点后面,例如1->2->3这样的链表就变成了这样1->1'->2'->3->3'
  for (let node = head, copy = null; node != null; node = node.next.next) {
    copy = new Node(node.val);
    copy.next = node.next;
    node.next = copy;
  }
  //把拷贝节点的random指针安排上
  for (let node = head; node != null; node = node.next.next) {
    if (node.random != null) {
        node.next.random = node.random.next;
    }
  }
  //分离拷贝节点和原节点,变成1->2->3和1'->2'->3'两个链表,后者就是答案
  let newHead = head.next;
  for (let node = head, temp = null; node != null && node.next != null;) {
    temp = node.next;
    node.next = temp.next;
    node = temp;
  }
  return newHead;
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值