LeetCode刷题笔记
LeetCode刷题笔记
记录一下自己在刷LEETCODE时踩过的一些坑和刷题心得,督促自己更好的学习,不定期更新
动态规划
152.乘积最大数组(2020.5.18)
public int maxProduct(int[] nums) {
int min_n=nums[0],max_n=nums[0],ans = nums[0];
for(int i=1;i<nums.length;i++){
int min = min_n,max = max_n;//保证两次计算时使用的是上次的的数据而不是更新后的数据
max_n=Math.max(Math.max(nums[i],nums[i]*max),nums[i]*min);
min_n=Math.min(Math.min(nums[i],nums[i]*max),nums[i]*min);
ans = Math.max(max_n,ans);
}
return ans;
}
一开始自己考虑的是动态规划,可感觉和最大子串和不太一样,采用了暴力的方法,时间复杂度O(n^2),这里的动态规划采取了分类的方法,记录前n-1项的最大和最小,如果是负的和最小×得到最大值,正数和最大的×得到最大值最大最小值要另外声明在循环中,如果直接用会导致第二个参数更新使用的是上一个更新过的值,造成结果不对
最初状态转移方程:
分类后的状态转移方程:
5.最长回文子串
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000
Ⅰ.动态规划
public String longestPalindrome(String s) {
int length = s.length();
boolean[][] P = new boolean[length][length];
int maxLen = 0;
String maxPal = "";
for (int len = 1; len <= length; len++) //遍历所有的长度
for (int start = 0; start < length; start++) {
int end = start + len - 1;
if (end >= length) //下标已经越界,结束本次循环
break;
P[start][end] = (len == 1 || len == 2 || P[start + 1][end - 1]) && s.charAt(start) == s.charAt(end); //长度为 1 和 2 的单独判断下
if (P[start][end] && len > maxLen) {
maxPal = s.substring(start, end + 1);
}
}
return maxPal;
状态转移方程:
对于字符串长度为1和2时单独进行讨论:
从长度较小的字符串开始遍历,时间复杂度为O(n^2) 空间复杂度为O(n^2)
Ⅱ.中心扩展算法
考虑上面的状态转移方程,可以看到
即所有的状态都可以由边界状态唯一得到,所以我们并不妨循环回文中心,再逐次向两边扩展,如果不相等则说明此回文中心已经达到最大
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
private int expandAroundCenter(String s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}
和被K整除的子数组
考虑到时间复杂度与数据输入不能采用暴力贪心求解
因为是子数组问题,所以考虑采用哈希表(HashMap)+前缀和解决问题,考虑到整除考虑余数定理,所以维护dq[]
保存前n项的余数和
注意:对于负数的项目将取余数之后变为正值再进行计算
状态转移方程:dq[i] = dq[i-1]+abs(num[i]%K)
核心代码same = record.getOrDefault(modulus, 0); ans+=same;
ORans += entry.getValue() * (entry.getValue() - 1) / 2;
public int subarraysDivByK(int[] A, int K) {
Map<Integer, Integer> record = new HashMap<>();
record.put(0, 1);
int sum = 0;
for (int elem: A) {
sum += elem;
// 注意 Java 取模的特殊性,当被除数为负数时取模结果为负数,需要纠正
int modulus = (sum % K + K) % K;
record.put(modulus, record.getOrDefault(modulus, 0) + 1);
}
int ans = 0;
for (Map.Entry<Integer, Integer> entry: record.entrySet()) {
ans += entry.getValue() * (entry.getValue() - 1) / 2;
}
return ans;
}
198.打家劫舍
动态转移方程:dq[i]=MAX(dq[i-2]+num[i],dq[i-1]);
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int[] dp = new int[length];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[length - 1];
}
字符串
14. 最长公共前缀
//方法一,考虑将最初的字符串作为初始化的前缀,然后定义一个i循环遍历strs中的
//各个字符串的位于j的元素是否相同,如果相同就继续遍历,知道找出最长的公共子串
//java
public String longestCommonPrefix(String[] strs) {
if(strs.length == 0)
return "";
String ans = strs[0];
for(int i =1;i<strs.length;i++) {
int j=0;
for(;j<ans.length() && j < strs[i].length();j++) {
if(ans.charAt(j) != strs[i].charAt(j))
break;
}
ans = ans.substring(0, j);
if(ans.equals(""))
return ans;
}
return ans;
}
//c++
/*
public:
string longestCommonPrefix(vector<string>& strs) {
if(strs.size()==0) return "";
string ans=strs[0];
for(int i = 0;i<strs.size();i++){
int j=0;
for(;j<ans.length()&&j<strs[i].length();j++){
if(ans[j]!=strs[i][j]) break;
if(ans == "") return ans;
}
ans = ans.substr(0,j);
if(ans == "") return ans;
}
return ans;
}
*/
680.回文字符串
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
int del = 0; //记录删除的字符次数
public boolean validPalindrome(String s) {
int i = 0,j = s.length()-1;
while(i < j){
if(s.charAt(i) == s.charAt(j)){
i++;
j--;
}else{
//不相等的话,若没有删除字符,则删除左边或右边的字符再判断;若删除过一次,则不是回文串
if(del == 0){
del++;
return validPalindrome(s.substring(i,j)) || validPalindrome(s.substring(i+1,j+1));
}
return false;
}
}
return true;
}
本题是一道简单题,记录的原因是我自己一开始写的比较麻烦,把所有的判断写在一起,比较复杂,如果采用递归的思想,代码会比较简介(substring(i,j)截取从i到j-1的字符串,即去掉左端字符后判断剩下的字符串是否为回文串)同时类似的还有下面这道题,练习的是双指针和各类字符串函数
125.回文字符串
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
public boolean isPalindrome(String s) {
if (s.length() == 0)
return true;
String low = s.toLowerCase();
int i = 0;
int j = low.length() - 1;
while (i < j){
if (!Character.isLetterOrDigit(low.charAt(i))){
i++;
continue;
}
if (!Character.isLetterOrDigit(low.charAt(j))){
j--;
continue;
}
if (low.charAt(i) != low.charAt(j))
return false;
else{
i++;
j--;
}
}
return true;
}
常用的字符串内容判断函数有
.isDigit() //判断是否是数字
.isUpperCase(); .isLowerCase(); .toUpperCase(); .toLowerCase() //对字母及其大小写的判断转化
.isLetterOrDigit() //判断是否是字母或数字
1371.每个元音包含偶数次的字符串长度(前缀和+状态压缩)
给你一个字符串 s ,请你返回满足以下条件的最长子字符串的长度:每个元音字母,即 'a','e','i','o','u' ,在子字符串中都恰好出现了偶数次。
1.暴力解法
遍历枚举所有子串并统计其中各字符出现的次数,更新最大长度,时间复杂度过高,故不予考虑
2.前缀和+状态压缩+hashMap
①通过前缀和避免对于子串的重复遍历,对于某个区间的子串采用前缀和的差值对其进行计算
考虑设计一个数组p[i][k]记录前i项子串中原因k出现的次数,然后采用一个for(int i,j)
的时间复杂度为O(n^2)的嵌套循环完成对于各个元音都为偶数最大值的更新
②我们考虑枚举字符串的每个位置i ,计算以它结尾的满足条件的最长字符串长度。其实我们要做的就是快速找到最小的j∈(0,i),使得p[i][]-p[j][]的各项均为偶数,这里可以考虑使用hashMap进行优化,使用hashmap对我们需要查找的状态进行保存,五位符号的奇偶性共有2^5=32种状态,使用hashmap<各个元音字符的状态,最早出现位置>记录每种状态出现的最早位置即可,当p[i][k]与p[j][k]的奇偶性都相等的话就更新最大值
③对于各个元音的奇偶性,采用一个statue记录,如对于aiu,a:1 e:0 i:1 o:0 u:1 则status的编码为10101B,即21D
public int findTheLongestSubstring(String s) {
int n = s.length();
Map<Integer,Interger> pos=new HashMap<>() ;
int ans = 0,statue=0;
pos[0] = 0;
for (int i = 0; i < n; i++) {
char ch = s.charAt(i);
if (ch == 'a') {
status ^= (1 << 0);
} else if (ch == 'e') {
status ^= (1 << 1);
} else if (ch == 'i') {
status ^= (1 << 2);
} else if (ch == 'o') {
status ^= (1 << 3);
} else if (ch == 'u') {
status ^= (1 << 4);
}
if (pos.isContains(status)) {
ans = Math.max(ans, i + 1 - pos.get(status));
} else {
pos.put(status,i+1);
}
}
return ans;
}
数组
4.寻找有序数组的中位数(二分查找)
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
if (nums1.length > nums2.length) {
int[] temp = nums1;
nums1 = nums2;
nums2 = temp;
}
int m = nums1.length;
int n = nums2.length;
// 分割线左边的所有元素需要满足的个数 m + (n - m + 1) / 2;
int totalLeft = (m + n + 1) / 2;
// 在 nums1 的区间 [0, m] 里查找恰当的分割线,
// 使得 nums1[i - 1] <= nums2[j] && nums2[j - 1] <= nums1[i]
int left = 0;
int right = m;
while (left < right) {
int i = left + (right - left + 1) / 2;
int j = totalLeft - i;
if (nums1[i - 1] > nums2[j]) {
// 下一轮搜索的区间 [left, i - 1]
right = i - 1;
} else {
// 下一轮搜索的区间 [i, right]
left = i;
}
}
int i = left;
int j = totalLeft - i;
int nums1LeftMax = i == 0 ? Integer.MIN_VALUE : nums1[i - 1];
int nums1RightMin = i == m ? Integer.MAX_VALUE : nums1[i];
int nums2LeftMax = j == 0 ? Integer.MIN_VALUE : nums2[j - 1];
int nums2RightMin = j == n ? Integer.MAX_VALUE : nums2[j];
if (((m + n) % 2) == 1) {
return Math.max(nums1LeftMax, nums2LeftMax);
} else {
return (double) ((Math.max(nums1LeftMax, nums2LeftMax) + Math.min(nums1RightMin, nums2RightMin))) / 2;
}
}
链表
146.LRU缓存机制
解决思路:构造一个hashMap<KEY,DLinkedNode>保存与方便查找对应得到节点,同时维护一个DLinkedNode维护访问顺序,将已经访问过的节点提到链表头部,当超过容量是将尾部的链表节点从cache中删除
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;
}
}
/**
* Your LRUCache object will be instantiated and called as such:
* LRUCache obj = new LRUCache(capacity);
* int param_1 = obj.get(key);
* obj.put(key,value);
*/
树
105.先序遍历中序遍历构造树&&106中序遍历后序遍历构造树
//先序中序遍历
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
private Map<Integer,Integer> map;
public TreeNode bulid(int[] pre, int[] in,int pre_l,int pre_r,int in_l,int in_r){
if(pre_l>pre_r) return null;
int pre_root = pre_l;
int in_root = map.get(pre[pre_root]);
int left_size = in_root-in_l;
int right_size = in_r-in_root;
TreeNode root = new TreeNode(pre[pre_root]);
root.left = bulid(pre,in,pre_l+1,pre_l+left_size,in_l,in_l+left_size-1);
root.right = bulid(pre,in,pre_r-right_size+1,pre_r,in_r-right_size+1,in_r);
return root;
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
int len = preorder.length;
map = new HashMap<Integer,Integer>();
for(int i=0;i<len;i++){
map.put(inorder[i],i);
}
return bulid(preorder,inorder,0,len-1,0,len-1);
}
//中序后序遍历
private Map<Integer,Integer> map;
public TreeNode build(int[] in,int[] post,int in_l,int in_r,int post_l,int post_r){
if(post_r < post_l) return null;
int post_root = post_r;
int in_root = map.get(post[post_root]);
int left_size = in_root-in_l;
int right_size = in_r-in_root;
TreeNode root = new TreeNode(in[in_root]);
root.left = build(in,post,in_l,in_root-1,post_l,post_l+left_size-1);
root.right = build(in,post,in_root+1,in_r,post_l+left_size,post_root-1);
return root;
}
public TreeNode buildTree(int[] inorder, int[] postorder) {
int len = inorder.length;
map = new HashMap<Integer,Integer>();
for(int i=0;i<len;i++){
map.put(inorder[i],i);
}
return build(inorder,postorder,0,len-1,0,len-1);
}
总结
101.对称二叉树
核心思想:维护两个指针p,q,check函数()执行两次,迭代为 check(p->left,q->right) check(p->right,q->left)
//递归的方法
public:
bool check(TreeNode *p, TreeNode *q) {
if (!p && !q) return true;
if (!p || !q) return false;
return p->val == q->val && check(p->left, q->right) && check(p->right, q->left);
}
bool isSymmetric(TreeNode* root) {
return check(root, root);
}
};
//迭代的方法
public:
bool check(TreeNode *u, TreeNode *v) {
queue <TreeNode*> q;
q.push(u); q.push(v);
while (!q.empty()) {
u = q.front(); q.pop();
v = q.front(); q.pop();
if (!u && !v) continue;
if ((!u || !v) || (u->val != v->val)) return false;
q.push(u->left);
q.push(v->right);
q.push(u->right);
q.push(v->left);
}
return true;
}
bool isSymmetric(TreeNode* root) {
return check(root, root);
}
图
栈&&堆
字符串解码
考虑存在括号嵌套的问题,采用两个栈堆分别维护数字与字符串,考虑一下情况
1.当c
是数字时,将multi*10+(int)c
2.当c
是字母时,将其加到暂存的字符串中进行记录
3.当c
是'['时,将当前保存的multi与暂存字符串全部压入相应的栈,同时初始化这两个量
4.当c
是']'时,将两个栈的内容弹出并进行字符串的拼接
public String decodeString(String s) {
StringBuffer ans=new StringBuffer();
Stack<Integer> multiStack=new Stack<>();
Stack<StringBuffer> ansStack=new Stack<>();
int multi=0;
for(char c:s.toCharArray()){
if(Character.isDigit(c))multi=multi*10+c-'0';
else if(c=='['){
ansStack.add(ans);
multiStack.add(multi);
ans=new StringBuffer();
multi=0;
}else if(Character.isAlphabetic(c)){
ans.append(c);
}else if(c==']'){
StringBuffer ansTmp=ansStack.pop();
int tmp=multiStack.pop();
for(int i=0;i<tmp;i++)ansTmp.append(ans);
ans=ansTmp;
}
}
return ans.toString();
}