链表的重要性,那是不言自明,那么一起刷题吧,如果觉得还行记得随手给个赞👍,嘿嘿。
后续还会有其他的专练,嘚嘚。都是一些比较经典的链表题目。
基本会控制在对应专练的每一篇博客在十个题目左右!
删除链表的倒数第n个结点
由于头节点可能是被删除的,所以技巧就是建立一个虚拟的头结点,让虚拟的头结点指向真正的结点。
由于要删除一个结点,所以需要找到要删除结点的前一个结点。也就是找到倒数第n+1个结点。
由于只可以遍历一次,所以使用快慢指针的思路
- 快指针前走n 步 之后 快慢指针一起走,当快指针走到最后一个点的时候,终止,此时的慢指针就是倒数第n+1个结点了。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//首先对n进行合法性的校验-- 题目已说是合法的
//要得到倒数第n个结点使用快慢指针来做
ListNode prev = new ListNode(-1);
prev.next = head;
ListNode slow = prev;
ListNode fast = prev;
while(n>0){
fast = fast.next;
n--;
}
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
// slow 指向的是要删除的结点的前一个 所以不会出现空指针异常的
slow.next = slow.next.next;
//要返回虚拟头节点的下一个结点,头节点可能被删掉
return prev.next;
}
}
删除链表中的结点
这个题的确写的是简单,但是别小瞧这个题,有点意思哈!
传入的参数很有意思,不是链表的头结点,也不知道链表的头结点。
但是如果我们要删除一个结点,一般的做法就是要知道要删除的结点的前一个,但是这里不可以获取,所以只可以使用一种假装删除的做法。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public void deleteNode(ListNode node) {
node.val = node.next.val;//复制下一个结点的值
node.next = node.next.next; // 由于给定的点一定不是最后一个点,所以不会出现空指针异常。
}
}
删除链表中的重复元素
使用快慢指针的做法
如果当前的值和下一个值不相等那么慢指针和快指针各向后一步走
如果当前的值和下一个值相等,那么就让快指针一直往后走,直到走到快慢指针的值不相等为止,然后是slow 的next 指向fast 然后fast 继续往后走。
这个题做过很多次,但是每次都没有办法一遍做对!
- 要走完整个链表而不是走到最后一个结点 所以条件是 cur != null
- 最后无论如何,都要把slow的next 置空处理,避免 如果整个链表全都是重复的 1-1-1-1 链表走完 都没有找到那个不重复的点,所以必须要置空!
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null) return null; //特例判定
ListNode slow = head;
ListNode fast = head.next; //一定有重复元素 所以fast 一定不为空
while(fast != null ){
if(slow.val == fast.val){
fast = fast.next;
}else{
slow.next = fast;
slow = fast;
fast = fast.next;
}
}
slow.next = null;//最后一定要置空处理
return head;
}
删除链表中的重复元素II(※)
思路一:
使用快慢指针的做法
- 判断两个指向的值是否一样,一样既让fast 一直往后,直到找到不一样的为止。然后slow 指向fast fast 指向fast的next 继续判断。
- 如果值不一样,那么当前的slow 指向的结点就可以加入到无重复的链表中去。
- 头节点可能重复,所以new 虚拟结点 同时虚拟结点作为无重复链表的前驱结点。
- 要考虑 fast 为空的时候 slow 的状态是怎么来的。
每一次做,代码写的都不一样…
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head == null || head.next == null) return head;
ListNode fake = new ListNode(-1);
fake.next = head;
ListNode prev = fake;
ListNode slow = head;
ListNode fast = head.next;
while(fast != null){
if(slow.val != fast.val){
prev.next = slow;
prev = slow;
slow = fast;
fast = fast.next;
}else{
while(fast != null && slow.val == fast.val){
fast = fast.next;
}
if(fast == null){ //走完链表都是重复的 也就是最后一个元素是重复的
prev.next = null;
return fake.next;
}else{
slow = fast;
fast = fast.next;
}
}
}
//这里跳出循环是因为fast 为空 并且最后一个元素没有重复 一定要把最后一个元素给串起来!
prev.next = slow;
return fake.next;
}
}
旋转链表
需要找到的点 是 移动的头节点和尾结点,可以使用双指针来确定。由于需要头结点的前一个结点,所以使用双指针。fast先走k步
然后更改 fast的next 是head 。head 指向 slow 的next,然后将slow的next 置空。
注意此题的k取值可能会很大,所以需要先求出链表的长度,然后将k %= size
class Solution {
public ListNode rotateRight(ListNode head, int k) {
ListNode fake = new ListNode(-1);
fake.next = head;
//首先对k 和 head 进行校验
if(head == null) return null;
//求链表的长度 然后取模 缩小k的范围
ListNode cur = head;
int count = 0;
while(cur != null){
count++;
cur = cur.next;
}
k = k %count;
if(k == 0) return head;
//也就是找到倒数的第k个的前一个
ListNode fast = fake;
ListNode slow = fake;
while( k > 0){
fast = fast.next;
k--;
}
while(fast.next != null){
slow = slow.next;
fast = fast.next;
}
fast.next = fake.next;
fake.next = slow.next;
slow.next = null;
return fake.next;
}
}
两两交换链表中的节点
头结点会发生变化,所以新建虚拟结点。同时以每一对作为一个单位 ,枚举的是每一对的头一个结点。
class Solution {
public ListNode swapPairs(ListNode head) {
//不存在头结点 或者 是头结点的下一个结点不存在那么就不需要交换
if(head == null || head.next == null) return head;
//说明至少有两个结点
ListNode prev = new ListNode(-1);
ListNode fake = prev;
prev.next = head;
ListNode cur = head;
while(cur != null && cur.next != null){
prev.next = cur.next;
cur.next = cur.next.next;
prev.next.next = cur;
prev = cur;
cur = cur.next;
}
return fake.next;
}
}
反转链表(※)
cur表示需要反转的结点以及 记录它的前驱和后继。(本质是两两操作记录两个即可,但是由于更改会影响下一个 所以需要记录第三个结点)
同时记得更改头节点的指向置空
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null || head.next == null) return head;
ListNode prev = head;
ListNode cur = head.next;//代表当前要翻转的结点
head.next = null;//这里一定要置空否则会循环
ListNode curNext = null;
while(cur != null ){ //要走完整个链表
curNext = cur.next;
cur.next = prev;
prev = cur;
cur = curNext;
}
return prev;
}
}
反转链表II
没啥难点,除了一些特殊的判断唯一需要多说一句的就是坑点。
因为要根据left 和 right 的差值确定有多少结点需要翻转,而这个值需要事先求出来,(中间的left 和 right 因为要确定翻转起始点和结束点所以会变化。)
public ListNode reverseBetween(ListNode head, int left, int right) {
if(head == null || left == right) return head;
//首先需要找到 left 的前驱 可能为空
ListNode fake = new ListNode(-1);
fake.next = head;
ListNode prev =fake;
ListNode cur = head;
//这个必须放在前面 left 会变动
int sub= right - left;
int temp = sub;//
while(left-1 > 0){
prev = cur;
cur = cur.next;
left --;
} //cur 指向了left
ListNode end = cur;
while(sub >0){
end = end.next;
sub--;
}
//end 指向 right
prev.next = end;
ListNode curNext = cur.next;
cur.next = end.next;
sub = temp;
while(sub > 0){
prev = cur;
cur = curNext;
curNext = cur.next;
cur.next = prev;
sub--;
}
return fake.next;
}
相交链表(※)
思路一:差值法
整个思路其实一点都不难想,但是就是想不到,对自己无语,这可是一个easy的题啊!而且还是做了很多遍的。
其实长短不一的链表,分别求出两个链表的和然后让长的那一个先走和短的的差值,之后两个一起走,如果相等就相交了,不相等如果一直走到null 就说明确实是不相交的链表
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode cur1 = headA;
int countA = 0;
int countB = 0;
ListNode cur2 = headB;
while(cur1 != null){
countA++;
cur1 = cur1.next;
}
cur1 = headA;
while(cur2 != null){
countB++;
cur2= cur2.next;
}
cur2 = headB;
int sub = countA-countB;
if(sub >0){ // 判断哪一个链表长 让长的链表先走差值 之后再一起走。
while(sub >0){
cur1 = cur1.next;
sub--;
}
while(cur1 != null){
if(cur1 == cur2){
return cur1;
}
cur1 = cur1.next;
cur2 = cur2.next;
}
}else{
while(sub <0){
cur2 = cur2.next;
sub++;
}
while(cur2 != null){
if(cur1 == cur2){
return cur2;
}
cur1 = cur1.next;
cur2 = cur2.next;
}
}
return null;
}
}
但是这样的做法链表需要先遍历一遍,并且可以看出来,代码的冗余度很大。
思路二:双指针(这个要多刷!)
可以让每一个结点都多走一个空结点,空结点也算一步,之后再指向下一个的头节点。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode cur1 = headA;
ListNode cur2 = headB;
while(cur1 != cur2){
if(cur1 == null){
cur1 = headB;
}else{
cur1 = cur1.next;
}
if(cur2 == null){
cur2 = headA;
}else{
cur2 = cur2.next;
}
}
return cur1;
}
}
思路三:哈希法
遍历一遍链表1,放大哈希表里面,然后遍历链表2的每一个结点,看当前遍历到的结点是否再哈希表中存在。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
Set<ListNode> set = new HashSet<>();
ListNode cur1 = headA;
while(cur1 != null){
set.add(cur1);
cur1 = cur1.next;
}
ListNode cur2 = headB;
while(cur2 != null){
if(set.contains(cur2)){
return cur2;
}
cur2 = cur2.next;
}
return null;
}
}
环形链表
如果链表有环,快慢指针一定相遇,两个相等,如果没有相遇,那么返回结果。
public class Solution {
public boolean hasCycle(ListNode head) {
if(head == null) return false;
ListNode slow = head;
ListNode fast = head.next;
while(fast != null && fast.next != null){
slow = slow.next;
fast = fast.next.next;
if(slow == fast){
return true;
}
}
return false;
}
}
环形链表II(※)
快慢指针的经典题目
思路概述:
好像是操场跑步一样,一个指针走一步,一个指针走两步,这两个指针一定会相遇的。但是相遇的地方不一定是入环的结点,因此当相遇的时候,让快指针回到起点,然后在和慢指针一人一步走,最后再次相遇的结点就是入环的结点位置。
证明原理:
为什么是一个一步,一个两步? 不可以是一个一步,一个三步走、四步走吗?
其实是可以的,当环的长度不确定的时候 有公式表明一个一步 一个两步相遇消耗的时间平均最少。(也就是可以最快的相遇)
两个指针一定会相遇 这个不用过多解释了,当慢指针入环的时候,快指针每走一次两个指针的距离缩小1
如何做到第一次相遇后 一个回到起点 和另一个分别一步走直到再次相遇的时候就是入环的点呢?
老说人家链表的题简单,其实是非常考验逻辑思维的,谁再说链表简单😕?
- 没有结点和一个结点不能成环。
- while (条件) 不仅要作为循环的判断逻辑还要作为进入的判断逻辑(所以 fast == slow 这个判断不可以加while上 必须是在里面的 否则都无法进入while)
- 结束循环的时候 是 由于什么情况结束的
public class Solution {
public ListNode detectCycle(ListNode head) {
if(head == null || head.next == null) return null;
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
if(fast == slow){
break;
}
}
//说明就是第一次相遇了 说明有环了
if(fast == slow ){
fast = head;
}else{
return null;//说明没有环
}
while(fast != slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
第一部分就暂时到这里啦!
接下来第二部分的链表题都会很变态,至少我个人这么觉得。争取或许可以在这周日前把他们搞出来?