js递归学习

递归:就是函数自己调用自己本身,或者在自己函数调用的下级函数中调用自己。
递归的两个必要因素
递归方程,递归结束条件
递归算法的核心:

  • 在有限次可预见性结果中,找到结果与上一次结果之间的关系
  • 梳理清楚本次结果和上一次结果的关系有哪些方面或是因素
  • 在草稿纸上写出前几次的结果,或者画图,更容易找到规律,这种规律实际上就是递归方程

递归的步骤

  1. 寻找递推规律的关系
  2. 判断递推关系的临界条件
  3. 将递推关系的结构转换为递归体
  4. 将临界条件加入递归体中

经典案例

1. 求和

求 1-100的和
分析:计算 1 + 2 + … + 99 + 100 的和,我们可以看成 1-n 的和
假设 n 为 3,即 和为 3 + (3-1) + ((3-1) -1),多观察不难发现递推关系 n + (n-1)的递归体
找到临界条件 1 的时候,和为1

function sum(n) {
  if (n === 1) return 1
  return sum(n - 1) + n
}

2. 斐波那契数列

1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, … 求第 n 项
分析:第1项和第2项都为1;第3项为第2项+第1项的和;第4项为第3项+第2项的和,可以看出递推关系:第n项为第n-1项+第n-2项的和
临界条件是:第1项和第2项都为1

function febo(n) {
  if ([1, 2].includes(n)) return 1
  return febo(n - 1) + febo(n - 2)
}

3.爬楼梯

js 递归案例:假设楼梯有 n 个台阶,每次可走1个或2个台阶,问走完这 n 个台阶有几种走法
分析:假设n为1,只有1种走法,n为2,只有2种走法,n为3有3种走法,n为4有5种走法,n为5有8种走法,…得出递归关系
临界条件就是n 为1和2时

function climb(n) {
  if (n === 1) return 1
  if (n === 2) return 2
  return climb(n - 1) + climb(n - 2)
}

4.深拷贝

克隆一个对象返回一个对象

function clone(obj) {
  let temp = {}
  for (let i in obj) {
    if (typeof obj[i] === 'object') {
      temp[i] = clone(obj[i])
    } else {
      temp[i] = obj[i]
    }
  }
  return temp
}

5.求阶乘

计算 n 的阶乘
分析:临界条件当n为1时,值为1
假设n为2,即21,n为3,即 32*1,得出递归体:n * fn(n-1)

function fn(n) {
  if (n === 1) return 1
  return n * fn(n - 1)
}

多级遍历测试

测试数据

let data = [
  {
    name: "所有物品",
    children: [
      {
        name: "水果",
        children: [{name: "苹果", children: [{name: '青苹果'}, {name: '红苹果'}]}]
      },
      {
        name: '主食',
        children: [
          {name: "米饭", children: [{name: '北方米饭'}, {name: '南方米饭'}]}
        ]
      },
      {
        name: '生活用品',
        children: [
          {name: "电脑类", children: [{name: '联想电脑'}, {name: '苹果电脑'}]},
          {name: "工具类", children: [{name: "锄头"}, {name: "锤子"}]},
          {name: "生活用品", children: [{name: "洗发水"}, {name: "沐浴露"}]}
        ]
      }
    ]
  }
]

功能要求:拿到每一级的最后一个数据的name值,即

['青苹果', '红苹果', '北方米饭', '南方米饭', '联想电脑', '苹果电脑', '锄头','锤子', '洗发水', '沐浴露']

递归实现:

function fn (data, arr = []) {
  data.forEach(item => {
    if (item.children && item.children.length > 0) {
      fn(item.children, arr)
    } else {
      arr.push(item.name)
    }
  })
  return arr
}

需求:需要依次输出1, 2, 3, 4, 5,每个输出中间间隔1s

function fn(i) {
  console.log(i)
  setTimeout(() => {
    if (i > 4) return
    i++
    return fn(i)
  }, 1000)
}

fn(1)

树结构节点排序并更新parent_id

树结构数据

const data = [
  {
    id: 1,
    name: '根目录1',
    parent_id: 0,
    children: [
      {
        id: 2,
        name: '子目录1-1',
        parent_id: 1
      },
      {
        id: 3,
        name: '子目录1-2',
        parent_id: 1
      },
    ]
  },
  {
    id: 4,
    name: '根目录2',
    parent_id: 0,
    children: [
      {
        id: 5,
        name: '子目录2-1',
        parent_id: 8
      },
      {
        id: 6,
        name: '子目录2-2',
        parent_id: 4
      },
    ]
  },
  {
    id: 7,
    name: '根目录3',
    parent_id: 0,
  },
  {
    id: 8,
    name: '根目录4',
    parent_id: 0,
    children: [
      {
        id: 9,
        name: '子目录4-1',
        parent_id: 8,
        children: [
          {
            id: 10,
            name: '子目录4-1-1',
            parent_id: 9
          },
        ]
      },
    ]
  },
]

递归实现

