基础和技术就像无敌的深渊,小伙子,你要不断的学哟~~…
先来看看链表常用操作对应的时间复杂度:
特此鸣谢在leetcode上分享答案的各位大神,让我能够对自己的笔记有如下补充:
- 链表提供了高效的
节点重排能力
,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度,换句话说就是增删操作效率很高
。
/**
*多个listNode可以通过prev和next两个指针组成双端链表,
*/
typedef struct listNode {
//前置节点,相当于前指针
struct listNode * prev;
//后置节点,相当于后指针
struct listNode * next;
//节点的值
void * value;
}listNode;
不管刷题多少,链表常用操作或者说技巧有如下几点:
- 判空操作,不同题目具体分析
//官话判空,不同题目具体分析
if(head == null){...}
if(head.next == null){...}
- 递归操作(比如在反转链表中)
public ListNode reverseList(ListNode head){
......
ListNode last = reverseList(head.next);
......
}
- 数组有迭代遍历,我链表也可以呀
void traverse(ListNode head){
for(ListNode cur = head; cur != null; cur = cur.next){
......
}
}
- 虽然经常返回dummy.next,但是很多时候会(合并两个、K个有序链表时)
ListNode dummy = new ListNode();
ListNode cur = dummy;
...
//运算过程中cur代替dummy进行步进等操作
...
return dummy.next;
- 步进的方式一定要注意,比如合并两个有序链表中,
while(...!=null && ... != null ){
if(xxx.val > xx.val){
cur.next = xx;//把小的视为cur(或者说dummy)的下一个节点,然后接在cur(或者说dummy)的屁股后面
xx = xx.next;//步进
} esle{
cur.next = xxx;//小的接在dummy或者说cur的屁股后面
xxx = xxx.next;//步进
}
cur = cur.next;//屁股自己也得步进呀
}
- 双指针,同向的,快慢的等等。
- 快慢指针一般初始化时都指向链表的头节点head
ListNode fast = head;
ListNode slow = head;
//除此之外可以构造一个dummy节点,然后步进时两个指针一前一后
常见的题目比如寻找或者删除在单链表的倒数第K个节点、链表成环问题、寻找环的起点等
- 比如说,咱们**
从前向后想找数组中第某个元素,比如(有时会说链表中第K个节点、第n个节点、第x个节点),可以通过迭代(**一个for循环或者一个while循环**找到,虽然效率有点低)直接找到,那咱们想找到链表中倒数第某个节点呢(有时候也会说找、倒数第K个节点、倒数第n个节点、倒数第x个节点
**…,都是一个德行)- 那倒数第K个节点不就是正数第n-k个节点嘛 ,思路都是一样的
- 找某个节点有一种简单的思路就是利用咱们上面说的双指针思想。
- 我先找一个指针point1指向链表的头节点head,然后point1指针走K步。
- 接着等上面第一个指针走了K步之后,我让第二个指针指向头指针,
- 然后开始让这俩指针同时走,等第一个指针走到链表末尾后,第二个指针刚好走了N-k步(N代表链表的总长度,K代表第K个节点),那是不是说明第二个指针正好走到了倒数第K个节点处呢。
- 接着等上面第一个指针走了K步之后,我让第二个指针指向头指针,
//返回链表的倒数第K个节点
ListNode findFromListNode(ListNode head, int k){
//官话判空
if(head == null && head.next == null){
return null;
}
ListNode point1 = head;
//point1指针先走K步
for(int i = 0; i < k; i++){
if(point1 != null){
point1 = point1.next;point1指针步进,向前走,一步一步走
}else{
//达不到k步说明链表过短,没有倒数k
return null;
}
}
//搞个第二个指针,两个指针相当于同时走n-k步
ListNode point2 = head;
while(point1 != null){
point2 = point2.next;
point1 = point1.next;
}
//point2现在就指向的是咱们倒数第K个节点,直接返回这个货即可
return point2;
}
针对于这道寻找链表中倒数第K个节点,有个大佬点醒了我这个green bird这样一句话,与诸君共勉:
然后呢,有时候咱们找到了第任意个节点(倒数第K个节点)后,人家让咱们删除这个节点并返回链表的头节点。
上面咱们不是已经找到了吗,那就按照链表删除节点的思路,删一波呗
......
head.next = head.next.next;//就相当于删除了链表head.next这个节点
此时咱们是不是直接可以用上面寻找倒数第K个节点这个方法了嘛。
public ListNode removeNthFromEnd(ListNode head, int n) {
//这就和上面那个ListNode dummy = new ListNode();ListNode cur = dummy;其实是一个道理,都是用来保证安全的。
//dummy嘛,找一个虚拟的头节点(head,head说,那我是啥,你是实的呗),返回的时候就把dummy返回就行了,//删除倒数第n个之前要先找到倒数第n+1个节点,获得他的引用,因为你要把指针指向第n+1个节点从而实现删除第n个节点,这不也就看出来了为什么要搞出一个dummy节点,因为你只用head的话,按照咱们算法逻辑应该要先找到倒数第6个节点,哪有倒数第六个节点,第一个前面是空的,但是dummy的出现就有倒数第六个节点了么,不就保证安全了嘛,不会突然蹦出来一个空指针异常啥的
ListNode dummy = new ListNode(-1);
dummy.next = head;
//这里要传入一个n + 1的原因是,咱们要删除第n个节点或者说下一个节点,咱们需要head这个头指针(或者说代替head步进的dummy指针指向下下一个节点就可以实现删除下一个节点这件事,所以咱们得这样写)
ListNode tempNode = findFromListNode(dummy, n + 1);//删除倒数第n个节点
tempNode.next = tempNode.next.next;
return dummy.next;
}
//返回链表的倒数第K个节点
ListNode findFromListNode(ListNode head, int k){......}
- 除了找第某个节点、倒数第K个节点以及删除这个节点,
- 还有就是人家不让咱找第某个节点,人家让找和某个值val相等的节点,然后删除等,该在弄呢,方法很多,咱们就依旧统一口径—双指针
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteNode(ListNode head, int val) {
//相当于刚开始头指针指的第一个元素就是我要删除的val,那我返回head.next,相当于忽略了第一个元素,还不就是相当于把第一个元素从我的链表中删除了嘛
if(head.val == val){
return head.next;
}
ListNode point1 = head;
ListNode point2 = head;
while(point1 != null && point1.val != val){
//没找到指定元素val,就要保证point1指针指向的节点是point2指针指向的下一个节点,那可不就是,你指向我的屁股,我向前一步,这个目标不就成了嘛
point2 = point1;
point1 = point1.next;
}
//上面while循环没捕捉到m,执行到这里就说明point1.val = val了
if(point1.val == val){
//本来point2指的下一个节点是point1,然后咱们现在不是已经找到该删除的节点(就是point1正指的那个节点呗),然后删除操作和之前一样,咱们之前删除都是head.next = head.next.next;相当于把head.next指的节点给删除了呀,这里一样的。或者这样说,如果判断到这个if(point1.val == val)里面后,此时point2之前A,point1指向A.next,也就是A节点的下一个节点,此时相当于point1指向的A的下一个节点就是咱们要删除的节点,那我直接point2.next,相当于把point2的指针指向point1的next,相当于直接让point2指向A的下一个节点的下一个节点,这不就相当于把A的下一个节点也就是point1正在指向的节点给删除了嘛,忽略不就相当于给删除了嘛
point2.next = point1.next;
}
return head;
}
}
- 还有就是双指针还可以解决判断链表中有没有环呀?,环的起点是哪个呀?—**快慢指针(比如求链表中点、是否成环、环的起点等都可以用快慢指针)**往上怼
- 比如求一波链表中点呗
- 让两个指针分别指向题目给的两个链表的头节点head,然后呢慢指针步进一步快指针就步进两步,
- 当快指针走到链表末尾时慢指针刚好走到链表中点
- 比如求一波链表中点呗
/**
* 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; }
* }
*/
ListNode middleNode(ListNode head){
//快慢指针初始化指向head
ListNode slow = head, fast = head;
//快指针走到末尾时停止
while(fast != null && fast.next != null){
//慢指针走一步,快指针走两步
slow = slow.next;
fast = fast.next.next;
}
//慢指针指向中点
return slow;//当链表长度为偶数时,也就是说中点有两个时,快慢指针这种解法中的慢指针返回的节点是靠后的那个节点。
}
- 再比如就是链表是否成环呀、环起点是哪个呀
- 判断单链表是否成环时咱们拍脑袋想到的暴力解法就是用一个HashSet等集合来缓存一下节点走过的路,然后遇到重复的不就说明有环对吧。但是呢咱们用双指针时可以稍微省一下空间
- 你看咱们上面求链表中点时
//说明当快指针走到链表最后那个null节点时,慢指针肯定刚好走到中间节点上
while(fast != null && fast.next != null){......}
这里成环问题和环起点问题是一样的,要是快慢指针相遇了说明有环(快指针刚好超过慢指针一圈,或者说第一次相遇时慢指针slow走了k步,fast肯定走了2k步,多走的k步其实就是fast指针在环中转圈圈转的圈数
,也说明k这个值就是环长度的整数倍)—就像咱们上面那个两个链表相交的问题,冥冥之中(若相交、若有环)这两个指针总会相遇的哦
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
boolean hasCycle(ListNode head){
//快慢指针初始化指向head
ListNode slow = head,
ListNode fast = head;
//快指针走到末尾时停止
while(fast != null && fast.next != null){
//慢指针走一步,快指针走两步
slow = slow.next;
fast = fast.next.next;
//快慢指针相遇,说明含有环
if(slow == fast){
return true;
}
}
//不包含环
return false;
}
- 寻找环的起点,感觉和那个先寻找倒数第K个节点,找到了之后再删除倒数第K个节点,都是有后续操作的,倒数那个咱直接是调封装好的寻找倒数第K个节点的方法,然后寻找到后续节点也就是要删除的节点的后一个节点(咱们删除要用到这个节点呀),进行删除,那这个寻找环的起点问题呢?咱们看看是不是调用前面封装好的方法或者是和前面判断是否成环有什么异同点
//为了严谨,其实上面这个也应该有
if(fast == null || fast.next == null){
//fast遇到空指针说明没有环,官话判空
return null;
}
如果求环的起点不写这个官话判空,就会出现
//快慢指针初始化指向head
ListNode slow = head,
ListNode fast = head;
//快指针走到末尾时停止
while(fast != null && fast.next != null){
//慢指针走一步,快指针走两步
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
//快慢指针相遇,说明含有环
if(slow == fast){
//return true;//上面判断成环时这里是直接返回true的,但是现在题目很明显让咱们返回的是一个链表节点呀,所以这里应该写其他的
break;
}
}
当快慢指针第一次相遇了说明是有环的,
…
相遇了。
然后呢,**咱们让慢指针又重新指向头节点,此时让两个指针同时以相同的步伐向前步进,**当第二次相遇时就说明相遇点是环的起点,不信自己走走
- 其中就是假设第一次相遇时慢指针走了K步快指针走了nK步,那么我让慢指针归位后再让他们俩继续同步伐走(此时就不用管你快指针在圈里多转了多少圈了,就都视为走了K步吧,那…),他们俩最后都会少走相同的路段从而相遇
所以上面那个当第一次相遇后,直接break,跳出即可
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
......
if(fast == null || fast.next == null){
//fast遇到空指针说明没有环,官话判空
return null;
}
//随便选一个,这里咱们选slow指向头节点
slow = head;
//快慢指针同步前进,相交点就是环起点
while(slow != fast){
fast = fast.next;
slow = slow.next;
}
return slow;
//完整代码:
public class Solution {
public ListNode detectCycle(ListNode head) {
if (head == null) {
return null;
}
ListNode slow = head, fast = head;
while (fast != null) {
slow = slow.next;
if (fast.next != null) {
fast = fast.next.next;
} else {
return null;
}
if (fast == slow) {
ListNode ptr = head;
while (ptr != slow) {
ptr = ptr.next;
slow = slow.next;
}
return ptr;
}
}
return null;
}
}
//法二
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode pos = head;
Set<ListNode> visited = new HashSet<ListNode>();
while (pos != null) {
if (visited.contains(pos)) {
return pos;
} else {
visited.add(pos);
}
pos = pos.next;
}
return null;
}
}
- 自己和自己相交那叫成环,那自己和别人相交,也可以求公共交点。见下:
拿到这个题个人第一反应就是,来一个单列集合中的无序不可重复的Set接口的子实现类,把题目给的两条链表从头到尾遍历一遍,记录一下元素出现的次数…哎,既然前面已经知道了链表题目这里最常使用双指针,那何不先上双指针,试一试再说。
- 具体思路就是,
- 搞两个指针分别指向题目所给的两个头节点,然后让这两个指针分别在两条链表上前进
- 然后呢,让第一个指针遍历完第一条链表之后接着开始从头接着遍历另一条链表;同样的,让第二个指针遍历完第二条链表之后接着从头开始遍历第一条链表,
相当于逻辑上将两条链表合并起来了
。 因为啥呢,就一句话,你让两个指针都这样玩(逻辑上将两条链表拼接成为一条之后),他俩指针有那么一刻总会两个都进这个相交的公共区间段(由相同的数字及组成的类似的段)了
- 上面是逻辑上合并,啥叫物理上合并呢,见两个有序数组合并、两个(K个)有序链表合并。咱们合并两个有序链表时,咱们不是搞了个dummy节点然后又搞了个cur指向dummy,然后比较,每次将最小的节点接到dummy(或者cur)的屁股后面,步进步进再步进,最后再判断一下把剩下的重复的节点接到dummy屁股后面;合并K个就是需要来个优先级队列…
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//正常初始化
ListNode p1 = headA;
ListNode p2 = headB;
//如果p1 == p2不就说明他俩一块进入公共区间段了,当还在while循环中游玩,说明p1和p2指针还没有双双牵手进入公共区间段,那就各自步进轮流扫,进到双双进入公共相交区间段为止
while(p1 != p2){
//p1 == null说明p1指针把自己那个链表扫完了,该接着扫人家另一条链表了
if(p1 == null){
p1 = headB;
}else{//说明p1指针把自己那个链表还没扫完,就别想其他的,先把自己门前雪扫完再说
p1 = p1.next;//步进
}
//同样的,p2 == null说明p2指针把自己那个链表扫完了,该接着扫人家另一条链表了
if(p2 == null){
p2 = headA;
}else{//说明p1指针把自己那个链表还没扫完,就别想其他的,先把自己门前雪扫完再说
p2 = p2.next;//步进
}
}
return p1;
}
}
除了上面,光玩双指针后,链表这里也有回文链表,看看下面:
- 判断一个单链表是不是回文链表:
思路就是把原始链表反转存于一条新的链表中,然后比较这两条链表是否相同。
- 回文的话就多了,不单单是回文链表,还有子串,比如判断一个字符串是不是回文串、最大回文子串、回文字串的个数、回文字串的长度等,除了子串,还有回文子序列、回文子数组等(回文相关题目在leetcode可不止这几道,随便搜索就很多:),去其他文章里面翻翻哦,惊喜连连。
- 肯定也不止这些,回文串(正着读和反着读都一样的字符串,比如aba和abba就是回文串,abac就不是回文串)问题。注意回文串的长度可能是奇数也可能是偶数,解决思路就是双指针、动态规划、中心扩散等。
- 回文的话就多了,不单单是回文链表,还有子串,比如判断一个字符串是不是回文串、最大回文子串、回文字串的个数、回文字串的长度等,除了子串,还有回文子序列、回文子数组等(回文相关题目在leetcode可不止这几道,随便搜索就很多:),去其他文章里面翻翻哦,惊喜连连。
/**
* 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 {
ListNode left;
public boolean isPalindrome(ListNode head) {
//左侧指针
left = head;
return traverse(left);
}
public boolean traverse(ListNode right){
if(right == null){
return true;
}
//迭代,一步两步
boolean result = traverse(right.next);
result = result && (right.val == left.val);
//或者这样写能更明白一点:
//boolean result = (traverse(right.next)) && (right.val == left.val);
left = left.next;
return result;
}
}
其中部分代码是借鉴反转整个链表:
/**
* 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 reverseList(ListNode head) {
//递归函数的base case,也相当于链表的官话判空
if(head == null){
return null;
}
if(head.next == null){
return head;//相当于链表只有一个节点时再怎么反转也是他自己,就直接返回自己即可
}
ListNode last = reverseList(head.next);
head.next.next = head;//反转链表节点之间的指针
head.next = null;//当链表递归反转之后,新的头节点变成了last,head变成了新链表的最后一个节点,别忘了链表的末尾指针要指向null,所以才有z了这一句代码
return last;
}
}
在题库的解题答案中看到有位大神把反转链表泛化了一下,现在不是反转整个链表嘛,这个大神整理的是反转以dog开头的节点或者说以dog为头节点的链表,或者最准确的说法是反转dog节点到最后一个null之间的节点。
//把这个dog换成head就是反转单链表的程序
ListNode reverseDogToLast(ListNode dog){
ListNode prev = null;//代表当前节点的前一个节点
ListNode cur = dog;//代表当前节点
ListNode next = dog;//代表当前节点的下一个节点
while(cur != null){
next = cur.next;
cur.next = prev;//当前节点的指针指向自己的前一个节点prev,不就和咱们那个head.next.next = head是一个道理的。
prev = cur;//步进前一个节点
cur = next;//步进当前节点
}
return prev;
}
再泛化一下,我不反转dog节点到最后一个null之间的节点,反转dog节点到cat节点之间的链表那一段
就是把上面dog到null的终止条件改一下就行了呗
ListNode reverseDogToCat(ListNode dog, ListNode cat){
ListNode prev = null;
ListNode cur = dog;
ListNode next = dog;
//改一下while处的终止条件
while(cur != cat){
next = cur.next;
cur.next = prev;//当前节点的指针指向自己的前一个节点prev,不就和咱们那个head.next.next = head是一个道理的。
prev = cur;//步进前一个节点
cur = next;//步进当前节点
}
return prev;
}
当然啦,反转链表也有很多相类似的体型咯
- 我不让你反转整个链表,我让你把整个链表其中的一小段,比如从left到right给咱反转一下----不就是从dog到cat嘛
先用一个for循环找到第m个位置,然后再用一个for循环将m和n之间的元素反转。迭代法的空间复杂度为O(1)
- 有了上面这个,那么如果让咱反转前N个节点,就稍微好想一点了----管他呢,咱们就双指针先暴力往上怼,试试再说。
//也就是反转以head开头的或者说以head为起点的n个节点,并返回新的头节点
public ListNode reverseOneToN(ListNode head, int n){
//if(n == 1){...}
//或者if(head.next == null){...}一个道理,都是说明链表中若只有一个节点,你咋玩,只能return head喽
ListNode nodeNPlusOne = null;
if(head.next == null){
nodeNPlusOne = head.next;
return head;
}
//那就成了。以head.next为起点需要反转前n - 1 个节点
ListNode last = reverseOneToN(head.next, n - 1);//咱们删除倒数第K个节点时这里也用的是这样的迭代,只不过那个返回的是第n + 1个节点,也就是后一个节点,方便咱们删除,但是0到N和1到n+1不就是一个道理嘛。
head.next.next = nodeNPlusOne;//
return last
}
- 那么如果让咱们链表中K个一组反转节点
- 先递归反转 以head开头的K个节点。
- 再将K+1作为head递归调用反转函数
- …
- 然后将上面众多个结果,也就是小链们串起来合起来(其实说起来这个和归并排序,有点像,找好点分好组再一组一组排好序,再把组们合并起来串起来)
ListNode reverseKGroup(ListNode head, int k){
//先官话判空呗
if(head == null){
return null;//空的再反转他还是空的
}
ListNode right = head;
for(int i = 0; i < k; i++){//觉不觉得熟悉,咱们找链表倒数第K个节点时以及找到链表倒数第K个节点并删除并返回头节点的那两道题中也用了这个for循环哟
if(right == null){
return head;//由point1和point2组成一个[point1, point2)区间,如果此时区间内元素不足K个,直接返回head,都不符合条件我反转个啥
}
right = right.next;//区间右边界步进,像不像那个滑动窗口呀
}
//反转[point1, point2)区间内的元素
ListNode left = head;
ListNode firstGroupNode = reverseLeftToRight(left, right);
//递归反转后,将小链表们串起来。因为把前一段链表反转好之后这个left就是头节点嘛,相当于是前一小段链表的最后一个节点,然后这个最后一个节点刚好指向下一段反转好的链表,不就连起来了
left.next = reverseKGroup(right, k);
return firstGroupNode;
}