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

Vector và danh sách

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

  • Cài vector động với chiến lược tăng gấp đôi
  • Cài danh sách liên kết đôi có nút đầu giả
  • Xử lý realloc thất bại mà không rò rỉ
  • Viết hàm duyệt nhận hàm gọi lại

Vector và danh sách là hai container nền tảng, và hai cách cài đặt của chúng minh họa hai kỹ thuật khác nhau: mảng liền kề với memcpy, và cấp phát nút kèm dữ liệu trong cùng một lần malloc.

#Vector: cấu trúc bên trong

vec.c, phần đầu
#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include "cds.h"

struct Vector {
    void  *d;         /* mang lien ke, do vector SO HUU */
    size_t n;         /* so phan tu dang co */
    size_t cap;       /* so phan tu chua duoc */
    size_t esz;       /* kich thuoc mot phan tu, biet luc CHAY */
};

/* Do doi cua phan tu thu i. Ep sang char * de so hoc theo byte. */
static void *o(const Vector *v, size_t i) {
    return (char *)v->d + i * v->esz;
}

Vector *vec_new(size_t esz) {
    if (esz == 0) return NULL;

    Vector *v = malloc(sizeof *v);
    if (v == NULL) return NULL;

    v->d = NULL;
    v->n = v->cap = 0;
    v->esz = esz;
    return v;
}
vec_free: ai giải phóng cái gì
void vec_free(Vector *v, FreeFn f, void *ctx) {
    if (v == NULL) return;                 /* chap nhan NULL, nhu free */

    if (f != NULL)
        for (size_t i = 0; i < v->n; ++i) f(o(v, i), ctx);

    free(v->d);        /* vector so huu MANG */
    free(v);           /* va so huu chinh no */
}

/* Ham goi lai giai phong TAI NGUYEN BEN TRONG tung phan tu.
   Vi du voi phan tu la "struct { char *ten; int ma; }": */

static void huy_sv(void *pt, void *ctx) {
    (void)ctx;
    free(((SinhVien *)pt)->ten);     /* giai phong "ten"
                                        KHONG free(pt): pt tro vao giua mang */
}

vec_free(v, huy_sv, NULL);

/* Neu phan tu khong so huu gi thi truyen NULL: */
vec_free(v, NULL, NULL);

#Thêm phần tử và chiến lược tăng

vec_reserve và vec_push
int vec_reserve(Vector *v, size_t n) {
    if (v == NULL) return -1;
    if (n <= v->cap) return 0;             /* da du cho */
    if (n > SIZE_MAX / v->esz) return -1;  /* chan tran, Bai 33.4 */

    void *moi = realloc(v->d, n * v->esz);
    if (moi == NULL) return -1;            /* v->d CON NGUYEN */

    v->d = moi;
    v->cap = n;
    return 0;
}

static int mo_rong(Vector *v) {
    size_t cap = v->cap ? v->cap * 2 : 8;
    if (v->cap > SIZE_MAX / 2) return -1;
    return vec_reserve(v, cap);
}

int vec_push(Vector *v, const void *pt) {
    if (v == NULL || pt == NULL) return -1;
    if (v->n == v->cap && mo_rong(v) != 0) return -1;

    memcpy(o(v, v->n), pt, v->esz);
    ++v->n;
    return 0;
}
Hệ số tăngSố lần realloc cho n phần tửBộ nhớ phí tối đaTái dùng khoảng trống cũ
Cộng thêm 1n0Không
Cộng thêm kn / kkKhông
Nhân 1.5log(n) / log(1.5)50 phần trămCó, sau vài lần
Nhân 2log2(n)100 phần trămKhông bao giờ

#Sắp xếp nhận ngữ cảnh

Heapsort tại chỗ
static void doi_cho(void *a, void *b, size_t n) {
    unsigned char *x = a, *y = b;
    for (size_t i = 0; i < n; ++i) {
        unsigned char t = x[i]; x[i] = y[i]; y[i] = t;
    }
}

static void chim_xuong(Vector *v, size_t goc, size_t n,
                       CmpFn cmp, void *ctx) {
    for (;;) {
        size_t lon = goc;
        size_t t = 2 * goc + 1;      /* con trai  */
        size_t p = t + 1;            /* con phai  */

        if (t < n && cmp(o(v, t), o(v, lon), ctx) > 0) lon = t;
        if (p < n && cmp(o(v, p), o(v, lon), ctx) > 0) lon = p;
        if (lon == goc) return;      /* da dung cho */

        doi_cho(o(v, goc), o(v, lon), v->esz);
        goc = lon;
    }
}

void vec_sort(Vector *v, CmpFn cmp, void *ctx) {
    if (v == NULL || cmp == NULL || v->n < 2) return;

    /* 1. Dung heap: chim xuong tu giua ve dau */
    for (size_t i = v->n / 2; i-- > 0; ) chim_xuong(v, i, v->n, cmp, ctx);

    /* 2. Lay lan luot phan tu lon nhat ra cuoi */
    for (size_t i = v->n; i-- > 1; ) {
        doi_cho(o(v, 0), o(v, i), v->esz);
        chim_xuong(v, 0, i, cmp, ctx);
    }
}
Dùng: sắp xếp chỉ số theo một mảng khóa ngoài
static int ss_theo_khoa(const void *a, const void *b, void *ctx) {
    const double *khoa = ctx;
    double x = khoa[*(const int *)a];
    double y = khoa[*(const int *)b];
    return (x > y) - (x < y);
}

double khoa[] = { 3.1, 1.2, 5.9, 0.4 };

Vector *idx = vec_new(sizeof(int));
for (int i = 0; i < 4; ++i) vec_push(idx, &i);

vec_sort(idx, ss_theo_khoa, khoa);

/* idx gio la { 3, 1, 0, 2 }, tuc thu tu sap xep theo khoa.

   Voi qsort chuan thi ban phai dat "khoa" vao mot bien toan cuc,
   va do la ba van de o Bai 55.1: khong an toan da luong,
   khong long nhau duoc, khong tai su dung duoc. */

#List: nút giả và một lần cấp phát

list.c, phần đầu
typedef struct Nut {
    struct Nut *truoc, *sau;
    /* du lieu nam NGAY SAU trong cung mot lan cap phat */
} Nut;

struct List {
    Nut    dau;          /* nut GIA, khong chua du lieu */
    size_t n;
    size_t esz;
};

static void *du_lieu(Nut *n) { return (char *)n + sizeof(Nut); }

List *list_new(size_t esz) {
    if (esz == 0) return NULL;

    List *l = malloc(sizeof *l);
    if (l == NULL) return NULL;

    l->dau.truoc = l->dau.sau = &l->dau;   /* tro vao chinh no */
    l->n = 0;
    l->esz = esz;
    return l;
}
Không có nút giả
/* Khong co nut gia: moi thao tac phai kiem tra NULL */

int push_front(List *l, const void *pt) {
    Nut *n = tao_nut(l, pt);
    if (n == NULL) return -1;

    n->truoc = NULL;
    n->sau   = l->dau;

    if (l->dau != NULL) l->dau->truoc = n;   /* kiem tra */
    else                l->cuoi = n;         /* danh sach dang rong */

    l->dau = n;
    ++l->n;
    return 0;
}

/* Va push_back, pop_front, pop_back, remove deu co
   nhung nhanh dac biet nhu vay. Do la noi loi hay nam. */
Nút giả, danh sách vòng
/* Co nut gia: KHONG truong hop dac biet nao */

static int chen_sau(List *l, Nut *truoc, const void *pt) {
    Nut *n = malloc(sizeof(Nut) + l->esz);
    if (n == NULL) return -1;

    memcpy(du_lieu(n), pt, l->esz);

    n->truoc = truoc;
    n->sau   = truoc->sau;
    truoc->sau->truoc = n;
    truoc->sau = n;

    ++l->n;
    return 0;
}

int list_push_front(List *l, const void *pt) {
    return chen_sau(l, &l->dau, pt);          /* chen sau nut gia */
}

int list_push_back(List *l, const void *pt) {
    return chen_sau(l, l->dau.truoc, pt);     /* chen sau phan tu cuoi */
}

/* Khong mot cau if nao. Danh sach rong cung chay dung,
   vi nut gia luon ton tai. */

#Các thao tác của list

Gỡ một nút, và bốn hàm dùng lại nó
static int go(List *l, Nut *n, void *ra) {
    if (n == &l->dau) return -1;           /* danh sach rong */

    if (ra != NULL) memcpy(ra, du_lieu(n), l->esz);

    n->truoc->sau = n->sau;
    n->sau->truoc = n->truoc;
    free(n);

    --l->n;
    return 0;
}

int list_pop_front(List *l, void *ra) {
    if (l == NULL) return -1;
    return go(l, l->dau.sau, ra);
}

int list_pop_back(List *l, void *ra) {
    if (l == NULL) return -1;
    return go(l, l->dau.truoc, ra);
}

/* Mot ham "go" dung cho ca hai dau, va cho ca viec xoa mot nut
   o giua neu ban them list_remove.

   Va cach kiem tra danh sach rong: "n == &l->dau".
   Voi danh sach rong thi dau.sau va dau.truoc deu tro vao chinh dau,
   nen ca hai ham pop deu tra ve -1 mot cach tu nhien. */
Duyệt và giải phóng
void list_foreach(List *l, ApplyFn f, void *ctx) {
    if (l == NULL || f == NULL) return;

    for (Nut *p = l->dau.sau; p != &l->dau; p = p->sau)
        f(du_lieu(p), ctx);
}

void list_free(List *l, FreeFn f, void *ctx) {
    if (l == NULL) return;

    Nut *p = l->dau.sau;
    while (p != &l->dau) {
        Nut *sau = p->sau;              /* LUU truoc khi free(p) */
        if (f != NULL) f(du_lieu(p), ctx);
        free(p);
        p = sau;
    }

    free(l);
}

/* Chu y dong "Nut *sau = p->sau;": doc p->sau SAU khi free(p)
   la dung sau khi giai phong. Bai 33.3 da do hau qua. */
terminal
gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -Wshadow -Wconversion -o chay_thu.exe vec.c list.c heap.c tree.c hashmap.c chay_thu.c && ./chay_thu.exe
[vector]
[list]
[heap]
[tree]
[hashmap]

3121 kiem tra, 0 hong
echo $?
0

#Chọn cái nào

Thao tácVectorListGhi chú
Truy cập theo chỉ sốO(1)O(n)Vector thắng tuyệt đối
Thêm vào cuốiO(1) khấu haoO(1)Hòa
Thêm vào đầuO(n)O(1)List thắng
Thêm vào giữaO(n)O(1) nếu đã có con trỏList thắng, nếu đã ở đó
Xóa ở giữaO(n)O(1) nếu đã có con trỏList thắng, nếu đã ở đó
Duyệt tuần tựRất nhanhChậm hơn nhiềuVector thắng
Bộ nhớ mỗi phần tửeszesz cộng 16 byteVector thắng
Con trỏ tới phần tử ổn địnhKhôngCóList thắng

Tự làm thử

  1. Cài vector với đủ mười ba hàm và chạy bộ kiểm thử.
  2. Đổi hệ số tăng từ 2 sang 1.5 và đo số lần realloc cho một triệu lần push.
  3. Chứng minh vec_at trả về con trỏ treo sau một lần vec_push gây realloc.
  4. Viết hàm so sánh dùng ngữ cảnh để sắp xếp chỉ số theo mảng khóa ngoài.
  5. Cài list không có nút giả rồi so số dòng và số câu if với bản có nút giả.
  6. Đo thời gian duyệt một triệu phần tử ở cả hai container.

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

  • Vector giữ esz lúc chạy và dùng memcpy; hàm o(v, i) ép sang char * trước khi nhân.
  • Ba chi tiết của vec_push: nhận realloc vào biến tạm, kiểm tra tràn trước khi nhân, cập nhật trạng thái sau cùng.
  • Heapsort tự cài nhận được ngữ cảnh, chạy tại chỗ, và O(n log n) trong mọi trường hợp; qsort_r có hai biến thể không tương thích.
  • Nút giả biến danh sách thành vòng và xóa sạch mọi trường hợp đặc biệt: không một câu if nào trong phép chèn và gỡ.
  • Mặc định dùng vector. Chỉ đổi sang list khi con trỏ phải ổn định, phần tử nằm trong nhiều danh sách, hoặc bạn đã cầm sẵn con trỏ tới chỗ cần sửa.