红色:四个,三个。蓝色:两个。黑色:一个。
推荐练习:代码随想录:203,707,206,24,19,142
黑马:206,203,19,83,82,21,23,876,234
吴师兄:24,160,203,19,21,328,92,125,680,292
谷歌师兄:206,21,24,160,234,83,328,19,148
(偷懒技巧:要么双指针迭代要么递归)
一、基本概念。每个元素指向下一个元素,元素存储上不连续。
在做链表的题时,先学习一下链表的递归遍历和递归。
链表可以被定义为一个节点(头节点),后面跟着一个较小的链表(剩余部分)。在链表中,每一个节点都有一个指向下一个节点的指针,这意味着链表的剩余部分本身也是一个链表。这种自我引用的性质使得链表天然具有递归的特性。
递归是编程中一种非常重要的概念。在一个递归过程中,函数直接或间接地调用自身,这样可以将复杂问题分解为更小、更易处理的子问题。如果这些子问题与原始问题具有相同的结构,并且更容易解决,那么就可以通过解决子问题来解决原始问题。
递归通常包含两个基本部分:
-
基本情况(Base Case):这是递归结束的情况,通常是问题规模最小时的解决方案。在设计递归函数时,首先要明确基本情况,防止无限递归。
-
递归情况(Recursive Case):在递归情况中,问题被分解为更小的子问题。然后,函数调用自身来解决这些子问题。
1.自己调用自己。2.每次调用函数处理的数据较上次缩减。3.内层函数调用完成外层才完成。
void f (Listnode node)
if(node==null){
return;
}
println(Listnode.val);
f(node.next);
println(Listnode.val);
}
(可重复看视频P41,有助于理解return和递归语句和整体的关系。)
如果想按高中的方法来思考的话感觉下面这个可能有启发,curr.next相当于n+1;都用同一个函数调用会不会有f(n)=f(n+1)的感觉,然后都执行了sout。
然后来观察一个现象
打印写在递归之前就是1234,写在递归之后就是4321,原因应该就是“递归的实质就是栈”
接下是递归流程
其实自己主要不明白的是return后的流程:底层运行结束之后就会到函数调用的位置继续向下执行。然后每一层运行结束就会自动返回上一层递归调用的位置继续执行。
再看一个图来理解递归
只要知道第一个数就可以了,即f(1)。像不像链表里的第一个节点的返回值,至于之间的关系就像这个式子里n其实在每一步都是已知的。链表里cur和cur。next的关系就像n和n+1的关系,后者的关系由函数来描述,前者则由代码return等等来描述。
以下是求阶乘的递归P42
public class Factorial {
public int f(int n) {
if (n == 1) {
return 1; //递归结束条件,同时是递归的最内层
}
return n * f(n - 1);//递归函数
}
}
f(int n =3) {
return 3 * f( int n = 2){
return 2 * f( int n = 1){
if (n == 1) {
return 1;
}
}
}
}
以上是递归求阶乘步骤的伪代码。返回值返回到调用它的地方。
public void f (int n,String str){
if (n==str.length()){
return;
}
f(n+1,str);
System.out.println(str.charAt(n));
}
以上是反向打印字符串的代码。这段代码也可以写成下面的样子
public void f (int n,String str){
if (n==str.length()){
return;
}
n=n+1;
f(n,str);
System.out.println(str.charAt(n-1));
}
二、leetcode题目
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
迭代:
class Solution {
public ListNode reverseList(ListNode head){
ListNode n1 = null;
ListNode o1 = head;
while(o1!=null){
ListNode o2 = o1.next;
o1.next=n1;
n1 = o1;
o1=o2;
}
return n1;
}
}
思路:总体思路:
1.定义一个链表头节点为n1,原来链表头节点为o1,
2.将原来的节点一个一个接在新链表上,通过while循环来实现。
3.用o2记录下下一个要操作的节点
4.o1.next=n1链接到新链表,n1 = o1让刚链接上的o1作为n1,o1=o2让刚刚记录下的o2作为o1继续被操作(即移动o1),然后开始listnode o2这个循环。
solution2(先不看)
class Solution {
public ListNode reverseList(ListNode head){
ListNode n1=null;
ListNode cur = head;
while (cur!=null){
n1= new ListNode(cur.val,n1);
cur = cur.next;
}
return n1;
}
}
递归:
class Solution {
public ListNode reverseList(ListNode head){
if (head==null||head.next==null){
return head;
}
ListNode last = reverseList(head.next);//每次调用的时候找他的下一个节点
head.next.next=head;
head.next=null;
return last;
}
}
首先找到最后一个节点,作为反转后的首节点。理解这个可以看下图
上面的代码实现了返回最后一个节点5.
然后看下图
ListNode last = reverseList(head.next)返回最后一个节点作为头节点,下面两行代码则使得相邻两个节点逆序。
2.203. 移除链表元素 根据值删除节点
给你一个链表的头节点 head
和一个整数 val
,请你删除链表中所有满足 Node.val == val
的节点,并返回 新的头节点 。
迭代
class Solution {
public ListNode removeElements(ListNode head, int val) {
ListNode dummyhead = new ListNode(-1,head);
ListNode left = dummyhead;
ListNode right = head;
while(right!=null){
if(right.val == val){
left.next = right.next;//删除,p2向后平移
}else{
left= left.next;//p1,p2向后平移
}
right = right.next;
}
return dummyhead.next;
}
}
思路:
1.定义一个dummyhead。
2.链表里要删除一个节点就要知道它的上一个节点。所以定义两个指针。right用于遍历链表。left指向被删除节点的上一个节点。
3.如果right等于目标,将这个节点删掉,right向后平移,注意此时left位置保持不变(因为是right来遍历链表检查是否需要移除)right不等于目标,right向后平移。因为删除了一个节点,所以其实还是相差一个身位。
4.right不等于目标,left,right向后平移。left=left.next left = right 其实都能解释通。前者好一些。
5.p2等于null的时候退出循环。
链表移除元素和数组移除元素有什么相似的地方
-
双指针策略:在两种情境中,我们都可以使用双指针策略。在链表问题中,我们使用了一个指针来遍历链表并检查每个节点,另一个指针则用于跟踪前一个节点以便进行删除操作。在数组问题中,我们可以使用两个指针,一个用于遍历数组,另一个用于跟踪下一个应该写入的位置。
-
“跳过”不需要的元素:在两个问题中,我们都是通过修改指针或索引的连接/引用来“跳过”或移除不需要的元素,而不是真正地删除它们。
递归
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head==null){
return null;
}
if(head.val==val){
return removeElements(head.next,val);
}else {
head.next=removeElements(head.next,val);
return head;
}
}
}
下面这种递归写法更好理解
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head==null){
return null;
}
if(head.val==val){
head.next=removeElements(head.next,val);
return head.next;
}else {
head.next=removeElements(head.next,val);
return head;
}
}
}
class Solution {
public ListNode removeElements(ListNode head, int val) {
if(head==null){
return null;
}
head.next=removeElements(head.next,val);
if(head.val==val){
return head.next;
}else {
return head;
}
}
}
Leetcode19删除链表倒数第N个节点
迭代的想法比较巧妙,其中需要注意到底要循环几次。
ListNode dummyHead = new ListNode(-1, head);
ListNode left = dummyHead;
ListNode right = dummyHead;
for (int i = 0; i < n + 1; i++) {
right = right.next;
}
while (right != null) {
right = right.next;
left = left.next;
}
left.next = left.next.next;
return dummyHead.next;
其实也可以求出链表的长度。
思路:
由于我们需要找到倒数第 n 个节点,因此我们可以使用两个指针 first 和 second同时对链表进行遍历,并且 first比 second 超前 n个节点。当 first 遍历到链表的末尾时,second 就恰好处于倒数第 n 个节点。
递归
当需要对第一个节点进行操作的时候往往需要dummyHead。
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyHead = new ListNode(-1,head);
recursion(dummyHead,n);
return dummyHead.next;
}
private int recursion(ListNode head, int n){
if(head==null){
return 0;
}
int nth = recursion(head.next,n);//null的时候返回的值是0,所以代表了下一次迭代的时候的返回值
if(nth==n){
head.next=head.next.next;
}
int x=nth+1;
return x;//本次节点的返回值
}
思路:
每一个值返回自己是倒数第几个节点然后由上一个节点来操作删除之。
这道题有点递归就是栈的意思了。
由最内层得知每一层返回的值是什么意思(即当前节点是倒数第几个)。所以nth的意思就是下一个节点是倒数第几个。
为什么要知道下一个节点是倒数第几个。因为要删除节点都是从这个要被删除的节点的上一个节点开始的。然后如果下一个节点就是要被删除的节点(通过if来判断)就通过代码将其删除。
然后返回自己这个节点对应的值,即之前思考的数学中n和n+1的关系在代码里会通过return等途径实现。
Leetcode83删除有序链表重复节点
迭代:(对比看203根据值删除节点)
现在我自己会写了
class Solution {
public ListNode deleteDuplicates(ListNode head) {
ListNode cur = head;
while (cur != null && cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
}
if(head==null||head.next==null){
return head;
}
ListNode p1= head;
ListNode p2= head.next;
while (p2!=null){
if(p2.val==p1.val){
p1.next=p2.next;
p2=p1.next;
}else{
p1=p1.next;
p2=p2.next;
}
}
return head;
if(head==null||head.next==null){
return head;
}
ListNode p1= head;
ListNode p2;
while ((p2=p1.next)!=null){
if(p2.val==p1.val){
p1.next=p2.next;
}else{
p1=p1.next;
}
}
return head;
递归
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head==null||head.next==null){
return head;
}
head.next=deleteDuplicates(head.next);
if(head.val==head.next.val){
return head.next;
}else{
return head;
}
}
}
82.有序链表去重
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null){
return head;
}
ListNode dummyHead = new ListNode(-1,head);
ListNode p = dummyHead;
ListNode p1 = head;
ListNode p2 = p1.next;
while(p1 != null && p2 != null) {
if (p1.val == p2.val) {
while (p2 != null && p1.val == p2.val) {
p2 = p2.next;
}
p.next = p2;
} else {
p.next = p1;
p = p.next;
}
p1 = p2;
if (p2 != null) {
p2 = p2.next;//这三行相当于是从if和else里面给提了出来
}
}
return dummyHead.next;
}
递归
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if (head == null || head.next == null) {
return head;
}
if (head.val == head.next.val) {
ListNode x = head.next.next;
while (x != null && x.val == head.val) {
x = x.next;
}
return deleteDuplicates(x);
} else {
head.next = deleteDuplicates(head.next);
return head;
}
}
}
Leetcode21
public ListNode mergeTwoLists(ListNode list1, ListNode list2){
ListNode dummyHead = new ListNode(-1,null);
ListNode n = dummyHead;
while(list1!=null&&list2!=null){
if(list1.val<list2.val){
n.next=list1;
list1=list1.next;
}else{
n.next=list2;
list2=list2.next;
}
n=n.next;
}
if(list1==null){
n.next=list2;
}
if (list2==null){
n.next=list1;
}
return dummyHead.next;
}
递归
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if(list1==null){
return list2;
}
if (list2==null){
return list1;
}
if(list1.val<list2.val){
list1.next=mergeTwoLists(list1.next,list2);
return list1;
}else{
list2.next=mergeTwoLists(list1,list2.next);
return list2;
}
}
}
Leetcode23
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0){
return null;
}
int i = 0;
int j = lists.length-1;
return split(lists,i,j);
}
//返回合并后的链表
private ListNode split(ListNode[] lists,int i,int j){
if(i==j){
return lists[i];
}
int m = (i+j)>>>1;
ListNode left = split(lists, i,m);
ListNode right = split(lists,m+1,j);
return mergeTwoLists(left,right);
}
private ListNode mergeTwoLists(ListNode left,ListNode right){
if(left==null){
return right;
}
if (right==null){
return left;
}
if(left.val<right.val){
left.next=mergeTwoLists(left.next,right);
return left;
}else {
right.next = mergeTwoLists(left, right.next);
return right;
}
}
}
Leetcode876 找中间点
class Solution {
public ListNode middleNode(ListNode head) {
ListNode p1=head;
ListNode p2=head;
while (p2!=null&&p2.next!=null){
p1=p1.next;
p2=p2.next;
p2=p2.next;
}
return p1;
}
}
Leetcode234 判断回文链表,用到了之前找中间点和反转链表的知识。
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode half = middle(head);
ListNode f = reverse(half.next);
while(f !=null){
if(f.val!=head.val){
return false;
}
f =f .next;
head=head.next;
}
return true;
}
private ListNode middle(ListNode head){
if(head==null ||head.next==null){
return head;
}
ListNode p1 = head;
ListNode p2 = head.next;
while(p2!=null&&p2.next!=null){
p1=p1.next;
p2=p2.next.next;
}
return p1;
}
private ListNode reverse(ListNode head){
ListNode n1 = null;
ListNode o1=head;
while(o1!=null) {
ListNode o2 = o1.next;
o1.next = n1;
n1 = o1;
o1 = o2;
}
return n1;
}
}
Leetcode24
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路:
用一个cur节点来遍历,位于需要交换的两个节点的前面一位节点。
firstnode节点和secondnode节点每次交换都需要根据cur来重新定义
cur = firstNode;这句话要注意,交换完之后cur最后是落在哪一个节点上。
中间的顺序是可以交换,但是不能任意交换。为了方便记忆就用下面这个顺序,即相对于两个节点后面那个节点temp(cur.next.next)来说从后向前归位。
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummyhead = new ListNode(-1,head);
ListNode cur = dummyhead;//用于遍历的节点
ListNode firstNode;//每次交换的第一个节点,每次需要重新定义
ListNode secondNode;//每次交换的第二个节点,每次需要重新定义
while ((firstNode=cur.next)!=null&&(secondNode=cur.next.next)!=null){
firstNode.next =secondNode.next;
secondNode.next = firstNode;
cur.next = secondNode;
cur = firstNode;
}
return dummyhead.next;
}
}
if(head==null||head.next==null){
return head;
}
ListNode newHead = head.next;
head.next=swapPairs(newHead.next);
newHead.next=head;
return newHead;