⭐ACM格式输入输出⭐
输入:数字、字符、字符串 间隔:空格、换行、符号
-
数字——int——单独读取,以空格分开nextInt();——int等基本数据类型的数组,同行或不同都可以
-
字符——char
-
字符串——String——单独读取用next(),以空格划分
-
*next()读取单独字符串,到空格停止,在读取输入后将光标放在同一行中。 -
*nextLine()读取整行为字符串,到回车停止,在读取输入后将光标放在下一行。 -
split(",");按逗号分割单行内容,一般都是存到数组中 -
如果多行输入
- 循环调用
scanne.nextLine();获取每行 scanne.nextLine().split(",");来分隔单行中的内容
- 循环调用
-
单行很长输入
scanne.nextLine()获取改行,再层层分割。例如
String[] s = sc.nextLine().split(",");
for (int i = 0; i < s.length; ++i) {
String[] ss = s[i].split(":");
int a = Integer.parseInt(ss[0]);
int b = Integer.parseInt(ss[1]);
}
数组-二分查找
⭐特征:有序数组+无重复元素
基础二分
搜索插入位置
同二分,多一个返回插入索引
在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target,返回 [-1, -1]。
你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。
示例 1:
输入: nums = [5,7,7,8,8,10], target = 8
输出: [3,4]
示例 2:
输入: nums = [5,7,7,8,8,10], target = 6
输出: [-1,-1]
示例 3:
输入: nums = [], target = 0
输出: [-1,-1]
提示:
0 <= nums.length <= 105-109 <= nums[i] <= 109nums是一个非递减数组-109 <= target <= 109
双指针法
情况:1数组为空。2目标在数组左。3目标在数组右。4目标在数组区间内,但不在数组中。5在数组中
数组为空、目标在数组左,目标在数组右、直接返回【-1,-1】
左右指针定义为初值为-1,因为左右指针,分别从左往右/从右往左遍历时。如果没有目标值,即情况4。不会修改指针。定义为[-1,-1]后可以直接返回 。
两次二分,找到目标后继续往左往右二分查找,看是否有左右边界。
注意数组声明:
int[] array = new int[2];
int[] array = new int[]{1,2,3,4};
int m = 1, n = 2;
int[] array = new int[]{m,n};
27. 移除元素
给你一个数组 nums **和一个值 val,你需要 原地 移除所有数值等于 val **的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
说明:
为什么返回数值是整数,但输出的答案是数组呢?
请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下:
// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
示例 1:
输入: nums = [3,2,2,3], val = 3
输出: 2, nums = [2,2]
解释: 函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。
示例 2:
输入: nums = [0,1,2,2,3,0,4,2], val = 2
输出: 5, nums = [0,1,4,0,3]
解释: 函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。
只需要记录重复个数返回,同时修改nums数组即可,会自动根据返回的答案,输出移除后数组。不用重新创建数组。
⭐容器遍历——foreach
简洁,但没有索引不能操作元素
for (类型 X :容器对象) {
语句,要用到X;
}
977有序数组的平方
创立数组
int[] array = nwe int[length];
数组排序:直接调用Array.sort(要排序数组)
熟悉排序方法
链表
- 声明链表(头节点)对象!!
- 头节点可能被删除,用新对象指向原链表head
- 操作链表,再声明一个节点变量来操作
⭐if判断,需要加else,不能直接写。会报空指针异常。
class Solution {
public ListNode removeElements(ListNode head, int val) {
//声明链表(头节点)对象!!
ListNode pre = new ListNode(0);
//头节点可能被删除,用新对象指向原链表head
pre.next = head;
//操作链表,再声明一个节点变量来操作
ListNode temp = pre ;
while (temp.next != null) {
if (temp.next.val == val) {
temp.next = temp.next.next;
}
//相等跳过,不等往后顺延。
//不加这个,会原地一直while
else {temp = temp.next;}
}
return pre.next;
}
}
设计链表
- //注意
- //插入索引节点前,操作(索引节点的)上一个节点的next指向插入节点。所以循环i不能等于index
- //删除索引节点,操作(索引节点的)上一个节点的next指向下一个节点。所以循环i不能等于index
- //两个类————链表节点类ListNode和链表类LinkedList
- //初始化空链表,直接赋值/new,不用在声明变量
- //获取or返回链表要声明新的头节点,#指向#链表头节点
- //操作处理链表,声明新指针节点=head。
//链表类
class MyLinkedList {
//属性1:头节点,指向链表第一个节点
ListNode head;
//属性2:长度,get时需要
int size;
//无参构造方法,初始化一个空链表
public MyLinkedList() {
//头节点,val为0
head = new ListNode(0);
//长为0,空链表
size = 0;
}
public int get(int index) {
//int n = index;
if (index < 0 || index > size - 1) {
return -1;
}
//处理链表,都声明一个节点类型的指向链表的指针
ListNode cur = head;
for (int i = 0; i <= index; i++) {
cur = cur.next;
}
return cur.val;
}
//头插法
public void addAtHead(int val) {
//ListNode ah = new ListNode(val);
//ah.next = head.next;
//head.next = ah;
addAtIndex(0, val);
}
public void addAtTail(int val) {
addAtIndex(size, val);
}
//插入索引位置节点前.
//如下标为0-1,size=2,index为2,插入最后
public void addAtIndex(int index, int val) {
if (index < 0 || index > size) {
return;
}
size++;
ListNode cur = head;
for (int i = 0; i < index; i++) {
cur= cur.next;
}
ListNode toadd = new ListNode(val);
//cur.next = toadd;
//toadd.next = cur.next.next;注意顺序,这样插入后面链表断了
toadd.next = cur.next;
cur.next = toadd;
}
public void deleteAtIndex(int index) {
if (index < 0 || index > size - 1) {
return;
}
size--;
ListNode cur = head;
//把上一个节点指向下一个节点,索引从上一个节点开始操作。不能等于index
for (int i = 0; i < index; i++) {
cur= cur.next;
}
cur.next = cur.next.next;
}
}
//节点类
class ListNode{
int val;
ListNode next;
ListNode(){};
ListNode(int val){
this.val = val;
}
ListNode(int val, ListNode next){
this.val = val;
this.next = next;
}
}
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList obj = new MyLinkedList();
* int param_1 = obj.get(index);
* obj.addAtHead(val);
* obj.addAtTail(val);
* obj.addAtIndex(index,val);
* obj.deleteAtIndex(index);
*/
反转链表
迭代
遍历期间,改变指向
链表分头指针和头节点。头节点就是链表第一个节点,存第一个数据。
链表中操作或者声明的pre,head,cur,cur.next等,都是节点
- “=”符号是赋值(等号右表赋值给左边),根据顺序不同,可以是赋值,可以是指向。
- head = cur.next——头是cur的下一个节点
- cur.next = head——cur的下一个节点,指向头
注意:
- while循环条件:
cur != null,让最后一个节点也参与遍历- 如果是
cur.next != null,遍历不到最后一个
- 如果是
- cur指针遍历期间,要对链表.next(指向)进行操作,要声明一个临时节点,⭐
保存节点.next⭐。- 否则next改变,无法继续遍历
/**
* 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; }
* }
*/
//head就是链表第一个节点
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode cur = head;
while (cur != null) {
//既要把cur.next指向pre,指向后,cur链表断连,但还要把cur往右移动
//取一个临时节点,做值传递
//记录cur遍历的下一个节点
ListNode temp = cur.next;
//cur指向pre
cur.next = pre;
//头插,pre和cur都右移一位
pre = cur;
cur = temp;
}
return pre;
}
}
递归
关键:cur。
- 不要理解为返回cur这个节点,要理解为返回的是已经反转过的部分的头指针,而恰巧这个头指针是确定的。
- 递到最后一个节点,归的时候,每层改变已经反转部分的末尾,指向该层的前一个节点。同时层层返回翻转后的头节点。
//递归——方法中调用方法
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
//调用递归,反转好后面的链表
//用一个节点接收递归返回值
//递归到最后一层后head=5,返回第二层,此时head=4
ListNode cur = reverseList(head.next);
//让5指向4
head.next.next = head;
//此时4还指向5防止成环,4引用置空
head.next = null;
return cur;
}
}
删除倒数第n个节点
核心:根据倒数n,找到正序索引位置
常规思路
遍历,得到链表长度L 再次遍历,到L-n+1节点时,该是要删除节点
栈
要点:
- 声明一个标识节点,指向头。结尾返回标识.next。即原链表
- 栈的声明
Deque<ListNode> stack = new LinkedList<ListNode>();
- 声明指针处理链表节点
ListNode cur = res;指针指向标识节点
ListNode dummy = new ListNode();
dummy.next = head;
或者
ListNode res = new ListNode();
res.next = head;
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//初始化哑节点,作为返回标记,且不用特殊判断
ListNode res = new ListNode();
res.next = head;
//初始化一个链表构成的空栈
//声明元素格式为节点的链表对象
//用元素格式为节点的Deque双向队列【容器】接收
Deque<ListNode> stack = new LinkedList<ListNode>();
//初始化指针节点,处理链表
ListNode cur = res;
//遍历链表,压栈
while(cur != null){
stack.push(cur);
cur = cur.next;
}
//链表入完栈,弹栈弹出n个,栈顶即为前驱节点。
for(int i = 0; i < n ;i++ ) {
stack.pop();
}
ListNode pre = stack.peek();
pre.next = pre.next.next;
return res.next;
}
}
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//初始化哑节点,作为返回标记,且不用特殊判断
ListNode dummy = new ListNode( 0 , head);
//初始化一个链表构成的空栈
//声明元素格式为节点的链表对象
//用元素格式为节点的Deque双向队列【容器】接收
Deque<ListNode> stack = new LinkedList<ListNode>();
//初始化指针节点,处理链表
ListNode cur = dummy;
//遍历链表,压栈
while(cur != null){
stack.push(cur);
cur = cur.next;
}
//链表入完栈,弹栈弹出n个,栈顶即为前驱节点。
for(int i = 0; i < n ;i++ ) {
stack.pop();
}
ListNode pre = stack.peek();
pre.next = pre.next.next;
return dummy.next;
}
}
快慢双指针,指针相隔n
快慢指针初始为0/1。快指针先遍历n,之后同时遍历。快指针指向空时,慢指针即为要删除节点。
//用双指针
/*倒数2——删除5
1 2 3 4 5 6 null
l r
l r
l r
*/
//dummy——head
// | |
//慢指针——快指针
//dummy节点,获取链表备份,用来返回。dummy指向head
//快指针,单纯移动。可以取在原链表=head
//慢指针,要删除节点。取在备份链表上=dummy
//快比慢提前n个节点,间隔n-1个节点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode();
ListNode left = new ListNode();
ListNode right = new ListNode();
dummy.next = head;
right = head;
left = dummy;
for(int i = 0; i < n; i++) {
right = right.next;
}
while(right != null) {
left = left.next;
right = right.next;
}
left.next = left.next.next;
return dummy.next;
}
}
代码如下
核心:
- 声明哑节点指向链表,让后续操作实现在链表上
- 快指针初始在1,慢指针初始在0
- 这样删倒数n,快慢就相隔n-1,即快指针移动n-1
- 当快.next为空,则慢.next就是要删除的节点
/**
倒数2 删4
1 2 3 4 5
s f
s f
s f
s f
s f
快指针初始在1,慢指针初始在0
这样删倒数n,快慢就相隔n-1,即为快指针先移动n-1
当快.next为空,则慢.next就是要删除的节点
*/
//双指针
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
//初始哑节点作指向链表
//这样才能让后面操作是基于dummy链表修改
ListNode dummy = new ListNode();
dummy.next = head;
//初始快慢指针
ListNode first = new ListNode();
first = dummy.next;
ListNode second = new ListNode();
second = dummy;
for (int i = 0; i < n-1; i++) {
first = first.next;
}
while (first.next != null) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
return dummy.next;
}
}
交换列表节点
⭐【递归思路】
先往下层层递,再往上层层归,专注本级调用单元的操作
⭐主要三个要点:
- 返回值
- 调用单元做了什么操作
- 终止条件
本题中:
声明head【A】和next【B】
递归交换这两个相邻节点
故:
- 返回值——交换后的子链表
- 交换后,子链表新的头节点是NEXT【B】
- 调用单元做了什么操作——交换链表两个节点
- next【B】指向head【A】,B成为新头节点
- head【A】指向后面【递归交换好】的子链表
- 终止条件——当无节点,或者只有一个节点时截止
- head为空或next为空
/**
【递归思路】
先往下层层递,再往上层层归
专注本级调用单元的操作
主要三个要点:
1、返回值
2、调用单元做了什么操作
3、终止条件
本题中:
声明head【A】和next【B】
递归交换这两个相邻节点
故:
1、交换后的子链表
2、交换链表两个节点
next【B】指向head【A】,B成为新头节点
head【A】指向后面【递归交换好】的子链表
3、当无节点,或者只有一个节点时截至
head为空或next为空
*/
class Solution {
public ListNode swapPairs(ListNode head) {
//终止条件:链表只剩空节点,或一个节点时,终止
if (head == null|| head.next == null) {
return head;
}
//声明B节点
ListNode NEXT = head.next;
//声明后面递归部分链表
ListNode rest = NEXT.next;
//B指向A
NEXT.next = head;
//A指向后面递归好部分
head.next = swapPairs(rest);
//返回新的头节点
return NEXT;
}
}
链表相交
- 链表相交说明两链表从这开始后面节点是相同的
- 【节点的地址相同。或者说。前一个节点的next指针,指向同一个节点地址】,不单单是节点的value相同。
- 如A【1234】和B【1234】的节点值都相同。但A的2.next=B的2.next。则3才是第一个相交节点。2是不同地址的节点,只是value等于2
- 节点不等时,遍历链表直到第一个节点相同
思路:
- 遍历A,B链表,当各自遍历到结尾时,分别拼接上遍历B、A链表。
- 这样最终遍历的长度一样了
- 如果有交点,必然在拼接部分汇合。及A = B
- 如果没有交点。到拼接部分的最后一个节点,A != B,都后移一位。则A、B都为null此时A、B就相等了,跳出循环。
//链表相交说明节点相同,不是节点的value相同
//节点不等时,遍历链表直到第一个节点相同————while
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode A = headA;
ListNode B = headB;
while (A != B) {
A = (A == null ? headB : A.next);
B = (B == null ? headA : B.next);
}
//跳出循环说明A = B 了
return A;
}
}
为什么while循环中不用三目运算符超时??
while (A != B) {
if (A == null) {
A = headA;
}else {
A = A.next;
}
if (B == null) {
B = headA;
}else {
B = B.next;
}
}
TIPS
ListNode cur = head;
和
ListNode cur = new ListNode();
cur.next = head;
- 声明上都可以,只是一个直接是head,一个指向head
链表结束条件 cur != null 和 cur.next != null 区分不同情况
cur != null:通常用于遍历整个链表。最后一个节点也参与循环cur.next != null:通常用于在链表上执行某些操作,例如插入、删除或反转节点。在cur的下一个节点为空之前,您可以安全地访问cur.next。最后一个节点不参与循环
环形链表
判断是否有环形链表。有环返回成环的节点,无环返回null
- 声明:
Set<ListNode> hashSet = new HashSet<ListNode>(); - 存:
hashSet.add(节点); - 判断是否有重复:
hashSet.contains(节点);
重点:⭐哈希Set的使用
- HashSet 是一种基于哈希表的集合数据结构,它不是一个有序的集合。它的主要用于高效地存储和查找元素,而不是按照特定顺序访问它们。
if (hashSet.contains(nodeToFind)) { ————————判断是否存在
// 如果节点存在于 HashSet 中
// 遍历 HashSet 来找到匹配的节点
for (ListNode node : hashSet) { ————————遍历集合
if (node.equals(nodeToFind)) { ————————找到需要的节点
// 找到匹配的节点
// 在这里使用 node 进行操作 }
}
}
/**哈希表
遍历链表【while】,把节点都存到哈希表中
判重,如果有当前节点节点,已经存在于哈希表,说明有环。
如果遍历完都不存在,返回null
存哈希表add(ListNode)
判重contains(ListNode),有为true,没有为false
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode cur = head;
Set<ListNode> hashSet = new HashSet<ListNode>();
while (cur != null) {
//先判断哈希有没有,再存入。
//第一个节点开始,哈希表为空,这样写也没影响
if (hashSet.contains(cur)) {
//为true说明已存在,返回环的节点
return cur;
}else {
//说明不存在,存入哈希
hashSet.add(cur);
}
cur = cur.next;
}
return null;
}
}
二叉树
-
栈是递归的一种实现结构
- ⭐调用栈保证的递归能正确的层层返回
- 上层递归先进调用栈,最后出调用栈
- 底层最后进调用栈,但是最先出,最先往上层递归返回
-
深度优先遍历(前中后序遍历)——递归
-
广度优先遍历(层序遍历)——队列
- 层序是从上往下,一层层遍历。
- 所以用队列存储可以顺序打印
深度优先遍历:前中后序遍历——递归
前序
⭐终止条件一般放在递归方法第一行
- 入参————根节点,遍历的结果集(数组列表)。因为要输出遍历结果,传入容器存储数据。
- 返回值————遍历方法除了把遍历数据放入结果集无其他数据操作,无返回值
- 终止条件————当前节点为空说明本层结束
- 每层递归操作————先add根节点进容器,再对左子节点调用遍历方法,再对右遍历。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>(); //声明数组列表存储结果集
preorder(root, res); //调用遍历(的递归)方法,传入根节点+结果集列表(空的)
return res; //前序遍历方法的返回值,返回遍历结果集
}
//定义前序遍历的(递归)方法
//无返回值,是遍历方法,把结果add进入参给的结果集中
public void preorder(TreeNode root, List<Integer> list) {
if (root == null ) { //结束条件:方法没有返回值,如果节点为空返回上一层递归。
return;
}
list.add(root.val); //前序,先把根节点add到结果集。再遍历左节点,再遍历右节点
preorder(root.left, list); //调用遍历(的递归)方法,(前序)遍历左节点
preorder(root.right, list); //同上调用递归遍历右节点
}
}
⭐动态规划
- 确定dp数组的含义 dp[]或dp[][]
- 如何初始化dp[0][0]。其他数组的值有时候也需要初始化,全0、-1、100等都可以。
- 确定遍历顺序+递推公式
所有情况都遍历到,且递推逻辑正确。最终结果就是dp[m][n]
爬楼梯
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
//dp[2] = 2;
for(int i = 2; i <= n; i++){
dp[i] = dp[i-1] + dp[i - 2];
}
return dp[n];
}
}
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n + 2];
//dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for(int i = 3; i <= n; i++){
dp[i] = dp[i-1] + dp[i - 2];
}
return dp[n];
}
}
最小路径和
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length; //列长度
int n = grid[0].length; //(第0)行长度
int[][] dp = new int[m+1][n+1]; //声明dp及含义,到(i,j)位置的最小和
dp[0][0] = grid[0][0]; //初始化
//确定遍历顺序+确定递推公式
//1、第一列只能往下,第一行只能往右。对应元素只能分别由上方、左方元素递推
//2、非第一行第一列元素,可以由上往下或左往右得到
//遍历第一列:上往下
for (int i = 1; i < m; i++) { //dp[0][0]已经定义了,所以从第二个元素【下标为1】开始。
dp[i][0] = dp[i-1][0] + grid[i][0]; //并且0 - 1,明显越界
}
//遍历:第一行左往右
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i-1] + grid[0][i];
}
//遍历其他所有:先下再右
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
}