目录
length属性→数组;length()方法→字符串String;size()方法→泛型集合
数组
2024.3.4
704 二分法
一般是左闭右闭和左闭右开
左闭右闭时,一般是left<=right ; right=middle-1; left=middle+1
左闭右开时,一般是left<right ; right=middle ; left=middle+1
mid = left + ((right - left) >> 1)
的目的是为了计算left
和right
之间的中间索引,而不必担心整数溢出的问题(如果使用(left + right) / 2
,当left
和right
都很大时可能会遇到溢出的问题)
- 暴力解法时间复杂度:O(n)
- 二分法时间复杂度:O(logn)
27 移除元素
双指针:快慢指针,双向指针
快慢指针:fast!=val时,将fast的值给slow,否则将fast+1然后再继续
双向指针:left和right,如果left的值为val,就将right的值赋给当前left,然后将right--,即删去这个值(即互换),然后再left++,当left和right指向同一个时,先判断再结束。
- 暴力解法时间复杂度:O(n^2)
- 双指针时间复杂度:O(n)
977 有序数组的平方
双指针复杂度为O(n),i=0,j为最后一个,然后比较两个数的平方,谁大就放在新数组的后面,新数组下标减一,i++或者j--,再次比较(i<=j)
2024.3.5
209 最小长度的子数组
滑动窗口:用j表示终止位置,i初始位置为0,sum为总和,当sum>=target时,才将sum减去i所对应的值,并将i向后移一个,直到小于为止,因此用while,每次比较长度的大小取最小值;
在这里,将长度设置为int类型的最大值;如果sum直到j为最后一个值都没有大于等于target,说明本身数组总和就不大于等于这个数,不会进行比较取最小长度,那么长度的值仍为一开始的最大值
result = Integer.MAX_VALUE;
所以最后要加上一个判断,当仍为最大值时即不存在,返回0
return result == Integer.MAX_VALUE ? 0 : result;
- 暴力解法时间复杂度:O(n^2)
- 滑动窗口时间复杂度:O(n)
59 螺旋矩阵Ⅱ
循环不变量:对每条边的处理规则一样 左闭右开
用startx和starty记录每次循环的开始位置,初始值为0;用offset=1记录循环几圈,每一圈有四个循环,满足offset<=n/2时,进行循环填数,count初值为1,每次填数之后++。每一次循环之后,初始位置都要++,因为变成了里面一圈的位置开始,offset++进行下一圈循环。
如果n为奇数,那么循环完之后会有一个最中间的数,此时startx和starty表示的位置就是最后一个数的位置。
总结
来自 代码随想录知识星球 (opens new window)成员:海螺人的总结
链表
单链表,双链表,循环链表
2024.3.6
203 移除链表元素
要先判断要移除的元素是不是头结点,就得用两种方法移除;(持续移除的过程,为while )
可以使用虚拟头结点 dummy head,这样删除的结点不管是不是头结点都一样的操作
首先要判断链表是否为空!!!
if(head==null){
return head;
}
定义虚拟结点 ListNode dummyhead=new ListNode(-1,head);值为-1,指针指向头结点
用.val表示值,.next表示指针指向的位置
2024.3.8
※707 设计链表(不太会)
//单链表
class ListNode {
int val;
ListNode next;
ListNode(){}
ListNode(int val) {
this.val=val;
}
要注意可以用for也可以用while,但是每次添加和删除节点时,已知的index要为遍历时cur.next,这样才能找到前一个节点进行连接。
206 反转链表(双指针法,递归法)
双指针:pre和cur,pre初始为虚拟,cur初始为head,结束的时候cur为null,pre为最后一个节点时就不用反转了,最后返回pre;每次先把cur.next保存下来,然后反转一个指针,将pre和cur向后移,先移动pre再移动cur,这样可以找到
while(cur!=null){
temp=cur.next;
cur.next=pre;
pre=cur;
cur=temp;
}
递归法:还是利用双指针,只不过是将函数进行递归使用
在主函数中直接赋值返回即可。return reverse(null,head);
private ListNode reverse(ListNode pre, ListNode cur){
if(cur==null){
return pre;
}
ListNode temp=cur.next;
cur.next=pre;
return reverse(cur,temp); 递归的关键地方
}
2024.3.10
24 两两交换链表中的节点
首先要判断结束标志:长度为0时,cur.next=null; 长度为偶数时,cur.next=null; 长度为寄数时,cur.next.next=null
while(cur.next!=null && cur.next.next!=null) 不能写反,若cur.next=null; 写反了就会报错!
2024.3.11
19 删除链表的倒数第N个节点
快慢指针,先让快指针走n,之后快慢指针一起走,当快指向null时,慢指向想要删除的节点,但是我们要删除这个节点,就得知道前一个节点;因此,让快先走n+1即可。在while(n!=0 && fast!=null)之前,将n+1;while中每次n--
面试题 02.07 两两相交
先把两个链表长度计算出来,然后将长的链表附给A,找到两者长度差值gap,让长的链表的cur节点移动gap,之后判断两个链表的cur节点是否相等,不相等往后移动,相等返回当前节点。
142 环形链表 Ⅱ
如何判断是否有环:快慢指针是否相遇;快走2 ,慢走1
有环时找到环的入口:从头节点开始的指针和相遇点开始的指针相遇的位置就是环入口
注:因为快每次比慢多一个,所以相遇的时候肯定慢在第一圈就可以相遇
判断条件 while(fast!=null && fast.next!=null)
哈希表
想知道一个元素是否出现过
三种数据结构:数组,set,map
范围可控,数比较小的情况下用数组;数值很大,用set;key:value用map
242 有效的字母异位词
就是两个字符串中出现的字母以及次数相同,不考虑顺序;
用哈希数组,字母有26个,数组的长度就为26,每个位置代表一个字母在字符串中出现的次数;第一个字符串中hash++,第二个字符串中hash--;如果最后hash中有不为0的,说明有字母个数不一样,则返回fasle;都为0,返回true。
2024.3.12
在 Java 中,不用一开始规定长度,可以用 add
方法动态添加元素的集合类包括 ArrayList
、HashSet
、LinkedList
、TreeSet
、LinkedHashSet
等。
349 两个数组的交集
使用数组来做哈希的题目,是因为题目都限制了数值的大小。而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。
1. 哈希集合HashSet底层为哈希表,不允许重复
Set<Integer> set1=new HashSet<>();
Set<Integer> resSet = new HashSet<>();
遍历添加的时候,可以普通的for循环
for(int j=0;j<nums2.length;j++){
if(set1.contains(nums2[j])){
result.add(nums2[j]);
}
或者 将数组
nums1
中的元素逐个添加到名为set1
的集合中;元素i,‘for-each’语句for (int i : nums1) {
set1.add(i);
}
转换为数组
return result.stream().mapToInt(x -> x).toArray();
2. 因为leetcode中又规定了数组的程度最大为1000,因此可以用哈希数组来做
int[] Hash1=new int[1002];
int[] Hash2=new int[1002];
List<Integer> resList = new ArrayList<>();
要将存放结果的列表转换为数组,并将所有值输出:
int index=0;
int[] res=new int[resList.size()];
for(int i:resList){
res[index++]=i;
}
202 快乐数
用哈希集合完成,首先写一个函数计算出每次平方和更新之后的数,用哈希集合将每次的数记录下来,数为1时停止,说明是快乐数;当哈希集合中有该数时,则说明又一次进入了数的循环,不可能到1;若最后n为1,则为快乐数。
快慢指针:相当于判断链表是否有环;当n为快乐数时,无环,最终快比慢先到达1;不是快乐数,那么就会循环,快和慢会在某一点相遇。快每次走两个,getNext()的嵌套。
1 两数之和
HashMap:需要知道一个元素是否出现过,并返回元素的下标,因此将元素作为key,value存储下标;map存储遍历过的元素,遍历当前元素时,看看map中有没有要找的元素,如果有,则返回这两个的下标。如果没有,把这个元素和下标存储在map中。
int[] res=new int[2]用这个数组输出结果
Map<Integer,Integer> map=new HashMap<>();
map.containsKey(temp) 是否包含这个元素;map.get(temp) 获得value值;map.put(nums[i],i) 添加元素
2024.3.13
454 四数相加Ⅱ
分成两两的和进行寻找,不仅要找出和为0,还要记录有多少个元组可以满足,因此用map,key放和,value放出现的次数。
先遍历A+B,将其值和出现的次数记录在map中,然后遍历C+D,在map中寻找0-(C+D),如果有,则将count+其value的值,因为统计的是符合条件的个数
map.put(num, map.getOrDefault(num, 0) + 1) 用来统计num出现的次数; map.getOrDefault(num, 0) 如果num在里面,则返回value,如果不在里面就返回0
383 赎金信
和有效的字母异位词差不多,hash数组记录字符串中字符出现的次数,另一个字符串中出现一次减去1,最后看看什么符合条件
要先把字符串转换为字符数组: for(char i:ransomNote.toCharArray()) 才能一个个遍历
15 三数之和-双指针法
要去重,因为要找的是元素的三元组,可能不同位置上三个元素相加为0【(-2,1,1)和(1,-2,1)】,但是三个元素的值是一样的,这时候只能算一个,但是哈希找这个数的时候是没有顺序的
双指针:对数组进行排序(从小到大)Arrays.sort(nums),遍历i,左边为left,右边为right,判断三者之和是大于0还是小于0;对两个指针进行适当的移动,但是也要注意去重。要先获取一个之后再去重
注:数组变量的嵌套写法 List<List<Integer>> res=new ArrayList<>();可以在里面放入list;这些list再组成list;
- 对于每遍历一个i,判断当前最小数是否小于0,大于0就不可能有和为0的;
- 对a去重,要与前一个比较,若值一样,直接跳过该值
- 定义left和right,如果left<right就判断三数之和与0的关系,大于0将right--,小于0将left++;等于0的时候,将三个数加入到结果中res.add(Arrays.asList(nums[i], nums[left], nums[right])),并继续对b,c进行去重,如果left和后一个一样,就后移,right与前一个一样就前移;若没有应将left,right移动,进行下一次判断;
18 四数之和 -双指针法
剪枝:现在不能排序之后判断第一个数是不是大于target了,因为target可能是负数,负数相加会变小;当nums[i] > target && nums[i] > 0 && target>0三个条件都满足时,后面的数是越来越大的,相加肯定超过target,因此可以直接返回res或者break;二次剪枝可省略
注:因为第一次剪枝时,是在一个循环中,不符合条件可以直接返回res;但是在第二次剪枝的时候,判断的是nums[i]+num[j],如果大于target,说明j之后的数都大,如果直接res,相当于后面的情况都不考虑了,但是,有可能i和j中间有很多数,其他数可以相加等于target,能和nums[i]相加的数已经在这之前遍历过了,因此需要break跳出循环,遍历i++的情况。
nums[k] + nums[i] + nums[left] + nums[right] > target int会溢出
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
字符串
2024.3.14
344 反转字符串
用两个指针在左右两边进行交换就行,结束条件就是l>=r;或者l<s.length/2;定义的中间变量temp为char类型
541 反转字符串Ⅱ
题中给的是字符串,首先要变为字符数字才能进行反转, char[] ch=s.toCharArray();对于每次循环i间隔2k,这样在每次循环中对前k个进行反转就可;结束条件是i<ch.length;
在一次循环里(最后一次)时,确定end,要判断反转结束位置(start+k-1)与ch的最后一个位置的大小(ch.length-1),使用Math.min(),反转结束位置小说明剩下的可能大于k小于2k;最后一个位置小,说明剩下的不够k,对这些进行反转。最后的返回要变成字符串形式: return new String(ch);
2024.3.15
※151 反转字符串里的单词
不使用创建新的数组,在原数组上进行改动,空间复杂度为O(1);使用双指针,时间复杂度为O(n)
思路:先用快慢指针移除多余的空格;再整体反转字符串;再每个单词进行翻转。
charAt()方法用于返回指定索引处的字符值,而字符值是不可变的;所以 s.charAt(slow) = ' '
就是错误的,只能用StringBuilder
,
是可变的字符串类,可以方便地进行字符位置的修改和操作。
循环中,如果快指针指向空就向后移动,慢指针不动;若快指针不为空,要判断slow是不是指向0,指向0说明在开头,那么就将fast指向的值加入到创建的对象sb中,直到fast再次为空,说明一个单词结束;然后fast继续向后移动,再次不为空的时候判断slow是不是在0的位置,很显然肯定不是0说明不在开头单词,那么这时候就要给单词之间加上空格,给sb加入一个空格,并将slow向后移动,就会继续将fast的值给slow(即将值插入到sb中)直到这个单词结束,以此往复。当fast到最后位置的时候也会结束。
注:while里面是一个单词中所有字符的判断,每加入一个字符就将两个指针后移一位,当一个单词结束,fast指向空就会结束while循环,进行for循环里面的fast++;直到fast再次不为空,进行if里面slow的判断,由此判断是不是第一个单词,不是第一个单词就加入空格并将slow++
public StringBuilder removeSpace(String s){
StringBuilder sb = new StringBuilder();
//定义快慢指针
int slow=0;
for(int fast=0;fast<s.length();fast++){
if(s.charAt(fast)!=' '){
//保留单词间的空格
if(slow!=0){
sb.append(' ');
slow++;
}
while(fast<s.length() && s.charAt(fast)!=' '){
sb.append(s.charAt(fast));
slow++;
fast++;
}
}
}
return sb;
}
第二步就是对整个字符串进行反转,sb.setCharAt(0,c)
是将值c赋给0的位置
public void reverseString(StringBuilder sb, int start, int end) {
while (start < end) {
char temp = sb.charAt(start);
sb.setCharAt(start, sb.charAt(end));
sb.setCharAt(end, temp);
start++;
end--;
}
}
之后对每个单词进行反转,调用 reverseString()方法,先找到每个单词的起始位置和结束位置,初始让end=1,如果end不为空,就后移,直到为空,这时将这个单词反转;之后下一个单词的开始位置为end+1,结束位置向后一个,并重新开始找到新的单词的结束位置;直到开始位置或者结束位置到最后的时候结束。
private void reverseEachWord(StringBuilder sb) {
int start = 0;
int end = 1;
int n = sb.length();
while (start < n) {
while (end < n && sb.charAt(end) != ' ') {
end++;
}
reverseString(sb, start, end - 1);
start = end + 1;
end = start + 1;
}
}
最后输出要转化 return sb.toString();
※KMP算法
前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。
后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。
找一个字符串是否在主串中出现过
最长相等前后缀;例:aabaaf,前缀表就为010120,数字代表这之前子串的最长相等前后缀长度
每次匹配失败的位置(失败了就找去掉这个位置的子串的最长相等前后缀长度也就是next的前一个值,看看匹配的长度是多少,下次直接从哪里开始匹配,虽然next里面的值是长度,假如值为2,也就是跳到下标为2的位置,相当于从第三个数开始比较,前两个数是匹配的),即找到前一位的前缀表值,直接回到下一个继续匹配的位置。next数组里面就是前缀表里的值,代表前缀的长度,但是有的是统一减一(右移一位,初始位置为-1);如果0开头就是原始的。
找模式串的前缀表:1.先初始化,i表示后缀末尾,j表示前缀末尾位置,j也代表着包括i之前的子串的最长相等前后缀的长度。 2.前后缀不相同 3.前后缀相同 4. 更新next
j=0;
next[0]=0;
for(int i=1;i<s.size;i++){
while(j>0 && s[i]!=s[j]){ #不相等时说明前后缀不匹配,j看前一个位置对应的数退回
j=next[j-1];
}
if(s[i]==s[j]) j++; #相等说明最长相等前后缀的值j+1
next[i]=j;
}
※459 重复的子字符串
如果一个字符串是由重复子串组成的,那么他的最小重复单位就是他的最长相等前后缀不包含的那一部分(自己可以去推导);因为前后缀都是不包括开头和结尾的,所以剩下的必定是一个重复单元
1. 移动匹配:字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。如s=abcabc里面由两个一样的abc组成,s+s中,剔除首和尾,就一定还会出现s
2. KMP: 数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
if (s.equals("")) return false;
int len = s.length();
// 原串加个空格(哨兵),使下标从1开始,这样j从0开始,也不用初始化了
s = " " + s;
char[] chars = s.toCharArray();
int[] next = new int[len + 1];
// 构造 next 数组过程,j从0开始(空格),i从2开始
for (int i = 2, j = 0; i <= len; i++) {
// 匹配不成功,j回到前一位置 next 数组所对应的值
while (j > 0 && chars[i] != chars[j + 1]) j = next[j];
// 匹配成功,j往后移
if (chars[i] == chars[j + 1]) j++;
// 更新 next 数组的值
next[i] = j;
}
// 最后判断是否是重复的子字符串,这里 next[len] 即代表next数组末尾的值
if (next[len] > 0 && len % (len - next[len]) == 0) {
return true;
}
return false;
代码中next中相当于将所有值向后移,每个字符对应位置上的next的值就是以该字符结尾的子串的最长相等前后缀大小。[ , ,0,0,1,2,3,4,5,6]
2024.3.16
※28 找出字符串中第一个匹配项的下标
先算出模式串的next数组
然后定义两个指针对应模式串j和主串i的下标,均从0开始,i循环,对于每一个i小于主串的长度,如果模式串中的j对应的数相等,那么j++,i++同时向后移动开始下一次循环,如果不相等,就将j回退到上次前后缀相等的位置看是否相等,直到相等或者j=0;循环中每次判断j和模式串的长度是否相等,因为j代表的就是主串中与模式串相匹配的长度,如果相等了说明在主串中出现了,就返回第一个匹配项的下标 i-len+1(因为还在这次的循环中,i指的是匹配的最后一个位置的下标)
int n = haystack.length(), m = needle.length();
if (m == 0) {
return 0;
}
int[] pi = new int[m];
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && needle.charAt(i) != needle.charAt(j)) {
j = pi[j - 1];
}
if (needle.charAt(i) == needle.charAt(j)) {
j++;
}
pi[i] = j;
}
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = pi[j - 1];
}
if (haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if (j == m) {
return i - m + 1;
}
}
return -1;
双指针(前面做了)
2024.3.17
数组,字符串,链表,多数之和(去重)
27 移除元素
344 反转字符串
151 反转字符串里的单词
206 反转链表
19 删除链表的倒数第N个节点
面试题 02.07 两两相交
142 环形链表 Ⅱ
15 三数之和
18 四数之和