2017-06-08
JavaScript
算法
两类铺平问题和它们的逆
这标题取的好数学 233 ,虽然如此,这些问题其实都遇到过,那么铺平是什么?
数组和对象都可以构成递归结构,暂且把使这类结构平铺成单个对象或者数组的过程称之为铺平,按被铺平的数据类型可以分为两类:
  1. 数组的铺平
  2. 对象的铺平
比如 [[1, 2], [3, 4]] 变成 [1, 2, 3, 4]
或者 {a: {b: 'hello'}, c: 'world'} 变成 ["a", "b", "c"]["hello", "world"]

# 数组的铺平和它的逆问题

用递归很好解决铺平
00var flatArr = arr => {
01    if (!Array.isArray(arr)) return [arr]; 
02    else {
03        return arr.reduce((acc, cur) => {
04            return acc.concat( flatArr(cur) )
05        }, []); 
06    }
07}
08
09flatArr([1, 2, [3, 4, 5, [[9, 10, 11], [12, 13, 14, 15], 8]], [1, 23, 5, 6, [2, 3, 4, 5], [2]]])
10// => 
11// [1, 2, 3, 4, 5, 9, 10, 11, 12, 13, 14, 15, 8, 1, 23, 5, 6, 2, 3, 4, 5, 2]
它的逆问题其实跟我们平常遇到的 分页 是一样的:
00pageArr([1, 2, 3, 4, 5, 6, 7], 3); 
01// => 
02// [[1, 2, 3], [4, 5, 6], [7]]
可以用 reduce 来实现上述映射:
00var pageArr = (arr, n) => {
01    return arr.reduce((acc, cur, idx, its) => {
02        if (idx % n === 0) {
03            let subArr = its.slice(idx, idx + n).map(e => {
04                if (Array.isArray(e)) e = pageArr(e, n);     
05                return e; 
06            })
07
08            acc.push(subArr); 
09        }
10
11        return acc; 
12    }, []); 
13}
14
15pageArr([1, 2, [1, 2, 3, 4, 5], 4, 5, 6, 7], 3); 
16// => 
17// [  [1, 2, [  
18//              [1, 2, 3],
19//              [4, 5]
20//           ]
21//    ],
22//    [4, 5, 6],
23//    [7]
24// ]
子元素也可能是 数组,也得对他们 分页,因此要递归的处理这个问题。

# 数组铺平的非递归实现

补充下,这是最近王小姐考我的(19年1月底),具体实现如下:
00interface NestedArray<T> extends Array<T | NestedArray<T> > {}
01
02function flat<T>(arr: NestedArray<T>): T[] {
03    // 结果存在 result 
04    const result: Array<T> = []; 
05    // 我自己模拟一个栈
06    const stack: NestedArray<T> = []; 
07
08    while (true) {
09        if (stack.length) {
10            // 栈里有东西, 优先处理
11            const list = stack.pop(); 
12
13            if (Array.isArray(list)) {
14                if (list.length) {
15                    // 嗯 
16                    const [firstOfList, ...rest] = list; 
17
18                    if (Array.isArray(firstOfList)) {
19                        // 第一个被压入了,第二个要截断,因为 firstOfList 是 list 的第一个 
20                        stack.push(rest); 
21                        stack.push(firstOfList); 
22                    } else {
23                        stack.push(rest); 
24                        result.push(firstOfList); 
25                    }
26                } else {
27                    // 遇到了空数组。。。 不管了 
28                    // do nothing 
29                }
30            } else {
31                result.push(list); 
32            }
33        } else {
34            // 从 arr 里取出第一个元素出来。。。
35            const first = arr.shift(); 
36            
37            if (typeof first === 'undefined') {
38                // arr 没了,该结束了
39                break; 
40            } else {
41                // 其余的情况扔进栈
42                stack.push(first); 
43            }
44        }
45    }
46
47    return result; 
48}

# 对象的铺平和它的逆问题

实现 flat 处理下面的对象
00var o = {
01  "RuntimeSources": {
02    "flask-webapp": {
03      "eb-flask1.3": {
04        "s3url": ""
05      }
06    }
07  },
08  "DeploymentId": 4,
09  "Serial": 4
10}
使得 flat(o) 得到数组 ["RuntimeSources", "flask-webapp", "eb-flask1.3", "s3url", "DeploymentId", "Serial"]

