You are on page 1of 26

MỘT SỐ LOẠI ĐỆ QUY

1. Đệ quy tuyến tính

Đệ quy tuyến tính là hàm đệ quy chỉ gọi chính nó một lần trong thân hàm. Hiểu
đơn giản là trong một hàm, nếu có duy nhất một câu lệnh gọi chính hàm đó thì
được gọi là hàm đệ quy tuyến tính.

Cứ sau mỗi lần gọi hàm factorial(), kết quả sẽ được đẩy vào Stack cho đến khi gặp
điều kiện dừng hàm sẽ kết thúc và trả về kết quả 1. Sau đó gọi Stack để tính kết
quả, cơ chế Stack sẽ lấy từ trên xuống vì vậy kết quả sẽ là : 1 * 1 * 2 * 3 * 4 * 5 =
120.

2. Đệ quy đuôi

Đệ quy đuôi là tất cả phép tính được thực hiện trước rồi mới gọi đệ quy sau cùng. 

Đệ quy đuôi là một trường hợp đặc biệt của đệ quy tuyến tính. Giống như tên của
nó, đệ quy đuôi là hàm thực hiện gọi đệ quy ở sau cùng.

3. Đệ quy nhị phân

Đệ quy nhị phân là dạng đệ quy gọi hai lần chính nó. Hiểu đơn giản là trong một
hàm đệ quy, mà có dòng lệnh gọi chính hàm đó hai lần.

4. Đệ quy đa tuyến

Một hàm được gọi là đệ quy đa tuyến nếu mỗi lần gọi đệ quy nó phát sinh ra
khoảng n lần gọi đệ quy khác. Thông thường câu lệnh gọi đệ quy được đặt trong
các vòng lặp.

5. Đệ quy lồng
Đệ quy lồng là loại đệ quy gọi đối số của nó là một đệ quy. Hiểu đơn giản là tham
số truyền vào của hàm đệ quy là một đệ quy.

6. Đệ quy tương hỗ

Đệ quy tương hỗ là loại đệ quy không gọi đệ quy trực tiếp chính nó, mà gọi một
hàm khác. Trong hàm khác lại gọi lại nó.

CÁC THUẬT TOÁN SẮP XẾP

Sắp xếp nổi bọt (Bubble Sort)

Sắp xếp nổi bọt hay bubble sort là thuật toán sắp xếp đầu tiên mà mình giới thiệu
đến các bạn và cũng là thuật toán đơn giản nhất trong các thuật toán mà mình sẽ
giới thiệu, ý tưởng của thuật toán này như sau:

Duyệt qua danh sách, làm cho các phần tử lớn nhất hoặc nhỏ nhất dịch chuyển về
phía cuối danh sách, tiếp tục lại làm phần tử lớn nhất hoặc nhỏ nhất kế đó dịch
chuyển về cuối hay chính là làm cho phần tử nhỏ nhất (hoặc lớn nhất) nổi lên, cứ
như vậy cho đến hết danh sách Cụ thể các bước thực hiện của giải thuật này như
sau:

Gán i = 0

Gán j = 0

Nếu A[j] > A[j + 1] thì đối chỗ A[j] và A[j + 1]


Nếu j < n – i – 1:

Đúng thì j = j + 1 và quay lại bước 3

Sai thì sang bước 5

Nếu i < n – 1:

Đúng thì i = i + 1 và quay lại bước 2

Sai thì dừng lại

Thật đơn giản đúng không nào, chúng ta hãy cùng cài đặt thuật toán này trong C++
nha.

void BubbleSort(int A[], int n)

for (int i = 0; i < n - 1; i++)

for (int j = 0; j < n - i - 1; j++)

if (A[j] > A[j + 1])

swap(A[j], A[j + 1]); // đổi chỗ A[j] và A[j + 1]

Sắp xếp nổi bọt là một thuật toán sắp xếp ổn định. Về độ phức tạp, do dùng hai
vòng lặp lồng vào nhau nên độ phức tạp thời gian trung bình của thuật toán này là
O(n2).

Các bạn có thể xem mình trình bày ý tưởng của giải thuật này trong bên dưới:

Đừng quên đăng ký kênh Youtube Khiêm Lê để ủng hộ mình nha!

Sắp xếp chọn (Selection Sort)


Sắp xếp chọn hay selection sort sẽ là thuật toán thứ hai mà mình giới thiệu đến các
bạn, ý tưởng của thuật toán này như sau: duyệt từ đầu đến phần tử kề cuối danh
sách, duyệt tìm phần tử nhỏ nhất từ vị trí kế phần tử đang duyệt đến hết, sau đó đổi
vị trí của phần tử nhỏ nhất đó với phần tử đang duyệt và cứ tiếp tục như vậy.

Cho mảng A có n phần tử chưa được sắp xếp. Cụ thể các bước của giải thuật này
áp dụng trên mảng A như sau:

Gán i = 0

Gán j = i + 1 và min = A[i]

Nếu j < n:

Nếu A[j] < A[min] thì min = j

j=j+1

Quay lại bước 3

Đổi chỗ A[min] và A[i]

Nếu i < n – 1:

Đúng thì i = i + 1 và quay lại bước 2

Sai thì dừng lại

Ý tưởng và từng bước giải cụ thể đã có, bây giờ mình sẽ sử dụng thuật toán này
trong C++:

void SelectionSort(int A[], int n)

int min;
for (int i = 0; i < n - 1; i++)

min = i; // tạm thời xem A[i] là nhỏ nhất

// Tìm phẩn tử nhỏ nhất trong đoạn từ A[i] đến A[n - 1]

for (int j = i + 1; j < n; j++)

if (A[j] < A[min]) // A[j] mà nhỏ hơn A[min] thì A[j] là nhỏ nhất

min = j; // lưu lại vị trí A[min] mới vừa tìm được

if (min != i) // nếu như A[min] không phải là A[i] ban đầu thì đổi chỗ

swap(A[i], A[min]);

Đối với thuật toán sắp xếp chọn, do sử dụng 2 vòng lặp lồng vào nhau, độ phức tạp
thời gian trung bình của thuật toán này là O(n2). Thuật toán sắp xếp chọn mình cài
đặt là thuật toán sắp xếp không ổn định, nó còn có một phiên bản khác cải tiến là
thuật toán sắp xếp chọn ổn định.

Sắp xếp chèn (Insertion Sort)

Sắp xếp chèn hay insertion sort là thuật toán tiếp theo mà mình giới thiệu, ý tưởng
của thuật toán này như sau: ta có mảng ban đầu gồm phần tử A[0] xem như đã sắp
xếp, ta sẽ duyệt từ phần tử 1 đến n – 1, tìm cách chèn những phần tử đó vào vị trí
thích hợp trong mảng ban đầu đã được sắp xếp.

Giả sử cho mảng A có n phần tử chưa được sắp xếp. Các bước thực hiện của thuật
toán áp dụng trên mảng A như sau:
Gán i = 1

Gán x = A[i] và pos = i – 1

Nếu pos >= 0 và A[pos] > x:

A[pos + 1] = A[pos]

pos = pos – 1

Quay lại bước 3

A[pos + 1] = x

Nếu i < n:

Đúng thì i = i + 1 và quay lại bước 2

Sai thì dừng lại

Bây giờ thì áp dụng nó vào trong C++ thôi!

void InsertionSort(int A[], int n)

int pos, x;

for (int i = 1; i < n; i++)

x = A[i]; // lưu lại giá trị của x tránh bị ghi đè khi dịch chuyển các phần tử

pos = i - 1;

// tìm vị trí thích hợp để chèn x

while (pos >= 0 && A[pos] > x)


{

// kết hợp với dịch chuyển phần tử sang phải để chừa chỗ cho x

A[pos + 1] = A[pos];

pos--;

// chèn x vào vị trí đã tìm được

A[pos + 1] = x;

Cũng tương tự như sắp xếp chọn, thuật toán sắp xếp chèn cũng có độ phức tạp thời
gian trung bình là O(n2) do có hai vòng lặp lồng vào nhau.

Sắp xếp trộn (Merge Sort)

Sắp xếp trộn (merge sort) là một thuật toán dựa trên kỹ thuật chia để trị, ý tưởng
của thuật toán này như sau: chia đôi mảng thành hai mảng con, sắp xếp hai mảng
con đó và trộn lại theo đúng thứ tự, mảng con được sắp xếp bằng cách tương tự.

Giả sử left là vị trí đầu và right là cuối mảng đang xét, cụ thể các bước của thuật
toán như sau:

Nếu mảng còn có thể chia đôi được (tức left < right)

Tìm vị trí chính giữa mảng

Sắp xếp mảng thứ nhất (từ vị trí left đến mid)

Sắp xếp mảng thứ 2 (từ vị trí mid + 1 đến right)


Trộn hai mảng đã sắp xếp với nhau

Bây giờ mình sẽ cài đặt thuật toán cụ thể trong C++ như sau:

// Hàm trộn hai mảng con vào nhau theo đúng thứ tự

void Merge(int A[], int left, int mid, int right)

int n1 = mid - left + 1; // Số phần tử của mảng thứ nhất

int n2 = right - mid; // Số phần tử của mảng thứ hai

// Tạo hai mảng tạm để lưu hai mảng con

int *LeftArr = new int[n1];

int *RightArr = new int[n2];

// Sao chép phần tử 2 mảng con vào mảng tạm

for (int i = 0; i < n1; i++)

LeftArr[i] = A[left + i];

for (int i = 0; i < n2; i++)

RightArr[i] = A[mid + 1 + i];

// current là vị trí hiện tại trong mảng A

int i = 0, j = 0, current = left;


// Trộn hai mảng vào nhau theo đúng thứ tự

while (i < n1 && j < n2)

if (LeftArr[i] <= RightArr[j])

A[current++] = LeftArr[i++];

else

A[current++] = RightArr[j++];

// Nếu mảng thứ nhất còn phần tử thì copy nó vào mảng A

while (i < n1)

A[current++] = LeftArr[i++];

// Nếu mảng thứ hai còn phần tử thì copy nó vào mảng A

while (j < n2)

A[current++] = RightArr[j++];

// Xóa hai mảng tạm đi

delete[] LeftArr, RightArr;

}
// Hàm chia đôi mảng và gọi hàm trộn

void _MergeSort(int A[], int left, int right)

// Kiểm tra xem còn chia đôi mảng được không

if (left < right)

// Tìm phần tử chính giữa

// left + (right - left) / 2 tương đương với (left + right) / 2

// việc này giúp tránh bị tràn số với left, right quá lớn

int mid = left + (right - left) / 2;

// Sắp xếp mảng thứ nhất

_MergeSort(A, left, mid);

// Sắp xếp mảng thứ hai

_MergeSort(A, mid + 1, right);

// Trộn hai mảng đã sắp xếp

Merge(A, left, mid, right);

}
// Hàm sắp xếp chính, được gọi khi dùng merge sort

void MergeSort(int A[], int n)

_MergeSort(A, 0, n - 1);

Về độ phức tạp, thuật toán Merge Sort có độ phức tạp thời gian trung bình là
O(nlog(n)), về không gian, do sử dụng mảng phụ để lưu trữ, và 2 mảng phụ dài
nhất là hai mảng phụ ở lần chia đầu tiên có tổng số phần tử bằng đúng số phần tử
của mảng nên độ phức tạp sẽ là O(n). Sắp xếp trộn là thuật toán sắp xếp ổn định.

Sắp xếp nhanh (Quick Sort)

Sắp xếp nhanh (quick sort) hay sắp xếp phân đoạn (Partition) là là thuật toán sắp
xếp dựa trên kỹ thuật chia để trị, cụ thể ý tưởng là: chọn một điểm làm chốt (gọi là
pivot), sắp xếp mọi phần tử bên trái chốt đều nhỏ hơn chốt và mọi phần tử bên phải
đều lớn hơn chốt, sau khi xong ta được 2 dãy con bên trái và bên phải, áp dụng
tương tự cách sắp xếp này cho 2 dãy con vừa tìm được cho đến khi dãy con chỉ còn
1 phần tử.

