算法挑战记录——Java
-
最近感觉需要提高一下自己的算法水平,因此开始进行算法挑战训练专题,希望自己能够坚持下来。开始于2020-11-08.
-
最近同时也在学习python,打算同步做一份Python实现的算法,flag先立在这里,之后会把链接附在上面。于2020-11-10.
-
执行效率还蛮高,可能是现在只做到6个,当天就搞定了Python的版本,这里是Python实现的链接:
算法挑战记录——Python -
本文篇幅已经超过600行,不便继续维护,因此接下来的内容在其他文章维护。于2020-11-18
第二篇地址 算法挑战记录Ⅱ——Java
1.旋转字符串挑战
描述
-
给定一个字符串(以字符数组的形式给出)和一个偏移量,根据偏移量原地旋转字符串(从左向右旋转)。
在数组上原地旋转,使用O(1)的额外空间
思路
- 原地旋转并使用O(1)的额外空间,意味着只能在字符数组上进行操作,通过交换索引位置来进行交换。
- 交换次数为数组长度-1,代码如下:
public class Solution {
/**
* @param str: An array of char
* @param offset: An integer
* @return: nothing
*/
public void rotateString(char[] str, int offset) {
// write your code here
if (str.length < 2 || offset == 0 ) return;//字符数组长度小于2时无需交换
int len = str.length;
offset %= len;//超过长度取余数
if (offset == 0) return;//交换位置为0时无需操作
char temp; //临时字符位置
int pos =0;//需要进行替换的元素位置
int check = 0;//如果字符数组长度不是素数,会出现循环
for ( int i =1;i<str.length ;i++ ){//只需要进行length-1次
int newPos = (pos+offset)%len;
temp = str[newPos];
str[newPos] = str[check];
str[check] = temp;
pos+= offset;
if (pos%len == check){//check最多为offset-1,循环就可结束
check += 1;
pos =check;
}
}
}
}
2.尾随零
描述
-
给定一个整数n,返回n!(n的阶乘)的尾随零的个数。
您的解法时间复杂度应为对数级别。
思路
- 尾随0以为着计算结果有多少个10,10 =2×5。也就是说寻找阶乘中因数2和5的个数。由于阶乘中含有因数2的数量将远多于5的个数,因此只需要统计因数为5的个数。
- 如果针对5的数量直接进行统计,时间复杂度将不是对数级别。因此如何统计5的个数就是解决问题的关键。
- 我们不妨针对所给的n进行考虑,思考5 25 125 情况下包含5的个数规律,不难发现,每隔5倍,因子中包含5的数量都为n/5个。
- 综合上述分析,循环次数仅需次数为log5n,代码如下。
public class Solution {
/**
* @param n: a integer
* @return: return a integer
*/
public int trailingZeroes(int n) {
// write your code here
int num5 = 0;
for (int i =n;i>=5;i/=5){
num5 += i/5;
}
return num5;
}
}
3.落单的数Ⅰ
描述
-
给出 2 * n + 1个数字,除其中一个数字之外其他每个数字均出现两次,找到这个数字。(n≤100)
挑战: 一次遍历,常数级的额外空间复杂度
思路
- 最为直观的思路是利用set的特性来进行去重操作,但无法做到常数级别的空间复杂度,代码如下。
public class Solution {
/**
* @param A: An integer array
* @return: An integer
*/
public int singleNumber(int[] A) {
// write your code here
Set hs = new HashSet();
for (int i = 0;i<A.length ;i++ ){
if (hs.contains(A[i])){
hs.remove(A[i]);
}else{
hs.add(A[i]);
}
}
return (int)hs.iterator().next();
}
}
- 利用位操作异或的特性,即a ^ a ^ b = b,可以实现空间复杂度为常数的目标。
public class Solution {
/**
* @param A: An integer array
* @return: An integer
*/
public int singleNumber(int[] A) {
// write your code here
int res = A[0];
for (int i = 1;i<A.length ;i++ ){
res ^= A[i];
}
return res;
}
}
4.统计数字
描述
- 计算数字 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)。
思路
在不进行分析的情况下,使用二维for循环暴力计算显然会耗费大量的时间。
因此我们需要对数字出现次数分布进行分析,寻找更好的方法。
-
寻找规律
-
我们很容易想到数字本身就是从0-9循环累加,每一位都是从0到9之后进位。
-
不妨对数字首位进行补零。补零后全部个位数、十位数、百位数…的总位数和分别为10 2×102 3×103…。
-
设位数为n,n位数以内的全部数字出现的次数为n×10n
-
因此每个数字出现的次数为n×10n/10=n×10n-1
-
由于除个位外最高位数字不能为零,需要去掉除个位0之外的多余的零,此时需要分别统计k=0和k!=0两种情况。
-
k!=0时:
在n位以内的全部数字中k出现的次数为: s u m k = n × 1 0 n − 1 sum_k =n×10^{n-1} sumk=n×10n−1 -
k=0时:
在n位以内的全部数字中k出现的次数为: s u m k = n × 1 0 n − 1 − ∑ 1 n 1 0 n − 1 + 1 sum_k =n×10^{n-1} - \sum_1^n 10^{n-1}+1 sumk=n×10n−1−1∑n10n−1+1最末尾的+1是指个位数时0可以为最高位数字
-
-
总结上述规律之后,我们在统计时可以将统计数字简化为
1.统计当前位所含数字k数量
2.统计之前位所含数字k数量 -
统计当前位数字k数量时需要对比当前位数字和k之间的关系,分类进行统计。
-
综合上述分析,采用n/10>0循环来作为判断条件,当n为零时需要单独考虑,该算法循环次数仅需次数为log10n,空间复杂度为O(1),代码如下。
public class Solution {
/**
* @param k: An integer
* @param n: An integer
* @return: An integer denote the count of digit k in 1..n
*/
public int digitCounts(int k, int n) {
// write your code here
if (n == 0) {
if(k == 0 ) return 1;
else return 0;
}
int unit = 1;//当前位的单位1、10、100、
int place = 0;//当前位所在的前一位
int sum =0;//统计结果
int temp;//存储n每一位的数值
int count=0;//统计次一位数的总数
int tail = 0;//当k为0时统计过多计算的0
while (n>0){
temp =n%10;
//统计当前位所含数字k的数量
if(temp>k) {
sum += unit;
}else if (temp == k){
sum += count+1;
}
//统计之前位所含数字k的数量
if(k == 0){
sum += temp*place*unit/10 -tail;//注意第一次个位时tail = 0
}else{
sum += temp*place*unit/10;
}
count += temp * unit;
place+=1;
unit *= 10;
n/=10;
tail = unit;
}
return sum;
}
}
5.移除9
描述
-
从整数1开始,删除任意整数包含9,像是9, 19, 29…
现在,我们有一串新的整数序列: 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, …
给定正整数n,你需要返回删除之后序列的第n个整数。注意1将会是第一个整数。n 不会超过 9 x 108.
样例
输入:88
输出:107
解释:
移除包含9的数字,第88个数字为107
思路
- 实质上是进行了进制转换,将十进制的数转化为了9进制
- 该算法循环次数仅需次数为log9n,空间复杂度为O(1)具体代码如下:
public class Solution {
/**
* @param n: an integer
* @return: return an long integer
*/
public long newInteger(int n) {
// write your code here
long res = 0;
int times = 0;
while (n >0){
res += Math.pow(10,times)*(n%9);
n = n / 9;
times+=1;
}
return res;
}
}
6.丑数 II
描述
-
设计一个算法,找出只含素因子2,3,5 的第 n 小的数。
符合条件的数如:1, 2, 3, 4, 5, 6, 8, 9, 10, 12…我们可以认为 1 也是一个丑数。
挑战: 要求时间复杂度为 O(nlogn) 或者 O(n)。
样例
输入:9
输出:10
思路
-
分析题目之后,需要明确这道题的关键在于找出只含2,3,5因子的数字。
-
看到这道题时我很自然想到的是筛法,但筛法无法剔除掉不满足条件的因子。如果采取暴力方式,在筛掉数据之后,显然需要去逐个筛查每个字符是否满足只含2、3、5的因子。这样做无疑会严重增加算法的时间复杂度。
-
因此我们这里需要将思路从
剔除不满足条件的因子转换到如何利用因子逐个生成满足条件的数字。 -
从数学的角度讲,这道题就是找出由集合{2i3j5k: i,j,k ∈ N}以及乘法运算构成的半群,按从小到大顺序的第n个元素。
-
可以发现:从1开始,
当1分别乘2,3,5之后,1就无需使用到
当2分别乘2,3,5之后,2就无需使用到
当某一个数字乘2过后,那么只需要考虑该数字乘3和5的情况。 -
最好的方法就是采用动态规划的方式,逐个生成未使用的最小数字
使用长度为n的数组存放依次生成的数字。 3个指针指向数组中未被使用到的最小的数字的位置。 从数组第二个位置开始到数组最后位置结束: 找出三个指针指向的数字生成的最小的数字,存放在数组的空位。 该数字对应的指针右移一位。 数组的最后一位数字即为满足条件的结果。
-
该算法时间复杂度为O(n),空间复杂度为O(n),具体代码如下:
public class Solution {
/**
* @param n: An integer
* @return: return a integer as description.
*/
int[] temp = new int[3];//存放乘2、3、5的指针
public int nthUglyNumber(int n) {
// write your code here
if (n <= 6) return n;//小于6返回本身即可
int[] res = new int[n];//存放生成的结果
res[0] = 1;//初始化种子为1
for (int i =1;i<n ;i++ ){
res[i] = getMin(res);//获取最小的满足条件结果
}
return res[n-1];
}
public int getMin(int[] res){
int a =res[temp[0]]*2;
int b =res[temp[1]]*3;
int c =res[temp[2]]*5;
int min = (min = a<b?a:b) <c? min:c;
if (a == min) temp[0]++;
if (b == min) temp[1]++;
if (c == min) temp[2]++;
return min;
}
}
7.落单的数Ⅱ
描述
- 给出3*n + 1 个非负整数,除其中一个数字之外其他每个数字均出现三次,找到这个数字。
样例
输入: [1,1,2,3,3,3,2,2,4,1]
输出: 4
挑战: 一次遍历,常数级的额外空间复杂度
思路
-
最为直观的思路是对数组进行排序,然后每隔3个数间隔对比之后一个数。但这样做显然无法做到一次遍历,因为在排序时就已经进行过遍历。
-
仔细观察题目要求,可以试图将思路转变为如何将三个相同的数之和变为0。
由于非负整数,无需考虑补码存储问题,这里我们可以使用一个长度为32的int数组统计每一位1的数量,然后对3取余,然后再将数组转换为int即可。
-
这样能够满足只遍历一次的要求,并且额外的空间复杂度只有常数级。算法时间复杂度为O(n)。
public class Solution {
/**
* @param A: An integer array
* @return: An integer
*/
public int singleNumberII(int[] A) {
// write your code here
int[] temp =new int[32];
int pos =0;
int res = 0;
for(int i = 0;i< A.length;i++){
while(A[i] >0){
if (A[i]%2 == 1)
temp[pos] +=1;
pos ++;
A[i] /= 2;
}
pos = 0;
}
for(int i =31; i>=0;i--){
res *= 2;
res += temp[i]%3;
}
return res;
}
}
-
上述算法需要每次都进行额外的最差32次的循环,而且还需要将数组重组为int类型。如果能够使用位运算来处理,可以有效提高算法的速度,为此回顾了有限状态机相关的内容。
构造一个三进制的归零的进制规则 即 00->01->10->00 这样每三个相同的数字运算之后,都会回到0 由于对于int来说额外增加一位共需64位的空间,因此这里使用两个int类型的变量分别存储高位和低位。最终高位将都是0,因此结果为低位值。 前一步的低位^A[i] & ~高位 得到当前低位值 前一步的高位^A[i] & ~当前低位 得到当前高位值
-
这样能够满足只遍历一次的要求,并且额外的空间只有2个int大小。算法时间复杂度为O(n),运行时间将小于上面的算法。
public class Solution {
/**
* @param A: An integer array
* @return: An integer
*/
public int singleNumberII(int[] A) {
// write your code here
int high = 0;
int low = 0;
for(int i = 0;i< A.length;i++){
low = low ^ A[i] & ~high;
high = high ^A[i] & ~low;
}
return low;
}
}
8.落单的数Ⅲ
描述
- 给出2*n + 2个的数字,除其中两个数字之外其他每个数字均出现两次,找到这两个数字。
样例
样例 1:
输入: [1,2,2,3,4,4,5,3]
输出: [1,5]
样例 2:
输入: [1,1,2,3,4,4]
输出: [2,3]
挑战: O(n)时间复杂度,O(1)的额外空间复杂度
思路
-
最为直观的思路是统计每个数字出现的次数,显然额外空间复杂度时O(n),想要完成挑战目标,还是要从位运算考虑。
-
很容易发现这道题和Ⅰ相比多了一个数,对数组中的数字按位异或后的数为a^b,我们这里需要对a和b进行区分,区分之后便可以区分出这两个数。
假设数组为A 考察a^b 由于a、b两数不同,那么必然存在1位k为1 并且该位必定属于a、b其中之一 那么也就是说我们以这一位k作为判断 也就是对A[i] & k ==0 判断 ,同时分别进行按位异或, 就可以区分出来这两个数。
-
找到k位为1数时可以逐位%2取余判断,也可以按位&1来判断。
这里可以利用补码的特性,非零数 n & -n 可以得到 n 最后一位非零位。以8 bit为例 3: 0000 0011 -3: 1111 1101 3&-3: 0000 0001 6: 0000 0110 -3: 1111 1011 3&-3: 0000 0010
-
这样能够满足O(1)额外的空间复杂度,需要遍历两次算法时间复杂度为O(n)。
public class Solution {
/**
* @param A: An integer array
* @return: An integer array
*/
public List<Integer> singleNumberIII(int[] A) {
// write your code here
int temp = 0;
int[] res =new int[2];
for (int i : A){
temp ^=i;
}
temp &= -temp;//找到一位不为0的数
for (int i : A){
if((i & temp) == 0){
res[0] ^=i;
}else{
res[1] ^=i;
}
}
return Arrays.asList(res[0],res[1]);
}
}
9.骰子求和
描述
- 扔 n 个骰子,向上面的数字之和为 S。给定 n,请列出所有可能的 S 值及其相应的概率。
样例
输入:n = 2
输出:[[2,0.03],[3,0.06],[4,0.08],[5,0.11],[6,0.14],[7,0.17],[8,0.14],[9,0.11],[10,0.08], [11,0.06],[12,0.03]]
思路
-
这道题实际上是求掷骰子结果的概率分布,总的投掷可能性是6n,可能的结果范围为 n~6n。
当然我们可以模拟掷骰子的全部可能性,统计各个结果的总数 但无疑这样做的时间复杂度和空间复杂度都会相当大 这里应该致力于减少算法的空间复杂度和时间复杂度 通常的解法是使用动态规划来进行处理,这里我并不想这样处理,可以参见 LintCode 20题评论下的解答方法。
-
这次我们从数学的角度考虑,由于每一次投掷的结果都会依赖上一次的掷骰子的结果,实际上就是一种离散卷积。
-
概率分布如下:
F ( n ) = ∑ 1 6 F ( n − 1 ) k F(n) = \sum_{1}^6F(n-1)k F(n)=1∑6F(n−1)k 边 界 值 F ( 1 ) = k = [ 1 6 , 1 6 , 1 6 , 1 6 , 1 6 , 1 6 ] 边界值F(1)=k=[\frac1 6,\frac1 6,\frac1 6,\frac1 6,\frac1 6,\frac1 6] 边界值F(1)=k=[61,61,61,61,61,61] -
或者也可以理解为下式的展开项的系数: ( 1 + x + x 2 + x 3 + x 4 + x 5 ) n 6 n \frac{(1+x+x^2+x^3+x^4+x^5)^n}{6^n} 6n(1+x+x2+x3+x4+x5)n
-
下面是具体算法,空间复杂度O(n),时间复杂度为O(n2)。
public class Solution {
/**
* @param n an integer
* @return a list of Map.Entry<sum, probability>
*/
public List<Map.Entry<Integer, Double>> dicesSum(int n) {
// Write your code here
// Ps. new AbstractMap.SimpleEntry<Integer, Double>(sum, pro)
// to create the pair
HashMap<Integer, Double> map = new HashMap<Integer, Double>();
if (n == 1){//为1时直接返回结果
for (int i = 1;i< 7 ;i++){
map.put(i,1.0/6.0);
}
} else{//计算骰子个数为n时的结果概率分布
int all = 5*n+1;
long[] temp = new long[all];
for(int i=0; i<6;i++){//初始化起始迭代
temp[i] = 1L;
}
int current = 5;
int next;
for (int s=1;s< n;s++){//统计频数,(利用技巧降低空间复杂度)
next = current + 5;
for(int i = 0;i< next/2+1;i++){
temp[next-i] += temp[next-i-1]+temp[next-i-2]+temp[next-i-3]+temp[next-i-4]+temp[next-i-5];
}
for(int i = 0;i< next/2+1;i++){//减少计算次数
temp[i] = temp[next -i];
}
current = next;
}
double sum = Math.pow(6,n);//计算总数
for(int i=0;i<all;i++){
map.put(i+n,temp[i]/sum);
}
}
return new ArrayList<Map.Entry<Integer, Double>>(map.entrySet());//输出符合条件结果。
}
}
- 若使用FFT时间复杂度会降到O(nlogn),有兴趣可以尝试以下。
10 两数之和
描述
- 给一个整数数组,找到两个数使得他们的和等于一个给定的数 target。
你需要实现的函数twoSum需要返回这两个数的下标, 并且第一个下标小于第二个下标。注意这里下标的范围是 0 到 n-1。
样例
样例1:
给出 numbers = [2, 7, 11, 15], target = 9, 返回 [0, 1].
样例2:
给出 numbers = [15, 2, 7, 11], target = 9, 返回 [1, 2]。
挑战
- O(n) 空间复杂度,O(nlogn)时间复杂度,
O(n) 空间复杂度,O(n)时间复杂度,
思路
- 很明显这道题想要降低时间复杂度需要使用额外的空间
- 注意到当一个数a在数组中,那么必然target - a也在数组中
- 因此我们构造一个存储target - number[i] 的另一个容器类,然后判断是否number[i]在该容器中,即可查找到两个目标数的位置。
- 这里选用set可以使时间复杂度降为O(n),因为set.contains()方法为O(1)时间复杂度。
- 下面是具体算法,时间复杂度O(n),空间复杂度O(n).
public class Solution {
/**
* @param numbers: An array of Integer
* @param target: target = numbers[index1] + numbers[index2]
* @return: [index1 + 1, index2 + 1] (index1 < index2)
*/
public int[] twoSum(int[] numbers, int target) {
// write your code here
Set<Integer> set = new HashSet<Integer>();
int check = 0;
int[] res = new int[]{-1,-1};
int find1 = 0;
for (int i = 0;i<numbers.length ; i++){
set.add(target - numbers[i]);
}
for (int i = 0; i<numbers.length;i++){
if(set.contains(numbers[i])){
if(target == 0){
if (check == 0){
res[0] = i;
check +=1;
}else if(numbers[res[0]] + numbers[i] == target){
res[1] = i;
return res;
}else if(check == 1){
res[1] = i;
check +=1;
}else{
if(numbers[res[0]] + numbers[i] == target){
res[1] = i;
return res;
}else{
res[0] = res[1];
res[1] = i;
return res;
}
}
}else{
if (check == 0){
res[0] = i;
check +=1;
}else{
res[1] = i;
return res;
}
}
}
}
return res;
}
}