哈希
1.两数之和:
给定一个整数数组nums和一个整数目标值target,请你再该数组中找出和为目标值target的那两个整数,并返回它们的数组下标。
思路:暴力解法是使用两层循环来遍历每一个数,然后找出两数之和等于target的数,但是这样时间复杂度就是O(n^2)。为了减少时间复杂度,我们可以考虑用空间换时间,考虑使用HashMap。HashMap内部是使用数组来存储数据的,因此他提供O(1)查找效率。我们再遍历数组的时候,同时将元素插入到HashMap中,然后我们检查target减去当前元素后的值再HashMap中是否存在,存在的话说明找到了两个数组元素相加等于target,将这两个元素用一个新的数组保存起来作为最终返回的答案即可。这样做我们只需要遍历一次数组就可以找到结果把时间复杂度降低到O(n)。
import java.util.*;
import java.io.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int target = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++){
nums[i] = in.nextInt();
}
Map<Integer,Integer> map = new HashMap<>();
int[] res = new int[2];
for(int i=0;i<n;i++){
int num = target - nums[i];
if(map.containsKey(num)){
res[0] = i;
res[1] = map.get(num);
}
map.put(nums[i],i);
}
System.out.println(Arrays.toString(res));
// 对于二维数组或多维数组可以使用Arrays.deepToString()
// System.out.println(Arrays.deepToString(res));
}
}
2.字母异位词分组
给你一个字符串数组,请你将字母异位词组合在一起。可以按任意顺序返回结果列表。字母异位词是由重新排列源单词的所有字母得到的一个新单词。
思路:因为是字母异位词,因为把他们提取出来做个排序,得到的字符串肯定是相等的,因此我们可以利用HashMap的key-value的数据结构,把排序后的字符串作为key,这样遍历到数组中的字符串时,先做个排序,然后查询下HashMap中是否有该字符串,没有的话就创建一个然后加入进去,有的话就直接加入到该key的value中,因为value里面可能存放不止一个字符串,因此构建HashMap的时候valu的结构需要是List<String>,这样遍历一遍之后就一键分类完成了。
import java.util.*;
import java.io.*;
public class Main {
public static void main(String args[]) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
in.nextLine(); // 消费掉nextInt后的换行符
if(n == 0) {
System.out.println(new ArrayList<>());
return;
}
String[] strs = new String[n];
for (int i = 0; i < n; i++) {
strs[i] = in.nextLine();
}
Map<String, List<String>> map = new HashMap<>();
for (String s : strs) {
char[] c = s.toCharArray();
Arrays.sort(c);
String sorted = new String(c);
// if(!map.containsKey(sorted)){
// map.put(sorted,new ArrayList<>());
// }
map.putIfAbsent(sorted, new ArrayList<>()); // 更简洁的方式来处理不存在的键
map.get(sorted).add(s); // 正确的方法是add
}
// 直接打印map的values即可,它会自动调用toString()方法
System.out.println(new ArrayList<>(map.values()));
}
}
3.最长连续序列
给定一个未排序的整数数组nums,找出数字连续的最长序列(不要求序列元素在元数组中连续)的长度。请设计并实现时间复杂度为O(n)的算法解决此问题。
思路:我们可以思考一种办法,就是遍历每一个数,然后以该数为起点,通过+1的方式继续寻找下一个连续的数,然后记录最大的连续长度,因为HashSet(没必要使用HashMap,键值对在这题用不上,只需要存储唯一的元素集合即可)可以提供O(1)查找时间复杂度,因此我们可以考虑先把数组里面的元素放入到HashSet中,然后再遍历数组中的每一个元素,如果改元素在数组里没有连续的前缀元素,说明可以以该元素作为起点开始查找连续序列,然后设置计数器,遍历完之后比较以该元素作为起点的连续序列长度是否是最长。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++) nums[i] = in.nextInt();
Set<Integer> set = new HashSet<>();
for(int i=0;i<n;i++) set.add(nums[i]);
int res = 0;
for(int i=0;i<n;i++){
if(!set.contains(nums[i] - 1)){
int num = nums[i] + 1;
int count = 1;
while(set.contains(num)){
count++;
num++;
}
res = Math.max(res,count);
}
}
System.out.println(res);
}
}
双指针
4.移动零
给定一个数组nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。请注意,必须在不复制数组的情况下原地对数组进行操作。
思路:我们可以考虑先把不是0的元素往前排,然后把是0的元素在后边补上就可以了。具体的操作是,遍历数组,如果遍历到0就跳过,遍历到非0就操作指针。操作指针为双指针思路,i用来遍历数组,k用来标记非0元素调整后的位置。遍历一次后,非零元素就会全部排在前面,然后用0元素去填充n-k内的数组就完成了。
import java.util.*;
import java.io.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++) nums[i] = in.nextInt();
int k = 0;
for(int i=0;i<n;i++){
if(nums[i]!=0) nums[k++] = nums[i];
}
for(int i=k;i<n;i++) nums[i] = 0;
System.out.println(Arrays.toString(nums));
}
}
5.盛最多水的容器
给定一个长度为n的整数数组height。有n条垂线,第i条线的两个端点是(i,0)和(i,height[i])。找出其中的两条线,使得它们与x轴共同构成的容器可以容纳最多的水。
思路:改题目的意思相当于找出最大的能够围城矩形面积的两个端点,因为矩形的高是取两个端点高的最小值。因为是找出两端,因此就得考虑使用双指针的方法,一个指针放在开头,一个指针放在末尾,让后向中间遍历。指针移动的时候有个细节,就是左右两个指针,应该先移动哪一个;这时我们需要思考,高度和宽度对面积的影响了,因为指针每向内移动一个,宽度就会-1。
如果两个指针先移动高度高的那个,会出现的情况是:得到矩形面积一定会变小,因为长的部分向内移动,矩形的高度最多不变(高度是由二者小的那个决定),但是宽度必定缩小,因此计算出来的面积一定变小。
如果两个指针先移动高度低的那个,会出现的情况是:得到矩形面积即有可能变小,也有可能变大。因为假如内部遍历到提升的高度大于1的,面积就会增大。
自此,确定思路,设置两个指针,一个开头,一个末尾,然后遍历数组,每次高度短的指针向内移动,然后计算面积和目前最大面积进行比较,最后输出resArea面积,面积的计算公式是Area = Math.min(height[l], height[r]) * (r - l)。
import java.util.*;
import java.io.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] height = new int[n];
for(int i=0;i<n;i++) height[i] = in.nextInt();
int l = 0, r = n-1;
int resArea = 0;
while(l < r){
resArea = Math.max(resArea, Math.min(height[l],height[r]) * (r-l));
if(height[l] < height[r]) l++;
else r--;
}
System.out.println(resArea);
}
}
6.三数之和
给你一个整数数组nums,判断是否存在三元组[nums[i],nums[j],nums[k]]满足i != j,i != k,且j != k,同时还满足nums[i] + nums[j] + nums[k] == 0。请你返回所有和为0且不重复的三元组。
思路:暴力解法就是通过三层的for循环一个个遍历找到答案,并且要进行去重,但是这样写的话时间复杂度就是O(n^3)显然太高了。可以考虑使用排序+双指针,之所以要先排序,是因为顺序数组方便双指针进行遍历,例如吧双指针一个放置在开头,一个放置在末尾,如果数组时顺序的就能够较为轻松的控制最终三数之和的大小。不过这里的双指针方式和先前的有所不同,其实可以算是三指针,遍历数组的指针i,还有以该指针为成员,从数组后这个范围寻找另外两个成员,一个l定义为i+1,一个r定义为nums.length-1,然后便利[l ,r]之间的元素,找出nums[i] + nums[l] + nums[r] == 0的组合将其加入到最终list中。去重,当遇到当前遍历的数与前一个数相同,则直接continue跳过,当找到了一组三元组之后,l和r的指针移动时也要跳过重复的数。
自此,代码逻辑为,先对数组进行排序,然后遍历数组,若遇到当前元素和上一个元素相同则直接continue跳过,然后定当前元素为成员1,然后设置双指针,l = i+1,l = nums.length-1。while(l < r)开始遍历。当找到一组nums[i] + nums[l] + nums[r] == 0时,先把组合加入到list中,然后移动l和r指针到新的不重复的位置,继续遍历。最终返回结果。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] nums = new int[n];
for(int i = 0;i < n;i++) nums[i] = in.nextInt();
List<List<Integer>> res = new ArrayList<>();
Arrays.sort(nums);
for(int i=0;i<n;i++){
if(i>0 && nums[i] == nums[i-1]) continue;
int l = i+1,r = n-1;
while(l < r){
int sum = nums[i] + nums[l] + nums[r];
if(sum == 0){
res.add(Arrays.asList(nums[i],nums[l],nums[r]));
while(l<r && nums[r]==nums[r-1])r--;
while(l<r && nums[l]==nums[l+1])l++;
l++;r--;
}
else if(sum > 0)r--;
else l++;
}
}
System.out.println(res.toString());
}
}
7.接雨水
给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
思路:每个点能接水的量取决于他左边的最大高度和他右边的最大高度比较之后的最小值,之所以需要知道两边高度是因为知道两边才能围成一个凹槽。因为需要记录两边高度,因此我们可以考虑使用双指针的方法,左指针设置在数组开头,右指针设置在末尾,同时设置两边的最大高度lmax和rmax,然后开始遍历数组。同样采用先行移动高度低的指针,先计算要移动指针该点的接水量,接水量的计算方法是两侧最大高度的最小值减去该点的高度就能得到,然后把它加到接水总量的计数变量中,然后在移动指针。遍历结束后返回计数变量值。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] height = new int[n];
for(int i=0;i<n;i++) height[i] = in.nextInt();
int l = 0, r = n-1;
int lmax = 0,rmax = 0;
int ressum = 0;
while(l < r){
lmax = Math.max(lmax, height[l]);
rmax = Math.max(rmax, height[r]);
if(lmax < rmax) ressum += lmax - height[l++];
else ressum += rmax - height[r--];
}
System.out.println(ressum);
}
}
滑动窗口
8.无重复字符的最长子串
给定一个字符串s,请你找出其中不含有重复字符的最长子串的长度。
思路:因为是求最长的子串,我们可以考虑用一个哈希表来记录遍历过的字符,使用HashMap,用key-value的格式记录字符和其存在的位置,然后使用双指针。之所以使用双指针是因为要计算长度需要知道其子串的首尾。先创建start=0和end=0,然后开始遍历s字符串,先判断当前遍历的字符串是否在HashMap中已存在,如果已存在说明是重复字符,先进行判断,这个重复字符map的value的位置+1的值会不会使目前start值会退,就是变小,变小的话就不需要更新start的值了,因为start的值是不能会退的,回退必然导致重复。如果变大的话,就更新胃当前重复字符在map中的value位置+1,保证start->end这个范围内是没有重复的字符的。每次遍历都进行end-start+1的长度计算,然后用这个长度和最终长度计数变量进行比较,保证该变量一直是最大子串长度。如果当前字符不是重复的,则直接加入到map中。遍历结束后输出计数变量。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
String s = in.nextLine();
int n = s.length();
Map<Character,Integer> map = new HashMap<>();
int start = 0, end = 0;
int reslen = 0;
for(end = 0;end < n;end++){
if(map.containsKey(s.charAt(end))){
start = Math.max(map.get(s.charAt(end)) + 1, start);
}
reslen = Math.max(reslen, end - start + 1);
map.put(s.charAt(end),end);
}
System.out.println(reslen);
}
}
9.找到字符串中所有字母异位词
给定两个字符串s和p,找到s中所有p的异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。异位词指由相同字母重排列形成的字符串(包括相同的字符串)。
思路:因此是要找到p的异位词,因此我们需要控制一个长度,这个长度等于p字符串的长度,也就是窗口,然后拿着这个窗口去s里面比对。比对的方法是,创建两个可以包含26个字母的数组,一个是记录p窗口字母的个数,一个是记录s窗口字母个数,然后先把p的长度的字符转化为数组下标个数存入到数组中,然后先比较一下两个数组是否相等,相等说明开头的p长度就存在一个异位词,然后将其开头的下标加入到res列表中。然后继续遍历,左边收缩窗口,右边继续移动窗口,移动的方式为左边的元素依次[]--退出窗口右边为i+p.length()然后++进入窗口,然后再判断两个数组是否相等,相等就说明找到了一个异位词,然后将其开头的下标加入到res列表中。最后返回res列表。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
String s = in.nextLine();
String t = in.nextLine();
int[] scount = new int[26];
int[] tcount = new int[26];
int n = s.length();
int m = t.length();
if(n<m){
System.out.println(new ArrayList<>());
return ;
}
List<Integer> res = new ArrayList<>();
for(int i=0;i<m;i++){
scount[s.charAt(i) - 'a']++;
tcount[t.charAt(i) - 'a']++;
}
if(Arrays.equals(scount,tcount)) res.add(0);
for(int i=0;i<n-m;i++){
scount[s.charAt(i) - 'a']--;
scount[s.charAt(i+m) - 'a']++;
if(Arrays.equals(scount,tcount)) res.add(i+1);
}
System.out.println(res);
}
}
子串
10.和为k的子数组
给你一个整数数组nums和一个整数k,请你统计并返回该数组中和为k的子数组个数。子数组时数组中元素的连续非空序列。
思路:因为是要找到连续序列和等于k的个数,我们可以用两层遍历,第一层就是遍历数组,第二层就是从遍历到的当前下下标开始往回遍历,把从这个下标开始之前的所有数都加起来,然后查看是否等于k,等于k则count++。最后返回count。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int k = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++) nums[i] = in.nextInt();
int count = 0;
for(int i=0;i<n;i++){
int sum = 0;
for(int j=i;j>=0;j--){
sum += nums[j];
if(sum == k) count++;
}
}
System.out.println(count);
}
}
11.滑动窗口最大值
给你一个整数数组nums,又一个大小为k的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到滑动窗口内的k个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。
思路:因为是滑动窗口,因为我们需要手动维护,维护的方式是每次向右移动的时候,左边窗口缩减,右边窗口增加。因为需要记录每一个窗口的最大值,因此需要做一些特殊的处理:
(1)创建一个res结果数组,数组长度为输入nums的长度 - 窗口长度k + 1用于记录每一个窗口的最大值
(2)每次窗口滑动的时候都要判断,队头元素的下标是否已经小于窗口左侧的下标,如果小于那就要出队,因为要维持窗口大小不变,所以左侧先入队的元素不能小于目前窗口的左侧值,左侧值的计算是i-k+1,i为当前遍历到的数组下标,k为窗口大小。
(3)需要设计一个队列来维持窗口内的元素,队列里面记录是遍历的数组元素的下标,每次窗口滑动的时候都要拿队尾的下标和新进的下标进行nums[]对比,比新进的值小的队尾都出列都出队。然后新进的再入对
然后判断如果i-k+1>=0说明完整的窗口已经形成,因为一开始滑动的时候是从0开始的,每次向右移动一格,所以一开始可能窗口还未形成。如果已经形成窗口,然后就记录当前窗口的最大值,最大值为nums[que.peek()]队头元素肯定是最大值,因为步骤(3)的时候进队时比他小的元素都出队了,所以当他变成队头时一定是队列力的最大值。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
if(n == 0) System.out.println(new int[0]);
int k = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++) nums[i] = in.nextInt();
int[] res = new int[n-k+1];
Deque<Integer> q = new LinkedList<>();
for(int i=0;i<n;i++){
while(!q.isEmpty() && q.peek() < (i-k+1)) q.poll();
while(!q.isEmpty() && nums[q.peekLast()] < nums[i]) q.pollLast();
q.offer(i);
if(i - k + 1 >= 0) res[i-k+1] = nums[q.peek()];
}
System.out.println(Arrays.toString(res));
}
}
12.最小覆盖子串
给你一个字符串s,一个字符串t。返回s中涵盖t所有字符的最小字串。如果s中不存在涵盖t所有字符的子串,则返回空字符串""。
思路:因为是最小包含t字符串的子串,因此我们可考虑使用HashMap,因为其实我们只需要记录t字符串中每个字符出现的次数就可以了。同时因为是求子串,因此我们需要记录长度,因此我们可以考虑使用滑动窗口的思路。
代码逻辑:
(1)创建两个HashMap,twin,window来记录窗口和子串t中的字符个数
(2)创建left和right用来表示窗口的左侧和右侧
(3)创建len初始化为Integer.MAX_VALUE为窗口最小长度,即最小子串的长度
(4)创建start来表示最小子串的起始位置
(5)创建valid来记录遍历s字符串时包含t字符串中字符的数量
(6)先讲t字符串中的字符和数量都记录到HashMap中
(7)遍历s字符串中的字符c,如果遇到字符是t字符串中存在的(twin.containsKey(c)),则将其加入到记录s字符串字符的HashMap中(window.put(c,window.getOrDefault(0)+1)),同时判断如果该字符在map中的数量在两个HashMap中都相等(window.get(c).equals(twin.get(c))),则让valid++。
(8)当valid==twin.size()说明此时window中已经包含了t字符串中所有的字符,因为每次valid++说明window已经包含了t字符串中某个字符的所有数量。twin.size()表示t字符串中所有的不同字符,因此valid == twin.size()就表示window已经包含了t字符串中所有不同字符及其数量。
(9)然后进入二层循环,如果right-left < len说明是找了新的更小的子串,于是把len = right - left;
start = left;
(10)接着该窗口已经满足,因此继续遍历,因此需要left++;然后把left的字符剔除。如果剔除之后导致子串不再满足包含t中所有字符,则需要相应的valid--。char d = s.charAt(left);left++;if(twin.containsKey(d)){if(window.get(c).equals(twin.get(c)))valid--;window.put(d,window.get(d)-1);}
(11)最后输出,需要判断len的长度是否还是初始化长度,是的话说明s中找不到一个包含t所有字符串的子串则输出"",否则输出s.substring(start,start+len);
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
String s = in.nextLine();
String t = in.nextLine();
Map<Character,Integer> twin = new HashMap<>();
Map<Character,Integer> window = new HashMap<>();
for(char c:t.toCharArray()) twin.put(c,twin.getOrDefault(c,0)+1);
int valid = 0;
int start = 0;
int len = Integer.MAX_VALUE;
int left = 0;
int right = 0;
while(right < s.length()){
char c = s.charAt(right);
right++;
if(twin.containsKey(c)){
window.put(c,window.getOrDefault(c,0)+1);
if(window.get(c).equals(twin.get(c))) valid++;
}
while(valid == twin.size()){
if(right-left < len){
len = right - left;
start = left;
}
char d = s.charAt(left);
left++;
if(twin.containsKey(d)){
if(window.get(d).equals(twin.get(d))) valid--;
window.put(d,window.get(d)-1);
}
}
}
System.out.println(len==Integer.MAX_VALUE?"":s.substring(start,start+len));
}
}
普通数组
13.最大子数组和
给你一个整数数组nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组的一个连续部分。
思路:因为是求最大和,因此我们在累加的时候,也就是在考虑加上当前数组下标的元素的时候需要考虑前面加和的情况,类似递推的形似,当我们遇到可以使用递推的思路进行解题时,就可以去思考使用动态规划的思想。具体逻辑
(1)遍历然后累加,但是如果后面累加上来的数字已经小于0了,即nums[i]+dp[i-1] < nums[i],就果断放弃然后以当前下标作为起点重新累加
(2)再遍历一遍找出dp最大值,输出
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++) nums[i] = in.nextInt();
int[] dp = new int[n];
dp[0] = nums[0];
int res = nums[0];
for(int i=1;i<n;i++){
dp[i] = Math.max(nums[i],dp[i-1]+nums[i]);
}
for(int i=0;i<n;i++){
res = Math.max(res,dp[i]);
}
System.out.println(res);
}
}
14.合并区间
以数组intervals表示若干个区间的集合,其中单个区间为intervals[i] = [starti,endi]。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
思路:因为是要合并重复的区间,因此我们需要先把数组根据左区间的大小进行一个排序,这样就方便后续的合并操作,这个合并操作涉及到左区间和右区间,因为要比较上一个范围的右区间和当前范围的左区间是否重合来判断是否需要合并,因此我们需要考虑使用双指针的方法来进行区间判断。具体逻辑为:
(1)先以每个数组的左区间标的值来进行排序
(2)创建一个新的List<int[]> res来存储合并后的集合
(3)创建一个start和end来代表左区间和右区间的指针,初始化为第一个数组的左右区间
(4)遍历数组intervals,用遍历到当前数组的左区间和end进行比较,如果左区间小于end,说明前一个区间和当前区间可以进行合并,start不变,令end变为当前区间的右区间和原始end最大值,end = Math.max(end,intervals[i][1])。如果当前数组的左区间大于end,说明合并不了,那就吧start到end的区间存入到res中(res.add(new int[]{start,end})),然后令start = intervals[i][0];end = intervals[i][1];作为新的起始指针进行遍历。
(5)遍历结束后,最后的start和end还没有存入,先存入res.add(new int[]{start,end});因为遍历时得逻辑是只有在发现当前区间的左区间大于end才进行存入的,这就会导致最后遍历完如果没遇到大于的就不会加入,因此需要手动把最后一个区间加入
(6)因为需要返回一个二维数组,因此需要将ArrayList转化为二维数组res.toArray(new int[res.size()][])
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[][] intervals = new int[n][2];
for(int[] num : intervals){
num[0] = in.nextInt();
num[1] = in.nextInt();
}
List<int[]> res = new ArrayList<>();
int start = intervals[0][0];
int end = intervals[0][1];
for(int i=1;i<n;i++){
if(intervals[i][0] > end){
res.add(new int[]{start,end});
start = intervals[i][0];
end = intervals[i][1];
}else{
end = Math.max(end,intervals[i][1]);
}
}
res.add(new int[]{start,end});
System.out.println(Arrays.deepToString(res.toArray(new int[res.size()][])));
}
}
15.轮转数组
给定一个整数数组nums,将数组中的元素向右轮转k个位置,其中k是非负数。
思路:如果我们把数组向右移动k次,实际上是把末尾的k个数移动到开头,所以我们换一个思路来思考,移动之后,开头的前k个数就是移动前末尾的k个数。我们可以考虑直接把整个数组做一个翻转,那么末尾的数自然就来到了开头,但是目前是倒序的,我们再把开头的前k个数做一个单独的翻转,就实现了移动后前个k数是末尾的k个数。然后我们再把剩下的数再单独翻转一下,完成了整个数组向右移动k次的功能。但是需要考虑一个点,就是k是有可能取值大于数组的长度的,但是其实有效的部分不过只是移动了n圈之后继续移动的部分,因为加入你移动的次数等于数组的长度,结果就是相当于没移动,所以我们直接对k取个模就行了k = k%nums.length;
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int k = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++) nums[i] = in.nextInt();
k = k%n;
reverse(nums,0,n-1);
reverse(nums,0,k-1);
reverse(nums,k,n-1);
System.out.println(Arrays.toString(nums));
}
public static void reverse(int[] nums,int l, int r){
while(l < r){
int tmp = nums[l];
nums[l] = nums[r];
nums[r] = tmp;
l++;r--;
}
}
}
16.除自身以外数组的乘积
给你一个整数数组nums,返回数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。题目数据保证数组nums之中任意元素的全部前缀元素和后缀的乘积都在32位整数范围内。请在O(n)时间复杂度内完成,不要使用除法。
思路:因为是乘出自己以外的其他数,而且不能是除法,还需要o(n)的复杂度,我们可以考虑多次遍历,主要是两次遍历,第一次从前向后遍历,这次遍历的任务是记录以当前数组下标为中心,只计算除自身外之前的乘积。第二次遍历从后向前,只计算除自身外之后的乘积,然后两次遍历的结果相乘就是答案。我们可以考虑使用answer数组在第一次遍历的时候记录之前的乘积,但第二次遍历的时候,用一个num来记录乘积,然后遍历的时候先计算num乘积值,然后再让num和answer的当前数组下标的值相乘,就是最终答案。记录之前和之后乘积的方法就是累乘,但是累乘的时候不要乘当前数组的值,同时用一个变量来存储累乘的结果。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++) nums[i] = in.nextInt();
int[] answer = new int[n];
answer[0] = 1;
int num = 1;
for(int i=1;i<n;i++){
num *= nums[i-1];
answer[i] = num;
}
num = 1;
for(int i=n-2;i>=0;i--){
num *= nums[i+1];
answer[i] = answer[i]*num;
}
System.out.println(Arrays.toString(answer));
}
}
17.缺失的第一个正数
给你一个未排序的整数数组nums,请你找出其中没有出现的最小的正整数。请你实现时间复杂度位O(n)并且只使用常数级别额外空间的解决方案。
思路:因为题目要求要是时间复杂度是O(n),空间复杂度是O(1),因此我门没法使用哈希表来存储数组元素从而快速定位第一个缺失的元素。但是我们可以考虑修改数组,使其拥有类似哈希表的效果,这样就不需要额外的空间了。
假如一个数组长度为n,如果没有缺失的最小正整数,那么最小正整数只能是n+1,因为没有缺失意味着数组里面有1-n,但是数组长度又为n,那么最小的只能是n+1。
我们可以这样设计,
第一次遍历,先把数组内值不在1-n的数设置成大于1-n的数,例如n+1。
然后第二次遍历,把遍历到1-n之内的数组的值,他所对应的数组下标的值修改为负数。这里需要注意的是这个数是有可能已经被打了标记的,因此我们需要取他的绝对值,如果绝对值符合1-n的范围,我把下标|x|-1的值添加一个符号,但是如果已经有了就无需重复添加,因为我们的目的是把这个下标当作1-n没有确实数的标记,负数表示这个下标对应的1-n的正整数没有缺失。
然后第三次遍历,把第一个不是负数的数组下标+1返回,就是答案。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] nums = new int[n];
for(int i=0;i<n;i++) nums[i] = in.nextInt();
for(int i=0;i<n;i++){
if(nums[i] <= 0) nums[i] = n+1;
}
for(int i=0;i<n;i++) {
int num = Math.abs(nums[i]);
if(num <= n) nums[num-1] = -Math.abs(nums[num-1]);
}
int num = n+1;
for(int i=0;i<n;i++){
if(nums[i] > 0) {
num = i+1;
break;
}
}
System.out.println(num);
}
}
矩阵
18.矩阵置零
给定一个mxn的举证,如果一个元素为0,则将其所在行和列的所有元素都设为0.请使用原地算法。
思路:使用两个boolean类型的数组来表示行和列,然后先遍历一边数组,只要是数组值为0,就把他所载的行和列都标记为true。在进行一次遍历,然后判断每一个数组他所在的行和列是否为true,是的话这个数组值就改为0。
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int[][] nums = new int[n][m];
boolean[] row = new boolean[n];
boolean[] col = new boolean[m];
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
nums[i][j] = in.nextInt();
}
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(nums[i][j] == 0) row[i] = col[j] = true;
}
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(row[i] || col[j]) nums[i][j] = 0;
}
}
System.out.println(Arrays.deepToString(nums));
}
}
19.螺旋矩阵
给你一个m行n列的矩阵matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
思路:模拟的方式,就是按转圈圈的思路,一个个的填进去。先从左到右,然后从上倒下,然后从右到左,然后再从下到上。因此我们需要先定义left,right,top,bottom的左右上下的边界。
left=0,right = n-1,top = 0,bottom = n-1。然后设置一个计数器count=0,每一次填入,count++。当count = n*n,说明数组内已经填充完毕,就退出。
(1)从左到右,[left,right]
(2)从上到下,[top,bottom]
(3)从右到左,[right,left]
(4)从下到上,[bottom,top]
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[][] res = new int[n][n];
int left = 0, right = n-1, top = 0, bottom = n-1;
int count = 0, target = n*n;
while(count <= target){
for(int j=left;j<=right;j++) res[top][j] = count++;
top++;
for(int i=top;i<=bottom;i++) res[i][right] = count++;
right--;
for(int j=right;j>=left;j--) res[bottom][j] = count++;
bottom--;
for(int i=bottom;i>=top;i--) res[i][left] = count++;
left++;
}
System.out.println(Arrays.deepToString(res));
}
}
20.旋转图像
给定一个nxn的二位举证matrix表示一个图像。请你将图像顺时针旋转90度。你必须在原地旋转图像,这意味着你需要直接修改输入的二位矩阵。请不要使用另一个矩阵来旋转图像。
思路:可以考虑使用翻转来替代旋转,这样比较好思考和理解。顺时针旋转90度,其实就是先上下翻转,然后再进行主对角线翻转就能实现。
(1)上下翻转:进行上下下标的交换swap(nums[i][j],nums[n-i-1][j])
(2)主对角线翻转swap(nums[i][j],nums[j][i])
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[][] matrix = new int[n][n];
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
matrix[i][j] = in.nextInt();
for(int i=0;i<n/2;i++){
for(int j=0;j<n;j++){
int tmp = matrix[i][j];
matrix[i][j] = matrix[n-i-1][j];
matrix[n-i-1][j] = tmp;
}
}
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
int tmp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = tmp;
}
}
System.out.println(Arrays.deepToString(matrix));
}
}
21.搜索二位矩阵||
编写一个高效的算法来搜索mxn矩阵matrix中的一个目标值target。该矩阵具有以下特性:
(1)每行的元素从左到右升序排列
(2)每列的元素从上到下升序排列
思路:因为该矩阵有两个特征,所以我们可以考虑从矩阵右上角开始遍历,原因如下:
(1)因为行列都是严格递增,因此如果target大于当前的数组,那说明该行左边的数肯定都小于target,所以让行+1再遍历
(2)如果target小于当前的数组,那说明该列下面所有的数都大于target,所以让列-1,再遍历
(3)这样的好处,每次遍历直接排除一整行或者一整列,效率高
import java.io.*;
import java.util.*;
public class Main{
public static void main(String args[]){
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int target = in.nextInt();
int[][] matrix = new int[n][n];
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
matrix[i][j] = in.nextInt();
}
}
int row = 0;
int col = n-1;
while(row<n && col >= 0){
if(target == matrix[row][col]) {
System.out.println(true);
break;
}else if(target > matrix[row][col]) row++;
else col--;
}
}
}
链表
22.相交链表
给你两个单链表的头节点headA和headB,请你找出并返回楼昂哥单链表相交的其实节点。如果两个链表不存在相交节点,返回null。
思路:假如两个链表是相交的,那么相交之后的地方都是一样的,主要区别在于相交之前的长度不同。因此我们要思考如何抹除这个长度差。有两种办法:
(1)分别遍历两个链表一次记录其长度,然后让长的链表先走到和短的一样长度,然后开始一起遍历,然后每次遍历判断是否相等,最后遍历完都没返回就返回null。代码太长了,不推荐。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA;
ListNode q = headB;
int num1 = 0;
int num2 = 0;
while(p!=null) {
p = p.next;num1++;
}
while(q!=null){
q = q.next;num2++;
}
if(num1>num2){
int num = num1-num2;
p = headA;
q = headB;
while(num>0){
p = p.next;
num--;
}
while(p!=null){
if(p==q)return p;
p = p.next;
q = q.next;
}
return p;
}else{
int num = num2-num1;
p = headA;
q = headB;
while(num>0){
q = q.next;
num--;
}
while(q!=null){
if(p==q)return q;
p = p.next;
q = q.next;
}
return q;
}
}
}
(2)直接让两个链表向后遍历,但是当链表遍历到末尾为空时,让他变为对方的头节点继续遍历,这相当于两个指针都遍历两个链表。假如两个链表相交,那么这个方式遍历它们一定会相遇,如果不相交,其实也会相等,只不过相等的时候指针指向的为null
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p = headA;
ListNode q = headB;
while(p!=q){
p = p==null?headB:p.next;
q = q==null?headA:q.next;
}
return p;
}
}
23.反转链表
给你单链表的头节点head,请你反转链表,并返回反转后的链表。
思路:反转链表需要三个指针,一个指向之前的节点,一个指向当前遍历到的节点,一个指向下一个节点。三个分别为pre,link,linknext。三个节点的作用:
(1)让当前节点的指向指针指向pre,实现当前节点指针的反转,然后遍历下一个节点时,pre走到当前节点的位置,表示上一次遍历的节点
(2)link用来遍历节点,并操作指针进行反转,反转完成后去到下一个节点
(3)linknext用来记录当前的下一个节点的位置,因为当前节点反转指针后,其link.next不再指向原先下一个节点的位置,因此需要考linknext来记录这个位置,然后直接跳转。
注意点:pre初始化为null,因为原先链表指针末尾指向的也是null;link初始化为head表示从第一个节点开始遍历;linknext只需要在遍历过程中创建即可。
/**
* 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; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode link = head;
while(link!=null){
ListNode next = link.next;
link.next = pre;
pre = link;
link = next;
}
return pre;
}
}
24.回文链表
给你一个单链表的头节点head,请你判断该链表是否为回文链表。如果是,返回true;否则,返回false。
思路:这个判断回文一般的思路就是把整个数折半,然后判断是不是回文。这里有两个思路:
(1)使用栈,但是比较复杂;先遍历链表把长度统计出来,然后计算奇偶,在除以2,然后再遍历,把前半部分入栈,然后后半部分就出栈对比值是否相等,奇偶就是如果是奇数中间会多出来一个数,直接跳过就可以了。
/**
* 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; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
int n = 0;
ListNode p = head;
while(p!=null){
n++;
p = p.next;
}
Stack<Integer> s = new Stack<>();
int num = n%2;
int target = n/2;
p = head;
while(target>0){
target--;
s.push(p.val);
p = p.next;
}
while(p!=null){
if(num == 1){
num = 0;
p = p.next;continue;
}
int nums = s.pop();
if(nums!=p.val)return false;
p = p.next;
}
return true;
}
}
(2)第二种思路是使用list数组+双指针,也是先遍历一边链表把所有值存入list,然后设置两个指针l=0,r = size()-1,然后遍历判断是否相等。
/**
* 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; }
* }
*/
class Solution {
public boolean isPalindrome(ListNode head) {
List<Integer> list = new ArrayList<>();
ListNode p = head;
while(p!=null){
list.add(p.val);
p = p.next;
}
int l=0, r = list.size()-1;
while(l<r){
if(!list.get(l).equals(list.get(r))) return false;
l++;r--;
}
return true;
}
}
25.环形链表
给你一个链表的头节点head,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪next指针再次到达,则链表存在环。为了表示给定链表中的环,评测系统内部使用整数pos来表示链表尾连接到链表中的位置(索引从0开始)。注意:pos不作为参数进行传递。仅仅是为了标识链表的实际情况。如果链表中存在环,则返回true。否则,返回false。
思路:使用快慢指针,只要存在环,快慢指针一定会在某个位置相遇。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast!=null){
fast = fast.next;
slow = slow.next;
if(fast == null) return false;
if(fast!=null) fast = fast.next;
if(slow == fast)return true;
}
return false;
}
}
26.环形链表二
给定一个链表的头节点 head
,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
如果链表中有某个节点,可以通过连续跟踪 next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos
是 -1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
思路:和上一题一样使用快慢指针,但是不同的是当相遇后,需要把其中一个指针移动到头节点,然后两个指针开始步长为1的向前走,当再次相遇的时候,那个位置就是入环的第一个节点。为什么呢?这里需要逻辑和数学的推到:
简单来说就是设a为从头节点走到环的步数,b为环的长度,n为在环里绕了多少次的次数。那么:
(1)只要符合走了a+nb步,那么节点一定是在环的入口
(2)假设慢指针走了s步,那么快指针就走了2s步,快慢指针都入环了,但是快指针肯定在环内绕的圈子更多,那么假设快指针比慢指针多绕了n个环就能得出一个公式2s = s + nb -> s = nb也就是说慢指针走了nb步
(3)当快慢指针第一次相遇时,让其中一个指针回到头节点,让后两个指针都开始步长为1的向前走,前面说过,从头节点走到环是a步,那么一个指针走了a步,另一个指针走了a+nb步。因此他们都走到了环的入口处,这就是第二次相遇,然后输出节点即可。
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast!=null){
fast = fast.next;
slow = slow.next;
if(fast!=null) fast = fast.next;
else return null;
if(fast == slow){
slow = head;
while(slow != fast){
fast = fast.next;
slow = slow.next;
}
return fast;
}
}
return fast;
}
}
27.合并两个有序链表
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思路:先创建一个新的节点作为头节点的前置节点,然后设置两个指针,一个指向第一个链表的头节点,一个指向第二个链表的头节点,然后遍历这两个链表,比较两个链表当前遍历到的值大小,小的先插入,然后移动指针,然后如此下去。遍历结束后要考虑可能两个链表长度不一致,那么长的那一个剩下的全部接入到尾部,然后返回新的节点指向的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; }
* }
*/
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode pre = new ListNode();
ListNode link = pre;
ListNode p1 = list1;
ListNode p2 = list2;
while(p1!=null && p2!=null){
if(p1.val < p2.val){
link.next = p1;
link = p1;
p1 = p1.next;
}else{
link.next = p2;
link = p2;
p2 = p2.next;
}
}
if(p1!=null) link.next = p1;
if(p2!=null) link.next = p2;
return pre.next;
}
}
28.两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。请你将两个数相加,并以相同形式返回一个表示和的链表。你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
思路:直接相加,然后查看需不需要进位,需要就进位,然后返回具体思路,先创建两个指针然后遍历两个链表,然后创建一个头节点的前驱节点,然后每次相加之后先计算需不需要进位,然后创建一个新的节点让前驱节点指向这个节点,然后赋值。遍历时判断节点是否为空,只有一个为空就直接赋值,两个都为空表示遍历结束。
/**
* 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; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode p = l1;
ListNode q = l2;
ListNode pre = new ListNode();
ListNode node = pre;
int carry = 0;
while(p!=null || q!=null){
int num1 = p==null?0:p.val;
int num2 = q==null?0:q.val;
int sum = num1+num2+carry;
carry = sum/10;
sum = sum%10;
ListNode link = new ListNode(sum);
node.next = link;
node = link;
if(p!=null) p = p.next;
if(q!=null) q = q.next;
}
if(carry > 0 )node.next = new ListNode(carry);
return pre.next;
}
}
29.删除链表的倒数第N个节点
给你一个链表,删除链表的倒数第 n
个结点,并且返回链表的头结点。
思路:使用快慢指针,让快的指针先走n步,然后慢的指针也开始走,让快的指针到达null的时候,慢的指针就到达了要删除的位置。
/**
* 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; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode pre = new ListNode();
ListNode slow = pre;
slow.next = head;
ListNode fast = head;
while(n > 0){
fast = fast.next;
n--;
}
while(fast!=null){
slow = slow.next;
fast = fast.next;
}
if(slow.next == head)head = head.next;
slow.next = slow.next.next;
return head;
}
}
30.两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
思路:指针三个指针,然后每次操作指针让当前节点和下一个节点交换位置,然后再跳到下一个需要交换的位置。
/**
* 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; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
if(head==null || head.next==null)return head;
ListNode sentinel = new ListNode();
sentinel.next = head;
ListNode pre = sentinel;
ListNode link = head;
while(link.next!=null){
ListNode linknext = link.next;
ListNode tmp = link.next.next;
linknext.next = link;
link.next = tmp;
pre.next = linknext;
if(tmp!=null){
pre = link;
link = tmp;
}
}
return sentinel.next;
}
}
31.K个一组翻转链表
给你链表的头节点 head
,每 k
个节点一组进行翻转,请你返回修改后的链表。
k
是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k
的整数倍,那么请将最后剩余的节点保持原有顺序。
你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
思路:其实大体上和两两反转的是差不多,但是这个是翻转k个,因此要做三个位置的特殊处理
(1)如何确定k的长度,这里是使用一个指针,通过循环遍历,走到k的位置然后作为末尾节点
(2)如何反转这k个节点,找到开头的节点和前驱节点,和末尾的节点和后驱节点,然后将链条断开,反转之后再接上。反转的代码就是之前做过的链表反转一模一样,start就是头节点。
(3)不足k个长度怎么处理,不足k个话说明已经到了末尾了,那指针肯定遍历到null了,直接break跳出循环即可
/**
* 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; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode sentinel = new ListNode();
sentinel.next = head;
ListNode pre = sentinel;
ListNode end = sentinel;
while(end!=null){
for(int i=0;i<k;i++) end = end.next;
if(end==null) break;
ListNode start = pre.next;
ListNode next = end.next;
end.next = null;
pre.next = reverse(start);
start.next = next;
pre = start;
end = start;
}
return sentinel.next;
}
public ListNode reverse(ListNode start){
ListNode pre = null;
ListNode link = start;
while(link!=null){
ListNode linknext = link.next;
link.next = pre;
pre = link;
link = linknext;
}
return pre;
}
}
32.随机链表的复制
给你一个长度为 n
的链表,每个节点包含一个额外增加的随机指针 random
,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next
指针和 random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X
和 Y
两个节点,其中 X.random --> Y
。那么在复制链表中对应的两个节点 x
和 y
,同样有 x.random --> y
。
返回复制链表的头节点。
用一个由 n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。
你的代码 只 接受原链表的头节点 head
作为传入参数。
思路:主要是要弄清楚什么是深拷贝,其实主要就是需要new一个新的对象在jvm堆中开辟一个新的地址空间来存放数据,如果不new的话,原来的变量在堆中一直是指向的原来的地址,那就属于是浅拷贝,一修改就会把原来的值也修改了。知道这一点之后,就好办了,就是遍历链表,然后把链表的节点完整的new出一个新的然后把信息都拷贝到新的对象中。具体思路:
(1)创建一个map用来存放旧节点和新节点
(2)遍历链表,把旧节点的值拷贝到新节点上
(3)再遍历一次,这次把next和random也配置好
(4)返回头节点map.get的value。
/*
// Definition for a Node.
class Node {
int val;
Node next;
Node random;
public Node(int val) {
this.val = val;
this.next = null;
this.random = null;
}
}
*/
class Solution {
public Node copyRandomList(Node head) {
Map<Node,Node> map = new HashMap<>();
Node node = head;
while(node!=null){
map.put(node,new Node(node.val));
node = node.next;
}
node = head;
while(node!=null){
map.get(node).next = map.get(node.next);
map.get(node).random = map.get(node.random);
node = node.next;
}
return map.get(head);
}
}
33.排序链表
给你链表的头结点 head
,请将其按 升序 排列并返回 排序后的链表 。
思路:链表要排序,其实很多排序算法基本没法用,但是归并排序是可以的,归并排序就是先把链表折半拆分,然后最后拆到只有1个,然后排完序再合并起来。合并的方式就是之前做过的两个链表合并的代码。
/**
* 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; }
* }
*/
class Solution {
public ListNode sortList(ListNode head) {
if(head == null || head.next == null)return head;
ListNode slow = head;
ListNode fast = head.next;
while(fast!=null && fast.next!=null){
slow = slow.next;
fast = fast.next.next;
}
ListNode tmp = slow.next;
slow.next = null;
ListNode left = sortList(head);
ListNode right = sortList(tmp);
ListNode h = new ListNode();
ListNode link = h;
while(left!=null && right!=null){
if(left.val < right.val){
link.next = left;
link = left;
left = left.next;
}else{
link.next = right;
link = right;
right = right.next;
}
}
link.next = left==null?right:left;
return h.next;
}
}
34.合并k个升序链表
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
思路:这题就是合并两个链表的升级版,主要的思路就是,他给的不是一个链表数组吗,我们只需要先把前两个合并成一个链表,然后拿着这个链表找数组中下一个链表合并。相当于直接两两合并,这样大大简化了难度。具体思路:
(1)创建一个res节点,作为结果链表,默认为null
(2)遍历链表数组,让res和第一个数组链表合并
(3)合并之后的res再和下一个链表合并
(4)最后输出res
/**
* 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; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode res = null;
for(ListNode list : lists){
res = merge(res,list);
}
return res;
}
public ListNode merge(ListNode res,ListNode list){
if(res==null || list==null)return list==null?res:list;
ListNode h = new ListNode();
ListNode node = h;
while(res!=null && list != null){
if(res.val < list.val){
node.next = res;
res = res.next;
}else{
node.next = list;
list = list.next;
}
node = node.next;
}
node.next = res==null?list:res;
return h.next;
}
}
35.LRU缓存
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache
类:
LRUCache(int capacity)
以 正整数 作为容量capacity
初始化 LRU 缓存int get(int key)
如果关键字key
存在于缓存中,则返回关键字的值,否则返回-1
。void put(int key, int value)
如果关键字key
已经存在,则变更其数据值value
;如果不存在,则向缓存中插入该组key-value
。如果插入操作导致关键字数量超过capacity
,则应该 逐出 最久未使用的关键字。
函数 get
和 put
必须以 O(1)
的平均时间复杂度运行。
思路:使用双向链表+哈希表的方式。LRU最近最少使用缓存机制:
(双向链表)想象一下,你比较常用的东西你会放的离自己比较近,不常用会离自己比较远(因为常用的都被你拿到靠近自己的位置了)。现在有个问题是,假如是桌子,桌子的大小(容量)是有限的,当桌子放满之后,你就需要把一些不常用的就拿出桌子,腾出空间给其他东西放。
(哈希表)为了找到快速的找到每个东西,你就拿个小本本记录每个东西的位置,当你需要某个东西时,你就查询这个小本本,然后就能快速定位和找到你需要的东西。没有说明没有这个东西。
(get查找)先查找小本本,如果有,就把拿到离你最近的位置,表示你最近刚用过,就是把这个数据移动到双向链表头部
(put添加更新)如果桌子还有位置还能放,就直接放到最近位置,就是链表头部,然后在小本本中记录它的位置,就是再HashMap中写入这个数据的值和对应的地址。如果桌子满了,就把最远处的东西移出桌子,然后再把新拿的东西放到离你最近的位置就是链表头部。如果放入的已经存在的东西,但是他的内容变了,那就需要更新他的信息,再放到头部
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
二叉树
36.二叉树的中序遍历
给定一个二叉树的根节点root,返回他的中序遍历。
思路:有两种办法一种是递归的方法,还有一种遍历的方法:
(1)递归,就是直接按照中序的中左右去调用函数本身,当到root本身时就将其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 {
List<Integer> res = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
if(root == null)return res;
inorder(root);
return res;
}
public void inorder(TreeNode root){
if(root == null)return ;
inorder(root.left);
res.add(root.val);
inorder(root.right);
}
}
(2)遍历,使用栈的方式来,先一直向左子树去遍历,并把遍历的节点存入栈中,当遍历到为空时说明已经到了最左侧了,然后出栈,把节点放入到列表中,然后遍历右子树。
class Solution {
List<Integer> list = new ArrayList<>();
public List<Integer> inorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode pre = null;
while(root!=null || !stack.isEmpty()){
while(root!=null){
stack.push(root);
root = root.left;
}
root = stack.pop();
list.add(root.val);
root = root.right;
}
return list;
}
}
37.二叉树的最大深度
给定一个二叉树 root
,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
思路:使用层序遍历,使用一个队列,节点出队的时候,判断其有没有子节点,有的话就入队,每次把队列内的节点都出队后,就代表着一层的节点已经遍历完了,哪深度就可以+1了。遍历结束后就返回深度。
/**
* 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 int maxDepth(TreeNode root) {
if(root == null)return 0;
Deque<TreeNode> que = new LinkedList<>();
int res = 0;
que.offer(root);
while(!que.isEmpty()){
int num = que.size();
for(int i=0;i<num;i++){
TreeNode node = que.poll();
if(node.left!=null) que.offer(node.left);
if(node.right!=null) que.offer(node.right);
}
res++;
}
return res;
}
}
38.翻转二叉树
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
思路:采用递归的方式来翻转,先从底部从下到上翻转。
/**
* 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 TreeNode invertTree(TreeNode root) {
if(root == null)return root;
reverse(root);
return root;
}
public void reverse(TreeNode root){
if(root == null)return ;
reverse(root.left);
reverse(root.right);
TreeNode tmp = root.left;
root.left = root.right;
root.right = tmp;
}
}
39.对称二叉树
给你一个二叉树的根节点 root
, 检查它是否轴对称。
思路:我们可以实现这样一个递归函数,通过「同步移动」两个指针的方法来遍历这棵树,p指针和 q 指针一开始都指向这棵树的根,随后 p右移时,q左移,p 左移时,q右移。每次检查当前 p和 q节点的值是否相等,如果相等再判断左右子树是否对称。
/**
* 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 boolean isSymmetric(TreeNode root) {
return judge(root.left,root.right);
}
public boolean judge(TreeNode left,TreeNode right){
if(left==null && right==null)return true;
if(left==null || right==null)return false;
if(left.val != right.val)return false;
boolean leftroot = judge(left.left,right.right);
boolean rightroot = judge(left.right,right.left);
return leftroot&&rightroot;
}
}
40.二叉树直径
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
思路:直径就是左右子树最大深度之和,最大深度就是最右子树最大深度+1.
/**
* 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 {
int ans = 0;
public int diameterOfBinaryTree(TreeNode root) {
findpath(root);
return ans;
}
public int findpath(TreeNode node){
if(node == null) return 0;
int l = findpath(node.left);
int r = findpath(node.right);
ans = Math.max(ans,l+r);
return Math.max(l,r) + 1;
}
}
41.二叉树层序遍历
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
思路,使用队列遍历,
首先根元素入队
当队列不为空的时候
求当前队列的长度
依次从队列中取
个元素进行拓展,然后进入下一次迭代
/**
* 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<List<Integer>> levelOrder(TreeNode root) {
if(root == null) return new ArrayList<>();
List<List<Integer>> res = new ArrayList<>();
Deque<TreeNode> que = new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int num = que.size();
List<Integer> path = new ArrayList<>();
for(int i=0;i<num;i++){
TreeNode node = que.poll();
path.add(node.val);
if(node.left!=null) que.offer(node.left);
if(node.right!=null) que.offer(node.right);
}
res.add(path);
}
return res;
}
}
42.将有序数组转换为二叉搜索树
给你一个整数数组 nums
,其中元素已经按 升序 排列,请你将其转换为一棵平衡二叉搜索树。
思路:
二叉搜索树的中序遍历是升序序列,题目给定的数组是按照升序排序的有序数组,因此可以确保数组是二叉搜索树的中序遍历序列。递归的基准情形是平衡二叉搜索树不包含任何数字,此时平衡二叉搜索树为空。
/**
* 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 TreeNode sortedArrayToBST(int[] nums) {
TreeNode root = buildTree(nums,0,nums.length-1);
return root;
}
public TreeNode buildTree(int[] nums,int left,int right){
if(left > right)return null;
int mid = (left+right)/2;
TreeNode root = new TreeNode(nums[mid]);
root.left = buildTree(nums,left,mid-1);
root.right = buildTree(nums,mid+1,right);
return root;
}
}
43.验证二叉搜索树
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的作子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
思路:使用中序遍历,比较当前节点的值和上一个节点值是否大于,不是则返回false;先遍历左子树,返回值,查看是否为true,是的话继续不是返回false;使用一个pre节点记录上一次遍历的节点,然后返回遍历右子树的结果。
/**
* 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 {
TreeNode pre;
public boolean isValidBST(TreeNode root) {
if(root == null)return true;
boolean left = isValidBST(root.left);
if(left == false || (pre!=null && pre.val >= root.val))return false;
pre = root;
return isValidBST(root.right);
}
}
44.二叉搜索树中第k小的元素
给定一个二叉搜索树的根节点 root
,和一个整数 k
,请你设计一个算法查找其中第 k
个最小元素(从 1 开始计数)。
思路:使用中序遍历,遍历到第k个数的时候,直接返回即可。使用栈的方式遍历,先一致向左遍历,遍历到的节点都入栈,直到为空,为空后栈节点出栈然后k--,判断k==0就直接返回当前节点的值,然后继续向右子树遍历。
/**
* 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 int kthSmallest(TreeNode root, int k) {
int res = 0;
Stack<TreeNode> stack = new Stack<>();
while(root!=null || !stack.isEmpty()){
while(root != null){
stack.push(root);
root = root.left;
}
root = stack.pop();
k--;
if(k == 0)return root.val;
root = root.right;
}
return root.val;
}
}
45.二叉树的右视图
给定一个二叉树的 根节点 root
,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
思路:使用层序遍历的方式,然后每层遍历的时候,最后一位节点将它放入到res的列表中。
/**
* 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> rightSideView(TreeNode root) {
if(root == null)return new ArrayList<>();
Deque<TreeNode> que = new LinkedList();
List<Integer> res = new ArrayList<>();
que.offer(root);
while(!que.isEmpty()){
int num = que.size();
for(int i=0;i<num;i++){
TreeNode node = que.poll();
if(node.left!=null)que.offer(node.left);
if(node.right!=null)que.offer(node.right);
if(i==num-1) res.add(node.val);
}
}
return res;
}
}
46.二叉树展开为链表
给你二叉树的根结点 root
,请你将它展开为一个单链表:
- 展开后的单链表应该同样使用
TreeNode
,其中right
子指针指向链表中下一个结点,而左子指针始终为null
。 - 展开后的单链表应该与二叉树 先序遍历 顺序相同。
思路:先序遍历的情况下是先遍历根节点,再遍历左子树,再遍历右子树。因此我们每次遍历都要吧左子树根节点接到右子树去,那原本的右子树就接到左子树最右下的位置。因此先序遍历的时候,左子树最右下的位置遍历完之后刚好轮到右子树。因此整体的思路是:
如果当前节点的左子树不为空,那么就先把右子树接到左子树最右下的节点的右子树上,然后再把整个左子树接到右子树上,这里是直接覆盖,然后左子树就变为null。
/**
* 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 void flatten(TreeNode root) {
while(root!=null){
if(root.left==null) root = root.right;
else {
TreeNode pre = root.left;
while(pre.right!=null)pre = pre.right;
pre.right = root.right;
root.right = root.left;
root.left = null;
root = root.right;
}
}
}
}
47.从前序和中序遍历序列构造二叉树
给定两个整数数组 preorder
和 inorder
,其中 preorder
是二叉树的先序遍历, inorder
是同一棵树的中序遍历,请构造二叉树并返回其根节点。
思路:因为是使用前序+中序遍历来构造二叉树,那么我们可以先定位跟节点,前序的跟节点在开头,我们根据前序的跟节点来查找中序跟节点的位置,找到之后,我们就可以实现吧二叉树划分为两个部分,中序遍历,跟节点位置的左侧是左子树的节点,同时还可以计算出左子树的节点个数也也就是长度,右侧是右子树的节点。我们可以先把当前跟节点创建了,然后其左子树和右子树再递归调用来创建,最终就可以创建好二叉树了。
/**
* 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 {
Map<Integer,Integer> inmap = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i=0;i<inorder.length;i++) inmap.put(inorder[i],i);
return buildTreeNode(preorder,inorder,0,preorder.length-1,0,inorder.length-1);
}
public TreeNode buildTreeNode(int[] preorder,int[] inorder,int preleft,int preright,int inleft,int inright){
if(inleft > inright)return null;
int num = preorder[preleft];
int mid = inmap.get(num);
TreeNode node = new TreeNode(num);
int leftlen = mid - inleft;
node.left = buildTreeNode(preorder,inorder,preleft+1,preleft+leftlen,inleft,mid-1);
node.right = buildTreeNode(preorder,inorder,preleft+leftlen+1,preright,mid+1,inright);
return node;
}
}
48.路径总和
给定一个二叉树的根节点 root
,和一个整数 targetSum
,求该二叉树里节点值之和等于 targetSum
的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
思路:使用队列,然后遍历每个节点,遍历到该节点,就以该节点为起点,向左右子树一直遍历,然后累加值,判断是否等于targetsum,设置一个全局变量count,等于的话就count++;最后返回count
/**
* 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 {
int count = 0;
int targetSums;
public int pathSum(TreeNode root, int targetSum) {
if(root == null)return 0;
targetSums = targetSum;
Deque<TreeNode> que = new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
TreeNode node = que.poll();
check(node,0);
if(node.left!=null)que.offer(node.left);
if(node.right!=null)que.offer(node.right);
}
return count;
}
public void check(TreeNode node,int sum){
if(node == null) return;
sum += node.val;
if(sum == targetSums)count++;
check(node.left,sum);
check(node.right,sum);
}
}
49.二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
思路:采用递归的,方式直接拆分为六种判断:
(1)当前节点root为空,返回null
(2)当前节点root等于p,说明遍历到了p返回p
(3)当前节点root等于q,说明遍历到了q返回q
(4)然后递归遍历root的左子树和右子树,然后得到left和right
(5)如果left和right有一个为空,返回不为空的
(6)如果两个都不会空,说明找到的最近公共祖先了,返回root
(7)其他情况一律返回null
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null)return null;
if(root == p)return p;
if(root == q)return q;
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
if(left!=null && right==null)return left;
if(left==null && right!=null)return right;
if(left!=null && right!=null)return root;
return null;
}
}
50.二叉树的最大路径和
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
思路:从局部最优推理到最大值,就是递归遍历,遍历子树的最大贡献值,如果左右子树的贡献为负数,那就直接放弃这个路径,把贡献调整为0,计算路径就是当前节点的值+左子树的最大贡献值+右子树的最大贡献值,然后和目前的最大maxres值进行比较。节点返回的贡献值就是左右子树的最大贡献值+当前节点的值->当前节点的最大贡献值。因为只要是负数就抛弃,因此当递归回溯到跟节点的时候,sum加起来的值就一定是全局的最大值。
/**
* 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 {
int maxsum = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
findmax(root);
return maxsum;
}
public int findmax(TreeNode root){
if(root == null)return 0;
int left = Math.max(findmax(root.left),0);
int right = Math.max(findmax(root.right),0);
maxsum = Math.max(maxsum,root.val+left+right);
return root.val+Math.max(left,right);
}
}
图论
51.岛屿数量
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
思路:采用bfs的方式,广度优先搜索,先把相邻的位置都遍历到,遍历过的直接标为2,然后继续,每次只遍历值为1的,就是岛屿,然后令数量++
class Solution {
public int numIslands(char[][] grid) {
int n = grid.length;
int m = grid[0].length;
int num = 0;
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j] == '1'){
inject(grid,i,j);
num++;
}
}
}
return num;
}
public void inject(char[][] grid,int i,int j){
if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]!='1')return ;
grid[i][j] = '2';
inject(grid,i-1,j);
inject(grid,i,j-1);
inject(grid,i+1,j);
inject(grid,i,j+1);
}
}
52.腐烂的橘子
在给定的 m x n
网格 grid
中,每个单元格可以有以下三个值之一:
- 值
0
代表空单元格; - 值
1
代表新鲜橘子; - 值
2
代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1
。
思路:使用队列+bfs的方式,具体的逻辑是,可以参照上一题的做法,但是这一题加入了时间这个维度,因此我们通过引入队列来表示时间。目前队列里面的就是这个时间点存在的烂橘子,然后这个时间段,把队列里面的烂橘子都遍历,然后把它四周的橘子都感染为烂橘子然后把这些新成为烂橘子的在入队即可,然后每一次遍历时间就++。注意要先遍历一边数组,好橘子的数量统计和烂橘子的位置入队。统计好橘子数量是为了最后返回结果的时候需要判断是否吧所有橘子感染了,不是需要返回-1;而让烂橘子的位置入队就是为了以时间为节点,bfs感染。
class Solution {
public int orangesRotting(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
int freshNum = 0;
Deque<int[]> que = new ArrayDeque<>();
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j] == 1) freshNum++;
if(grid[i][j] == 2) que.offer(new int[]{i,j});
}
}
int min =0;
while(!que.isEmpty()){
if(freshNum == 0) return min;
min++;
int size = que.size();
for(int i=0;i<size;i++){
int[] rot = que.pop();
int x = rot[0];
int y = rot[1];
freshNum -= roting(grid,x-1,y,que);
freshNum -= roting(grid,x,y-1,que);
freshNum -= roting(grid,x,y+1,que);
freshNum -= roting(grid,x+1,y,que);
}
}
return freshNum==0?min:-1;
}
public int roting(int[][] grid,int x,int y,Deque<int[]> que){
if(x<0 || x >= grid.length||y<0 || y >= grid[0].length || grid[x][y] != 1) return 0;
grid[x][y] = 2;
que.offer(new int[]{x,y});
return 1;
}
}
53.课程表
你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites
给出,其中 prerequisites[i] = [ai, bi]
,表示如果要学习课程 ai
则 必须 先学习课程 bi
。
- 例如,先修课程对
[0, 1]
表示:想要学习课程0
,你需要先完成课程1
。
请你判断是否可能完成所有课程的学习?如果可以,返回 true
;否则,返回 false
。
思路:这是一个典型的拓扑的问题,我们需要先构建一个图,然后通过深度优先搜索来遍历每个路径,然后将节点入栈。如果一个图不存在环,那么他一定能够学习完所有课程。具体步骤为:
(1)构建一个图,其数据结构为List<List<Integer>> edges;然后初始化,就是将每个节点填充空列表,设置一个全局变量valid初始化为true表示最终结果
(2)遍历二维数组prerequisites,因为每个数组0下标为想要学,1下标为需要先学的课程,因此遍历的时候就查找到1下标的位置然后把0下标的值add进去,相当于在这个位置拉了一个链表
(3)dfs遍历每一个numCourses课程,然后先设置一个全局变量visited[]用来表示访问节点的状态,0位未被访问,1为正在访问即在dfs过程中,2为访问完毕,如果在一个节点在dfs过程中遇到了visited为1的节点,那说明肯定有环,那就让valid等于false。如果valid已经为false后面就不需要再进行了直接返回false即可
(4)dfs实现,进入dfs先将节点的visited设置为1,然后开始从edges中寻找其下一个链接的节点,然后判断其visited是多少,是0的话继续dfs,是1话说明有环,valid=false然后return退出,如果节点遍历结束了,就吧该节点设置为2
class Solution {
List<List<Integer>> edges;
int[] visited;
boolean valid = true;
public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for(int i=0;i<numCourses;i++){
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses];
for(int[] info : prerequisites){
edges.get(info[1]).add(info[0]);
}
for(int i=0;i<numCourses && valid;i++){
if(visited[i] == 0) dfs(i);
}
return valid;
}
public void dfs(int u){
visited[u] = 1;
for(int v: edges.get(u)){
if(visited[v] == 0){
dfs(v);
if(!valid)return ;
}else if(visited[v] == 1){
valid = false;
return ;
}
}
visited[u] = 2;
}
}
54.实现前戳树
Trie(发音类似 "try")或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie()
初始化前缀树对象。void insert(String word)
向前缀树中插入字符串word
。boolean search(String word)
如果字符串word
在前缀树中,返回true
(即,在检索之前已经插入);否则,返回false
。boolean startsWith(String prefix)
如果之前已经插入的字符串word
的前缀之一为prefix
,返回true
;否则,返回false
。
思路:
前缀树或字典树,是一棵有根树,其每个节点包含以下字段:
指向子节点的指针数组 children。对于本题而言,数组长度为 26,即小写英文字母的数量。此时 children[0] 对应小写字母 aaa,children[1] 对应小写字母 b,…,children[25]对应小写字母 z。
布尔字段 isEnd,表示该节点是否为字符串的结尾。
插入字符串
我们从字典树的根开始,插入字符串。对于当前字符对应的子节点,有两种情况:
子节点存在。沿着指针移动到子节点,继续处理下一个字符。
子节点不存在。创建一个新的子节点,记录在 children 数组的对应位置上,然后沿着指针移动到子节点,继续搜索下一个字符。
重复以上步骤,直到处理字符串的最后一个字符,然后将当前节点标记为字符串的结尾。
查找前缀
我们从字典树的根开始,查找前缀。对于当前字符对应的子节点,有两种情况:
子节点存在。沿着指针移动到子节点,继续搜索下一个字符。
子节点不存在。说明字典树中不包含该前缀,返回空指针。
重复以上步骤,直到返回空指针或搜索完前缀的最后一个字符。
若搜索到了前缀的末尾,就说明字典树中存在该前缀。此外,若前缀末尾对应节点的 isEnd\textit{isEnd}isEnd 为真,则说明字典树中存在该字符串。
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for(int i=0;i<word.length();i++){
char c = word.charAt(i);
int index = c - 'a';
if(node.children[index] == null){
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = searchPrefix(word);
return node!=null && node.isEnd;
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix)!=null;
}
private Trie searchPrefix(String prefix){
Trie node = this;
for(int i=0;i<prefix.length();i++){
char c = prefix.charAt(i);
int index = c - 'a';
if(node.children[index] == null){
return null;
}
node = node.children[index];
}
return node;
}
}
回溯
55.全排列
给定一个不含重复数字的数组 nums
,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
思路:回溯的方法就是遍历每一种可能的组合,然后添加起来,但是要注意,每个值只能用一次因此要对值进行去重。主要思路是:
创建两个集合列表,一个是输出最终答案的res,一个是用来存放组合的path,然后递归遍历,每次讲遍历到的数值加入到path中,如果path长度已经和数组长度相等了,就讲起添加到res中。去重方法是如果path里面已经存在了这个值就跳过。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
findpath(nums);
return res;
}
public void findpath(int[] nums){
if(path.size() == nums.length){
res.add(new ArrayList<>(path));
return ;
}
for(int i=0;i<nums.length;i++){
if(path.contains(nums[i]))continue;
path.add(nums[i]);
findpath(nums);
path.remove(path.size()-1);
}
}
}
56.子集
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的
子集(幂集)。解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思路:创建一个res和一个path数组,res用来存在最终的结果集合,path用来存放单个子集的组合。使用递归的方式,因为不能重复,因此需要在递归的时候要让下标不会有相同。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
findpath(nums,0);
return res;
}
public void findpath(int[] nums,int index){
res.add(new ArrayList<>(path));
for(int i=index;i<nums.length;i++){
path.add(nums[i]);
findpath(nums,i+1);
path.remove(path.size()-1);
}
}
}
57.电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
思路:先创建一个一个res列表和path的字符变量和String的数组,把数字对应的字符都存入,然后开始递归遍历,使用一个num用来计数,表示遍历digits的长度。当num==digits.length()时,就将path添加到res中。然后先把digits的数数字取出来,然后得到对应的字母字符,然后遍历这些字符,得到字符组合。
class Solution {
List<String> res = new ArrayList<>();
StringBuffer path = new StringBuffer();
public List<String> letterCombinations(String digits) {
if(digits.length() == 0)return res;
String[] digit = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
findpath(digits,digit,0);
return res;
}
public void findpath(String digits,String[] digit,int num){
if(digits.length() == num){
res.add(path.toString());
return ;
}
String str = digit[digits.charAt(num) - '0'];
for(int i=0;i<str.length();i++){
path.append(str.charAt(i));
findpath(digits,digit,num+1);
path.deleteCharAt(path.length()-1);
}
}
}
58.组合总和
给你一个 无重复元素 的整数数组 candidates
和一个目标整数 target
,找出 candidates
中可以使数字和为目标数 target
的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。
candidates
中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。
对于给定的输入,保证和为 target
的不同组合数少于 150
个。
思路:这个也是通过递归的方式来求解,因为题目说里面的数字可以重复,因此在递归的时候也可以从当前下标继续遍历,同时使用一个sum来计数目前的加和值,如果值等于target就直接放入到的结果res中,如果sum>target直接返回。
class Solution {
List<List<Integer>> res = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
findpath(candidates,target,0,0);
return res;
}
public void findpath(int[] candidates, int target,int sum, int index){
if(sum > target)return ;
if(sum == target){
res.add(new ArrayList<>(path));
return ;
}
for(int i=index;i<candidates.length;i++){
path.add(candidates[i]);
sum += candidates[i];
findpath(candidates,target,sum,i);
sum -= candidates[i];
path.remove(path.size()-1);
}
}
}
59.括号生成
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
思路:左括号一定是先添加的,然后最后添加完之后,左右括号应该都为0.简单来说就是,设置左括号可添加数为n,右括号可添加数也为n,当左右括号数相等的时候,一定是先添加左括号,不然就不合法了。如果左括号数小于右括号数的话,即可以选择添加左括号,也可以选择添加右括号,最后都为0的时候添加res结果集中。
class Solution {
List<String> res = new ArrayList<>();
public List<String> generateParenthesis(int n) {
if(n <= 0)return res;
findpath("",n,n);
return res;
}
public void findpath(String str,int l,int r){
if(l==0 && r==0){
res.add(str);
return ;
}
if(l == r){
findpath(str+"(",l-1,r);
}else if(l < r){
if(l > 0) findpath(str+"(",l-1,r);
findpath(str+")",l,r-1);
}
}
}
60.单词搜索
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
思路:bfs回溯的方式遍历每一种可能的组合,从每一个下标作为起点,向四周递归遍历,然后如果符合word就继续向下遍历,不符合就返回,如果最后有长度达到和word一样则说明找到路径,则直接翻译true,找不到则返回false;
class Solution {
boolean res = false;
public boolean exist(char[][] board, String word) {
if(board.length == 0)return false;
boolean[][] visited = new boolean[board.length][board[0].length];
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
findpath(board,word,visited,i,j, 0);
}
}
return res;
}
public void findpath(char[][] board, String word, boolean[][] visited,int i,int j, int num){
if(i>= board.length || i<0 || j>=board[0].length || j<0 || visited[i][j] || res || board[i][j]!= word.charAt(num))return;
if(num == word.length() -1){
res = true;
return ;
}
visited[i][j] = true;
findpath(board,word,visited,i+1,j,num+1);
findpath(board,word,visited,i-1,j,num+1);
findpath(board,word,visited,i,j+1,num+1);
findpath(board,word,visited,i,j-1,num+1);
visited[i][j] = false;
}
}
61.分割回文串
给你一个字符串 s
,请你将 s
分割成一些子串,使每个子串都是 回文串 。返回 s
所有可能的分割方案。
思路:
-
初始化:首先,初始化结果列表
res
和当前路径列表path
。 -
启动递归搜索:通过
partition
方法,传入字符串s
,从第0个字符开始搜索。 -
递归分割:
findpath
方法递归地尝试所有可能的分割点。对于每个分割点i
(从num
到字符串末尾):- 首先,检查从当前位置
num
到i
的子串是否是回文。如果是,将这个子串添加到path
中。 - 然后,递归地调用
findpath
,以i+1
作为新的起始位置,继续搜索下一个可能的分割点。 - 完成对当前分割点的探索后,从
path
中移除最后一个元素(回溯),以尝试其他可能的分割点。
- 首先,检查从当前位置
-
检查是否到达字符串末尾:在
findpath
的开始,如果num
等于字符串的长度,说明找到了一种分割方式,将当前路径复制并添加到结果列表res
中。 -
回文验证:
isvalid
方法通过双指针技术检查一个子串是否是回文。指针从子串的两端开始向中间移动,如果在过程中发现任何不匹配的字符,则子串不是回文。 -
结束条件:递归将持续进行,直到所有可能的分割方式都被探索完毕。最终,
res
将包含所有有效的分割方案。 -
class Solution { List<List<String>> res = new ArrayList<>(); List<String> path = new ArrayList<>(); public List<List<String>> partition(String s) { findpath(s,0); return res; } public void findpath(String s,int num){ if(num == s.length()){ res.add(new ArrayList<>(path)); return ; } for(int i=num;i<s.length();i++){ if(isvalid(s.substring(num,i+1))){ path.add(s.substring(num,i+1)); }else continue; findpath(s,i+1); path.remove(path.size()-1); } } public boolean isvalid(String s){ int l = 0; int r = s.length()-1; while(l < r){ if(s.charAt(l) != s.charAt(r)) return false; l++;r--; } return true; } }
62.N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n
个皇后放置在 n×n
的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n
,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q'
和 '.'
分别代表了皇后和空位。
思路:和上一题的思路差不多,只不过添加到res的时候和isvalid判断的时候需要做一些特殊处理:
findpath(int n, int row, char[][] board)
:递归方法,用于深度优先搜索所有可能的棋盘配置。它尝试在当前行row
的每一列放置一个皇后,然后递归到下一行。Array2List(char[][] board)
:将棋盘转换为列表形式,以便存储到res
中。这个方法遍历棋盘的每一行,将其转换为字符串,并添加到列表中。isvalid(int row, int col, int n, char[][] board)
:检查在board[row][col]
位置放置一个皇后是否是一个有效的操作。它检查当前列、左上对角线和右上对角线上是否已经有皇后存在。
-
初始化棋盘:将
n×n
的棋盘全部填充为.
,表示初始时棋盘为空。 -
递归搜索:从第一行开始,尝试在每一行的每一列上放置一个皇后。对于每个位置,首先使用
isvalid
方法检查在这个位置放置皇后是否合法(即是否不会被其他皇后攻击)。- 如果合法,就在这个位置放置皇后(标记为
Q
),然后递归地在下一行继续尝试放置下一个皇后。 - 如果在某一行的所有列上都不能放置皇后,递归将自动回溯到上一行,尝试将上一个皇后放置到不同的列中。
- 如果合法,就在这个位置放置皇后(标记为
-
记录解决方案:当成功在最后一行放置了皇后,这意味着找到了一个有效的棋盘配置。此时,使用
Array2List
方法将这个配置转换为列表形式,并将其添加到res
中。 -
回溯和清理:在探索完一个有效的配置或回溯后,需要将当前位置的皇后移除(将
board[row][col]
重置为.
),以便尝试其他可能的配置。
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] path = new char[n][n];
for(char[] c: path) Arrays.fill(c,'.');
findpath(n,0,path);
return res;
}
public void findpath(int n,int row,char[][] board){
if(n == row){
res.add(ArrayToList(board));
return ;
}
for(int i=0;i<n;i++){
if(isvalid(row,i,n,board)){
board[row][i] = 'Q';
findpath(n,row+1,board);
board[row][i] = '.';
}
}
}
public List ArrayToList(char[][] board){
List<String> list = new ArrayList<>();
for(char[] c: board){
list.add(String.copyValueOf(c));
}
return list;
}
public boolean isvalid(int row,int col,int n,char[][] board){
for(int i=0;i<row;i++){
if(board[i][col] == 'Q') return false;
}
for(int i=row-1,j=col-1;i>=0&&j>=0;i--,j--)if(board[i][j] == 'Q') return false;
for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++)if(board[i][j] == 'Q')return false;
return true;
}
}
二分查找
63.搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n)
的算法。
思路:因为是一个排序的数组,因此直接用排序的算法例如二分查找的方法来查找即可。主要就是设置一个左指针和右指针,然后让他们加起来除以二,然后设置为mid,如果数组mid下标的值比target大说明插入位置可能在mid左侧那就让r = mid-1,如果mid小标的值比target小,说明插入位置在mid右侧。最后返回即可。
class Solution {
public int searchInsert(int[] nums, int target) {
int n = nums.length;
int l = 0,r = n-1;
while(l <= r){
int mid = l+ (r-l)/2;
if(nums[mid] >= target) r = mid-1;
else l = mid+1;
}
return l;
}
}
64.搜索二位矩阵
给你一个满足下述两条属性的 m x n
整数矩阵:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
给你一个整数 target
,如果 target
在矩阵中,返回 true
;否则,返回 false
。
思路:因为是顺序排列,采用二分查找的方式,遍历每一行,把每一行当作一个数组,然后二分查找,找到返回true,遍历结束没返回说明没找到,返回false;
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
for(int[] num : matrix){
int l = 0, r = num.length-1;
while(l <= r){
int mid = l+(r-l)/2;
if(num[mid] == target)return true;
else if(num[mid] > target) r = mid-1;
else l = mid+1;
}
}
return false;
}
}
65.在排序数组中查找元素的第一个和最后一个位置
给你一个按照非递减顺序排列的整数数组 nums
,和一个目标值 target
。请你找出给定目标值在数组中的开始位置和结束位置。
如果数组中不存在目标值 target
,返回 [-1, -1]
。
你必须设计并实现时间复杂度为 O(log n)
的算法解决此问题。
思路:先通过二分查找的方式找到对应的target,然后设置两个指针,向右左扩张,扩张到该target的边界然后加入到res的数组中作为起始位置和终止位置。
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] res = new int[2];
res[0] = -1;
res[1] = -1;
int n = nums.length;
int l = 0,r = n-1;
while(l <= r){
int mid = l + (r-l)/2;
if(nums[mid] == target){
l = mid;r = mid;
while(l>=0&&nums[l] == target)l--;
while(r<n&&nums[r] == target)r++;
res[0] = l+1;
res[1] = r-1;
break;
}else if(nums[mid] > target) r = mid-1;
else l = mid+1;
}
return res;
}
}
66.搜索旋转排序数组
整数数组 nums
按升序排列,数组中的值 互不相同 。
在传递给函数之前,nums
在预先未知的某个下标 k
(0 <= k < nums.length
)上进行了 旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]]
(下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7]
在下标 3
处经旋转后可能变为 [4,5,6,7,0,1,2]
。
给你 旋转后 的数组 nums
和一个整数 target
,如果 nums
中存在这个目标值 target
,则返回它的下标,否则返回 -1
。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
思路:将数组一分为二,其中一定有一个是有序的,另一个可能是有序,也能是部分有序。 此时有序部分用二分法查找。无序部分再一分为二,其中一个一定有序,另一个可能有序,可能无序。就这样循环.
这启示我们可以在常规二分查找的时候查看当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定我们该如何改变二分查找的上下界,因为我们能够根据有序的那部分判断出 target 在不在这个部分:
如果 [l, mid - 1] 是有序数组,且 target 的大小满足 [nums[l],nums[mid]),则我们应该将搜索范围缩小至 [l, mid - 1],否则在 [mid + 1, r] 中寻找。
如果 [mid, r] 是有序数组,且 target 的大小满足 (nums[mid+1],nums[r]],则我们应该将搜索范围缩小至 [mid + 1, r],否则在 [l, mid - 1] 中寻找。
class Solution {
public int search(int[] nums, int target) {
int n = nums.length;
if(n == 0)return -1;
if(n == 1)return nums[0]==target?0:-1;
int l = 0,r = n-1;
while(l <= r){
int mid = l + (r-l)/2;
if(nums[mid] == target)return mid;
if(nums[0] <= nums[mid]){
if(nums[0] <= target && nums[mid] > target) r = mid-1;
else l = mid+1;
}
else {
if(nums[mid]<target && nums[n-1] >= target) l = mid+1;
else r = mid-1;
}
}
return -1;
}
}
67.寻找旋转排序数组中的最小值
已知一个长度为 n
的数组,预先按照升序排列,经由 1
到 n
次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7]
在变化后可能得到:
- 若旋转
4
次,则可以得到[4,5,6,7,0,1,2]
- 若旋转
7
次,则可以得到[0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], ..., a[n-1]]
旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], ..., a[n-2]]
。
给你一个元素值 互不相同 的数组 nums
,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
你必须设计一个时间复杂度为 O(log n)
的算法解决此问题。
思路:和上一题几乎是一模一样的思路。
-
旋转排序数组的特性:旋转之后,数组被分为两个排序的子数组,前一个子数组的所有元素都大于后一个子数组的元素。这意味着数组的最小值一定在较小的那个子数组里。
-
二分查找的应用:通过比较中间元素和右端元素的大小,我们可以判断最小值是在左半部分还是右半部分。
- 如果
nums[mid] < nums[r]
,这说明mid
右侧的元素是升序的,最小值不可能在mid
的右侧,所以将搜索范围缩小到左半边,即r = mid
。 - 如果
nums[mid] >= nums[r]
,这说明最小值在mid
的右侧或mid
本身就是最小值,因此将搜索范围缩小到右半边,即l = mid + 1
。
- 如果
-
循环和终止条件:循环继续执行,直到
l < r
不再成立,即l == r
。此时,l
和r
指向同一个位置,根据上述逻辑,这个位置上的元素就是旋转排序数组的最小值。
class Solution {
public int findMin(int[] nums) {
int n = nums.length;
int l = 0,r = n-1;
while(l < r){
int mid = l + (r-l)/2;
if(nums[mid] < nums[r])r = mid;
else l = mid+1;
}
return nums[l];
}
}
68.寻找两个正序数组的中位数
给定两个大小分别为 m
和 n
的正序(从小到大)数组 nums1
和 nums2
。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
。
思路,用空间换时间,先把两个数组复制合并到一个新的数组,然后直接返回其中位数。但是合并前需要先判断是否有一个会空,为空的话就直接返回另一个的中位数。然后还需要判断是奇数还是偶数,偶数的话还需要中间的两个数相加再求平均值。
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length;
int[] nums = new int[m+n];
if(n == 0){
if(m%2==0) return (nums2[m/2-1]+nums2[m/2])/2.0;
else return nums2[m/2];
}
if(m == 0){
if(n%2==0) return (nums1[n/2-1]+nums1[n/2])/2.0;
else return nums1[n/2];
}
int i=0,j=0,count=0;
while(count != (n+m)){
if(i==n){
while(j!=m)nums[count++] = nums2[j++];
break;
}
if(j==m){
while(i!=n)nums[count++] = nums1[i++];
break;
}
if(nums1[i]<nums2[j]){
nums[count++] = nums1[i++];
}else{
nums[count++] = nums2[j++];
}
}
if(count%2 == 0) return (nums[count/2-1] + nums[count/2])/2.0;
else return nums[count/2];
}
}
栈
69.有效的括号
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串 s
,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
思路:遍历这个字符串里面的每一个字符,如果遇到是左括号,就把其对应的右括号添加到栈中,如果不是左括号,先判断栈是不是为空,如果栈为空,说明这个字符是多余的,返回false。如果不为空,则判断栈顶的字符和这个字符是否相等,相等代表是匹配的那么栈内字符直接出栈,遍历下一个,不相等返回false;最后遍历结束返回栈是否为空,为空说都是一一对应的,不为空说明有左括号是多余的。
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(char c : s.toCharArray()){
if(c == '(') stack.push(')');
else if(c == '[') stack.push(']');
else if(c == '{') stack.push('}');
else if(stack.isEmpty() || stack.peek() != c) return false;
else stack.pop();
}
return stack.isEmpty();
}
}
70.最小栈
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack
类:
MinStack()
初始化堆栈对象。void push(int val)
将元素val推入堆栈。void pop()
删除堆栈顶部的元素。int top()
获取堆栈顶部的元素。int getMin()
获取堆栈中的最小元素。
思路:因为栈是先进先出的特性,因此我们可以在每个元素入栈时把当前的最小值m存储起来,用另外一个栈来存储,在之后如果栈顶元素出栈,那么直接返回存储的另一个栈的栈定元素也就是当前的最小值。
class MinStack {
Deque<Integer> xstack;
Deque<Integer> mstack;
public MinStack() {
xstack = new LinkedList<Integer>;
mstack = new LinkedList<Integer>;
mstack.push(Integer.MAX_VALUE);
}
public void push(int val) {
xstack.push(val);
mstack.push(Math.min(mstack.peek(),val));
}
public void pop() {
xstack.pop();
mstack.pop();
}
public int top() {
return xstack.peek();
}
public int getMin() {
return mstack.peek();
}
}
71.字符串解码
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string]
,表示其中方括号内部的 encoded_string
正好重复 k
次。注意 k
保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k
,例如不会出现像 3a
或 2[4]
的输入。
思路:创建两个栈,一个用来存储整数,一个用来存储字符串。先遍历字符串里面的每个字符,如果是数字就通过k = k*10 + (c - '0')的方式变成整数累积起来,遇到字符串就添加到一个保存字符串的变两种,然后如果遇到'['说明要入栈,就把整数k和字符串current分别入栈。然后如果遇到了']'就代表括号内的已经计算完了,出栈然后循环把出栈的字符串添加到current。最后返回current。
class Solution {
public String decodeString(String s) {
Stack<Integer> num = new Stack<>();
Stack<String> str = new Stack<>();
int k = 0;
String current = "";
for(char c : s.toCharArray()){
if(Character.isDigit(c)) k = k*10 + ( c - '0');
else if(c == '['){
num.push(k);
str.push(current);
k = 0;
current = "";
}else if(c == ']'){
StringBuilder tmp = new StringBuilder(str.pop());
int nums = num.pop();
for(int i=0;i<nums;i++) tmp.append(current);
current = tmp.toString();
}else current += c;
}
return current;
}
}
72.每日温度
给定一个整数数组 temperatures
,表示每天的温度,返回一个数组 answer
,其中 answer[i]
是指对于第 i
天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0
来代替。
思路:使用栈的方法,先遍历每一个数,将其下标入栈,如果遍历到的数比前面的大,就把前面的数依次出栈知道当前数不大于栈内的数,出栈后,使用当前下标减去出栈的下标就是该下标之后的第几天出现的更高气温。将其保存到一个新的数组,最后返回。
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int length = temperatures.length;
int[] ans = new int[length];
Deque<Integer> stack = new LinkedList<Integer>();
for(int i=0;i < length;i++){
int temperature = temperatures[i];
while(!stack.isEmpty() && temperature > temperatures[stack.peek()]){
int prevIndex = stack.pop();
ans[prevIndex] = i - prevIndex;
}
stack.push(i);
}
return ans;
}
}
73.柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
思路:这题考的基础模型其实就是:在一维数组中对每一个数找到第一个比自己小的元素。这类“在一维数组中找第一个满足某种条件的数”的场景就是典型的单调栈应用场景。
单调增栈:
- 进栈前弹出的都是左边比自己大的→确定左边界;
- 出栈时必定是右边第一次遇到比自己小的→确定右边界
- 我们遍历数组,枚举每个元素
nums[i]
,就称为新元素吧。若新元素大于栈顶,就加入新元素;若新元素小于栈顶元素,栈顶元素出栈,进行相关处理
,然后一直出栈,直到栈中栈顶元素小于新元素,将新元素入栈。这样我们就维持了单调栈的定义,即栈中保存的是单调递增的序列。 - 那这个
相关处理
是什么?就要看我们的目标。我们的目标是求最大矩形面积,其实就是对数组中的每个元素高度,找最大的长度。最大的长度是多少?举个例子,heights = [2,1,5,6,2,3]
中,5高度对应的最大长度是多少?从5开始分别向左向右找,向左找到元素5第一个小于5的元素1,向右找到5第一个小于5的元素2
,则元素5对应的长度是4-1-1=2
,这是暴力的思路,我们用单调栈来处理这个过程。
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
Stack<Integer> stack = new Stack<>();
int maxarea = 0;
for(int i=0;i<=n;i++){
int h = (i == n)?0:heights[i];
while(!stack.isEmpty() && h < heights[stack.peek()]){
int heigh = heights[stack.pop()];
int width = stack.isEmpty()?i : i - stack.peek() -1;
maxarea = Math.max(maxarea,heigh*width);
}
stack.push(i);
}
return maxarea;
}
}
堆
74.数组中的第k个最大元素
给定整数数组 nums
和整数 k
,请返回数组中第 k
个最大的元素。
请注意,你需要找的是数组排序后的第 k
个最大的元素,而不是第 k
个不同的元素。
你必须设计并实现时间复杂度为 O(n)
的算法解决此问题。
思路:1.使用快速排序然后返回第k个最大值,这里注意的是,求的是n-k位置的数因此快排在递归的时候小于n-k,就是左侧其实完全可以不做排序,只排序右侧的,这样可以减少时间复杂度。
2.使用堆排序,建立一个大根堆,做k-1次删除操作后堆顶元素就是我们要找的答案。构建堆
(1)按顺序先构建成一颗二叉树
(2)从第一个非叶子节点为根节点子树开始,将其调整为大根堆
(3)然后调整完之后,堆顶元素删除
-
findKthLargest
: 这是主方法,用于找到数组中第 k 大的元素。它首先构建一个最大堆,然后通过交换堆顶元素(即最大元素)到正确位置,逐步减小堆的大小,直到找到第 k 大的元素。 -
buildMaxHeap
: 此方法用于将一个普通数组转换成最大堆。它从最后一个非叶子节点开始,逐个确保每个节点都遵循最大堆的性质。 -
maxHeapify
: 这个方法是堆排序的核心。它确保以索引i
为根的子树是一个最大堆。如果i
的子节点大于i
,它们会被交换,然后递归地在交换后的子树上调用自己。 -
swap
: 简单的辅助方法,用于交换数组中的两个元素。
总体流程
- 建堆: 通过
buildMaxHeap
把数组转换成最大堆。 - 排序:
- 把堆顶元素(当前最大元素)与数组最后一个元素交换。
- 减少堆的大小(排除已排序的最大元素)。
- 通过
maxHeapify
重新调整剩余元素,确保顶部是当前最大的元素。
- 重复步骤2,直到找到第 k 个最大元素。
class Solution {
public int findKthLargest(int[] nums, int k) {
int n = nums.length;
// 大跟堆排序
buildMaxHeap(nums, n);
for(int i=n-1;i>=nums.length-k+1;--i){
swap(nums, 0, i);
--n;
maxHeapify(nums, 0, n);
}
return nums[0];
}
// 从中间开始交换
public void buildMaxHeap(int[] a, int n){
for(int i=n /2;i>=0;--i) maxHeapify(a, i, n);
}
// 把左右子树比自己大的调整到前面去
public void maxHeapify(int[] a, int i, int n){
int l = i*2+1, r = i*2+2, largest = i;
if(l < n && a[l] > a[largest]) largest = l;
if(r < n && a[r] > a[largest]) largest = r;
if(largest != i){
swap(a,i,largest);
maxHeapify(a,largest,n);
}
}
public void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
75.前k个高频元素
给你一个整数数组 nums
和一个整数 k
,请你返回其中出现频率前 k
高的元素。你可以按 任意顺序 返回答案。
思路:
首先遍历整个数组,并使用哈希表记录每个数字出现的次数,并形成一个「出现次数数组」。找出原数组的前 k 个高频元素,就相当于找出「出现次数数组」的前 k 大的值。
最简单的做法是给「出现次数数组」排序。但由于可能有 O(N) 个不同的出现次数(其中 N 为原数组长度),故总的算法复杂度会达到 O(NlogN),不满足题目的要求。
在这里,我们可以利用堆的思想:建立一个小顶堆,然后遍历「出现次数数组」:
如果堆的元素个数小于 k,就可以直接插入堆中。
如果堆的元素个数等于 k,则检查堆顶与当前出现次数的大小。如果堆顶更大,说明至少有 k 个数字的出现次数比当前值大,故舍弃当前值;否则,就弹出堆顶,并将当前值插入堆中。
遍历完成后,堆中的元素就代表了「出现次数数组」中前 kkk 大的值。
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int num : nums) map.put(num, map.getOrDefault(num, 0) + 1);
// 构建一个队列用来表示小根堆
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] m, int[] n) {
return m[1] - n[1];
}
});
// 堆内的元素不够k个时,直接插入,多余k个时,比较当前和堆顶的多少,
// 多就把堆顶的弹出,当前的插入。少就直接忽略
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
int num = entry.getKey(), count = entry.getValue();
if (queue.size() == k) {
if (queue.peek()[1] < count) {
queue.poll();
queue.offer(new int[]{num, count});
}
} else {
queue.offer(new int[]{num, count});
}
}
// 最后依次加入到结果数组众
int[] ret = new int[k];
for (int i = 0; i < k; ++i) {
ret[i] = queue.poll()[0];
}
return ret;
}
}
76.数据流的中位数
中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类:
-
MedianFinder()
初始化MedianFinder
对象。 -
void addNum(int num)
将数据流中的整数num
添加到数据结构中。 -
double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
思路:左边大顶堆,右边小顶堆,小的加左边,大的加右边,平衡俩堆数,新加就弹出,堆顶给对家,奇数取多的,偶数取除2.
我们用两个优先队列 queMax 和 queMin 分别记录大于中位数的数和小于等于中位数的数。当累计添加的数的数量为奇数时,queMin 中的数的数量比 queMax 多一个,此时中位数为 queMin 的队头。当累计添加的数的数量为偶数时,两个优先队列中的数的数量相同,此时中位数为它们的队头的平均值。
当我们尝试添加一个数 num 到数据结构中,我们需要分情况讨论:
num≤max{queMin}
此时 num 小于等于中位数,我们需要将该数添加到 queMin 中。新的中位数将小于等于原来的中位数,因此我们可能需要将 queMin 中最大的数移动到 queMax\textit{queMax}queMax 中。
num>max{queMin}
此时 num\textit{num}num 大于中位数,我们需要将该数添加到 queMin 中。新的中位数将大于等于原来的中位数,因此我们可能需要将 queMax 中最小的数移动到 queMin 中。
特别地,当累计添加的数的数量为 000 时,我们将 num 添加到 queMin 中。
class MedianFinder {
PriorityQueue<Integer> queMin;
PriorityQueue<Integer> queMax;
public MedianFinder() {
//构建两个堆
queMin = new PriorityQueue<Integer>((a, b) -> (b - a));
queMax = new PriorityQueue<Integer>((a, b) -> (a - b));
}
public void addNum(int num) {
// 维持两个堆的数量差距不超过1,超了就把堆顶弹出给另一边
// 比中位数大的都放小根堆,小的放大根堆
if (queMin.isEmpty() || num <= queMin.peek()) {
queMin.offer(num);
if (queMax.size() + 1 < queMin.size()) {
queMax.offer(queMin.poll());
}
} else {
queMax.offer(num);
if (queMax.size() > queMin.size()) {
queMin.offer(queMax.poll());
}
}
}
public double findMedian() {
// 中位数先判断是奇偶,奇的话小根堆会多出一个,直接返回堆顶即可,偶的话相加取平均值
if (queMin.size() > queMax.size()) {
return queMin.peek();
}
return (queMin.peek() + queMax.peek()) / 2.0;
}
}
贪心算法
77.买卖股票的最佳时机
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0
。
思路:可以使用动态规划或者贪心,对于贪心思路,使用两个变量,一个记录当前的最大利润,一个记录到目前为止的最低值。然后遍历数组,我们用当前遍历到的价格减去历史最低值得到利润,然后和最大利润比较,最后返回
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int max = 0;
int min = prices[0];
for(int i=1;i<n;i++){
max = Math.max(max,prices[i] - min);
min = Math.min(min,prices[i]);
}
return max;
}
}
78.跳跃游戏
给你一个非负整数数组 nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标,如果可以,返回 true
;否则,返回 false
。
思路:使用res变量来记录目前能够跳跃到最远位置,然后遍历数组,以目前的下标+下标的值代表所能跳跃的距离和res进行比较,如果大于等于数组的长度,直接返回true,如果数组遍历结束都没有返回true,说明走不到就返回false;
class Solution {
public boolean canJump(int[] nums) {
int n = nums.length;
int res = 0;
for(int i=0;i<=res;i++){
res = Math.max(res,i+nums[i]);
if(res>=n-1) return true;
}
return false;
}
}
79.跳跃游戏II
给定一个长度为 n
的 0 索引整数数组 nums
。初始位置为 nums[0]
。
每个元素 nums[i]
表示从索引 i
向前跳转的最大长度。换句话说,如果你在 nums[i]
处,你可以跳转到任意 nums[i + j]
处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1]
的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]
。
思路:这个是要记录跳跃的次数,因此我们需要一个变量来记录之前依次跳跃的最大位置,当我们遍历到最大位置时,就让跳跃次数+1。最后遍历完返回次数。
class Solution {
public int jump(int[] nums) {
int n = nums.length;
int pre = 0;
int res = 0;
int num = 0;
for(int i=0;i<n-1;i++){
res = Math.max(res,i+nums[i]);
if(i == pre){
num++;
pre = res;
}
}
return num;
}
}
80.划分字母区间
给你一个字符串 s
。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s
。
返回一个表示每个字符串片段的长度的列表。
思路:先进行依次字符串遍历,把每个字符的最远距离记录下来,然后再依次遍历字符串,使用一个res来记录当前字符的最远距离,使用pre来记录上一次划分的为止,不断刷新最远距离,当遍历到一个当前字符串时的下标刚好等于最远距离,说明可以从这个为止开始划分,划分长度为res-pre;pre = res,因为前面所有的字符都不会再后面出现了。
class Solution {
public List<Integer> partitionLabels(String s) {
int n = s.length();
List<Integer> list = new ArrayList<>();
int[] num = new int[26];
char[] c = s.toCharArray();
for(int i=0;i<n;i++){
num[c[i] - 'a'] = i;
}
int res = 0;
int pre = -1;
for(int i=0;i<n;i++){
res = Math.max(res,num[c[i] - 'a']);
if(i == res){
list.add(res - pre);
pre = res;
}
}
return list;
}
}
动态规划
81.爬楼梯
假设你正在爬楼梯。需要 n
阶你才能到达楼顶。
每次你可以爬 1
或 2
个台阶。你有多少种不同的方法可以爬到楼顶呢?
思路:按照动态规划五部曲的思路:
(1)确定dp数组以及下标的含义:dp数组以及下标表示爬到该层台阶的最大方法数
(2)确定递推公式:dp[0] = 0,dp[1] = 1,dp[2] = 2,从3开始dp[i] = dp[i-1] + dp[i-2]
(3)dp数组如何初始化:把0,1,2直接初始化,从3开始可以使用递推公式
(4)确定便利顺序:正向遍历,因为递推公式需要依赖前边的数组值
(5)举例推导dp数组:手算一下前面几个数组的值
class Solution {
public int climbStairs(int n) {
int[] dp = new int[n+1];
if(n <3)return n;
dp[0] = 0;
dp[1] = 1;
dp[2] = 2;
for(int i=3;i<=n;i++){
dp[i] = dp[i-1]+dp[i-2];
}
return dp[n];
}
}
82.杨辉三角
给定一个非负整数 numRows
,生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
思路:杨辉三角的外围都是1,因此我们遍历每一层的时候开头和末尾都先设置为1,然后中间数值是根据上一层的j和j-1的值计算出来的公式为:row.add(prerow.get(j-1)+prerow.get(j));然后先创建一个list数组存储当前层的数值,遍历完这一层之后就把list存入最终的数组中。
(1)创建一个res数组来保存最终的结果
(2)先把第一层存入1,因为第一层只有一个数
(3)遍历numRows层数,先创建存储当前层数值的数组row,再把上一层的数组拿出来prerow
(4)当前层的开头和末尾补上1,中间数值使用公式row.add(prerow.get(j-1)+prerow.get(j))
(5)遍历结束后添加到res中,最后返回res
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> res = new ArrayList<>();
if(numRows == 0)return res;
res.add(new ArrayList<>());
res.get(0).add(1);
for(int i=1;i<numRows;i++){
List<Integer> row = new ArrayList<>();
List<Integer> prerow = res.get(i-1);
row.add(1);
for(int j=1;j<i;j++){
row.add(prerow.get(j-1)+prerow.get(j));
}
row.add(1);
res.add(row);
}
return res;
}
}
83.打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
思路:因为不能两个房屋连着偷,因此就延伸出的题推公式。按照动规五部曲:
(1)确定dp数组以及下标的含义:dp数组以及下标表示偷到当前家可偷的最大金额
(2)确定递推公式:dp[0] = nums[0],dp[1] = Math.max(nums[0],nums[1]),从2开始dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i])
(3)dp数组如何初始化:把0,1直接初始化,从2开始可以使用递推公式
(4)确定便利顺序:正向遍历,因为递推公式需要依赖前边的数组值
(5)举例推导dp数组:手算一下前面几个数组的值
class Solution {
public int rob(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
if(n==1) return nums[0];
if(n==2) return Math.max(nums[0],nums[1]);
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i=2;i<n;i++){
dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
84.完全平方数
给你一个整数 n
,返回 和为 n
的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1
、4
、9
和 16
都是完全平方数,而 3
和 11
不是。
思路:因为是寻找由完全平方数组成整数的最少数量,按照动规五部曲
(1)确定dp数组以及下标的含义:dp数组以及下标表示组成当前数的最少平方数数量
(2)确定递推公式:dp[0] = 0,从1开始dp[j] = Math.min(dp[j], dp[j - i*i] + 1)
(3)dp数组如何初始化:把dp数组都初始化为最大整数
(4)确定便利顺序:两层for循环,外层遍历平方数,内层遍历数字
(5)举例推导dp数组:手算一下前面几个数组的值
class Solution {
public int numSquares(int n) {
int[] dp = new int[n+1];
for(int i=0;i<=n;i++)dp[i] = Integer.MAX_VALUE;
dp[0] = 0;
for(int i=1;i<=n;i++){
for(int j=i*i;j<=n;j++){
dp[j] = Math.min(dp[j],dp[j - i*i]+1);
}
}
return dp[n];
}
}
85.零钱兑换
给你一个整数数组 coins
,表示不同面额的硬币;以及一个整数 amount
,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
。
你可以认为每种硬币的数量是无限的。
思路:因为是寻找由完全平方数组成整数的最少数量,按照动规五部曲
(1)确定dp数组以及下标的含义:dp数组以及下标表示组成当前金额的最少硬币数
(2)确定递推公式:dp[0] = 0,从1开始dp[j] = Math.min(dp[j], dp[j] - coins[i]] + 1)
(3)dp数组如何初始化:把dp数组都初始化为最大整数
(4)确定便利顺序:两层for循环,外层遍历硬币,内层遍历金额
(5)举例推导dp数组:手算一下前面几个数组的值
class Solution {
public int coinChange(int[] coins, int amount) {
int n = coins.length;
int[] dp = new int[amount+1];
for(int i=0;i<=amount;i++) dp[i] = Integer.MAX_VALUE;
dp[0] = 0;
for(int i=0;i<n;i++){
for(int j=coins[i];j<=amount;j++){
if(dp[j- coins[i]]!=Integer.MAX_VALUE)dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount] == Integer.MAX_VALUE?-1:dp[amount];
}
}
86.单词拆分
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
思路:采用字符串拼接的方式,使用布尔类型的数组来表当前截取的字符串是否能匹配。创建一个set集合保存给出的字典,用于匹配截取的字符串。
(1)确定dp数组以及下标的含义:dp数组以及下标表示当前截取到的字符串是否能被字典组成
(2)确定递推公式:if(set.contains(s.substring(j,i))&&res[j]==true)res[i] = true;
(3)dp数组如何初始化:res[0] = true
(4)确定便利顺序:两层for循环,外层遍历字符串的自负,内层遍历之前的字符
(5)举例推导dp数组:手算一下前面几个数组的值
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
Set<String> set = new HashSet<>(wordDict);
boolean[] res = new boolean[n+1];
res[0] = true;
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
if(set.contains(s.substring(j,i))&&res[j]==true)res[i] = true;
}
}
return res[n];
}
}
87.最长递增子序列
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的子序列。
思路:
(1)确定dp数组以及下标的含义:dp数组以及下标表示以当前数值最终点的最长递增子序列长度
(2)确定递推公式:if(nums[i] > nums[j]) dp[i] = Math.max(dp[i],dp[j]+1);
(3)dp数组如何初始化:Arrays.fill(dp,1)
(4)确定便利顺序:两层for循环,外层数组,内层遍历之前的dp数组
(5)举例推导dp数组:手算一下前面几个数组的值
class Solution {
public int lengthOfLIS(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp,1);
for(int i=1;i<n;i++){
for(int j=0;j<i;j++){
if(nums[i] > nums[j]) dp[i] = Math.max(dp[i],dp[j]+1);
}
}
int max = dp[0];
for(int i=0;i<n;i++) max = Math.max(max,dp[i]);
return max;
}
}
88.乘积最大子数组
给你一个整数数组 nums
,请你找出数组中乘积最大的非空连续
子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
测试用例的答案是一个 32-位 整数。
思路:
- 使用一个循环从
i = 1
到n
遍历数组。对于每个i
,进行以下操作:dpmin[i] = Math.min(dpmin[i-1]*nums[i], Math.min(nums[i], dpmax[i-1]*nums[i]));
- 计算以nums[i]
结尾的子数组的最小乘积。dpmax[i] = Math.max(dpmax[i-1]*nums[i], Math.max(nums[i], dpmin[i-1]*nums[i]));
- 计算以nums[i]
结尾的子数组的最大乘积。
- 更新
dpmin[i]
和dpmax[i]
时,考虑三种情况:- 只取当前元素
nums[i]
,这适用于情况,比如之前的乘积太小不如重新开始。 - 将当前元素和之前计算的最小乘积
dpmin[i-1]
相乘,这在当前数字和之前最小乘积都为负数时,可能会得到最大的正数。 - 将当前元素和之前计算的最大乘积
dpmax[i-1]
相乘,这在两者都为正数时可能会得到更大的正数。
- 只取当前元素
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length;
int[] dpmax = new int[n];
int[] dpmin = new int[n];
System.arraycopy(nums,0,dpmax,0,n);
System.arraycopy(nums,0,dpmin,0,n);
for(int i=1;i<n;i++){
dpmin[i] = Math.min(dpmin[i-1]*nums[i],Math.min(nums[i],dpmax[i-1]*nums[i]));
dpmax[i] = Math.max(dpmax[i-1]*nums[i],Math.max(nums[i],dpmin[i-1]*nums[i]));
}
int res = dpmax[0];
for(int i=0;i<n;i++) res = Math.max(res,dpmax[i]);
return res;
}
}
89.分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路:因为是把数组分割为两个相等的子集,因此,首先要判断能不能分割的了。如果把数组值全部加起来取余是1,说明分割不了。如果最大的数组值比全部加起来的值一般还要大,也分割不了。如果都不成立,说明可以分割。分割的方法就是寻找一组能够达到全部加来数值的一半的组合即可。采用动规五部曲:
(1)确定dp数组以及下标的含义:dp数组以及下标表示以当前数组下标i时,是否能够组合成j值
(2)确定递推公式:if(j > nums[i]) dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i]];
(3)dp数组如何初始化:j=0全部初始化为true,dp[0][nums[0]]=true
(4)确定便利顺序:两层for循环,外层遍历数组,内层遍历目标值
(5)举例推导dp数组:手算一下前面几个数组的值
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
int maxnum = 0;
for(int i=0;i<n;i++) {
sum += nums[i];
maxnum = Math.max(maxnum, nums[i]);
}
if(sum%2 == 1)return false;
int target = sum/2;
if(maxnum > target)return false;
boolean[][] dp = new boolean[n][target+1];
for(int i=0;i<n;i++)dp[i][0] = true;
dp[0][nums[0]] = true;
for(int i=1;i<n;i++){
for(int j=1;j<=target;j++){
if(j > nums[i]) dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i]];
else dp[i][j] = dp[i-1][j];
}
}
return dp[n-1][target];
}
}
90.最长有效括号
给你一个只包含 '('
和 ')'
的字符串,找出最长有效(格式正确且连续)括号
子串的长度。
思路:动态规划的核心思想是通过将大问题分解为小问题,并找到小问题之间的关系,从而解决大问题。在这个问题中,我们定义一个数组 dp
,其中 dp[i]
表示以 s[i]
结尾的最长有效括号子串的长度。遍历字符串 s
,按以下规则更新 dp
数组,最终求得的 maxlen
就是问题的答案。
-
初始化变量:
maxlen
用于记录遍历过程中遇到的最长有效括号子串的长度。n
为字符串s
的长度。dp
是一个长度为n
的数组,初始化全为 0,用于记录到当前位置为止的最长有效括号子串的长度。
-
遍历字符串:
- 从
i = 1
开始遍历字符串s
,因为一个有效的括号至少需要两个字符,所以第一个字符不用考虑。
- 从
-
主要逻辑:
- 如果
s[i]
是右括号')'
,则有两种情况可以构成有效括号子串:- 情况一:
s[i-1]
是左括号'('
,即形如"...()"
。此时,dp[i] = dp[i-2] + 2
。需要注意的是,如果i-2
小于 0,则dp[i-2]
应视为 0。 - 情况二:
s[i-1]
是右括号')'
,且s[i - dp[i-1] - 1]
是左括号'('
,即形如"...))"
,其中s[i - dp[i-1] - 1]
是与s[i]
匹配的左括号。此时,dp[i] = dp[i-1] + dp[i - dp[i-1] - 2] + 2
。这里dp[i-1]
是内部子串的长度,dp[i - dp[i-1] - 2]
是与当前右括号匹配的左括号之前的有效括号子串长度。如果i - dp[i-1] - 2
小于 0,则dp[i - dp[i-1] - 2]
应视为 0。
- 情况一:
- 每次计算后,更新
maxlen
。
- 如果
-
返回结果:
- 遍历完成后,
maxlen
中存储的就是最长有效括号子串的长度。
- 遍历完成后,
class Solution {
public int longestValidParentheses(String s) {
int maxlen = 0;
int n = s.length();
int[] dp = new int[n];
for(int i=1;i<n;i++){
if(s.charAt(i) == ')'){
if(s.charAt(i-1) == '('){
dp[i] = (i>=2?dp[i-2]:0)+2;
}
else if(i - dp[i-1] > 0 && s.charAt(i - dp[i-1] -1) == '('){
dp[i] = dp[i-1] + (i-dp[i-1]>=2?dp[i-dp[i-1]-2]:0)+2;
}
maxlen = Math.max(maxlen, dp[i]);
}
}
return maxlen;
}
}
91.不同路径
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
思路:第一行和第一列路径只能是1,然后内部的路径可以通过递推公式来计算:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i=0;i<m;i++) dp[i][0] = 1;
for(int i=0;i<n;i++) dp[0][i] = 1;
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
92.最小路径和
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
思路:第一行和第一列的数字总和就是直接累加,先初始化第一行和第一列,然后遍历数组,递推公式为:dp[i][j] = Math.min(dp[i-1][j],dp[i][j-1])
class Solution {
public int minPathSum(int[][] grid) {
int n = grid.length;
int m = grid[0].length;
for(int i=1;i<n;i++) grid[i][0] += grid[i-1][0];
for(int j=1;j<m;j++) grid[0][j] += grid[0][j-1];
for(int i=1;i<n;i++){
for(int j=1;j<m;j++){
grid[i][j] += Math.min(grid[i-1][j],grid[i][j-1]);
}
}
return grid[n-1][m-1];
}
}
93.最长回文子串
给你一个字符串 s
,找到 s
中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。
思路:
-
初始化动态规划数组 (
dp
):dp[i][j]
表示字符串从索引i
到j
的子串是否是回文。初始化时,每个字符自身构成的子串被认为是回文 (dp[i][i] = true
)。同时,为了方便后续计算,将dp[i+1][i]
也设为true
(这代表了一个空串,可以认为是回文)。 -
变量
begin
和end
: 这两个变量用来记录当前找到的最长回文子串的起始和结束位置。 -
外层循环:循环变量
l
代表考虑的子串的长度,从2开始到n
。这是因为长度为1的子串已经在初始化时考虑过了。 -
内层循环:内层循环通过变量
i
遍历字符串,用来确定子串的起始位置。结束位置j
通过i
和子串长度l
确定(j = l + i - 1
)。 -
更新
dp
: 对于每个子串(i, j)
,它是回文的条件是子串(i+1, j-1)
是回文,并且s.charAt(i)
等于s.charAt(j)
。这是因为如果两端的字符相同,且去掉两端字符后的子串是回文,那么整个子串也是回文。 -
记录最长回文子串:如果找到了一个回文子串,更新
begin
和end
来记录这个子串的位置。 -
返回结果:使用
begin
和end
通过substring
方法提取并返回最长的回文子串。
class Solution {
public String longestPalindrome(String s) {
int n =s.length();
boolean[][] dp = new boolean[n][n];
int start = 0;
int end = 0;
for(int i=0;i<n;i++){
dp[i][i] = true;
if(i+1<n)dp[i+1][i] = true;
}
for(int l = 2;l<n;l++){
for(int i=0;i<n;i++){
int j = l+i-1;
if(j>=n)break;
dp[i][j] = dp[i+1][j-1] && s.charAt(i) == s.charAt(j);
if(dp[i][j]){
start = i;
end = j;
}
}
}
return s.substring(start,end+1);
}
}
94.最长公共子序列
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
思路:两层for循环,分别遍历两个字符串,dp的含义是遍历到下标为i和j时,两个字符串的最长公共子序列的长度。遍历内部判断,当前下标对应的字符是否相等,相等就令当前dp的值为上一层的dp+1,否则就是去Math.max(dp[i-1][j],dp[i][j-1]);
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int n = text1.length();
int m = text2.length();
int[][] dp = new int[n+1][m+1];
dp[0][0] = 0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(text1.charAt(i-1) == text2.charAt(j-1)) dp[i][j] = dp[i-1][j-1]+1;
else dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[n][m];
}
}
95.编辑距离
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
思路:二位dp数组,初始化第一行和第一列,因为另一个字符串为0,因此编辑距离最少就是这个字符串的长度。然后两层for循环遍历两个字符串,如果两个字符串遍历到的当前字符相等,那说明编辑次数不用增加,否则就是取以下三种可能的最小次数:
(1)dp[i-1][j]+1
(2)dp[i][j-1]+1
(3)dp[i-1][j-1]+1
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
int[][] dp = new int[n+1][m+1];
for(int i=0;i<=n;i++) dp[i][0] = i;
for(int j=0;j<=m;j++) dp[0][j] = j;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(word1.charAt(i-1) == word2.charAt(j-1))dp[i][j] = dp[i-1][j-1];
else dp[i][j] = Math.min(dp[i-1][j-1]+1,Math.min(dp[i-1][j],dp[i][j-1])+1);
}
}
return dp[n][m];
}
}
96.只出现一次的数字
给你一个 非空 整数数组 nums
,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
思路:
方法1.先排序再遍历,如果当前下标的值和下一个下标的值相等,则i++遍历下一个。不相等就直接返回当前下标的值,如果i==nums.lenght-1说明出现在末尾则直接返回,因为最多只出现两次,因此这样可行。
class Solution {
public int singleNumber(int[] nums) {
quicksort(nums,0,nums.length-1);
for(int i=0;i<nums.length;i++){
if(i == nums.length-1)return nums[i];
if(nums[i] != nums[i+1])return nums[i];
else i++;
}
return -1;
}
public void quicksort(int[] nums,int l,int r){
if(l>r)return ;
int pir = nums[l];
int i = l;
int j = r;
while(i < j){
while(i<j && nums[j]>pir)j--;
nums[i] = nums[j];
while(i<j && nums[i]<=pir)i++;
nums[j] = nums[i];
}
nums[i] = pir;
quicksort(nums,l,i-1);
quicksort(nums,i+1,r);
}
}
97.多数元素
给定一个大小为 n
的数组 nums
,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
思路:先对数组进行排序,排序后取最中间的值,因为如果一个元素超过一半,那中间的值一锭是该元素。
class Solution {
public int majorityElement(int[] nums) {
quicksort(nums,0,nums.length-1);
return nums[nums.length/2];
}
public void quicksort(int[] nums,int l, int r){
if(l > r)return;
int pir = nums[l];
int i = l;
int j = r;
while(i < j){
while(i<j && nums[j]>pir)j--;
nums[i] = nums[j];
while(i<j && nums[i]<=pir)i++;
nums[j] = nums[i];
}
nums[i] = pir;
quicksort(nums, l, i-1);
quicksort(nums, i+1, r);
}
}
98.颜色分类
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
思路:直接使用一种排序方法即可,这里我使用快速排序。
class Solution {
public void sortColors(int[] nums) {
quickSort(nums,0,nums.length-1);
}
public void quickSort(int[] nums,int l,int r){
if(l > r) return ;
int priot = nums[l];
int i = l;
int j = r;
while(i < j){
while(i<j && nums[j]>priot) j--;
nums[i] = nums[j];
while(i<j && nums[i]<=priot) i++ ;
nums[j] = nums[i];
}
nums[i] = priot;
quickSort(nums,l,i-1);
quickSort(nums,i+1,r);
}
}
99.下一个排列
思路:
我们希望下一个数 比当前数大,这样才满足 “下一个排列” 的定义。因此只需要 将后面的「大数」与前面的「小数」交换,就能得到一个更大的数。比如 123456,将 5 和 6 交换就能得到一个更大的数 123465。
我们还希望下一个数 增加的幅度尽可能的小,这样才满足“下一个排列与当前排列紧邻“的要求。为了满足这个要求,我们需要:
在 尽可能靠右的低位 进行交换,需要 从后向前 查找
将一个 尽可能小的「大数」 与前面的「小数」交换。比如 123465,下一个排列应该把 5 和 4 交换而不是把 6 和 4 交换
将「大数」换到前面后,需要将「大数」后面的所有数 重置为升序,升序排列就是最小的排列。以 123465 为例:首先按照上一步,交换 5 和 4,得到 123564;然后需要将 5 之后的数重置为升序,得到 123546。显然 123546 比 123564 更小,123546 就是 123465 的下一个排列
(1)从后向前遍历,找到第一个不是降序的位置,记录其位置。
(2)如果不是降序的位置存在,再设立一个变量找到从末尾到这个位置如果出现比他小的值,就进行数值交换。不存在则跳过
(3)然后吧该位置到末尾的这一段数组进行反转,就得到了下一个更大的排列
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
int i = n-2;
while(i>=0 && nums[i]>=nums[i+1]) i--;
if(i>=0){
int j = n-1;
while(j>=0 && nums[i] >= nums[j]) j--;
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
int left = i+1, right = n-1;
while(left < right){
int tmp = nums[left];
nums[left] = nums[right];
nums[right] = tmp;
left++;right--;
}
}
}
100.寻找重复数
思路:排序之后在遍历一次找到重复的数值。
class Solution {
public int findDuplicate(int[] nums) {
quicksort(nums,0,nums.length-1);
int num = 0;
for(int i=0;i<nums.length-1;i++){
if(nums[i]==nums[i+1]){
num = nums[i];
break;
}
}
return num;
}
public void quicksort(int[] nums,int l,int r){
if(l > r)return ;
int priot = nums[l];
int i = l;
int j = r;
while(i < j){
while(i<j && nums[j] > priot)j--;
nums[i] = nums[j];
while(i<j && nums[i] <= priot)i++;
nums[j] = nums[i];
}
nums[i] = priot;
quicksort(nums,l,i-1);
quicksort(nums,i+1,r);
}
}