搞定算法-巩固链表基础

1,140 阅读6分钟

搞定算法-链表专题

链表

   了解链表的同学都知道,它是通过指针将一组零散的内存串联起来。可见链表对内容的要求降低了,但是随机访问的性能就是没有那么好了,需要O(n)的时间复杂度。

如何操作链表

   对与链表的操作主要是添加和删除,其实对链表的操作本质是也是对链表之间的指针进行操作。其数据结构大致为:

var node = {
	val : 1 ,
    	next : {   // next 可理解为指针指向下一个节点
	  xxx  //表示下一个节点
    	}
  }

对链表的操作也就是更改其指针的指向,就是将next的值改为对应的节点。

leedcode真题

话不多说,我们一起来做几道链表道题,也是面试中常考的链表题。

合并两个有序链表(leedcode21)

leetcode-cn.com/problems/me…

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

思路一(递归)

  • 将两个链表头部较小的一个与剩下的元素合并
  • 当两条链表中的一条为空时终止递归

复杂度分析

N + M 为两条链表的长度

  • 时间复杂度:(M+N)
  • 空间复杂度:(M+N)
 const mergeTwoLists  = function  (l1, l2) {
    if(!l1) {
        return l2
    }
    if(!l2) {
        return l1
    }
    if(l1.val < l2.val) {
        l1.next = mergeTwoLists(l1.next,l2)
        return l1
    }else {
        l2.next = mergeTwoLists(l2.next,l1)
        return l2
    }
 }

思路二(迭代)

  • 定义一个’亚节点‘ prev
  • 将两个链表头部较小的一个添加到’亚节点‘的下个节点
  • 将对应链表中的节点向后移一位。

复杂度分析

N + M 为两条链表的长度

  • 时间复杂度:(M+N)
  • 空间复杂度:1 (我们只需要常数的空间存放若干变量。)
var mergeTwoLists = function(l1, l2) {
   var preNode = new ListNode(-1),
   prev = preNode
   while(l1&&l2){
       if(l1.val <l2.val) {
           prev.next = l1
           l1 = l1.next
       }else {
           prev.next = l2
           l2 = l2.next
       }
       prev = prev.next
   }
    l1 === null ? prev.next = l2 :prev.next = l1
    return preNode.next
};

反转链表(leedcode206)

leetcode-cn.com/problems/re…

思路一(递归)

  • 当前节点的 next为空时递归结束,找到最后一个节点
  • 让最后一个节点指向上一个节点
  • 上一个节点指向空 (如果忽略了这一点,链表中可能会产生环)

复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度。需要对链表的每个节点进行反转操作。

  • 空间复杂度:O(n),其中 n 是链表的长度。空间复杂度主要取决于递归调用的栈空间,最多为 n 层。

var reverseList = function(head) {
    if(head === null || head.next === null) return head
   	var node = reverseList(head.next)
    head.next.next = head
    head.next = null
    return node
};

思路二(迭代)

  • 初始化前驱节点为null,初始化目标节点为头节点
  • 遍历链表,记录next节点
  • prev和 curr指针分别前移一步
  • 反转结束后,prev成为新的头节点

复杂度分析

  • 时间复杂度:O(n),其中 n 是链表的长度。需要对链表的每个节点进行反转操作。

  • 空间复杂度:O(1)

var reverseList = function(head) {
    let prev = null
    let curr = head
    while(curr !=null) {
        var next = curr.next
        curr.next = prev 
        prev = curr
        curr = next 
    }
    return prev
};

环形链表(leedcode141)

leetcode-cn.com/problems/li…

思路一(双指针法)

  • 使用快慢不同的指针遍历,快指针一次走两步,满指针一次走一步
  • 如果没有环,快指针会先到达尾部,返回false

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
var hasCycle = function(head) {
 if(!head || !head.next) return fasle
 let fast = head.next
 let slow =  head
 while(fast != slow){
 	if(!fast || !fast.next) return false
	fast = fast.next.next
 	slow = slow.next
 }
 return ture
};

思路二(哈希法)

  • 遍历节点,判断哈希表是否存在当前节点
  • 如果没有当前节点加入哈希,如果有则判断有环

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
var hasCycle = function(head) {
 var map = new Map()
 while(head) {
     if(map.has(head)) {
         return true
     }else{
         map.set(head,true)
         head = head.next
     }
 }
 return false
};

删除结点的倒数第N个结点(leedcode19)

leetcode-cn.com/problems/re…