Cụ thể áp dụng thuật toán cho mảng như sau:

Chọn một phần tử làm chốt

Sắp xếp phần tử bên trái nhỏ hơn chốt

Sắp xếp phần tử bên phải nhỏ hơn chốt

Sắp xếp hai mảng con bên trái và bên phải pivot
Phần tử được chọn làm chốt rất quan trọng, nó quyết định thời gian thực thi của
thuật toán. Phần tử được chọn làm chốt tối ưu nhất là phần tử trung vị, phần tử này
làm cho số phần tử nhỏ hơn trong dãy bằng hoặc sấp xỉ số phần tử lớn hơn trong
dãy. Tuy nhiên, việc tìm phần tử này rất tốn kém, phải có thuật toán tìm riêng, từ
đó làm giảm hiệu suất của thuật toán tìm kiếm nhanh, do đó, để đơn giản, người ta
thường sử dụng phần tử chính giữa làm chốt.

Trong bài viết này, mình cũng sẽ sử dụng phần tử chính giữa làm chốt, thuật toán
cài đặt trong C++ như sau:

void Partition(int A[], int left, int right)

// Kiểm tra xem nếu mảng có 1 phần tử thì không cần sắp xếp

if (left >= right)

return;

int pivot = A[(left + right) / 2]; // Chọn phần tử chính giữa dãy làm chốt

// i là vị trí đầu và j là cuối đoạn

int i = left, j = right;

while (i < j)

while (A[i] < pivot) // Nếu phần tử bên trái nhỏ hơn pivot thì ok, bỏ qua

i++;
while (A[j] > pivot) // Nếu phần tử bên phải nhỏ hơn pivot thì ok, bỏ qua

j--;

// Sau khi kết thúc hai vòng while ở trên thì chắc chắn

// vị trí A[i] phải lớn hơn pivot và A[j] phải nhỏ hơn pivot

// nếu i < j

if (i <= j)

if (i < j) // nếu i != j (tức không trùng thì mới cần hoán đổi)

swap(A[i], A[j]); // Thực hiện đổi chổ ta được A[i] < pivot và A[j] >
pivot

i++;

j--;

// Gọi đệ quy sắp xếp dãy bên trái pivot

Partition(A, left, j);

// Gọi đệ quy sắp xếp dãy bên phải pivot

Partition(A, i, right);
}

// Hàm sắp xếp chính

void QuickSort(int A[], int n)

Partition(A, 0, n - 1);

Thuật toán sắp xếp nhanh không phải là thuật toán sắp xếp ổn định, tuy nhiên vẫn
có thể cải tiến nó thành thuật toán sắp xếp ổn định. Độ phức tạp thời gian trung
bình của thuật toán này là O(nlog(n)).

CÂY NHỊ PHÂN

Cây nhị phân (tiếng Anh: binary tree) là một cấu trúc dữ liệu cây mà mỗi nút có
nhiều nhất hai nút con, được gọi là con trái (left child) và con phải (right child).
Một định nghĩa đệ quy chỉ sử dụng các khái niệm lý thuyết tập hợp là cây nhị phân
không trống là một tuple (L, S, R), với L và R là các cây nhị phân hay tập hợp
rỗng và S là tập đơn (singleton set).[1] Một số tác giả cho phép cây nhị phân cũng
có thể là tập hợp trống.[2]

Cây nhị phân là một trường hợp đặc biệt của cấu trúc cây và nó cũng phổ biến
nhất. Đúng như tên gọi của nó, cây nhị phân có bậc là 2 và mỗi nút trong cây nhị
phân đều có bậc không quá 2.

Cây nhị phân


Các khái niệm

Có một số khái niệm khác về cây nhị phân các bạn cần nắm như sau:

Cây nhị phân đúng: là cây nhị phân mà mỗi nút của nó đều có bậc 2. Ví dụ như
hình trên, hoặc hình trên bỏ đi nút H và I cũng là cây nhị phân đúng.

