# 常用数据结构处理

# 1、扁平数组转换为树结构

这个可以处理具有唯一父节点的, 比如下面id为0的节点就是唯一的根节点。

function flatArrayToTree(data, rootParentId, parentKey, id = 'id') {
  let r = null;
  let tempObj = {};
  data.forEach(function (node) {
    tempObj[node[id]] = {
      ...node,
      children: tempObj[node[id]] && tempObj[node[id]].children
    };

    if (node[parentKey] === rootParentId) {
      r = tempObj[node[id]];
    } else {
      tempObj[node[parentKey]] = tempObj[node[parentKey]] || {};
      tempObj[node[parentKey]].children = tempObj[node[parentKey]].children || [];
      tempObj[node[parentKey]].children.push(tempObj[node[id]]);
    }
  }, Object.create(null));
  tempObj = null;
  return r;
}
const testData = [
  {
    id: 'A-2-1',
    name: 'A-2-1',
    _parentId: 'A-2'
  },
  {
    id: 'A',
    name: 'A',
    _parentId: '0'
  },
  {
    id: '0',
    name: 'root',
    _parentId: '-1'
  },

  {
    id: 'B',
    name: 'B',
    _parentId: '0'
  },
  {
    id: 'C',
    name: 'C',
    _parentId: '0'
  },
  {
    id: 'A-1',
    name: 'A-1',
    _parentId: 'A'
  },
  {
    id: 'A-2',
    name: 'A-2',
    _parentId: 'A'
  },

  {
    id: 'B-1',
    name: 'B-1',
    _parentId: 'B'
  },
  {
    id: 'C-1',
    name: 'C-1',
    _parentId: 'C'
  }
];

console.log(JSON.stringify(flatArrayToTree(testData, '-1', '_parentId'), null, 2));

不过很多时候后台返回的数据都是没有唯一根节点的, 可能是多个根节点, 那么我们稍微改造即可

function flatArrayToTree(data, root, parentKey, id = 'id') {
  let r = [];
  let tempObj = {};
  data.forEach(function (node) {
    tempObj[node[id]] = {
      ...node,
      children: tempObj[node[id]] && tempObj[node[id]].children
    };

    if (node[parentKey] === root) {
      r.push(tempObj[node[id]]);
    } else {
      tempObj[node[parentKey]] = tempObj[node[parentKey]] || {};
      tempObj[node[parentKey]].children = tempObj[node[parentKey]].children || [];
      tempObj[node[parentKey]].children.push(tempObj[node[id]]);
    }
  }, Object.create(null));
  tempObj = null;
  return r;
}
const testData = [
  {
    id: 'A-2-1',
    name: 'A-2-1',
    _parentId: 'A-2'
  },
  {
    id: 'A',
    name: 'A',
    _parentId: '0'
  },
  {
    id: '0',
    name: 'root',
    _parentId: '-1'
  },

  {
    id: 'B',
    name: 'B',
    _parentId: '0'
  },
  {
    id: 'C',
    name: 'C',
    _parentId: '0'
  },
  {
    id: 'A-1',
    name: 'A-1',
    _parentId: 'A'
  },
  {
    id: 'A-2',
    name: 'A-2',
    _parentId: 'A'
  },

  {
    id: 'B-1',
    name: 'B-1',
    _parentId: 'B'
  },
  {
    id: 'C-1',
    name: 'C-1',
    _parentId: 'C'
  },
  {
    id: 'root2id',
    name: 'root2',
    _parentId: '-1'
  },
  {
    id: 'D-1',
    name: 'D-1',
    _parentId: 'root2id'
  }
];

console.log(JSON.stringify(flatArrayToTree(testData, '-1', '_parentId'), null, 2));

再进阶一下的话, 给一个断言函数, 是不是root节点由使用者自己传入函数决定

function flatArrayToTree(data, rootNodePredicateFn, parentKey, id = 'id') {
  let r = [];
  data.forEach(function (node) {
    this[node[id]] = {
      ...node,
      children: this[node[id]] && this[node[id]].children
    };
    if (rootNodePredicateFn(node, parentKey)) {
      r.push(this[node[id]]);
    } else {
      this[node[parentKey]] = this[node[parentKey]] || {};
      this[node[parentKey]].children = this[node[parentKey]].children || [];
      this[node[parentKey]].children.push(this[node[id]]);
    }
  }, Object.create(null));
  return r;
}
      
      
flatArrayToTree(
  list,
  (node, parentKey) => {
    // 如果节点的parentKey键对应的值不存在 实为root根节点
    if (!node[parentKey]) {
      return true;
    }
    return false;
  },
  '_parentId',
  'id'
);

