文章目录
JavaOj & 链表十连问
- Java数据结构 & LinkedList & 链表_s:103的博客-CSDN博客
- 紧接上文,我们开始投入链表的oj题中吧~
- 最后我补充两个个知识点
- LinkedList链表的反向遍历~
- ArrayList与LinkedList区别
- 链表归并排序~【涉及排序原理】
- 最后我补充两个个知识点
1. 删除链表中等于给定值 val 的所有节点
1.1 代码实现~
- 这个模式在数据结构的题目中,尤为常见,一定要重点熟悉~
- 这个类就是现成的节点类~
class Solution {
public ListNode removeElements(ListNode head, int key) {
if(head == null) {
return head;
}
ListNode prev = head;
ListNode cur = head.next;
while(cur != null) {
if(cur.val == key) {
prev.next = cur.next;
}else {
prev = cur;
}cur = cur.next;
}
if(head.val == key) {
head = head.next;
}
return head;
}
}
1.2 深度讲解 + 动图分析
-
这道题就是我们的removeAllKey方法呀~
-
再讲一次~
-
删除所有同键值节点~
- 这个稍微复杂,思想仍然是(跳跃式忽视节点的方法)【即prev的后驱指向cur的后驱】
- 用到了一个前后指针的算法
- prev一开始处于head的位置,cur在head.next的位置
- 遍历链表,结束条件是cur为null
- 正常情况下,即删除节点之前,prev和cur都是同步走的
- 但cur遇到要删除的节点的时候,要让cur去找下一个非此键值的节点(如果还是该键值,这个节点将不会被后续操作删除)或者null,cur每次遇到键值匹配的节点,size–
- cur每次找到key,prev的后驱指向cur的后驱 ===》实现删除连续节点
- cur一旦找不到key,prev就会继承cur的位置
- 最不应该忘记的是这个:
- 头节点留到最后再判断,如果一开始将头结点删了,那么后续仍然有头结点无法被判断,反反复复无法解决问题~,这样还不如先判断后面的节点,最后再判断头节点
- 如果头节点该删,head = head.next~
-
动图演示:
2.反转单链表
2.1 代码实现
- see it again~
class Solution {
public ListNode reverseList(ListNode head) {
if(head == null) {
return null;
}
ListNode cur = head.next;
head.next = null;
while(cur != null) {
ListNode curNext = cur.next;
cur.next = head;
head = cur;
cur = curNext;
}
return head;
}
}
2.2 深度解析 + 动图分析
- 不知道你有没有联想到可以用头插法~
- *你可能在链表学习中,在测试的时候会发现,连续头插的结果是逆序的,那么我们就可以借助这一现象,逆序链表,借助探路指针还有“**传送指针”*进行操作,且听我细细道来~
- head 为 null,不用逆序,直接返回null
- cur探路指针去遍历,curNext去记录cur的后驱,防止回收
- 将cur直接头插链表~
- 通过 “传送指针curNext” 前往原来的位置的后驱~
- 直到cur被传送到null,退出循环~
- return head;
- 动图解析:
3. 链表的中间结点
3.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 middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast != null && fast .next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
3.2 深度解析 + 动图分析
- 这里有一个很重要的思想
- “快慢指针” --> 在Java中是引用
- 两个探路指针去探路,但是有一个指针速度是另一个的一倍,这就是快指针
- 那么快指针到达null的时候,慢指针就处于中间位置了~
- 但是要区分这个链表是偶数节点数还是奇数节点数~
- 我们可以通过节点数确定该节点的下标,但是我要求在只遍历一次的情况下去解决问题~
- 奇数:正中间节点~
- 偶数:正中间右节点~
- 很巧的是,题目要求的就是这个~
- 动图解析:
3.2.1 奇数:
3.2.2 偶数:
4. 链表中倒数第k个结点
链表中倒数第k个结点_牛客题霸_牛客网 (nowcoder.com)
4.1 代码实现
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
if(head == null) {
return null;
}
ListNode fast = head;
ListNode slow = head;
for(int i = 0; i < k; i++) {
fast = fast.next;
if(fast == null && i < k - 1) {
return null;
}
}
while(fast != null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
4.2 深度解析 + 动图分析
-
通过第三题,我们这道题不难联想到,也可以用“快慢指针”
-
但是 “快慢指针” 一定要速度不相同吗?
- 显然不是必要的~
- 这里的快慢指针,有一个指针快人一步~
- 也就是说,除了速度快,还可以 “笨鸟先飞”~
- 我们学习也是要这样,虽然可能学得不快,但是可以先学~
-
我们当然也可以遍历两次表去解决这个问题,但是我要求只遍历一次~
-
那么我们就可以让fast的快指针,先走k步,然后再与slow一起走,那么fast走完的时候,slow的位置,就是倒数第k个~
- 因为fast与slow差距k,最终就是slow与null差距k~
- 结合牛客给的测试用例,有可能k很大,或者head为null~
- 我们要处理这些细节~
- 动图分析:
5.合并有序链表
5.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 mergeTwoLists(ListNode list1, ListNode list2) {
ListNode head = new ListNode();
ListNode cur = head;
while(list1 != null && list2 != null) {
if(list1.val <= list2.val) {
cur.next = list1;
list1 = list1.next;
}else {
cur.next = list2;
list2 = list2.next;
}
cur = cur.next;
}
if(list1 != null) {
cur.next = list1;
}else {
cur.next = list2;
}
head = head.next;
return head;
}
}
5.2 深度解析 + 动图分析
- 这时候我们就需要这个特殊的链表:带头链表
- 这头指针又称之为:“哨兵”
- 而根据它的用途,我更喜欢称之为:“外来异物”
外界异物侵入珍珠蚌内后,在保护机制下,珍珠蚌会分泌一种珍珠质,将异物层层包裹,最终形成了珍珠。
- 那么这个头,不是有效节点,但是可以帮助我们累积构造形成链表~
- list1.val <= list2.val 那么就连接list1,list1往后走
- 反之,连接list2,list2往后走
- 无论连接谁,cur保持处于待构造表的尾部
- 动图解析:
6. 链表分割
6.1 代码实现
public class Partition {
public ListNode partition(ListNode pHead, int x) {
if (pHead == null) {
return null;
}
ListNode fast = pHead.next;
ListNode prev = pHead;
ListNode slow = null;
ListNode cur = null;
while (fast != null) {
if (fast.val < x) {
if (slow == null) {
slow = fast;
cur = slow;
} else {
cur.next = fast;
cur = cur.next;
}
} else {
prev.next = fast;
prev = fast;
}
fast = fast.next;
}
prev.next = null;
if (pHead.val < x) {
ListNode tmp = pHead;
pHead = pHead.next;
tmp.next = slow;
slow = tmp;
}
if (slow == null) {
slow = pHead;
} else {
cur.next = pHead;
}
return slow;
}
}
6.2 深度解析 + 动图分析
-
这个问题会比较难,因为涉及多个中介节点
-
要求以给定的一个值为标准,小于该值的放在左边,大于该值放在右边,等于的话在哪边都无所谓~
- 并且要求,左右两条子链表是按照原来的顺序排的~
-
-
写一次不用 “外来异物” 的方法~
-
重要思想:
- 类似于单链表删除操作~
- 头节点不应该在最开始的时候进行判断,而是留到最后判断
- fast做为最初的探路指针,它完整遍历原链表~
- slow的存在是“较小数节点”的链表~
- 如果fast此时键值小于x
- 如果fast此时键值不小于x
- 处理原链表的头节点
- 连接两条链表~
6.2.1 头节点为较小节点图示:
6.2.2 头结点不为较小节点图示:
7. 回文链表
链表的回文结构_牛客题霸_牛客网 (nowcoder.com)
7.1 代码实现
public class PalindromeList {
public boolean chkPalindrome(ListNode A) {
ListNode fast = A;
ListNode slow = A;
if (fast == null) {
return false;
}
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
//逆序后面的表
ListNode cur = slow.next;
slow.next = null;
while (cur != null) {
ListNode curNext = cur.next;
cur.next = slow;
slow = cur;
cur = curNext;
}
while (slow != null) {
if (A.val != slow.val) {
return false;
}
A = A.next;
slow = slow.next;
}
return true;
}
}
7.2 深度解析 + 动图分析
- 回文,我们可以分别判断首与尾,首与尾,首与尾,就像顺序表那样~
- 但是这种算法的时间复杂度为O(N2)
-
- 我们可以逆序一整个链表,判断是否与原链表一样
- 我们可以只逆序一半,判断前半段与后半段是否相同
- 法2, 显然是最合适的~
- 这道题是中间节点,与逆序链表的结合题~
- 找到中间节点,用头插逆序的方法将后半段进行逆序
- 判断是否回文~
-
对于奇数偶数节点的链表,用刚才的方法有如上两种不同形式~
- 相交链表~
-
只要slow指针能达到null,则说明true~
- 只要不相等,直接返回false~
7.2.1 奇数节点链表后半段逆序 + 判断回文动图解释:
- 逆序后半段:
- 判断回文:
7.2.2 偶数节点链表后半段逆序 + 判断回文动图解释:
- 逆序后半段:
- 判断回文:
8. 相交链表
8.1 代码实现
- 节点类型跟之前的一样~
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
while(curA != null && curB != null) {
curA = curA.next;
curB = curB.next;
}
while(curA != null) {
headA = headA.next;
curA = curA.next;
}
while(curB != null) {
headB = headB.next;
curB = curB.next;
}
while(true) {
if(headA == headB) {
return headA;
}
headA = headA.next;
headB = headB.next;
}
}
}
8.2 深度解析 + 动图分析
- 一开始想到的方法就是,遍历N2次, 即一个链表,每一个节点都在另一条链表上(整趟)找
- 很笨~
- 相交单链表的特点就是,交点到链尾,两个表是完全重合的,也就是说,两条相交的单链表,个数上的差异取决于节点之前的个数差异~
- 当然也可以用两次遍历确认两条的长度,然后让他们前半段的个数差抵消~
- 但是我不喜欢多遍历一次去测长度~
- 且看看我的方法~
- 是相交链表:
- 不是相交链表:
- 这样都指向null的时候,就会返回null
- 不相交返回空~
9. 循环链表之判断是否带环
9.1 代码实现
- 节点类型跟之前的一样~
public class Solution {
public boolean hasCycle(ListNode head) {
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;
}
}
9.2 深度解析 + 动图分析
- 这里结合了 “快慢指针” 的性质
- 让两个指针速度不一样,当进入循环链表的时候
- 由于速度不一样,两个指针会相遇~
- 如果不是循环链表,快指针会很快指向null~
- 为什么“快慢指针”的速度为2:1?
-
因为其他比例,例如3:1 ,可能会导致循环链表,两个引用一直没相遇,反复错过,绕多了很多圈~
- 如果结尾出现,两个节点的循环,并且slow在前,fast在后~
- 就会出现死循环~
-
对于第十问,2:1 有很大的帮助!
-
10. 循环链表之入口点
10.1 代码实现
- 节点类型跟前面一模一样~
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
ListNode cur = head;
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if(fast == slow) {
while(cur != slow) {
cur = cur.next;
slow = slow.next;
}
return slow;
}
}
return null;
}
}
10.2 深度解析 + 动图分析
- 只需要在第九题的基础上,做一些更改~
- 作为返回值,如果不是循环链表返回null
- 如果是循环链表,返回入口点,那么入口点的计算算法是什么呢?
-
- 在第九题的基础上,我们找到了相遇点,而相遇点不一定是入口点~
- 所以两个指针必须同时走,一快一慢~
- 对于相遇点有这么一个性质:相遇点与链表头节点,两个引用这两个点出发,速度为1的情况下,会在入口点相遇~
- 下面看我证明:
- 在第九题的基础上,我们找到了相遇点,而相遇点不一定是入口点~
-
-
得到这个结论,因为我是按单位长度来算的,所以,只要是循环链表,就满足这个公式~
- 无非就是有些量为0的特殊情况~
- 这也是为什么用两倍的关系,这样才有这么好的性质~
-
这就是这一段代码的由来~
-
- 差不多就这样:
11. 补充知识点~
-
LinkedList有一个和顺序表一样的构造方法,可以提供一个集合类,完成拷贝~
-
牛客网在线编程_编程学习|练习题_数据结构|系统设计题库 (nowcoder.com)
- 学会自主学习~
- 你现在有能力刷题了哦~
11.1 LinkedList链表的反向遍历~
- 对于单链表,逆序打印开销实在太大了~
- 所以可以借助迭代器(了解)
- ListIterator是List受重写后的一个方法,listIterator(int index)
- index为迭代器内部的引用,指向的是index下标~
- 这个迭代器既可以顺序打印,也可以逆序打印~
- 逆序的话,用
listIterator.hasPrevious()
为条件,内置引用前有元素,即可打印listIterator.previous()
为内置引用的元素- 并且调用一次,引用往前走一步
public static void main(String[] args) {
List<Integer> list = new LinkedList<>();
list.add(55);
list.add(56);
list.add(57);
list.add(58);
list.add(59);
ListIterator<Integer> listIterator = list.listIterator(list.size());
while(listIterator.hasPrevious()) {
System.out.print(listIterator.previous() + " ");
}
}
- 结果正常~
11.2 ArrayList 与 LinkedList的区别
不同点 | ArrayList | LinkedList |
---|---|---|
存储空间 | 空间分布紧密连续 | 逻辑上连续,但是空间分布分散 |
随机访问 | O(1) | O(N) |
头插 | 需要挪动元素,O( N ) | O(1) |
插入 | 空间不够需扩容 | 无容量概念 |
应用场景 | 元素高效存储 + 访问次数多 | 频繁插入,频繁删除 |
11.3 链表归并排序(知识扩展)【涉及排序原理】
-
时间复杂度O(N * log2N), 空间复杂度O( 1 );
-
思路跟刚才归并怎么排一致,重点在于用快慢指针找到中点,节省速度
- 是可以先求长度然后一直用的(就是算一次长度,然后每次都用这个长度为基准,去找mid)
-
之后不断合并有序链表。
-
看几次动图:
-
递归的重点就分清“整体感”,比如说下面的,我们就看做左右边已经弄好了,只要满足小问题(递归出口
-
),通过数学归纳法就能知道成立
-
ListNode left = sortList(head); ListNode right = sortList(tmp);
public ListNode sortList(ListNode head) {
if(head == null || head.next == null) {
return head;
}
ListNode slow = head;
ListNode fast = head.next;
//避免当剩余两个节点时,中间节点变成右边那个,这样会死递归!
//(打断链表更没有意义,右边链表是null,没用)
while(fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
ListNode tmp = slow.next;
slow.next = null; //打断链表
技巧1:平分链表(快慢指针在Java中应该是引用,或者下标)
- 快慢指针,慢指针应该走到中间偏前的一个位置【不然会死递归】
- 以两个节点为例子,快指针走两步,慢指针走一步,然后
slow.next = null
不就等于没有意义吗, 进入递归后,(左侧)依旧是两个节点,以此造成栈溢出!- 解决方法:
fast
先走一步,最终slow停在中节点偏前 - fast不先走一步,最终slow停在中节点偏后
- 解决方法:
- 以两个节点为例子,快指针走两步,慢指针走一步,然后
ListNode left = sortList(head);
ListNode right = sortList(tmp);
ListNode ret = new ListNode(0);
//临时头节点,最终不要即可,因为我们不能确定头结点是否是谁的
ListNode cur = ret;
while(left != null && right != null) {
if(left.val <= right.val) {
cur.next = left;
left = left.next;
}else {
cur.next = right;
right = right.next;
}
cur = cur.next;
}
cur.next = left != null ? left : right;
return ret.next;
}
-
技巧2: 给需要构造的链表提供一个起始节点,(就像珍珠需要有一个小石子,最终才能积累成珠)
ListNode ret = new ListNode(0);
这一句代码,提供一个带头链表- 目的是因为,我们不知道首节点是谁,毕竟尾入法在首节点需要判断
- 那我们不如直接给一个,最终不考虑进去就行了
-
技巧3:合并有序链表,不用多说
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆!