數據結構系列(1)之 二叉搜索樹


本文將主要以動圖方式展示二叉搜索樹的結構,以及動態操作;但是對於基本的概念和性質則不會有過多的提及,如果想系統了解建議查看鄧俊輝老師的《數據結構》課程;

一、結構概述

二叉樹:融合了向量的靜態操作(二分查找)和列表的動態操作(插入和刪除)的優點;使得樹成了應用廣泛的數據結構;

二叉搜索樹:即順序排列,可以搜索的樹就是二叉搜索樹;如下圖所示;

BST

忽略二叉樹的大小,高度等信息的簡版結構如下:

public class BST<T extends Comparable<? super T>> {
  private BSTNode<T> root;

  public BST() {root = null;}
  ...
  
  class BSTNode<T extends Comparable<? super T>> {
    T key;
    BSTNode<T> parent;
    BSTNode<T> left;
    BSTNode<T> right;

    BSTNode(T key, BSTNode<T> parent, BSTNode<T> left, BSTNode<T> right) {
      this.key = key;
      this.parent = parent;
      this.left = left;
      this.right = right;
    }
  }
}

二、查找

根據二叉搜索樹左孩子節點小於父節點,右孩子節點大於等於父節點,逐步深入查找;

bstsearch

實現如下:

private BSTNode<T> search(BSTNode<T> x, T key) {
  if (x == null) return x;

  int cmp = key.compareTo(x.key);
  if (cmp < 0)
    return search(x.left, key);
  else if (cmp > 0)
    return search(x.right, key);
  else
    return x;
}

public BSTNode<T> search(T key) {
  return search(root, key);
}

更好的實現還應該記錄查找失敗的上一個節點,除此之外還可以實現查找最大值,最小值;

三、插入

插入,同樣根據 BST 的排序規則,先查找插入位置,然后確定插入節點的引用關系;

1. 插入不重復節點

bstinsert1

2. 插入重復節點

bstinsert1

3. 實現

如果有 size 等信息,則需要從節點向上依次更新祖先節點

private void insert(T key) {
  BSTNode<T> node = new BSTNode<T>(key, null, null, null);

  BSTNode<T> r = this.root;
  BSTNode<T> p = null;
  int cmp;

  // 查找插入位置
  while (r != null) {
    p = r;
    cmp = node.key.compareTo(r.key);
    if (cmp < 0)
      r = r.left;
    else
      r = r.right;
  }

  if (p == null)
    this.root = node;
  else {
    node.parent = p;
    cmp = node.key.compareTo(p.key);
    if (cmp < 0)
      p.left = node;
    else
      p.right = node;
  }
}

四、刪除

刪除,同樣首先需要查找該節點,然后刪除;

1. 刪除不完全節點

目標節點沒有左孩子或者右孩子時,直接刪除節點,然后令其后代代替;

bstremove

2. 刪除完全節點

當目標節點同時擁有左孩子和右孩子時:

  • 需要首先找到目標節點的直接后繼(自然順序或者中序遍歷順序的下一個節點),目標節點右孩子一直往左;當然這里同樣可以找目標節點的前驅節點替換;
  • 然后交換目標節點和后繼節點的位置;
  • 最后同上面的刪除一樣,直接刪除節點;
bstremove

3. 實現

注意這里只是實現的一種,還可以是可前驅進行交換

public void delete(T key) {
  BSTNode<T> node = search(root, key);
  if (node != null) delete(node);
  node = null;
}

private BSTNode<T> delete(BSTNode<T> node) {
  // 將完全節點和直接后繼交換
  if (node.left != null && node.right != null) {
    BSTNode<T> succ = successor(node);
    node.key = succ.key;
    node = succ;
  }

  // 將孩子節點的父親接到祖父上
  BSTNode<T> child = node.left != null ? node.left : node.right;
  if (child != null) child.parent = node.parent;

  // 目標節點沒有父節點則是根節點
  if (node.parent == null) root = child;

    // 目標節點是左孩子時
  else if (node == node.parent.left) node.parent.left = child;

    // 目標節點是右孩子時
  else node.parent.right = child;

  return node;
}

