问题:
段位:4
说明:
假设一个数组 arr,里面的元素是这样的规律:
1、arr[ 0 ] = 1;
2、每一个 arr[ i ] 都存在 2 * arr[ i ] + 1 和 3 * arr[ i ] + 1 的元素在数组里面。
3、arr 是从小到大排序的数组
4、arr 的元素不重复
例子就是: arr = [1, 3, 4, 7, 9, 10, 13, 15, 19, 21, 22, 27, ...] (1 存在 2 * 1 + 1 和 3 * 1 + 1 的元素,3 和 4 也是)
然后输入一个数 n,要求返回 arr 从下标 1 开始的 第 n 个元素值
题目连接:https://www.codewars.com/kata/5672682212c8ecf83e000050
相关算法:
Hamming Numbers(汉明数字):https://blog.youkuaiyun.com/qq_28033719/article/details/115953335
N Linear(N线性):https://blog.youkuaiyun.com/qq_28033719/article/details/115956290
输入案例:
dbl_linear(10) should return 22 案例的数组数一下刚好第 10 个是 22
我的代码:
因为数组里面有 2 * arr[ i ] + 1 和 3 * arr[ i ] + 1 两种线性的元素存在,并且是 从小到大有序的,首先先看这些元素的产生:
1、元素产生,是一个二叉树:
从 1 开始 左边 x * 2 + 1 右边 x * 3 + 1 开始生成树
首先第2层存在 x * 2 + 1 < x * 3 + 1 的规律
然后第3层开始 7 10 9 13 这个排序开始无序
然后再到 第 5 层 开始 出现 31 这个元素 和 第 4 层的元素重复,并且得出 第5 层元素 存在 值 小于 第 4 层的元素 (31 < 40)
2、要求去重排序
一时间没有特别好的想法,肯定是每个元素计算,去重,排序,这样处理是最好的。
但是再观察可以发现:
arr = [1, 3, 4, 7, 9, 10, 13, 15, 19, 21, 22......]
= [1, 1 * 2 + 1, 1 * 3 + 1, 3 * 2 + 1, 4 * 2 + 1, 3 * 3 + 1, 4 * 3 + 1, 7 * 2 + 1, 9 * 2 + 1, 10 * 2 + 1, 7 * 3 + 1....]
标记的数组里面的绿色排列就是 1 1 1 3 4 3 4 7 9 10 7.... 这个微妙的规律,不得不说,arr 的下标 1 开始的 元素都肯定是 前面某一个元素 通过一个线性计算得出的。
那么怎么取某个前面的元素,按照 arr 本来就是有序的规则,肯定是从下标 0 开始,最小的元素开始取,必定获得 经 x * 2 + 1 或者 x * 3 + 1 运算得出的符合下一个排序的元素值。
而且还得考虑去重,那么每次运算时候都判断下 * 2 或者 * 3 是否有相同的,有就修改即可。
因为这个计算是有序进行,所以不用考虑第几层出现更小的数据
先贴出优化的代码
Java代码:
class DoubleLinear {
public static int dblLinear (int n) {
int[] tempArr = new int[n + 1];
tempArr[0] = 1;
int ai = 1, bi = 1, av = 3, bv = 4;
for(int i = 1; i <= n; i ++) {
tempArr[i] = Math.min(av, bv);
if(tempArr[i] == av) av = (tempArr[ai ++] << 1) + 1;
if(tempArr[i] == bv) bv = (tempArr[bi ++] * 3) + 1;
}
return tempArr[n];
}
}
C++代码:
class DoubleLinear
{
public:
static int dblLinear(int n) {
int *tempArr = new int[n + 1],
a = 3, b = 4, aIdx = 0, bIdx = 0;
tempArr[0] = 1;
for(int i = 1; i <= n; i ++) {
tempArr[i] = std::min(a, b);
if(tempArr[i] == a) a = (tempArr[++ aIdx] << 1) + 1;
if(tempArr[i] == b) b = tempArr[++ bIdx] * 3 + 1;
}
return tempArr[n];
};
};
其实没有优化的话,直接暴力地用数组实现也行,还有更好的一个数据结构就是 java 的 sortset 可以排序并且去重,几乎是一举两得:
使用 sortset:
Java:
import java.util.*;
class DoubleLinear {
public static int dblLinear (int n) {
if (n == 0) return 1;
SortedSet<Integer> u = new TreeSet<>();
u.add(1);
for(int i=0; i < n; i++) {
int x = u.first();
u.add((x << 1) + 1);
u.add(x + x + x + 1);
u.remove(x);
}
return u.first();
}
}
也可用 bfs 广度搜索,然后再加个数组暴力地遍历,然后来个 hashset 记录重复数据,不过当数据量继续变大时候,这个就缺点更多了:
import java.util.Arrays;
import java.util.HashSet;
class DoubleLinear {
private static int begin = 0, curIdx = 0;
private static int[] queue = new int[1000000]; // 一百万长度暴力地遍历
public static int dblLinear (int n) {
HashSet<Integer> set = new HashSet<>(); // 另外需要去重
begin = curIdx = 0;
queue[curIdx ++] = 1;
while(curIdx <= n) { // 开始 bfs
int i = begin, end = curIdx;
begin = curIdx;
for(; i < end; i ++) {
int[] arr = new int[]{(queue[i] << 1) + 1, queue[i] * 3 + 1};
for(int j : arr) {
if(!set.contains(j)) {
queue[curIdx ++] = j;
set.add(j);
}
}
}
}
for(int x = 0; x < 3; x ++) { // 因为还有可能下面的层出现元素,所以还需要大概地多来几次
int i = begin, end = curIdx;
begin = curIdx;
for(; i < end; i ++) {
int[] arr = new int[]{(queue[i] << 1) + 1, queue[i] * 3 + 1};
for(int j : arr) {
if(!set.contains(j)) {
queue[curIdx ++] = j;
set.add(j);
}
}
}
}
int[] res = Arrays.stream(queue, 0, curIdx).toArray(); // 最后直接截取长度并排序
Arrays.sort(res);
return res[n];
}
}