【小基礎】數據結構與算法——JS二叉搜索樹

前言

大學的東西都忘的差很少了吧?下面咱們一塊兒用js簡單複習一下大學裏《數據結構與算法》中的樹。 本文僅使用js實現二叉樹,實際工做中可能並用不到(瞎說什麼大實話),可是面試挺喜歡問的,畢竟這是計算機相關專業的基礎嘛。同時,代碼裏用了不少遞歸,這對後期對代碼量優化仍是頗有幫助的。 虛擬dom,使用的就是樹結構。 若是你對二叉樹很熟悉,那麼此文對你可能毫無價值。node

參考資料:學習JavaScript數據結構與算法面試

樹數據結構簡介

生活中常見樹結構有企業的組織架構圖、家譜圖等。 一個樹結構包含一系列存在父子關係的節點。每一個節點都有一個父節點(除了頂部的第一個節點,稱爲根結點)以及零個或多個子節點:算法

二叉樹和二叉搜索樹

二叉樹中的節點最多隻能有兩個子節點:一個是左側子節點,另外一個是右側子節點。瀏覽器

二叉搜索樹(BST)是二叉樹的一種,可是它只容許你在左側節點存儲(比父節點)小的值, 在右側節點存儲(比父節點)大(或者等於)的值。數據結構

二叉搜索樹將是本文所複習的主要內容。架構

二叉搜索樹的實現

實現二叉搜索樹

二話不說,直接上代碼,實現一個二叉搜索樹的類,複製代碼,放到瀏覽器便可。能夠直接看代碼,後面再細談。dom

function BinarySearchTree() {
  // 初始化根結點root爲null
  let root = null;

  // 用於初始化節點,key爲值,left right分別爲左右子節點
  function Node(key) {
    this.key = key;
    this.left = null;
    this.right = null;
  };

  // 獲取樹並打出
  this.getRoot = function () {
    return root
  }

  // 向樹中插入新數據
  this.insert = function (key) {
    let newNode = new Node(key);
    if (root === null) {
      root = newNode;
    } else {
      insertNode(root, newNode);
    }
  };

  // 插值處理遞歸函數
  function insertNode(node, newNode) {
    if (newNode.key < node.key) {
      if (node.left === null) {
        node.left = newNode;
      } else {
        insertNode(node.left, newNode);
      }
    } else {
      if (node.right === null) {
        node.right = newNode;
      } else {
        insertNode(node.right, newNode);
      }
    }
  };

  // 中序遍歷
  this.inorderTraversal = function (callback) {
    inorderTraversalNode(root, callback);
  };

  // 中序遍歷處理遞歸函數
  function inorderTraversalNode(node, callback) {
    if (node !== null) {
      inorderTraversalNode(node.left, callback);
      callback(node.key);
      inorderTraversalNode(node.right, callback);
    }
  };

  // 先序遍歷
  this.preorderTraversal = function (callback) {
    preorderTraversalNode(root, callback);
  };

  // 先序遍歷遞歸函數
  function preorderTraversalNode(node, callback) {
    if (node !== null) {
      callback(node.key);
      preorderTraversalNode(node.left, callback);
      preorderTraversalNode(node.right, callback);
    }
  };

  // 後序遍歷
  this.postorderTraversal = function (callback) {
    postorderTraversalNode(root, callback);
  };

  // 後序遍歷遞歸函數
  function postorderTraversalNode(node, callback) {
    if (node !== null) {
      postorderTraversalNode(node.left, callback);
      postorderTraversalNode(node.right, callback);
      callback(node.key);
    }
  };

  // 查詢節點,若存在則返回true 反之 false
  this.search = function (key) {
    return searchNode(root, key);
  };

  // 查詢節點遞歸函數
  function searchNode(node, key) {
    if (node === null) {
      return false;
    }
    if (key < node.key) {
      return searchNode(node.left, key);
    } else if (key > node.key) {
      return searchNode(node.right, key);
    } else {
      return true;
    }
  };

  // 查詢最小值
  this.min = function () {
    return minNode(root);
  };

  function minNode(node) {
    if (node) {
      while (node && node.left !== null) {
        node = node.left;
        return node.key;
      }
      return null;
    };
  }

  // 查詢最大值
  this.max = function () {
    return maxNode(root);
  };

  function maxNode(node) {
    if (node) {
      while (node && node.right !== null) {
        node = node.right;
      }
      return node.key;
    }
    return null;
  };

  // 移除一個節點
  this.remove = function (key) {
    root = removeNode(root, key);
  }
  function removeNode(node, key) {
    if (node === null) {
      return null;
    }
    if (key < node.key) {
      node.left = removeNode(node.left, key);
      return node;
    } else if (key > node.key) {
      node.right = removeNode(node.right, key);
      return node;
    } else { //鍵等於node.key
      //第一種狀況——一個葉節點
      if (node.left === null && node.right === null) {
        node = null;
        return node;
      }
      //第二種狀況——一個只有一個子節點的節點 
      if (node.left === null) {
        node = node.right;
        return node;
      } else if (node.right === null) {
        node = node.left;
        return node;
      }
      //第三種狀況——一個有兩個子節點的節點
      var aux = findMinNode(node.right);
      node.key = aux.key;
      node.right = removeNode(node.right, aux.key);
      return node;
    }
  };

  function findMinNode(node) {
    while (node && node.left !== null) {
      node = node.left;
    }
    return node;
  };
}

