📖 第 17 章:经典抽象数据类型(Classic Abstract Data Types)

🔤 Kenneth Reek《Pointers on C》(C和指针)· Chapter 17
🗨️ 本章金句:「ADT 三件套:堆栈管「后进先出」,队列管「先进先出」,二叉搜索树管「有序查找」。」
这一章在干嘛? 用前面全部家底(结构、指针、动态内存、函数指针)实现三个经典 ADT。重点不在背代码,而在体会「接口与实现分离」的工程思想——头文件给接口,.c 藏细节。
17.1 堆栈:后进先出17.2 队列:先进先出17.3 二叉搜索树:有序数据的查找利器

17.1 堆栈:后进先出

/* stack.h —— 接口(调用者只见这个) */
void stack_push(int v);      /* 压栈 */
int  stack_pop(void);        /* 弹栈并返回;空栈调用是契约违约 */
int  stack_is_empty(void);
/* stack.c —— 实现 1:定长数组(静态) */
#define CAPACITY 100
static int stack[CAPACITY];  /* static:模块私有,外界摸不到 */
static int top;              /* 栈顶游标(下一个空位) */

void stack_push(int v)
{
    assert(top < CAPACITY);  /* 满栈是实现bug或契约违约 */
    stack[top++] = v;
}
int stack_pop(void)
{
    assert(top > 0);
    return stack[--top];
}
int stack_is_empty(void) { return top == 0; }

数组版简单高效但容量定死;把 static int stack 换成 malloc 动态数组 + 扩容,或换成第 12 章的链表(头插头弹),接口一行不改——这就是接口分离的红利。

17.2 队列:先进先出

/* 循环数组实现:front 出、rear 进,下标对容量取模 */
static int queue[CAPACITY];
static size_t front, count;          /* count 免得区分空/满 */

void queue_enqueue(int v)
{
    assert(count < CAPACITY);
    queue[(front + count) % CAPACITY] = v;
    count++;
}
int queue_dequeue(void)
{
    assert(count > 0);
    int v = queue[front];
    front = (front + 1) % CAPACITY;  /* 出队后 front 前移 */
    count--;
    return v;
}

顺序数组出队会把前端空间浪费掉,循环取模让数组首尾相接复用空间——硬件环形缓冲区(串口收发)同款思路。链表实现则一头进一头出,无需预分配。

17.3 二叉搜索树:有序数据的查找利器

typedef struct TreeNode {
    int             value;
    struct TreeNode *left;    /* 全部 < 本节点 */
    struct TreeNode *right;   /* 全部 > 本节点 */
} TreeNode;

/* 递归插入:返回(可能的)新子树根 */
TreeNode *bst_insert(TreeNode *root, int v)
{
    if (root == NULL) {
        root = malloc(sizeof *root);
        if (root) { root->value = v; root->left = root->right = NULL; }
        return root;
    }
    if (v < root->value)      root->left  = bst_insert(root->left,  v);
    else if (v > root->value) root->right = bst_insert(root->right, v);
    /* v == 已存在:不插 */
    return root;
}

/* 中序遍历 = 升序输出;前中后序只差「访问自己」的位置 */
void bst_inorder(const TreeNode *root)
{
    if (root == NULL) return;
    bst_inorder(root->left);
    printf("%d ", root->value);
    bst_inorder(root->right);
}
操作平均复杂度最坏(退化成链)
查找 / 插入O(log n)O(n)(按序插入树失衡)
删除节点找右子树最小值补位O(n)

删除的三个情形:叶子直接摘;单子树让子树上移顶替;双子树用「右子树最小值(或左子树最大值)」换值后再删那个替身。按序插入会退化成链表,工程上用平衡树(AVL/红黑)救场——思想相同,多了旋转。

本章通关标准: 说得出堆栈/队列各自「进出」规则与典型应用(函数调用栈、撤销栈 / 打印队列、缓冲区);能手写 BST 的递归插入和中序遍历;理解接口与实现分离给「换底层」留的活口。
🧠 小测验
1. 循环队列为什么用 (front + count) % CAPACITY?
数组队列反复出入队会让前端空间废弃。取模让下标绕回首尾相接;用 count 计数避免「front==rear 时分不清空还是满」的二义性。串口/网络收发缓冲区同款结构。
2. 二叉搜索树的中序遍历为什么正好升序?
BST 性质:左子树全部小于节点、右子树全部大于节点。中序遍历顺序是「左-根-右」,递归展开后每个节点都在其左子树之后、右子树之前被访问,恰好就是从小到大的序列。
3. BST 删除双子树节点怎么办?
用右子树的最小值(最左节点)替换被删节点的值,然后去右子树里删掉那个替身节点(替身必无左孩子,落入简单情形)。用左子树最大值同理。这样保持 BST 有序性质不变。
← 上一篇🏠 顶层目录下一篇 →