代码随想录的Java算法学习笔记
单链表
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。
链表的入口节点称为链表的头结点也就是head。
双链表
单链表中的指针域只能指向节点的下一个节点。
双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
双链表 既可以向前查询也可以向后查询。
循环链表
循环链表,顾名思义,就是链表首尾相连。
循环链表可以用来解决约瑟夫环问题。
链表的虚拟头结点为方便操作,返回时需要省略其虚拟头结点
链表节点定义
public class ListNode {
//结点的值
int val;
//下一个结点
ListNode next;
//节点的构造函数(无参)
public ListNode() {
}
//节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
//节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
1.移除链表元素
时间复杂度 O(n)
空间复杂度 O(1)
import java.util.*;
//测试
public class test01 {
public static void main(String[] args) {
/*题意:删除链表中等于给定值 val 的所有节点。
示例 1:输入:head = [1,2,6,3,4,5,6], val = 6 输出:[1,2,3,4,5]
示例 2:输入:head = [], val = 1 输出:[]
示例 3:输入:head = [7,7,7,7], val = 7 输出:[]*/
ListNode head = new ListNode();
ListNode cur = head;
List<Integer> list =Arrays.asList(1,2,3,4,5,6,7,7,7);
//循环添加
for (int i = 0; i < list.size(); i++) {
cur.next = new ListNode(list.get(i));
cur = cur.next;
}
cur=head.next;
ListNode result = removeElements(cur, 7);
//遍历输出
while (result != null) {
System.out.print(result.val + " ");
result = result.next;
}
}
public static class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
/**
* 添加虚节点方式
* 时间复杂度 O(n)
* 空间复杂度 O(1)
*
* @param head
* @param val
* @return
*/
public static ListNode removeElements(ListNode head, int val) {
if (head == null) {
return head;
}
// 因为删除可能涉及到头节点,所以设置dummy节点,统一操作
ListNode dummy = new ListNode(-1, head);
ListNode pre = dummy;
ListNode cur = head;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return dummy.next;
}
}
/*
输出结果:
1 2 3 4 5 6
*/
2.设计链表
//测试
public class test02 {
public static void main(String[] args) {
/*题意:
在链表类中实现这些功能:
1.get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
2.addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。
插入后,新节点将成为链表的第一个节点。
3.addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
4.addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。
如果 index 等于链表的长度,则该节点将附加到链表的末尾。
如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
5.deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
*/
List<Integer> list = Arrays.asList(1, 2, 34, 5, 6, 7);
MyLinkedList linkedList1 = new MyLinkedList();
MyLinkedList linkedList2 = new MyLinkedList();
//在链表最前面插入一个节点
for (Integer value : list) {
linkedList1.addAtTail(value);
}
System.out.println(linkedList1.get(2));//34
//在链表最后面插入一个节点
for (Integer integer : list) {
linkedList2.addAtHead(integer);
}
System.out.println(linkedList2.get(2));//5
//在第 index 个节点之前插入一个新节点
linkedList1.addAtIndex(-1, 520);
System.out.println(linkedList1.get(0));//520
linkedList1.addAtIndex(linkedList1.size, 520);
System.out.println(linkedList1.get(linkedList1.size - 2));//7
System.out.println(linkedList1.get(linkedList1.size - 1));//520
//删除第index个节点
linkedList1.deleteAtIndex(linkedList1.size - 1);
System.out.println(linkedList1.get(linkedList1.size - 1));//7
}
//定义节点
public static class ListNode {
//节点的值
int val;
//下一个节点
ListNode next;
//节点的构造函数(无参)
public ListNode() {
}
//节点的构造函数(一个参数)
public ListNode(int val) {
this.val = val;
}
//节点的构造函数(两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
//设计链表
public static class MyLinkedList {
//size存储链表元素的个数
int size;
//虚拟头结点
ListNode head;
//初始化链表
public MyLinkedList() {
size = 0;
head = new ListNode(0);
}
//获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点
public int get(int index) {
//如果index非法,返回-1
if (index < 0 || index >= size) {
return -1;
}
ListNode currentNode = head;
//包含一个虚拟头节点,所以查找第 index+1 个节点
for (int i = 0; i <= index; i++) {
currentNode = currentNode.next;
}
return currentNode.val;
}
//在链表最前面插入一个节点,等价于在第0个元素前添加
public void addAtHead(int val) {
addAtIndex(0, val);
}
//在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加
public void addAtTail(int val) {
addAtIndex(size, val);
}
// 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。
// 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点
// 如果 index 大于链表的长度,则返回空
public void addAtIndex(int index, int val) {
if (index > size) {
return;
}
if (index < 0) {
index = 0;
}
size++;
//找到要插入节点的前驱
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
ListNode toAdd = new ListNode(val);
//完成插入
//加入的节点(toAdd)的next等于pred的下一个节点的存储位置
toAdd.next = pred.next;
//pred的next等于加入的节点(toAdd)的存储位置
pred.next = toAdd;
}
//删除第index个节点
public void deleteAtIndex(int index) {
if (index < 0 || index >= size) {
return;
}
size--;
if (index == 0) {
head = head.next;
return;
}
ListNode pred = head;
for (int i = 0; i < index; i++) {
pred = pred.next;
}
pred.next = pred.next.next;
}
}
}
3.反转链表
1.双指针
- 时间复杂度: O(n)
- 空间复杂度: O(1)
package com.mytest.mk5.ListNode;
import java.util.Arrays;
import java.util.List;
//测试
public class test03 {
public static void main(String[] args) {
//定义列表添加数据
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
//定义节点
ListNode head = new ListNode();
//定义节点用来添加数据
ListNode cur = head;
//增强for添加数据
for (Integer integer : list) {
cur.next = new ListNode(integer);
cur=cur.next;
}
//将cur指针返回到添加的第一个数据
cur=head.next;
//实现反转方法
ListNode result = reverseList(cur);
//输出结果
while (result != null) {
System.out.print(result.val+" ");
result = result.next;
}
}
//定义节点
public static class ListNode {
int val;
ListNode next;
public ListNode() {
}
public ListNode(int val) {
this.val = val;
}
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
public static ListNode reverseList(ListNode head) {
ListNode prev = null; // 前一个节点
ListNode cur = head; // 当前节点
ListNode temp; // 临时节点用于保存下一个节点
while (cur != null) {
temp = cur.next; // 保存下一个节点
cur.next = prev; // 将当前节点的next指向前一个节点,实现反转
prev = cur; // 更新前一个节点为当前节点
cur = temp; // 更新当前节点为下一个节点
}
return prev; // 返回反转后的头节点
}
}
/*输出结果:
5 4 3 2 1
*/
2.递归反转
- 时间复杂度: O(n), 要递归处理链表的每个节点
- 空间复杂度: O(n), 递归调用了 n 层栈空间
// 递归
class Solution {
public ListNode reverseList(ListNode head) {
return reverse(null, head);
}
private ListNode reverse(ListNode prev, ListNode cur) {
if (cur == null) {
return prev;
}
ListNode temp = null;
// 先保存下一个节点
temp = cur.next;
// 反转
cur.next = prev;
//通过递归更新prev、cur位置(传递参数)
//prev = cur; cur = temp;
return reverse(cur, temp);
}
}
/*
这段代码是使用递归来反转单链表的实现。
首先,定义了一个 reverseList 方法,该方法接受一个头结点 head 作为参数,并且返回反转后的链表的头结点。
在 reverseList 方法中,调用了一个私有方法 reverse,该方法用于实际执行链表反转操作。初始时,传入的 prev 参数为 null,表示反转后的链表的头结点的前一个节点为空。
在 reverse 方法中,首先进行终止条件判断,即如果当前节点 cur 为 null,表示已经遍历到链表的末尾,此时返回 prev,即反转后的链表的头结点。
接下来,定义一个临时节点 temp,用于保存当前节点 cur 的下一个节点。然后将当前节点 cur 的 next 指针指向前一个节点 prev,实现反转操作。
最后,通过递归调用 reverse(cur, temp),传入当前节点 cur 和下一个节点 temp,继续反转剩余的链表部分。
整个递归过程会一直执行到链表末尾,然后逐层返回,最终返回反转后的链表的头结点。
需要注意的是,在每次递归调用时,需要更新 prev 和 cur 的值,将 prev 更新为当前节点 cur,将 cur 更新为临时节点 temp,以便下一次递归调用时使用正确的参数。
这样,通过递归的方式,可以实现链表的反转操作。
*/
// 从后向前递归
class Solution {
ListNode reverseList(ListNode head) {
// 边缘条件判断
if(head == null) return null;
if (head.next == null) return head;
// 递归调用,翻转第二个节点开始往后的链表
ListNode last = reverseList(head.next);
// 翻转头节点与第二个节点的指向
head.next.next = head;
// 此时的 head 节点为尾节点,next 需要指向 NULL
head.next = null;
return last;
}
}
/*
这段代码是使用递归从后向前翻转单链表的实现。
首先,在 reverseList 方法中进行边缘条件判断。如果头结点 head 为 null,表示链表为空,直接返回 null。如果头结点 head 的下一个节点为 null,表示链表只有一个节点,无需翻转,直接返回头结点。
接下来,进行递归调用,传入头结点的下一个节点 head.next,即翻转从第二个节点开始往后的链表部分。将返回的结果保存在变量 last 中。
然后,将头结点 head 的下一个节点的 next 指针指向头结点 head,实现翻转头结点与第二个节点的指向关系。
接着,将头结点 head 的 next 指针指向 null,将头结点变为尾节点,确保翻转后的链表尾节点的 next 指向为 null。
最后,返回变量 last,即翻转后的链表的头结点。
通过这种从后向前的递归方式,可以实现链表的翻转操作。每次递归调用都会翻转当前节点与下一个节点的指向关系,直至遍历到链表的末尾,然后逐层返回,最终返回翻转后的链表的头结点。
在这段代码中,递归调用是通过 reverseList 方法本身来实现的。具体来说,递归调用发生在以下代码行:
ListNode last = reverseList(head.next);
在这行代码中,reverseList 方法被传入了当前头结点的下一个节点 head.next 作为参数进行调用。这样就实现了从后向前递归调用,翻转从第二个节点开始往后的链表部分。
通过递归调用,会一直向链表的末尾递归下去,直到遇到边缘条件的基准情况,即链表为空或只有一个节点的情况。然后逐层返回,将翻转后的链表的头结点逐个连接起来,最终返回整个翻转后的链表的头结点。
需要注意的是,每次递归调用中的 head 参数都是当前处理的节点,而 head.next 参数则是下一个节点。通过不断传递下去,递归调用会依次处理链表中的每个节点,实现链表的翻转操作。
*/
4.两两交换链表中的节点
1.递归
时间复杂度:O(n),其中 n 是链表的长度。由于我们需要遍历整个链表,递归地交换每一对节点,所以时间复杂度是线性的。
空间复杂度:O(n),其中 n 是链表的长度。空间复杂度取决于递归调用的栈空间。在最坏情况下,递归调用可能达到 n 个,代表 n 个节点的交换。因此,空间复杂度是线性的。
package com.mytest.mk5.ListNode;
import java.util.Arrays;
import java.util.List;
public class test04 {
public static void main(String[] args) {
/*
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
*/
ListNode cur = new ListNode();
List<Integer> list = Arrays.asList(1, 2, 3, 5);
//添加数据
cur = AddListNode(list, cur);
//两两交换其中相邻的节点
cur = swapPairs(cur);
//输出cur的值
while (cur != null) {
System.out.print(cur.val + " ");
cur = cur.next;
}
}
//定义节点
public static class ListNode {
int val;
ListNode next;
public ListNode() {
}
public ListNode(int val) {
this.val = val;
}
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
//定义添加列表数据进节点的方法(尾插法)
public static ListNode AddListNode(List<Integer> list, ListNode head) {
ListNode cur = head;
for (Integer val : list) {
cur.next = new ListNode(val);
cur = cur.next;
}
return head.next;
}
//使用递归实现两两交换
public static ListNode swapPairs(ListNode head) {
// base case 退出提交
if (head == null || head.next == null) return head;
// 获取当前节点的下一个节点
ListNode next = head.next;
// 进行递归
ListNode newNode = swapPairs(next.next);
// 这里进行交换
next.next = head;
head.next = newNode;
return next;
}
}
/*输出结果
*2 1 5 3
* */
2.迭代版本
- 时间复杂度:O(n)
- 空间复杂度:O(1)
public static ListNode swapPairs(ListNode head) {
ListNode dumyhead = new ListNode(-1); // 设置一个虚拟头结点
dumyhead.next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
ListNode cur = dumyhead;
ListNode temp; // 临时节点,保存两个节点后面的节点
ListNode firstnode; // 临时节点,保存两个节点之中的第一个节点
ListNode secondnode; // 临时节点,保存两个节点之中的第二个节点
while (cur.next != null && cur.next.next != null) {
temp = cur.next.next.next;
firstnode = cur.next;
secondnode = cur.next.next;
cur.next = secondnode; // 步骤一
secondnode.next = firstnode; // 步骤二
firstnode.next = temp; // 步骤三
cur = firstnode; // cur移动,准备下一轮交换
}
return dumyhead.next;
}
5.删除链表的倒数第N个节点
- 时间复杂度:O(n)
- 空间复杂度:O(1)
//测试
public class test05 {
public static void main(String[] args) {
/*
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
进阶:你能尝试使用一趟扫描实现吗?*/
ListNode cur = new ListNode();
List<Integer> list = Arrays.asList(1, 2, 3, 5);
int target = 1;
//添加数据
cur = AddListNode(list, cur);
ListNode result = removeNthFromEnd(cur, target);
while (result != null) {
System.out.print(result.val+" ");
result = result.next;
}
}
public static class ListNode {
int val;
ListNode next;
public ListNode() {
}
public ListNode(int val) {
this.val = val;
}
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
public static ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummyNode = new ListNode(); // 创建一个虚拟节点作为头节点
dummyNode.next = head; // 将虚拟节点的下一个节点指向实际的头节点
ListNode fastIndex = dummyNode; // 快指针从虚拟节点开始
ListNode slowIndex = dummyNode; // 慢指针从虚拟节点开始
// 快指针先移动n个节点,使快慢指针相差n个节点
for (int i = 0; i < n; i++) {
fastIndex = fastIndex.next;
}
// 同时移动快慢指针,直到快指针指向链表末尾
// 此时慢指针的位置就是待删除元素的前一个位置
while (fastIndex.next != null) {
fastIndex = fastIndex.next;
slowIndex = slowIndex.next;
}
// 跳过待删除元素,将慢指针的下一个节点指向下下个节点
slowIndex.next = slowIndex.next.next;
// 返回虚拟节点的下一个节点作为新的头节点
return dummyNode.next;
}
//定义添加列表数据进节点的方法(尾插法)
public static ListNode AddListNode(List<Integer> list, ListNode head) {
ListNode cur = head;
for (Integer val : list) {
cur.next = new ListNode(val);
cur = cur.next;
}
return head.next;
}
}
/*输出结果:
1 2 3
*/
6.链表相交
不知道怎么做链表相交(不想做),先放着吧
/*
给你两个单链表的头节点 headA 和 headB
请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。
(这有啥用?)
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
简单来说,就是求两个链表交点节点的指针。
注意,交点不是数值相等,而是指针相等。
* */
public static ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode curA = headA;
ListNode curB = headB;
int lenA = 0, lenB = 0;
while (curA != null) { // 求链表A的长度
lenA++;
curA = curA.next;
}
while (curB != null) { // 求链表B的长度
lenB++;
curB = curB.next;
}
curA = headA;
curB = headB;
// 让curA为最长链表的头,lenA为其长度
if (lenB > lenA) {
//1. swap (lenA, lenB);
int tmpLen = lenA;
lenA = lenB;
lenB = tmpLen;
//2. swap (curA, curB);
ListNode tmpNode = curA;
curA = curB;
curB = tmpNode;
}
// 求长度差
int gap = lenA - lenB;
// 让curA和curB在同一起点上(末尾位置对齐)
while (gap-- > 0) {
curA = curA.next;
}
// 遍历curA 和 curB,遇到相同则直接返回
while (curA != null) {
if (curA == curB) {
return curA;
}
curA = curA.next;
curB = curB.next;
}
return null;
}
7.环形链表II
- 时间复杂度: O(n)
- 空间复杂度: O(1)
//测试
public class test07 {
public static void main(String[] args) {
/*
题意: 给定一个链表,返回链表开始入环的第一个节点。
如果链表无环,则返回 null。
为了表示给定链表中的环,
使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
如果 pos 是 -1,则在该链表中没有环。
说明:不允许修改给定的链表。*/
ListNode cur = new ListNode();
ListNode protocur = new ListNode();
List<Integer> list = Arrays.asList(3, 2, 0, -4);
//添加数据
cur = AddListNode(list, cur);
protocur = AddListNode(list, protocur);
int target = 1;
ListNode errorList = ErrorList(target, cur);
//测试结果:-4 2 0 -4 2 0...........
/* while (errorList != null) {
System.out.print(errorList.val+" ");
errorList=errorList.next;
}*/
ListNode result = detectCycle(errorList);
if (result == null) {
System.out.println("无环");
} else {
System.out.println("环的入口点为" + result.val);
}
ListNode protoresult = detectCycle(protocur);
if (protoresult == null) {
System.out.println("无环");
} else {
System.out.println("环的入口点为" + protoresult.val);
}
}
public static class ListNode {
int val;
ListNode next;
public ListNode() {
}
public ListNode(int val) {
this.val = val;
}
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
//定义添加列表数据进节点的方法(尾插法)
public static ListNode AddListNode(List<Integer> list, ListNode head) {
ListNode cur = head;
for (Integer val : list) {
cur.next = new ListNode(val);
cur = cur.next;
}
return head.next;
}
//尾部成环
public static ListNode ErrorList(int target, ListNode head) {
ListNode dummy = new ListNode();
dummy.next = head;
ListNode cur = dummy;
int size = 1;
while (cur.next != null) {
cur = cur.next;
size++;
}
if (target >= size) return null;
for (int i = 0; i < target; i++) {
head = head.next;
}
cur.next = head;
return dummy.next;
}
//判断是否为环形链表
public static ListNode detectCycle(ListNode head) {
ListNode slow = head; // 慢指针,每次前进一步
ListNode fast = head; // 快指针,每次前进两步
while (fast != null && fast.next != null) { // 当快指针未到达链表尾部时循环继续
slow = slow.next; // 慢指针前进一步
fast = fast.next.next; // 快指针前进两步
if (slow == fast) { // 如果两个指针相遇,则链表中存在环
ListNode index1 = fast; // 创建指针index1指向相遇点
ListNode index2 = head; // 创建指针index2指向链表头部
// 两个指针分别从相遇点和链表头部开始,每次前进一步,直到它们再次相遇,此时相遇点即为环的入口点
while (index1 != index2) {
index1 = index1.next;
index2 = index2.next;
}
return index1; // 返回环的入口点
}
}
return null; // 如果链表中没有环,则返回null
}
}
/*输出结果:
环的入口点为2
无环
*/