搞定算法-链表专题
链表
了解链表的同学都知道,它是通过指针将一组零散的内存串联起来。可见链表对内容的要求降低了,但是随机访问的性能就是没有那么好了,需要O(n)的时间复杂度。
如何操作链表
对与链表的操作主要是添加和删除,其实对链表的操作本质是也是对链表之间的指针进行操作。其数据结构大致为:
var node = {
val : 1 ,
next : { // next 可理解为指针指向下一个节点
xxx //表示下一个节点
}
}
对链表的操作也就是更改其指针的指向,就是将next
的值改为对应的节点。
leedcode真题
话不多说,我们一起来做几道链表道题,也是面试中常考的链表题。
合并两个有序链表(leedcode21)
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路一(递归)
- 将两个链表头部较小的一个与剩下的元素合并
- 当两条链表中的一条为空时终止递归
复杂度分析
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)
思路一(递归)
- 当前节点的
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)
思路一(双指针法)
- 使用快慢不同的指针遍历,快指针一次走两步,满指针一次走一步
- 如果没有环,快指针会先到达尾部,返回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)
思路一(暴力解决法)
- 求倒数第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)
思路(快慢指针法)
- 使用快慢指针,快指针一次走两步,慢指针走一步。
- 当指针到达终点时,慢指针刚好走到中间
复杂度分析
- 时间复杂度: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)
利用链表的中间结点与反转链表
思路(利用链表的中间结点与反转链表)
- 求链表的中间结点,反转头结点为中间结点的链表
- 遍历两个链表结点是否相同
复杂度分析
- 时间复杂度: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)
思路(迭代)
- 定义一个哨兵结点dummyHead ,初始化到达的结点temp,每次循环定义前一个结点node1,后一个结点node2指针
- 将node2指向node1,node1指向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
};
总结
请大家关注后续会将持续更新