предисловие
Во время интервью или серфинга в техническом сообществе вы случайно увидите понятия поиска в глубину и поиска в ширину, На этот раз соответствующие знания используются в одном из требований проекта, поэтому я обобщу его здесь через теорию + практика один раз.
1. Пример
Следующая картинка может сравнить и понять разницу между ними:
Чтобы облегчить операцию позже, мы сначала определим набор параллельных узлов следующим образом, предполагая общий корневой узел:
const root = [
{
id: '1',
children: [
{
id: '1-1',
children: [{ id: '1-1-1' }, { id: '1-1-2' }],
},
{
id: '1-2',
children: [{ id: '1-2-1' }, { id: '1-2-2' }],
},
],
},
{
id: '2',
children: [
{
id: '2-1',
children: [{ id: '2-1-1' }, { id: '2-1-2' }],
},
{
id: '2-2',
children: [{ id: '2-2-1' }, { id: '2-2-2' }],
},
],
},
{
id: '3',
children: [
{
id: '3-1',
children: [{ id: '3-1-1' }, { id: '3-1-2' }],
},
{
id: '3-2',
children: [{ id: '3-2-1' }, { id: '3-2-2' }],
},
],
},
];
const target = '2-2-2';
2. Поиск в глубину
Поиск в глубину (deep first search), как также видно из рисунка, начинается с корневого узла, ищет по глубине дерева, и ищет ветви как можно глубже. Когда ребро, в котором расположен узел, было найдено, вернитесь к предыдущему узлу, а затем выполните поиск оставшихся ребер.
Поиск в глубину использует структуру стека, последний вошел первым.
алгоритм:
js рекурсивная реализация и нерекурсивная реализация:
const depthFirstSearchWithRecursive = source => {
const result = []; // 存放结果的数组
// 递归方法
const dfs = data => {
// 遍历数组
data.forEach(element => {
// 将当前节点 id 存放进结果
result.push(element.id);
// 如果当前节点有子节点,则递归调用
if (element.children && element.children.length > 0) {
dfs(element.children);
}
});
};
// 开始搜索
dfs(source);
return result;
};
const depthFirstSearchWithoutRecursive = source => {
const result = []; // 存放结果的数组
// 当前栈内为全部数组
const stack = JSON.parse(JSON.stringify(source));
// 循环条件,栈不为空
while (stack.length !== 0) {
// 最上层节点出栈
const node = stack.shift();
// 存放节点
result.push(node.id);
// 如果该节点有子节点,将子节点存入栈中,继续下一次循环
const len = node.children && node.children.length;
for (let i = len - 1; i >= 0; i -= 1) {
stack.unshift(node.children[i]);
}
}
return result;
};
3. Поиск в ширину
Поиск в ширину (breadth first search), который также можно увидеть из рисунка, начинается с корневого узла и ведет поиск по ширине дерева, при посещении всех узлов алгоритм прерывается.
Поиск в ширину использует форму очереди «первым пришел — первым вышел».
js-реализация:
const breadthFirstSearch = source => {
const result = []; // 存放结果的数组
// 当前队列为全部数据
const queue = JSON.parse(JSON.stringify(source));
// 循环条件,队列不为空
while (queue.length > 0) {
// 第一个节点出队列
const node = queue.shift();
// 存放结果数组
result.push(node.id);
// 当前节点有子节点则将子节点存入队列,继续下一次的循环
const len = node.children && node.children.length;
for (let i = 0; i < len; i += 1) {
queue.push(node.children[i]);
}
}
return result;
};
4. Практическое применение
Практическое применение определенно больше, чем то, с которым я сталкивался, например, я буду использовать свой собственный опыт в качестве примера.
Требования следующие:
Можно создавать уровни организации, а под большим уровнем есть маленькие уровни, которые можно создавать бесконечно. При этом при редактировании должны быть перечислены все родительские уровни (компоненты дерева iview и каскада).
Проще говоря, это найти узел в дереве и вернуть путь к узлу.
4.1 Поиск в глубину
алгоритм:
- Сначала поместите корневой узел в стек (в примере нет корневого узла, а параллельные узлы размещены напрямую);
- Возьмите первый узел из стека, сохраните и проверьте, является ли он целевым;
- Если цель найдена, вернуть путь к хранилищу;
- В противном случае поместите в стек непосредственных потомков текущего узла;
- Повторите шаг 2;
- Если нет прямых дочерних элементов, которые не были найдены, извлеките последний узел в узле хранения.
- Если сохраненный узел пуст, определите, есть ли в стеке другие узлы, повторите шаг 2.
- В противном случае завершите поиск и сообщите о результатах.
- Получить последний узел в узле хранения и открыть первый прямой дочерний узел;
- Если после удаления все еще есть прямой дочерний узел текущего последнего узла, повторите шаг 2 (используйте первый прямой дочерний узел текущего узла);
- Повторите шаг 2 (используя текущий узел);
// 深度优先搜索
const findPathByDepthFirstSearch = source => {
const stack = JSON.parse(JSON.stringify(source));
const result = [];
const dfs = data => {
// 保存当前节点
// (在路口洒下面包屑)
result.push(data);
// 当前节点的值为真,则返回路径
//(如果这个路口的终点是生门,通过记录的面包屑就找到了路径)
if (data.id === target) {
return result.map(r => r.id);
}
// 如果当前节点有子节点,则继续查找子节点
//(如果这个路口后面还有分叉路口,就先去第一个分叉路口下的第一条路)
if (data.children && data.children.length > 0) {
return dfs(data.children[0]);
}
// 最后一个节点的值为假,弹出路径中的该节点
//(最后一个路口是死路,清理最后一个路口的面包屑)
result.pop();
// 如果路径数组为空,则判断源节点是否还有待搜索的节点
//(如果面包屑都清空了,也就是回到了原点,那就看看还有没有别的路口)
if (result.length === 0) {
return stack.length > 0 ? dfs(stack.shift()) : result;
}
// 获取路径中最后一个节点,是当前节点的父节点
//(去撒有面包屑的最后一个路口看看,当前路口的面包屑已经在上面被清理了)
const lastNode = result[result.length - 1];
// 弹出路径中最后节点中的第一个子节点(前面已经查找失败了)
//(当前路子不够野【在上面已经试过这条路,是死路】)
lastNode.children.shift();
// 查找最后一个有效节点的下一个子节点(前一个被 shift 了)
//(如果这个路口下还有其他没尝试过的路,从第一条(实际是下一条了)开始尝试)
if (lastNode.children.length > 0) {
return dfs(lastNode.children[0]);
}
// 最后节点下的子节点全部尝试查找失败,返回上一个节点查找
//(这个路口如果没有其他路了,清理面包屑且去上一个路口的第二条路看看【本条是第一条路,已经走过了】)
return dfs(result.pop());
};
// 开始找路
return dfs(stack.shift());
};
4.2 Поиск в ширину
алгоритм:
- Сначала поместите в очередь корневой узел (в примере нет корневого узла, а параллельные узлы размещаются напрямую);
- Возьмите первый узел из очереди и проверьте, является ли он целевым;
- Если цель найдена, завершить поиск и рекурсивно найти родительское хранилище, возвращая путь
- В противном случае добавьте все его непроверенные прямые дочерние элементы (требуется результат пути, установите родительский флаг для прямых дочерних элементов) в очередь
- Если очередь пуста, это означает, что были просмотрены все узлы и цели нет, и в конце поиска возвращается пустой результат;
- Повторите шаг два;
код показывает, как показано ниже:
// 广度优先搜索
const findPathByBreadthFirstSearch = source => {
let result = [];
let queue = JSON.parse(JSON.stringify(source));
while (queue.length > 0) {
// 遍历队列(队列会动态增加)
//(从第一个路口开始试探)
for (let i = 0; i < queue.length; i += 1) {
// 获取当前队列的一项
// (这是一个路口)
const node = queue[i];
// 判断节点是否为目标节点
//(路口是不是生门?)
if (node.id === target) {
// 队列清空
//(已经找到生门,不用再接着找了)
queue = [];
// 通过 parent 一层层查找路径
//(从这个路口通过面包屑【parent】找归途,直到找到回家的路)
return (function findParent(data) {
result.unshift(data.id);
if (data.parent) {
return findParent(data.parent);
}
return result;
})(node);
}
// 节点有子节点,设置子节点的 parent 为当前节点,推入队列
//(这个路口下还有其他路,先记住这个这个路口下的路是属于现在这个路口的【parent】
// 然后去下一个路口,按顺序来试)
if (node.children && node.children.length > 0) {
queue.push(
...node.children.map(leaf => {
leaf.parent = node;
return leaf;
})
);
}
}
}
return result;
};
Суммировать
Вообще говоря, поиск в глубину также можно использовать для поиска в ширину, С точки зрения мышления мозга, поиск в глубину больше соответствует когнитивному поведению людей. В то же время, когда узлы достаточно сложны, чтобы рассмотреть возможность использованияИтеративный поиск в глубину с углублением(неоднократно выполняя поиск в глубину с ограничением по глубине), временная сложность такая же, как и при поиске в ширину, а пространственная сложность намного выше.