这里仅仅是自己刷题的记录,没啥规律,也没啥系统的讲解,就是一些题目的注意点和当时难以理解的地方。
二分
如果一个排序数组中的元素互不相同,那么按照二分查找会返回目标元素的下标。
如果有重复的元素,并且我们想得到第一个重复元素的下标,那么可以用如下代码:
private int binarySearch(int[] nums, int target) {
int l = 0, r = nums.length-1;
while (l <= r) {
int m = l + ((r-l)>>1);
if (nums[m] >= target) {
r = m - 1;
}else {
l = m + 1;
}
}
return l;
}
我们以1 2 2 5 7举例,如果target是2,那么上述程序最终会返回第一个2的下标1。
leetcode第34题便可用上述代码求解:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
public int[] searchRange(int[] nums, int target) {
if (nums.length == 0) return new int[]{-1, -1};
int l1 = binarySearch(nums, target);
int l2 = binarySearch(nums, target+1);
if (l1 == nums.length || nums[l1] != target) return new int[]{-1, -1};
else return new int[] {l1, l2-1};
}
private int binarySearch(int[] nums, int target) {
int l = 0, r = nums.length-1;
while (l <= r) {
int m = l + ((r-l)>>1);
if (nums[m] >= target) {
r = m - 1;
}else {
l = m + 1;
}
}
return l;
}
先得到target的下标l1,然后再得到target+1的下标l2,最后的结果即为{l1, l2-1}
此外要考虑一些边界情况:
- 如果target大于数组中任意一个元素,那么l最终会超出数组下标范围,此时返回-1
- 如果target在数组的范围内,但数组中并没有target,例子{5,7,7,8,8,10},最终l为1,r为0,这是无意义的。所以要用nums[l1] != target来判断一下。
回溯 排列组合
leetcode第40题,https://leetcode-cn.com/problems/combination-sum-ii/
这题我是看这篇帖子的题解:https://leetcode-cn.com/problems/combination-sum-ii/solution/man-tan-wo-li-jie-de-hui-su-chang-wen-shou-hua-tu-/
广搜
No 637 二叉树的层平均值:https://leetcode-cn.com/problems/average-of-levels-in-binary-tree/
代码如下:
public List<Double> averageOfLevels(TreeNode root) {
List<Double> res= new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while (!q.isEmpty()) {
int size = q.size();
double sum = 0;
for (int i = 0; i < size; i++) {
TreeNode tmp = q.poll();
sum += tmp.val;
if (tmp.left != null) q.add(tmp.left);
if (tmp.right != null) q.add(tmp.right);
}
res.add(sum / size);
}
return res;
}
我刚开始没用for循环,用的while:
public List<Double> averageOfLevels(TreeNode root) {
List<Double> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while (!q.isEmpty()) {
int size = q.size();
double sum = 0;
while (size > 0) {
TreeNode tmp = q.poll();
sum += tmp.val;
if (tmp.left != null) q.add(tmp.left);
if (tmp.right != null) q.add(tmp.right);
size--;
}
res.add(sum / size);
}
return res;
}
但报错了,原因是每次循环都改变了size的值,最后size=0,而下方还有一个除法,因此会出错。所以用for循环可能会更好,如果坚持用while的话则需要用一个新的变量来保存size。
No 513 找树左下角的值:https://leetcode-cn.com/problems/find-bottom-left-tree-value/
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode> queue = new LinkedList<>();
queue.add(root);
while (!queue.isEmpty()) {
root = queue.poll();
if (root.right != null) queue.add(root.right);
if (root.left != null) queue.add(root.left);
}
return root.val;
}
这里注意先将右子树加到队列中,然后才是左子树。
BST
No 669 修剪二叉搜索树: https://leetcode-cn.com/problems/trim-a-binary-search-tree/
这题刚看没太懂,题解下方有个评论写的挺好的:
在二叉树中找根节点到某一结点的路径
bool GetNodePath(TreeNode* pHead, TreeNode* pNode, std::list<TreeNode*>& path)
{
if(pHead == pNode)
return true;
path.push_back(pHead);
bool found = false;
if(pHead->m_pLeft != NULL)
found = GetNodePath(pHead->m_pLeft, pNode, path);
if(!found && pHead->m_pRight)
found = GetNodePath(pHead->m_pRight, pNode, path);
if(!found)
path.pop_back();
return found;
}
9.回文数
https://leetcode-cn.com/problems/palindrome-number/
public boolean isPalindrome(int x) {
if (x == 0) return true;
if (x < 0 || x % 10 == 0) return false;
int right = 0;
while (x > right) {
right = right * 10 + x % 10;
x = x / 10;
}
return right / 10 == x || right == x;
}
不将数字转换为字符串,也不能使用额外空间。这题的做法是,将x分为左右两部分,怎么分呢?
用一个变量right,每次用right * 10 + x % 10的方式将其逆序增大,如果x的位数是奇数,那么最后跳出while循环后right应该比x大,且只大一位,如果x是回文数,那么right / 10就应该等于x,例如x=123,最后right=12,x=1,所以right/10=1,即121是回文数;如果x的位数是偶数,如x=1221,那么最后right应该等于12,x也等于12,right=x,即1221是回文数。
位运算
https://blog.csdn.net/navyifanr/article/details/19496459
巧用n&(n-1),可以将最低位不为1的那位变为0。为啥呢?下面来证明一下:
- 进行n-1运算分两种情况:
- 最低位为1,设n为011011,此时最低位-1后变为0,即 (n-1) = 011010,那么n&(n-1)就是 (011011) & (011010) = (011010),我们发现n和n-1只有最后一位不同,前面所有的位都相同;
- 最低为不为1.设n为011010,此时n-1要向倒数第二位借1,即 (n-1) = 011001,那么n&(n-1)就是 (011010) & (011001) = (011000),我们发现n和n-1倒数第二位之前的所有位数都相同,只有最低位不为1(倒数第二位)的那位会变为0。
利用这个结论可以判断一个数是不是2的整数次幂:
2 10
4 100
8 1000
16 10000
我们发现2的整数次幂只有最高位为1,因此用n&(n-1)就可以直接判断,若为0说明只有一个1,那就为2的整数次幂。
前面判断了是否为2的幂,下面这道题用来判断是否为4的幂:
4的幂也一定是2的幂,但却是2的偶数次幂,如4、16、64是2的2、4、6次幂,在这种情况下,1出现在奇数位置;而2的奇数次幂,如8、32却不是4的幂,在这种情况下,1出现在偶数位置。此外只要一个数是2的整数次幂,那在所有位中就只会有一位是1。
因此如果能判断一个数是否是2的偶数次幂就行了,首先可以判断其是否为2的整数次幂,若不是直接返回false,若是则再看其是否为2的偶数次幂。
然后,由于输入的数只有32位,那么我们可以手动写出1出现奇数位的情况: (010101010101…01) 一共32位,然后让n&这一串,如果结果是1,说明n确实为4的幂。否则返回false。
啥是补码啊?对正数来说,补码就是将每一位都取反。
假设我们有1011001,我们发现他与1111111异或的结果就是0100110:
这不正好是每一位都取反了吗?所以思路就是,先统计这个数n有多少位,然后让1左移这么多位,再减1,得到的数记为x且全都是1,位数和n相同,最后让n和x进行异或运算即可求出答案。
先上代码:
这题咋做呢?我们先看看字符串数组中有多少个元素,然后创建一个和元素数量相同的数组,记为wordNum。
注意第一个双重循环,对于字符串数组中的每一个元素,如(abcd, xafr),首先取abcd,我们让这个元素中的每一个字符减去’a’,得到的结果就是此字符与’a’的差值,然后让1左移这个差值大小的位数,将每次左移后的结果进行或运算。例如abcd的第一个字符是’a’,与’a’相减是0,那么1向左移0位仍然是1,然后’b’-‘a’=1,那么1左移1位变成10,再与刚刚1左移1位的结果进行或运算,即1 | 10 = 11;然后’c’ - ‘a’ = 2,那么1左移两位变成100,再与11进行或运算,即11 | 100 = 111,然后’d’ - ‘a’ = 3,那么1左移3位变为1000,再与111或运算,即111 | 1000 = 1111,至此abcd字符串遍历结束,wordNum[0] = 1111。
接下来该遍历xafr了,仍然是重复上述的操作,但由于其中含有和abcd相同的字符a,那么wordNum[1]的最低为(‘a’ - ‘a’ = 0)必然是1,所以wordNum[0] & wordNum[1]比然不为0(因为最低为都是1)。
所以通过上述方式就能判断出两个字符串中有无重复字符,那剩下的就是在没有重复字符的字符串中找到两个长度乘积最大的字符串了。
最长递增子序列
这题可以用贪心和动态规划两种方法做,题解就不写了,leetcode里面很详细,但无论哪种方法,都需要先对数组进行排序。
这里只记录一下为啥用贪心的时候只能对数对的后一个数进行排序。
将数对记为pair,对于贪心来说,只能对pair[1]进行排序,为啥呢?下面举个例子:
如果我们对pair[0]按照升序排列,那么排完之后应该是:
可以看到,排序完[-9, 8]位于第一位,因为-9是所有数对[0]位置的数中最小的。我们再来看一下贪心的代码:
cur初始值为最小,第一次循环,pair为[-9, 8],而cur小于-9,所以将cur赋值为8,但之后的遍历中,已经找不到哪个数的[0]位置比8大了,到最后答案为1,显然此答案是错误的。
这种排序方式错误的原因在于,只按照pair[0]排序,却忽略了pair[1]的大小,如果pair[1]很大,那么就会错过很多可能的情况,如上例所示,[-9, 8]这个区间太大了,因此中间的很多小区间都错过了。
但是使用动态规划的方式两种排序方法都可以。
NO. 494
递归的写法,简单易懂:
private int count;
public int findTargetSumWays(int[] nums, int target) {
core(nums, 0, 0, target);
return count;
}
private void core(int[] nums, int index, int sum, int target) {
if (index == nums.length) {
if (sum == target) {
count++;
return;
}
}else {
core(nums, index+1, sum+nums[index], target);
core(nums, index+1, sum-nums[index], target);
}
}
动态规划1:
这也是官方题解的版本
主要就是这么个式子:
如何理解呢?
和背包问题不同,背包问题是遇见一个物品要么装要么不装,而本题是遇见一个数字,要么加上它要么减去它。
设dp[i][j]表示当前到了第i个数字,且这些数字互相加减能得到j的方法的数量。
当走到dp[i][j]时,dp[i][j-1]、dp[i-1][j]…都已经产生,现在面临两个选择:
- 加上num[i],那么dp[i][j] += dp[i-1][j-nums[i]],为啥呢?可以这么想:现在选择加上nums[i]才能使0 – i这些数进行加加减减后之和是j,那么dp[i-1][j-nums[i]]的方法数+1一定是dp[i][j]的方法数。
- 减去num[i],那么 dp[i][j] += dp[i-1][j+nums[i]],同理,从0 – i这些数中,减去i才能使加减之和等于j,那么dp[i-1][j+nums[i]]的方法数+1一定是dp[i][j]的方法数。
代码:https://leetcode-cn.com/problems/target-sum/solution/mu-biao-he-by-leetcode/
动态规划2
这是更吊的版本,原作者:https://leetcode-cn.com/problems/target-sum/solution/494-mu-biao-he-dong-tai-gui-hua-zhi-01be-78ll/
代码:
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num: nums) sum += num;
if (target > sum || (((sum + target) % 2) & 1) != 0) return 0;
int a = (sum + target) / 2;
int[] dp = new int[a+1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = a; j >= nums[i]; j--) {
dp[j] = dp[j] + dp[j-nums[i]];
}
}
return dp[a];
}
No. 322 零钱兑换
https://leetcode-cn.com/problems/coin-change/
写了一种易于理解但是二维dp数组的方法:
public int coinChange(int[] coins, int amount) {
int[][] dp = new int[coins.length+1][amount+1];
for (int i = 0; i < coins.length; i++) {
for (int j = 1; j < amount+1; j++) {
dp[i][j] = 10001;
}
}
dp[0][0] = 0;
for (int i = 1; i <= coins.length; i++) {
for (int j = 1; j < amount+1; j++) {
if (j >= coins[i-1]) {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-coins[i-1]]+1);
}else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[coins.length][amount] != 10001 ? dp[coins.length][amount]: -1;
}
其中dp[i][j]表示目前可选的硬币种类为i,待找零钱是j时,所需最少的硬币数量。
由于dp时只用到了本行以及上一行的信息,因此可以用一维数组做到,下面是一维数组的写法:
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, 10001);
dp[0] = 0;
for(int coin : coins){
for(int j = coin; j < amount + 1; j++){
dp[j] = Math.min(dp[j], dp[j - coin] + 1);
}
}
return dp[amount] != 10001 ? dp[amount] : -1;
}
作者:edelweisskoko
链接:https://leetcode-cn.com/problems/coin-change/solution/322-ling-qian-dui-huan-dong-tai-gui-hua-e2nt7/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
No. 518 零钱兑换2
https://leetcode-cn.com/problems/coin-change-2/
和上一题不同的是,此题求的是各种面值的硬币若干,互相组合后,值之和等于目标值的组合的数量。
dp[i][j]表示目前可选的硬币种类为i,待找零钱是j时,所有组合的数量。
所以递推式子为
- dp[i][j] = dp[i-1][j] + dp[i][j-coins[i]], if j >= coins[i];
- dp[i][j] = dp[i-1][j], if j < coins[i]
这个二维的dp同样可以简化为一维的。
代码:
public int change(int amount, int[] coins) {
int[] dp = new int[amount+1];
dp[0] = 1;
for (int i = 0; i < coins.length; i++) {
for (int j = coins[i]; j <= amount; j++) {
dp[j] = dp[j] + dp[j-coins[i]];
}
}
return dp[amount];
}
NO. 139 单词拆分
这里主要说一下dp的解法中为什么要把遍历字典中的单词放在内层循环。
先上代码:
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (int i = 1; i <= n; i++) {
for (String word : wordDict) { // 对物品的迭代应该放在最里层
int len = word.length();
if (len <= i && word.equals(s.substring(i - len, i))) {
dp[i] = dp[i] || dp[i - len];
}
}
}
return dp[n];
}
在之前的类背包问题中,都是将物品/硬币放到外层循环(其实放到内层也行),为啥这道题就必须放到内层循环呢?
我们先来看看放到外层循环会有啥问题,设字符串s为: applepenapple,字典为: [apple, pen],那么将字典(相当于硬币)放到外层的代码应该是:
// 错误的版本
public boolean wordBreak2(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp = new boolean[n + 1];
dp[0] = true;
for (String word: wordDict) {
System.out.println(word + " start: " + Arrays.toString(dp));
for (int i = 1; i <= n; i++) { // 对物品的迭代应该放在最里层
int len = word.length();
if (len <= i && word.equals(s.substring(i - len, i))) {
dp[i] = dp[i] || dp[i - len];
}
}
System.out.println(word + " end: " + Arrays.toString(dp));
}
return dp[n];
}
我们观察输出结果:
发现结果是false,显然是错误的。
错误的原因在于,我们将字典放到了外层循环,所以每个字符串只会遍历一次,以apple举例,他只会被遍历一次,因此只会在下图位置将dp元素设为true:
现在到了pen,也只会被遍历一次:
也就是说,把字典放到外层循环只能检查到applepen,对于applepenapple后面多出的apple是无能为力的,这就是错误的原因。
那么把字典放到内层循环为什么是正确的?
此时外层循环是字符串s的长度,因此对于每一个长度,字典中的元素都会被遍历到,我们还是看上面的例子:
由于i从1开始,而字典中长度最小的元素是pen,长度为3,所以当i=3时内层循环才会开始执行,当i=3时,apple的长度大于3暂时不必考虑,pen的长度等于3但根据if的第二个条件,pen不符合,因此if语句进不去;i=4同理;到i=5时,pen的长度虽然小于5,但不满足if的第二个条件,而apple长度等于5且s的前五位(apple)正好是apple,因此dp[5] = true;i=6、7时也进不去if;到i=8时,pen的长度小于8,且if的第二个条件是pen刚好等于pen,因此dp[8] = true(dp[8] | dp[8-3]的意思是,可以不选pen,看看字典中pen之前的的元素能不能组成s从0到i为止的字符串;可以选pen,那就要看看s从0到i-pen的长度 的子串中能否被拼成,这两种情况有一种是true就行);到了i=13,if的第二个条件,s的子串为apple,所以dp[13] = dp[13] | dp[13-5] = true。
No. 377组合总和Ⅳ
递归+剪枝的代码:
private HashMap<Integer, Integer> map = new HashMap<>();
public int combinationSum4(int[] nums, int target) {
return core(nums, target);
}
private int core(int[] nums, int target) {
if (target == 0) return 1;
if (map.containsKey(target)) return map.get(target);
int res = 0;
for (int i = 0; i < nums.length; i++) {
if (target >= nums[i]) {
res += core(nums, target-nums[i]);
}
}
map.put(target, res);
return res;
}
动态规划版本:
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for (int i = 1; i <= target; i++) {
for (int num : nums) {
if (num <= i) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/combination-sum-iv/solution/zu-he-zong-he-iv-by-leetcode-solution-q8zv/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
注意:这题和前面提到的No. 518 零钱兑换2代码非常相似,此题是将硬币遍历放到内层循环,而518题是将硬币遍历放到外层循环。
No.72 编辑距离
https://leetcode-cn.com/problems/edit-distance/
代码:
public int minDistance(String word1, String word2) {
intn1 = word1.length(), n2 = word2.length();
int[][] dp = new int[n1+1][n2+1];
for (int i = 1; i <= n1; i++) dp[i][0] += dp[i-1][0]+1;
for (int j = 1; j <= n2; j++) dp[0][j] += dp[0][j-1]+1;
for (int i = 1; i <= n1; i++)
for (int j = 1; j <= n2; 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][j-1], Math.min(dp[i-1][j-1], dp[i-1][j])) + 1;
return dp[n1][n2];
}
dp数组的含义:dp[i][j]表示,word1 到 i 位置转换成 word2 到 j 位置需要最少步数
当来到(i, j)时,如果word1[i] == word2[j] ,说明只需看i-1和j-1就行了,如果dp[i-1][j-1]已知,假设为a,那么dp[i][j] = a。
如果word1[i] != word2[j] , 这时需要分三种情况考虑:
- 若word1只是在i位置与word2在j位置不等,而word1的前i-1位置可以通过a步变换为word2的前j-1位,那么此时dp[i][j] = dp[i-1][j-1]+1,即只需将word1的第i位变换位word2的第j位即可。举例,若i=5,j=3,现在要求dp[5][3],显然word1[5] != word2[3]
- 若word1在i位置与word2在j位置不等,且word1从1到i可以通过b步转换为word2从1到j-1,那么此时dp[i][j] = dp[i][j-1]+1,即需要在word1的i后面插入一个word2[j]
- 若word1在i位置与word2在j位置不等,且word1从1到i-1可以通过c步转换为word2从1到j,那么此时dp[i][j] = dp[i-1][j]+1,即需要删除word1[i]
以下均以 word1 为 “horse”,word2 为 “ros”,且 dp[5][3] 为例,即要将 word1的前 5 个字符转换为 word2的前 3 个字符,也就是将 horse 转换为 ros。
No. 650 只有两个键的键盘
https://leetcode-cn.com/problems/2-keys-keyboard/
public int minSteps(int n) {
int[] dp = new int[n+1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[1] = 0;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i/2; j++) {
if (i % j == 0)
dp[i] = Math.min(dp[i], dp[j] + i / j);
}
}
return dp[n];
}