Cây nhị phân đầy đủ là cây nhị phân có mức của các nút lá đều bằng nhau. Ví dụ
hình trên, tất cả các nút lá đều có mức 3.

Cây nhị phân tìm kiếm (sẽ tìm hiểu bên dưới)

Cây nhị phân cân bằng: số phần tử của cây con bên trái chênh lệch không quá 1 so
với cây con bên phải.

Định nghĩa cấu trúc nút

Nhìn vào hình, ta có thể dễ dàng phân tích được rằng, mỗi nút trong cây nhị phân
sẽ gồm 3 thành phần như sau:

Thành phần dữ liệu: có thể là bất kỳ kiểu dữ liệu nào.

Thành phần liên kết trái: lưu trữ địa chỉ của nút gốc của cây con bên trái. Kiểu dữ
liệu là con trỏ trỏ vào node.

Thành phân liên kết phải: lưu trữ địa chỉ của nút gốc của cây con bên phải. Kiểu dữ
liệu là con trỏ trỏ vào node.

Chúng ta sẽ có struct lưu trữ một node như sau – ở đây để đơn giản mình sử dụng
kiểu dữ liệu int cho thành phần dữ liệu của node:

struct Node

int data;
Node *left;

Node *right;

};

Khi tạo một nút node mới, chúng ta cần phải gán lại các thành phần của node để nó
không nhận giá trị rác, tránh lỗi không mong muốn. Chúng ta sẽ tạo một biến động
cho node và trả về địa chỉ của node đó, mình sẽ có đoạn code tạo node như sau:

Node *CreateNode(int init)

Node *p = new Node;

p->data = init;

p->left = NULL;

p->right = NULL;

return p;

Định nghĩa cấu trúc cây

Để quản lý một cái cây, bạn chỉ cần quản lý được nút gốc, bạn có thể đi được đến
các nhánh và lá của nó từ đó. Trên thực tế bạn không cần phải định nghĩa một kiểu
dữ liệu nào để quản lý cả, tuy nhiên, để cho code rõ ràng hơn, bạn nên định nghĩa
một kiểu dữ liệu cây nữa.

typedef Node* Tree;

Lúc này, khi tạo một cây, bản chất là nó sẽ tạo cho bạn một con trỏ có thể trỏ vào
một node.
Tree myTree;

Vì nó là con trỏ nên các bạn gán nó bằng NULL để tránh lỗi, nhưng để mọi thứ rõ
ràng hơn, mình sẽ dùng hàm tạo cây đơn giản gán nó bằng NULL.

void CreateTree(Tree &root)

root = NULL;

// Khi tạo cây

CreateTree(myTree);

Duyệt cây nhị phân

Có 3 cách duyệt cây nhị phân:

Duyệt tiền tự (NLR): duyệt nút gốc, duyệt tiền tự cây con trái, duyệt tiền tự cây
con phải.

Duyệt trung tự (LNR): duyệt trung tự cây con trái, duyệt nút gốc, duyệt trung tự
cây con phải.

Duyệt hậu tự (LRN): duyệt hậu tự cây con trái, duyệt hậu tự cây con phải, duyệt
nút gốc.

Để bạn hiểu rõ hơn ba cách duyệt này, chúng ta sẽ sử dụng lại hình ảnh cây nhị
phân trên:

Cây nhị phân


Duyệt tiền tự: A B D H I E K L C F M N G O P

Duyệt trung tự: H D I B K E L A M F N C O G P

Duyệt hậu tự: H I D K L E B M N F O P G C A

Ứng với từng cách duyệt đó, chúng ta sẽ có các hàm duyệt cây như sau:

Duyệt tiền tự

void NLR(Tree root)

if (root)

// Xử lý nút gốc (root)

NLR(root->left);

NLR(root->right);

Duyệt trung tự

void LNR(Tree root)

if (root)

LNR(root->left);
// Xử lý nút gốc (root)

LNR(root->right);

Duyệt hậu tự

void LRN(Tree root)

if (root)

LRN(root->left);

LRN(root->right);

// Xử lý nút gốc (root)

Hủy cây nhị phân