function recursive (data, parent_id = 0, arr = []) {
  if (data?.length) {
    for (let i = 0; i < data.length; i++) {
      arr.push({
        id: data[i].id,
        name: data[i].name,
        sort: i,
        parent_id: parent_id
      })
      if (data[i].children?.length) {
        recursive(data[i].children, data[i].id, arr)
      }
    }
  }
  return arr
}

给树结构的每个对象添加属性

resolveTreeData(treeData) {
  const tree = treeData.map((item) => {
    item.flag= true;
    if (Object.prototype.hasOwnProperty.call(item, "children")) {
      item.children && this.resolveTreeData(item.children);
    }
    return item;
  })
  return tree;
}

在树结构中找到当前节点的父节点

// data:树结构,id:当前节点的id,parent:父节点,第一次传空
findParentNode(data, id, parent) {
  for (let item of data) {
    if (item.id === id) {
      return parent || item;
    }
    if (item?.children?.length) {
      let node = this.findParentNode(item.children, id, item);
      if (node) {
        return node;
      }
    }
  }
  return null;
},

递归获取当前菜单的code和name

// 获取当前选中的菜单的rs_code和name
function getMenuInfo() {
  const frame = window.location.hash.substring(1); // 页面路径
  const menuList = [.....];
  console.log(menuList)
  if (menuList?.length) {
    let obj = {
      menu_code: "",
      menu_name: "",
    }
    const resCall = (data) => {
      return data.find((item) => {
        if (item.frame === frame) {
          obj.menu_code = item.rs_code;
          obj.menu_name = item.name;
        }
        if (item.tabs?.length) {
          resCall(item.tabs);
        }
      })
    }
    resCall(menuList);
    return obj;
  }
}

menuList的数据结构
在这里插入图片描述

递归给树结构的节点改名字

resolveMenu(menu) {
  return menu.map((item) => {
    let tabs = item.sub_list;
    if (tabs?.length > 0) {
      tabs = this.resolveMenu(tabs);
    }
    return Object.assign({}, item, {
      name: item.rs_cn_name,
      frame: item.rs_url,
      sub_list: null,
      tabs,
    });
  });
},

原结构
在这里插入图片描述

现结构
在这里插入图片描述

根据叶子节点查父节点
在这里插入图片描述
在这里插入图片描述

参数为一个一维数组,值是树结构的id列表,函数功能是树结构中的父节点如果在参数列表中,需要判断子节点是否全部在参数列表中,如果子节点不全在参数列表中,需要将当前父节点从参数列表中删除,最后返回参数列表

// 它接受两个参数:一个一维数组arr和一个树形结构数据treeData。其中,arr表示树结构的id列表,treeData包含整个树形结构的数据。
// 函数的主要逻辑是遍历arr中的每一个元素,并根据其对应的节点判断子节点是否全部在参数列表中。如果子节点不全在参数列表中,则将当前父节点从参数列表中删除,并继续检查下一个节点。最后返回更新后的参数列表。
function removeIncompleteParents(arr, treeData) {
  const idSet = new Set(arr);
  let index = 0;
  while (index < arr.length) {
    const node = findNodeById(treeData, arr[index]);
    if (node && node.children && node.children.length > 0) {
      // 判断子节点是否全部在参数列表中
      const childrenIds = node.children.map(child => child.id);
      const allChildrenIncluded = childrenIds.every(id => idSet.has(id));
      if (!allChildrenIncluded) {
        // 如果子节点不全在参数列表中,将当前父节点从参数列表中删除
        idSet.delete(node.id);
        arr.splice(index, 1);
        continue; // 继续检查下一个节点
      }
    }
    index++;
  }
  return Array.from(idSet);
}

// 为了方便查找节点,定义d的辅助函数。它接受两个参数:一个树形结构数据data和一个节点的id值id,返回对应的节点对象。函数使用递归的方式进行查找,首先比较当前节点的id值是否与目标值相等,如果相等就返回该节点对象;否则,如果当前节点有子节点,则继续递归查找子节点。如果找不到匹配的节点,则返回null。
function findNodeById(data, id) {
  for (let i = 0; i < data.length; i++) {
    if (data[i].id === id) {
      return data[i];
    } else if (data[i].children && data[i].children.length > 0) {
      const node = findNodeById(data[i].children, id);
      if (node) {
        return node;
      }
    }
  }
  return null;
}

const data = [
  {
    id: '1',
    name: 'Node 1',
    children: [
      {
        id: '2',
        name: 'Node 1-1',
        children: [
          { id: '3', name: 'Node 1-1-1' },
          { id: '4', name: 'Node 1-1-2' },
        ],
      },
      {
        id: '5',
        name: 'Node 1-2',
        children: [
          { id: '6', name: 'Node 1-2-1' },
          { id: '7', name: 'Node 1-2-2' },
        ],
      },
    ],
  },
];

const arr = ['1', '2', '4', '7'];
const updatedArr = removeIncompleteParents(arr, data);
console.log(updatedArr) // ['4', '7']

修改树结构的多个字段名

