程序 = 数据结构 + 算法
力扣解题思路
23. 合并K个升序链表
- 利用归并(分治法)的思路
- 分是将问题不断分成一些小问题然后递归求解,治是将分的各个阶段结果合并
- 归并算法时间复杂度为nlogn
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if(lists.length==0) return null;
return split(lists,0,lists.length-1);
}
public ListNode split(ListNode[] lists,int left,int right){
if(left==right){
return lists[left];
}
int mid = (left+right)/2;
ListNode l = split(lists,left,mid);
ListNode r = split(lists,mid+1,right);
return merge(l,r);
}
public ListNode merge(ListNode l1,ListNode l2){
ListNode dummy = new ListNode(-1);
ListNode p = dummy;
while(l1!=null&&l2!=null){
if(l1.val <= l2.val){
p.next = new ListNode(l1.val);
l1 = l1.next;
}else {
p.next = new ListNode(l2.val);
l2 = l2.next;
}
p = p.next;
}
p.next = l1 == null? l2:l1;
return dummy.next;
}
}
24.两两交换链表中的节点
- 简单模拟,重点在于定一个哑节点简化操作
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
ListNode cur = head;
while(cur!=null&&cur.next!=null){
pre.next = cur.next;
cur.next = pre.next.next;
pre.next.next = cur;
pre = cur;
cur = cur.next;
}
return dummy.next;
}
}
25.K个一组翻转链表
- 模拟
- 首先定义哑节点简化操作,然后遍历长度算出需要循环的组数
- 每一组内的翻转都需要三个指针(pre,cur,next)来操作,固定好pre指针,每次翻转都是将next指针移动到pre后面
- 一组内的翻转结束后,重置pre和cur
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode cur = dummy;
int total = 0;
while(cur.next!=null){
total++;
cur = cur.next;
}
int groupNum = total / k;
ListNode pre = dummy;
cur = head;
for(int i=0;i<groupNum;i++){
for(int j=0;j<k-1;j++){
ListNode next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
pre = cur;
cur = cur.next;
}
return dummy.next;
}
}
31.下一个排列
- 从后往前找,用尽可能小的「大数」去交换前面最近的「小数」。具体为从后往前找找到第一个升序对(x1,x2),此时x1<x2,x2后面的数全部为降序,即x2为顶点。此时x1就是要替换的「小数」。再反向从降序组中找到比「小数」大的数即为「大数」。
- 然后将「大数」后面的数全部置为升序。由于置换后降序组仍然会保持降序,所以只需要反转降序组就可以置为升序。
class Solution {
public void nextPermutation(int[] nums) {
int n = nums.length;
if(n==1) return ;
for(int i=n-1;i>0;i--){
//找到第一个升序对(i-1,i)
if(nums[i]>nums[i-1]){
//下标i-1即为「小数」
int x1 = nums[i-1];
//再反向从降序组中找到比「小数」大的数即为「大数」。
for(int j=n-1;j>=i;j--){
if(nums[j]>x1){
swap(nums,j,i-1);
reverse(nums,i,n-1);
return;
}
}
}
}
//如果没找到升序对,说明整体降序,直接翻转为最小值即可。
reverse(nums,0,n-1);
}
}
32.最长有效括号
-
动态规划
-
定义dp[i]为包含下标为i在内的最长有效括号子串的长度。
-
当s[i]为左括号时,dp[i]为0
-
当s[i]为右括号时:
- s[i-1]为右括号,则要判断i-s[i-1]-1是否为左括号,如果是则还要加上dp[i-s[i-1]-2],如果不是则为0
- s[i-1]为左括号,则加上dp[i-2]
-
要注意下标越界的情况
class Solution {
public int longestValidParentheses(String s) {
char cs[] = s.toCharArray();
int n = cs.length;
if(n<=1) return 0;
int max = 0;
int dp[] = new int[n];
for(int i=1;i<n;i++){
if(cs[i]==')'){
if(cs[i-1]=='('){
dp[i] = 2 + (i-2>=0?dp[i-2]:0);
}else{
int left = i-dp[i-1]-1;
if(left>=0&&cs[left]=='('){
dp[i] = 2 + dp[i-1] + (left-1>=0?dp[left-1]:0);
}
}
}
max = Math.max(max,dp[i]);
}
return max;
}
}
33.搜索旋转排序数组
- 二分查找,复杂度为logn
- 重点在于判断出左右哪一部分是有序的,然后根据target是不是在有序序列中来区别边界。
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 m =(l+r)/2;
if(target==nums[m]){
return m;
}
//左半部分有序
if(nums[0]<=nums[m]){
if(nums[0]<=target&&target<nums[m]){
r = m-1;
}else{
l = m+1;
}
}
//右半部分有序
else{
if(nums[m]<target&&target<=nums[n-1]){
l = m+1;
}else{
r = m-1;
}
}
}
return -1;
}
}
39.组合总数
- 无重复,无限制重复选取
- 利用dfs深度搜索
- 无限制重复选取只需要dfs方法传一个depth,每次遍历都将当前下标i传入进行递归。
public void dfs(int[] candidates,int target,Stack<Integer> path,int depth){
if(target==0){
res.add(new ArrayList<>(path));
return;
}
if(target<0) return ;
for(int i=depth;i<candidates.length;i++){
path.push(candidates[i]);
dfs(candidates,target-candidates[i],path,i);
path.pop();
}
}
41.缺失的第一个正数
- 循环遍历数组
- 每一个「大于0且小于等于数组长度的数」都要置换到它应在的地方
- 如果置换回来的数也是「大于0且小于等于数组长度的数」则要循环继续置换,并且置换回来的数不能等于原来的数,否则会陷入死循环
- 如果换回来的数不是「大于0且小于等于数组长度的数」则说明该下标对应的数还未遍历到
- 如果两个数置换后正好都处在应该在的位置,则下一次遍历nums[i]-1就是本身,就会停止
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for(int i=0;i<n;i++){
while(nums[i]>=1&&nums[i]<=n&&nums[nums[i]-1]!=nums[i]){
swap(nums,i,nums[i]-1);
}
}
for(int i=0;i<n;i++){
if(nums[i]!=i+1){
return i+1;
}
}
return n+1;
}
42.接雨水
- 遍历三次数组
- 第一次遍历:从左到右计算往左看能看到的最大柱子高度
- 第二次遍历:从右往左计算往右看能看到的最大柱子高度
- 第三次遍历:每一根柱子上所能累积的水,即当前下标左右所能看到的最大柱子高度中的较小值和当前柱子的高度差。
- 第二次遍历和第三次遍历可同时进行。
public int trap(int[] height) {
int n = height.length;
int[] left = new int[n];
int[] right = new int[n];
left[0] = height[0];
right[n-1] = height[n-1];
for(int i=1;i<n;i++){
left[i] = Math.max(left[i-1],height[i]);
}
int res = 0;
for(int i=n-2;i>=0;i--){
right[i] = Math.max(right[i+1],height[i]);
res += Math.min(left[i],right[i])-height[i];
}
return res;
}
48.旋转图像
- 左上角到右下角的翻转
- 上下对称翻转
public void rotate(int[][] matrix) {
int n = matrix.length;
int m = matrix[0].length;
//对角线翻转
for(int i=0;i<n-1;i++){
for(int j=0;j<m-1-i;j++){
int t = matrix[i][j];
matrix[i][j] = matrix[n-1-j][m-1-i];
matrix[n-1-j][m-1-i] = t;
}
}
//左右翻转
for(int i=0;i<n/2;i++){
for(int j=0;j<m;j++){
int t = matrix[i][j];
matrix[i][j] = matrix[n-1-i][j];
matrix[n-1-i][j] = t;
}
}
}
49.字母异位词分组
- 定义一个map,key为排序后的字符串,value为对应的字符串数组
public List<List<String>> groupAnagrams(String[] strs) {
Map<String,List<String>> map = new HashMap<>();
for(String str:strs){
char[] cs = str.toCharArray();
Arrays.sort(cs);
String key = String.valueOf(cs);
List<String> temp = map.getOrDefault(key,new ArrayList<>());
temp.add(str);
map.put(key,temp);
}
return new ArrayList<>(map.values());
}
51.N皇后
- dfs深度搜索,
- 用三个数组记录列,主对角线,斜对角线
- 用dfs的传参i来控制行数
class Solution {
List<List<String>> res = new ArrayList<>();
int n;
boolean[] col;
boolean[] main;
boolean[] dia;
public List<List<String>> solveNQueens(int n) {
this.n = n;
col = new boolean[n];
main = new boolean[n*2-1];
dia = new boolean[n*2-1];
dfs(0,new ArrayList<>());
return res;
}
public void dfs(int i,List<String> path){
if(path.size()==n){
res.add(new ArrayList<>(path));
return ;
}
for(int j=0;j<n;j++){
if(col[j]||main[j-i+n-1]||dia[i+j]) continue;
col[j] = true;
main[j-i+n-1] = true;
dia[i+j] = true;
StringBuilder sb = new StringBuilder();
for(int k=0;k<n;k++){
if(k==j){
sb.append('Q');
}else{
sb.append('.');
}
}
path.add(sb.toString());
dfs(i+1,path);
col[j] = false;
main[j-i+n-1] = false;
dia[i+j] = false;
path.remove(path.size()-1);
}
}
}
55.跳跃游戏
- 计算每一个位置能跳到的最远距离,不断迭代总的最远距离。
- 当当前位置下标大于总最远距离,即返回false
public boolean canJump(int[] nums) {
int res = 0;
for(int i=0;i<nums.length;i++){
if(res<i) return false;
res = Math.max(i+nums[i],res);
}
return true;
}
56.合并区间
- 首先按照「主x1,副x2」从小到大排序
- 定义结果数组和当前数组,遍历当前数组的对象「当前数」,如果结果数组为空,直接放入。如果不为空,取出结果数组中的最后一个数「结果数」,如果「结果数」的x2>=「当前数」的x1,则修改「结果数」的x2为「结果数」的x2和「当前数」的x2中的较大值,反之则将「当前数」加入到结果数组。
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals,(i,j)->{
if(i[0]==j[0]) return i[1] - j[1];
return i[0] - j[0];
});
int n = intervals.length;
int[][] res = new int[n][2];
res[0] = intervals[0];
int index = 0;
for(int i=1;i<n;i++){
if(intervals[i][0]<=res[index][1]){
res[index][1] = Math.max(intervals[i][1],res[index][1]);
}else{
res[++index] = intervals[i];
}
}
return Arrays.copyOf(res,index+1);
}
75.颜色分类
- 快排排序
- 定义三个指针分别表示012三个分区。
- 当num为2时,2号指针赋值并后移
- 当num为1时,2号指针先赋值后移,然后1号指针赋值后移,保证数字2不被覆盖
- 当num为1时,按照210的顺序进行赋值后移,保证1号2号的数字不被覆盖
public void sortColors(int[] nums) {
int num0 = 0,num1 = 0,num2 = 0;
for(int i=0;i<nums.length;i++){
if(nums[i]==2){
nums[num2++] = 2;
}else if(nums[i]==1){
nums[num2++] = 2;
nums[num1++] = 1;
}else{
nums[num2++] = 2;
nums[num1++] = 1;
nums[num0++] = 0;
}
}
}
最小覆盖子串
- 滑动窗口
- 用i,j定一个窗口的左右边界
- 用need[128]定义为当前每个字母还需要多少个,如果need[c]>0,表示我们需要这个数并且还需要多少个,need[c]<0表示那些不需要的数
- 用count记录当前总共需要的数,可以避免每次遍历判断是否已经凑齐需要的数
- 首先j滑动直到所有的数进站,然后i滑动去掉不需要的数,记录j-i+1,然后i继续右移一个位置,使得循环继续,直到j越界。
public String minWindow(String s, String t) {
if(s.length()<t.length()) return "";
char[] ss = s.toCharArray();
char[] ts = t.toCharArray();
int need[] = new int[128];
for(int i=0;i<ts.length;i++){
need[ts[i]-'0']++;
}
int i=0,j=0,size=Integer.MAX_VALUE,index=0,count=ts.length;
while(j<ss.length){
if(need[ss[j]-'0']>0){
count--;
}
need[ss[j]-'0']--;
if(count==0){
while(i<j&&need[ss[i]-'0']<0){
need[ss[i]-'0']++;
i++;
}
if(j-i+1<size){
size = j-i+1;
index = i;
}
need[ss[i]-'0']++;
i++;
count++;
}
j++;
}
return size==Integer.MAX_VALUE?"":s.substring(index,index+size);
}
子集
- dfs
- 重点在于子集没有顺序,「123」和「321」是相同的
public void dfs(int[] nums,List<Integer> path,boolean[] used,int depth){
if(path.size()>nums.length){
return;
}
res.add(new ArrayList<>(path));
for(int i=depth;i<nums.length;i++){
if(used[i]) continue;
used[i] = true;
path.add(nums[i]);
dfs(nums,path,used,i+1);
used[i] = false;
path.remove(path.size()-1);
}
}
79.单词搜索
- DFS
public boolean exist(char[][] board, String word) {
boolean[][] used = new boolean[board.length][board[0].length];
char[]cs = word.toCharArray();
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
if(dfs(i,j,board,cs,0,used)){
return true;
}
}
}
return false;
}
public boolean dfs(int x,int y,char[][]board,char[] cs,int depth,boolean[][] used){
if(x<0||x>=board.length||y<0||y>=board[0].length||used[x][y]) return false;
if(depth==cs.length-1&&board[x][y]==cs[depth]){
return true;
}
if(board[x][y]!=cs[depth]) return false;
used[x][y]=true;
boolean flag = dfs(x+1,y,board,cs,depth+1,used)||dfs(x-1,y,board,cs,depth+1,used)||dfs(x,y+1,board,cs,depth+1,used)||dfs(x,y-1,board,cs,depth+1,used);
used[x][y] = false;
return flag;
}
84.柱状图中最大的矩形
- 第一种暴力解法,找到当前柱子左右两边各第一个比他矮的柱子下标,分别为left,right,则当前柱子所能形成的最大矩形面积为height[i]*(right-left)
- 第二种,使用单调递增栈快速寻找边界。
- 栈内存放「下标」,由于是单调递增栈只会在「新来的元素」小于「当前栈顶高度」时出栈,所以出栈时「当前栈顶下标对应高度」左右两边第一个比它小的元素就确定了,分别是「出栈后的栈顶」和「新来的元素」,就可以在每次出栈的时候去计算面积了。
- 为了确保所有的元素都能出栈,在数组的最后面加个0
- 为了确保第一个元素前面有比他小的元素,在数组的最前面也加个0
public int largestRectangleArea(int[] heights) {
//在数组的前后都加个0
int n = heights.length;
int[] new_heights = new int[n+2];
for(int i=1;i<n+1;i++){
new_heights[i] = heights[i-1];
}
int max = 0;
//定义单调递增栈,存储下标
Deque<Integer> stack = new ArrayDeque<>();
for(int i=0;i<new_heights.length;i++){
while(!stack.isEmpty()&&new_heights[stack.peek()]>new_heights[i]){
int cur = stack.pop();
int l = stack.peek();
int r = i;
max = Math.max(max,(r-l-1)*new_heights[cur]);
}
stack.push(i);
}
return max;
}
92.反转链表2
- 定义pre,cur,next三个指针,pre保持不动,每次将next移到pre后面。
public ListNode reverseBetween(ListNode head, int left, int right) {
if(head==null||head.next==null) return head;
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode pre = dummy;
for(int i=0;i<left-1;i++){
pre = pre.next;
}
ListNode cur = pre.next;
for(int i=0;i<right-left;i++){
ListNode next = cur.next;
cur.next = next.next;
next.next = pre.next;
pre.next = next;
}
return dummy.next;
}
93.复原IP地址
94.二叉树的中序遍历
- 简单遍历
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode p = root;
while(!stack.isEmpty()||p!=null){
while(p!=null){
stack.push(p);
p = p.left;
}
if(!stack.isEmpty()){
p = stack.pop();
res.add(p.val);
p = p.right;
}
}
return res;
}
96.不同的二叉搜索树
- 不同二叉搜索树的个数只和结点个数有关
- 假设固定当前根结点为i,左分支个数和右分支结点个数确定,则可通过左分支个数乘以右分支个数计算当前固定结点i所拥有的不同二叉搜索树的个数,遍历i从0到n-1即可计算出全部。
- 利用动态规划记录已经计算好的数
public int numTrees(int n) {
if(n<=2) return n;
int dp[] = new int[n+1];
dp[0] = 1;
dp[1] = 1;
dp[2] = 2;
for(int i=3;i<=n;i++){
for(int j=0;j<=i-1;j++){
dp[i] += dp[j] * dp[i-j-1];
}
}
return dp[n];
}
98.验证二叉搜索树
- 根据中序遍历,二叉搜索树访问的数依次递增,确保当前访问到的值比上一次访问的值大就可以了
- 再定义一个pre值存储上一次访问的值。
long pre = Long.MIN_VALUE;
public boolean isValidBST(TreeNode root) {
if(root==null) return true;
if(!isValidBST(root.left)){
return false;
}
if(root.val<=pre) return false;
pre = root.val;
return isValidBST(root.right);
}
112.路径总和
- 注意到从根结点到叶子结点的路径即可
- 利用dfs
public boolean dfs(TreeNode root,int targetSum){
if(root==null) return false;
targetSum -= root.val;
if(targetSum==0&&root.left==null&&root.right==null){
return true;
}
return dfs(root.left,targetSum)||dfs(root.right,targetSum);
}
113.路径总和2
- 同上,多一个path记录路径即可.
- 重点在于找到一条满足的结果时不能立即return,此时return会带着叶子结点的值回到上一层。
public void dfs(TreeNode root,int targetSum,List<Integer> path){
if(root==null) return ;
targetSum-=root.val;
path.add(root.val);
if(targetSum==0&&root.left==null&&root.right==null){
res.add(new ArrayList<>(path));
}
dfs(root.left,targetSum,path);
dfs(root.right,targetSum,path);
path.remove(path.size()-1);
}
114.二叉树展开为链表
- 模拟递归即可
public void flatten(TreeNode root) {
if(root==null) return ;
TreeNode left = root.left;
TreeNode right = root.right;
if(left!=null){
TreeNode p = left;
while(p.right!=null){
p = p.right;
}
root.left = null;
root.right = left;
p.right = right;
}
flatten(root.right);
}
122.买卖股票的最佳时机2
- 完成多笔交易
- 重点在于赚取每一笔的利润
- 买卖每一笔上升交易
public int maxProfit(int[] prices) {
int res = 0;
for(int i=1;i<prices.length;i++){
if(prices[i]>prices[i-1]){
res += prices[i] - prices[i-1];
}
}
return res;
}
124.二叉树中的最大路径和
- 定义一个递归函数maxsum:以该节点为根结点的最大路径和。
- 则每个节点的最大路径和就等于maxsum(left)+maxsum(right)+1。
- 并且递归向上返回的路径只能包含左右两条分支中的一条,所以return Math.max(maxsum(left),maxsum(right))+1;
- 递归求的即可
int res = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return res;
}
public int dfs(TreeNode root){
if(root==null) return 0;
int left = dfs(root.left);
int right = dfs(root.right);
if(left+right+root.val>res){
res = left+right+root.val;
}
int up = Math.max(left,right)+root.val;
return up>0?up:0;
}
128.最长连续序列
- 利用set去重
- 然后遍历每一个数,如果这个数的前一个数在set中存在,则跳过。
- 只从每个序列的最小的数开始计算
public int longestConsecutive(int[] nums) {
Set<Integer> set = new HashSet<>();
for(int num:nums){
set.add(num);
}
int res = 0;
for(int num:nums){
if(set.contains(num-1)) continue;
int temp = 1;
while(set.contains(num+1)){
temp ++;
num ++;
}
if(temp>res) res = temp;
}
return res;
}
139.单词拆分
- 动态规划
- 定义:dp[i]表示前i个字母构成的单词是否在wordDict中。
- substring左闭右开。
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length(),maxlen = 0;
if(n==0) return false;
boolean dp[] = new boolean[n+1];
dp[0] = true;
Set<String> set = new HashSet<>();
for(String t:wordDict){
set.add(t);
maxlen = Math.max(maxlen,t.length());
}
for(int i=1;i<=n;i++){
for(int j=i-1;j>=0&&i-j<=maxlen;j--){
if(dp[j]&&set.contains(s.substring(j,i))){
dp[i] = true;
break;
}
}
}
return dp[n];
}
141.环形链表
- 快慢指针
public boolean hasCycle(ListNode head) {
if(head==null) return false;
ListNode slow = head;
ListNode fast = head;
while(true){
if(fast==null||fast.next==null) return false;
slow = slow.next;
fast = fast.next.next;
if(fast==slow) return true;
}
}
142.环形链表2
- 快慢指针
- 首先判断有没有环,没环则返回null
- 如果有环则此时slow==fast
- 要求得a+kb
- f=2s,f = s+kb
- 所以s=kb,只需再走a步
public ListNode detectCycle(ListNode head) {
if(head==null) return null;
ListNode slow = head;
ListNode fast = head;
while(true){
if(fast==null||fast.next==null) return null;
slow = slow.next;
fast = fast.next.next;
if(slow==fast) break;
}
fast = head;
while(fast!=slow){
fast = fast.next;
slow = slow.next;
}
return fast;
}
146.LRU缓存机制
- 最近最少使用
- 因为要求O(1)需要用到HashSet快速定位到节点
- 定义一个双端队列,当存取时都要将其放在队头,当队列满时将队尾除去。
class Node{
int key;
int value;
Node pre;
Node next;
public Node(){}
public Node(int key,int value){
this.key = key;
this.value = value;
}
}
class LRUCache {
Node head,tail;
int capacity;
Map<Integer,Node> map;
public LRUCache(int capacity) {
head = new Node();
tail = new Node();
head.next = tail;
tail.pre = head;
this.capacity = capacity;
map = new HashMap<>();
}
public int get(int key) {
Node node = map.get(key);
if(node!=null){
removeToHead(node);
return node.value;
}
return -1;
}
public void put(int key, int value) {
Node node = map.get(key);
if(node==null){
Node newNode = new Node(key,value);
addFirst(newNode);
map.put(key,newNode);
capacity--;
if(capacity<0){
Node removeNode = remove(tail.pre);
map.remove(removeNode.key);
capacity++;
}
}else{
node.value = value;
map.put(key,node);
removeToHead(node);
}
}
public void removeToHead(Node node){
remove(node);
addFirst(node);
}
public Node remove(Node node){
node.pre.next = node.next;
node.next.pre = node.pre;
return node;
}
public void addFirst(Node node){
node.next = head.next;
node.next.pre = node;
node.pre = head;
head.next = node;
}
}