基础篇
1.基础知识(时间复杂度、空间复杂度等)
2.线性表(顺序表、单链表)
3.双链表、循环链表
4.队列
5.栈
6.递归算法
7.树、二叉树(递归、非递归遍历)
8.二叉搜索树(BST)
9.二分查找
10.二叉平衡树(AVL)
提高篇
1.散列表(哈希表)
2.堆
3.哈夫曼树、哈夫曼编码
4.并查集
5.串(KMP算法)
6.分治算法
7.贪心算法
8.回溯算法
9.动态规划(常见LCS、LIS等)
图论必会算法
1.DFS深度优先搜索
2.BFS广度优先搜索
3.Dijkstra(单源)最短路径算法
4.Floyd(多源)最短路径算法
5.Prim、Kruskal最小生成树算法
6.拓扑排序、关键路径
排序必知算法
1.交换类:冒泡排序
2.交换类:快速排序
3.插入类:直接插入排序
4.插入类:希尔排序
5.选择类:简单选择排序
6.选择类:堆排序
7.归并排序
8.桶排序
9.计数排序
10.基数排序
高级数据结构
1.红黑树
2.B树
3.跳跃表
4.字典树(Trie)
5.树状数组
6.后缀数组和后缀树
7.线段树
笔试面试常遇
1.快速幂
2.大数加减乘除
3.位运算
4.欧几里得(GCD)最大公约数、最小公倍数
5.滑动窗口、双指针
6.约瑟夫环问题
7.求素数(素数筛)
8.回文串(马拉车算法)
时间复杂度、空间复杂度
数组中的数之和
3个不同值的数字和为target,只需输出一组符合数组
import java.util.Arrays;
public class Main {
public static int[] threeSum(int[] nums, int target) {
Arrays.sort(nums); // 先对数组排序
int n = nums.length;
// 遍历数组,固定第一个数
for (int i = 0; i < n - 2; i++) {
// 跳过重复的数字
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1; // 第二个数的下标
int right = n - 1; // 第三个数的下标
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == target) {
return new int[] { nums[i], nums[left], nums[right] };
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
// 如果不存在符合条件的三个数,则返回空数组
return new int[] {};
}
public static void main(String[] args) {
int[] nums = { -1, 0, 1, 2, -1, -4 };
int target = 0;
int[] result = threeSum(nums, target);
if (result.length == 3) {
System.out.println("输出:" + Arrays.toString(result));
} else {
System.out.println("没有找到符合条件的三个数");
}
}
}
任意数量的可重复值数字和为target,需要输出所有符合数组
import java.util.ArrayList;
import java.util.List;
public class Main {
public static List<List<Integer>> combinationSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, target, 0, new ArrayList<>(), result);
return result;
}
private static void backtrack(int[] nums, int target, int start, List<Integer> current, List<List<Integer>> result) {
if (target == 0) {
result.add(new ArrayList<>(current));
return;
}
for (int i = start; i < nums.length; i++) {
if (target - nums[i] >= 0) {
current.add(nums[i]);
backtrack(nums, target - nums[i], i, current, result);
current.remove(current.size() - 1);
}
}
}
public static void main(String[] args) {
int[] nums = {2, 3, 6, 7};
int target = 7;
List<List<Integer>> result = combinationSum(nums, target);
// System.out.println("输出:");
for (List<Integer> combination : result) {
System.out.println(combination);
}
}
}
任意数量的子串数组和为target,需要统计所有符合数组数量
输入:nums = [1,1,1], k = 2
输出:2
public class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0;
for (int start = 0; start < nums.length; ++start) {
int sum = 0;
for (int end = start; end >= 0; --end) {
sum += nums[end];
if (sum == k) {
count++;
}
}
}
return count;
}
}
最大子数组和
你找出一个具有最大和的连续子数组,子数组是数组中的一个连续部分
输入: nums = [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续了数组 [4,1,2,1] 的和最大,为 6
class Solution {
public int maxSubArray(int[] nums) {
int pre = 0, maxAns = nums[0];
for (int x : nums) {
pre = Math.max(pre + x, x);
maxAns = Math.max(maxAns, pre);
}
return maxAns;
}
}
3个不同位置数和为0(值可能重复),需要输出所有符合数组
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。注意,输出的顺序和三元组的顺序并不重要。
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
解法:用了双指针
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
int n = nums.length;
Arrays.sort(nums);
List<List<Integer>> ans = new ArrayList<List<Integer>>();
// 枚举 a
for (int first = 0; first < n; ++first) {
// 需要和上一次枚举的数不相同,原来保证数据不一致就这啊
if (first > 0 && nums[first] == nums[first - 1]) {
continue;
}
// c 对应的指针初始指向数组的最右端
int third = n - 1;
int target = -nums[first];
// 枚举 b
for (int second = first + 1; second < n; ++second) {
// 需要和上一次枚举的数不相同
if (second > first + 1 && nums[second] == nums[second - 1]) {
continue;
}
// 需要保证 b 的指针在 c 的指针的左侧
while (second < third && nums[second] + nums[third] > target) {
--third;
}
// 如果指针重合,随着 b 后续的增加
// 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
if (second == third) {
break;
}
if (nums[second] + nums[third] == target) {
List<Integer> list = new ArrayList<Integer>();
list.add(nums[first]);
list.add(nums[second]);
list.add(nums[third]);
ans.add(list);
}
}
}
return ans;
}
}
哈希Map、List
字母异位词
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。字母异位词 是由重新排列源单词的所有字母得到的一个新单词。
输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]
解法:由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
char[] array = str.toCharArray();
Arrays.sort(array);
String key = new String(array);//排序后的结果为该字符串的key
List<String> list = map.getOrDefault(key, new ArrayList<String>());
//getOrDefault从map中获取与由key索引得到的对应值,这个值存了之前key索引下的字符串列表,如果找到则将其赋值给变量list
//如果没有找到,则创建一个新的空列表并将其赋值给list,这个不影响输出
list.add(str);
//在这个字符串列表里加入该字符串
map.put(key,list);
//如果key在map中已经存在,将更新后的列表list覆盖旧的list。
//如果 key 在 map 中不存在,则会添加新的键值对。
}
return new ArrayList<List<String>>(map.values());
}
}
双指针
判断链表是否有环
1、定义两个指针,一个快指针(每次移动两步),一个慢指针(每次移动一步),初始时都指向链表的头节点。
2、使用一个循环来遍历链表,每次循环中快指针移动两步,慢指针移动一步。
3、如果链表中存在环,则快指针和慢指针最终会相遇;如果链表中不存在环,则快指针会先到达链表尾部,此时可以判断链表无环。
4、当快指针和慢指针相遇时,说明链表中存在环。
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
所有 0 移动到数组的末尾
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
解法:两个指针一前一后(从左开始遍历到右,前就是右)。右指针识别为0数,右指针动左指针不动;右指针识别为非0数,该数放到左指针,然后两个指针一起动;最后根据左指针位置补齐0
class Solution {
public void moveZeroes(int[] nums) {
if(nums==null) {
return;
}
//两个指针i和j
int j = 0;
for(int i=0;i<nums.length;i++) {
//当前元素!=0,就把其交换到左边,等于0的交换到右边
if(nums[i]!=0) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j++] = tmp;
}
}
}
}
盛最多水的容器
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。
输入:[1,8,6,2,5,4,8,3,7] 输出:49 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。
双指针i,j分列水槽左右两端,选定两板高度中的较短板,向中间收窄一格,直至双指针相遇时跳出;更新面积最大值 res,最后返回面积最大值
class Solution {
public int maxArea(int[] height) {
int i = 0, j = height.length - 1, res = 0;
while(i < j) {
res = height[i] < height[j] ?
Math.max(res, (j - i) * height[i++]):
Math.max(res, (j - i) * height[j--]);
}
return res;
}
}
滑动窗口
最小覆盖字串
大字符串str1以及一个独立的小字符串str2,从这个大字符串str1里找到包含独立小字符串str2中所有字符的最小子字符串str3。例如,大字符串"meituan2019"和一个子字符串"i2t”,答案就应该是"ituan2”
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class Main {
public static String findMinimumSubstring(String str1, String str2) {
if (str2 == null || str2.isEmpty()) {
return str1;
}//处理特殊情况
Map<Character, Integer> targetMap = new HashMap<>();
for (char ch : str2.toCharArray()) {
targetMap.put(ch, targetMap.getOrDefault(ch, 0) + 1);
}
//构建目标映射,对于每个字符ch,如果该字符还没有在 targetMap 中出现过,返回默认值 0;
//如果该字符已经在 targetMap 中存在,则返回其出现的次数,然后将这个次数加1
int left = 0, right = 0;//窗口的左右边界
int minLen = Integer.MAX_VALUE;
int count = str2.length();//窗口中满足条件的字符个数
int startIndex = 0;//最小子字符串的起始位置
Map<Character, Integer> windowMap = new HashMap<>();
while (right < str1.length()) {
char ch = str1.charAt(right);
if (targetMap.containsKey(ch)) {//windowMap指窗口内字符的出现次数
windowMap.put(ch, windowMap.getOrDefault(ch, 0) + 1);
if (windowMap.get(ch).intValue() <= targetMap.get(ch).intValue()) {
count--;
}
}
while (count == 0) {
if (right - left + 1 < minLen) {
minLen = right - left + 1;
startIndex = left;
}
char leftChar = str1.charAt(left);
if (targetMap.containsKey(leftChar)) {
windowMap.put(leftChar, windowMap.get(leftChar) - 1);
if (windowMap.get(leftChar).intValue() < targetMap.get(leftChar).intValue()) {
count++;
}
}
left++;
}
right++;
}
//在每一步中,窗口右移一格,将字符加入窗口,并更新 windowMap 和 count。
//当 count 减为0时,表示当前窗口包含了 str2 中的所有字符,开始尝试缩小窗口。
//每次左移窗口时,更新最小子字符串的长度和起始位置。
return minLen == Integer.MAX_VALUE ? "" : str1.substring(startIndex, startIndex + minLen);
//如果找到了最小子字符串,则返回该子字符串;否则返回空字符串
}
public static void main(String[] args) {
// String str1 = "meituan2019";
// String str2 = "i2t";
Scanner in = new Scanner(System.in);
String str1 = in.nextLine();
String str2 = in.nextLine();
System.out.println(findMinimumSubstring(str1, str2));
}
}
无重复字符的最长子串
给定一个字符串 s,请你找出其中不含有重复字符的 最长子串 的长度
输入: s ="abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。
class Solution {
public int lengthOfLongestSubstring(String s) {
char[] arr = s.toCharArray();
int left = 0;
int right = 0;
int maxLen = 0;
HashSet<Character> set = new HashSet<>();
while (right < arr.length) {
char ch = arr[right];
if (!set.contains(ch)) {
set.add(ch);
right++;
maxLen = Math.max(maxLen, set.size());
} else {
set.remove(arr[left]);
left++;
}
}
return maxLen;
}
}
子串是指定串的异位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length(), pLen = p.length();
if (sLen < pLen) {
return new ArrayList<Integer>();
}//异常值排查,相当于返回null
List<Integer> ans = new ArrayList<Integer>();
int[] sCount = new int[26];//sCount 用于记录目标字符串中的字符出现次数
int[] pCount = new int[26];//pCount 用于记录字母异位词中的字符出现次数。
for (int i = 0; i < pLen; ++i) {
++sCount[s.charAt(i) - 'a'];
++pCount[p.charAt(i) - 'a'];
} //把字符串转成数组,s是S字符串的前plength位
if (Arrays.equals(sCount, pCount)) {
ans.add(0);
}//首值排查,当两个字符串一样
for (int i = 0; i < sLen - pLen; ++i) {
--sCount[s.charAt(i) - 'a'];
++sCount[s.charAt(i + pLen) - 'a'];
//对于每个窗口,减少窗口左边界字符的计数,并增加窗口右边界字符的计数,这样就将窗口滑动到下一个位置
//意思就是s是个和p同长的滑块,从左往右算每个plength长的滑块符不符合
if (Arrays.equals(sCount, pCount)) {//符合就添加当前位置
ans.add(i + 1);
}
}
return ans;
}
}
滑动窗口中的最大值
找出数字连续的最长序列
不要定性思维,比如下题
给定一个未排序的整数数组 nums
,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
输入:nums = [100,4,200,1,3,2] 输出:4
import java.util.HashSet;
class Solution {
public int longestConsecutive(int[] nums) {
if (nums == null || nums.length == 0) return 0;
HashSet<Integer> set = new HashSet<>();
for (int num : nums) {
set.add(num);
}//将数组中的所有元素存入集合中,以便快速判断一个数字是否在数组中
int longest = 0;
for (int num : nums) {
if (!set.contains(num - 1)) { // 如果当前数字的前一个数字不在集合中,说明当前数字是一个连续序列的起点
int currentNum = num;
int currentLength = 1;
while (set.contains(currentNum + 1)) { // 向后查找连续的数字
currentNum++;
currentLength++;
}
longest = Math.max(longest, currentLength); // 更新最长连续序列的长度
}
}
return longest;
}
}