Tree & Binary Search Tree

Tree คือ abstract data type ที่เก็บข้อมูลแบบลำดับชั้น เป็นเซตของ node ที่มีความสัมพันธ์แบบ parent–child

นิยามและคำศัพท์

คำศัพท์ความหมาย
parent / childA เป็น parent ของ B และ C / B และ C เป็น child ของ A
siblingB กับ C เป็นพี่น้องกัน (parent เดียวกัน)
ancestorบรรพบุรุษ — ไล่ขึ้นไปทางราก เช่น D เป็น ancestor ของ A, B, C, E
descendantลูกหลาน — ไล่ลงมา เช่น B เป็น descendant ของ A และ D
subtreeต้นไม้ย่อยที่มี node ใด node หนึ่งเป็นราก

Depth และ Height

นิยามวัดจาก
Depth ของ nodeความยาวเส้นทางจาก node นั้นขึ้นไปถึง rootนับขึ้นบน
Height ของ nodeความยาวเส้นทางที่ยาวสุดจาก node นั้นลงไปถึง leafนับลงล่าง
Height ของ treeheight ของ root
จำง่าย: depth มองขึ้น height มองลง — leaf มี height = 0 เสมอ ส่วน root มี depth = 0

การใช้งานจริง

Binary Tree และชนิดต่าง ๆ

Binary Tree = ต้นไม้ที่ทุก node มีลูกได้ไม่เกิน 2 ตัว (left, right)

ชนิดเงื่อนไข
Empty Binary Treeไม่มี node เลย
Full Binary Treeทุก node มีลูก 0 หรือ 2 ตัว (ห้ามมีลูกตัวเดียว)
Complete Binary Treeเต็มทุกชั้นยกเว้นชั้นล่างสุด และชั้นล่างสุดเรียงชิดซ้าย
Perfect Binary Treeเต็มทุกชั้น ทุก leaf อยู่ชั้นเดียวกัน
Balanced Binary Treeความสูงของ subtree ซ้ายกับขวาต่างกันไม่เกิน 1 ทุก node (AVL = แบบเข้มงวด)

จำนวน node ของ complete binary tree

height k  →  จำนวน node มากสุด = 2^(k+1) - 1

height 1 → 3 node
height 2 → 7 node
height 3 → 15 node

โครงสร้าง node ในโค้ด

public class BinaryTreeNode {
    int element;
    BinaryTreeNode left;
    BinaryTreeNode right;

    public BinaryTreeNode(int data){
        this.element = data;
        this.left = null;
        this.right = null;
    }
}

สร้างต้นไม้:

BinaryTreeNode root = new BinaryTreeNode(1);
root.left  = new BinaryTreeNode(2);
root.right = new BinaryTreeNode(3);
root.left.left  = new BinaryTreeNode(4);
root.left.right = new BinaryTreeNode(5);
ถ้าไม่มีลูก ให้ attribute นั้นเป็น NULL — โค้ดข้างบนอ่านยากเมื่อต้นไม้ใหญ่ จึงควรห่อด้วยคลาส BinaryTree ที่เก็บ root ไว้

DFS — Depth-First Search

ลงลึกสุดก่อน แล้วค่อยถอยกลับ (backtrack) ใช้ recursion (เบื้องหลังคือ stack)

1 8 6 3 4 2 7 9 5 0 root = 1 · leaf = 3, 9, 5, 0, 7 · height ของต้นไม้ = 3

ต้นไม้ตัวอย่างที่ใช้ทุกหัวข้อ traversal ในบทนี้

แบบลำดับการพิมพ์ผลลัพธ์จากต้นไม้ข้างบน
PreorderRoot → Left → Right1, 8, 3, 4, 9, 5, 6, 2, 0, 7
InorderLeft → Root → Right3, 8, 9, 4, 5, 1, 2, 0, 6, 7
PostorderLeft → Right → Root3, 9, 5, 4, 8, 0, 2, 7, 6, 1
Preorder 1 8 3 4 9 5 6 2 0 7 Inorder 3 8 9 4 5 1 2 0 6 7 Postorder 3 9 5 4 8 0 2 7 6 1 BFS 1 8 6 3 4 2 7 9 5 0

ต่างกันแค่ตำแหน่งของคำสั่ง print เทียบกับการลงซ้าย–ลงขวา

public void printPreOrder(BinaryTreeNode x)
{
    if(x != null)
    {
        System.out.print(x.element + " ");
        printPreOrder(x.left);
        printPreOrder(x.right);
    }
}
ตำแหน่งของบรรทัด print คือสิ่งเดียวที่ต่างกัน — บนสุด = preorder, กลาง = inorder, ล่างสุด = postorder
inorder ของ BST จะได้ค่าเรียงจากน้อยไปมากเสมอ — ใช้เช็คว่าต้นไม้เป็น BST จริงไหมได้

BFS — Breadth-First Search

ไล่ทีละชั้น ซ้ายไปขวา (level order) ใช้ queue

ผลลัพธ์จากต้นไม้ข้างบน:  1, 8, 6, 3, 4, 2, 7, 9, 5, 0
1. ใส่ root ลง queue
2. dequeue มา 1 ตัว พิมพ์ค่า
3. enqueue ลูกซ้ายและลูกขวาของมัน
4. ทำซ้ำจนกว่า queue จะว่าง
DFSBFS
โครงสร้างที่ใช้stack (recursion)queue
ทิศทางลงลึกก่อนกว้างทีละชั้น
เส้นทางสั้นสุดไม่การันตีการันตี (กราฟไม่ถ่วงน้ำหนัก)

Binary Search Tree (BST)

ให้ x เป็น node ใด ๆ
ถ้า y อยู่ใน subtree ซ้ายของ x แล้ว y.element ≤ x.element
ถ้า y อยู่ใน subtree ขวาของ x แล้ว y.element ≥ x.element
กับดักข้อสอบ: เงื่อนไขต้องเป็นจริงกับ ทุก node ใน subtree ไม่ใช่แค่ลูกตัวติดกัน — node ทางซ้ายต้องน้อยกว่า ancestor ทุกตัวที่มันอยู่ทางซ้าย

ปัญหาค่าซ้ำ

ถ้าใส่ค่าซ้ำเป็น node ใหม่ ค่าที่เท่ากันจะกระจายอยู่คนละชั้น ทำให้จัดการยาก ทางแก้ที่นิยมคือไม่สร้าง node ใหม่ แต่เก็บตัวนับจำนวนครั้งไว้ใน node เดิม

ทำไม BST เป็น O(h)

log₂ n  ≤  h  ≤  n
รูปร่างต้นไม้heightเวลาค้นหา
เอียงเป็นเส้นตรง (ใส่ข้อมูลเรียงมาแล้ว)nO(n) — ไม่ต่างจาก linked list
balancedlog nO(log n)

เทียบให้เห็นภาพ

ต้นไม้เอียง 1-2-3-4-5-6 หา 6  →  ต้องไล่ทุก node = 6 ครั้ง

ต้นไม้ balanced หา 3:
   3 < 4 ไปซ้าย
   3 > 2 ไปขวา
   เจอ 3        → ใช้ 3 ครั้ง = height ของต้นไม้
ใส่ข้อมูลชุดเดียวกัน 1..6 แต่คนละลำดับ balanced → h = log n 4 2 5 1 3 6 หา 3 ใช้ 3 ครั้ง (4→2→3) เอียงเป็นเส้นตรง → h = n 1 2 3 4 5 6 หา 6 ต้องไล่ครบ 6 ครั้ง

ข้อมูลเท่ากันแต่รูปร่างต่างกัน ทำให้ค้นหาเป็น O(log n) หรือ O(n) ก็ได้

ความสูงของ complete binary tree:

n = 1  → h = 0        log₂1 = 0
n = 3  → h = 1        log₂3 ≈ 1
n = 7  → h = 2        log₂7 ≈ 2
n = 15 → h = 3        log₂15 ≈ 3
ถ้า BST เป็น balanced tree จริง ๆ การค้นหาจะเป็น O(log n) — นี่คือเหตุผลที่มี AVL tree
public boolean search(int element) {
    BinaryTreeNode current = root;
    while (current != null) {
        if (current.element == element) {
            return true;
        } else if (current.element > element) {
            current = current.left;
        } else if (current.element < element) {
            current = current.right;
        }
    }
    return false;
}

แนวคิดเดียวกับ binary search: เริ่มที่ root → น้อยกว่าไปซ้าย มากกว่าไปขวา → ทำซ้ำจนเจอหรือชน null

BST: Insert

เดินเหมือน search ถ้าเจอค่าเดิมก็ไม่ทำอะไร ถ้าไม่เจอให้แทรกที่ตำแหน่งสุดท้ายที่เดินถึง

ใส่ 3 ลงต้นไม้ที่มี 6, 2, 8, 1, 4

3 < 6 : ไปซ้าย
3 > 2 : ไปขวา
3 < 4 : ไปซ้าย
node 4 ไม่มีลูกซ้าย → แขวน 3 ไว้ตรงนั้น
public void insert(int element) {
    BinaryTreeNode current = root;
    while (current != null) {
        if (current.element == element) {
            break;
        } else if (current.element > element) {
            if (current.left != null) {
                current = current.left;
            } else {
                current.left = new BinaryTreeNode(element);
                break;
            }
        } else {
            if (current.right != null) {
                current = current.right;
            } else {
                current.right = new BinaryTreeNode(element);
                break;
            }
        }
    }
}

BST: Delete ทั้ง 3 กรณี

กรณีวิธีทำ
1. ไม่มีลูก (leaf)ลบทิ้งได้เลย ตั้ง pointer ของ parent เป็น null
2. มีลูก 1 ตัวดึงลูกขึ้นมาแทนที่ตัวเอง
3. มีลูก 2 ตัวแทนที่ด้วย ค่ามากสุดของ subtree ซ้าย หรือ ค่าน้อยสุดของ subtree ขวา แล้วลบ node นั้นทิ้ง

ลบ node ที่มีลูก 2 ตัว

ต้นไม้:      7
          /     \
         4        8
       /   \     /
      2     5   ...
     / \     \
    1   3     6

ลบ 4:
  ค่าน้อยสุดของ subtree ขวาของ 4 = 5
  1. เอา 5 มาแทนค่าใน node 4
  2. ลบ node 5 เดิม (ซึ่งกลายเป็นกรณีมีลูก 1 ตัว → ดึง 6 ขึ้นมา)
เหตุผลที่ใช้ค่ามากสุดฝั่งซ้ายหรือน้อยสุดฝั่งขวา: เป็นค่าเดียวที่แทนแล้วยังรักษาเงื่อนไข BST ไว้ได้ และ node ที่ถูกลบจะกลายเป็นกรณีที่ 1 หรือ 2 เสมอ

BFS หาเส้นทางสั้นสุด

โจทย์: หาเส้นทางสั้นสุดจาก S ไป G เดินได้ทีละช่อง ทิศทางพิจารณาตามลำดับ ขวา ล่าง ซ้าย บน (ช่องสีเทาเดินไม่ได้)

  1. ไล่ขยายทีละชั้นด้วย BFS ให้เลขกำกับลำดับที่เจอแต่ละช่อง
  2. เมื่อถึง G ให้ย้อนกลับตาม parent เก็บลง stack — ได้ 7, 4, 2, 1
  3. Pop stack ออกมาจะได้เส้นทางตามลำดับจริง: 1, 2, 4, 7
BFS การันตีเส้นทางสั้นสุด แต่ DFS ไม่การันตี เพราะ DFS อาจลงลึกผิดทางไปไกลก่อนจะเจอเป้าหมาย