我们可以借助 Object.keys 实现递归地实现 flat
00var flat = o => {
01    if (typeof o !== 'object') return []; 
02    
03    // 当层键名 
04    var keys = Object.keys(o); 
05    
06    return keys.reduce((acc, cur) => {
07        return acc.concat( flat(o[cur]) ); 
08    }, keys); 
09}
以下是简单的调用和返回结果:
00var hello = {
01    hello: '~', 
02    wolrd: {
03        nice: '~',
04        to: '~', 
05        meet: '~',
06        you: '~'
07    }
08}
09
10flat(hello); 
11// => 
12// ["hello", "wolrd", "nice", "to", "meet", "you"]

# 另外一个角度解决对象平铺问题

不论是 数组 还是 对象 都可以作为 JSON.stringify 的参数,并可以被转化成 JSON 字符串。
由此,铺平问题就被转化成字符串问题了。
调用处理刚刚的 hello : JSON.stringify(hello) 可以得到结果:
"{"hello":"~","wolrd":{"nice":"~","to":"~","meet":"~","you":"~"}}"
进一步观察得知:
JSON中,在冒号前的是键名
据此可以实现 flatJSON ,先写几个工具函数:
00// 把在 str 中的 willBeReplaced 替换为 toPlace
01var replaceAll = (str, willBeReplaced, toPlace) => {
02    return str.split(willBeReplaced).join(toPlace)
03}
04
05// 把在 str 的全部 willBeCut 替换成 ''
06var cut = (str, willBeCut) => {
07    return replaceAll(str, willBeCut, ''); 
08}
使用工具函数实现 flatJSON
00var flatJSON = o => {
01    var str = JSON.stringify(o); 
02    
03    let tokens = ['{', '}', ':', ','].reduce((acc, e) => {
04        // 给 '{' '}' ':' ',' 的两侧补上空格
05        return replaceAll(acc, e, ` ${e} `);
06
07        // 然后调用 split(' ') 把空格删掉,
08        // 最后把空字符串滤掉
09    }, str).split(' ').filter(e => e !== ""); 
10
11    // 从 tokens 取得结果 
12    // 冒号前的token就是键名 
13    return tokens.reduce((acc, cur, idx, its) => {
14        if (cur === ':'){
15            acc.push(its[idx - 1]); 
16        }
17        
18        return acc;
19    }, []).map(e => cut(e, '"', ''));  
20}
以下是调用
00flatJSON(hello); 
01// => 
02// ["hello", "wolrd", "nice", "to", "meet", "you"]

# 在上面这个角度上解决数组平铺问题

00var testArr = [1, 2, [3, 4, 5, [[9, 10, 11], [12, 13, 14, 15], 8]], [1, 23, 5, 6, [2, 3, 4, 5], [2]]]; 
01
02console.log(JSON.stringify(testArr)); 
03// => 
04// [1,2,[3,4,5,[[9,10,11],[12,13,14,15],8]],[1,23,5,6,[2,3,4,5],[2]]]
[1,2,[3,4,5,[[9,10,11],[12,13,14,15],8]],[1,23,5,6,[2,3,4,5],[2]]]
进而观察得出:
逗号前的是数组元素
类似解决刚刚上面的那个对象平铺的实现,需要得到 tokens 然后把逗号前的元素挑选出来就 ok 了。
00var flatArrJSON = arr => {
01    let str = JSON.stringify(arr); 
02
03    let tokens = ['[', ']', ','].reduce((acc, e) => {
04        // 给 '{' '}' ':' ',' 的两侧补上空格
05        return replaceAll(acc, e, ` ${e} `);
06
07        // 然后调用 split(' ') 把空格删掉,
08        // 最后把空字符串滤掉
09    }, str).split(' ').filter(e => e !== ""); 
10
11    return tokens.reduce((acc, cur, idx, its) => {
12        if (cur === ','){
13            acc.push(its[idx - 1]); 
14        }
15
16        return acc; 
17    }, []).filter(e => e !== ']'); // 过滤 ']' 因为数组字面量里可能会出现 '],' 这种写法 
18}
19
20flatArrJSON(testArr); 
21// => 
22// ["1", "2", "3", "4", "5", "9", "10", "12", "13", "14", "1", "23", "5", "6", "2", "3", "4"]




回到顶部