function renameTreeFields(tree, fieldMap) {
  for (let oldFieldName in fieldMap) {
    if (fieldMap.hasOwnProperty(oldFieldName)) {
      const newFieldName = fieldMap[oldFieldName];
      
      if (tree !== null && tree.hasOwnProperty(oldFieldName)) {
        Object.defineProperty(tree, newFieldName,
          Object.getOwnPropertyDescriptor(tree, oldFieldName));
        delete tree[oldFieldName];
      }
  
      if (tree !== null && typeof tree === 'object') {
        for (let key in tree) {
          if (tree.hasOwnProperty(key)) {
            renameTreeFields(tree[key], fieldMap);
          }
        }
      }
    }
  }
}
// 示例
const tree = {
  id: 1,
  name: 'root',
  children: [
    {
      id: 2,
      name: 'child1'
    },
    {
      id: 3,
      name: 'child2',
      children: [
        {
          id: 4,
          name: 'grandchild1'
        }
      ]
    }
  ]
};

renameTreeFields(tree, {name: 'label', id: 'key'});
console.log(tree);

/* 输出结果:
{
  key: 1,
  label: 'root',
  children: [
    {
      key: 2,
      label: 'child1'
    },
    {
      key: 3,
      label: 'child2',
      children: [
        {
          key: 4,
          label: 'grandchild1'
        }
      ]
    }
  ]
}
*/

树结构中找到指定的数据

树结构如下

const arr = [
  { "text": "首页", "link": "/", "collapse": true },
  {
    "text": "Vue教程",
    "link": "/vue",
    "collapse": true,
    "type": "folder",
    "children": [
      { "text": "Vue3基础入门", "link": "/vue/vue3", "collapse": true, "type": "file" }
    ]
  },
  { "text": "vite配置", "link": "/vite/config", "collapse": true, "type": "file" },
  {
    "text": "站点设置",
    "link": "/site",
    "collapse": true,
    "children": [
      {
        "text": "基本信息设置",
        "link": "11",
        "collapse": true,
        "children": [
          { "text": "如何切换站点主题?", "link": "/site/change" },
          { "text": "如何切换站点主题?", "link": "/site/exchalenge" },
        ]
      },
      {
        "text": "主题样式设置",
        "link": "55"
      }
    ]
  }
]
案例1:找到树结构中指定的link值的对象

解析:

// 方法1
const handerData = (arr, link) => {
  for(let key of arr) {
    if (key.link === link){
      return key;
    }
    if (key?.children?.length) {
      const result = handerData(key.children, link)
      if (result) {
        return result
      }
      // 下面的写法是错误的,相当于return return xx
      // return handerData(key.children, link)
    }
  }
  return null
}
console.log(handerData(arr, "22"))

// 方法2,存在可能变量污染的问题
let res
const handerData = (arr, link) => {
  arr.map((i) => {
    if (i?.link === link) {
      res = i;
    }
    if (i?.children?.length) {
      handerData(i.children, link)
    }
  })
}
handerData(arr, "22")
console.log(res)


// 方法3
const handerData = (arr, link) => {
  let res
  const findLink = (arr, link) => {
    arr.map((i) => {
      if (i?.link === link) {
        res = i;
      }
      if (i?.children?.length) {
        findLink(i.children, link)
      }
    })
  }
  findLink(arr, link)
  return res
}

console.log(handerData(arr, "22"))
案例2:找到树结构中指定link和其父对象组成的一维数组,类似于面包屑的数组
function findItemByLink(data, link) {
  for (let i = 0; i < data.length; i++) {
    const item = data[i];
    if (item.link === link) {
      return [item];
    } else if (item.children) {
      const result = findItemByLink(item.children, link);
      if (result) {
        return [item, ...result];
      }
    }
  }
  return null;
}
console.log(findItemByLink(arr, "/site/change"))
// 输出结果
[
  {
    text: '站点设置',
    link: '/site',
    collapse: true,
    children: [ [Object], [Object] ]
  },
  {
    text: '基本信息设置',
    link: '11',
    collapse: true,
    children: [ [Object], [Object] ]
  },
  { text: '如何切换站点主题?', link: '/site/change' }
]

给树添加层级


    // 给树的每个节点添加属性
    resolveTreeData(treeData, layer) {
      const tree = treeData.map((item) => {
        if (!item.item_weight) {
          item.item_weight = "";
        }
        item.layer = layer;
        if (Object.prototype.hasOwnProperty.call(item, "children")) {
          item.children && this.resolveTreeData(item.children, layer + 1);
        }
        return item;
      });
      return tree;
    },
    let returnData = this.resolveTreeData(data, 1);

获取树结构的最深层级

 // 获取树的最深层级
    getTreeDepth(tree) {
      if (!tree || !Array.isArray(tree)) {
        return 0;
      }
      
      let maxDepth = 0;
      
      for (let i = 0; i < tree.length; i++) {
        const node = tree[i];
        const depth = getTreeDepth(node.children) + 1;
        maxDepth = Math.max(maxDepth, depth);
      }
      
      return maxDepth;
    },
  • 4
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值