【算法】前端遇到的廣度/深度優先搜索

前言

在面試或者技術社區衝浪的時候,一不當心就會看到深度優先搜索、廣度優先搜索這兩個概念,這一次在項目中一個需求用到了相關的知識,故此在這裏經過理論+實際來總結一下。node

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. 深度優先搜索

深度優先搜索(depth first search),從圖中也能夠看出來,是從根節點開始,沿樹的深度進行搜索,儘量深的搜索分支。當節點所在的邊都已經搜多過,則回溯到上一個節點,再搜索其他的邊。數組

深度優先搜索採用棧結構,後進先出。iview

算法:ui

js 遞歸實現和非遞歸實現:spa

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),從圖中也能夠看出來,是從根節點開始,沿樹的寬度進行搜索,若是全部節點都被訪問,則算法停止。code

廣度優先搜索採用隊列的形式,先進先出。cdn

js 實現:blog

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 的 tree 以及 cascader 組件)。

簡單來講就是從樹中找到某個節點,並返回節點的路徑。

tree

cascader

4.1 深度優先搜索

算法:

  1. 首先將根節點放入棧中(示例中沒有根節點,直接將平行節點置入);
  2. 從棧中取出第一個節點,存儲並檢驗是否爲目標;
    • 若是找到目標,則返回存儲路徑;
    • 不然將當前節點的直接子節點推入棧中;
  3. 重複步驟2;
  4. 若是不存在未搜索的直接子節點,彈出存儲節點中的最後一個節點
    • 若是存儲的節點爲空,判斷棧中是否還有其餘節點,重複步驟2
    • 不然結束搜索,報告結果
  5. 獲取存儲節點當中最後一個節點,彈出第一個直接子節點;
  6. 若是移除後的當前最後節點還存在直接子節點,重複步驟2(使用當前節點的第一個直接子節點);
  7. 重複步驟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 廣度優先搜索

算法:

  1. 首先將根節點放入隊列(示例中沒有根節點,直接將平行節點置入);
  2. 從隊列中取第一個節點,並檢驗是否爲目標;
    • 若是找到目標,結束搜索並遞歸查找 parent 存儲,返回路徑
    • 不然將它全部未檢驗過的直接子節點(須要路徑結果,給直接子節點設置標誌 parent)加入到隊列
  3. 若隊列爲空,表示全部節點都已經搜索過且無目標,結束搜索回傳空;
  4. 重複步驟二;

代碼以下:

// 廣度優先搜索
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;
};
複製代碼

總結

通常來講,能用深度優先搜索的場景也能用廣度優先搜索,從大腦的思考方式來講,深度優先搜索更符合人們的認知行爲。與此同時,當節點足夠複雜,能夠考慮使用迭代深化深度優先搜索(重複運行一個有深度限制的深度優先搜索),時間複雜度與廣度優先搜索一致,而空間複雜度遠優。

相關文章
相關標籤/搜索