删除链表中重复节点
- 题目介绍
存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中 没有重复出现 的数字。返回同样按升序排列的结果链表。 - 思路分析
-
首先要解读题目条件:
1.有序链表中的重复节点一定是相邻的
2.全部删除掉指的是值域为34的节点全部删除 -
解题思路:
1.删除一个节点的前提是要知道该节点的前一个结点
2.倘若在原链表进行操作将会出现所有重复元素至少要保留1个的弊端,不符合题意,因此我们需要单独设立一个虚拟节点,然后将原链表中不重复的节点按顺序添加到新链表中 -
按我们现在的思路我们大致能够得到以下这样的结果
-
但是存在这样一种特殊的情况:如果原链表中最后一个节点也是重复节点的话,新得到的链表将会出现以下情况:
可以看到新的链表的最后一个节点的next域不为空,这样形成的链表在遍历时有多恐怖想必大家是了解的,因此我们需要将新形成的链表中最后一个节点的next域设为空
-
- 相关代码片段
/**
* 将列表中所有重复的节点全部删除掉,其他节点顺序不变
* 12 34 34 34 1 =====> 12 1
*/
public ListNode deleteRepeat(ListNode head) {
//判断链表是否为空
if (head == null) {
return null;
}
//设立虚拟节点
ListNode newHead = new ListNode(-1);
ListNode tmp = newHead;
ListNode cur = head;
//遍历原链表将不重复的节点按顺序添加到新链表中
while (cur != null) {
if (cur.next != null && cur.val == cur.next.val) {
while (cur.next != null && cur.val == cur.next.val) {
cur = cur.next;
}
cur = cur.next;
} else {
tmp.next = cur;
cur = cur.next;
tmp = tmp.next;
}
}
//将新链表的末尾节点的next域设为null
tmp.next = null;
return newHead.next;
}
链表回文结构
- 题目介绍
给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。 - 思路分析
- 首先在这里介绍一下回文结构:诸如ABCBA这样正着遍历和倒着遍历的结果一样的链表满足回文结构
- 首先单向链表的结构决定了它只能往一个方向访问,因此要判断链表是否为回文结构就应该让该链表后半部分反转形成一个两头指向中间的链表
- 因此该题共有以下几个步骤
1.找到中间节点
2.以中间节点开始将之后的节点指向进行反转
3.然后首尾向中间访问,看是否满足回文结构 - 依照上面的思路我们可以分析得到:
图中关于如何寻找中间节点、如何反转链表可以参考我之前的一篇文章:单链表操作详解(反转、遍历). - 这里仍旧存留一个细节待解决:在上述图中我们假定链表中节点的个数是奇数,这样我们的中间节点毫无疑问就是中间一个,但当链表中节点的个数是偶数时,中间节点就变成了两个,我们在这里研究的是中间节点的后一个节点。因此,在判断是否满足回文结构时,我们需要设立两个不同的循环结束条件:针对奇数个节点的链表,当头尾节点向中间走时走到同一个节点时结束循环,即
slow == fast
;针对偶数个节点的链表,当头尾节点向中间走时当走到头节点的下一个节点为尾节点时结束循环,即slow.next==fast
,且这两个判断条件是相互独立的,即奇数个节点的链表中是不可能存在偶数个节点的链表中循环结束那样的条件出现
- 相关代码片段
/**
* 判断链表是否是回文结构
*/
public boolean chkPalindrome(ListNode head) {
// 判断链表是否为空
if (head == null) {
return true;
}
//定义快慢指针
ListNode fast = head;
ListNode slow = head;
//寻找中间节点
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
ListNode cur = slow.next;
//反转链表的后半部分
while (cur != null) {
ListNode curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
//以奇数为外循环条件遍历链表
//看链表是否遵循回文结构
while (head != slow) {
if (head.val != slow.val) {
return false;
}
/*在奇数节点的链表中单独判断偶数节点的链表是否遍历结束
能这么做的原因是因为两个判断条件是相互独立的,
二者有且仅有可能在自己的循环中出现*/
if (head.next == slow) {
return true;
}
head = head.next;
slow = slow.next;
}
return true;
}
寻找相交链表第一个节点
-
题目介绍
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。 -
思路分析
-
根据题目,我们需要首先理解什么是相交链表:即相交链表是y型还是x型,相交节点指的是值域相同还是next域相同。因为我们研究的是单链表,它单一的指向就是指next域只能指向另一个具体的节点,所以只可能存在多对一,不可能存在一对多的现象,因此相交链表即为y型且next域相同的才是相交节点
-
现在我们来思考这个问题:如何判断两个链表有无相交节点?我们能够想到如果两个指针分别从各自链表开始向后遍历,当两者能够在同一个节点相遇就证明存在相交节点。但由于两个链表的长度是无法确定的,所以无法通过控制二者的速度来让二者在某个位置相遇。
-
考虑一个追及问题:A、B速度相同,且A、B二者各自需要走的总路程分别已知,如何让A、B同时在某一个地点相遇?我们可以让两者中总路程更长的那个先走二者的路程差步,然后二者再同时出发,这样A、B二者的剩余路程相同、速度也相同,所以能够同时到达一个地点
-
同样到我们这道题:想要保证两个链表的访问节点同时走到相交节点,就需要让较长的链表先走两个链表长度的差值步;然后两个访问节点再同时走,直到两个节点相同
-
-
相关代码片段
/**
* 返回两个相交链表的第一个相交节点
*/
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//判断二者是否均不为空
if (headA == null || headB == null) {
return null;
}
//定义两个指针使pl永远指向较长的链表头,ps永远指向较短的链表头
ListNode pl = headA;
ListNode ps = headB;
int lenA = 0;
int lenB = 0;
while (pl != null) {
lenA++;
pl = pl.next;
}
//pl==null
pl = headA;
while (ps != null) {
lenB++;
ps = ps.next;
}
//ps==null
ps = headB;
int len = lenA - lenB;
//差值步
if (len < 0) {
pl = headB;
ps = headA;
len = lenB - lenA;
}
//1、pl永远指向了最长的链表 ps 永远指向了最短的链表 2、求到了差值len步
//走差值len步
while (len > 0) {
pl = pl.next;
len--;
}
//同时走 直到相遇
while (pl != null && ps != null) {
if (pl == ps) {
return pl;
}
pl = pl.next;
ps = ps.next;
}
return null;
}
判断链表是否有环
- 题目介绍
给定一个链表,判断链表中是否有环。 - 思路分析
-
我们依旧考虑一个追及问题:考虑1个情景,两个人同时同地在圆形操场出发,当1个人比另1个人跑的快时,理想条件下二个人一定会再次相遇
-
由此引发本题,即当1个结点跑的比另一个结点快时,如果两个节点能够再次相遇,那么就足以说明该链表是存在环的。
-
至此我们需要考虑的问题就只剩下规定fast指针1次走几步的问题显然2步是没有问题的,但是能不能3步、4步。。(面试问题)至此我们需要考虑的问题就只剩下规定fast指针1次走几步的问题显然2步是没有问题的,但是能不能3步、4步。。(面试问题)
-
我们考虑这样1种情况,当fast走3步,slow走1步时,下面这种情况二者是没法相遇的.总是会错过。(当然如果fast和slow指向同一个节点,那只要让另一个间节点比另一个节点快就一定会相遇,只不过2步的平均效率更高)
由此我们可以归纳出这样一个结论:fast走k步(k>2),slow走1步。那么当一个首尾相连的环形链表的初始情况满足fast指向尾节点,slow指向尾节点的前一个节点时fast和slow永远不会相遇(不严谨证明)
-
因此我们可以定义两个指针fast和slow,fast每次走2步,slow每次走1步。二者同时从头节点出发,如果能够指向同一个节点,说明该链表是有环的。
-
- 相关代码片段
/**
* 判断链表是否有环
*/
public boolean hasCycle(ListNode head) {
//判断链表是否为空
if (head == null) {
return false;
}
//定义快慢指针
ListNode fast = head;
ListNode slow = head;
//遍历链表,如果没环一定会走到尾
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
return true;
}
}
return false;
}
返回链表的第一个入环节点
-
题目介绍
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 -
思路分析
- 首先该题需要判断链表是否有环,然后在有环的基础上去寻找第一个入环节点。因此该题应该建立在上一个题的基础上去进行
- 由此让两个节点分别从起始点和相遇点以相同速度同时出发,当两个节点相遇时所在节点位置即为入环节点
- 首先该题需要判断链表是否有环,然后在有环的基础上去寻找第一个入环节点。因此该题应该建立在上一个题的基础上去进行
-
相关代码片段
/**
* 返回链表入环的第一个节点,没有环就返回null
*/
public ListNode detectCycle(ListNode head) {
//判断链表是否为空
if (head == null) {
return null;
}
//定义快慢指针
ListNode fast = head;
ListNode slow = head;
//如果只有一个节点还谈啥环不环的!
if (fast.next == null) {
return null;
}
//找到相遇点
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) {
break;
}
}
/*
如果上一个循环不是因为找到相遇点break出来的而是因为fast走到头了,说明该链表是没有环的,因此就更没有入环的第一个节点了,直接返回null就好了
*/
if (fast == null || fast.next == null) {
return null;
}
slow = head;
//头节点和相遇点同时出发直到二者走到同一个节点
while (fast != slow) {
fast = fast.next;
slow = slow.next;
}
//返回fast和slow是一样的
return fast;
}
本文中的其余模块的代码(节点结构、测试)在我的这篇文章中:单链表操作详解(反转、遍历),有需要的请自取。
总结
本文主要解决了关于链表的回文结构、环以及相交链表方面的一些问题。当然这些问题仅仅是一小部分,最重要的是体会解决这种链表问题时的常见思想:诸如快慢指针、追及等等,同时我也会陆续更新链表的相关问题,希望大家能有所收获!