学习记录之算法篇——链表

229 阅读5分钟

本人是个算法渣渣,该内容为个人在练习算法中的刷题记录。记录了个人对于题解的一些理解总结。如果错误欢迎指正!

1.反转链表

题目来源:LeetCode 206. 反转链表

描述

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 反转链表

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

进阶: 链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题?

迭代解法

  • 解题思路:

    • 先定义两个指针precur 分别表示前置节点和当前节点,要进行链表反转的话,只要实现所有节点的的 next 指向其前置节点即可
    • 设置pre初始值为nullcur初始值为head,因为反转后,head节点应成为链表末端节点,其next值为null。因此这样设置两个指针
    • 遍历所有节点,将所有cur节点的next指向pre,之后让curpre顺着链表原来的方向往下遍历,直到所有节点都被遍历结束
    • curnull时,意味着已经遍历到链表末端,此时的pre成为了新链表的其实端,此时结束循环,返回 pre 即可
  • 注意

    • 在遍历过程中,因为会更改cur.next的指向,因此要先用一个临时指针(tmp)保存下一个节点的位置,使得原链表的遍历能够正常执行下去
  • 代码:

function reverseList (head) {
  let pre = null
  let cur = head
  while (cur) {
    let tmp = cur.next
    cur.next = pre
    pre = cur
    cur = tmp
  }
  return pre
}

递归解法

参考:简单易懂 Java/C++ /Python/js 动画讲解 - 反转链表 讲解的十分细致

  • 解题思路:

    • 根据递归的思想,可以将链表反转问题拆解成:头节点与除头节点以外的子链表进行反转;而子链表的反转一样的可以像上述解法一样,也进行拆分。如:
    1 -> 2 -> 3 -> 4 -> 5 -> null
    其反转可以拆分为:
    
    | head |          List            |
    |  1   | 2 -> 3 -> 4 -> 5 -> null |
    
    而对于其子链表List,又可以同样的进行拆分
           | head |          List        |
           |  2   |  3 -> 4 -> 5 -> null |
    
           如此往复下去即可...
    
    • 就有,根据上述规则一直拆分问题,就是,等拆分到只剩一个节点时,就无需继续拆分,开始,即将拆分到无法拆分的节点返回。(这个节点也就是我们要的新链表的头节点)
    • 的同时,我们也要进行链表反转的操作,也就是解决上述的头节点和子链表的反转,要进行这样的反转,我们只要进行head.next.next = head;head.next=null即可
  • 注意:

    • 递归的中止条件为链表只剩一个节点时结束递归,因此判断条件为head.next === null。但是如果在执行时,传入一个空链表,函数也应立即结束,返回head。所以递归中止条件为head===null||head.next===null
    • 递归中止返回的节点为新链表的头节点,我们应当保存下来,用于结果返回
  • 代码:

if (head === null || head.next === null) {
  return head
}

const p = reverseList(head.next)

head.next.next = head
head.next = null

return p

2.删除链表的节点

题目来源:剑指 Offer 18. 删除链表的节点

描述

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。 返回删除后的链表的头节点。

输入:list=[4,5,1,9],val=5
输出:[4,1,9]

解法

  • 解题思路:
    • 本题比较简单,只要去遍历链表,然后去找到符合条件的节点,再进行删除操作即可。即将要删除节点的前一个节点的 next指向要删除节点的 next
    • 本题可以使用双指针逻辑比较清晰,这里我直接建立一个空节点指向链表头部,判断时使用下一节点的值进行比较,直接使用指针定位到要删除节点的前驱节点 p ,然后将其 next 设置为 p.next.next
  • 注意

代码:

var deleteNode = function (head, val) {
  let dummyHead = new ListNode(undefined, head)
  for (let p = dummyHead; p.next; p = p.next) {
    if (p.next.val == val) {
      p.next = p.next.next
      break
    }
  }
  return dummyHead.next
}

3.移除链表元素

题目来源:LeetCode 203. 移除链表元素

描述

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。 例题

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

迭代解法

  • 解题思路:

    • 迭代思路比较简单,本题与删除链表节点比较类似,遍历节点,如果遇到符合条件的节点,就直接删除。
    • 迭代方法这里依旧使用空节点指向头节点,然后使用while循环遍历链表,同样的直接使用p先指向我们创建的空节点,然后判断p.next.val===val;如果相等则令p.next=p.next.next
    • 最后返回我们创建节点的 next 即可
  • 代码:

var removeElements = function (head, val) {
  let dummyHead = new ListNode(undefined, head)
  let p = dummyHead
  while (p.next) {
    if (p.next.val === val) {
      p.next = p.next.next
    } else {
      p = p.next
    }
  }
  return dummyHead.next
}

递归解法

  • 解题思路:
    • 链表被定义的时候就具有递归的性质,因此这题也能够使用递归来解决
    • 对于链表,我们只要逐个的判断 head 的值是否等于传入的 val,如果是就删除,不是就不删除,而除了 head 意外的以 head.next 为首的子链也是以此的拿出首节点,判断是否要删除,要删除则删除,不要则返回
    • 对于递归中,我们直接判断 head.val===val 如果为真,则代表要删除,如果为假,则可以直接返回该节点
    • 意味着从末尾到前面,因此当节点为null时,就不需要接着判断下去,就可以开始了,而返回的东西,应当作为上一次的head的 next,但是我们在的时候,需要判断,这个值是否是要归的值,其实就是上一步里说的判断head.val === val如果条件成立,则意味着这个节点要舍弃,那么这个节点就不是我们要归的内容。而应该是舍弃节点的下一个节点,即head.next
  • 代码:
var removeElements = function (head, val) {
  if (head == null) {
    return head
  }
  head.next = removeElements(head.next, val)
  return head.val === val ? head.next : head
}