1、特征题
1、回文系列
方法论:中心拓展法
因为回文串是中心对称的,我们可以先枚举子串的中心,然后从中心处向两边探测,直到发现两端字符不相等或者到达字符串边缘。
- s长度为 奇数,中心是单个字符,以 s[i]为中心向两边扩展
- s长度为 偶数,中心是两个字符,以 s[i]、s[i+1]为中心向两边扩展
案例:
2、重复或不重复
方法论:用数组、map、bitmap记录,根据实际场景记录次数或者最后位置
案例:
3、数组合并
方法论:双指针
案例:
4、环
4.1 判断
方法论:快慢指针,是否相遇(快指针两步,慢指针一步)
案例:
4.2 增删
5、旋转数组
方法论:二分法
此题运用二分法的特殊变种情况来区分:
if nums[mid] == target
if nums[left] <= nums[mid],mid的左侧是单调递增区间。注意,当nums中元素较少时,可能left和mid的下标值相同,因此这里的 nums[left] 也可能等于 nums[mid]。
if nums[left] <= target && target < nums[mid]
right = mid - 1
else
left = mid + 1
else /*if nums[left] > nums[mid]*/,mid的左侧不是单调递增区间,说明右侧是单调递增区间
if nums[mid] < target && target <= nums[right]
left = mid + 1
else
right = mid - 1
案例:
6、第k大、前k大
方法论:小顶堆
堆,又称优先级队列,在逻辑上可以视为一棵完全二叉树,且满足每个节点的值小于等于(小根堆)其左右孩子节点的值。
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> queue = new PriorityQueue<>();//系统默认即为小根堆
int i = 0;
for(;i < k;i++)
queue.add(nums[i]);//前k个元素建堆,也可以用offer
for(;i < nums.length;i++)
if(queue.peek()<nums[i]){//peek拿出堆顶元素比大小
queue.poll();
queue.add(nums[i]);//java中没有replace操作,拆分成两步
}
return queue.peek();
}
public int findKthLargest(int[] nums, int k) {
// nums[0...k-1]建小根堆,从最后一个节点的父节点开始往前heapify,其余基本与上个版本一致
for(int j = (k-2)/2;j >= 0;j--)
heapify(nums,j,k);
for(int j = k;j < nums.length;++j){
if(nums[0] >= nums[j])
continue;
swap(nums,0,j);
heapify(nums,0,k);
}
return nums[0];
}
// 调整大小为size的堆arr中的i号元素
public void heapify(int[] arr,int i,int size){
int left = i*2+1;
while(left < size){
int small = left+1 < size && arr[left] > arr[left+1]? left+1: left;
if(arr[i]<=arr[small])
break;
swap(arr,i,small);
i = small;
left = i*2+1;
}
}
public void swap(int[] arr,int a,int b){
arr[a] ^= arr[b];
arr[b] ^= arr[a];
arr[a] ^= arr[b];
}
案例:
7、成对匹配(括号等)
方法论:栈、数组;匹配到了出栈
案例:
8、两两匹配
方法论:kmp算法
9、子串问题
方法论:双指针,再结合具体问题动态规划等
案例:
备注:子序列是非连续的,子串是连续的
2、通用题
1、找出前后更大/更小
方法论:单调栈,记录前面最大的
案例:
2、矩阵遍历
void dfs(int[][] grid, int r, int c) {
// 判断 base case
if (!inArea(grid, r, c)) {
return;
}
// 如果这个格子不是岛屿,直接返回
if (grid[r][c] != 1) {
return;
}
grid[r][c] = 2; // 将格子标记为「已遍历过」
// 访问上、下、左、右四个相邻结点
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
// 判断坐标 (r, c) 是否在网格中
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
案例:
3、链表变更
方法论:找到交换节点的前节点、指针
//1、备份
//2、先解除
//3、后插入
ListNode tmp = pre.next;
pre.next = post.next;
post.next = post.next.next;
pre.next.next = tmp;
案例:
4、全排列和子集
方法论:回溯
class Solution {
private int[] nums;
private List<Integer> path;
private boolean[] onPath;
private final List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
this.nums = nums;
path = Arrays.asList(new Integer[nums.length]);
onPath = new boolean[nums.length];
dfs(0);
return ans;
}
private void dfs(int i) {
if (i == nums.length) {
ans.add(new ArrayList<>(path));
return;
}
for (int j = 0; j < nums.length; ++j) {
if (!onPath[j]) {
path.set(i, nums[j]);
onPath[j] = true;
dfs(i + 1);
onPath[j] = false; // 恢复现场
}
}
}
}
/**
* 循环枚举
*/
public static List<List<Integer>> enumerate(int[] nums) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
res.add(new ArrayList<Integer>());
for (Integer n : nums) {
int size = res.size();
for (int i = 0; i < size; i++) {
List<Integer> newSub = new ArrayList<Integer>(res.get(i));
newSub.add(n);
res.add(newSub);
}
}
return res;
}
案例:
5、树
方法论:
- 先中后:递归
- 层序遍历:队列
- 深度搜索:
案例:
6、动态规划
常见的判断:依赖去前一步的结果进行下一步
方法论:存储并重复利用己经计算的结果
6.1 一维
案例:
6.2 二维
结合二维数组使用,常用图、或者多个条件的状态
案例:
7、贪心
方法论:以最小成本倒推或者前进
案例:
附录
java常用结构
善用二维数组
树
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) {
val = x;
}
}
队列
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
TreeNode node = queue.poll();
栈
Stack<Character> stack = new Stack<>();
stack.pop();
stack.push(c);
链表
public class ListNode {
int val;
ListNode next;
//双链表多一个pre节点
}
用数组实现栈、队列
用栈实现队列