思路一(暴力解决法)

  • 求倒数第N个结点也就是求前链表总长度 - 数第N个结点 + 1 个结点
  • 找到倒数第n-1个结点也就是链表总长度 - 数第N个结点 个结点,将指针指向下一个个结点

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
var removeNthFromEnd = function(head, n) {
    var l = getLinkLength(head) // 获取链表总长度
    var dummy = new ListNode(0, head);
    var cur = dummy;
    for (var i = 1; i < l - n + 1; i++) {
            cur = cur.next;
    }
        cur.next = cur.next.next;
        return dummy.next;
};
 function getLinkLength(head) {
     var index = 0
     while(head) {
         index++
         head = head.next
     }
     return index
 }

思路二(双指针法)

  • 删除倒数第n个结点,我们需要找到倒数第n-1个结点,将指针指向第n+1个结点
  • 添加pre,可以称为哨兵节点,处理边界问题
  • 使用双指针法,快指针first先走n步,然后快慢指针同步往前走,直到 first,second 为null
  • 返回prev.next

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
var removeNthFromEnd = function(head, n) {
    var prev = new ListNode(0,head)
    var first = head
    var second = prev
    for (var i = 0;i<n;i++) {
        first =  first.next
    }
    while(first) {
        first = first.next
        second = second.next
    }
    second.next = second.next.next
    return prev.next
};

求链表的中间结点(leedcode876)

leetcode-cn.com/problems/mi…

思路(快慢指针法)

  • 使用快慢指针,快指针一次走两步,慢指针走一步。
  • 当指针到达终点时,慢指针刚好走到中间

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
var middleNode = function(head) {
    let fast = head
    let slow = head
    while(fast && fast.next) {
        fast = fast.next.next
        slow = slow.next
    }
    return slow
};

回文链表(进阶)(leedcode234)

利用链表的中间结点与反转链表

leetcode-cn.com/problems/pa…

思路(利用链表的中间结点与反转链表)

  • 求链表的中间结点,反转头结点为中间结点的链表
  • 遍历两个链表结点是否相同

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
var isPalindrome = function(head) {
    let fast = head;
    /**中位数 */
    let mid = head;
    while (fast && fast.next) {
        mid = mid.next;
        fast = fast.next.next;
    }

    let newH = reverseList(mid);
    while (newH && head) {
        if (newH.val != head.val) {
            return false;
        }
        newH = newH.next;
        head = head.next;
    }
    return true;
};

/**反转链表 */
var reverseList = function(head) {
    if (!head || !head.next) return head;
    let end = head;
    while (end.next) {
        let newH = end.next;
        end.next = end.next.next;
        newH.next = head;
        head = newH;
    }
    return head;
};

两两交换链表中的结点(leedcode24)

leetcode-cn.com/problems/sw…

思路(迭代)

  • 定义一个哨兵结点dummyHead ,初始化到达的结点temp,每次循环定义前一个结点node1,后一个结点node2指针
  • node2指向node1node1指向node2的下一个结点
  • 更新到达的结点 temp = node1;

复杂度

时间复杂度:O(n),其中 n 是链表的节点数量。需要对每个节点进行更新指针的操作。

空间复杂度:O(1)。

const dummyHead = new ListNode(0);
    dummyHead.next = head;
    let temp = dummyHead;
    while (temp.next !== null && temp.next.next !== null) {
        const node1 = temp.next;
        const node2 = temp.next.next;
        temp.next = node2;
        node1.next = node2.next;
        node2.next = node1;
        temp = node1;
    }
    return dummyHead.next;

92. 反转链表 II(leedcode92)

与第206题不同的是,给定左右节点,请反转两个节点中间的节点

思路【双指针】

var reverseBetween = function(head, left, right) {
    let newHead = new ListNode(-1)
    newHead.next = head
    let cur = newHead,before,after  // before 保存left之前的节点,after 为之后的节点
    for(let i = 1;i< left;i++) {
        cur = cur.next
    }
    before = cur
    cur = cur.next
    before.next =  null
    var prev = null
    for(let j = left ;j<=right;j++) {
        if(cur == null)    break;
        if(j == right) {after = cur.next}  
        var temp = cur.next  // 正常反转
        cur.next = prev
        prev = cur
        cur = temp
    }
        before.next = prev
    
    while(prev.next) {
        prev = prev.next  
    }
    
    prev.next = after
    return newHead.next
};

leetcode-cn.com/problems/re…

总结

请大家关注后续会将持续更新