Để hủy đi cây nhị phân, các bạn cũng thực hiện duyệt và xóa đi các nút của cây,
tuy nhiên, các bạn dễ thấy rằng, nếu ta duyệt tiền tự và trung tự, khi xóa nút nhánh
thì sẽ bị mất luôn địa chỉ của các nút con. Do đó, việc hủy cây nhị phân bắt buộc
phải duyệt hậu tự. Hay nói cách khác, bạn phải xóa các phần tử là nút lá xóa dần
lên đến nút gốc.

Chúng ta sẽ có hàm hủy như sau:


void DestroyTree(Tree &root)

if (root)

DestroyTree(root->left);

DestroyTree(root->right);

delete root;

Như vậy là chúng ta đã tìm hiểu về cách tạo một nút, kết nối chúng lại thành một
cây nhị phân, duyệt cây và hủy cây. Tiếp theo chúng ta sẽ tìm hiểu về cây nhị phân
đặc biệt khác là cây nhị phân tìm kiếm.

Tham khảo thêm các vị trí tuyển lập trình viên C++ mới nhất.

Cây nhị phân tìm kiếm

Cây nhị phân tìm kiếm là cây nhị phân mà trong đó, các phần tử của cây con bên
trái đều nhỏ hơn phần tử hiện hành và các phần tử của cây con bên phải đều lớn
hơn phần tử hiện hành. Do tính chất này, cây nhị phân tìm kiếm không được có
phần tử cùng giá trị.

Nhờ vào tính chất đặc biệt này, cây nhị phân tìm kiếm được sử dụng để tìm kiếm
phần tử nhanh hơn (tương tự với tìm kiếm nhị phân). Khi duyệt cây nhị phân theo
cách duyệt trung tự, bạn sẽ thu được một mảng có thứ tự. Chúng ta sẽ lần lượt tìm
hiểu qua chúng.
Thêm phần tử vào cây nhị phân tìm kiếm

Để thêm phần tử vào cây nhị phân tìm kiếm, ta phải thêm vào cây nhưng vẫn đảm
bảo được cây đó vẫn là cây nhị phân tìm kiếm. Ví dụ thêm phần tử 12 vào cây
trong hình trên, mình sẽ cần chèn vào vị trí bên trái 13. Hàm duyệt tìm vị trí thích
hợp và chèn của mình như sau:

void AddNode(Tree &root, Node *node)

if (root)

if (root->data == node->data) // Nếu bị trùng giá trị thì không thêm

return;

if (node->data < root->data) // Thêm vào cây con bên trái (nhỏ hơn nút hiện
tại)

AddNode(root->left, node);

else

AddNode(root->right, node); // Thêm vào cây con bên phải (lớn hơn nút
hiện tại)

else

root = node; // Đã tìm thấy vị trí thích hợp, thêm node vào
}

Tìm một phần tử trong cây nhị phân tìm kiếm

Như đã giới thiệu ở trên, để tìm một phần tử trong cây nhị phân tìm kiếm, chúng ta
sẽ thực hiện tương tự việc tìm kiếm nhị phân. Nếu như nút cần tìm nhỏ hơn nút
đang xét, chúng ta sẽ tìm cây con bên trái, ngược lại chúng ta sẽ tìm trong cây con
bên phải, nếu đúng nút cần tìm thì mình sẽ trả về địa chỉ của nút đó. Mình sẽ có
thuật toán sau:

Node *FindNode(Tree root, int x)

if (root)

if (root->data == x) // Tìm thấy

return root;

if (x < root->data)

return FindNode(root->left, x); // Tìm cây con bên trái

return FindNode(root->right, x); // Tìm cây con bên phải

return NULL; // Không tìm thấy

Hủy nút trên cây nhị phân tìm kiếm


Để hủy một nút có khóa X trong cây nhị phân tìm kiếm, chúng ta cần giải quyết ba
trường hợp sau:

Nút X là nút lá, ta xóa đi mà không làm ảnh hưởng đến các nút khác. Ví dụ xóa nút
15 đi không ảnh hưởng gì đến các nút khác.

Nút X có 1 cây con, chúng ta chỉ cần nối nút cha của X với nút con của X. Ví dụ
xóa nút 13 đi, ta chỉ cần nối nút 18 và 15 lại, sau đó xóa nút 13 đi.

Nút X có đầy đủ 2 cây con: vì X có đầy đủ 2 nút nên nếu ta xóa đi, ta sẽ bị mất
toàn bộ cây con. Do đó chúng ta cần tìm phần tử thế mạng cho X mà vẫn đảm bảo
được cây nhị phân tìm kiếm, sau đó mới xóa X đi.

Đối với hai trường hợp đầu thì dễ, tuy nhiên, với trường hợp thứ 3, chúng ta cần
phải giải quyết vấn đề tìm phần tử thế mạng cho x, chúng ta sẽ có hai cách thực
hiện như sau:

Nút thế mạng là nút có khóa nhỏ nhất (trái nhất) của cây con bên phải x.

Nút thế mạng là nút có khóa lớn nhất (phải nhất) của cây con bên trái x.

Lấy ví dụ cho các bạn dễ hiểu hơn, hình phía trên, xóa đi phần tử 18 theo cách 1,
phần tử lớn nhất của cây con bên trái là 15, vậy thì thay 18 bằng 15 rồi xóa đi nút
15 cuối. Cách 2, phần tử nhỏ nhất của cây con bên phải là 23, vậy 18 sẽ thay bằng
23 và xóa nút 23 đó đi.

Đối với hai trường hợp đầu tiên khá đơn giản, nên mình sẽ lồng nó vào code luôn ở
phần dưới, mình sẽ giải quyết cách tìm phần tử thế mạng ở trường hợp 3 trước và
theo cả hai cách. Theo cách 1, mình sẽ làm như sau:

Trường hợp 1

// nút p là nút cần thay thế, tree là cây đang xét (cây bên phải)
void FindAndReplace1(Tree &p, Tree &tree)

if (tree->left) // chưa phải nhỏ nhất (trái nhất)

FindAndReplace1(p, tree->left); // tiếp tục tìm

else // tree là nút trái nhất

p->data = tree->data; // copy data

p = tree; // trỏ nút p vào nút tree sẽ làm thế mạng bị xóa

tree = tree->right; // nút trái không còn tuy nhiên nút phải có thể còn nên ta
phải nối chúng lại

Đối với trường hợp này, các bạn phải gọi hàm FindAndReplace1(p, root->right)
trong hàm DeleteNode ở phía trên. Trường hợp thứ 2 thì ngược lại.

Trường hợp 2

// nút p là nút cần thay thế, tree là cây đang xét (cây bên trái)

void FindAndReplace2(Tree &p, Tree &tree)

if (tree->right) // chưa phải lớn nhất (phải nhất)

FindAndReplace2(p, tree->right); // tiếp tục tìm

else // tree là nút trái nhất


{

p->data = tree->data; // copy data

p = tree; // trỏ nút p vào nút tree sẽ làm thế mạng bị xóa

tree = tree->left; // nút phải không còn tuy nhiên nút trái có thể còn nên ta
phải nối chúng lại

Và trong hàm DeleteNode, các bạn sẽ gọi hàm FindAndReplace(p, root->left). Bây
giờ, tổng hợp lại, chúng ta đã có thể dể dàng xóa một nút khỏi cây nhị phân tìm
kiếm, mình sẽ code như sau:

void DeleteNode(Tree &root, int x)

if (root)

if (x > root->data)

DeleteNode(root->right, x);

else if (x < root->data)

DeleteNode(root->left, x);

else // nút hiện tại (root) là nút cần xóa

Node *p = root; // lưu lại nút cần xóa tránh bị ghi đè


if (!root->left)

root = root->right; // trường hợp 1

else if (!root->right)

root = root->left; // trường hợp 2

else

FindAndReplace1(p, root->right); // cách 1

// FindAndReplace2(p, root->left); // cách 2

delete p; // xóa nút

else

cout << "Not found!\n"; // Không tìm thấy phần tử cần xóa

You might also like