# 1.1 没有id父子关系的扁平数组转化为树

有时候会受到后台传递的扁平数组,按照层级排序过,没有利用id形成父子关系,而是利用层级顺序表示父子关系, 将这类数据转换为树结构。

下面的示例代码只兼容到深度为3的场景,具体有什么需求可以额外更改

function transSortedFlatArrayToTree(arr = [], levelKey = 'levelNo') {
  const tree = [];
  let level1Obj;
  let level2Obj;
  for (let i = 0, len = arr.length; i < len; i++) {
    const item = arr[i];
    const level = item[levelKey];
    if (level === 0) {
      level1Obj = { ...item };
      level1Obj.children = [];
      tree.push(level1Obj);
    }

    if (level === 1) {
      level2Obj = { ...item };
      level2Obj.children = [];
      level1Obj.children.push(level2Obj);
    }
    if (level === 2) {
      level2Obj.children.push({ ...item });
    }
  }
  return tree;
}

let arr = [
  {
    levelNo: 0,
    title: 'item0'
  },
  {
    levelNo: 1,
    title: 'item0_1'
  },
  {
    levelNo: 2,
    title: 'item0_1_1'
  },
  {
    levelNo: 2,
    title: 'item0_1_2'
  },
  {
    levelNo: 0,
    title: 'item1'
  },
  {
    levelNo: 1,
    title: 'item1_1'
  },
  {
    levelNo: 2,
    title: 'item1_1_1'
  },
  {
    levelNo: 2,
    title: 'item1_1_2'
  },
  {
    levelNo: 2,
    title: 'item1_1_3'
  }
];

console.log(JSON.stringify(transSortedFlatArrayToTree(arr), null, 2));

# 1.2 一维数组分类

一维数组根据父类名称分类。需要自己生成父节点,变成一个深度为2的树结构

如下的例子, 这种还是很多的,后端只会入库下面四条数据

const flatList = [
  { name: 'A-1', groupName: '分类A' },
  { name: 'A-2', groupName: '分类A' },
  { name: 'B-3', groupName: '分类B' },
  { name: 'B-4', groupName: '分类B' }
];

// 输出 
[
    {
        "groupName": "分类A",
	      "leaf": false,
        "title": "分类A",
        "children": [
            {
                "name": "A-1",
                "groupName": "分类A",
                "leaf": true,
                "title": "A-1"
            },
            {
                "name": "A-2",
                "groupName": "分类A",
                "leaf": true,
                "title": "A-2"
            }
        ]
    },
    {
        "groupName": "分类B",
      	"leaf": false,
        "title": "分类B",
        "children": [
            {
                "name": "B-3",
                "groupName": "分类B",
                "leaf": true,
                "title": "B-3"
            },
            {
                "name": "B-4",
                "groupName": "分类B",
                "leaf": true,
                "title": "B-3"
            }
        ]
    }
]
function groupFlatList(list = [], groupKey = 'groupName', cb = _ => _) {
  const result = [];
  const map = {};
  for (let i = 0, len = list.length; i < len; i++) {
    let item = list[i];
    const groupName = item[groupKey];
    if (!map[groupName]) {
      map[groupName] = [];
      let parentNode = {
        [groupKey]: groupName,
        children: map[groupName],
        leaf: false
      };
      parentNode = cb(parentNode);
      result.push(parentNode);
    }
    map[groupName].push(cb(item));
  }
  return result;
}

Usage

const groupTree = groupFlatList(flatList, 'groupName', item => {
  if (item.leaf === false) {
    // 定义一些父节点要用的属性
    item.title = item.groupName;
    return item;
  }
  item.leaf = true;
  item.title = item.name;
  return item;
});

# 2、树结构转换为扁平数组

这个就是树的遍历, 查看二叉树的各种遍历即可。稍加改造就可变成各种嵌套数据的遍历,无论是深度优先还是广度优先, 或是层序遍历。很容易即可得到一个扁平的数组。

举个例子吧还是

