Leetcode复盘2——链表
导读
这是我写的第二篇复盘总结,总结了10道链表相关的题目,里面包含了所有链表的基本操作,包括:
- 如何定义一个指针(C++ ListNode *pA = head, Java ListNode pA = head)
- 如何反转链表;(设置一个临时指针temp,先记录cur的下一个,反转,pre和cur各往前走一步)
- 如何区分指针移动和指向:当为指向时“.next”在等号左边,即用当前指针指向等号右边的节点;当为移动时”.next”在等号右边,即把某一个指针指向的节点赋值给等号左边
1.相交链表 / 找出两个链表的交点(Leetcode160)
难度:简单Easy
idea: 双指针法(two pointers)
定义两个指针pA和pB,用pA指向链表A的开头,pB指向链表B的开头,链表A的长度为a+c,链表B的长度为b+c,其中c为重复的部分
e.g
链表A: 1->2->3->4->5->6->null
链表B: 9->5->6->null, 交点为5
还有可能公共的部分只有一个null
e.g
链表A: 1->2->3->4->5->6->null
链表B: 7->8->9->null,
把A看成主链表,B看成辅链表.当pB指针走完链表B回到链表A的开头时,相当于pA指针比pB指针多走了链表B的长度;同理,当pA在链表A走完后又在链表B走完时,相当于pA又让了pB共链表B的长度.最终两个指针一定是一起走到终点.此时 a + c + b = b + c + a
如果两个链表最后的部分是重合的话,则两个指针会在第一个重合的节点相遇,不会等到最后,因为此时已不满足while(pA!=pB)
若两个链表没有交点,则两个指针都会走到各自链表的结尾null,此时满足(pA = pB = null),此时 a + b = b + a
代码:
C++版
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode *pA = headA, *pB = headB;
// 若有交点,则走到第一个交点的时候跳出while循环; 若没有交点,则走到结尾null时跳出while循环
while (pA != pB) {
pA = pA ? pA->next : headB; // 若pA不存在,即pA走到头了,则从pB继续开始
pB = pB ? pB->next : headA; // 若pB存在,表示还没走到尾部,pB继续向前走
}
return pA;
}
};
Java版
class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if(headA == null && headB == null) return null;
ListNode pA = headA, pB = headB;
while(pA != pB) {
pA = pA == null? headB : pA.next;
pB = pB == null? headA : pB.next;
}
return pA;
}
}
2.反转链表(Leetcode206)
难度:简单Easy
idea: 双指针迭代法(iterative method)
定义两个指针pre和cur,
e.g
1 -> 2 -> 3 -> 4 -> 5 -> null
pre cur temp
分三步:
- 用temp记录cur的下一个节点;
- 用cur指向pre(关键);
- pre和cur各往前一步,顺序不能反了,先把cur给pre,再把temp给cur
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode *pre = NULL, *cur = head; // 定义两个指针,pre和cur
while(cur) {
ListNode* temp = cur -> next; // 1.记录cur的下一个节点
cur -> next = pre; // 2.用cur指向pre(关键)
// 3.pre和cur各往前一步(顺序不能乱,先把cur给pre,再把temp给cur)
pre = cur;
cur = temp;
}
return pre;
}
};
3.合并两个有序链表 / 归并两个有序的链表(LeetCode21)
难度:简单Easy
idea: 双指针法
定义两个指针:dummy(用于返回,即返回dummy.next
)和cur(用于移动)
分为两种情况:
- 当l1和l2都存在时:
- 若l1较小
cur指向l1: cur.next = l1;
l1往前一步: l1 = l1.next;
cur往前一步: cur = cur.next;
(下一次用新的l1和l2比较) - 若l2较小
cur指向l2: cur.next = l2;
l2往前一步: l2 = l2.next;
cur往前一步: cur = cur.next;
- 若l1较小
- 当l1或l2有一个不存在时:
用C++或Java里面的三元运算符判断哪个链表为null,用cur指向不为null的,
或者用Python里的"…=… if… else…"。
代码:
C++版
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode dummy(INT_MIN); // 定义dummy为负无穷类型的数?
ListNode *cur = &dummy; // cur取dummy的地址即可
while(l1 && l2) {
if(l1 -> val < l2 -> val) {
cur -> next = l1;
l1 = l1 -> next; // 下次用新的l1指向的节点值和l2比
}
else {
cur -> next = l2; // 注意区分"指向"和"移动"
l2 = l2 -> next;
}
cur = cur -> next;
}
// 当不满足while循环时,即有一个链表为空了
cur -> next = l1? l1 : l2; // l1? 即 "l1 != null"
return dummy.next; // dummy是一个数
}
};
Python版
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
# 在两个头节点之外再定义一个0节点
dummy = cur = ListNode(0)
while l1 and l2:
if l1.val < l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
# 跳出while循环了,证明l1或l2有一个为空了
cur.next = l1 if l1 else l2 # 或cur.next = l1 or l2
return dummy.next
4.删除排序链表中的重复元素 / 从有序链表中删除重复节点(LeetCode83)
难度:简单Easy
本题考察点:指针的指向和移动
定义一个指针cur用于移动,while循环保证cur指向的元素是存在的,即cur.next存在(不然拿cur当前的值跟谁比呢),分两种情况:
- 当cur当前指向的值和下一个相等时(cur == cur.next),cur跳过下一个,再往前指一个(cur.next = cur.next.next)
- 当cur当前指向的值和下一个不相等时,cur往前移一步(cur = cur.next)
代码:
C++版
class Solution {
public:
ListNode* deleteDuplicates(ListNode* head) {
if(head == NULL || head -> next == NULL) return head;
ListNode *cur = head;
while(cur -> next != NULL) { // 此处一定是看cur指向的是否为空,因为cur可能一直不动
if(cur -> val == cur -> next -> val) { // 若cur当前的值和指向的值一样,cur就往前指,再循环
cur -> next = cur -> next -> next;
}
else { // 若不一样,则cur往前走一步
cur = cur -> next;
}
}
return head;
}
};
Java版:
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode cur = head;
while(cur.next != null) { // 要保证cur指向的节点一直存在,否则拿cur跟谁比
if(cur.val == cur.next.val) { // 当cur当前的值和指向的值相等时,cur跳过下一个指向下下个
cur.next = cur.next.next;
} else { // 当cur当前的值和指向的值不相等时,cur往前走一步
cur = cur.next;
}
}
return head;
}
}
5.删除链表的倒数第 N 个结点(LeetCode19)
idea: 双指针法(two pointers)
定义快慢指针fast和slow,以及防守指针dummy
分三步:
1.用for循环fast先走n步;
2.然后fast和slow一起走(期间fast和slow始终差n);
3.fast走到头,此时slow指的就是要删除的(即从尾数倒数第n个),跳过它
代码
C++
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode *dummy = new ListNode(0);
dummy -> next = head;
ListNode *slow = dummy, *fast = dummy;
// 1.fast先走n步
for(int i = 0 ; i < n + 1 ; i ++) {
fast = fast -> next;
}
// 2.fast和slow一起走到头,期间fast和slow的差距始终为n,fast走到头后,slow指向的即为要删除的(从尾数第n个)
while(fast) {
slow = slow -> next;
fast = fast -> next;
}
// 3.此时slow指向的就是要删除的节点,删除它
ListNode *delNode = slow -> next;
slow -> next = delNode -> next;
delete delNode;
ListNode* retNode = dummy->next; // 有些情况head被删除了
delete dummy;
return retNode;
}
};
6.两两交换链表中的节点 / 交换链表中的相邻结点(LeetCode24)
idea: 双指针法(two pointers)
e.g
1 -> 2 -> 3 -> 4
目的是把2指向1,4指向3,定义两个指针start和end,start指向1或3,end指向3或4,还需要一个临时指针temp,用来指向下一个start的前一个,即当前end指向的节点
假如要交换1和2,分三步:先定住1和2的首位,最后一步再交换,(开始交换前temp指向1,1赋值给start,2赋值给end)
1.定住首: temp指向2(temp.next = end);
2.定住尾: 1指向3(start.next = end.next)
3.交换(end.next = start)
因为temp指向下一个start的前一个,即当前的start位置(temp = start)
代码:
Java
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0); // 设一个最后返回的哑指针dummy
dummy.next = head; // dummy指向head,用于最后返回
ListNode temp = dummy;
// 确保temp有下一个和下下个
while(temp.next != null && temp.next.next != null) {
// 每次循环开始时都把start和end定义好,即要互相交换的两个节点
ListNode start = temp.next;
ListNode end = temp.next.next;
// 三步指开始:
temp.next = end; // 1.定住首
start.next = end.next; // 2.定住尾
end.next = start; // 3.交换
// temp始终指向下一个start的前一个,即当前start处
temp = start;
}
return dummy.next;
}
}
7.两数相加 II / 链表求和(LeetCode445)
idea: 栈(stack)
考虑到个位在链表的尾部,加法要从个位开始加起,故考虑栈的方法,先入后出
申请两个栈s1和s2,把链表的值加到栈里,然后开始pop,分为以下几步:
1.求计算当前位的和sum(有可能包括上一次的进位1);
2.生成当前位节点tmp,其值为sum模10即可;
3.设置当前位的指向,即和头节点dummy一样;
4.设置新的dummy指针的指向;
5.处理进位,用于下一轮迭代;
代码:
C++
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
if(!l1 || !l2) return l1 == NULL? l2 : l1;
stack<int> s1, s2; // Java的是Stack<Integer> = new Stack<Integer>();
while(l1 != NULL) {
s1.push(l1 -> val);
l1 = l1 -> next; // l1往前走一步;
}
while(l2 != NULL) {
s2.push(l2 -> val);
l2 = l2 -> next;
}
int carry = 0, n1 = 0, n2 = 0, sum = 0;
ListNode* dummy = new ListNode(-1); // 创建头节点
ListNode* tmp = NULL;
while(!s1.empty() || !s2.empty() || carry) {
// 求计算当前位的和sum
if(s1.empty()) n1 = 0;
else {n1 = s1.top(); s1.pop();}
if(s2.empty()) n2 = 0;
else {n2 = s2.top(); s2.pop();}
sum = (n1 + n2 + carry);
// 2.生成当前位节点tmp
tmp = new ListNode(sum % 10);
// 3.设置当前位的指向,即和头节点dummy一样
tmp -> next = dummy -> next;
// 4.设置新的dummy指针的指向
dummy->next = tmp;
// 5.处理进位,给下一次迭代用
carry = sum / 10;
}
// 释放dummy指针防止内存泄露
ListNode* res = dummy->next; // 此时dummy指向最终生成的头节点
delete dummy;
return res;
}
};
Java
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
Stack<Integer> s1 = new Stack<Integer>();
Stack<Integer> s2 = new Stack<Integer>();
while (l1 != null) { // 把链表1的每个值存入栈中,最高位的在最下面,个位在栈顶
s1.push(l1.val);
l1 = l1.next;
}
while (l2 != null) {
s2.push(l2.val);
l2 = l2.next;
}
int sum = 0;
ListNode d = new ListNode(0);
while (!s1.empty() || !s2.empty()) {
// 从个位开始加起,1.求计算当前位的和sum
if (!s1.empty()) sum += s1.pop();
if (!s2.empty()) sum += s2.pop();
d.val = sum % 10; // 2.求当前位
ListNode head = new ListNode(sum / 10); // 3.生成下一位节点head,值是0或1只是暂时的,需要下一轮更正,若没有下一轮了,生成的0或1就是最终的了.(关键)
head.next = d; // 4.设置好指向
d = head; // 5.d后退一步,准备下一轮
sum /= 10; // 6.求进位
}
return d.val == 0 ? d.next : d; // 最后没有下一轮了,head生成的0或1就是最终的(此时d也指head),当d为0时,返回0的下一个;当d为1时,从d开始返回
}
}
8. / 回文链表(LeetCode234)
idea: 快慢指针法(fast and slow pointers)
定义快慢指针fast和slow,一共分3步:
1.快指针每次走两步,慢指针每次走一步;
2.反转后半部分,即slow指向的部分(a.记录cur的下一个;b.反转;c.cur和pre各往前走一步,注意先后顺序)
3.头指针(指向前半部分))和slow指针(指向后半部分)挨个比数;
e.g
1 -> 2 -> 3 -> 3 -> 2 -> 1
代码:
Java
class Solution {
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
// 定义为ListNode fast = head.next也行,区别在于是第一个3指null,而ListNode fast = head是第二个3指null
ListNode fast = head;
ListNode slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
slow = reverse(slow);
while (head != null && slow != null) {
if (head.val != slow.val) {
return false;
}
head = head.next;
slow = slow.next;
}
return true;
}
// 定义反转函数
public ListNode reverse(ListNode cur) {
ListNode pre = null;
while (cur != null) {
ListNode tmp = cur.next; // a.先记录
cur.next = pre; // b.再反转
pre = cur; // c.最后pre和cur各往前一步(注意先后顺序)
cur = tmp;
}
return pre; // cur=null了,不满足while循环,返回pre即可
}
}
9.分隔链表(LeetCode725)
idea:
1.计算链表的长度;
2.由题意一共要分成k个块, 计算每个块的基本大小,以及较长块的个数,得到要返回的res信息,即每个块的个数;
3.切分;
e.g
1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11, k = 3
对于最后的切分过程: res = [4,4,3]
index = 0, num = 4; index = 1, num = 4; index = 2, num = 3
第一轮
prev = null, curr = 1, 不执行if prev;
res[0] = 1, prev和curr一起往前走4个位置: prev = 4, curr = 5
第二轮
prev = 4, curr = 5, 执行if prev,切断;
res[1] = 5, prev和curr一起往前走4个位置: prev = 8, curr = 9
第三轮
prev = 8, curr = 9, 执行if prev,切断;
res[2] = 9, prev和curr一起往前走3个位置,结束
代码
Java
class Solution {
public ListNode[] splitListToParts(ListNode root, int k) {
// 1.计算链表的长度
int length = 0;
ListNode cur = root;
while(cur != null) {
length++;
cur = cur.next;
}
int longer_chunks = length % k;
int chunk_size = length / k;
ListNode[] res = new ListNode[k];
cur = root;
for (int i = 0; cur != null && i < k; i++) {
res[i] = cur;
// 一共有longer_chunk较长的,比正常的多1.curSize要么为4,要么为3
int curSize = chunk_size + (longer_chunks-- > 0 ? 1 : 0);
for (int j = 0; j < curSize - 1; j++) {
cur = cur.next;
}
// 3.切割
ListNode tmp = cur.next; // a.先记录cur的下一个
cur.next = null; // b.断开
cur = tmp; // c.cur再往前进一位
}
return res;
}
}
Python
class Solution:
def splitListToParts(self, root: ListNode, k: int) -> List[ListNode]:
# 1.计算链表的长度
curr, length = root, 0
while curr:
curr, length = curr.next, length + 1 # length = 11
# 计算每个块的大小,以及较长快的个数
chunk_size, longer_chunks = length // k, length % k
# chunk_size=3(每个块的基本大小),longer_chunks=2(较长块即长度为4的个数为2个,剩下的1个为基本大小即3)
res = [chunk_size + 1] * longer_chunks + [chunk_size] * (k - longer_chunks)
# res = [3+1]*2+[3]*(3-2) = [4,4,3]
# 切分链表
prev, curr = None, root
for index, num in enumerate(res): # index = 0,num = 4; index = 1,num=4, index=2, num=3
if prev: # 若当前prev指的不是空(即排除刚开始的情况),让其指向空
prev.next = None
# 对于每组index和num
res[index] = curr # 先在res中确定每个链表的头
for i in range(num): # cur和prev一起往前走,再由上面的if prev确定尾
prev, curr = curr, curr.next
return res
10.链表元素按奇偶聚集(LeetCode328)
idea: 双指针法(two pointers)
定义两个指针odd和even, odd指向奇数下标,even指向偶数下标; dummy指针指向偶数下标的开头且不再移动
分为三步:
1.odd和even各指向其下一个节点:odd.next = odd.next.next, even.next = even.next.next, 注意这里的顺序不能反了,必须奇数下标先指,偶数下标后指
2.odd和even各往前走一步:odd = odd.next, even = even.next(这里二者的顺序可以反过来不影响)
3.最后把odd的尾接到even的头(刚开始用dummy记录even的头节点)
代码
Java
class Solution {
public ListNode oddEvenList(ListNode head) {
if(head == null) return null;
ListNode odd = head, even = head.next, dummy = head.next; // 可写到一行,用dummy记录even头节点
while(odd.next != null && even.next != null) {
// 1.先指,注意下面两行顺序不能颠倒了,必须是先奇后偶
odd.next = odd.next.next;
even.next = even.next.next;
// 2.再各往前走一步
odd = odd.next;
even = even.next;
}
// 3.用odd的尾指向even的头
odd.next = dummy;
return head;
}
}