js - 深拷贝非递归实现

ru shui 2021-10-28 Javascript
  • Javascript
About 4 min

# 深度优先 DFS

对于二叉树的深度优先算法,遍历顺序如下图所示:

img

对应递归版本的代码实现如下:

function dfs(root) {
  visit(root)
  if (root.left) {
    dfs(root.left)
  }
  if (root.right) {
    dfs(root.right)
  }
}
1
2
3
4
5
6
7
8
9

对于非递归版本而言,我们需要借助栈来实现:

function dfsNonRecursive(root) {
  const stack = [root]
  while (stack.length) {
    const node = stack.pop()
    visit(node)
    if (node.right) {
      stack.push(node.right)
    }
    if (node.left) {
      stack.push(node.left)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

注意:这里需要先将右结点推入栈中,再将左节点推入栈中,这与栈的工作原理有关,也就是先进后出。

img

了解了深度遍历的原理后,我们就可以开始写我们的深拷贝代码了。 由于 JavaScript 中的对象子节点可能不只有一个,所以我们需要 将所有的子节点推入到栈中,而且同上面二叉树的深度遍历类似,我们 需要先将右结点推入栈中。

参考下面的例子:

img

我们要推入栈的顺序如下:

img

a 出栈时,由于 obj.a 为引用类型,根据深度优先算法, 我们需要将 a 的子节点也推入栈中:

img

注意:还是需要先将右结点推入到栈中。

为了方便操作,我们用 src[key] 保持当前要拷贝的对象,具体代码如下:

function isObject(obj) {
  return typeof obj === 'object' && obj !== null
}

/**
 * 深度优先非递归实现
 * 这里只针对 Array 和 Object 的拷贝
 * @param {*} obj
 */
export function deepCopyDFS(obj) {
  // 对于非对象类型,直接返回
  if (!isObject(obj)) {
    return obj
  }

  // Map<src, dest>:为了避免循环引用
  const seen = new Map()
  seen.set(obj, obj)
  const result = Array.isArray(obj) ? [] : {}
  const stack = [[obj, result, null]]

  // 以上面的对象为了: obj = {a: {a1: 1, a2: 2}, b: 1, c: {c1: 1}}
  while (stack.length) {
    const [src, dest, key] = stack.pop()
    // 相当于遍历根结点: 此时栈中的数据为
    //          top
    // |                       |
    // |                       |
    // |   [src, dest, 'a']       |
    // |   [src, dest, 'b']       |
    // |   [src, dest, 'c']       |
    // ------------------------
    if (key === null) {
      const keys = Object.keys(src)
      for (let i = keys.length - 1; i >= 0; i--) {
        stack.push([src, dest, keys[i]])
      }

      // 对于 key 的判断,例如:obj.a
    } else if (isObject(src[key])) {
      // 需要判断是否已经遍历过,避免循环引用
      if (seen.has(src[key])) {
        dest[key] = seen.get(src[key])
      } else {
        // 对于引用类型,再次将子节点推入栈中
        dest[key] = Array.isArray(src[key]) ? [] : {}
        stack.push([src[key], dest[key], null])
      }
    } else {
      // 对于基本数据类型,直接添加即可
      dest[key] = src[key]
    }
  }
  return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

需要注意的是,将所有的子节点推入到栈中对应的代码为 for (let i = keys.length - 1; i >= 0; i--) {}

# 广度优先 BFS

广度优先算法其实和深度优先算法类似,只不过我们采用的队列去实现,而不是栈。 同时,子节点入队的顺序也将改为从左结点开始。对应代码如下,注意高亮部分:






 





 
 
 














export function deepCopyBFS(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj
  }
  const result = Array.isArray(obj) ? [] : {}
  const queue = [[obj, result, null]]
  const seen = new Map()
  seen.set(obj, obj)
  while (queue.length) {
    const [src, dest, key] = queue.shift()
    if (key === null) {
      Object.keys(src).forEach(key => {
        queue.push([src, dest, key])
      })
    } else if (isObject(src[key])) {
      if (seen.has(src[key])) {
        dest[key] = seen.get(src[key])
      } else {
        dest[key] = Array.isArray(src[key]) ? [] : {}
        queue.push([src[key], dest[key], null])
      }
    } else {
      dest[key] = src[key]
    }
  }
  return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 测试

import { deepCopyBFS, deepCopyDFS } from '.'
describe('deepCopy', () => {
  let keys = []
  const a = new Proxy(
    { a1: 1, a2: 2 },
    {
      get(target, key) {
        if (!keys.includes(key)) keys.push(key)
        return Reflect.get(target, key)
      },
    },
  )
  const b = 1
  const c = new Proxy(
    { c1: 1 },
    {
      get(target, key) {
        if (!keys.includes(key)) keys.push(key)
        return Reflect.get(target, key)
      },
    },
  )
  const proxy = new Proxy(
    { a, b, c },
    {
      get(target, key) {
        if (!keys.includes(key)) keys.push(key)
        return Reflect.get(target, key)
      },
    },
  )

  beforeEach(() => {
    keys = []
  })

  describe('DFS non-recursive implementation', () => {
    it('should perform the correct deepCopy function', () => {
      // TODO: add test
      expect(deepCopyDFS(null)).toEqual(null)
      function func() {}
      expect(deepCopyDFS(func)).toEqual(func)
      expect(deepCopyDFS([1, 2, 3, { a: 1 }])).toEqual([1, 2, 3, { a: 1 }])
      const obj1 = { a: 1, b: 1, c: [1, 2] }
      const copiedObj1 = deepCopyDFS(obj1)
      expect(copiedObj1).toEqual(obj1)
      copiedObj1.c.push(3)
      expect(copiedObj1).not.toEqual(obj1)
    })

    it('should avoid cycle reference', () => {
      const obj1 = { name: 'obj1' }
      const obj2 = { name: 'obj2' }
      // obj1.a = obj2,  obj2.a = obj1
      // cycle reference
      obj1.a = obj2
      obj2.a = obj1
      const copiedObj1 = deepCopyDFS(obj1)
      expect(copiedObj1).toEqual(obj1)
    })

    it('should perform DFS copy', () => {
      deepCopyDFS(proxy)
      expect(keys).toEqual(['a', 'a1', 'a2', 'b', 'c', 'c1'])
    })
  })

  describe('BFS non-recursive implementation', () => {
    it('should perform the correct deepCopy function', () => {
      // TODO: add test
      expect(deepCopyBFS(null)).toEqual(null)
      function func() {}
      expect(deepCopyBFS(func)).toEqual(func)
      expect(deepCopyBFS([1, 2, 3, { a: 1 }])).toEqual([1, 2, 3, { a: 1 }])
      const obj1 = { a: 1, b: 1, c: [1, 2] }
      const copiedObj1 = deepCopyBFS(obj1)
      expect(copiedObj1).toEqual(obj1)
      copiedObj1.c.push(3)
      expect(copiedObj1).not.toEqual(obj1)
    })

    it('should avoid cycle reference', () => {
      const obj1 = { name: 'obj1' }
      const obj2 = { name: 'obj2' }
      // obj1.a = obj2,  obj2.a = obj1
      // cycle reference
      obj1.a = obj2
      obj2.a = obj1
      const copiedObj1 = deepCopyBFS(obj1)
      expect(copiedObj1).toEqual(obj1)
    })

    it('should perform DFS copy', () => {
      deepCopyBFS(proxy)
      expect(keys).toEqual(['a', 'b', 'c', 'a1', 'a2', 'c1'])
    })
  })
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
Last update: October 28, 2021 17:20
Contributors: Laishuxin