const data = {
  id: '0',
  name: 'root',
  children: [
    {
      id: 'A',
      name: 'A',
      _parentId: '0',
      children: [
        {
          id: 'A-1',
          name: 'A-1',
          _parentId: 'A'
        },
        {
          id: 'A-2',
          name: 'A-2',
          _parentId: 'A',
          children: [
            {
              id: 'A-2-1',
              name: 'A-2-1',
              _parentId: 'A-2'
            }
          ]
        }
      ]
    },
    {
      id: 'B',
      name: 'B',
      _parentId: '0',
      children: [
        {
          id: 'B-1',
          name: 'B-1',
          _parentId: 'B'
        }
      ]
    },
    {
      id: 'C',
      name: 'C',
      _parentId: '0',
      children: [
        {
          id: 'C-1',
          name: 'C-1',
          _parentId: 'C'
        }
      ]
    }
  ]
};

function deepFirstSearch(node) {
  const nodes = [];
  if (node != null) {
    var stack = [];
    stack.push(node);
    while (stack.length != 0) {
      let item = stack.pop();
      const children = item.children;
      nodes.push({
        ...item,
        children: null
      });
      if (children && children.length > 0) {
        for (let i = children.length - 1; i >= 0; i--) {
          stack.push(children[i]);
        }
      }
    }
  }
  return nodes;
}

console.log(deepFirstSearch(data));
/* 
[
  { id: '0', name: 'root', children: null },
  { id: 'A', name: 'A', _parentId: '0', children: null },
  { id: 'A-1', name: 'A-1', _parentId: 'A', children: null },
  { id: 'A-2', name: 'A-2', _parentId: 'A', children: null },
  { id: 'A-2-1', name: 'A-2-1', _parentId: 'A-2', children: null },
  { id: 'B', name: 'B', _parentId: '0', children: null },
  { id: 'B-1', name: 'B-1', _parentId: 'B', children: null },
  { id: 'C', name: 'C', _parentId: '0', children: null },
  { id: 'C-1', name: 'C-1', _parentId: 'C', children: null }
]
*/

以上是一个深度优先的遍历结果

# 3、过滤树结构

第二个参数为一个断言函数, 参数是当前节点

/**
 * 过滤树结构
 *
 * @param {*} tree 多节点的树结构
 * @param {boolean} [predicate=node => true] 断言函数
 * @param {*} [arr=[]]
 * @return {*} []
 */
function filterTree(tree, predicate = node => true, arr = []) {
  if (!tree || tree.length === 0) {
    return [];
  }
  for (let i = 0, len = tree.length; i < len; i++) {
    const node = tree[i];

    if (predicate(node)) {
      const newNode = { ...node, children: [] };
      arr.push(newNode);
      if (node.children && node.children.length > 0) {
        filterTree(node.children, predicate, newNode.children);
      }
    }
  }
  return arr;
}

如果只想过滤叶子节点,可以这样子写断言函数

const ret = filterTree(tree, node => {
  if (node.children && node.children.length > 0) {
    return true;
  }
  return node.prop > 0;
});

# 4、过滤固定层级的树结构,去除没有子节点的节点

有时候一些树结构,固定层级只有三级,但是如果一个一级或者二级节点没有三级子节点, 那么这个一级和二级的也都不要了。 实现比较傻瓜, 就是三个循环

function deleteTree(tree, predicate = node => false, childKey = 'children') {
  for (let i = tree.length - 1; i >= 0; i--) {
    const level1Item = tree[i];
    const level1Children = level1Item[childKey];
    if (level1Children && level1Children.length > 0) {
      for (let j = level1Children.length - 1; j >= 0; j--) {
        const level2Children = level1Children[j][childKey];
        if (level2Children && level2Children.length > 0) {
          for (let k = level2Children.length - 1; k >= 0; k--) {
            const level3Item = level2Children[k];
            if (predicate(level3Item)) {
              level2Children.splice(k, 1);
            }
          }
        }
        if (!level2Children || level2Children.length === 0) {
          level1Children.splice(j, 1);
        }
      }
    }
    if (!level1Children || level1Children.length === 0) {
      tree.splice(i, 1);
    }
  }
  return tree;
}
console.log(deleteTree(tree, node => !node.name));
上次更新: 4/1/2025, 9:33:10 AM