动态规划
125. 背包问题 II
563. 背包问题 V
前言
使用 Google Chrome浏览器,添加插件 九章刷题小助手 可以看到较优答案。
- 本文参考程序来自九章算法,由 @ 提供。版权所有,转发请注明出处。
- 九章算法致力于帮助更多中国人找到好的工作,教师团队均来自硅谷和国内的一线大公司在职工程师。
- 现有的面试培训课程包括:九章算法班,系统设计班,算法强化班,Java入门与基础算法班,Android 项目实战班,
- Big Data 项目实战班,算法面试高频题班, 动态规划专题班
- 更多详情请见官方网站:http://www.jiuzhang.com/?source=code
算法记录
1. a+b问题
算法详解
给出两个整数 a 和 b , 求他们的和。
我的想法:
除了 return a+b; 我没什么想法……
九章解法:
class Solution {
public int aplusb(int a, int b) {
while (b != 0) {
int _a = a ^ b;
int _b = (a & b) << 1;
a = _a;
b = _b;
}
return a;
}
};
- a^b: a与b按位异或;由于 0^0=0; 0^1=1; 1^0=1; 1^1=0,所以异或的结果就是a和b相加之后,该进位的地方不进位(异或运算有一个别名叫不进位加法)。
- a&b: a与b按位与;0&0=0; 0&1=0; 1&0=0; 1&1=1,
- <<: 位运算符,对一个数的二进制进行移位。 例如,a=(a<<2), 如果a的二进制是 0000 0011,经过运算,左移两位 0000 1100,后面补零。
我用二进制的 7+15 即 0111 + 1111 ,逐步推算,结果完全正确。
原理用十进制来解释类似于:7+15 不进位相加得:12,记录进位在第一位:01,左移一位:10,是实际进位的数值,相加的最终结果:12+10=22。
public class Solution {
public int aplusb(int a, int b) {
// write your code here
if (b == 0)
return a;
else
return aplusb( a ^ b, (a & b) << 1);
}
}
算法分析
- 这样的效率更高吗?
- 该方法是否只限定于整数?
- 举个负数的例子
2. 尾部的零
算法详解
设计一个算法,计算出n阶乘中尾部零的个数
我的想法:
因为偶数比5的倍数分布更稠密,所以能分出来几个5,就有几个零
注意25、125等5的次方可以分出来多个5。
所以应该先整除5,再找离它最近的5的次方的倍数。
例如:377 377/5=75,所以至少有75个0,再找25或125的倍数,377=125*3+2;一个125多出2个5,3个多出来6个5;然后是25,25和125之间有4个25的倍数,一个25多一个5,4个多4个5;所以总共有 75+6+4=85个5,就有这么多0。但答案是93个,惊,哪里还少了呀 。对了,还有25的6倍,7,8,9,11,12,13,14倍,够了93个。所以是不是可以这样做,377/5=75;377/25=15;377/125=3;75+15+3=93;15个25里应该有30个5,但75个5里包含了15个,3个125应该有9个5,但75个5里包含了3个,15个25里包含了3个,最后也只剩3个了。
用这个方法再试一个789,789/625=1;789/125=6;789/25=31;789/5=157;
157+31+6+1=195;
答案对了,所以关键是找到比 n 小的5的最大的次方
这么找行不行,789/5=157,157/5=31,31/5=6,6/5=1,1/5=0,直到得0。
class Solution {
public:
long long trailingZeros(long long n) {
long long sum=0;
while(n){
n=n/5;
sum=sum+n;
}
return sum;
}
};
九章解法:
和我的思路一样的,哈哈哈,还是很有成就感的。
class Solution {
public:
long long trailingZeros(long long n) {
long long sum = 0;
while (n != 0) {
sum += n / 5;
n /= 5;
}
return sum;
}
};
算法分析
出错,分析,改正,出错,分析,改正……循环往复,得到正解
3. 统计数字
算法详解
计算数字 k 在 0 到 n 中的出现的次数,k 可能是 0~9 的一个值。
例如:
输入:k = 1, n = 12
输出:5
解释:在 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] 中,我们发现 1 出现了 5 次 (1, 10, 11, 12)(注意11中有两个1)。
我的想法:
举个例子:234
想法一:
一位数:0-9;个位循环1次
二位数:10-99;个位循环9次; 十位1-9各10个
三位数:100-199;个位循环10次;十位各10个;百位1有100个
200-229;个位循环3次;十位0-2各10个;百位2有30个
230-234:个位0-4各一个,十位3有5个,百位2有5个
合计:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
44 | 154 | 89 |
想法一虽然正确,但规律性不高,不太容易编程,想到想法二:
每10个数分一组,个位0到9每10个数完成一次循环,
到了二位数以后,个位的规律不变,十位每10个数加1,每100个数完成一次循环,
到了三位数,个位、十位的规律不变,百位每100个数加1,每1000个数完成一次循环,
最后再注意一下0,0不能做数字开头,算多的要减去
还是以234为例
234/10=23+4;每10个数分一组,个位完成23次循环,剩余 0-4 五个数
234/100=2+34;每100个数分一组,十位完成2次循环,每个数加20;剩余35个数,0-2各10次,3 五次。
234/1000=0+234;百位没有发生循环;剩余235个数,0-1各100次,2 三十五次
0不能做开头,二位数是算0做开头 -10;三位数是算0做开头 -100;
合计:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|
44 | 154 | 89 |
分析,
1、程序可以由循环实现,n是几位数,循环几次
2、第几次循环就用n/(10的几次),需要用到商和余数
3、第k次循环,商 乘 (10的k-1次) 表示因完成循环增加的次数
余数 除 (10的k-1 ) 的商 表明未完成的循环进行到了几,例如商是3,0-2 完成,各加 10的k次,余数 除 (10的k-1 ) 的余数 表明最后的数的次数,例如商是3,3的个数加 余数+1 次
4、每次循环判断 数 是否是 0,如果是,每次减去 (10的k-1)次。循环第一次不减
class Solution {
public:
/**
* @param k: An integer
* @param n: An integer
* @return: An integer denote the count of digit k in 1..n
*/
int digitCounts(int k, int n) {
// write your code here
//先判断n是几位数
int num=0; //默认是0位数
int count=0; //记录k出现的次数
int _n=n;
while(_n/10){
_n/=10;
num+=1;
}
num+=1;
//得到n的位数 num
for(int i=1;i<=num;i++){
int _s=n/pow(10,i); //商
int _y=n-_s*pow(10,i); //余数
count+=_s*pow(10,i-1);
int _ys=_y/pow(10,i-1);
int _yy=_y-_ys*pow(10,i-1);
if(k<_ys){
count+=pow(10,i-1);
}else if(k==_ys){
count+=_yy+1;
}
if(k==0&&i!=1){
count-=pow(10,i-1);
}
}
return count;
}
};
九章解法:
这个答案好暴力,直接把每个数的每一位都提取出来判断,思路很简单,但效率不高吧。
class Solution {
public:
int digitCounts(int k, int n) {
int count = 0;
if (k == 0) {
count = 1;
}
for (int i = 1; i <= n; i++) {
int number = i;
while (number > 0) {
if (number % 10 == k) {
count += 1;
}
number /= 10;
}
}
return count;
}
};
算法分析
感觉 lintcode 的效率分析会受电脑性能等的影响,并不完全准确。
4. 丑数 II
算法详解
设计一个算法,找出只含素因子2,3,5 的第 n 小的数。(找到第n个丑数)
符合条件的数如:1, 2, 3, 4, 5, 6, 8, 9, 10, 12…
样例 1:
输入:9
输出:10
样例 2:
输入:1
输出:1
挑战
要求时间复杂度为 O(nlogn) 或者 O(n)。
注意事项
我们可以认为 1 也是一个丑数。
我的想法:
想法一:
从2开始找,一个数一个数判断,找到一个丑数,计数变量加一,直到计数变量等于n
class Solution {
public:
int nthUglyNumber(int n) {
int count=1;
int num=2; //从2开始,依次判断
int targ=1; //找到的丑数赋值给 targ;
while(count!=n){
//判断num是不是丑数,如果是 count+1,且赋值给 targ
int _num=num;
while(_num%2==0){
_num/=2;
}
if(_num!=1){
while(_num%3==0){
_num/=3;
}
if(_num!=1){
while(_num%5==0){
_num/=5;
}
}
}
if(_num==1){
targ=num;
count+=1;
}
num+=1;
}
return targ;
}
};
可以实现,但时间复杂度太高,当n变大时,丑数越来越稀疏,需要好久才能算出来。
九章解法:
class Solution {
public:
int nthUglyNumber(int n) {
int *uglys = new int[n]; //创建数组,存储丑数
uglys[0] = 1; //第一个丑数是1
int next = 1; //循环变量,递增,循环n次,每次循环找到一个丑数
int *p2 = uglys; //指针,指向丑数数组首地址 uglys[0]
int *p3 = uglys; //指针,指向丑数数组首地址 uglys[0]
int *p5 = uglys; //指针,指向丑数数组首地址 uglys[0]
//开始循环
while (next < n){
int m = min(min(*p2 * 2, *p3 * 3), *p5 * 5);
uglys[next] = m;
while (*p2 * 2 <= uglys[next])
*p2++;
while (*p3 * 3 <= uglys[next])
*p3++;
while (*p5 * 5 <= uglys[next])
*p5++;
next++;
}
int uglyNum = uglys[n - 1];
delete[] uglys;
return uglyNum;
}
};
④下一个丑数一定是已有的丑数里的某一个乘2、3或5
循环过程是:
1乘2得2,1乘3得3,1乘5得5,2最小,下一个丑数是2,
1乘2已经使用,p2指向2,1乘3、乘5还没有用过,p3、p5依然指向1
算法分析
九章解法,由丑数得丑数,更加快捷
5. 第k大元素
算法详解
在数组中找到第 k 大的元素。
样例 1:
输入:n = 1, nums = [1,3,4,2]
输出:4
样例 2:
输入:n = 3, nums = [9,3,2,4,8]
输出:4
挑战
要求时间复杂度为O(n),空间复杂度为O(1)。
注意事项
你可以交换数组中的元素的位置
我的想法:
这个之前有过接触,我当时是全部排序之后,输出第k大元素,但这样效率太低了,而且没有必要,因为只要第k大,所以全部排序也是无用功,只需要排列前k个就行了。
所以思路就是前k个元素由大到小排列好,其余的依次往里插,找到自己的位置,所以最后就只得到前k大元素。 使用折半排序
九章解法:
class Solution {
public:
int kthLargestElement(int k, vector<int> nums) {
if (nums.size() == 0 || k < 1 || k > nums.size()){
return -1;
}
return partition(nums, 0, nums.size() - 1, nums.size() - k);
}
private:
int partition(vector<int> &nums, int start, int end, int k) {
if (start >= end) {
return nums[k];
}
int left = start, right = end;
int pivot = nums[(start + end) / 2];
while (left <= right) {
while (left <= right && nums[left] < pivot) {
left++;
}
while (left <= right && nums[right] > pivot) {
right--;
}
if (left <= right) {
swap(nums, left, right);
left++;
right--;
}
}
if (k <= right) {
return partition(nums, start, right, k);
}
if (k >= left) {
return partition(nums, left, end, k);
}
return nums[k];
}
void swap(vector<int> &nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
};
算法分析
32. 最小子串覆盖
算法详解
给定两个字符串 source 和 target. 求 source 中最短的包含 target 中每一个字符的子串.
样例 1:
输入: source = “abc”, target = “ac”
输出: “abc”
样例 2:
输入: source = “adobecodebanc”, target = “abc”
输出: “banc”
解释: “banc” 是 source 的包含 target 的每一个字符的最短的子串.
样例 3:
输入: source = “abc”, target = “aa”
输出: “”
解释: 没有子串包含两个 ‘a’.
挑战
O(n) 时间复杂度
注意事项
如果没有答案, 返回 “”.
保证答案是唯一的.
target 可能包含重复的字符, 而你的答案需要包含至少相同数量的该字符.
我的想法:
九章解法:
class Solution {
public:
string minWindow(string s, string t) {
unordered_map<char, int> mp;
for (char now : t) {
mp[now] ++;
}
int count = mp.size();
int j = 0;
int ans = INT_MAX;
string res;
for (int i = 0; i < s.size(); i++) {
while(count != 0 && j < s.size()) {
mp[s[j]]--;
if (mp[s[j]] == 0) {
count--;
}
j++;
if (count == 0) {
break;
}
}
if (count == 0 && j - i< ans) {
ans = j - i;
res = s.substr(i, j - i);
}
if(mp[s[i]] == 0) {
count++;
}
mp[s[i]]++;
}
return res;
}
};
算法分析
384. 最长无重复字符的子串
算法详解
给定一个字符串,请找出其中无重复字符的最长子字符串。
样例 1:
输入: “abcabcbb”
输出: 3
解释: 最长子串是 “abc”.
样例 2:
输入: “bbbbb”
输出: 1
解释: 最长子串是 “b”.
挑战
O(n) 时间复杂度
我的想法:
九章解法:
外层循环是右指针,左指针根据情况移动到合适的位置。
class Solution {
public:
int lengthOfLongestSubstring(string& s) {
// 题意为求不包含重复字符的最长子串
// left用以记录合法的最远左边界位置,last记录字符上一次出现的位置
int ans = 0, left = 0, len = s.length();
int last[255];
memset(last, -1, sizeof last);
for (int i = 0; i < len; i++) {
// 上次出现位置在当前记录边界之后,即该子串中出现了重复字符,需调整left使得子串合法
if (last[s[i]] >= left) left = last[s[i]] + 1;
last[s[i]] = i;
ans = max(ans, i - left + 1);
}
return ans;
}
};
算法分析
406. 和大于S的最小子数组
算法详解
给定一个由 n 个正整数组成的数组和一个正整数 s ,请找出该数组中满足其和 ≥ s 的最小长度子数组。如果无解,则返回 -1。
样例 1:
输入: [2,3,1,2,4,3], s = 7
输出: 2
解释: 子数组 [4,3] 是该条件下的最小长度子数组。
样例 2:
输入: [1, 2, 3, 4, 5], s = 100
输出: -1
挑战
如果你已经完成了O(nlogn)时间复杂度的编程,请再试试 O(n)时间复杂度。
我的想法:
由大到小排序后从前往后选,肯定可以实现,但这样会有浪费
可以先找出最大的,不行的话,在剩下的数据中在找到最大的。每次只找最大的
思路错了,子数组只能是连续的
九章解法:
九章视频课中讲解了解法的优化
题目类型是 寻找符合条件的子数组
初始化:
获取数组长度:int n=nums.size();
初始化左右指针(开始时都指向数组第一个元素):int right=0,left=0;
初始化子数组的和:int total=0;
初始化符合条件的子数组的长度:int ans=n+1;
开始寻找:
右指针依次右移,直到移动到数组末位
while(right<n){
right++;
}
右移过程中子数组每次新增一个元素,累加total
while(right<n){
total+=nums[right++];
}
累加过程中判断total值是否大于等于s,如果大于等于更新ans
while(right<n){
total+=nums[right++];
if(total>=s){
ans=min(ans,right-left+1);
}
}
此时,暂停右指针的移动,左指针右移,直到与right重合,或者total值小于了s
while(right<n){
total+=nums[right++];
if(total>=s){
ans=min(ans,right-left);
while(left<right&&total>=s){
total-=nums[left++];
if(total>=s){
ans=min(ans,right-left);
}
}
}
}
答案写法,left是外层循环
int minimumSize(vector<int> &nums, int s) {
int n = nums.size();
int j=0;
int left = 0, right = 0, total = 0, ans = n + 1;
for(int i=0;i<n;i++){
while(j<n&&total<s){
total+=nums[j++];
}
if(total>=s){
ans=min(ans,j-i);
}
total-=nums[i];
}
if (ans == n + 1)
return -1;
else
return ans;
}
算法分析
2000. 模板
算法详解
题目:
我的想法:
九章解法:
算法分析
九章算法强化班学习
- 题目永远做不完,要学会使用 一个模板解决一类题目
子串问题
问题特点
在一个数组或字符串中找到连续的符合条件的最短或最长的子串
题目举例
lintcode406题
lintcode384题
lintcode32题