// 上面代碼已經實現二叉搜索樹,下面開始用起來!

// 先new一個樹
let tree = new BinarySearchTree();
console.log('原始樹爲', tree.getRoot())
// 依次向樹中插入數據
tree.insert(10);
tree.insert(2);
tree.insert(5);
tree.insert(3);
tree.insert(9);
tree.insert(8);
tree.insert(13);
tree.insert(12);
tree.insert(14);

// 獲取當前樹
console.log('插值後的樹爲:', tree.getRoot())

// 用於遍歷的回調函數
function printNode(value) {
  console.log(value);
}
// 先序遍歷
console.log('下方爲先序遍歷結果')
tree.preorderTraversal(printNode);
// 中序遍歷
console.log('下方爲中序遍歷結果')
tree.inorderTraversal(printNode);
// 後序遍歷
console.log('下方爲後序遍歷結果')
tree.postorderTraversal(printNode);
// 查詢
console.log('查詢10的結果', tree.search(10))
// 最大值最小值
console.log('最大值爲:', tree.max())
console.log('最小值爲:', tree.min())
// 移除節點
console.log('移除節點 10 以前', tree.getRoot())
tree.remove(10)
console.log('移除節點 10 以後', tree.getRoot())
複製代碼

聲明一個Node類表示節點

二叉樹須要記錄當前節點的值,以及其2個子節點,此處以left right 分別表明左子節點和右子節點。 當建立新當節點時,new 一個Node類便可。koa

function Node(key) {
  this.key = key;
  this.left = null;
  this.right = null;
};
複製代碼

實現插值函數

插入一個新的值,就是插入一個新節點。這個時候就須要new一個Node類了。若是根結點不存在,即root爲null,則表示當前值爲第一個節點。不然,則須要一個插值函數用來循環插值。 在二叉樹中,基本都是遞歸,因此這塊須要詳細思考一下代碼的具體運行方式。 代碼解釋見註釋:函數

// 向樹中插入新數據
this.insert = function (key) {
  let newNode = new Node(key);
  if (root === null) {
    root = newNode;
  } else { // 若是根結點不爲空,則就須要調用插值函數來計算往哪插值了
    insertNode(root, newNode);
  }
};

// 插值處理遞歸函數
function insertNode(node, newNode) {
  // 若是要插入的值小於當前所在節點的值
  if (newNode.key < node.key) {
    // 若是新值小於當前節點的值且左側子節點爲空,則當前節點的左子節點就爲新插入的值 (1)
    if (node.left === null) {
      node.left = newNode;
    } else {
      // 若是當前節點左側子節點不爲空,則把當前節點的左子節點傳入insertNode中,開始遞歸,直到知足上面的 (1)才能順利插入值 
      insertNode(node.left, newNode);
    }
  } else {  // 若是要插入的值大於當前所在節點的值,則說明要插的值須要在右側子節點,遞歸邏輯同上
    if (node.right === null) {
      node.right = newNode;
    } else {
      insertNode(node.right, newNode);
    }
  }
};
複製代碼

實現先序、中序、後續遍歷

這3種遍歷沒有本質區別,只不過是回調函數的位置不一樣而已。對比代碼一看即懂。post

// 中序遍歷
this.inorderTraversal = function (callback) {
  // 傳root,完整的樹,做爲初始值
  inorderTraversalNode(root, callback);
};

