关于作者:
大家好,我是Leetcode 2020--2022,连续3年金牌获得者,和亚洲区域赛铜牌获得者,
先后在字节和大疆从事技术研发,现在是阿里达摩院的扫地僧,面试专家,CSDN博客专家。
对算法一定的见解,是一个刷题10年的算法爱好者 ,利用工作之余刷leetcode。成为leetcode官方答案贡献者之一。
链表题目重要顺序分类:
2 两数相加(ok)
21. 合并两个有序链表(ok) 链表 【类似于题2】
234. 回文链表
24 : 链表交换
206. 反转链表
92: 反转链表 II (特别重要,比较难)
剑指offer:
24:反转链表
25:合并链表
52:2个链表第一个公共节点
第2等级:
23: 合并K个升序链表
141: 环形链表
142: 环形链表2
148: 排序链表
160: 相交链表
143: 重排链表
19: 删除链表的倒数第 N 个结点
25: K 个一组翻转链表
总结:递归,快慢指针比较
链表的6大题型:删除,合并,反转,环形,排序, 翻转, 想加
链表题目分类:第一类: 删除类-----------快慢指针
19: 删除链表的倒数第 N 个结点 链表
public ListNoderemoveNthFromEnd(ListNode head, int n) {
// n并不知道,只有遍历完成才能得到
ListNode dummy =new ListNode(0, head); // 生成一个0开始的dummy节点
ListNode first = head;
ListNode second = dummy;
for (int i =0; i < n; ++i) {// 先把快指针移动n。移动到了3的位置
first = first.next;
}
while (first !=null) {// 一直遍历到快指针结束
first = first.next; // 为什么要移动这个????因为这个才会移动链表,或者是遍历链表
second = second.next;
}
second.next = second.next.next;// 下一个节点直接指向下下个
ListNode ans = dummy.next;// 因为要返回头节点
return ans;
}
删除有序/无序链表中重复的元素(并不重要)
leetcode 83
第二类: 合并链表(21,23)------递归
例题:21 Merge Two Sorted Lists 【easy】
题意:将两个排序好的链表合并成新的有序链表
public ListNodemergeTwoListsDigui(ListNode list1, ListNode list2) {
if (list1 ==null) {
return list2;
}
if (list2 ==null)return list1;
if (list1.val > list2.val) {// l2小
list2.next = mergeTwoListsDigui(list2.next, list1); // 下一个元素是:自己的下一个和大的元素比较
return list2;
}else {// l1 小
list1.next = mergeTwoListsDigui(list1.next, list2); // 下一个元素是:自己的下一个和大的元素比较
return list1;
}
}
第三类: 翻转类题型-------递归
最基础最常在面试中遇到的提醒,206一定要熟练掌握
/***
* 递归的方式反转链表
* 1.递归依次返回的数据
* 2。下下个指向自己
* @param head
* @return
*/
public ListNodereverseListDigui(ListNode head) {
if (head ==null || head.next ==null) {
return head;
}
ListNode res = reverseListDigui(head.next); // res=5 head=4
head.next.next = head; // 4->5->null变成4->5->4
head.next =null;// 4->null
return res;
}
翻转部分单链表---92(用到206题)
public ListNodereverseBetween(ListNode head, int left, int right) {
ListNode dummyNode =new ListNode(-1);
dummyNode.next = head;
ListNode pre = dummyNode;
for (int i =0; i < left -1; i++) {
pre = pre.next;
}
ListNode rightNode = pre;
for (int i =0; i < right - left +1; i++) {
rightNode = rightNode.next;
}
ListNode leftNode = pre.next;
ListNode curr = rightNode.next;
pre.next =null;
rightNode.next =null;
reverseLinkedList(leftNode);
pre.next = rightNode;
leftNode.next = curr;
return dummyNode.next;
}
/***
* 206题,递归的方式反转链表
* @param head
*/
public void reverseLinkedList(ListNode head) {
// 也可以使用递归反转一个链表
ListNode pre =null;
ListNode cur = head;
while (cur !=null) {
ListNode next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
}
25:
public ListNodereverseKGroup(ListNode head, int k) {
ListNode dummy =new ListNode(0);
dummy.next = head;
ListNode pre = dummy;
ListNode end = dummy;
while (end.next !=null) {
for (int i =0; i < k && end !=null; i++) end = end.next;
if (end ==null)break;
ListNode start = pre.next;
ListNode next = end.next;
end.next =null;
pre.next = reverse(start);
start.next = next;
pre = start;
end = pre;
}
return dummy.next;
}
/**
* 206
* @param head
* @return
*/
private ListNodereverse(ListNode head) {
ListNode pre =null;
ListNode curr = head;
while (curr !=null) {
ListNode next = curr.next;
curr.next = pre;
pre = curr;
curr = next;
}
return pre;
}
第四类: 环形链表------快慢指针
例题:141 Linked List Cycle 【easy】
例题:142 Linked List Cycle 【easy】
public ListNodedetectCycle(ListNode head) {
if (head ==null || head.next ==null) {
return null;
}
ListNode fast = head, slow = head;
// step1: 快慢指针一起走,指针碰头快回头
while (fast !=null && fast.next !=null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
break;
}
}
if (fast ==null || fast.next ==null) {
return null;
}
fast = head;
// step2:一步一步走,再相遇就是goal
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
return fast;
}
相交链表: 相交链表(160)
public ListNodegetIntersectionNode(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; // 写法,pA = pA == null
pB = pB ==null ? headA : pB.next;
}
return pA;
}
第六类:排序链表
143
public void reorderList(ListNode head) {
if (head ==null) {
return;
}
ListNode mid = middleNode(head);
ListNode l1 = head;
ListNode l2 = mid.next;
mid.next =null;
l2 = reverseList(l2);
mergeList(l1, l2);
}
public ListNodemiddleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast.next !=null && fast.next.next !=null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
public ListNodereverseList(ListNode head) {
ListNode prev =null;
ListNode curr = head;
while (curr !=null) {
ListNode nextTemp = curr.next;
curr.next = prev;
prev = curr;
curr = nextTemp;
}
return prev;
}
public void mergeList(ListNode l1, ListNode l2) {
ListNode l1_tmp;
ListNode l2_tmp;
while (l1 !=null && l2 !=null) {
l1_tmp = l1.next;
l2_tmp = l2.next;
l1.next = l2;
l1 = l1_tmp;
l2.next = l1;
l2 = l2_tmp;
}
}
148:(用到21题)
public ListNodesortList(ListNode head) {
return sortList(head, null);
}
private ListNodesortList(ListNode head, ListNode tail) {
if (head ==null) {
return head;
}
if (head.next == tail) {
head.next =null;
return head;
}
ListNode slow = head, fast = head;
while (fast != tail) {
slow = slow.next;
fast = fast.next;
if (fast != tail) {
fast = fast.next;
}
}
ListNode mid = slow;
ListNode listNode1 = sortList(head, mid);
ListNode listNode2 = sortList(mid, tail);
ListNode sorted = merge(listNode1, listNode2);
return sorted;
}
private ListNodemerge(ListNode list1, ListNode list2) {
if (list1 ==null) {
return list2;
}
if (list2 ==null)return list1;
if (list1.val > list2.val) {// l2小
list2.next = merge(list2.next, list1); // 下一个元素是:自己的下一个和大的元素比较
return list2;
}else {// l1 小
list1.next = merge(list1.next, list2); // 下一个元素是:自己的下一个和大的元素比较
return list1;
}
}
回文链表: 234
public boolean isPalindrome(ListNode head) {
if (head ==null) {
return true;
}
ListNode firstHalfEnd = endOfFirstHalf(head);
ListNode secondHalfStart = reverseList(firstHalfEnd.next);
ListNode p1 = head;
ListNode p2 = secondHalfStart;
boolean result =true;
while (result && p2 !=null) {
if (p1.val != p2.val) {
result =false;
}
p1 = p1.next;
p2 = p2.next;
}
firstHalfEnd.next = reverseList(secondHalfStart);
return result;
}
private ListNodereverseList(ListNode head) {
ListNode prev =null;
ListNode curr = head;
while (curr !=null) {
ListNode nextTemp = curr.next;
curr.next = prev; // 交换
prev = curr;
curr = nextTemp;
}
return prev;
}
private ListNodeendOfFirstHalf(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while (fast.next !=null && fast.next.next !=null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
第七类:拆分链表
例题:86 Partition List 【medium】
题意:给定一个链表以及一个目标值,把小于该目标值的所有节点都移至链表的前端,大于或等于目标值的节点移至链表的尾端,同时要保持这两部分在原先链表中的相对位置。
test case:
解题思路: 二分法。设置两个指针left和right,顺序遍历整条链表,left、mid、target三者比较,根据情况left右移或者right左移。关键就在于边界情况和元素有重复。
当 nums[mid] = nums[left] 时,这时由于很难判断 target 会落在哪,那么只能采取 left++
当 nums[mid] > nums[left] 时,这时可以分为两种情况,判断左半部比较简单
当 nums[mid] < nums[left] 时,这时可以分为两种情况,判断右半部比较简单
code
链表:(单链表:反转、插入、删除)
基础知识:链表如何实现,如何遍历链表。链表可以保证头部尾部插入删除操作都是O(1),查找任意元素位置O(N)
基础题目:
Leetcode 206. Reverse Linked List
Leetcode 876. Middle of the Linked List
注意:快慢指针和链表反转几乎是所有链表类问题的基础,尤其是反转链表,代码很短,建议直接背熟。
第8类:链表相加求和
题目: 假设链表中每一个节点的值都在 0-9 之间,那么链表整体可以代表一个整数。 例如: 9->3->7 可以代表 937 给定两个这样的链表,头节点为 head1 head2 生成链表相加的新链表。 如 9->3->7 和 6 -> 3 生成的新链表应为 1 -> 0 -> 0 -> 0
总结链表: 基本操作:反转链表,合并一个链表,找链表的中间节点. 删除一个节点,添加一个节点.
采用的方法:递归,快慢指针,迭代法
如何判断一个单链表有环?链表翻转;
链表每 k 位逆序;
K个一组反转链表(重点)
链表反转
算法题:两个有序链表合并
6.算法题,链表求和
链表表示一个数字,求两个数字相加之和,返回一个链表
算法题:给定一个链表L1、L2,每个元素是为10以内的正整数,链表表示一个数字,表头为高位。 求两个链表之差,以链表形式返回
给定两个链表,存储着两个16进制数,链表的一个节点存储着16进制数的其中一个数,从高位到低位,求相加的值,返回一个链表,链表中保存相加的结果。(先反转链表,然后逐位相加,记录进位值,再与高位相加)手写代码
判断单链表相交,找出节点,手写代码
算法题:查找单链表中倒数第k个节点
从长序列中找出前K大的数字
算法题:冒泡排序的链表实现
链表逆序(头条几乎是必考的)
public ListNode reverseList(ListNode head) { if (head== null) { return null;} if (head.next== null) { return head;} ListNodeprev= null;ListNodecurrent= head;while (current != null) { ListNodenext= current.next;current.next= prev;prev= current;current= next;} return prev;}复制代码
删除排序数组中的重复项
public int removeDuplicates(int[]nums) { intlength= nums.length;if (length==0|| length ==1) { return length;} intsize=1;intpre= nums[0];for (inti=1; i < length; ){ if (nums[i]== pre) { i++;} else {pre= nums[size++] = nums[i++];} } return size;}复制代码
数组中找到重复元素
n个长为n的有序数组,求最大的n个数
用O(1)的时间复杂度删除单链表中的某个节点 把后一个元素赋值给待删除节点,这样也就相当于是删除了当前元素,只有删除最后一个元素的时间为o(N)平均时间复杂度仍然为O(1)
public void deleteNode(ListNode node) { ListNodenext= node.next;node.val= next.val;node.next= next.next;}复制代码
删除单链表的倒数第N个元素 两个指针,第一个先走N步第二个再走,时间复杂度为O(N),参考link
public ListNode removeNthFromEnd(ListNode head, int n) { if (head== null) { return null;} if (head.next== null) { returnn==1? null : head;} intsize=0;ListNodepoint= head;ListNodenode= head;do { if (size >= n + 1) {point= point.next;}node= node.next;size++;} while (node != null);if (size== n) { return head.next;}node= point.next;point.next= node == null ? null : node.next;return head;}复制代码
从长序列中找出前K大的数字
用数组实现双头栈
publicstaticclass Stack 两个链表求和,返回结果也用链表表示 1 -> 2 -> 3 + 2 -> 3 -> 4 = 3 -> 5 -> 7 public ListNode addTwoNumbers(ListNode node1, ListNode node2) { ListNodehead= null;ListNodetail= null;booleanupAdd=false;while (!(node1== null && node2 == null)) { ListNodemidResult= null;if (node1 != null) {midResult= node1;node1= node1.next;} if (node2 != null) { if (midResult== null) {midResult= node2;} else { midResult.val += node2.val;}node2= node2.next;} if (upAdd) { midResult.val += 1;} if (midResult.val >= 10) {upAdd=true;midResult.val %= 10;} else {upAdd=false;} if (head== null) {head= midResult;tail= midResult;} else {tail.next= midResult;tail= midResult;} } if (upAdd) {tail.next= new ListNode(1);} return head;}复制代码 交换链表两两节点 public ListNode swapPairs(ListNode head) { if (head== null) { return null;} if (head.next== null) { return head;} ListNodecurrent= head;ListNodeafter= current.next;ListNode nextCurrent;head= after;do {nextCurrent= after.next;after.next= current;if (nextCurrent== null) {current.next= null;break;}current.next= nextCurrent.next;after= nextCurrent.next;if (after== null) {current.next= nextCurrent;break;}current= nextCurrent;} while (true);return head;}复制代码 找出数组中和为给定值的两个元素,如:[1, 2, 3, 4, 5]中找出和为6的两个元素。 public int[]twoSum(int[]mun,int target) { Maptable= new HashMap<>();for (inti=0; i < mun.length; ++i){ Integervalue= table.get(target - mun[i]);if (value != null) { return new int[]{i, value};} table.put(mun[i], i);} return null;} 单链表的基本操作(增删改查) 获取单链表的长度 public int getLength(Node head){ if(head ==null){ return 0; } int len =0; while(head !=null){ len++; head = head.next; } return len; } 查询指定索引的节点值或指定值得节点值的索引 /** 获取指定角标的节点值 */ public int getValueOfIndex(Node head, int index)throws Exception { if (index <0 || index >= getLength(head)) { throw new Exception("角标越界!"); } if (head ==null) { throw new Exception("当前链表为空!"); } Node dummyHead = head; while (dummyHead.next !=null && index >0) { dummyHead = dummyHead.next; index--; } return dummyHead.value; } /** 获取节点值等于 value 的第一个元素角标 */ public int getNodeIndex(Node head, int value) { int index = -1; Node dummyHead = head; while (dummyHead !=null) { index++; if (dummyHead.value == value) { return index; } dummyHead = dummyHead.next; } return -1; } 链表添加一个元素 头部添加一个元素 public NodeaddAtHead(Node head, int value){ Node newHead =new Node(value); newHead.next = head; return newHead; } 尾部添加一个元素 public void addAtTail(Node head, int value){ Node node =new Node(value); Node dummyHead = head; //找到未节点 注意这里是当元素的下一个元素为空的时候这个节点即为未节点 while( dummyHead.next !=null){ dummyHead = dummyHead.next; } dummyHead.next = node; } 链表删除一个元素 由单链表的增加删除可以看出,链表的想要对指定索引进行操作(增加,删除),的时候必须获取该索引的前一个元素。记住这句话,对链表算法题很有用。 public NodedeleteElement(Node head, int index)throws Exception { int size = getLength(head); if (index <0 || index >= size) { throw new Exception("角标越界!"); } if (index ==0) { return deleteHead(head); }else if (index == size -1) { deleteTail(head); }else { Node pre = head; while (pre.next !=null && index >1) { pre = pre.next; index--; } //循环结束后 pre 保存的是索引的上一个节点 将其指向索引的下一个元素 if (pre.next !=null) { pre.next = pre.next.next; } } return head; } 寻找单链表的中间元素:快慢指针 (类似于判读一个链表是否有环)----题目(143)归并排序 已知一个单链表求倒数第 N 个节点() 常用技巧 1.使用dummy node。dummy node就是在链表的head前加一个节点指向head,即dummy->head,可以理解成一个虚拟节点。多针对于单链表没有前向指针的问题,保证链表的head不会在删除操作中丢失。通常情况下,如果链表的head会发生变化,譬如删除或者被修改等,可以创建dummy node: ListNodedummy=newListNode(0);dummy.next = head;复制代码 这样就使得操作head节点与操作其他节点没有区别。 2.双指针法。对于寻找链表的某个特定位置,或者判断是否有环等问题时,可以用两个指针变量fast和slow: ListNodeslow=head;ListNodefast=head;复制代码 以不同的速度遍历该链表,以找到目标位置。注意:在测试时,需要分别选取链表长度为奇数和偶数的test case,可以验证算法在一般情况下的正确性避免遗漏。 3.交换节点的处理。如果需要交换两个节点的位置,譬如24题 Swap Nodes in Pairs,需要交换两个相邻位置的节点,对于这两个前驱节点,他们的next指针会受到影响,这两个节点本身也会受到影响,可以用以下步骤: 1)先交换两个前驱节点的next指针的值 2)再交换这两个节点的next指针的值 无论这两个节点的相对位置和绝对位置如何,以上的处理方式均可成立 4.同时操作两个链表的处理。遇到这种题目,循环的条件一般可以用while(list1 && list2),当循环跳出来后,再处理剩下非空的链表,这相当于:边界情况特殊处理,常规情况常规处理 链表的问题是面试当中常常会问到的,比如链表的倒置,删除链表中某个结点,合并两个排序链表,合并 k 个排序链表,排序两个无序链表等。 这些题目一般有一些相应的技巧可以解决。 第一,引入哨兵结点。如我们开头说的 dummy node 结点。在任何时候,不管链表是不是空,head结点都会一直指向这个哨兵结点。我们也把这种有哨兵结点的链表叫做带头链表。 第二,双指针法。这种用法适用于查找链表中某个位置,判断链表是否有环等 第三,分之归并法。这种用法适用于链表的排序处理,如合并 k 个排序链表,排序两个无序链表等。 第四,在解答的过程中,要多考虑边界情况。 链表为空 链表中只有一个结点 链表中只包含两个结点 代码在处理头结点跟尾结点是否存在什么问题 链表的相关重要的操作: 1.dummo:添加头节点。 因为会移动链表。所以要有一个保存最开始的 2.pr-->node 3.next-->node 如何移动指针 特点:没有指向前面的指针,所以需要定义一个pre 最后返回dummy .next()。就是链表 链表的一些操作: 1.如何遍历一个链表 while (head !=null) { head=head.next; } 2.如何指向下一个指针 head=head.next;区别: // head.next = head.next.next; // head=head.next; 链表专项突破 https://juejin.cn/post/6855865111354851335 https://juejin.cn/post/6844903573508063246