private BSTNode<T> successor(BSTNode<T> node) {
    BSTNode<T> succ = node.right;
    while (succ.left != null) succ = succ.left;
    return succ;
}

// 這里只是右子樹中的直接后繼;整個中序遍歷中直接后繼還可能是其父節點或者祖先節點;

五、前序遍歷

對於一個數據結構而言,了解他最直接的方式就是遍歷操作,而對於二叉樹則有前、中、后、層次遍歷四種;

1. 描述

  • 首先遍歷根節點
  • 再一次遞歸遍歷左子樹和右子樹;

具體過程如下:

bstpre

2. 實現

// 遞歸實現
private void travPre(BSTNode<T> tree) {
  if (tree == null) return;

  System.out.print(tree.key + " ");
  travPre(tree.left);
  travPre(tree.right);
}

// 迭代實現
public void travPre2(BSTNode<T> tree) {
  Stack<BSTNode<T>> stack = new Stack<>();

  while (true) {
    visitAlongVine(tree, stack);
    if (stack.isEmpty()) break;
    tree = stack.pop();
  }
}

private void visitAlongVine(BSTNode<T> tree, Stack<BSTNode<T>> stack) {
  while (tree != null) {
    System.out.print(tree.key + " ");
    stack.push(tree.right);
    tree = tree.left;
  }
}

六、中序遍歷

1. 描述

  • 對於中序遍歷,則首先遍歷左子樹
  • 再遍歷根節點
  • 最后遍歷右子樹

具體過程如下:

bstin

2. 實現

// 遞歸實現
private void travIn(BSTNode<T> tree) {
  if (tree == null) return;

  travIn(tree.left);
  System.out.print(tree.key + " ");
  travIn(tree.right);
}

// 迭代實現
public void travIn2(BSTNode<T> tree) {
  Stack<BSTNode<T>> stack = new Stack<>();

  while (true) {
    goAlongVine(tree, stack);
    if (stack.isEmpty()) break;
    tree = stack.pop();
    System.out.print(tree.key + " ");
    tree = tree.right;
  }
}

private void goAlongVine(BSTNode<T> tree, Stack<BSTNode<T>> stack) {
  while (tree != null) {
    stack.push(tree);
    tree = tree.left;
  }
}

七、后序遍歷

1. 描述

  • 對於后序遍歷,則首先遍歷左子樹
  • 再遍歷根右子樹
  • 最后遍歷根節點

具體過程如下:

bstpost

2. 實現

// 遞歸實現
private void travPost(BSTNode<T> tree) {
  if (tree == null) return;

  travPost(tree.left);
  travPost(tree.right);
  System.out.print(tree.key + " ");
}

// 迭代實現
public void travPost2(BSTNode<T> tree) {
  Stack<BSTNode<T>> stack = new Stack<>();

  if (tree != null) stack.push(tree);
  while (!stack.isEmpty()) {
    if (stack.peek() != tree.parent)
      gotoHLVFL(stack);
    tree = stack.pop();
    System.out.print(tree.key + " ");
  }
}

private void gotoHLVFL(Stack<BSTNode<T>> stack) {
  BSTNode<T> tree;
  while ((tree = stack.peek()) != null)
    if (tree.left != null) {
      if (tree.right != null) stack.push(tree.right);
      stack.push(tree.left);
    } else
      stack.push(tree.right);
  stack.pop();
}

八、層次遍歷

1. 描述

對於層次遍歷而言就相對簡單,即由上到下逐層遍歷;

2. 實現

public void travLevel() {
  Queue<BSTNode<T>> queue = new LinkedList<>();
  queue.offer(root);
  while (!queue.isEmpty()) {
    BSTNode<T> x = queue.poll();
    System.out.print(x.key + " ");
    if (x.left != null) queue.offer(x.left);
    if (x.right != null) queue.offer(x.right);
  }
}

總結

  • 正是因為二叉樹同時具有向量和列表的特性,所以他的使用場景非常廣泛,當然他還有很多的變種,這些我們后續會繼續講到;
  • 對於文中的動圖大家可以在這個網站自行實驗;http://btv.melezinek.cz/binary-search-tree.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM