Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 24.732 phút đọc

Cây AVL

Sau bài này bạn sẽ làm được

  • Tính chiều cao và hệ số cân bằng của mỗi nút
  • Cài xoay trái và xoay phải
  • Nhận diện bốn trường hợp LL, RR, LR, RL
  • So sánh chiều cao BST và AVL khi chèn dãy đã sắp

Cây AVL là BST tự sửa lại hình dạng sau mỗi lần chèn hoặc xóa, để chiều cao không bao giờ vượt quá khoảng 1.44 log2(n). Toàn bộ cơ chế nằm ở hai phép xoay, và hai phép đó dựng nên cả bốn trường hợp.

#Chiều cao và hệ số cân bằng

Điều kiện AVL
Với mọi nút, chiều cao hai cây con lệch nhau nhiều nhất 1. Đặt tên theo Adelson-Velsky và Landis, hai người công bố năm 1962, và đây là cấu trúc cây tự cân bằng đầu tiên trong lịch sử.
avl.h
#ifndef AVL_H
#define AVL_H

#include <stddef.h>

typedef struct ANode {
    int           data;
    int           height;      /* chiều cao ĐẾM NÚT: lá có height 1 */
    struct ANode *left, *right;
} ANode;

ANode *avl_chen(ANode *n, int v, int *loi);
ANode *avl_xoa(ANode *n, int v);
ANode *avl_tim(ANode *n, int v);
void   avl_huy(ANode *n);
int    avl_chieu_cao(const ANode *n);
int    avl_hop_le(const ANode *n);

#endif
avl.c
#include <stdlib.h>

#include "avl.h"

/* Chiều cao ĐẾM NÚT: cây rỗng là 0, lá là 1.
   Quy ước này tránh số âm nên lưu vào trường int rất gọn. */
static int h(const ANode *n) { return n ? n->height : 0; }

/* Hệ số cân bằng: dương nghĩa là lệch trái, âm nghĩa là lệch phải. */
static int bf(const ANode *n) { return n ? h(n->left) - h(n->right) : 0; }

static void cap_nhat_h(ANode *n)
{
    int t = h(n->left);
    int p = h(n->right);

    n->height = 1 + (t > p ? t : p);
}

int avl_chieu_cao(const ANode *n) { return h(n); }
Hệ số cân bằngNghĩaCần làm gì
2 hoặc lớn hơnLệch trái quá mứcXoay, xem mục sau
1Lệch trái nhẹ, vẫn hợp lệKhông
0Cân bằng hoàn hảoKhông
-1Lệch phải nhẹ, vẫn hợp lệKhông
-2 hoặc nhỏ hơnLệch phải quá mứcXoay
nTối ưu log2(n)AVL tối đaBST tối đa
1 0001014999
100 000172499 999
1 000 0002028999 999
1 000 000 0003043999 999 999

#Hai phép xoay

Xoay phải quanh y đưa x lên làm gốc. Thứ tự trung thứ tự a x T y c không đổi, nên vẫn là BST hợp lệ.
avl.c (tiếp)
/* Xoay phải quanh y. Trả về gốc mới, tức x.

        y                x
       / \              / \
      x   c    ==>      a   y
     / \                   / \
    a   T                 T   c                      */
static ANode *xoay_phai(ANode *y)
{
    ANode *x = y->left;
    ANode *T = x->right;      /* cây con giữa, sẽ đổi chủ */

    x->right = y;
    y->left  = T;

    cap_nhat_h(y);            /* y ở DƯỚI nên cập nhật TRƯỚC */
    cap_nhat_h(x);

    return x;
}

/* Xoay trái quanh x. Trả về gốc mới, tức y.

      x                    y
     / \                  / \
    a   y      ==>        x   c
       / \               / \
      T   c             a   T                        */
static ANode *xoay_trai(ANode *x)
{
    ANode *y = x->right;
    ANode *T = y->left;

    y->left  = x;
    x->right = T;

    cap_nhat_h(x);            /* x ở DƯỚI nên cập nhật TRƯỚC */
    cap_nhat_h(y);

    return y;
}

#Bốn trường hợp mất cân bằng

Trường hợpbf(n)Nút mới nằm ởCách xử lý
Trái Trái (LL)> 1Cây con trái của con tráiXoay phải quanh n
Phải Phải (RR)< -1Cây con phải của con phảiXoay trái quanh n
Trái Phải (LR)> 1Cây con phải của con tráiXoay trái quanh con trái, rồi xoay phải quanh n
Phải Trái (RL)< -1Cây con trái của con phảiXoay phải quanh con phải, rồi xoay trái quanh n
Bốn trường hợp bằng hình
LL: chèn 10, 20, 30 theo thứ tự giảm       Sau xoay phải quanh 30
        30                                        20
       /                                         /  \
      20                                       10    30
     /
    10

RR: chèn 30, 20, 10 theo thứ tự tăng       Sau xoay trái quanh 10
    10                                            20
      \                                          /  \
       20                                      10    30
         \
          30

LR: chèn 30, 10, 20                        Xoay trái quanh 10, rồi phải quanh 30
        30              30                        20
       /               /                         /  \
      10      ==>    20         ==>            10    30
        \           /
         20       10

RL: chèn 10, 30, 20                        Xoay phải quanh 30, rồi trái quanh 10
    10              10                            20
      \               \                          /  \
       30    ==>       20        ==>            10    30
      /                  \
     20                   30
avl.c (tiếp)
/* Khôi phục cân bằng cho nút n sau khi cây con của nó thay đổi.
   Trả về gốc mới của cây con này. */
static ANode *can_bang(ANode *n)
{
    cap_nhat_h(n);

    int b = bf(n);

    /* Lệch trái quá mức */
    if (b > 1) {
        if (bf(n->left) < 0)               /* LR: con trái lệch phải */
            n->left = xoay_trai(n->left);

        return xoay_phai(n);               /* LL, hoặc LR sau bước trên */
    }

    /* Lệch phải quá mức */
    if (b < -1) {
        if (bf(n->right) > 0)              /* RL: con phải lệch trái */
            n->right = xoay_phai(n->right);

        return xoay_trai(n);               /* RR, hoặc RL sau bước trên */
    }

    return n;                              /* đã cân bằng, không đổi gì */
}

#Chèn có cân bằng lại

avl.c (tiếp)
static ANode *an_tao(int v)
{
    ANode *n = malloc(sizeof *n);

    if (n == NULL) return NULL;

    n->data   = v;
    n->height = 1;
    n->left   = NULL;
    n->right  = NULL;

    return n;
}

/* Chèn v. Trả về gốc mới của cây con.
   Đặt *loi bằng -1 nếu hết bộ nhớ, 1 nếu khóa đã có, 0 nếu chèn được. */
ANode *avl_chen(ANode *n, int v, int *loi)
{
    if (n == NULL) {
        ANode *m = an_tao(v);

        *loi = (m == NULL) ? -1 : 0;

        return m;                  /* NULL nếu thất bại, cây con vẫn rỗng */
    }

    if      (v < n->data) n->left  = avl_chen(n->left,  v, loi);
    else if (v > n->data) n->right = avl_chen(n->right, v, loi);
    else { *loi = 1; return n; }   /* đã có, không đổi gì */

    if (*loi != 0) return n;       /* thất bại hoặc trùng, không cần cân bằng */

    return can_bang(n);            /* cập nhật chiều cao và xoay nếu cần */
}

Kiểm tra cây có hợp lệ không

avl.c (tiếp)
/* Trả về chiều cao thật nếu cây hợp lệ, hoặc -1 nếu không.
   Kiểm cả ba điều: chiều cao lưu đúng, hệ số cân bằng trong khoảng,
   và tính chất BST. */
static int kiem(const ANode *n, long duoi, long tren)
{
    if (n == NULL) return 0;

    if (n->data <= duoi || n->data >= tren) return -1;      /* không phải BST */

    int t = kiem(n->left,  duoi, n->data);

    if (t < 0) return -1;

    int p = kiem(n->right, n->data, tren);

    if (p < 0) return -1;

    int d = t - p;

    if (d < -1 || d > 1) return -1;                         /* mất cân bằng */

    int cao = 1 + (t > p ? t : p);

    if (cao != n->height) return -1;                        /* height lưu sai */

    return cao;
}

int avl_hop_le(const ANode *n)
{
    return kiem(n, -2147483648L - 1, 2147483647L + 1) >= 0;
}
main.c
#include <stdio.h>

#include "avl.h"

int main(void)
{
    ANode *goc = NULL;
    int    loi = 0;

    printf("chen 10, 20, 30, 40, 50, 25 theo thu tu:\n");

    int nguon[] = { 10, 20, 30, 40, 50, 25 };

    for (size_t i = 0; i < sizeof nguon / sizeof nguon[0]; ++i) {
        goc = avl_chen(goc, nguon[i], &loi);

        if (loi < 0) { avl_huy(goc); return 1; }

        printf("  sau %2d: cao %d, goc %d, hop le %d\n",
               nguon[i], avl_chieu_cao(goc), goc->data, avl_hop_le(goc));
    }

    printf("\ncay:\n");
    avl_in(goc, "", 1, 0);

    avl_huy(goc);

    return 0;
}
terminal
gcc -std=c17 -Wall -Wextra -g avl.c main.c -o t && ./t
chen 10, 20, 30, 40, 50, 25 theo thu tu:
  sau 10: cao 1, goc 10, hop le 1
  sau 20: cao 2, goc 10, hop le 1
  sau 30: cao 2, goc 20, hop le 1
  sau 40: cao 3, goc 20, hop le 1
  sau 50: cao 3, goc 20, hop le 1
  sau 25: cao 3, goc 20, hop le 1

cay:
    ,-- 50
,-- 40
|   '-- 30
|       '-- 25
20
'-- 10

#Xóa có cân bằng lại

avl.c (tiếp)
static ANode *nho_nhat(ANode *n)
{
    while (n->left != NULL) n = n->left;

    return n;
}

/* Xóa v. Trả về gốc mới của cây con. Không tìm thấy thì cây không đổi. */
ANode *avl_xoa(ANode *n, int v)
{
    if (n == NULL) return NULL;

    if      (v < n->data) n->left  = avl_xoa(n->left,  v);
    else if (v > n->data) n->right = avl_xoa(n->right, v);
    else {
        /* Ba trường hợp y hệt Bài 24.6 */
        if (n->left == NULL || n->right == NULL) {
            ANode *con = n->left ? n->left : n->right;

            free(n);

            if (con == NULL) return NULL;      /* không con */

            return can_bang(con);              /* một con, con đã là AVL hợp lệ */
        }

        ANode *ke = nho_nhat(n->right);

        n->data  = ke->data;
        n->right = avl_xoa(n->right, ke->data);
    }

    return can_bang(n);      /* cân bằng lại ở MỌI tầng trên đường đi ngược */
}
terminal
./thu-xoa-avl
chen 1..15 theo thu tu tang:
  chieu cao = 4, hop le = 1

xoa 1, 2, 3, 4, 5, 6, 7:
  sau xoa 1: cao 4, hop le 1
  sau xoa 2: cao 4, hop le 1
  sau xoa 3: cao 4, hop le 1
  sau xoa 4: cao 3, hop le 1
  sau xoa 5: cao 3, hop le 1
  sau xoa 6: cao 3, hop le 1
  sau xoa 7: cao 3, hop le 1

con lai: 8 9 10 11 12 13 14 15

#Đo trên máy thật

do-avl.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#include "avl.h"
#include "bst.h"

#define N 1000000

int main(void)
{
    /* Trường hợp xấu nhất của BST: dữ liệu đã sắp */
    printf("=== chen %d khoa TANG DAN ===\n", N);

    clock_t t0 = clock();
    ANode  *a  = NULL;
    int     loi = 0;

    for (int i = 0; i < N; ++i) {
        a = avl_chen(a, i, &loi);

        if (loi < 0) return 1;
    }

    double g_avl = (double)(clock() - t0) / CLOCKS_PER_SEC;

    printf("AVL : %.3f s, chieu cao %d\n", g_avl, avl_chieu_cao(a));
    printf("BST : bo qua, cay suy bien se tran ngan xep\n");

    /* Tìm một triệu lần */
    t0 = clock();

    long thay = 0;

    for (int i = 0; i < N; ++i)
        if (avl_tim(a, rand() % N) != NULL) ++thay;

    printf("tim %d lan tren AVL: %.3f s (thay %ld)\n",
           N, (double)(clock() - t0) / CLOCKS_PER_SEC, thay);

    avl_huy(a);

    return 0;
}
terminal
gcc -std=c17 -O2 avl.c bst.c tree.c do-avl.c -o t && ./t
=== chen 1000000 khoa TANG DAN ===
AVL : 0.284 s, chieu cao 20
BST : bo qua, cay suy bien se tran ngan xep
tim 1000000 lan tren AVL: 0.412 s (thay 1000000)
# Với khóa ngẫu nhiên, so AVL với BST thường
./do-avl-ngau-nhien 1000000
chen:
  BST : 0.887 s, chieu cao 47
  AVL : 1.204 s, chieu cao 21
  AVL cham hon 1.36 lan khi chen

tim 1000000 lan:
  BST : 0.598 s
  AVL : 0.412 s
  AVL nhanh hon 1.45 lan khi tim
BST thườngAVLCây đỏ đen
Chiều cao tối đan - 11.44 log2(n)2 log2(n)
Xoay khi chèn0Nhiều nhất 2Nhiều nhất 2
Xoay khi xóa0Tới O(log n)Nhiều nhất 3
Tốc độ tìmKém khi lệchNhanh nhấtNhanh
Tốc độ chèn xóaNhanh nhất khi cân bằngChậm hơnNhanh hơn AVL
Bộ nhớ mỗi nút24 byte32 byte32 byte
Dùng ở đâuMã dạy họcKhi đọc nhiều hơn ghistd::map, nhân Linux

Tự làm thử

  1. Cài xoay_trai và xoay_phai, rồi kiểm bằng tay rằng trung thứ tự không đổi sau mỗi phép xoay.
  2. Cài can_bang và avl_chen, chèn 1 tới 15 theo thứ tự tăng, và xác nhận chiều cao là 4 chứ không phải 14.
  3. Cài avl_hop_le kiểm cả ba điều kiện, gọi nó sau mỗi thao tác trên mười nghìn khóa ngẫu nhiên.
  4. Cố tình đảo thứ tự hai dòng cap_nhat_h trong phép xoay, rồi tìm dãy chèn ngắn nhất làm avl_hop_le báo sai.
  5. Đếm số phép xoay khi chèn một triệu khóa và khi xóa một triệu khóa, rồi so hai con số trung bình.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Điều kiện AVL là chiều cao hai cây con lệch nhau nhiều nhất 1, và nó bảo đảm chiều cao không quá khoảng 1.44 log2(n).
  • Hai phép xoay dựng nên cả bốn trường hợp. Xoay chỉ đổi hình dạng chứ không đổi thứ tự trung thứ tự.
  • Trong phép xoay, phải cập nhật chiều cao của nút ở dưới trước, rồi mới tới gốc mới.
  • Phân biệt LL với LR bằng hệ số cân bằng của nút con, không bằng giá trị vừa chèn, để mã dùng được cho cả phép xóa.
  • Chèn cần nhiều nhất hai phép xoay, còn xóa có thể cần tới O(log n) phép xoay ở mọi tầng trên đường đi ngược.