Tree និង Binary Search Tree
មាតិកា
- ទិន្នន័យដែលបែកមែក
- វាក្យសព្ទរបស់ Tree
- Tree ដែលអ្នកប្រើរួចហើយ
- Depth-First Traversal
- Breadth-First Traversal
- Binary Tree និង Binary Search Tree
- ការស្វែងរក និងការបញ្ចូល
- ការលុប Node
- កម្ពស់ និងតុល្យភាព
- បញ្ហាសម្ភាសន៍
- រូបមន្ត៖ tree.ts សម្រាប់ algolab
- សាកល្បងដោយខ្លួនឯង
1. ទិន្នន័យដែលបែកមែក
Array, linked list, stack និង queue សុទ្ធតែជា បន្ទាត់៖ ធាតុមួយតាមក្រោយធាតុមួយ។ ទិន្នន័យពិតជាច្រើនមិនមែនជាបន្ទាត់ទេ។ Folder មួយផ្ទុក folder ដែលផ្ទុក folder ច្រើនទៀត។ ទំព័រ web មាន <body> ដែលផ្ទុក <div> ដែលផ្ទុក <ul> ដែលផ្ទុកធាតុ <li> ជាច្រើន។ ក្រុមហ៊ុនមួយមាន CEO, អ្នកគ្រប់គ្រងនៅក្រោម CEO និងវិស្វករនៅក្រោមអ្នកគ្រប់គ្រង។
ទាំងអស់នេះគឺជា tree៖ ទិន្នន័យដែលបែកចុះក្រោមពីចំណុចចាប់ផ្តើមតែមួយ។ Tree មាននៅគ្រប់ទីកន្លែងដែលអ្នកមើល៖
- DOM នៃគ្រប់ទំព័រ web ដែល React ដើរតាមនៅពេលវាធ្វើបច្ចុប្បន្នភាពអេក្រង់
- file system លើ laptop របស់អ្នក
- JSON ដែលអ្នកទទួលបានពី API (object នៅក្នុង object)
- menu របស់ហាងអនឡាញ (Electronics បន្ទាប់មក Phones បន្ទាប់មក Android)
- comment thread ក្រោមប្រកាសមួយ ដែលមានការឆ្លើយតបទៅលើការឆ្លើយតប
- database index ដែលធ្វើឲ្យ
WHERE id = 42លឿន ដែលជាធម្មតាជា tree ប្រភេទមួយហៅថា B-tree
នៅក្នុងជំពូកនេះ អ្នកនឹងរៀនវាក្យសព្ទ វិធីពីរយ៉ាងដើម្បីដើរតាម tree (depth-first និង breadth-first) ហើយបន្ទាប់មកករណីពិសេសដែលមានប្រយោជន៍បំផុត៖ binary search tree ដែលរក្សាទិន្នន័យតាមលំដាប់ ដើម្បីឲ្យអ្នកស្វែងរកវាបានលឿនដូច binary search ពីជំពូក Searching ទន្ទឹមនឹងនោះនៅតែអាចបញ្ចូល និងលុបបាន។
2. វាក្យសព្ទរបស់ Tree
នេះគឺជា tree តូចមួយ។ យើងគូរវាដោយដាក់ធាតុទីមួយនៅខាងលើ ដែលជារបៀបដែលអ្នកវិទ្យាសាស្ត្រកុំព្យូទ័រតែងតែគូរ tree (ក្រឡាប់ក្បាលបើប្រៀបធៀបនឹងដើមឈើពិត)។
A <- root
/ \
B C
/ \ \
D E F <- D, E, F are leaves
ពាក្យដែលត្រូវដឹង៖
| ពាក្យ | អត្ថន័យ | ក្នុងរូប |
|---|---|---|
| Node | ធាតុមួយក្នុង tree | A, B, C, ... |
| Root | node នៅកំពូល ដែលគ្មាន parent | A |
| Edge | ខ្សែតភ្ជាប់ពី parent ទៅ child | A-B, B-D, ... |
| Parent | node ដែលនៅពីលើដោយផ្ទាល់ | B ជា parent របស់ D |
| Child | node ដែលនៅពីក្រោមដោយផ្ទាល់ | D និង E ជា child របស់ B |
| Sibling | node ដែលមាន parent ដូចគ្នា | D និង E |
| Leaf | node ដែលគ្មាន child | D, E, F |
| Subtree | node មួយ បូកនឹងអ្វីៗទាំងអស់នៅពីក្រោមវា | B, D, E |
| Depth | ចំនួន edge ពី root ចុះមក node មួយ | D មាន depth 2 |
| Height | ចំនួន edge ពី node មួយចុះទៅ leaf ដែលជ្រៅបំផុតរបស់វា | A មាន height 2 |
ច្បាប់ពីរដែលធ្វើឲ្យ tree ជា tree៖
- មាន root តែមួយប៉ុណ្ណោះ។
- មាន ផ្លូវ តែមួយប៉ុណ្ណោះពី root ទៅ node ណាមួយ។ គ្មាន node ណាមាន parent ពីរទេ ហើយក៏គ្មានរង្វិលជុំដែរ។
ប្រសិនបើអ្នកបំពានច្បាប់ទី 2 (node មាន parent ពីរ ឬមានខ្សែតត្រឡប់ឡើងលើ) អ្នកលែងមាន tree ទៀតហើយ។ អ្នកមាន graph ដែលជាប្រធានបទនៃជំពូកបន្ទាប់ពីជំពូកបន្ទាប់។ Tree គឺគ្រាន់តែជា graph ដែលមានច្បាប់តឹងរ៉ឹងប៉ុណ្ណោះ។
កំណត់សម្គាល់អំពីការរាប់៖ យើងវាស់ depth និង height ជា edge មិនមែនជា node ទេ។ Tree ដែលមាន node តែមួយមាន height 0។ តាមទម្លាប់ tree ទទេមាន height -1។ សៀវភៅខ្លះរាប់ជា node វិញ ដូច្នេះ node តែមួយមាន height 1។ ទាំងពីរបែបត្រឹមត្រូវ។ គ្រាន់តែត្រូវច្បាស់ថាអ្នកប្រើបែបណា។ ក្នុងជំពូកនេះយើងរាប់ជា edge។
3. Tree ដែលអ្នកប្រើរួចហើយ
តោះបំប្លែងរចនាសម្ព័ន្ធ folder ទៅជាទិន្នន័យ។ Tree node ត្រូវការ value និងបញ្ជីនៃ child៖
class TreeNode<T> {
children: TreeNode<T>[] = [];
constructor(public value: T) {}
add(value: T): TreeNode<T> {
const child = new TreeNode(value);
this.children.push(child);
return child; // so we can keep adding below the new node
}
}
const root = new TreeNode("src");
const components = root.add("components");
components.add("Button.tsx");
components.add("Navbar.tsx");
root.add("lib").add("api.ts");
root.add("index.ts");
នោះបង្កើត folder នេះ៖
src
├── components
│ ├── Button.tsx
│ └── Navbar.tsx
├── lib
│ └── api.ts
└── index.ts
Node ដែលមាន child ចំនួនប៉ុន្មានក៏បានហៅថា n-ary tree (ឬ "general tree")។ DOM គឺជា n-ary tree មួយ៖ element មួយអាចផ្ទុក child element ចំនួនប៉ុន្មានក៏បាន។ File system ក៏ដូចគ្នា។ ក្នុងផ្នែកបន្ទាប់ យើងនឹងដើរតាម folder នេះតាមលំដាប់ផ្សេងៗគ្នា។
public value: T ក្នុង constructor គឺជាអក្សរកាត់របស់ TypeScript។ វាប្រកាស property ហើយកំណត់តម្លៃឲ្យវាក្នុងមួយបន្ទាត់ ដូចគ្នានឹងការសរសេរ this.value = value ដោយខ្លួនឯង។
4. Depth-First Traversal
ការ traverse tree មានន័យថាទៅមើលគ្រប់ node ម្តងម្នាក់។ មានវិធីពីរក្រុមដើម្បីធ្វើវា។ ក្រុមទីមួយគឺ depth-first search (DFS)៖ ចុះឲ្យជ្រៅបំផុតតាមមែកមួយ បន្ទាប់មកត្រឡប់មកវិញ ហើយសាកល្បងមែកបន្ទាប់។
ដោយសារ tree ត្រូវបានសាងសង់ពី tree តូចៗ (រាល់ child ជា root នៃ subtree របស់ខ្លួន) DFS សមស្របនឹង recursion យ៉ាងធម្មជាតិ ដែលអ្នកបានរៀនក្នុងជំពូក Recursion។ រូបមន្តគឺ៖ ដោះស្រាយ node នេះ បន្ទាប់មកប្រាប់ child នីមួយៗឲ្យធ្វើដូចគ្នា។
function preorder<T>(root: TreeNode<T>, visit: (v: T) => void): void {
visit(root.value);
for (const child of root.children) preorder(child, visit);
}
const seen: string[] = [];
preorder(root, (v) => seen.push(v));
console.log(seen.join(", "));
// src, components, Button.tsx, Navbar.tsx, lib, api.ts, index.ts
នេះគឺ preorder៖ ទៅមើល node មុន បន្ទាប់មកទៅ child របស់វា។ វាជារបៀបដែលអ្នកនឹងបោះពុម្ពបញ្ជី folder ចម្លង folder (អ្នកត្រូវបង្កើត folder មេមុនឯកសារនៅក្នុងវា) ឬ serialize tree ទៅជា JSON។
ប្តូរការហៅ visit ទៅនៅក្រោយ loop ហើយអ្នកនឹងបាន postorder៖ child មុន បន្ទាប់មក node។
function postorder<T>(root: TreeNode<T>, visit: (v: T) => void): void {
for (const child of root.children) postorder(child, visit);
visit(root.value);
}
const done: string[] = [];
postorder(root, (v) => done.push(v));
console.log(done.join(", "));
// Button.tsx, Navbar.tsx, components, api.ts, lib, index.ts, src
Postorder គឺជារបៀបដែលអ្នក លុប folder (អ្នកត្រូវសម្អាតវាឲ្យទទេមុននឹងអាចដកវាចេញ) ឬ បូកសរុបទំហំ៖ ទំហំរបស់ folder គឺជាផលបូកនៃទំហំ child របស់វា ដូច្នេះ child ត្រូវតែរួចរាល់មុន។ នេះគឺជាការប្រើប្រាស់ទីពីរ ដោយមានទំហំជា kilobyte៖
interface Entry {
name: string;
size: number; // for a file; folders use 0
}
const files = new TreeNode<Entry>({ name: "src", size: 0 });
const ui = files.add({ name: "components", size: 0 });
ui.add({ name: "Button.tsx", size: 3 });
ui.add({ name: "Navbar.tsx", size: 5 });
files.add({ name: "index.ts", size: 2 });
function totalSize(node: TreeNode<Entry>): number {
let total = node.value.size;
for (const child of node.children) total += totalSize(child);
return total;
}
console.log(totalSize(files)); // 10
console.log(totalSize(ui)); // 8
សរុបមានបីលំដាប់សម្រាប់ binary tree (tree ដែលគ្រប់ node មាន child យ៉ាងច្រើនពីរ ដែលនឹងមកដល់ក្នុងផ្នែកទី 6) ហើយលំដាប់ទីបីគឺ inorder៖ left subtree បន្ទាប់មក node បន្ទាប់មក right subtree។ យើងនឹងរៀនវានៅពេលយើងជួប binary search tree ដែលវាមានលក្ខណៈសម្បត្តិមានប្រយោជន៍ខ្លាំង។
Call stack និងតម្លៃនៃ recursion មានសារៈសំខាន់នៅទីនេះ។ រាល់ recursive call រង់ចាំនៅលើ call stack ដូច្នេះ DFS ប្រើអង្គចងចាំសមាមាត្រនឹង កម្ពស់ របស់ tree។ សម្រាប់ tree ដែលមានតុល្យភាព វាតូច។ សម្រាប់ tree ដែលខ្ពស់ និងស្គមខ្លាំង វាអាចបណ្តាលឲ្យ stack overflow ដូចគ្នានឹង recursion ជ្រៅដែលអ្នកបានឃើញក្នុងជំពូក Recursion។ អ្នកអាចសរសេរ DFS ឡើងវិញដោយប្រើ array stack ផ្ទាល់ខ្លួនជំនួស recursion នៅពេលដែលហានិភ័យនេះកើតមាន។
5. Breadth-First Traversal
ក្រុមមួយទៀតគឺ breadth-first search (BFS) ហៅម្យ៉ាងទៀតថា level order៖ ទៅមើលគ្រប់ node នៅ depth 0 បន្ទាប់មកគ្រប់ node នៅ depth 1 បន្ទាប់មក depth 2 ហើយបន្តទៅ។ វាបោសឆ្លងកាត់ tree ម្តងមួយជួរ។
សម្រាប់រឿងនេះ អ្នកត្រូវការ queue ពីជំពូក Stack និង Queue។ Queue គឺ first in, first out ដូច្នេះ child ដែលត្រូវបានរកឃើញមុនត្រូវបានទៅមើលមុន។ កំណែសាមញ្ញសម្រាប់ tree របស់យើងរក្សា array មួយសម្រាប់ level បច្ចុប្បន្ន ហើយសាងសង់ level បន្ទាប់ពី child៖
function levelOrder<T>(root: TreeNode<T>): T[][] {
const levels: T[][] = [];
let current: TreeNode<T>[] = [root];
while (current.length > 0) {
levels.push(current.map((n) => n.value));
current = current.flatMap((n) => n.children);
}
return levels;
}
console.log(JSON.stringify(levelOrder(root)));
// [["src"],["components","lib","index.ts"],["Button.tsx","Navbar.tsx","api.ts"]]
រាល់ជុំនៃ loop ដោះស្រាយមួយ level ទាំងមូល។ flatMap ប្រមូល child ទាំងអស់នៃ level បច្ចុប្បន្នចូលក្នុង level បន្ទាប់។ លទ្ធផលគឺជា array នៃ level ដែលក៏ជាអ្វីដែលអ្នកត្រូវការយ៉ាងពិតប្រាកដដើម្បីគូរ tree ម្តងមួយជួរ ឬដើម្បីឆ្លើយសំណួរ "តើអ្វីនៅ level ទីបី?"។
តើគួរប្រើមួយណា?
| សំណួរ | ប្រើ |
|---|---|
| ដោះស្រាយ child មុន parent (លុប, បូកទំហំ) | DFS postorder |
| ដោះស្រាយ parent មុន child (ចម្លង, បោះពុម្ព, serialize) | DFS preorder |
| ទៅមើលតាមលំដាប់រៀបចំ (binary search tree) | DFS inorder |
| រក ការផ្គូផ្គងដែល ជិតបំផុត ទៅ root | BFS |
| ម្តងមួយជួរ ម្តងមួយ level | BFS |
ទាំងពីរទៅមើលគ្រប់ node ម្តងពិតប្រាកដ ដូច្នេះទាំងពីរមានតម្លៃពេលវេលា O(n) សម្រាប់ n node។ DFS ប្រើអង្គចងចាំរហូតដល់កម្ពស់របស់ tree។ BFS ប្រើអង្គចងចាំរហូតដល់ level ដែលទូលាយបំផុត ដែលសម្រាប់ tree ទូលាយអាចមានប្រហែលពាក់កណ្តាលនៃ node ទាំងអស់។ ចងចាំថា "ខ្លីបំផុតតាមចំនួនជំហានមានន័យថា BFS"។ វានឹងត្រឡប់មកវិញក្នុងជំពូក Graph។
6. Binary Tree និង Binary Search Tree
Binary tree គឺជា tree ដែលគ្រប់ node មាន child យ៉ាងច្រើនពីរ ហៅថា left និង right។ ការដាក់កំហិតតូចនេះធ្វើឲ្យ code សាមញ្ញជាង (field ពីរមានឈ្មោះ ជំនួសឲ្យបញ្ជី) ហើយបើកទ្វារទៅរកគំនិតដ៏មានអានុភាព។
Binary search tree (BST) គឺជា binary tree ដែលមានច្បាប់បន្ថែមមួយ ដែលអនុវត្តនៅ រាល់ node៖
- អ្វីៗទាំងអស់ក្នុង subtree ខាងឆ្វេង តូចជាង node
- អ្វីៗទាំងអស់ក្នុង subtree ខាងស្តាំ ធំជាង node
50
/ \
30 70
/ \ / \
20 40 60 80
ពិនិត្យ node 50៖ ខាងឆ្វេងមាន 30, 20, 40 ទាំងអស់តូចជាង។ ខាងស្តាំមាន 70, 60, 80 ទាំងអស់ធំជាង។ ពិនិត្យ node 30៖ ខាងឆ្វេងគឺ 20 ខាងស្តាំគឺ 40។ វាត្រឹមត្រូវរហូតដល់បាតខាងក្រោម។
នេះគឺជាល្បិចដូចគ្នានឹង binary search លើ array ដែលបានរៀបចំ ប៉ុន្តែត្រូវបានបញ្ចូលក្នុងរូបរាងនៃទិន្នន័យ។ នៅរាល់ node អ្នកអាចបោះបង់ពាក់កណ្តាលនៃ tree។ ហើយឥឡូវនេះយើងអាចធ្វើការដើរលំដាប់ទីបីបាន។ Inorder (left, node, right) ទៅមើល BST តាម លំដាប់រៀបចំ៖
20, 30, 40, 50, 60, 70, 80
នោះគឺជាលក្ខណៈសម្បត្តិដែលគួរចងចាំ៖ ការ traverse BST ដោយ inorder ផ្តល់លទ្ធផលតាមលំដាប់រៀបចំ។ វាក៏ជាមូលដ្ឋាននៃសំណួរសម្ភាសន៍បុរាណមួយដែរ (ផ្នែកទី 10)។
នេះគឺជា node និងរចនាសម្ព័ន្ធគ្រោងនៃ class។ យើងប្រើ function compare ដើម្បីឲ្យ tree ដំណើរការសម្រាប់លេខ string ឬ object (គំនិតដូចគ្នានឹង comparator ដែលអ្នកបានបញ្ជូនទៅ Array.sort ក្នុងជំពូក Sorting)៖
class BSTNode<T> {
left: BSTNode<T> | null = null;
right: BSTNode<T> | null = null;
constructor(public value: T) {}
}
function defaultCompare<T>(a: T, b: T): number {
return a < b ? -1 : a > b ? 1 : 0;
}
class BinarySearchTree<T> {
private root: BSTNode<T> | null = null;
private count = 0;
constructor(private compare: (a: T, b: T) => number = defaultCompare) {}
get size(): number {
return this.count;
}
}
null មានន័យថា "គ្មាន child នៅទីនេះ"។ Method នីមួយៗខាងក្រោមត្រូវដាក់នៅខាងក្នុង class។
7. ការស្វែងរក និងការបញ្ចូល
ការស្វែងរកធ្វើតាមច្បាប់។ ប្រៀបធៀបគោលដៅជាមួយ node បច្ចុប្បន្ន។ ស្មើមានន័យថារកឃើញ។ តូចជាងមានន័យថាទៅឆ្វេង។ ធំជាងមានន័យថាទៅស្តាំ។ ប្រសិនបើអ្នកធ្លាក់ចេញពី tree (ជួប null) នោះ value មិនមានទេ។
has(value: T): boolean {
let node = this.root;
while (node !== null) {
const c = this.compare(value, node.value);
if (c === 0) return true;
node = c < 0 ? node.left : node.right;
}
return false;
}
ស្វែងរក 60 ក្នុង tree របស់យើង៖ 50 ធំជាង ដូច្នេះទៅស្តាំ។ 70 តូចជាង ដូច្នេះទៅឆ្វេង។ 60 រកឃើញ។ បីជំហាន ជំនួសឲ្យការពិនិត្យតម្លៃទាំងប្រាំពីរ។ ស្វែងរក 65៖ 50 ទៅស្តាំ; 70 ទៅឆ្វេង; 60 ទៅស្តាំ; នោះគឺ null។ រកមិនឃើញ។
ការបញ្ចូលប្រើការដើរដូចគ្នា ហើយដាក់ value ថ្មីនៅកន្លែងទទេដំបូងដែលការស្វែងរកនឹងបញ្ចប់។ នោះរក្សាច្បាប់ BST ឲ្យនៅពិត ដោយមិនចាំបាច់ផ្លាស់ទី node ផ្សេងឡើយ។
insert(value: T): boolean {
if (this.root === null) {
this.root = new BSTNode(value);
this.count++;
return true;
}
let node = this.root;
while (true) {
const c = this.compare(value, node.value);
if (c === 0) return false; // already there: no duplicates
const side = c < 0 ? "left" : "right";
const next = node[side];
if (next === null) {
node[side] = new BSTNode(value);
this.count++;
return true;
}
node = next;
}
}
ល្បិច node[side] ជ្រើស node.left ឬ node.right តាមឈ្មោះ ដូច្នេះយើងសរសេរតក្កវិជ្ជា "ចុះក្រោម ឬភ្ជាប់" តែម្តង ជំនួសឲ្យពីរដង។ TypeScript ពេញចិត្តព្រោះ side អាចជាបានតែ "left" ឬ "right" ប៉ុណ្ណោះ។
យើងបដិសេធ duplicate ហើយ return false ដែលធ្វើឲ្យ tree ដើរដូច set ដែលបានរៀបចំ។ ប្រសិនបើអ្នកត្រូវការ duplicate អ្នកអាចរក្សាទុកចំនួនក្នុងរាល់ node ដូចជា frequency map ពីជំពូក Hash Map និង Set។
Helper តូចពីរ បន្ទាប់មកការដើរតាមលំដាប់ និងការដើរតាម level៖
min(): T | undefined {
if (this.root === null) return undefined;
return minNode(this.root).value;
}
max(): T | undefined {
let node = this.root;
if (node === null) return undefined;
while (node.right !== null) node = node.right;
return node.value;
}
inOrder(): T[] {
const out: T[] = [];
const walk = (node: BSTNode<T> | null): void => {
if (node === null) return;
walk(node.left);
out.push(node.value);
walk(node.right);
};
walk(this.root);
return out;
}
Value តូចបំផុតត្រូវបានរកឃើញដោយទៅឆ្វេងរហូតដល់មិនអាចទៅបាន ហើយ value ធំបំផុតដោយទៅស្តាំ។ ជាមួយ minNode ជា function ធម្មតានៅខាងក្រៅ class៖
function minNode<T>(node: BSTNode<T>): BSTNode<T> {
while (node.left !== null) node = node.left;
return node;
}
8. ការលុប Node
ការបញ្ចូល និងការស្វែងរកគឺងាយស្រួល។ ការលុប គឺជាផ្នែកនៃ BST ដែលមនុស្សភ័យខ្លាច ប៉ុន្តែមានតែបីករណីប៉ុណ្ណោះ។ រក node ឲ្យឃើញ បន្ទាប់មក៖
- វាជា leaf (គ្មាន child)។ គ្រាន់តែដកវាចេញ។ ជំនួសខ្សែតដោយ
null។ - វាមាន child មួយ។ ជំនួសវាដោយ child នោះ។ subtree ទាំងមូលរបស់ child ឡើងលើមួយ level ហើយច្បាប់នៅតែពិត។
- វាមាន child ពីរ។ អ្នកមិនអាចដកវាចេញដោយផ្ទាល់បានទេ ព្រោះមាន subtree ពីរព្យួរពីវា។ ផ្ទុយទៅវិញ ត្រូវរក in-order successor របស់ node៖ value តូចបំផុតក្នុង right subtree របស់វា។ ចម្លង value នោះចូល node នេះ បន្ទាប់មកលុប successor ពី right subtree។ Successor គ្មាន left child (វាតូចបំផុត) ដូច្នេះការលុបវាគឺជាករណីទី 1 ឬទី 2។
ហេតុអ្វីបានជា successor? វាជា value បន្ទាប់ក្នុងលំដាប់រៀបចំ។ អ្វីៗនៅខាងឆ្វេងតូចជាងវា ហើយអ្វីៗផ្សេងទៀតនៅខាងស្តាំធំជាងវា ដូច្នេះវាអាចអង្គុយជំនួសកន្លែង node ដែលត្រូវលុប ដោយមិនបំបែកច្បាប់។
យក tree របស់យើង ហើយលុប 30 ដែលមាន child ពីរ (20 និង 40)៖
Before: 50 After: 50
/ \ / \
30 70 40 70
/ \ / \ / / \
20 40 60 80 20 60 80
Successor គឺជា value តូចបំផុតក្នុង right subtree របស់ 30 ដែលជា 40។ យើងចម្លង 40 ចូល node ហើយដក leaf 40 ចាស់ចេញ។
នេះគឺជា code។ យើងសរសេរវាជា recursive ព្រោះជំហាននីមួយៗ return "អ្វីដែលគួរនៅកន្លែងនេះឥឡូវនេះ"៖
delete(value: T): boolean {
const before = this.count;
this.root = this.deleteFrom(this.root, value);
return this.count < before;
}
private deleteFrom(node: BSTNode<T> | null, value: T): BSTNode<T> | null {
if (node === null) return null; // not found: nothing changes
const c = this.compare(value, node.value);
if (c < 0) {
node.left = this.deleteFrom(node.left, value);
return node;
}
if (c > 0) {
node.right = this.deleteFrom(node.right, value);
return node;
}
// Found it. Cases 1 and 2: zero or one child.
if (node.left === null) {
this.count--;
return node.right;
}
if (node.right === null) {
this.count--;
return node.left;
}
// Case 3: two children. Copy the successor here, then delete it below.
const successor = minNode(node.right);
node.value = successor.value;
node.right = this.deleteFrom(node.right, successor.value);
return node;
}
អានវាថា៖ "លុបពីខាងឆ្វេង ឬពីខាងស្តាំ ហើយរក្សាអ្វីដែលត្រឡប់មកវិញ"។ នៅពេលរកឃើញ node leaf return node.right ដែលជា null ដូច្នេះវាបាត់ទៅ។ Node ដែលមានតែ right child return child នោះ ហើយបើមានតែ left child ក៏ return child នោះដែរ។ មានតែករណី child ពីរប៉ុណ្ណោះដែលធ្វើល្បិចចម្លង ហើយ recursive call នៅចុងបញ្ចប់គឺជាអ្វីដែល decrement count។
9. កម្ពស់ និងតុល្យភាព
តើការស្វែងរក ការបញ្ចូល និងការលុបលឿនប៉ុណ្ណា? ម្នាក់ៗដើរតាម ផ្លូវមួយពី root ចុះក្រោម ដូច្នេះតម្លៃគឺជា កម្ពស់ របស់ tree គឺ h។ នេះគឺជារបៀបគណនាវា៖
height(): number {
const h = (node: BSTNode<T> | null): number =>
node === null ? -1 : 1 + Math.max(h(node.left), h(node.right));
return h(this.root);
}
កម្ពស់របស់ tree ទទេគឺ -1 leaf គឺ 1 + max(-1, -1) = 0 ហើយអ្វីៗផ្សេងទៀតគឺច្រើនជាង child ដែលខ្ពស់ជាងរបស់វាមួយ។ ចាំ postorder៖ child ត្រូវបានគណនាមុន។
ឥឡូវនេះសំណួរសំខាន់៖ តើ h ធំប៉ុនណា? វាអាស្រ័យលើ រូបរាង របស់ tree ហើយរូបរាងអាស្រ័យលើលំដាប់ដែលអ្នកបានបញ្ចូលទិន្នន័យ។
បញ្ចូល 50, 30, 70, 20, 40, 60, 80 ហើយអ្នកនឹងបាន tree ដែលរាលដាលល្អពីមុន ដែលមាន height 2 សម្រាប់ 7 node។ ឥឡូវបញ្ចូល 1, 2, 3, 4, 5, 6, 7 តាមលំដាប់៖
1
\
2
\
3
\
4 ...and so on down to 7
រាល់ value ថ្មីធំជាងអ្វីៗទាំងអស់ដែលមានរហូតមកដល់ពេលនេះ ដូច្នេះវាតែងតែទៅខាងស្តាំ។ BST បានប្រែទៅជា linked list ដែលមាន height 6 សម្រាប់ 7 node។ ការស្វែងរកវាគឺជាការស្កេនតាមលំដាប់ (linear scan)។ ទិន្នន័យដែលបានរៀបចំ (ឬរៀបចំបញ្ច្រាស) គឺជា input អាក្រក់បំផុតសម្រាប់ BST ធម្មតា ហើយទិន្នន័យដែលបានរៀបចំគឺជារឿងធម្មតាណាស់ក្នុងជីវិតពិត។
| រូបរាង tree | Height សម្រាប់ n node | ស្វែងរក / បញ្ចូល / លុប |
|---|---|---|
| មានតុល្យភាព (រាលដាលស្មើគ្នា) | ប្រហែល log₂ n | O(log n) |
| Skewed (ជាបន្ទាត់) | n - 1 | O(n) |
សម្រាប់ value 1,000,000 tree ដែលមានតុល្យភាពមាន height ប្រហែល 19 ដូច្នេះរាល់ការស្វែងរកពិនិត្យយ៉ាងច្រើនប្រហែល 20 node។ Tree ដែល skewed ដែលមាន value ដូចគ្នាអាចប្រើពេលមួយលានជំហាន។ ភាពខុសគ្នាគឺធំធេងណាស់ ដែលជាមូលហេតុដែល tree ដែលមានតុល្យភាពមានសារៈសំខាន់។
នេះជាមូលហេតុដែល code production ស្ទើរតែមិនដែលប្រើ BST ធម្មតាទេ។ វាប្រើ tree ដែលតុល្យភាពខ្លួនឯង (self-balancing) ដែលរៀបចំខ្លួនវាឡើងវិញដោយចលនាតូចៗហៅថា rotation បន្ទាប់ពីរាល់ការបញ្ចូល ឬការលុប ដូច្នេះ height នៅជិត log₂ n មិនថាទិន្នន័យមកតាមលំដាប់អ្វីឡើយ។ ដែលល្បីជាងគេគឺ AVL tree និង red-black tree (ប្រើនៅក្នុង standard library របស់ Java និង C++ សម្រាប់ sorted map)។ Database ប្រើញាតិជិតស្និទ្ធមួយគឺ B-tree ដែលមាន child ច្រើនក្នុងមួយ node ដើម្បីអានទំព័រពី disk តិចជាង។
អ្នកមិនចាំបាច់អនុវត្ត rotation សម្រាប់ការសម្ភាសន៍ភាគច្រើនទេ។ ប៉ុន្តែអ្នកត្រូវតែអាចនិយាយក្នុងមួយប្រយោគថា ហេតុអ្វីបានជា BST ធម្មតាអាចថយចុះទៅ O(n) និងអ្វីជាដំណោះស្រាយ។ អ្នកក៏អាចរក្សា BST ធម្មតាឲ្យមានសុខភាពល្អ ដោយបញ្ចូលទិន្នន័យតាមលំដាប់ដែលបានលាយ (shuffle) ព្រោះលំដាប់ចៃដន្យមានទំនោរផ្តល់ tree ដែលមានតុល្យភាពប្រហែល។
ប្រៀបធៀប BST ជាមួយរចនាសម្ព័ន្ធដែលអ្នកស្គាល់រួចហើយ៖
| រចនាសម្ព័ន្ធ | ស្វែងរក | បញ្ចូល | លុប | លំដាប់រៀបចំ? |
|---|---|---|---|---|
| Array ដែលមិនបានរៀបចំ | O(n) | O(1) នៅចុង | O(n) | ទេ |
| Array ដែលបានរៀបចំ | O(log n) | O(n) | O(n) | បាទ/ចាស |
| Hash map / set | O(1) ជាមធ្យម | O(1) ជាមធ្យម | O(1) ជាមធ្យម | ទេ |
| BST មានតុល្យភាព | O(log n) | O(log n) | O(log n) | បាទ/ចាស |
| BST skewed | O(n) | O(n) | O(n) | បាទ/ចាស |
Hash map លឿនជាងសម្រាប់ការស្វែងរកធម្មតា ដូច្នេះហេតុអ្វីប្រើ BST? ព្រោះ BST រក្សាទិន្នន័យ តាមលំដាប់។ វាអាចឆ្លើយ "តើ value តូចបំផុតជាអ្វី?", "តើអ្វីមកបន្ទាប់ពី 45?" និង "ផ្តល់អ្វីៗទាំងអស់រវាង 30 និង 60 ឲ្យខ្ញុំ" ដោយមិនចាំបាច់ស្កេន ឬ sort។ Hash map មិនអាចធ្វើបានទេ។
10. បញ្ហាសម្ភាសន៍
សំណួរអំពី tree គឺស្ថិតក្នុងចំណោមសំណួរទូទៅបំផុតក្នុងការសម្ភាសន៍។ ស្ទើរតែទាំងអស់គឺជាលំនាំ recursive ដូចគ្នា៖ ដោះស្រាយវាសម្រាប់ left subtree ដោះស្រាយវាសម្រាប់ right subtree បន្ទាប់មកបូកបញ្ចូលចម្លើយទាំងពីរនៅ node នេះ។ ឧទាហរណ៍ទាំងនេះប្រើរូបរាង node តូចបំផុត ដើម្បីឲ្យអ្នកអាចសរសេរវានៅលើក្តារ whiteboard បាន៖
interface BinaryNode {
value: number;
left: BinaryNode | null;
right: BinaryNode | null;
}
const node = (
value: number,
left: BinaryNode | null = null,
right: BinaryNode | null = null,
): BinaryNode => ({ value, left, right });
ផ្ទៀងផ្ទាត់ BST។ ផ្តល់ tree មួយ តើវាជា BST ត្រឹមត្រូវឬទេ? ចម្លើយខុសទូទៅគ្រាន់តែពិនិត្យថា node នីមួយៗធំជាង left child របស់វា និងតូចជាង right child របស់វា។ នោះខកខាន node ជ្រៅដែលបំបែកច្បាប់ដែលកំណត់នៅខាងលើឆ្ងាយ។ ចម្លើយត្រឹមត្រូវគឺបញ្ជូនជួរដែលអនុញ្ញាតចុះក្រោម៖
function isValidBST(
root: BinaryNode | null,
low = -Infinity,
high = Infinity,
): boolean {
if (root === null) return true;
if (root.value <= low || root.value >= high) return false;
return (
isValidBST(root.left, low, root.value) &&
isValidBST(root.right, root.value, high)
);
}
const good = node(50, node(30, node(20), node(40)), node(70));
const bad = node(50, node(30, node(20), node(60)), node(70));
console.log(isValidBST(good)); // true
console.log(isValidBST(bad)); // false (60 is in the left subtree of 50)
ក្នុង bad នោះ 60 គឺជា right child ដែលល្អសម្រាប់ 30 ប៉ុន្តែវាស្ថិតក្នុង left subtree របស់ 50 ហើយធំជាង 50។ ការទៅឆ្វេងពី 50 កំណត់ដែនកំណត់ខាងលើជា 50 ដូច្នេះ 60 ធ្លាក់។ វិធីមួយទៀតគឺរត់ inorder ហើយពិនិត្យថាលទ្ធផលកើនឡើងយ៉ាងតឹងរ៉ឹង។ ទាំងពីរគឺ O(n)។
ត្រឡប់ binary tree (Invert)។ ប្តូរ left និង right នៅរាល់ node។ វាស្តាប់ទៅពិបាក ប៉ុន្តែគ្រាន់តែប៉ុន្មានបន្ទាត់ប៉ុណ្ណោះ៖
function invert(root: BinaryNode | null): BinaryNode | null {
if (root === null) return null;
const left = invert(root.left);
const right = invert(root.right);
root.left = right;
root.right = left;
return root;
}
សម្រាប់ 2 ដែលមាន child 1 និង 3 លទ្ធផល inorder ប្តូរពី 1, 2, 3 ទៅ 3, 2, 1។
Lowest common ancestor ក្នុង BST។ ផ្តល់ value ពីរ រក node ទាបបំផុតដែលមានពួកវាទាំងពីរនៅពីក្រោម (ឬជាមួយក្នុងចំណោមពួកវា)។ ក្នុង BST អ្នកមិនចាំបាច់ស្វែងរកទាំងសងខាងទេ។ ប្រសិនបើ value ទាំងពីរតូចជាង node បច្ចុប្បន្ន ទៅឆ្វេង។ ប្រសិនបើទាំងពីរធំជាង ទៅស្តាំ។ បើមិនដូច្នោះទេ ផ្លូវបំបែកនៅទីនេះ ដូច្នេះនេះគឺជាចម្លើយ៖
function lowestCommonAncestor(root: BinaryNode, p: number, q: number): number {
let current = root;
while (true) {
if (p < current.value && q < current.value) current = current.left!;
else if (p > current.value && q > current.value) current = current.right!;
else return current.value;
}
}
const tree = node(
50,
node(30, node(20), node(40)),
node(70, node(60), node(80)),
);
console.log(lowestCommonAncestor(tree, 20, 40)); // 30
console.log(lowestCommonAncestor(tree, 20, 60)); // 50
console.log(lowestCommonAncestor(tree, 30, 40)); // 30 (a node can be its own ancestor)
វារត់ក្នុងពេល O(h) និងអង្គចងចាំ O(1)។ ! ប្រាប់ TypeScript ថា "ជឿខ្ញុំ នេះមិនមែន null ទេ"។ វាមានសុវត្ថិភាពនៅទីនេះ ព្រោះយើងសន្មត់ថា value ទាំងពីរមានក្នុង tree។
Right side view។ ប្រសិនបើអ្នកឈរនៅខាងស្តាំនៃ tree តើអ្នកនឹងឃើញ node មួយណាខ្លះ? វាគឺជា node ចុងក្រោយនៃរាល់ level ដូច្នេះវាគឺជាការ traverse តាម level order ដោយមានការកែបន្តិចបន្តួច៖
function rightSideView(root: BinaryNode | null): number[] {
if (root === null) return [];
const view: number[] = [];
let level = [root];
while (level.length > 0) {
view.push(level[level.length - 1].value);
level = level.flatMap((n) =>
[n.left, n.right].filter((c): c is BinaryNode => c !== null),
);
}
return view;
}
console.log(rightSideView(tree)); // [ 50, 70, 80 ]
Filter ដែលមាន c is BinaryNode គឺជា type guard របស់ TypeScript។ វាដកធាតុ null ចេញ ហើយប្រាប់ compiler ថាអ្វីដែលនៅសល់សុទ្ធតែជា node ពិត។
Inorder បែប iterative។ អ្នកសម្ភាសន៍តែងតែសួរបន្តថា "ឥឡូវធ្វើវាដោយគ្មាន recursion"។ ប្រើ stack ផ្ទាល់ខ្លួនរបស់អ្នក៖
function inOrderIterative(root: BinaryNode | null): number[] {
const out: number[] = [];
const stack: BinaryNode[] = [];
let current = root;
while (current !== null || stack.length > 0) {
while (current !== null) {
stack.push(current); // remember the way back
current = current.left;
}
const top = stack.pop()!;
out.push(top.value);
current = top.right;
}
return out;
}
វាទៅឆ្វេងឲ្យបានឆ្ងាយបំផុត ដោយ push node ពេលដើរ បន្ទាប់មក pop មួយ (ទៅមើលវា) បន្ទាប់មកផ្លាស់ទៅ right child របស់ node នោះ ហើយធ្វើម្តងទៀត។ Array stack ដើរតួជា call stack ដែលបានដើរតួក្នុងកំណែ recursive។
11. រូបមន្ត៖ tree.ts សម្រាប់ algolab
បង្កើត src/bst.ts ក្នុង algolab ជាមួយ helper របស់ n-ary tree និង binary search tree ពីជំពូកនេះ៖
// ---------- A general tree (DOM, folders, menus) ----------
export class TreeNode<T> {
children: TreeNode<T>[] = [];
constructor(public value: T) {}
add(value: T): TreeNode<T> {
const child = new TreeNode(value);
this.children.push(child);
return child;
}
}
export function preorder<T>(root: TreeNode<T>, visit: (v: T) => void): void {
visit(root.value);
for (const child of root.children) preorder(child, visit);
}
export function postorder<T>(root: TreeNode<T>, visit: (v: T) => void): void {
for (const child of root.children) postorder(child, visit);
visit(root.value);
}
export function levelOrder<T>(root: TreeNode<T>): T[][] {
const levels: T[][] = [];
let current: TreeNode<T>[] = [root];
while (current.length > 0) {
levels.push(current.map((n) => n.value));
current = current.flatMap((n) => n.children);
}
return levels;
}
export function treeHeight<T>(root: TreeNode<T> | null): number {
if (root === null) return -1;
let tallest = -1;
for (const child of root.children) {
tallest = Math.max(tallest, treeHeight(child));
}
return tallest + 1;
}
// ---------- A binary search tree ----------
class BSTNode<T> {
left: BSTNode<T> | null = null;
right: BSTNode<T> | null = null;
constructor(public value: T) {}
}
export class BinarySearchTree<T> {
private root: BSTNode<T> | null = null;
private count = 0;
constructor(private compare: (a: T, b: T) => number = defaultCompare) {}
get size(): number {
return this.count;
}
insert(value: T): boolean {
if (this.root === null) {
this.root = new BSTNode(value);
this.count++;
return true;
}
let node = this.root;
while (true) {
const c = this.compare(value, node.value);
if (c === 0) return false;
const side = c < 0 ? "left" : "right";
const next = node[side];
if (next === null) {
node[side] = new BSTNode(value);
this.count++;
return true;
}
node = next;
}
}
has(value: T): boolean {
let node = this.root;
while (node !== null) {
const c = this.compare(value, node.value);
if (c === 0) return true;
node = c < 0 ? node.left : node.right;
}
return false;
}
min(): T | undefined {
if (this.root === null) return undefined;
return minNode(this.root).value;
}
max(): T | undefined {
let node = this.root;
if (node === null) return undefined;
while (node.right !== null) node = node.right;
return node.value;
}
delete(value: T): boolean {
const before = this.count;
this.root = this.deleteFrom(this.root, value);
return this.count < before;
}
private deleteFrom(node: BSTNode<T> | null, value: T): BSTNode<T> | null {
if (node === null) return null;
const c = this.compare(value, node.value);
if (c < 0) {
node.left = this.deleteFrom(node.left, value);
return node;
}
if (c > 0) {
node.right = this.deleteFrom(node.right, value);
return node;
}
if (node.left === null) {
this.count--;
return node.right;
}
if (node.right === null) {
this.count--;
return node.left;
}
// Two children: copy the in-order successor here, then delete it below.
const successor = minNode(node.right);
node.value = successor.value;
node.right = this.deleteFrom(node.right, successor.value);
return node;
}
inOrder(): T[] {
const out: T[] = [];
const walk = (node: BSTNode<T> | null): void => {
if (node === null) return;
walk(node.left);
out.push(node.value);
walk(node.right);
};
walk(this.root);
return out;
}
levelOrder(): T[][] {
const levels: T[][] = [];
let current: BSTNode<T>[] = this.root === null ? [] : [this.root];
while (current.length > 0) {
levels.push(current.map((n) => n.value));
const next: BSTNode<T>[] = [];
for (const n of current) {
if (n.left !== null) next.push(n.left);
if (n.right !== null) next.push(n.right);
}
current = next;
}
return levels;
}
height(): number {
const h = (node: BSTNode<T> | null): number =>
node === null ? -1 : 1 + Math.max(h(node.left), h(node.right));
return h(this.root);
}
}
function minNode<T>(node: BSTNode<T>): BSTNode<T> {
while (node.left !== null) node = node.left;
return node;
}
function defaultCompare<T>(a: T, b: T): number {
return a < b ? -1 : a > b ? 1 : 0;
}
ឥឡូវនេះ test។ បង្កើត src/bst.test.ts៖
import { test } from "node:test";
import assert from "node:assert/strict";
import {
TreeNode,
preorder,
postorder,
levelOrder,
treeHeight,
BinarySearchTree,
} from "./bst.ts";
function sample(): TreeNode<string> {
const root = new TreeNode("src");
const components = root.add("components");
components.add("Button.tsx");
components.add("Navbar.tsx");
root.add("lib").add("api.ts");
root.add("index.ts");
return root;
}
test("preorder visits a parent before its children", () => {
const seen: string[] = [];
preorder(sample(), (v) => seen.push(v));
assert.deepEqual(seen, [
"src", "components", "Button.tsx", "Navbar.tsx", "lib", "api.ts", "index.ts",
]);
});
test("postorder visits children before the parent", () => {
const seen: string[] = [];
postorder(sample(), (v) => seen.push(v));
assert.deepEqual(seen, [
"Button.tsx", "Navbar.tsx", "components", "api.ts", "lib", "index.ts", "src",
]);
});
test("levelOrder groups by depth, and height counts edges", () => {
assert.deepEqual(levelOrder(sample()), [
["src"],
["components", "lib", "index.ts"],
["Button.tsx", "Navbar.tsx", "api.ts"],
]);
assert.equal(treeHeight(sample()), 2);
assert.equal(treeHeight(null), -1);
});
test("BST keeps values sorted and ignores duplicates", () => {
const bst = new BinarySearchTree<number>();
for (const n of [50, 30, 70, 20, 40, 60, 80]) bst.insert(n);
assert.deepEqual(bst.inOrder(), [20, 30, 40, 50, 60, 70, 80]);
assert.equal(bst.insert(60), false);
assert.equal(bst.size, 7);
assert.equal(bst.has(60), true);
assert.equal(bst.has(65), false);
assert.equal(bst.min(), 20);
assert.equal(bst.max(), 80);
assert.equal(bst.height(), 2);
});
test("BST delete handles leaf, one child, and two children", () => {
const bst = new BinarySearchTree<number>();
for (const n of [50, 30, 70, 20, 40, 60, 80]) bst.insert(n);
assert.equal(bst.delete(20), true); // leaf
assert.equal(bst.delete(30), true); // now has one child (40)
assert.equal(bst.delete(50), true); // two children (root)
assert.equal(bst.delete(99), false); // not there
assert.deepEqual(bst.levelOrder(), [[60], [40, 70], [80]]);
assert.equal(bst.size, 4);
});
test("sorted input makes a lopsided tree", () => {
const bst = new BinarySearchTree<number>();
for (let i = 1; i <= 7; i++) bst.insert(i);
assert.equal(bst.height(), 6);
});
រត់ test៖
npx tsx --test
អ្នកគួរតែឃើញ test ទាំងប្រាំមួយឆ្លងកាត់។ មើល test លុប៖ វាលុប leaf (20) បន្ទាប់មក node ដែលឥឡូវមាន child តែមួយ (30 បន្ទាប់ពី 20 បាត់) បន្ទាប់មក root ដែលមាន child ពីរ (50)។ Test ចុងក្រោយបញ្ជាក់ការព្រមានពីផ្នែកទី 9 ដោយមាន height 6 សម្រាប់ត្រឹមតែ 7 value។
បន្ថែម export ទៅ src/index.ts៖
export * from "./bst.ts";
បន្ទាប់មកបង្កើត demo-trees.ts នៅក្នុង root របស់ project៖
import {
BinarySearchTree,
TreeNode,
levelOrder,
preorder,
postorder,
} from "./src/bst.ts";
const bst = new BinarySearchTree<number>();
for (const n of [50, 30, 70, 20, 40, 60, 80]) bst.insert(n);
console.log("sorted:", bst.inOrder().join(" "));
console.log("levels:", JSON.stringify(bst.levelOrder()));
console.log("height:", bst.height());
console.log("has 60:", bst.has(60), "| has 65:", bst.has(65));
bst.delete(30);
console.log("after delete 30:", JSON.stringify(bst.levelOrder()));
const root = new TreeNode("src");
root.add("components").add("Button.tsx");
root.add("index.ts");
const before: string[] = [];
const after: string[] = [];
preorder(root, (v) => before.push(v));
postorder(root, (v) => after.push(v));
console.log("preorder:", before.join(", "));
console.log("postorder:", after.join(", "));
console.log("levels:", JSON.stringify(levelOrder(root)));
រត់វា៖
npx tsx demo-trees.ts
sorted: 20 30 40 50 60 70 80
levels: [[50],[30,70],[20,40,60,80]]
height: 2
has 60: true | has 65: false
after delete 30: [[50],[40,70],[20,60,80]]
preorder: src, components, Button.tsx, index.ts
postorder: Button.tsx, components, index.ts, src
levels: [["src"],["components","index.ts"],["Button.tsx"]]
បន្ទាប់ពីលុប 30 tree មាន 40 នៅកន្លែងដែល 30 ធ្លាប់នៅ។ នោះគឺជាល្បិច in-order successor ពីផ្នែកទី 8 ដោយ 20 នៅតែព្យួរនៅខាងឆ្វេង។
12. សាកល្បងដោយខ្លួនឯង
- គូរ និងដើរ។ គូរ BST ដែលអ្នកទទួលបានពីការបញ្ចូល
8, 3, 10, 1, 6, 14, 4, 7, 13តាមលំដាប់នោះ។ សរសេរបញ្ជី preorder, inorder, postorder និង level-order របស់វាដោយដៃ បន្ទាប់មកពិនិត្យក្នុង code។ តើ height របស់វាជាប៉ុន្មាន? - រាប់ និងបូក។ បន្ថែម
count(root)(ចំនួន node) និងsumValues(root)(សម្រាប់TreeNode<number>)។ ទាំងពីរគឺជា postorder ក្នុងស្មារតី។ តើcountreturn អ្វីសម្រាប់ node តែមួយ? - រក depth អតិបរមា។ សរសេរ
maxDepth(root: BinaryNode | null)ដែល return ចំនួន node លើផ្លូវវែងបំផុតពី root ចុះទៅ leaf។ តើវាខុសពីheight()ក្នុងជំពូកនេះយ៉ាងដូចម្តេច? - ធ្វើឲ្យវា skewed។ បញ្ចូល
1ដល់1000តាមលំដាប់ចូលBinarySearchTreeហើយវាស់ពេលការហៅhas(1000)1000 ដង។ ឥឡូវបញ្ចូលលេខដូចគ្នាតាមលំដាប់ដែលបានលាយ ហើយវាស់ពេលម្តងទៀត (លាយដោយប្រើគំនិត Fisher-Yates ពីជំពូក Probability ក្នុង Math for Programmers)។ តើ height ខុសគ្នាប៉ុណ្ណា? - សម្ភាសន៍៖ kth smallest។ ផ្តល់ BST និងលេខ
kreturn value តូចបំផុតទីk។ ព័ត៌មានជំនួយ៖ inorder ផ្តល់លំដាប់រៀបចំ ដូច្នេះឈប់បន្ទាប់ពីទៅមើលkដង។ តើអ្នកអាចធ្វើវាដោយមិនប្រមូល array ដែលបានរៀបចំទាំងមូលបានទេ? - សម្ភាសន៍៖ tree ដូចគ្នាឬទេ? សរសេរ
isSameTree(a, b)ដែល returntrueនៅពេល binary tree ពីរមានរូបរាងដូចគ្នា និង value ដូចគ្នា។ ប្រៀបធៀប node បន្ទាប់មក left subtree បន្ទាប់មក right subtree។ - សម្ភាសន៍៖ serialize។ សរសេរ
serialize(root)ដែលបំប្លែង binary tree ទៅជា string ដូចជា"1,2,#,#,3,#,#"ដោយប្រើ preorder ហើយប្រើ#សម្រាប់nullនិងdeserialize(text)ដែលសាងសង់វាឡើងវិញ។ ហេតុអ្វីអ្នកត្រូវការសញ្ញា#?
Tree គឺជារបៀបរក្សាទុកទិន្នន័យដែលបែកមែក ហើយ binary search tree គឺជារបៀបរក្សាវាឲ្យនៅតាមលំដាប់។ បន្ទាប់មកគឺ tree ដែលមានច្បាប់ខុសគ្នា ដែល item តូចបំផុតត្រូវតែនៅកំពូលជានិច្ច ដើម្បីឲ្យអ្នកចាប់យកវាបានភ្លាមៗ។ ក្នុង Heap និង Priority Queue យើងសាងសង់មួយលើ array ធម្មតា ហើយប្រើវាដើម្បីរក item K ដំបូងក្នុងបញ្ជី និងកំណត់ពេលការងារតាមអាទិភាព។
ទិញកាហ្វេជូនខ្ញុំ
ចូលចិត្តមេរៀននេះមែនទេ? សូមពិចារណាទិញកាហ្វេជូនខ្ញុំមួយកែវ ដើម្បីគាំទ្រការបង្កើតមាតិកាឥតគិតថ្លៃបន្ថែមទៀត!