文章目录
LeetBook 新手村
笔记整理于:新手村
1480. 一维数组的动态和
给你一个数组 nums 。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]…nums[i]) 。
请返回 nums 的动态和。
示例:
输入:nums = [1,2,3,4]
输出:[1,3,6,10]
解释:动态和计算过程为 [1, 1+2, 1+2+3, 1+2+3+4] 。
官方解答:
//第一步,第一个元素自然是不变,
//第二步,第二个元素变成第一个元素与原来第二个元素之和,
//第三步,(此时第二个元素已经是动态和了)第三个元素,变成第二个元素与原来第三个元素之和,以此类推
//第一步1,2,3,4
//第二步1,3,3,4
//第三步1,3,6,4
//第四步1,3,6,10
class Solution {
public int[] runningSum(int[] nums) {
for(int i = 1; i < nums.length; i++){
nums[i] += nums[i - 1];
}
return nums;
}
}
复杂度分析
- 时间复杂度:O(n),其中 nn 是给定数组长度。
- 空间复杂度:O(1)。我们只需要常数的空
精选解答:
此题使用求和公式暴力求解的效率较低,因为包含大量重复计算。考虑借助「前一个动态和 f(i-1)f(i−1) 」来计算得到「当前动态和 f(i)f(i) 」,此题被约化为一个简单动态规划问题。
nums | 1 | 2 | 3 | 4 |
---|---|---|---|---|
i | ||||
dp | 1 | 3 | 6 | 10 |
i-1 | i |
// 细心的我们发现,如果原地修改 nums ,可以避免新建 dp 带来的内存开销。但通常情况下,不应改变输入变量,因此不建议原地修改 nums 数组。
// dp[i] = dp[i - 1] + nums[i];
//内存消耗较大
class Solution {
public int[] runningSum(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i] = dp[i - 1] + nums[i];
}
return dp;
}
}
复杂度分析
-
时间复杂度 O(N): 遍历 nums 使用线性时间。
-
空间复杂度 O(1) : 用于保存结果的 dp 是必须使用的空间,此处不计入。
1342. 将数字变成 0 的操作次数
给你一个非负整数 num
,请你返回将它变成 0 所需要的步数。 如果当前数字是偶数,你需要把它除以 2 ;否则,减去 1 。
示例
输入:num = 14
输出:6
解释:
步骤 1) 14 是偶数,除以 2 得到 7 。
步骤 2) 7 是奇数,减 1 得到 6 。
步骤 3) 6 是偶数,除以 2 得到 3 。
步骤 4) 3 是奇数,减 1 得到 2 。
步骤 5) 2 是偶数,除以 2 得到 1 。
步骤 6) 1 是奇数,减 1 得到 0 。
官方解答:
方法一:模拟
思路与算法
将 num 与 1 进行位运算来判断 num 的奇偶性。
class Solution {
public int numberOfSteps(int num) {
int ret = 0;
while (num > 0) {
ret += (num > 1 ? 1 : 0) + (num & 0x01);
num >>= 1;
}
return ret;
}
}
复杂度分析
- 时间复杂度:O(lognum),其中 num 是输入数值。每次循环都将 num 的数值减半,因此时间复杂度为 O(lognum)。
- 空间复杂度:O(1)。只需要常数空间。
大众解法
按位与运算符(&)
按位与运算将两个运算分量的对应位按位遵照以下规则进行计算:
0 & 0 = 0,
0 & 1 = 0,
1 & 0 = 0,
1 & 1 = 1。
即同为 1 的位,结果为 1,否则结果为 0。
例如,设3的内部表示为
00000011
5的内部表示为
00000101
则3&5的结果为
00000001
右移运算符(>>)
右移运算将一个位串信息向右移指定的位,右端移出的位的信息被丢弃。例如12>>2,结果为3。与左移相反,对于小整数,每右移1位,相当于除以2。在右移时,需要注意符号位问题。对无符号数据,右移时,左端空出的位用0补充。对于带符号的数据,如果移位前符号位为0(正数),则左端也是用0补充;如果移位前符号位为1(负数),则左端用0或用1补充,取决于计算机系统。对于负数右移,称用0 补充的系统为“逻辑右移”,用1补充的系统为“算术右移”。
//c++
class Solution {
public:
int numberOfSteps(int num) {
int step = 0;
while(num ){
// if(num % 2 == 0){
// num /= 2;
// }else{
// num--;
// }
(num & 1) == 0 ? num >>= 1 : --num;
step++;
}
return step;
}
};
补充知识:
1672. 最富有客户的资产总量
给你一个 m x n 的整数网格 accounts ,其中 accounts[i][j] 是第 i 位客户在第 j 家银行托管的资产数量。返回最富有客户所拥有的 资产总量 。
客户的 资产总量 就是他们在各家银行托管的资产数量之和。最富有客户就是 资产总量 最大的客户。
示例:
输入:accounts = [[1,2,3],[3,2,1]]
输出:6
解释:
第 1 位客户的资产总量 = 1 + 2 + 3 = 6
第 2 位客户的资产总量 = 3 + 2 + 1 = 6
两位客户都是最富有的,资产总量都是 6 ,所以返回 6 。
官方解答
分别计算每位客户在各家银行托管的资产数量之和,返回这些资产总量的最大值。
//java
class Solution {
public int maximumWealth(int[][] accounts) {
int maxWealth = Integer.MIN_VALUE;
for (int[] account : accounts) {
maxWealth = Math.max(maxWealth, Arrays.stream(account).sum());
}
return maxWealth;
}
}
//c
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int maximumWealth(int** accounts, int accountsSize, int* accountsColSize){
int maxWealth = INT_MIN;
for (int i = 0; i < accountsSize; i++) {
int sum = 0;
for (int j = 0; j < accountsColSize[0]; j++) {
sum += accounts[i][j];
}
maxWealth = MAX(maxWealth, sum);
}
return maxWealth;
}
复杂度分析
- 时间复杂度:O(mn),其中 m 和 n 分别是网格 accounts 的行数和列数。
- 空间复杂度:O(1)。
412. Fizz Buzz
给你一个整数 n ,找出从 1 到 n 各个整数的 Fizz Buzz 表示,并用字符串数组 answer(下标从 1 开始)返回结果,其中:
- answer[i] == “FizzBuzz” 如果 i 同时是 3 和 5 的倍数。
- answer[i] == “Fizz” 如果 i 是 3 的倍数。
- answer[i] == “Buzz” 如果 i 是 5 的倍数。
- answer[i] == i (以字符串形式)如果上述条件全不满足。
示例:
输入:n = 5
输出:["1","2","Fizz","4","Buzz"]
官方解答
class Solution {
public List<String> fizzBuzz(int n) {
List<String> answer = new ArrayList<String>();
for (int i = 1; i <= n; i++) {
StringBuffer sb = new StringBuffer();
if (i % 3 == 0) {
sb.append("Fizz");
}
if (i % 5 == 0) {
sb.append("Buzz");
}
if (sb.length() == 0) {
sb.append(i);
}
answer.add(sb.toString());
}
return answer;
}
}
复杂度分析
-
时间复杂度:O(n)。需要遍历从 1 到 n 的每个整数,对于每个整数 i,生成 answer[i] 的时间复杂度是 O(1)。
-
空间复杂度:O(1)。注意返回值不计入空间复杂度。
笨办法
class Solution {
public List<String> fizzBuzz(int n) {
List<String> answer = new ArrayList();
for(int i = 1;i<=n;++i){
if(i %3 == 0 && i % 5 == 0){
answer.add("FizzBuzz");
}else {
if(i %3 == 0){
answer.add("Fizz");
}
if(i %5 == 0){
answer.add("Buzz") ;
}
}
if(i %5 != 0 && i % 3 !=0){
answer.add(""+i);
}
}
return answer;
}
}
876. 链表的中间结点
题目:876. 链表的中间结点
给定一个头结点为 head
的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
示例:
输入:[1,2,3,4,5]
输出:此列表中的结点 3 (序列化形式:[3,4,5])
返回的结点值为 3 。 (测评系统对该结点序列化表述是 [3,4,5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
提示:
- 给定链表的结点数介于
1
和100
之间。
官方题解
-
方法一:数组
思路和算法
链表的缺点在于不能通过下标访问对应的元素。因此我们可以考虑对链表进行遍历,同时将遍历到的元素依次放入数组 A 中。如果我们遍历到了 N 个元素,那么链表以及数组的长度也为 N,对应的中间节点即为 A[N/2]。
class Solution { public ListNode middleNode(ListNode head) { ListNode[] A = new ListNode[100]; int t = 0; while (head != null) { A[t++] = head; head = head.next; } return A[t / 2]; } }
复杂度分析
- 时间复杂度:O(N),其中 N 是给定链表中的结点数目。
- 空间复杂度:O(N),即数组
A
用去的空间。
-
方法二:单指针法
我们可以对方法一进行空间优化,省去数组 A。
我们可以对链表进行两次遍历。第一次遍历时,我们统计链表中的元素个数 N;第二次遍历时,我们遍历到第 N/2 个元素(链表的首节点为第 0 个元素)时,将该元素返回即可。
class Solution { public ListNode middleNode(ListNode head) { int n = 0; ListNode cur = head; while (cur != null) { ++n; cur = cur.next; } int k = 0; cur = head; while (k < n / 2) { ++k; cur = cur.next; } return cur; } }
复杂度分析
- 时间复杂度:O(N),其中 NN 是给定链表的结点数目。
- 空间复杂度:O(1),只需要常数空间存放变量和指针。
-
方法三:快慢指针法
思路和算法
我们可以继续优化方法二,用两个指针 slow 与 fast 一起遍历链表。slow 一次走一步,fast 一次走两步。那么当 fast 到达链表的末尾时,slow 必然位于中间。
class Solution { public ListNode middleNode(ListNode head) { ListNode slow = head, fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } return slow; } }。
复杂度分析
-
时间复杂度:O(N)O(N),其中 NN 是给定链表的结点数目。
-
空间复杂度:O(1)O(1),只需要常数空间存放 slow 和 fast 两个指针。
-
快慢指针法分析
定义两个指针:
- 快指针:每次走两步,当下一个节点或者当前节点为空时,停止
- 慢指针:每次走一步,快指针停下时慢指针搜在的位置即为链表的中间位置
单链表的节点个数:
- 奇数:快指针走到5的位置时,判断下一个为空(要求 fast.next != null),停止,此时慢指针为3,中间位置
- 偶数:快指针走到5的位置时,判断下一个不为空,继续走下一步(到7的位置),慢指针也往下走一步(到4的位置),此时快指针为空(要求 fast != null ),停止,慢指针为4,中间位置
不管怎么样,慢指针都在中间位置,非常巧妙!
精选题解
-
-
使用递归函数,避免复杂的更改指针变量指向操作,使得求解问题变得简单
-
设置「虚拟头结点」,避免对链表第 1 个结点做单独讨论,这个思想在数组里我们见过,叫「哨兵」
-
使用「快慢指针」
上述相关题目见原文
-
383. 赎金信
题目:383. 赎金信
给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。
如果可以,返回 true ;否则返回 false 。
magazine 中的每个字符只能在 ransomNote 中使用一次。
示例:
输入:ransomNote = "aa", magazine = "ab"
输出:false
输入:ransomNote = "aa", magazine = "aab"
输出:true
官方解答
字符统计:
题目要求使用字符串 magazine中的字符来构建新的字符串ransomNode ,且 ransomNode中的每个字符只能使用一次,只需要满足字符串 magazine中的每个英文字母(‘a’-‘z’) 的统计次数都大于等于 ransomNode中相同字母的统计次数即可。
- 如果字符串 magazine的长度小于字符串 ransomNode的长度,则我们可以肯定magazine 无法构成 ransomNode,此时直接返回 false。
- 首先统计magazine 中每个英文字母 a的次数cnt[a] ,再遍历统计ransomNode 中每个英文字母 的次数,如果发现 ransomNode中存在某个英文字母 c的统计次数大于magazine 中该字母统计次数cnt[c] ,则此时我们直接返回 false。
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
if (ransomNote.length() > magazine.length()) {
return false;
}
int[] cnt = new int[26];
for (char c : magazine.toCharArray()) {
cnt[c - 'a']++;
}
for (char c : ransomNote.toCharArray()) {
cnt[c - 'a']--;
if(cnt[c - 'a'] < 0) {
return false;
}
}
return true;
}
}
复杂度分析
-
时间复杂度:O(m + n),其中 m 是字符串ransomNote 的长度,n 是字符串 magazine 的长度,我们只需要遍历两个字符一次即可。
-
空间复杂度:O(|S|),S 是字符集,这道题中 S 为全部小写英语字母,因此 |S| = 26。
分析
官方题解使用 长度为26的 int 数组 表示 26 个英文字母,判断 magazine 和 ransomNode 长度大小:
- magazine < ransomNode :题目要求判断 ransomNote 能不能由 magazine 里面的字符构成magazine 字母个数少于 ransomNode 肯定不能构成
- magazine < ransomNode:定义一个长度为26的 int数组,两次for循环,第一次把 magazine 中每个字母出现的次数记录到数组中,第二次把 ransomNode 中每个字母出现的次数与 数组中记录数相减,如果有负数说明不能够成
表格演示
以 ransomNote = “aa”, magazine = “ab” 为例:
ransomNote :两个a,数组表示:[2,0,0,0,…]
magazine : 一个a一个b,数组表示:[1,1,0,0,0,…]
第一次 for 循环后 数组变为:[1,1,…]
第二次 fo r循环后 数组变为:[-1,1,…]
ransomNode | 2 | 0 | . | . | . | ||||
---|---|---|---|---|---|---|---|---|---|
magazine | 1 | 1 | . | . | . | ||||
int 数组 | cnt[0] | cnt[1] | cnt[2] | . | . | . | . | . | cnt[25] |
字母 | a | b | c | d | z |
注意:
第一次 for 循环后 数组改变,第二次 for 循环 是在 现在数组基础上进行 相减,1-2=-1,有负数说明 a 字母数量不够,return false。