代码随想录-一刷(例题)
一、链表
1.设计链表
题目网址:https://leetcode.cn/problems/design-linked-list/
你可以选择使用单链表或者双链表,设计并实现自己的链表。
单链表中的节点应该具备两个属性:val
和 next
。val
是当前节点的值,next
是指向下一个节点的指针/引用。
如果是双向链表,则还需要属性 prev
以指示链表中的上一个节点。假设链表中的所有节点下标从 0 开始。
实现 MyLinkedList
类:
MyLinkedList()
初始化MyLinkedList
对象。int get(int index)
获取链表中下标为index
的节点的值。如果下标无效,则返回-1
。void addAtHead(int val)
将一个值为val
的节点插入到链表中第一个元素之前。在插入完成后,新节点会成为链表的第一个节点。void addAtTail(int val)
将一个值为val
的节点追加到链表中作为链表的最后一个元素。void addAtIndex(int index, int val)
将一个值为val
的节点插入到链表中下标为index
的节点之前。如果index
等于链表的长度,那么该节点会被追加到链表的末尾。如果index
比长度更大,该节点将 不会插入 到链表中。void deleteAtIndex(int index)
如果下标有效,则删除链表中下标为index
的节点。
思路:
单链表,
1.设置一个虚节点,更方便对于增删的处理
2.设置size属性初始化为0,在增删方法中size++或–,这样可以知道链表长度从而简化代码过程
做题过程:
1.在设置MyLinkedList的属性时设置了两个节点dummy 和head。这样是head多设置了。
因为链表是从零开始添加,通过addAtHead对链表进行扩容,如果我们多设置一个head,head没有赋值,会多出一个无意义的节点
所以开始只需要设置一个dummy 虚节点即可
在addAtIndex()方法中出现很多问题,首先在判断index值时需要将他们用判断else-if连接,
不然如果符合index0或size后,size的值会发生改变,会导致下面的判断出现异常
3.对于add或delete方法,size增减的语句要放进if里面
Java代码:
class ListNode1{
int val;
ListNode next;
public ListNode1(){}
public ListNode1(int val){
this.val=val;
}
}
class MyLinkedList {
int size;
ListNode dummy;
public MyLinkedList() {
size=0;
//head=new ListNode();
dummy=new ListNode();
//dummy.next=head;
}
public int get(int index) {
if(index>=0 && index<=size-1){
ListNode cur=dummy.next;
while(index>0){
cur=cur.next;
index--;
}
return cur.val;
}else{
return -1;
}
}
public void addAtHead(int val) {
ListNode cur=dummy;
ListNode add = new ListNode(val);
add.next=cur.next;
cur.next=add;
size++;
}
public void addAtTail(int val) {
ListNode cur=dummy;
ListNode add = new ListNode(val);
while(cur.next!=null){
cur=cur.next;
}
cur.next=add;
size++;
}
public void addAtIndex(int index, int val) {
if(index==0) {addAtHead(val);}
else if(index==size){ addAtTail(val);}
else if(index>0 &index<=size-1){
ListNode add = new ListNode(val);
ListNode cur=dummy;
while (index>0){
cur=cur.next;
index--;
}
add.next=cur.next;
cur.next=add;
size++;
}
}
public void deleteAtIndex(int index) {
if(index>=0 &index<=size-1){
size--;
ListNode cur=dummy;
while (index>0){
cur=cur.next;
index--;
}
cur.next=cur.next.next;
}
}
@Override
public String toString() {
return "MyLinkedList{" +
"size=" + size +
", head=" + dummy.next +
'}';
}
}
2.反转链表
给你单链表的头节点 head
,请你反转链表,并返回反转后的链表。
思路
使用双指针法,开始pre 指向null,cur指向头结点,然后改变next,依次移动
做题过程:
在while (cur!=null)判断中,开始我写成了 pre.next!=null,因为一直认为pre.next 指向cur。
这个想法是错误的,比如①–》②—》③,pre在1,cur在2,当翻转操作完成后,变成①《— ② ③
此时pre在② cur在③, ②与③之间 无连接
在初始化pre时写成了
ListNode pre=new ListNode()
pre.next=head
这么写是错的,因为我们翻转链表后,头节点一定要指向null,
按照代码逻辑第一次头结点指向我们刚设置的pre,所以pre开始必须为null,让头节点最终能指向null
代码
class Solution3 {
//双指针法
public ListNode reverseList1(ListNode head) {
if(head==null) return head;
//pre开始需要设为null
ListNode pre =null;
ListNode cur=head;
//注意,判断cur而不是pre.next
while (cur!=null){
ListNode tem=cur.next;
cur.next=pre;
pre=cur;
cur=tem;
}
return pre;
}
//递归法
public ListNode reverseList2(ListNode head) {
return reverse(null,head);
}
public ListNode reverse(ListNode pre,ListNode cur){
if(cur!=null){
ListNode temp=cur.next;
cur.next=pre;
//pre=cur;
//cur=tem;
return reverse(cur,temp);
}else {
return pre;
}
}
}
3.两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
https://leetcode.cn/problems/swap-nodes-in-pairs/
思路:
1.两两交换,我们需要从这两个的前一个开始操作,所以应该设置一个虚节点dummy
2.什么时候终止循环?
因为我们是两个、两个的进行操作,所以每次从前面开始,看后两个是否为空
所以是cur.next!=null && cur.next.next!=null
此外,我们每次循环后cur要向后移动两位,cur=cur.next.next;
3.因为交换的过程中节点的位置情况会丢失,所以我们需要提前保存相应节点的位置
做题过程:
开始的时候对于后面点的变动是根据移动cur来改变的
这样不仅繁琐,而且容易出错
直接保存后面三点的位置,然后更改指向,最后cur直接移动到两个之后,这样非常方便,清晰
代码:
class Solution {
public ListNode swapPairs(ListNode head) {
//设置虚节点
ListNode dummy=new ListNode();
dummy.next=head;
ListNode cur=dummy;
//要保存中途变动的节点
ListNode temp1=null;
ListNode temp2=null;
ListNode temp3=null;
while (cur.next!=null && cur.next.next!=null){
temp1=cur.next;
temp2=cur.next.next;
temp3=cur.next.next.next;
cur.next=temp2;
temp2.next=temp1;
temp1.next=temp3;
cur=cur.next.next;
}
return dummy.next;
}
}
4.删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
思路:
1.使用双指针的方式—快慢指针遍历,一次操作就能删除
2.采 用 虚 节 点 的 方 式
虚节点的方便之处在于:
不需要对所操作节点是不是头结点来进行特殊判断,从而统一了操作
3.快指针先走n+1步,然后快慢指针同时移动,这样当快指针指向null时,慢指针正好指向了倒数第n个节点的前一个
做题过程:
1.在开始移动快指针时没有考虑清楚特殊情况
写成了
while (n>0 && fastInde.next!=null)
在后面快慢指针一起运动的语句中又写成了
while (fastIndex!=null)
两次判断不统一
遇到特殊情况,就是当n大于链表长度时,
slowIndex.next=slowIndex.next.next; 可能会报空指针异常
代码
class Solution5 {
public ListNode removeNthFromEnd(ListNode head, int n) {
if(head==null) return head;
ListNode dummy=new ListNode();
dummy.next=head;
ListNode fastIndex=dummy;//定义快指针
ListNode slowIndex=dummy;//定义慢指针
n++;//我们要删除倒数第n个节点,就得在倒数n-1的位置上操作
//先让快节点移动n+1步
while (n>0 && fastIndex!=null){
n--;
fastIndex=fastIndex.next;
}
//再让快节点和慢节点一起运动
while (fastIndex!=null){
fastIndex=fastIndex.next;
slowIndex=slowIndex.next;
}
slowIndex.next=slowIndex.next.next;
return dummy.next;
}
}
5.链表相交
给你两个单链表的头节点 headA
和 headB
,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null
。
思路:
若两个链表相交,那么相交之后的链表相同
我们比较两个链表长度,长链表和短链表必会有重叠的部分,并且他们尾部对齐
所以我们可先将长链表的cur移动到和短链表的长度对应的部分(尾部对齐)
①-②-③-④-⑤
③-④-⑤
然后依次比较curlong 和curshort
若相同就说明他们相交
做题过程:
顺利
代码:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//先算出两者长度
int longlength=0;int shortlength=0;
ListNode curLong=headA;ListNode curShort=headB;
//得出链表A的长度
while (curLong!=null){
longlength++;
curLong=curLong.next;
}
//得出链表B的长度
while (curShort!=null){
shortlength++;
curShort=curShort.next;
}
//刷新cur的指向
curLong=headA;
curShort=headB;
//判断哪个表长度长
if(shortlength>longlength){
curLong=headB;
curShort=headA;
int templength=shortlength;
shortlength=longlength;
longlength=templength;
}
int substract=longlength-shortlength;
while (substract!=0){
curLong=curLong.next;
substract--;
}
while (curLong!=null&&curShort!=null){
//从当前开始判断
if(curLong==curShort){
return curLong;
}
curShort=curShort.next;
curLong=curLong.next;
}
return null;
}
}
6.环形链表
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思路:
有两个问题
1.如何判断是否存在环?
设快慢指针同时移动,但是快指针每次走两步,慢指针每次走一步
如果存在环,那么快指针最后一定会和慢指针相遇
(因为相对于慢指针而言,快指针是以每次一步的速度追赶)
2.如何找到环的入口?
假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示。
那么相遇时: slow指针走过的节点数为: x + y
, fast指针走过的节点数:x + y + n (y + z)
,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。
因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2(时间相同):
(x + y) * 2 = x + y + n (y + z)
两边消掉一个(x+y): x + y = n (y + z)
因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。
所以要求x ,将x单独放在左面:x = n (y + z) - y
,
再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z
注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。
因为y+z等于环的周长C 所以式子可以化为 x=(n-1) C +z
从这个式子我们可以知道,如果两指针一个从x起始出发,一个从z起始出发,他们必会在入口处相遇
做题过程:
顺利
代码:
public class Solution {
public ListNode detectCycle(ListNode head) {
//定义快指针和慢指针
ListNode fastcur=head;
ListNode slowcur=head;
while (fastcur!=null && fastcur.next!=null && fastcur.next.next!=null){
//快指针走两步,慢指针走一步,并且他们同时开始移动
//这样一定会相遇
fastcur=fastcur.next.next;
slowcur=slowcur.next;
//如果两者相遇了
if(fastcur==slowcur){
fastcur=head;
//将快指针置向头结点,让他们以相同速度进行移动,最终相遇的位置一定是入口
while (fastcur!=slowcur){
fastcur=fastcur.next;
slowcur=slowcur.next;
}
return fastcur;
}
}
return null;
}
}
二、哈希表
哈希值 小 优先选数组
哈希值 大 优先选set,map
2.1 有效的字母异位词
给定两个字符串 *s*
和 *t*
,编写一个函数来判断 *t*
是否是 *s*
的字母异位词。
**注意:**若 *s*
和 *t*
中每个字符出现的次数都相同,则称 *s*
和 *t*
互为字母异位词。
思路:
若两个字符串各个字符出现的次数都相同,则为字母异位词。
我们可以使用数组,将每个字符通过函数映射到对应的位置 ,对于位置做++或–
s.charAt(l)-‘a’ 相当于一个哈希函数,将对应的字母映射到相应的位置上
对第一个字符串每个字符相应位置做++
对第二个字符串每个字符相应位置做- -
最后判断数组各个位置是否都为0
做题过程:
代码:
class Solution1 {
public boolean isAnagram(String s, String t) {
int []record=new int[26];
int l=0;
for( l=0;l<s.length();l++){
//s.charAt(l)-'a' 相当于一个哈希函数,将对应的字母映射到相应的位置上
record[s.charAt(l)-'a']++;
}
System.out.println();
for(l=0;l<t.length();l++){
record[s.charAt(l)-'a']--;
}
for(l=0;l<record.length;l++){
if(record[l]!=0) return false;
}
return true;
}
}
2.2 两个数组的交集
给定两个数组 nums1
和 nums2
,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
思路:
1.使用数组
思路和第一题相同,通过函数映射到对应位置,这里的函数就是数字本身,标记每个数字出现的次数,记录在对应位置
2.使用哈希表
先将num1里的数字存到hashmap,再对num2里的数字依次查询
由于hashmap存储不重复,所以非常简单
做题过程 :
顺利
代码:
class Solution2 {
//1.用哈希表的方式
public int[] intersection1(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
return new int[0];
}
Set<Integer> hashset1=new HashSet<>();
Set<Integer> hashset2=new HashSet<>();
for(int i=0;i<nums1.length;i++){
hashset1.add(nums1[i]);
}
for(int i=0;i<nums2.length;i++){
if(hashset1.contains(nums2[i])){
hashset2.add(nums2[i]);
}
}
return hashset2.stream().mapToInt(x->x).toArray();
}
//第二种,使用hash数组
//注:有条件:原始数组中的数字大小不能超过1000
//当原始数组中的数很大时,最好使用set,因为用哈希数组的话,开辟空间很麻烦
public int[] intersection(int[] nums1, int[] nums2) {
int[] record=new int[1001];
int[] result=new int[1001];
int temp=0;
for(int i=0;i<nums1.length;i++){
record[nums1[i]]++;
}
for(int i=0;i<nums2.length;i++){
if(record[nums2[i]]!=0){
result[temp]=nums2[i];
temp++;
record[nums2[i]]=0;
}
}
return Arrays.copyOfRange(result,0,temp);
}
}
2.3 快乐数
编写一个算法来判断一个数 n
是不是快乐数。
「快乐数」 定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果这个过程 结果为 1,那么这个数就是快乐数。
思路:
为什么想到用哈希?
因为对于快乐数的操作,要么变成1,要么无限循环
而无限循环就代表着曾经出现的数又再次出现
而哈希表的优势就是快速判断某个数是否存在
由此想到使用哈希的方法
做题过程:
对于 将一个数替换为它每个位置上的数字的平方和 这个操作 不熟练
只要不断取10的模,然后再整除10,就能得到每个位置上的数
while (n>0){
temp=n%10;
sum+=temp*temp;
n=n/10;
}
代码:
class Solution3 {
public boolean isHappy(int n) {
Set<Integer> hashset=new HashSet<>();
//如果变成1,或者表中出现了之前出现的数,就停止
while (!(n==1 || hashset.contains(n))){
//把数放进哈希表里面
hashset.add(n);
n=getSum(n);
}
return n==1;
}
public int getSum(int n){
int sum=0;int temp=0;
while (n>0){
temp=n%10;
sum+=temp*temp;
n=n/10;
}
return sum;
}
}
2.4 两数之和
给定一个整数数组 nums
和一个整数目标值 target
,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
思路:
四个问题
1.为什么想到用哈希法
因为哈希表的优势在于快速寻找一个元素是否存在表中。而这题两数相加,我们可以通过遍历每个数时,寻找对应另一个数是否存在来求解
2.为什么用map
我们不仅要找出数,还要找出数对应的下标,需要存储两个值的哈希表,所以用map
3.map是用来做什么的
存放数的信息,以快速确认某个数是否存在
4.map的key和value是存什么的
key 存放数值
value 存放数的下标
做题过程:
顺利
代码:
class Solution4 {
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> hashmap=new HashMap<>();
int []res=new int[2];
if(nums == null || nums.length == 0){
return res;
}
for(int i=0;i<nums.length;i++){
int cur=target-nums[i];
if(hashmap.containsKey(cur)){
res[0]=hashmap.get(cur);
res[1]=i;
return res;
}
hashmap.put(nums[i],i);
}
return null;
}
}
2.5 四数相加
给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。
为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。
思路:
仿照上一题的两数之和,我们可以两两相结合
比如A,B,C,D
先将AB任意相加结合,值放入哈希表中 ,再遍历CD,观察是否有对应的值出现
注意:这里我们选哈希表时,如果选set,那么AB中相加如果有重复的那将不能记录。
但是题目要求返回元组数量,是允许重复值的(下标不同而已),所以我们需要哈希表能够同时记录值和出现次数
故选择map
解题过程:
在进行map的操作时用了繁琐的代码,但是可以通过函数来一步完成
for (int i:nums1){
for (int j:nums2){
sum=i+j;
// if(hashmap.containsKey((sum))){
// hashmap.replace(sum,hashmap.get(sum)+1);
// }else {
// hashmap.put(sum,1);
// }
//更简略的写法,用函数getOrDefault()
hashmap.put(sum, hashmap.getOrDefault(sum, 0) + 1);
}
代码:
class Solution5 {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int sum=0;
Map<Integer,Integer> hashmap=new HashMap<>();
int count=0;
for (int i:nums1){
for (int j:nums2){
sum=i+j;
// if(hashmap.containsKey((sum))){
// hashmap.replace(sum,hashmap.get(sum)+1);
// }else {
// hashmap.put(sum,1);
// }
//更简略的写法,用函数getOrDefault()
hashmap.put(sum, hashmap.getOrDefault(sum, 0) + 1);
}
}
//再遍历3,4,查看是否存在
for (int i:nums3){
for (int j:nums4){
// if(hashmap.containsKey(-(i+j))){
// count+=hashmap.get(-(i+j));
// }
//更简略的写法,用函数getOrDefault()
count+=hashmap.getOrDefault(-(i+j), 0);
}
}
return count;
}
}
2.6 三数之和 !
给你一个整数数组 nums
,判断是否存在三元组 [nums[i], nums[j], nums[k]]
满足 i != j
、i != k
且 j != k
,同时还满足 nums[i] + nums[j] + nums[k] == 0
。请
你返回所有和为 0
且不重复的三元组。
**注意:**答案中不可以包含重复的三元组。
思路:
这题使用哈希表,因为涉及到去重,所以非常麻烦
可以采用双指针的方式(类似滑动窗口)
先将数组由小到大排序,然后循环遍历第i元素
在每次遍历时,将left指向 i+1,right指向 length-1
判断sum=nums[i]+nums[left]+nums[right] 是否等于0
若sum =0,left++ right–
若sum>0, 将right-- ,减小sum
若sum<0,将left++ 增大sum
麻烦之处在于去重的操作:
1.在for循环的开始,我们要判断i ,看i是否和它的前一位对应的值相等,如果相等,那么就continue
2.在判断sum=0之后,我们要判断left+1是否和left对应的值相同,right-1 是否和right对应的值相同,如果相同,我们就直接+1 或-1 直到他们不再相同 ,我们再移动right -1 ,left +1
做题过程:
坎坷,耗费了数小时
首先对于i的判断就连续出现了两次问题 将continue写成i++,这种错误导致没有消去i对应值相同的重复情况
其次在while (right>left){中 sum值的分类处理下的去重,出现了连环错误
要牢记先去重,后移动right,left
代码:
class Solution7 {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
int left=0;int right=0;int sum=0;
for(int i=0;i<nums.length-2;i++){
//第一步,对i处理
if(nums[i]>0) return list;
//while改为if
if (i>0&&nums[i]==nums[i-1]){
continue;
}
left=i+1;right=nums.length-1;
while (right>left){
sum=nums[i]+nums[left]+nums[right];
if(sum==0){
list.add(Arrays.asList(nums[i], nums[left], nums[right]));
while (right>left&&nums[right]==nums[right-1]){
right--;
}
while (right>left&&nums[left]==nums[left+1]){
left++;
}
right--;
left++;
}else if(sum>0){
right--;
} else {
left++;
}
}
}
return list;
}
}
2.7 四数之和
给你一个由 n
个整数组成的数组 nums
,和一个目标值 target
。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]]
(若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a
、b
、c
和d
互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
思路:
只要弄明白三数之和 这题做起来就容易一些
在三数之和的基础上再套了一层for循环
三数之和是 定一 移2
四叔之和 是 定一后再定一 再移2
去重方面和三数之和类似
剪枝 需要注意 :
这里不是以相加等于0为条件,而是以相加等于target为条件,
而target会有小于0和大于0的情况,需要注意
做题过程:
leetcode上给的最后几个实例都挺逆天的
需要针对性的过滤
代码实现:
class Solution8 {
public List<List<Integer>> fourSum(int[] nums, int target) {
ArrayList<List<Integer>> list = new ArrayList<>();
Arrays.sort(nums);
int i=0,j=0,left=0,right=0,sum=0;
//面向实例的代码
if(target<0 && nums[0]>0){
return list;
}
for(i=0;i<nums.length-3;i++){
//先对i 剪枝
if(i>0 && nums[i]==nums[i-1]){
continue;
}
if(target>0 && nums[i]>target){
return list;
}
for(j=i+1;j<nums.length-2;j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) {
continue;
}
left=j+1;right=nums.length-1;
while (right > left) {
sum = nums[i] + nums[j] + nums[left] + nums[right];
if(sum==target){
list.add(Arrays.asList(nums[i], nums[j],nums[left], nums[right]));
while (right>left &&nums[right]==nums[right-1]){
right--;
}
while (right>left &&nums[left]==nums[left+1]){
left++;
}
left++;
right--;
}else if(sum>target){
right--;
}else {
left++;
}
}
}
}
return list;
}
}
三、字符串
一些疑惑
1.为什么Java做字符串类算法题,用stringbuilder的空间复杂度是O(n),而用char[]的空间复杂度是O(1)
Java中的String是不可变的,这意味着每次对String进行修改操作,如连接、替换等,都会生成一个新的String对象。因此,在进行字符串处理时,如果频繁进行修改操作,会导致大量的对象创建和垃圾回收,从而影响性能。
StringBuilder是为了解决这个问题而设计的,它是一个可变的字符串缓冲区,可以高效地进行字符串的修改操作,而不需要创建新的对象。因此,使用StringBuilder的空间复杂度是O(n),其中n是字符串的长度。
而char[]数组是一个固定大小的字符数组,可以存储一定数量的字符。在进行字符串处理时,如果已知字符串的长度,可以使用char[]数组来存储字符串,这样可以避免创建大量的String对象或StringBuilder对象,从而提高性能。因此,使用char[]的空间复杂度是O(1),其中1是数组的大小。
3.1 反转字符串
给定一个字符串 s 和一个整数 k,从字符串开头算起, 每计数至 2k 个字符,就反转这 2k 个字符中的前 k 个字符。
如果剩余字符少于 k 个,则将剩余字符全部反转。
如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样
思路:
第一种想法:每隔2k进行一次处理, 当剩下距离不够2k时再分别判断
第二种想法:不管距离够不够2k,其实除了不够k之外,都得处理k,
所以可以看成每隔一次k处理一次k
解题过程:
需要注意下标的位置:
每处理完一段之和,需要向前移动一位,开始新的一段的处理
代码:
class Solution2 {
public String reverseStr(String s, int k) {
char[] chars = s.toCharArray();
int temp=2*k-1;int cur=0;
while (temp<s.length()-1){
reverse(chars,cur,cur+k-1);
cur=temp+1;
temp=cur+(2*k-1);
}
if((s.length()-cur)<k){
reverse(chars,cur,s.length()-1);
}else {
reverse(chars,cur,cur+k-1);
}
return String.valueOf(chars);
}
public String reverseStr1(String s, int k) {
char[] chars = s.toCharArray();
int cur=0;
while (cur<s.length()-1){
if((cur+k-1)<=s.length()-1) {
reverse(chars, cur, cur + k - 1);
}else reverse(chars,cur,s.length()-1);
cur+=2*k;
}
return String.valueOf(chars);
}
public char[] reverse(char[] chars,int i,int l){
for(;i<l;i++,l--){
char temp=chars[i];
chars[i]=chars[l];
chars[l]=temp;
}
return chars;
}
}
3.2 替换空格
请实现一个函数,把字符串 s
中的每个空格替换成"%20"。
思路:
双指针法:
先计算出有多少空格,然后在原字符串上进行扩容
接下来使用双指针法,慢指针指向原字符串长度的位置,快指针指向扩容后的字符串长度位置
慢指针和快指针一起走,慢指针将指向的值赋给快指针。
当慢指针指向空格时,快指针走三步,填充上 ‘0’ ‘2’ ‘%’
做题过程:
1.通过题解补充了字符串通过StringBuilder快速解决的知识点
并且,在效率上,StringBuilder的效率一般比StringBuffer高。这主要是因为在单线程环境下,StringBuilder不需要进行线程同步操作,因此能保持更高的效率。但如果在多线程环境下,StringBuffer的效率会更高,因为其线程安全保证了操作的正确性和一致性。
2.用双指针方法时,
要注意:
第一:
扩容时是扩容2个空格,因为自身还有1个,加起来3个单位
第二:
注意指针的加减变化,如果操作不当会引发数组越界
代码
class Solution3 {
//第一种方法:用StringBuilder.append快速加入
public String replaceSpace1(String s) {
StringBuilder sb=new StringBuilder();
for(int i=0;i<=s.length()-1;i++){
if(s.charAt(i)==' '){
sb.append("%20");
}else sb.append(s.charAt(i));
}
return new String(sb);
}
//第二种方法:双指针
public String replaceSpace2(String s) {
StringBuilder sb=new StringBuilder();
for(int i=0;i<=s.length()-1;i++){
if(s.charAt(i)==' '){
sb.append(" ");
}
}
if(sb.length()==0) return s;
int left=s.length()-1;
s+=sb.toString();
char[] chars=s.toCharArray();
int right=chars.length-1;
while (left>=0){
if(chars[left]==' '){
chars[right--]='0';
chars[right--]='2';
chars[right--]='%';
left--;
}else chars[right--]=chars[left--];
}
return new String(chars);
}
}
3.3 翻转字符串里的单词
给你一个字符串 s
,请你反转字符串中 单词 的顺序。
单词 是由非空格字符组成的字符串。s
中使用至少一个空格将字符串中的 单词 分隔开。
返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。
**注意:**输入字符串 s
中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。
思路:
关键点:
- 如何修剪掉两端空格;
- 如何把单词反转过来;
- 如何跳过中间连续的空格。
这题 按卡哥的思路是:
1.去除两端多余空格
2.将整个字符串反转
3.反转每个单词并去除字符串里面多余空格
但是操作起来麻烦,并且移动后怎么去除多余的空格,由于Java没有对应方便的函数,所以十分不方便
所以用stringbuilder做更方便
思路是:
1.去除两端多余的空格
2.从末尾开始,依次将每个单词append到sb
使用双指针,依次读取单词,非常方便易懂
做题过程:
开始听了卡哥思路用char[] 写了个狗屎代码,虽然过了,但是效率很低而且丑陋
对于字符串题目,如果没有强求O(1),还是用 StringBuilder好一些
代码:
class Solution4_ {
public String reverseWords(String s) {
StringBuilder sb=new StringBuilder();
int left=0;int right=s.length()-1;
//先去除两端多余的空格
while (left<=s.length()-1&&s.charAt(left)==' '){left++;};
while (right>=0&&s.charAt(right)==' '){right--;};
//指定双指针 index 和right
int index=right;
//left标记终点
//每次循环加一个单词
while (left<=right){
//获取一个单词的长度,
//使 index指向这个单词头部前的位置,right指向这个单词的末尾
while (index>=left &&s.charAt(index)!=' '){
index--;
}
//从index+1开始遍历,可以将这个单词加入到sb中
//每次循环加一个单词
for(int i=index+1;i<=right;i++){
sb.append(s.charAt(i));
}
//如果不是最后一个被获取的单词,就在sb后加空格
if(index>=left){
sb.append(' ');
}
//刷新index到下一个单词的尾部
while (index>=left && s.charAt(index)==' '){
index--;
}
//刷新right
right=index;
}
return sb.toString();
}
}
3.4 左旋转字符串
字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。
思路:
这题巧妙思路就是,可以先对于n的两边,各自反转,然后再一起反转
这样就达到了目的
做题过程:
stringbuilder真方便
代码:
class Solution {
//用sb,空间复杂度为O(n)
public String reverseLeftWords(String s, int n) {
if(n==0) return s;
StringBuilder sb = new StringBuilder(s);
for(int i=0;i<=n-1;i++){
sb.append(s.charAt(i));
}
sb.delete(0,n);
return sb.toString();
}
//用char[],空间复杂度为O(1)
public String reverseLeftWords1(String s,int n){
char[] chars = s.toCharArray();
reverse(chars,0,n-1);
reverse(chars,n,chars.length-1);
reverse(chars,0,chars.length-1);
return new String(chars);
}
public void reverse(char[] chars,int i,int j){
char temp;
for(;i<j;i++,j--){
temp=chars[i];
chars[i]=chars[j];
chars[j]=temp;
}
}
}
3.5 找出字符串中第一个匹配串的下标
给你两个字符串 haystack
和 needle
,请你在 haystack
字符串中找出 needle
字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle
不是 haystack
的一部分,则返回 -1
。
思路:
- 暴力 (滑动窗口)
- kmp 痛苦
做题过程:
痛苦
代码:
class Solution {
//前缀表(不减一)Java实现
public int strStr(String haystack, String needle) {
if (needle.length() == 0) return 0;
int[] next = new int[needle.length()];
getNext(next, needle);
int j = 0;
for (int i = 0; i < haystack.length(); i++) {
while (j > 0 && needle.charAt(j) != haystack.charAt(i))
j = next[j - 1];
if (needle.charAt(j) == haystack.charAt(i))
j++;
if (j == needle.length())
return i - needle.length() + 1;
}
return -1;
}
private void getNext(int[] next, String s) {
int j = 0;
next[0] = 0;
for (int i = 1; i < s.length(); i++) {
while (j > 0 && s.charAt(j) != s.charAt(i))
j = next[j - 1];
if (s.charAt(j) == s.charAt(i))
j++;
next[i] = j;
}
}
}
3.6 重复的子字符串
给定一个非空的字符串 s
,检查是否可以通过由它的一个子串重复多次构成。
思路:
1.移动匹配
将两个相同的字符串拼在一起形成一个新字符串,
如果原字符串符合条件,那么就能在新字符串中找到原字符串
缺点:使用了库函数,时间复杂度较高
2.使用kmp算法
如果是符合条件的字符串,那么最小重复字串就是最长相等前后缀不包含的部分
那么求出next数组,然后取next[length-1]就行了
3.移位
假设基串长度为x,向左移动x和向右移x,如果原字符串符合条件,那么重叠部分就会相同
做题过程:
我是若只
代码:
class Solution7 {
//移动匹配
public boolean repeatedSubstringPattern1(String s) {
String s1=s.substring(1)+s.substring(0,s.length()-1);
return s1.contains(s);
}
//kmp
public boolean repeatedSubstringPattern2(String s) {
int[] next=getnext(s);
System.out.println(Arrays.toString(next));
return (s.length())/(s.length()-next[next.length-1]) >=2;
}
public int[] getnext(String s){
int[] next=new int[s.length()];
next[0]=0;
for(int j=0,i=1;i<s.length();i++){
while (j>0 &&s.charAt(i)!=s.charAt(j)){
j=next[j-1];
}
if(s.charAt(i)==s.charAt(j)){
j++;
}
next[i]=j;
}
return next;
}
//牛逼大佬的解法
//设基串长度为x,通过移位比较:假设是一个符合条件的字符串,
// 如果一个向左移动x,一个像右移动x,那么他们重叠的部分相同
public boolean repeatedSubstringPattern3(String s) {
int lens=s.length();
for(int i=1;i<=s.length()/2;i++) {
if (lens % i != 0) continue;
//判断是不是基串
if (s.substring(0, i).equals(s.substring(lens -i, lens ))) {
//判断重叠部分是否相同
if (s.substring(i, lens ).equals(s.substring(0, lens - i))) return true;
}
}
return false;
}
}
四、栈和队列
4.1 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
):
实现 MyQueue
类:
void push(int x)
将元素 x 推到队列的末尾int pop()
从队列的开头移除并返回元素int peek()
返回队列开头的元素boolean empty()
如果队列为空,返回true
;否则,返回false
思路:
使用两个栈,一个接受进栈的数据,一个负责出栈的数据
为什么分两个,是为了出栈时能够按照先入先出的顺序来
所以需要注意处理时,不能乱了顺序。
pop时,一定要先将stackOut中的数据pop完之后,然后再一次性将stackIn的数据输入到stackOut,保证顺序的连续
做题过程:
在进行 peek()时,可以直接拿写好的pop用。
因为peek是查看头数据而不是弹出,所以我们用pop完后就要再补充回来。
这里补充一定是补充到 stackOut中,所以是stackOut.push(res);
如果我们直接用push(),那么就push进stackIn了,造成错误
代码:
class MyQueue {
Stack<Integer> stackIn =null;//负责进栈
Stack<Integer> stackOut =null;//负责出栈
public MyQueue() {
stackIn =new Stack<>();
stackOut =new Stack<>();
}
public void push(int x) {
stackIn.push(x);
}
public int pop() {
if(!stackOut.empty()){
return stackOut.pop();
}else {
while (!stackIn.empty()) {
stackOut.push(stackIn.pop());
}
}
return stackOut.pop();
}
//巧写:拿上面的方法直接用
public int peek() {
int res=pop();
stackOut.push(res);
return res;
}
public boolean empty() {
if(stackOut.empty())
if(stackIn.empty()) return true;
return false;
}
}
4.2 有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
思路:
用栈来解决
遇到左部分时转成右部分放入栈,当遇到右部分时进行比较
做题过程:
代码:
class Solution3 {
public boolean isValid(String s) {
Stack<Character> chars = new Stack<>();
//剪枝
if(s.length()%2 !=0) return false;
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
if(c=='{') chars.add('}');
else if(c=='(') chars.add(')');
else if(c=='[') chars.add(']');
//if (chars.empty()) return false;
if(c=='}'||c==']'||c==')') if(chars.pop()!=c) return false;
System.out.println(chars.isEmpty());
}
if(chars.empty()) return true;
else return false;
}
}
4.3 删除字符串中的所有相邻重复项目
给出由小写字母组成的字符串 S
,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。
思路:
使用栈的思想来解决
做题过程:
这题直接用栈,效率较低
我们可以用其它数据结构来模拟栈的进出,从而实现
开始直接用了stack,200ms
然后改用sb,直接变为了13ms,很香
代码:
class Solution5 {
//直接用栈,效率不是很高
public String removeDuplicates(String s) {
Stack<Character> chars = new Stack<>();
//chars.add(s.charAt(0));
for (int i = 0; i < s.length(); i++) {
if (chars.empty() || s.charAt(i) != chars.peek())
chars.add(s.charAt(i));
else chars.pop();
}
if (chars.size() == 0) return "";
StringBuilder sb = new StringBuilder();
while (!chars.empty()) sb.append(chars.pop());
return sb.reverse().toString();
}
//用sb,模拟栈的思想
public String removeDuplicates2(String s) {
StringBuilder sb=new StringBuilder();
char[] chars = s.toCharArray();
for (int i = 0; i < s.length(); i++) {
if (sb.length()==0 || chars[i] != sb.charAt(sb.length()-1))
sb.append(chars[i]);
else sb.delete(sb.length()-1,sb.length());
}
return sb.toString();
}
}
4.4 滑动窗口最大值
给你一个整数数组 nums
,有一个大小为 k
的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k
个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
思路:
自定义一个单调队列,使得最大的数一直在队头,这样的话获取最大值,只需要pop就行了
解题过程:
在做题时,不清楚函数的功能导致卡了一段时间
对于LinkedList队列,add和push函数都是进行添加操作
但是因为LinkedList是双向链表,add是在队尾添加,而push是在队头添加
代码
class Solution6 {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res=new int[nums.length-k+1];
int temp=0;
MyQueue_ myQueue_ = new MyQueue_();
for(int i=0;i<k;i++){
myQueue_.add(nums[i]);
}
res[temp++]=myQueue_.getMax();
for(int i=k;i<nums.length;i++){
myQueue_.add(nums[i]);
myQueue_.pop(nums[i-2]);
res[temp++]=myQueue_.getMax();
}
return res;
}
}
//自定义单调队列
class MyQueue_{
Deque<Integer> integers = null;
MyQueue_(){integers=new LinkedList<>();}
//自定义弹出的操作
public int pop(int num){
if(integers.size()!=0 && integers.peek()==num){
return integers.pop();
}
return -1000;
}
//自定义加入的操作
public void add(int x){
while (integers.size()!=0 &&integers.getLast()<x){
integers.removeLast();
}
integers.add(x);
}
//获取最大值
public int getMax(){
return integers.peek();
}
}
4.5 前 K 个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
思路:
1.涉及到频率的点,我们可以用哈希法同时记录这个数和它出现的频率
2.用小顶堆来维护和接受高频率的数
3.根据题目要求,只维护大小为k的堆,这样可以提高效率
解题过程:
PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
pair1,pair2)->pair1[1]-pair2[1] 的含义是什么
这是Java中的Lambda表达式,用于定义PriorityQueue的排序规则。
'pair1’和’pair2’是int数组,这个表达式比较的是每个数组的第二个元素(索引为1的元素)。当PriorityQueue中的两个元素需要比较时,会调用这个Lambda表达式。如果pair1[1]大于pair2[1],那么pair1就被认为是’较大’的元素,反之亦然。如果pair1[1]等于pair2[1],那么这两个元素的优先级就相同。
代码:
class Solution7 {
public int[] topKFrequent(int[] nums, int k) {
// key代表这个数,value代表这个数出现的次数
HashMap<Integer, Integer> maps= new HashMap<>();
for(int num:nums){
maps.put(num,maps.getOrDefault(num,0)+1);
}
//小顶堆,定义为int数组的int[1]来决定排序
PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((pair1,pair2)->pair1[1]-pair2[1]);
for(Map.Entry<Integer,Integer> entry:maps.entrySet()){
//维护一个大小为k的堆,提高了效率
//如果大小小于k,就直接加入
if(priorityQueue.size()<k){
priorityQueue.add(new int[]{entry.getKey(),entry.getValue()});
}else {
如果大于k,就比较堆头的出现次数和要加进来的数的出现次数
if(priorityQueue.peek()[1]<entry.getValue()){
priorityQueue.poll();
priorityQueue.add(new int[]{entry.getKey(),entry.getValue()});
}
}
}
int[] res=new int[k];
int temp=k;
//因为小顶堆是从小到大pop出,所以我们逆序接受
while (priorityQueue.size()!=0){
int[] poll = priorityQueue.poll();
res[--temp]=poll[0];
}
return res;
}
}