// 下面纔是遍歷的真正方法
function inorderTraversalNode(node, callback) {
  // 最開始root爲完整的樹,當node不爲空,就一直遞歸。爲空時,則說明遍歷完畢
  if (node !== null) {
    // node不爲空時,繼續調用,查看左子節點是否爲空。若是左子節點不爲空,則還會再次進入方法,直到左子節點爲null
    inorderTraversalNode(node.left, callback);
    // 這個回調函數的調用有點相似koa的洋蔥圈模型。當inorderTraversalNode遞歸到左子節點爲空時,纔不會繼續調用。
    // 因此最早最早執行 callback(node.key) 中的節點值是最小的,而後依次愈來愈大。
    callback(node.key);
    inorderTraversalNode(node.right, callback);
  }
};
複製代碼

節點查詢

節點查詢就比較簡單了,就是循環全部節點,看看是否有相同的值,若是有就返回true,不然false

// 查詢節點
this.search = function (key) {
  return searchNode(root, key);
};

// 查詢節點遞歸函數
function searchNode(node, key) {
  // 此處每次遞歸時,都會走到。若是全部節點都走完了,也沒走到(1) ,那就放棄吧,確實沒有
  if (node === null) {
    return false;
  }
  if (key < node.key) {
    return searchNode(node.left, key);
  } else if (key > node.key) {
    return searchNode(node.right, key);
  } else {
    return true;  // 相等(1)
  }
};
複製代碼

查詢最大、最小值

這個也比較簡單,在二叉樹中,最小值確定在左側,最大值確定在右側。因此查詢最小值,只要循環樹左側的節點,直到節點沒了。下圖的箭頭分別表明尋找最大最小值的訪問路徑。 這裏有個特殊狀況須要處理,那就是樹自己就爲null,直接返回null。最大值同理。

// 查詢最小值
this.min = function () {
  return minNode(root);
};

function minNode(node) {
  if (node) {
    // 若是節點存在且左子節點不爲空,則一直循環,把node設爲node.left
    while (node && node.left !== null) {
      node = node.left;
      return node.key;
    }
    return null;
  };
}

// 查詢最大值
this.max = function () {
  return maxNode(root);
};

function maxNode(node) {
  if (node) {
    while (node && node.right !== null) {
      node = node.right;
    }
    return node.key;
  }
  return null;
};
複製代碼

刪除節點

刪除節點稍微比較複雜。見註釋。 移除含有2個子節點的節點比較複雜,若是所示,須要在他的子樹中(注意,是第一層子樹),右子樹尋找最小的節點,用這個最小的節點替換須要刪除的節點。

// 移除一個節點
this.remove = function (key) {
  // 刪除key後,新的樹爲removeNode的返回值
  root = removeNode(root, key);
}
function removeNode(node, key) {
  if (node === null) {
    return null;
  }
  if (key < node.key) {
    // 若是要刪除的值小於當前節點的值,則說明還沒找到那個要刪除的節點。
    // 遞歸removeNode時,只有找到了那個節點,即執行了 (1),纔會有返回值。並把當前節點的左子節點設爲返回值。而後返回node。
    // 注意,給node.left賦值時,是遞歸賦值,node在不一樣的循環指向不一樣的節點
    node.left = removeNode(node.left, key);
    return node;
  } else if (key > node.key) {
    node.right = removeNode(node.right, key);
    return node;
  } else { //鍵等於node.key (1)
  /* * 若是邏輯走到了這邊的代碼,則說明已經找到了須要刪除的節點。 * 可是,該節點有3種狀況。 * 1. 該節點沒有子節點:則說明該節點爲直接設爲null便可,也就是說,他的父節點的left設爲null, node.left = null; * 2. 該節點有一個左或者右子節點:若左子節點爲null,右子節點直接上移,替換該節點便可node = node.right;,其餘相似 * 3. 該節點有2個子節點。這個時候,須要在他的子樹中(注意,是第一層子樹),右子樹尋找最小的節點,用這個最小的節點替換須要刪除的節點。 */
    if (node.left === null && node.right === null) {
      node = null;
      return node;
    }
    //第二種狀況——一個只有一個子節點的節點 
    if (node.left === null) {
      node = node.right;
      return node;
    } else if (node.right === null) {
      node = node.left;
      return node;
    }
    //第三種狀況——一個有兩個子節點的節點
    var aux = findMinNode(node.right);
    node.key = aux.key;
    node.right = removeNode(node.right, aux.key);
    return node;
  }
};
// 這個方法用來尋找子樹中的最小節點
function findMinNode(node) {
  while (node && node.left !== null) {
    node = node.left;
  }
  return node;
};
複製代碼

平衡樹 紅黑樹 堆積樹

此類後續再詳細講。

相關文章
相關標籤/搜索